mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 01:00:14 +02:00
Add a Steam Frame flavour of the Android app with Frame controller bindings
The Steam Frame runs Android apps through Lepton with SteamVR's OpenXR runtime. A third headset flavour, steamFrame, targets it: - -mcpu=cortex-x4+nosve for the Snapdragon 8 Gen 3 (its firmware does not expose SVE, which clang otherwise auto-vectorises with), from one flavour-to-CPU map that the kit export now reads instead of guessing from the variant name. - MKW_ANDROID_HEADSET=steam_frame defines MKW_HEADSET_STEAM_FRAME for the runtime's own targets, for Frame-specific defaults. - A manifest without the Horizon OS entries, and FrameEntryActivity as the single real MAIN/LAUNCHER activity with the Khronos and Oculus VR categories, which Lepton needs to start an app in VR. It opens the setup panel and, when the selected game can start, the game on top. - XR_VALVE_frame_controller_interaction: the Frame controller profile with its left D-pad (new dpad_* actions, the Wii Remote's D-pad or the gamepad's), View as menu and the left shoulder as the panel button. Also requested on Windows for SteamVR streaming to a Frame. - Build-Quest.ps1, Build-QuestGame.ps1 and Run-Quest.ps1 take -Headset frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HBRGKTE1GnN2ah8gcZKr3
This commit is contained in:
13 files changed
+257
-22
No files matched your search
+14
-5
@@ -1,7 +1,10 @@
|
||||
# Builds the standalone Meta Quest APK on a Windows host.
|
||||
#
|
||||
# powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 [-Generated <dir>]
|
||||
# [-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.'
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# SDK's NDK and ninja:
|
||||
#
|
||||
# powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 [-Generated <dir>] [-Kit <dir>]
|
||||
# [-Headset modern|quest1] [-Configuration debug|release]
|
||||
# [-Headset modern|quest1|frame] [-Configuration debug|release]
|
||||
# [-Data <extracted disc dir>] [-Output <file.wcgame>] [-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) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# diagnostics for one session.
|
||||
#
|
||||
# powershell -ExecutionPolicy Bypass -File android/Run-Quest.ps1 [-Apk <path>] [-Data <extracted disc dir>]
|
||||
# [-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
|
||||
|
||||
@@ -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 "")
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- Horizon OS entries the Steam Frame has no use for: SteamVR's runtime needs no permission for
|
||||
XR_EXT_hand_tracking, offers no XR_FB_passthrough, and reads none of the com.oculus metadata. -->
|
||||
<uses-permission android:name="horizonos.permission.HAND_TRACKING" tools:node="remove" />
|
||||
<uses-permission android:name="com.oculus.permission.HAND_TRACKING" tools:node="remove" />
|
||||
<uses-feature android:name="com.oculus.feature.PASSTHROUGH" tools:node="remove" />
|
||||
<uses-feature android:name="oculus.software.handtracking" tools:node="remove" />
|
||||
|
||||
<application>
|
||||
<meta-data android:name="com.oculus.supportedDevices" tools:node="remove" />
|
||||
<meta-data android:name="com.oculus.vr.focusaware" tools:node="remove" />
|
||||
<meta-data android:name="com.oculus.trade_cpu_for_gpu_amount" tools:node="remove" />
|
||||
|
||||
<!-- Lepton, SteamOS's Android layer, starts the one real activity that is MAIN and
|
||||
LAUNCHER (it ignores activity-aliases) and runs the app in VR when that activity
|
||||
carries a VR category. This one only routes: to the game when it is ready, with the
|
||||
setup panel underneath (FrameEntryActivity.kt). -->
|
||||
<activity
|
||||
android:name=".FrameEntryActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:noHistory="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="org.khronos.openxr.intent.category.IMMERSIVE_HMD" />
|
||||
<category android:name="com.oculus.intent.category.VR" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".QuestActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="org.khronos.openxr.intent.category.IMMERSIVE_HMD" />
|
||||
<category android:name="com.oculus.intent.category.VR" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)$")
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Spec, 10> specs{{
|
||||
const std::array<Spec, 14> 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<Binding> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<float, 2> stick = NunchukStick(left);
|
||||
|
||||
Reference in new issue
Block a user