mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-04 22:00:08 +02:00
Added Android Meta Quest Support
This commit is contained in:
1 parent
49b8602543
commit
e52227baf5
67 files changed
+5662
-176
No files matched your search
@@ -75,3 +75,12 @@ output.txt
|
||||
|
||||
# Operating System
|
||||
.DS_Store
|
||||
|
||||
# Android (Meta Quest) app: Gradle outputs and the downloaded SDL3 AAR
|
||||
/android/.gradle/
|
||||
/android/.dependencies/
|
||||
/android/local.properties
|
||||
/android/app/build/
|
||||
/android/app/.cxx/
|
||||
/android/app/libs/*.aar
|
||||
/android/.runs/
|
||||
@@ -255,12 +255,13 @@ ended up; its original viewport is folded into the projection instead.
|
||||
| Backend | Status |
|
||||
| --- | --- |
|
||||
| Windows D3D12 | Implemented: same-adapter, same-device asynchronous OpenXR submission. |
|
||||
| Linux Vulkan | Capability-gated scaffold. The pinned Dawn package does not expose the complete native Vulkan instance/device/queue context needed for safe same-device OpenXR interop, so the runtime logs the limitation and falls back to desktop rendering. |
|
||||
| Android Vulkan (Meta Quest) | Implemented and running on a Quest 3: the OpenXR side owns its own Vulkan device (`XR_KHR_vulkan_enable2`, `XR_KHR_vulkan_enable` fallback) and shares eyes with Dawn through `AHardwareBuffer`s ordered by sync-fd fences. Controllers arrive through OpenXR actions as a virtual SDL gamepad. See `docs/quest-port.md`. |
|
||||
| Linux Vulkan | Not wired. The pinned Dawn package does not expose a native Vulkan device, and the AHardwareBuffer bridge is Android-only; a dma-buf/opaque-fd variant of the same design would cover desktop Linux. |
|
||||
| Other platforms | Not wired yet. |
|
||||
|
||||
The Vulkan path intentionally does not create an unrelated Vulkan device or use a CPU readback as
|
||||
a workaround. It accepts a future explicit Dawn native context, including external queue locking,
|
||||
so it can be enabled once Aurora exposes those handles safely.
|
||||
Both bindings share `openxr_integration.cpp`: the pacing thread, policy evaluation, the
|
||||
retained-layer protocol and the head-pose maths are compiled once against the neutral types in
|
||||
`vr/openxr_backend.h`, and only the backend class differs per platform.
|
||||
|
||||
### Interpolation validation
|
||||
|
||||
@@ -308,11 +309,12 @@ ends, including mid-frame flushes, so live setting changes cannot invalidate pen
|
||||
## Current limitations
|
||||
|
||||
- Only the project's supported PAL `RMCP01` translation has race instrumentation addresses.
|
||||
- Motion-controller/Wii Remote emulation and OpenXR action bindings are not implemented yet; use
|
||||
the existing game-controller input path.
|
||||
- Dedicated Quest, Android, and Apple visionOS packaging is not implemented. The static recompilation
|
||||
architecture avoids a runtime JIT, but each platform still needs an Aurora graphics bridge,
|
||||
windowing/lifecycle work, and packaging.
|
||||
- Wii Remote pointer/motion emulation from tracked controllers is not implemented. OpenXR action
|
||||
bindings exist only on the Android build, where they present the Touch controllers as one
|
||||
ordinary gamepad; on Windows use the existing game-controller input path.
|
||||
- The Quest build (`android/`, `docs/quest-port.md`) runs on a Quest 3 through menus and races.
|
||||
Lifecycle events and performance (about 43 game FPS) are still open. Apple visionOS packaging
|
||||
is not implemented.
|
||||
- Scene-specific comfort options, culling fixes, replay/spectator classification, and a broader VR
|
||||
settings UI beyond the current enable/replay controls are future work.
|
||||
- The desktop window remains available as a mirror/fallback.
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# Builds the standalone Meta Quest APK on a Windows host.
|
||||
#
|
||||
# powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 [-Generated <dir>] [-Flavor base|retroRewind]
|
||||
# [-Configuration debug|release] [-Install]
|
||||
#
|
||||
# -Generated names the translator output for the disc you own: the directory
|
||||
# holding data_sections_init.cpp, RuntimeConfig.h and build_shards/shards.cmake.
|
||||
# It defaults to the installer's build workspace next to this checkout. The
|
||||
# Windows-generated tree is used as-is; runtime/cmake/PublicProducts.cmake
|
||||
# rewrites its blob assembly for ELF at configure time.
|
||||
#
|
||||
# Prerequisites (see docs/quest-port.md): JDK 17, the Android SDK with NDK
|
||||
# 29.0.14206865 and CMake 3.22.1, and android/Prepare-QuestDependencies.ps1
|
||||
# having staged the SDL3 AAR.
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Generated = '',
|
||||
[string]$Dependencies = '',
|
||||
[string]$CMakeDir = '',
|
||||
[ValidateSet('base', 'retroRewind')] [string]$Flavor = 'base',
|
||||
[ValidateSet('debug', 'release')] [string]$Configuration = 'debug',
|
||||
[switch]$Install
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$repo = (Resolve-Path (Join-Path $root '..')).Path
|
||||
|
||||
if (-not $Generated) {
|
||||
$Generated = Join-Path $repo '.scratch\vr-build-workspace\BuildWorkspace\generated'
|
||||
}
|
||||
if (-not (Test-Path (Join-Path $Generated 'build_shards\shards.cmake'))) {
|
||||
throw "No translated graph at $Generated (expected build_shards\shards.cmake). Run the translator first; see docs/quest-port.md."
|
||||
}
|
||||
if (-not (Test-Path (Join-Path $root 'app\libs\SDL3-3.4.4.aar'))) {
|
||||
throw 'SDL3 AAR missing; run android/Prepare-QuestDependencies.ps1 first.'
|
||||
}
|
||||
|
||||
$javaHome = if ($env:JAVA_HOME) { $env:JAVA_HOME } else { 'C:\Program Files\Java\jdk-17' }
|
||||
$sdkRoot = if ($env:ANDROID_HOME) { $env:ANDROID_HOME } elseif ($env:ANDROID_SDK_ROOT) { $env:ANDROID_SDK_ROOT } else { Join-Path $env:LOCALAPPDATA 'Android\Sdk' }
|
||||
if (-not (Test-Path (Join-Path $javaHome 'bin\java.exe'))) { throw "JDK 17 not found at $javaHome (set JAVA_HOME)" }
|
||||
if (-not (Test-Path $sdkRoot)) { throw "Android SDK not found at $sdkRoot (set ANDROID_HOME)" }
|
||||
|
||||
$env:JAVA_HOME = $javaHome
|
||||
$env:ANDROID_HOME = $sdkRoot
|
||||
$env:ANDROID_SDK_ROOT = $sdkRoot
|
||||
|
||||
# aurora-main requires CMake 3.25+, newer than the SDK's bundled 3.22.1. Point
|
||||
# the Android Gradle plugin at a system CMake through local.properties and put
|
||||
# the SDK's Ninja on PATH, which a non-bundled CMake needs.
|
||||
if (-not $CMakeDir) {
|
||||
$cmakeExe = Get-Command cmake.exe -ErrorAction SilentlyContinue
|
||||
if ($cmakeExe) { $CMakeDir = Split-Path -Parent (Split-Path -Parent $cmakeExe.Source) }
|
||||
}
|
||||
if (-not $CMakeDir -or -not (Test-Path (Join-Path $CMakeDir 'bin\cmake.exe'))) {
|
||||
throw 'No CMake 3.25+ found; install one or pass -CMakeDir <install root>'
|
||||
}
|
||||
$sdkCMake = Get-ChildItem -Path (Join-Path $sdkRoot 'cmake') -Directory | Sort-Object Name -Descending | Select-Object -First 1
|
||||
if ($sdkCMake) { $env:PATH = (Join-Path $sdkCMake.FullName 'bin') + ';' + $env:PATH }
|
||||
$localProperties = @(
|
||||
'sdk.dir=' + $sdkRoot.Replace('\', '\\'),
|
||||
'cmake.dir=' + $CMakeDir.Replace('\', '\\')
|
||||
)
|
||||
Set-Content -Path (Join-Path $root 'local.properties') -Value $localProperties -Encoding ascii
|
||||
|
||||
if (-not $Dependencies) {
|
||||
$candidate = Join-Path $repo '.scratch\vr-build-workspace\BuildWorkspace\Dependencies'
|
||||
if (Test-Path $candidate) { $Dependencies = $candidate }
|
||||
}
|
||||
|
||||
$variant = (Get-Culture).TextInfo.ToTitleCase($Configuration)
|
||||
$task = "app:assemble$((Get-Culture).TextInfo.ToTitleCase($Flavor))$variant"
|
||||
$gradleArgs = @(
|
||||
'--project-dir', $root,
|
||||
"-PmkwGeneratedDir=$Generated"
|
||||
)
|
||||
if ($Dependencies) { $gradleArgs += "-PmkwDependenciesDir=$Dependencies" }
|
||||
$gradleArgs += $task
|
||||
Write-Host "gradlew $($gradleArgs -join ' ')"
|
||||
& (Join-Path $root 'gradlew.bat') @gradleArgs
|
||||
if ($LASTEXITCODE -ne 0) { throw "Gradle failed ($LASTEXITCODE)" }
|
||||
|
||||
$apkDir = Join-Path $root "app\build\outputs\apk\$Flavor\$Configuration"
|
||||
$apk = Get-ChildItem -Path $apkDir -Filter '*.apk' | Select-Object -First 1
|
||||
if (-not $apk) { throw "No APK under $apkDir" }
|
||||
Write-Host "APK: $($apk.FullName)"
|
||||
|
||||
if ($Install) {
|
||||
$adb = Join-Path $sdkRoot 'platform-tools\adb.exe'
|
||||
& $adb install -r $apk.FullName
|
||||
if ($LASTEXITCODE -ne 0) { throw "adb install failed ($LASTEXITCODE)" }
|
||||
Write-Host 'Installed. Push the extracted disc with:'
|
||||
Write-Host ' adb push DATA /sdcard/Android/data/org.wiicompiled.quest/files/WiiCompiledOpenXRVR/DATA'
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# Fetches the one dependency the Gradle project cannot resolve from a Maven
|
||||
# repository: the SDL3 Android AAR (SDLActivity plus the matching libSDL3.so).
|
||||
# Dawn, the OpenXR loader and every other native dependency are fetched and
|
||||
# pinned by the runtime's CMake during the Gradle build.
|
||||
#
|
||||
# Usage: powershell -ExecutionPolicy Bypass -File android/Prepare-QuestDependencies.ps1
|
||||
#
|
||||
# The AAR version must equal AURORA_SDL3_VERSION in aurora-main/CMakeLists.txt:
|
||||
# the Java classes in the AAR and libSDL3.so must come from the same release.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$sdlVersion = '3.4.4'
|
||||
$sdlArchiveSha256 = 'da67b5a43442e449511399c65aa86b724419f92850cf36a2a8c7de72eb992bc0'
|
||||
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$libs = Join-Path $root 'app\libs'
|
||||
$cache = Join-Path $root '.dependencies'
|
||||
New-Item -ItemType Directory -Force $libs, $cache | Out-Null
|
||||
|
||||
$archive = Join-Path $cache "SDL3-devel-$sdlVersion-android.zip"
|
||||
if (-not (Test-Path $archive)) {
|
||||
$url = "https://github.com/libsdl-org/SDL/releases/download/release-$sdlVersion/SDL3-devel-$sdlVersion-android.zip"
|
||||
Write-Host "Downloading $url"
|
||||
Invoke-WebRequest -Uri $url -OutFile $archive
|
||||
}
|
||||
$actual = (Get-FileHash -Algorithm SHA256 $archive).Hash.ToLowerInvariant()
|
||||
if ($actual -ne $sdlArchiveSha256) {
|
||||
Remove-Item $archive
|
||||
throw "SDL3 Android archive digest mismatch: $actual"
|
||||
}
|
||||
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
$zip = [System.IO.Compression.ZipFile]::OpenRead($archive)
|
||||
try {
|
||||
$entry = $zip.Entries | Where-Object { $_.Name -eq "SDL3-$sdlVersion.aar" } | Select-Object -First 1
|
||||
if (-not $entry) { throw "SDL3-$sdlVersion.aar not found in the archive" }
|
||||
$target = Join-Path $libs "SDL3-$sdlVersion.aar"
|
||||
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $target, $true)
|
||||
Write-Host "SDL3 AAR staged at $target"
|
||||
}
|
||||
finally {
|
||||
$zip.Dispose()
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# WiiCompiled VR for Meta Quest (Android)
|
||||
|
||||
Standalone Android/OpenXR build of the Mario Kart Wii recompilation for Quest 2,
|
||||
Quest 3, Quest 3S and Quest Pro. The full design, build walkthrough and current
|
||||
status live in [docs/quest-port.md](../docs/quest-port.md); this directory only
|
||||
holds the Gradle project and its two helper scripts.
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File android/Prepare-QuestDependencies.ps1 # stages the SDL3 3.4.4 AAR once
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Install # base game, debug-signed, installs over adb
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Flavor retroRewind # Retro Rewind product
|
||||
```
|
||||
|
||||
The translated game (the translator's `generated/` tree for **your own**
|
||||
PAL `RMCP01` disc) is passed with `-Generated <dir>`; it defaults to the
|
||||
installer workspace next to this checkout. No game data is ever part of the
|
||||
APK: push the extracted disc afterwards to
|
||||
|
||||
```
|
||||
/sdcard/Android/data/org.wiicompiled.quest/files/WiiCompiledOpenXRVR/DATA
|
||||
```
|
||||
|
||||
`Config.toml`, saves and logs live in the same `WiiCompiledOpenXRVR` directory.
|
||||
@@ -0,0 +1,111 @@
|
||||
# Installs the built APK on the connected Quest, places the game data, launches
|
||||
# the activity and collects diagnostics for one session.
|
||||
#
|
||||
# powershell -ExecutionPolicy Bypass -File android/Run-Quest.ps1 [-Apk <path>] [-Data <extracted disc dir>]
|
||||
# [-Seconds 60] [-NoLaunch] [-SkipInstall]
|
||||
#
|
||||
# -Data names the extracted disc partition (the directory holding sys/, files/,
|
||||
# disc/ ...). It is pushed once to a staging folder, then moved into the app's
|
||||
# data directory; both steps are skipped when DATA is already in place.
|
||||
# Diagnostics land in android/.runs/<stamp>/: logcat (UTF-8) and the runtime's
|
||||
# own per-run log folder pulled from the device.
|
||||
#
|
||||
# Android storage rules this script works within:
|
||||
# * Never create directories under Android/data/<package> through adb before
|
||||
# the app has run: they would belong to the shell user and the app could
|
||||
# not write its Config.toml, NAND or logs there. The app creates its own
|
||||
# WiiCompiledOpenXRVR directory on first launch, so this script launches it
|
||||
# once before placing DATA.
|
||||
# * Files adb places stay owned by the shell user, so DATA is made
|
||||
# world-readable (chmod -R a+rX). The game only ever reads it.
|
||||
# * run-as cannot reach shared storage (SELinux), so it is not used here.
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Apk = '',
|
||||
[string]$Data = '',
|
||||
[int]$Seconds = 60,
|
||||
[switch]$NoLaunch,
|
||||
[switch]$SkipInstall
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$sdkRoot = if ($env:ANDROID_HOME) { $env:ANDROID_HOME } elseif ($env:ANDROID_SDK_ROOT) { $env:ANDROID_SDK_ROOT } else { Join-Path $env:LOCALAPPDATA 'Android\Sdk' }
|
||||
$adb = Join-Path $sdkRoot 'platform-tools\adb.exe'
|
||||
if (-not (Test-Path $adb)) { throw "adb not found at $adb" }
|
||||
|
||||
$package = 'org.wiicompiled.quest'
|
||||
$activity = "$package/.QuestActivity"
|
||||
$appDir = "/sdcard/Android/data/$package/files/WiiCompiledOpenXRVR"
|
||||
$stageDir = '/sdcard/Download/WiiCompiledQuestStaging'
|
||||
|
||||
function Invoke-Adb { param([string[]]$Arguments) & $adb @Arguments; if ($LASTEXITCODE -ne 0) { throw "adb $($Arguments -join ' ') failed ($LASTEXITCODE)" } }
|
||||
# adb shell output carries a trailing CR, so trim before comparing.
|
||||
function Test-DevicePath { param([string]$Path, [string]$Kind = 'd') ((& $adb shell "test -$Kind '$Path' && echo yes") | Out-String).Trim() -eq 'yes' }
|
||||
|
||||
$devices = (& $adb devices) -match "device$"
|
||||
if (-not $devices) { throw 'No device in "device" state; check the Quest is connected and USB debugging is allowed.' }
|
||||
|
||||
if (-not $SkipInstall) {
|
||||
if (-not $Apk) {
|
||||
$Apk = Get-ChildItem -Path (Join-Path $root 'app\build\outputs\apk') -Recurse -Filter '*.apk' |
|
||||
Sort-Object LastWriteTime -Descending | Select-Object -First 1 | ForEach-Object FullName
|
||||
}
|
||||
if (-not $Apk -or -not (Test-Path $Apk)) { throw 'No APK found; run Build-Quest.ps1 first or pass -Apk' }
|
||||
Write-Host "Installing $Apk"
|
||||
Invoke-Adb @('install', '-r', '-g', $Apk)
|
||||
}
|
||||
|
||||
if (-not (Test-DevicePath "$appDir/DATA")) {
|
||||
if (-not (Test-DevicePath $appDir)) {
|
||||
Write-Host 'First launch so the app creates its own data directory'
|
||||
Invoke-Adb @('shell', 'am', 'start', '-W', '-n', $activity)
|
||||
for ($i = 0; $i -lt 30 -and -not (Test-DevicePath $appDir); ++$i) { Start-Sleep -Seconds 1 }
|
||||
& $adb shell am force-stop $package
|
||||
if (-not (Test-DevicePath $appDir)) { throw "The app did not create $appDir" }
|
||||
}
|
||||
if (-not (Test-DevicePath "$stageDir/DATA") -and $Data) {
|
||||
Write-Host "Pushing $Data to the staging folder (a few minutes)"
|
||||
Invoke-Adb @('shell', 'mkdir', '-p', $stageDir)
|
||||
Invoke-Adb @('push', $Data, "$stageDir/DATA")
|
||||
}
|
||||
if (Test-DevicePath "$stageDir/DATA") {
|
||||
Write-Host 'Moving staged DATA into the app directory'
|
||||
Invoke-Adb @('shell', "mv '$stageDir/DATA' '$appDir/DATA'")
|
||||
} else {
|
||||
Write-Warning 'No DATA on the device and -Data not given; the app will show its missing-data dialog.'
|
||||
}
|
||||
}
|
||||
if (Test-DevicePath "$appDir/DATA") {
|
||||
# Idempotent; also repairs a tree placed by hand.
|
||||
Invoke-Adb @('shell', "chmod -R a+rX '$appDir/DATA'")
|
||||
}
|
||||
|
||||
if ($NoLaunch) { return }
|
||||
|
||||
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$runDir = Join-Path $root ".runs\$stamp"
|
||||
New-Item -ItemType Directory -Force $runDir | Out-Null
|
||||
|
||||
& $adb logcat -c
|
||||
Write-Host "Launching $activity"
|
||||
Invoke-Adb @('shell', 'am', 'start', '-n', $activity)
|
||||
Start-Sleep -Seconds $Seconds
|
||||
$logcat = Join-Path $runDir 'logcat.txt'
|
||||
# Windows PowerShell's '>' writes UTF-16; keep the capture greppable.
|
||||
& $adb logcat -d -v time | Out-File -FilePath $logcat -Encoding utf8
|
||||
Write-Host "logcat: $logcat"
|
||||
|
||||
$running = ((& $adb shell pidof $package) | Out-String).Trim()
|
||||
if ($running) { Write-Host "Process still running (pid $running)" } else { Write-Warning 'Process is not running' }
|
||||
|
||||
$latest = ((& $adb shell "ls -t '$appDir/Logs' 2>/dev/null | head -1") | Out-String).Trim()
|
||||
if ($latest) {
|
||||
$pulled = Join-Path $runDir 'runtime-log'
|
||||
& $adb pull "$appDir/Logs/$latest" $pulled | Out-Null
|
||||
Write-Host "runtime log: $pulled"
|
||||
}
|
||||
|
||||
Write-Host '--- app log (WiiCompiled / WiiCompiledQuest / SDL / crashes) ---'
|
||||
Select-String -Path $logcat -Pattern '[VDIWEF]/(WiiCompiled|WiiCompiledQuest|SDL|AndroidRuntime|DEBUG|libc) *\(' |
|
||||
Select-Object -Last 120 | ForEach-Object { $_.Line }
|
||||
@@ -0,0 +1,124 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
// Translator output for the disc the user owns (data_sections_init.cpp,
|
||||
// RuntimeConfig.h, build_shards/shards.cmake). Never checked in.
|
||||
val mkwGeneratedDir = providers.gradleProperty("mkwGeneratedDir").orNull
|
||||
// Optional: a directory of already-fetched dependency sources (the installer's
|
||||
// BuildWorkspace/Dependencies) so the native configure does not download them.
|
||||
val mkwDependenciesDir = providers.gradleProperty("mkwDependenciesDir").orNull
|
||||
// Optional: a directory holding a second SDL3 AAR/prefab is not needed; the
|
||||
// AAR in app/libs is produced by Prepare-QuestDependencies.ps1.
|
||||
val mkwRepoRoot = rootProject.file("..").canonicalFile
|
||||
|
||||
// The runtime's read-only assets travel inside the APK and are unpacked by
|
||||
// QuestActivity into the app's private storage on first launch.
|
||||
val runtimeResources = layout.buildDirectory.dir("generated/assets/runtimeResources")
|
||||
val prepareRuntimeResources by tasks.registering(Copy::class) {
|
||||
val assets = File(mkwRepoRoot, "runtime/assets")
|
||||
from(File(assets, "wii")) { into("runtime_resources/wii_bootstrap") }
|
||||
from(File(assets, "dsp/dsp_coef.bin")) { into("runtime_resources") }
|
||||
from(File(assets, "pipeline/initial_pipeline_cache.db")) { into("runtime_resources") }
|
||||
into(runtimeResources)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.wiicompiled.quest"
|
||||
compileSdk = 36
|
||||
ndkVersion = "29.0.14206865"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "org.wiicompiled.quest"
|
||||
// Quest 2 ships Android 10 (API 29); AHardwareBuffer/Vulkan 1.1 need 26+.
|
||||
minSdk = 29
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "0.1.0-quest"
|
||||
|
||||
ndk {
|
||||
abiFilters += "arm64-v8a"
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments += listOf(
|
||||
"-DANDROID_STL=c++_shared",
|
||||
"-DANDROID_PLATFORM=android-29",
|
||||
"-DMKW_REPO_ROOT=${mkwRepoRoot.path.replace('\\', '/')}",
|
||||
)
|
||||
if (mkwGeneratedDir != null) {
|
||||
arguments += "-DMKW_GENERATED_DIR=${File(mkwGeneratedDir).canonicalPath.replace('\\', '/')}"
|
||||
}
|
||||
if (mkwDependenciesDir != null) {
|
||||
arguments += "-DMKW_DEPENDENCIES_DIR=${File(mkwDependenciesDir).canonicalPath.replace('\\', '/')}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions += "profile"
|
||||
productFlavors {
|
||||
create("base") {
|
||||
dimension = "profile"
|
||||
buildConfigField("String", "MAIN_LIBRARY", "\"main\"")
|
||||
buildConfigField("String", "PROFILE", "\"base\"")
|
||||
externalNativeBuild { cmake { targets += "WiiCompiled" } }
|
||||
}
|
||||
create("retroRewind") {
|
||||
dimension = "profile"
|
||||
applicationIdSuffix = ".retrorewind"
|
||||
versionNameSuffix = "-retro-rewind"
|
||||
buildConfigField("String", "MAIN_LIBRARY", "\"main_retro_rewind\"")
|
||||
buildConfigField("String", "PROFILE", "\"retro_rewind\"")
|
||||
externalNativeBuild { cmake { targets += "RetroRewind" } }
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
// aurora-main needs CMake 3.25+, newer than the SDK's bundled 3.22.1;
|
||||
// Build-Quest.ps1 points cmake.dir in local.properties at a system CMake.
|
||||
path = file("src/main/cpp/CMakeLists.txt")
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
prefab = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
sourceSets.named("main") {
|
||||
assets.srcDir(runtimeResources)
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
packaging {
|
||||
jniLibs {
|
||||
useLegacyPackaging = false
|
||||
}
|
||||
}
|
||||
lint {
|
||||
disable += setOf("ChromeOsAbiSupport", "DiscouragedApi")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("preBuild") {
|
||||
dependsOn(prepareRuntimeResources)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// SDLActivity and libSDL3.so; the AAR is downloaded by Prepare-QuestDependencies.ps1.
|
||||
implementation(files("libs/SDL3-3.4.4.aar"))
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<!-- The Khronos loader brokers the system OpenXR runtime through this permission. -->
|
||||
<uses-permission android:name="org.khronos.openxr.permission.OPENXR_SYSTEM" />
|
||||
|
||||
<queries>
|
||||
<provider android:authorities="org.khronos.openxr.runtime_broker;org.khronos.openxr.system_runtime_broker" />
|
||||
<intent>
|
||||
<action android:name="org.khronos.openxr.OpenXRRuntimeService" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="org.khronos.openxr.OpenXRApiLayerService" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<uses-feature android:name="android.hardware.vulkan.level" android:required="true" android:version="1" />
|
||||
<uses-feature android:name="android.hardware.vulkan.version" android:required="true" android:version="0x00401000" />
|
||||
<uses-feature android:name="android.hardware.vr.headtracking" android:required="true" android:version="1" />
|
||||
<uses-feature android:name="android.software.xr.api.openxr" android:required="true" />
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:appCategory="game"
|
||||
android:hasFragileUserData="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.WiiCompiled.Quest">
|
||||
|
||||
<meta-data android:name="com.oculus.supportedDevices" android:value="quest2|quest3|quest3s|questpro" />
|
||||
<meta-data android:name="com.oculus.vr.focusaware" android:value="true" />
|
||||
<!-- Let the runtime move CPU budget to the GPU; Aurora's replay is GPU-bound. -->
|
||||
<meta-data android:name="com.oculus.trade_cpu_for_gpu_amount" android:value="-1" />
|
||||
|
||||
<activity
|
||||
android:name=".QuestActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode|density"
|
||||
android:excludeFromRecents="false"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:preferMinimalPostProcessing="true"
|
||||
android:resizeableActivity="false"
|
||||
android:screenOrientation="landscape">
|
||||
<property
|
||||
android:name="android.window.PROPERTY_XR_ACTIVITY_START_MODE"
|
||||
android:value="XR_ACTIVITY_START_MODE_FULL_SPACE_UNMANAGED" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="com.oculus.intent.category.VR" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,58 @@
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
project(wiicompiled_quest LANGUAGES C CXX ASM)
|
||||
|
||||
# Gradle drives this file. It hands the repository's runtime/ tree to CMake
|
||||
# with the Android-specific choices the desktop build never needs:
|
||||
# * SDL3 comes from the AAR's prefab module (SDLActivity must load the same
|
||||
# libSDL3.so the Java classes were built against);
|
||||
# * the products are shared libraries (see runtime/cmake/PublicProducts.cmake);
|
||||
# * the translated graph is the one the Windows installer generated, whose
|
||||
# blob assembly PublicProducts.cmake rewrites for ELF.
|
||||
|
||||
if(NOT ANDROID OR NOT CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a")
|
||||
message(FATAL_ERROR "The Quest build supports Android arm64-v8a only")
|
||||
endif()
|
||||
if(NOT DEFINED MKW_REPO_ROOT OR NOT EXISTS "${MKW_REPO_ROOT}/runtime/CMakeLists.txt")
|
||||
message(FATAL_ERROR "MKW_REPO_ROOT must name the WiiCompiled checkout")
|
||||
endif()
|
||||
|
||||
# Translated PowerPC is Release-only by policy (see runtime/CMakeLists.txt);
|
||||
# a debug-signed APK still carries a release-compiled game.
|
||||
set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
|
||||
|
||||
# Reuse already-fetched dependency sources when the caller has them (the
|
||||
# installer's BuildWorkspace/Dependencies); FetchContent honours these
|
||||
# per-dependency overrides and skips the download.
|
||||
if(DEFINED MKW_DEPENDENCIES_DIR AND IS_DIRECTORY "${MKW_DEPENDENCIES_DIR}")
|
||||
file(GLOB _mkw_dependency_dirs LIST_DIRECTORIES true "${MKW_DEPENDENCIES_DIR}/*")
|
||||
foreach(_dir IN LISTS _mkw_dependency_dirs)
|
||||
if(IS_DIRECTORY "${_dir}")
|
||||
get_filename_component(_name "${_dir}" NAME)
|
||||
if(NOT _name MATCHES "^(dawn_prebuilt|native_prebuilt|cppwinrt|libusb)$")
|
||||
string(TOUPPER "${_name}" _upper)
|
||||
set("FETCHCONTENT_SOURCE_DIR_${_upper}" "${_dir}" CACHE PATH "" FORCE)
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
find_package(SDL3 REQUIRED CONFIG)
|
||||
set(AURORA_SDL3_PROVIDER system CACHE STRING "" FORCE)
|
||||
set(AURORA_SDL3_LINKAGE shared CACHE STRING "" FORCE)
|
||||
set(MKW_ENABLE_OPENXR ON CACHE BOOL "" FORCE)
|
||||
set(MKW_BUILD_PRODUCTS ON CACHE BOOL "" FORCE)
|
||||
set(MKW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
if(DEFINED MKW_GENERATED_DIR)
|
||||
set(MKW_TRANSLATED_SHARD_MANIFEST "${MKW_GENERATED_DIR}/build_shards/shards.cmake"
|
||||
CACHE FILEPATH "" FORCE)
|
||||
endif()
|
||||
|
||||
add_subdirectory("${MKW_REPO_ROOT}/runtime" wiicompiled-runtime)
|
||||
|
||||
# App-owned JNI glue lives in each product library SDLActivity loads.
|
||||
foreach(product IN ITEMS WiiCompiled RetroRewind)
|
||||
if(TARGET ${product})
|
||||
target_sources(${product} PRIVATE "${CMAKE_CURRENT_LIST_DIR}/quest_surface_jni.cpp")
|
||||
target_include_directories(${product} PRIVATE "${MKW_REPO_ROOT}/aurora-main/include")
|
||||
endif()
|
||||
endforeach()
|
||||
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// JNI side of org.wiicompiled.quest.QuestSurface. Compiled into the product
|
||||
// library (libmain.so) so the natives resolve once SDLActivity has loaded it,
|
||||
// which happens before the activity creates its surface.
|
||||
|
||||
#include <aurora/android.h>
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_wiicompiled_quest_QuestSurface_nativeBeginSurfaceMutation(JNIEnv*, jobject) {
|
||||
aurora_android_begin_surface_mutation();
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_wiicompiled_quest_QuestSurface_nativeEndSurfaceMutation(JNIEnv*, jobject, jboolean ready) {
|
||||
aurora_android_end_surface_mutation(ready == JNI_TRUE);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package org.wiicompiled.quest
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.system.Os
|
||||
import android.util.Log
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import org.libsdl.app.SDLActivity
|
||||
import org.libsdl.app.SDLSurface
|
||||
|
||||
/**
|
||||
* The one activity of the standalone Quest build.
|
||||
*
|
||||
* SDLActivity owns the surface, the Java-side event pump and the JNI plumbing
|
||||
* the OpenXR loader needs (the runtime fetches the JavaVM and this activity
|
||||
* through SDL_GetAndroidJNIEnv/SDL_GetAndroidActivity). What this subclass
|
||||
* adds is everything the native runtime cannot discover on its own:
|
||||
*
|
||||
* - the data directory the player fills with the extracted disc
|
||||
* (Android/data/<package>/files/WiiCompiled/DATA) and where Config.toml,
|
||||
* saves and logs live;
|
||||
* - the bundled read-only runtime resources, unpacked from the APK once;
|
||||
* - a Config.toml with VR enabled and the disc path filled in.
|
||||
*
|
||||
* The paths are handed over through the environment before SDLActivity's own
|
||||
* onCreate loads the native libraries, because the runtime resolves them from
|
||||
* static initialisers where SDL's JNI helpers are not yet usable.
|
||||
*/
|
||||
class QuestActivity : SDLActivity() {
|
||||
|
||||
override fun getLibraries(): Array<String> = arrayOf("SDL3", BuildConfig.MAIN_LIBRARY)
|
||||
|
||||
override fun createSDLSurface(context: Context): SDLSurface = QuestSurface(context)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val dataRoot = (getExternalFilesDir(null) ?: filesDir)
|
||||
val gameRoot = File(dataRoot, APP_DIRECTORY)
|
||||
gameRoot.mkdirs()
|
||||
val resources = unpackRuntimeResources()
|
||||
|
||||
Os.setenv("MKW_ANDROID_DATA_DIR", dataRoot.absolutePath, true)
|
||||
Os.setenv("MKW_ANDROID_RESOURCES_DIR", resources.absolutePath, true)
|
||||
ensureConfig(gameRoot)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (!File(gameRoot, DISC_DIRECTORY).isDirectory && !mBrokenLibraries) {
|
||||
Log.w(TAG, "No extracted disc found under ${gameRoot.absolutePath}")
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.missing_game_data_title))
|
||||
.setMessage(getString(R.string.missing_game_data_message, File(gameRoot, DISC_DIRECTORY).absolutePath))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> finish() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies runtime_resources/ from the APK assets into private storage the
|
||||
* first time this build runs. A stamp file keyed on the version code keeps
|
||||
* every later launch to one existence check.
|
||||
*/
|
||||
private fun unpackRuntimeResources(): File {
|
||||
val target = File(filesDir, "runtime_resources")
|
||||
val stamp = File(target, ".version")
|
||||
val expected = "${BuildConfig.VERSION_CODE}:${BuildConfig.VERSION_NAME}"
|
||||
if (stamp.isFile && stamp.readText() == expected) {
|
||||
return target
|
||||
}
|
||||
target.deleteRecursively()
|
||||
target.mkdirs()
|
||||
copyAssetTree("runtime_resources", target)
|
||||
stamp.writeText(expected)
|
||||
Log.i(TAG, "Unpacked runtime resources into ${target.absolutePath}")
|
||||
return target
|
||||
}
|
||||
|
||||
private fun copyAssetTree(assetPath: String, destination: File) {
|
||||
val entries = assets.list(assetPath) ?: emptyArray()
|
||||
if (entries.isEmpty()) {
|
||||
// A leaf: copy the file.
|
||||
assets.open(assetPath).use { input ->
|
||||
FileOutputStream(destination).use { output -> input.copyTo(output) }
|
||||
}
|
||||
return
|
||||
}
|
||||
destination.mkdirs()
|
||||
for (entry in entries) {
|
||||
copyAssetTree("$assetPath/$entry", File(destination, entry))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a first Config.toml pointing at the disc directory with VR on.
|
||||
* An existing file is left alone: the settings overlay edits it in place.
|
||||
*/
|
||||
private fun ensureConfig(gameRoot: File) {
|
||||
val config = File(gameRoot, "Config.toml")
|
||||
if (config.isFile) {
|
||||
return
|
||||
}
|
||||
val disc = File(gameRoot, DISC_DIRECTORY).absolutePath
|
||||
config.writeText(
|
||||
"""
|
||||
# WiiCompiled Quest configuration. Edit with adb pull/push or the in-game overlay.
|
||||
[paths]
|
||||
dvd_root = "$disc"
|
||||
|
||||
[video]
|
||||
widescreen = true
|
||||
resolution_multiplier = 1.0
|
||||
|
||||
[vr]
|
||||
enabled = true
|
||||
render_scale = 1.0
|
||||
""".trimIndent() + "\n",
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WiiCompiledQuest"
|
||||
// Must match kApplicationDirectoryName in runtime/include/runtime_config.h.
|
||||
private const val APP_DIRECTORY = "WiiCompiledOpenXRVR"
|
||||
private const val DISC_DIRECTORY = "DATA"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.wiicompiled.quest
|
||||
|
||||
import android.content.Context
|
||||
import android.view.SurfaceHolder
|
||||
import org.libsdl.app.SDLSurface
|
||||
|
||||
/**
|
||||
* Stock SDLSurface plus the two calls Aurora needs around every surface change.
|
||||
*
|
||||
* Aurora only presents while the surface is known to be usable and must never
|
||||
* touch an ANativeWindow that SDL is replacing or destroying. SDL keeps its own
|
||||
* activity mutex internal to libSDL3.so, so this subclass brackets SDL's
|
||||
* handling instead: begin takes Aurora's surface lock and pauses presentation,
|
||||
* end releases it and reports whether SDL left the surface ready.
|
||||
*/
|
||||
class QuestSurface(context: Context) : SDLSurface(context) {
|
||||
|
||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
||||
nativeBeginSurfaceMutation()
|
||||
try {
|
||||
super.surfaceChanged(holder, format, width, height)
|
||||
} finally {
|
||||
nativeEndSurfaceMutation(mIsSurfaceReady)
|
||||
}
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
nativeBeginSurfaceMutation()
|
||||
try {
|
||||
super.surfaceDestroyed(holder)
|
||||
} finally {
|
||||
nativeEndSurfaceMutation(false)
|
||||
}
|
||||
}
|
||||
|
||||
private external fun nativeBeginSurfaceMutation()
|
||||
private external fun nativeEndSurfaceMutation(ready: Boolean)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Placeholder mark (a headset outline); no game artwork is used. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M30,40 h48 a8,8 0 0 1 8,8 v16 a8,8 0 0 1 -8,8 h-14 l-6,-8 h-8 l-6,8 h-14 a8,8 0 0 1 -8,-8 v-16 a8,8 0 0 1 8,-8 z" />
|
||||
<path
|
||||
android:fillColor="#1A237E"
|
||||
android:pathData="M38,50 a6,6 0 1 0 12,0 a6,6 0 1 0 -12,0 z M58,50 a6,6 0 1 0 12,0 a6,6 0 1 0 -12,0 z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#1A237E</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">WiiCompiled VR</string>
|
||||
<string name="missing_game_data_title">Game data not found</string>
|
||||
<string name="missing_game_data_message">Copy the extracted DATA directory of your own PAL RMCP01 Mario Kart Wii disc to:\n\n%1$s\n\nFor example with adb: adb push DATA /sdcard/Android/data/org.wiicompiled.quest/files/WiiCompiledOpenXRVR/DATA\n\nThen start the app again.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Fullscreen, no action bar: the Android surface is only ever a mirror
|
||||
behind the OpenXR session. -->
|
||||
<style name="Theme.WiiCompiled.Quest" parent="android:Theme.Material.NoActionBar.Fullscreen">
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
plugins {
|
||||
id("com.android.application") version "8.13.2" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.21" apply false
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
|
||||
org.gradle.caching=true
|
||||
android.useAndroidX=true
|
||||
android.nonTransitiveRClass=true
|
||||
kotlin.code.style=official
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
Vendored
+248
@@ -0,0 +1,248 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,18 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "WiiCompiledQuest"
|
||||
include(":app")
|
||||
@@ -50,6 +50,8 @@ if (_aurora_dawn_provider STREQUAL "auto")
|
||||
set(_has_package TRUE)
|
||||
elseif (APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|x86_64)$")
|
||||
set(_has_package TRUE)
|
||||
elseif (CMAKE_SYSTEM_NAME STREQUAL "Android" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$")
|
||||
set(_has_package TRUE)
|
||||
endif ()
|
||||
|
||||
if (_has_package)
|
||||
@@ -161,6 +163,12 @@ elseif (_aurora_dawn_provider STREQUAL "package")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=7785373d569b3b0237918ec9c523239f7d0667857c5ea8242e3cdfde95e6aeab")
|
||||
endif ()
|
||||
if (NOT AURORA_DAWN_PACKAGE_URL_HASH
|
||||
AND AURORA_DAWN_VERSION STREQUAL "v20260603.191052"
|
||||
AND _dawn_system STREQUAL "android" AND _dawn_arch STREQUAL "aarch64")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=27d910dee1201fd1e5b6ac567f0ba2306ebf2135e9f40b6929976c365d38b09b")
|
||||
endif ()
|
||||
endif ()
|
||||
message(STATUS "aurora: Fetching prebuilt Dawn package from ${AURORA_DAWN_PACKAGE_URL}")
|
||||
|
||||
@@ -196,6 +204,25 @@ elseif (_aurora_dawn_provider STREQUAL "package")
|
||||
# Static Dawn packages may link Threads::Threads
|
||||
find_package(Threads QUIET)
|
||||
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "Android")
|
||||
# The android-aarch64 package records the absolute path of the liblog.so
|
||||
# its CI sysroot linked against. Rewrite that to the logical library name
|
||||
# so the NDK on this machine resolves it.
|
||||
foreach (_targets_file
|
||||
"${_dawn_pkg_dir}/lib/cmake/Dawn/DawnTargets.cmake"
|
||||
"${_dawn_pkg_dir}/lib64/cmake/Dawn/DawnTargets.cmake")
|
||||
if (EXISTS "${_targets_file}")
|
||||
file(READ "${_targets_file}" _dawn_targets_text)
|
||||
string(REGEX REPLACE "[^;\"]*/sysroot/usr/lib/aarch64-linux-android/[0-9]+/liblog\\.so" "log"
|
||||
_dawn_targets_sanitized "${_dawn_targets_text}")
|
||||
if (NOT _dawn_targets_sanitized STREQUAL _dawn_targets_text)
|
||||
file(WRITE "${_targets_file}" "${_dawn_targets_sanitized}")
|
||||
message(STATUS "aurora: Rewrote the Dawn package's absolute liblog.so reference")
|
||||
endif ()
|
||||
endif ()
|
||||
endforeach ()
|
||||
endif ()
|
||||
|
||||
# Find DawnConfig.cmake in the package
|
||||
set(_dawn_cmake_found FALSE)
|
||||
foreach (_cmake_path
|
||||
@@ -206,7 +233,10 @@ elseif (_aurora_dawn_provider STREQUAL "package")
|
||||
)
|
||||
if (EXISTS "${_cmake_path}/DawnConfig.cmake")
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
|
||||
find_package(Dawn REQUIRED CONFIG PATHS "${_cmake_path}" NO_DEFAULT_PATH)
|
||||
# NO_CMAKE_FIND_ROOT_PATH: cross toolchains (the Android NDK) root package
|
||||
# discovery inside their sysroot; the extracted package is a verified host
|
||||
# path this one lookup must be allowed to see.
|
||||
find_package(Dawn REQUIRED CONFIG PATHS "${_cmake_path}" NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH)
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
|
||||
set(_dawn_cmake_found TRUE)
|
||||
break()
|
||||
|
||||
@@ -40,6 +40,10 @@ if (AURORA_ENABLE_GX)
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL Windows)
|
||||
target_sources(aurora_core PRIVATE lib/webgpu/d3d12_interop.cpp)
|
||||
endif ()
|
||||
# Android/Vulkan counterpart: the AHardwareBuffer stereo bridge. The file
|
||||
# compiles to C ABI stubs on every other platform so the runtime's OpenXR
|
||||
# integration links everywhere.
|
||||
target_sources(aurora_core PRIVATE lib/webgpu/vulkan_interop.cpp)
|
||||
target_link_libraries(aurora_core PRIVATE dawn::webgpu_dawn)
|
||||
if (DAWN_ENABLE_VULKAN)
|
||||
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_VULKAN)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef AURORA_ANDROID_H
|
||||
#define AURORA_ANDROID_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#else
|
||||
#include "stdbool.h"
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Android surface lifecycle bridge.
|
||||
*
|
||||
* Aurora only presents while the Android surface is known to be usable, and
|
||||
* must never touch an ANativeWindow the Java side is tearing down. Stock SDL3
|
||||
* keeps its own activity mutex internal to libSDL3.so, so the host app brackets
|
||||
* every surface change instead: an SDLSurface subclass calls begin before
|
||||
* delegating surfaceChanged/surfaceDestroyed to SDL and end afterwards, with
|
||||
* `ready` saying whether SDL left the surface usable. Between the two calls
|
||||
* Aurora's surface lock is held and presentation is paused.
|
||||
*
|
||||
* Call from the Java UI thread only. The pair must always balance.
|
||||
*/
|
||||
void aurora_android_begin_surface_mutation(void);
|
||||
void aurora_android_end_surface_mutation(bool ready);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,129 @@
|
||||
#ifndef AURORA_VULKAN_INTEROP_H
|
||||
#define AURORA_VULKAN_INTEROP_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <cstdint>
|
||||
extern "C" {
|
||||
#else
|
||||
#include "stdbool.h"
|
||||
#include "stdint.h"
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Android/Vulkan counterpart of aurora/d3d12_interop.h.
|
||||
*
|
||||
* Dawn's Vulkan device cannot be bound to an OpenXR session (the pinned Dawn
|
||||
* package exposes no VkDevice/VkQueue and would not enable the runtime's
|
||||
* required extensions anyway), so the OpenXR side owns a second VkDevice
|
||||
* created through XR_KHR_vulkan_enable2. The two devices meet on
|
||||
* AHardwareBuffer-backed images: Aurora imports each buffer as Dawn shared
|
||||
* texture memory and copies an eye into it inside the frame worker's command
|
||||
* buffer; the OpenXR side imports the same buffer on its own device and copies
|
||||
* it into the acquired XrSwapchain image. Ordering across the two devices uses
|
||||
* Android sync file descriptors (Dawn's SharedFenceSyncFD).
|
||||
*
|
||||
* Every handle in this API is a plain C value so the runtime never includes
|
||||
* Dawn's C++ headers.
|
||||
*/
|
||||
|
||||
enum { AURORA_VULKAN_STEREO_MAX_TARGETS = 2 };
|
||||
|
||||
/**
|
||||
* Borrowed facts about Aurora's Dawn Vulkan device. colorVkFormat is the
|
||||
* VkFormat enum value matching Aurora's single-sample eye output.
|
||||
*/
|
||||
typedef struct {
|
||||
int64_t colorVkFormat;
|
||||
bool sharedTextureMemoryAHardwareBuffer;
|
||||
bool sharedFenceSyncFd;
|
||||
} AuroraVulkanNativeHandles;
|
||||
|
||||
/**
|
||||
* One AHardwareBuffer the next Aurora stereo sink must copy an eye into. The
|
||||
* buffer is imported into Dawn on first use and the import is cached for as
|
||||
* long as the bridge lives, so callers should recycle a small ring of buffers
|
||||
* rather than allocating per frame.
|
||||
*
|
||||
* acquireFenceFd is a sync file descriptor Dawn waits on before writing (the
|
||||
* OpenXR side's previous copy out of this buffer), or -1 when the buffer has no
|
||||
* pending reader. Ownership of the descriptor transfers to Aurora on a
|
||||
* successful aurora_vulkan_set_stereo_targets call; on failure the caller
|
||||
* still owns it.
|
||||
*
|
||||
* acquireImageLayout is the VkImageLayout the buffer's image currently holds
|
||||
* (VK_IMAGE_LAYOUT_UNDEFINED when the contents may be discarded). It must
|
||||
* equal the layout the OpenXR side's release barrier left the image in.
|
||||
*/
|
||||
typedef struct {
|
||||
struct AHardwareBuffer* buffer;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
int64_t vkFormat;
|
||||
int acquireFenceFd;
|
||||
int32_t acquireImageLayout;
|
||||
} AuroraVulkanStereoTarget;
|
||||
|
||||
/**
|
||||
* Per-target result handed to the submitted callback. releaseFenceFd is a
|
||||
* sync file descriptor that signals once Aurora's copy into the buffer has
|
||||
* completed on Dawn's queue (-1 if Dawn reported no fence); ownership passes to
|
||||
* the callee. releasedImageLayout is the VkImageLayout Dawn's release barrier
|
||||
* left the image in; the OpenXR side's acquire barrier must start from it.
|
||||
*/
|
||||
typedef struct {
|
||||
int releaseFenceFd;
|
||||
int32_t releasedImageLayout;
|
||||
} AuroraVulkanStereoRelease;
|
||||
|
||||
/**
|
||||
* Fired when Aurora either finishes or abandons the stereo sink. `success`
|
||||
* guarantees that the copies were submitted and that every release entry is
|
||||
* valid. The callback runs on Aurora's frame worker while its queue-submit
|
||||
* mutex is held: it may record and submit work on the OpenXR side's own Vulkan
|
||||
* queue, but must not wait for the GPU or re-enter Aurora.
|
||||
*/
|
||||
typedef void (*AuroraVulkanStereoSubmittedCallback)(uint64_t frameToken, bool success,
|
||||
const AuroraVulkanStereoRelease* releases,
|
||||
uint32_t releaseCount, void* userdata);
|
||||
|
||||
/** Returns false unless the active Aurora backend is Dawn Vulkan. */
|
||||
bool aurora_vulkan_get_native_handles(AuroraVulkanNativeHandles* handles);
|
||||
|
||||
/**
|
||||
* Installs the internal AHardwareBuffer stereo sink. Call while Aurora's frame
|
||||
* worker is idle, after aurora_initialize(). Fails when the Dawn device was not
|
||||
* created with the AHardwareBuffer shared-memory and sync-fd fence features.
|
||||
*/
|
||||
bool aurora_vulkan_enable_stereo_bridge(AuroraVulkanStereoSubmittedCallback submitted,
|
||||
void* userdata);
|
||||
|
||||
/**
|
||||
* Publishes the buffer(s) for frameToken. Immersive projection frames supply
|
||||
* two targets; virtual-screen quad frames supply one. Exactly one frame may be
|
||||
* pending at a time.
|
||||
*/
|
||||
bool aurora_vulkan_set_stereo_targets(uint64_t frameToken,
|
||||
const AuroraVulkanStereoTarget* targets,
|
||||
uint32_t targetCount);
|
||||
|
||||
/**
|
||||
* Withdraws frameToken only while its targets have not been encoded. Semantics
|
||||
* match aurora_d3d12_cancel_stereo_targets: false means the worker already
|
||||
* owns encoded work and the submitted callback remains the completion
|
||||
* authority. A successful cancellation closes the acquire descriptors it was
|
||||
* given and fires no callback.
|
||||
*/
|
||||
bool aurora_vulkan_cancel_stereo_targets(uint64_t frameToken);
|
||||
|
||||
/**
|
||||
* Removes the sink and releases the cached Dawn imports. The worker must be
|
||||
* idle. Returns false only when Dawn could not be drained, in which case the
|
||||
* bridge is retained for the process lifetime.
|
||||
*/
|
||||
bool aurora_vulkan_disable_stereo_bridge();
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
// Android-only diagnostic switches read from system properties, so an on-device
|
||||
// experiment can change renderer behaviour with `adb shell setprop` and an app
|
||||
// restart instead of a rebuild. debug.* properties are writable by the adb shell
|
||||
// and readable by apps. Values are read once per process by each caller.
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
#include <sys/system_properties.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
namespace aurora::android_debug {
|
||||
|
||||
inline int property_int(const char* name, int fallback) noexcept {
|
||||
char value[PROP_VALUE_MAX]{};
|
||||
if (__system_property_get(name, value) <= 0) {
|
||||
return fallback;
|
||||
}
|
||||
return std::atoi(value);
|
||||
}
|
||||
|
||||
} // namespace aurora::android_debug
|
||||
#endif
|
||||
@@ -1830,6 +1830,41 @@ static void handle_draw_overrun(u8 cmd, u16 vtxCount, u32 vtxSize, u32 totalVtxB
|
||||
Log.warn("stopping FIFO decode at truncated draw: need {} bytes at pos {}, have {}", totalVtxBytes, pos, size);
|
||||
}
|
||||
|
||||
// Uploads a draw's GX vertices with the stride populate_pipeline_config gave the shader. When that stride is padded
|
||||
// (Android, see padded_upload_stride), each vertex is copied with zeroed trailing bytes; attribute offsets inside the
|
||||
// vertex are unchanged. Every padded upload is a multiple of 4 bytes, so consecutive draws stay contiguous for merging.
|
||||
static gfx::Range push_draw_vertices(const u8* vertices, u32 vtxCount, u32 vtxSize) {
|
||||
const u32 uploadStride = padded_upload_stride(vtxSize);
|
||||
if (uploadStride == vtxSize)
|
||||
LIKELY { return gfx::push_verts(vertices, static_cast<size_t>(vtxCount) * vtxSize); }
|
||||
auto [buffer, range] = gfx::map_verts(static_cast<size_t>(vtxCount) * uploadStride);
|
||||
u8* dst = buffer.data();
|
||||
const u32 padding = uploadStride - vtxSize;
|
||||
for (u32 i = 0; i < vtxCount; ++i) {
|
||||
std::memcpy(dst, vertices + static_cast<size_t>(i) * vtxSize, vtxSize);
|
||||
std::memset(dst + vtxSize, 0, padding);
|
||||
dst += uploadStride;
|
||||
}
|
||||
return range;
|
||||
}
|
||||
|
||||
// Uploads an indexed vertex array with elements `uploadStride` bytes apart (see padded_upload_stride). A padded upload
|
||||
// copies each element and zeroes its trailing bytes; a trailing partial element is copied as far as the array goes.
|
||||
static gfx::Range push_vertex_array(const AttrArray& array, u32 uploadStride) {
|
||||
const auto* data = static_cast<const u8*>(array.data);
|
||||
if (uploadStride == array.stride || array.stride == 0)
|
||||
LIKELY { return gfx::push_storage(data, array.size); }
|
||||
const size_t count = (static_cast<size_t>(array.size) + array.stride - 1) / array.stride;
|
||||
auto [buffer, range] = gfx::map_storage(count * uploadStride);
|
||||
u8* dst = buffer.data();
|
||||
std::memset(dst, 0, count * uploadStride);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
const size_t start = i * array.stride;
|
||||
std::memcpy(dst + i * uploadStride, data + start, std::min<size_t>(array.stride, array.size - start));
|
||||
}
|
||||
return range;
|
||||
}
|
||||
|
||||
// Draw command handler - parses vertices inline and caches results
|
||||
static u32 calculate_last_vtx_size(GXVtxFmt fmt) {
|
||||
u32 vtxSize = 0;
|
||||
@@ -2248,7 +2283,7 @@ bool submit_raw_draw(GXPrimitive prim, GXVtxFmt fmt, const uint8_t* vertices, ui
|
||||
|
||||
// This entry point bypasses process(), so it owns the renderer lock itself.
|
||||
std::lock_guard gpuLock(aurora::renderer_gpu_mutex());
|
||||
const gfx::Range vertRange = gfx::push_verts(vertices, vertexBytes);
|
||||
const gfx::Range vertRange = push_draw_vertices(vertices, vtxCount, vtxSize);
|
||||
const bool interpolationIdentityActive = frame_interpolation_identity_needed();
|
||||
const PnMtxUsage matrixUsage = interpolationIdentityActive ? pn_mtx_usage(vertices, vtxCount, vtxSize) : PnMtxUsage{};
|
||||
handle_draw_unmerged(prim, fmt, vtxCount, vertRange, matrixUsage.mask, matrixUsage.topologySignature,
|
||||
@@ -2283,7 +2318,7 @@ static bool handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndi
|
||||
|
||||
// Push raw vertex data to buffer
|
||||
const uint8_t* vertices = data + pos;
|
||||
gfx::Range vertRange = gfx::push_verts(vertices, totalVtxBytes);
|
||||
gfx::Range vertRange = push_draw_vertices(vertices, vtxCount, vtxSize);
|
||||
pos += totalVtxBytes;
|
||||
|
||||
// Try to merge with previous draw call
|
||||
@@ -2350,12 +2385,14 @@ static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, g
|
||||
continue;
|
||||
}
|
||||
auto& array = g_gxState.arrays[i];
|
||||
if (array.cachedRange.size > 0) {
|
||||
const u32 uploadStride = padded_upload_stride(array.stride);
|
||||
if (array.cachedRange.size > 0 && array.cachedStride == uploadStride) {
|
||||
ranges.vaRanges[i - GX_VA_POS] = array.cachedRange;
|
||||
} else {
|
||||
const auto range = gfx::push_storage(static_cast<const uint8_t*>(array.data), array.size);
|
||||
const auto range = push_vertex_array(array, uploadStride);
|
||||
ranges.vaRanges[i - GX_VA_POS] = range;
|
||||
array.cachedRange = range;
|
||||
array.cachedStride = uploadStride;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
#include "../gfx/texture_convert.hpp"
|
||||
#include "../gfx/texture_replacement.hpp"
|
||||
#include "gx_fmt.hpp"
|
||||
#if defined(__ANDROID__)
|
||||
#include "../android_debug.hpp"
|
||||
#endif
|
||||
|
||||
#include <absl/container/flat_hash_map.h>
|
||||
#include <absl/container/flat_hash_set.h>
|
||||
@@ -1753,6 +1756,28 @@ static u8 index_attr_size(GXAttr attr, GXCompCnt cnt, GXAttrType type) noexcept
|
||||
return indexSize;
|
||||
}
|
||||
|
||||
// The Quest 3's Adreno 740 Vulkan driver decodes the wrong bytes when the vertex shader's
|
||||
// `ubuf.vtx_start + vidx * stride + offset` uses a stride that is not a multiple of 4. GX packs vertices byte-tight,
|
||||
// so every skinned character (a 1-byte PNMTXIDX first, stride 7) exploded and textured menu panels smeared. Rewriting
|
||||
// the WGSL byte helpers with constant shifts or integer division did not help; padding each uploaded vertex to a
|
||||
// 4-byte stride did, with attribute offsets inside the vertex unchanged (device-tested 2026-09-16). Indexed array
|
||||
// reads (`array_start + index * stride`, e.g. 6-byte S16 normals) get the same padding. KartPad reports the same
|
||||
// character corruption on Adreno 750. `adb shell setprop debug.wiicompiled.vtxpad 0` turns it off.
|
||||
u32 padded_upload_stride(u32 packedStride) noexcept {
|
||||
#if defined(__ANDROID__)
|
||||
static const bool pad = [] {
|
||||
const bool enabled = android_debug::property_int("debug.wiicompiled.vtxpad", 1) != 0;
|
||||
Log.info("Vertex stride padding for Adreno: {}", enabled ? "on" : "off");
|
||||
return enabled;
|
||||
}();
|
||||
const u32 padded = (packedStride + 3u) & ~3u;
|
||||
// ShaderConfig::vtxStride is a u8.
|
||||
return pad && padded <= 255u ? padded : packedStride;
|
||||
#else
|
||||
return packedStride;
|
||||
#endif
|
||||
}
|
||||
|
||||
void populate_pipeline_config(PipelineConfig& config, GXPrimitive primitive, GXVtxFmt fmt) noexcept {
|
||||
ZoneScoped;
|
||||
|
||||
@@ -1790,12 +1815,12 @@ void populate_pipeline_config(PipelineConfig& config, GXPrimitive primitive, GXV
|
||||
break;
|
||||
}
|
||||
case GX_INDEX8:
|
||||
mapping.stride = g_gxState.arrays[i].stride;
|
||||
mapping.stride = static_cast<u8>(padded_upload_stride(g_gxState.arrays[i].stride));
|
||||
mapping.le = g_gxState.arrays[i].le;
|
||||
vtxOffset += index_attr_size(attr, attrFmt.cnt, type);
|
||||
break;
|
||||
case GX_INDEX16:
|
||||
mapping.stride = g_gxState.arrays[i].stride;
|
||||
mapping.stride = static_cast<u8>(padded_upload_stride(g_gxState.arrays[i].stride));
|
||||
mapping.le = g_gxState.arrays[i].le;
|
||||
vtxOffset += index_attr_size(attr, attrFmt.cnt, type);
|
||||
break;
|
||||
@@ -1803,7 +1828,7 @@ void populate_pipeline_config(PipelineConfig& config, GXPrimitive primitive, GXV
|
||||
Log.fatal("populate_pipeline_config: Invalid vertex type {}", type);
|
||||
}
|
||||
}
|
||||
config.shaderConfig.vtxStride = vtxOffset;
|
||||
config.shaderConfig.vtxStride = static_cast<u8>(padded_upload_stride(vtxOffset));
|
||||
if (primitive == GX_LINES) {
|
||||
config.shaderConfig.lineMode = 1;
|
||||
} else if (primitive == GX_LINESTRIP) {
|
||||
|
||||
@@ -285,6 +285,8 @@ struct AttrArray {
|
||||
u8 stride;
|
||||
bool le = true;
|
||||
gfx::Range cachedRange;
|
||||
// Element stride of the cached upload, which differs from `stride` when the upload is padded.
|
||||
u32 cachedStride = 0;
|
||||
};
|
||||
inline bool operator==(const AttrArray& lhs, const AttrArray& rhs) {
|
||||
return lhs.data == rhs.data && lhs.size == rhs.size && lhs.stride == rhs.stride && lhs.le == rhs.le;
|
||||
@@ -771,4 +773,8 @@ void notify_copy_texture_created() noexcept;
|
||||
|
||||
u8 comp_type_size(GXAttr attr, GXCompType type) noexcept;
|
||||
u8 comp_cnt_count(GXAttr attr, GXCompCnt cnt) noexcept;
|
||||
// Bytes per uploaded vertex or vertex-array element, and so the stride the shader multiplies an index by, for GX
|
||||
// data packed at `packedStride` bytes. Equal to `packedStride` except on Android, which pads to a multiple of 4
|
||||
// (see gx.cpp).
|
||||
u32 padded_upload_stride(u32 packedStride) noexcept;
|
||||
} // namespace aurora::gx
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <magic_enum.hpp>
|
||||
#include <webgpu/webgpu_cpp.h>
|
||||
|
||||
#include "../android_debug.hpp"
|
||||
#include "../gfx/common.hpp"
|
||||
#include "../internal.hpp"
|
||||
#include "../window.hpp"
|
||||
@@ -170,6 +171,19 @@ wgpu::TextureFormat best_surface_format() {
|
||||
if (g_surfaceCapabilities.formatCount == 0) {
|
||||
return wgpu::TextureFormat::Undefined;
|
||||
}
|
||||
#if defined(__ANDROID__)
|
||||
// The OpenXR bridge shares eyes through AHardwareBuffers, and Android has no
|
||||
// BGRA AHardwareBuffer format, so an XR interop build must render RGBA8 to
|
||||
// keep the eye copy legal. Only the preference order changes; a surface
|
||||
// without RGBA8 still falls through to the generic choice below.
|
||||
if (g_config.xrInterop) {
|
||||
for (size_t i = 0; i < g_surfaceCapabilities.formatCount; ++i) {
|
||||
if (to_linear(g_surfaceCapabilities.formats[i]) == wgpu::TextureFormat::RGBA8Unorm) {
|
||||
return wgpu::TextureFormat::RGBA8Unorm;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
for (size_t i = 0; i < g_surfaceCapabilities.formatCount; ++i) {
|
||||
const auto format = to_linear(g_surfaceCapabilities.formats[i]);
|
||||
if (format == wgpu::TextureFormat::RGBA8Unorm || format == wgpu::TextureFormat::BGRA8Unorm) {
|
||||
@@ -552,7 +566,7 @@ bool initialize(AuroraBackend auroraBackend) {
|
||||
});
|
||||
if (g_config.xrInterop) {
|
||||
instanceDescriptor.nextInChain = &instanceToggles;
|
||||
Log.info("Enabling Dawn unsafe APIs for OpenXR D3D12 resource interop");
|
||||
Log.info("Enabling Dawn unsafe APIs for OpenXR resource interop");
|
||||
}
|
||||
#endif
|
||||
#if defined(WEBGPU_DAWN) && !defined(__MINGW32__)
|
||||
@@ -700,6 +714,15 @@ bool initialize(AuroraBackend auroraBackend) {
|
||||
feature == wgpu::FeatureName::SharedFenceDXGISharedHandle)) {
|
||||
requiredFeatures.push_back(feature);
|
||||
}
|
||||
#endif
|
||||
#if defined(WEBGPU_DAWN) && defined(__ANDROID__)
|
||||
// The OpenXR side shares eyes through AHardwareBuffers ordered by sync
|
||||
// file descriptors (lib/webgpu/vulkan_interop.cpp).
|
||||
if (g_config.xrInterop && g_backendType == wgpu::BackendType::Vulkan &&
|
||||
(feature == wgpu::FeatureName::SharedTextureMemoryAHardwareBuffer ||
|
||||
feature == wgpu::FeatureName::SharedFenceSyncFD)) {
|
||||
requiredFeatures.push_back(feature);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if (!implicitDeviceSynchronizationSupported) {
|
||||
@@ -731,8 +754,17 @@ bool initialize(AuroraBackend auroraBackend) {
|
||||
/* clang-format on */
|
||||
};
|
||||
#ifdef NDEBUG
|
||||
enableToggles.push_back("skip_validation");
|
||||
enableToggles.push_back("disable_robustness");
|
||||
#if defined(__ANDROID__)
|
||||
// `adb shell setprop debug.wiicompiled.validation 1` keeps WebGPU validation
|
||||
// and robustness on for a diagnostic run.
|
||||
const bool keepValidation = android_debug::property_int("debug.wiicompiled.validation", 0) == 1;
|
||||
Log.info("Android WebGPU validation: {}", keepValidation ? "on" : "off");
|
||||
if (!keepValidation)
|
||||
#endif
|
||||
{
|
||||
enableToggles.push_back("skip_validation");
|
||||
enableToggles.push_back("disable_robustness");
|
||||
}
|
||||
#endif
|
||||
if (g_backendType == wgpu::BackendType::Vulkan) {
|
||||
enableToggles.push_back("vulkan_monolithic_pipeline_cache");
|
||||
|
||||
@@ -0,0 +1,620 @@
|
||||
#include <aurora/vulkan_interop.h>
|
||||
|
||||
#include "../internal.hpp"
|
||||
#include "../stereo.hpp"
|
||||
#include "gpu.hpp"
|
||||
|
||||
#if defined(__ANDROID__) && defined(WEBGPU_DAWN)
|
||||
|
||||
#include <android/hardware_buffer.h>
|
||||
#include <magic_enum.hpp>
|
||||
#include <poll.h>
|
||||
#include <unistd.h>
|
||||
#include <vulkan/vulkan_core.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace aurora::vulkan_interop {
|
||||
namespace {
|
||||
|
||||
Module Log("aurora::vulkan_interop");
|
||||
|
||||
int64_t to_vk_format(wgpu::TextureFormat format) noexcept {
|
||||
switch (format) {
|
||||
case wgpu::TextureFormat::RGBA8Unorm:
|
||||
return VK_FORMAT_R8G8B8A8_UNORM;
|
||||
case wgpu::TextureFormat::RGBA8UnormSrgb:
|
||||
return VK_FORMAT_R8G8B8A8_SRGB;
|
||||
case wgpu::TextureFormat::BGRA8Unorm:
|
||||
return VK_FORMAT_B8G8R8A8_UNORM;
|
||||
case wgpu::TextureFormat::BGRA8UnormSrgb:
|
||||
return VK_FORMAT_B8G8R8A8_SRGB;
|
||||
case wgpu::TextureFormat::RGBA16Float:
|
||||
return VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||
default:
|
||||
return VK_FORMAT_UNDEFINED;
|
||||
}
|
||||
}
|
||||
|
||||
// vkCmdCopyImage and WebGPU CopyTextureToTexture both require the two formats
|
||||
// to be the same modulo sRGB encoding, so the bridge only accepts a target
|
||||
// whose VkFormat sits in the same family as Aurora's eye output.
|
||||
int copy_family(int64_t format) noexcept {
|
||||
switch (format) {
|
||||
case VK_FORMAT_R8G8B8A8_UNORM:
|
||||
case VK_FORMAT_R8G8B8A8_SRGB:
|
||||
return 1;
|
||||
case VK_FORMAT_B8G8R8A8_UNORM:
|
||||
case VK_FORMAT_B8G8R8A8_SRGB:
|
||||
return 2;
|
||||
case VK_FORMAT_R16G16B16A16_SFLOAT:
|
||||
return 3;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool same_copy_family(int64_t left, int64_t right) noexcept {
|
||||
const int family = copy_family(left);
|
||||
return family != 0 && family == copy_family(right);
|
||||
}
|
||||
|
||||
void close_fd(int& fd) noexcept {
|
||||
if (fd >= 0) {
|
||||
::close(fd);
|
||||
}
|
||||
fd = -1;
|
||||
}
|
||||
|
||||
// A sync file descriptor becomes readable once its fence signals, so a plain
|
||||
// poll() is a CPU-side wait that needs no libsync.
|
||||
void wait_sync_fd(int fd) noexcept {
|
||||
if (fd < 0) {
|
||||
return;
|
||||
}
|
||||
pollfd request{.fd = fd, .events = POLLIN, .revents = 0};
|
||||
while (::poll(&request, 1, 1000) == 0) {
|
||||
Log.warn("Waiting on a Dawn release fence took over a second");
|
||||
}
|
||||
}
|
||||
|
||||
bool device_supports_bridge() noexcept {
|
||||
return webgpu::g_device && webgpu::g_backendType == wgpu::BackendType::Vulkan &&
|
||||
webgpu::g_device.HasFeature(wgpu::FeatureName::SharedTextureMemoryAHardwareBuffer) &&
|
||||
webgpu::g_device.HasFeature(wgpu::FeatureName::SharedFenceSyncFD);
|
||||
}
|
||||
|
||||
// One AHardwareBuffer imported into Dawn. The import is created on first use
|
||||
// and kept until the bridge is disabled; the ring the OpenXR side recycles is
|
||||
// tiny (two buffers per eye), so this never grows beyond a handful of entries.
|
||||
struct Import {
|
||||
AHardwareBuffer* buffer = nullptr;
|
||||
wgpu::SharedTextureMemory memory;
|
||||
wgpu::Texture texture;
|
||||
wgpu::TextureFormat format = wgpu::TextureFormat::Undefined;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
bool initialized = false;
|
||||
bool accessBegun = false;
|
||||
};
|
||||
|
||||
struct PendingTarget {
|
||||
AHardwareBuffer* buffer = nullptr;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
int64_t vkFormat = VK_FORMAT_UNDEFINED;
|
||||
int acquireFenceFd = -1;
|
||||
int32_t acquireImageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
};
|
||||
|
||||
class StereoBridge final {
|
||||
public:
|
||||
StereoBridge(AuroraVulkanStereoSubmittedCallback callback, void* userdata) noexcept
|
||||
: m_callback(callback), m_userdata(userdata) {}
|
||||
|
||||
~StereoBridge() { ReleaseImportsLocked(); }
|
||||
|
||||
bool Initialize() noexcept {
|
||||
if (!device_supports_bridge()) {
|
||||
Log.error("Dawn Vulkan device lacks SharedTextureMemoryAHardwareBuffer or SharedFenceSyncFD");
|
||||
return false;
|
||||
}
|
||||
m_auroraFormat = webgpu::g_graphicsConfig.surfaceConfiguration.format;
|
||||
return to_vk_format(m_auroraFormat) != VK_FORMAT_UNDEFINED;
|
||||
}
|
||||
|
||||
bool PrepareForDestruction() noexcept {
|
||||
std::lock_guard lock(m_mutex);
|
||||
// Dawn's queue is drained by aurora_quiesce_frame_worker() before this is
|
||||
// reached; all that can be outstanding here is a begun access whose
|
||||
// EndAccess never ran because the frame was abandoned.
|
||||
for (auto& [buffer, import] : m_imports) {
|
||||
if (import.accessBegun) {
|
||||
wgpu::SharedTextureMemoryEndAccessState end{};
|
||||
import.memory.EndAccess(import.texture, &end);
|
||||
import.accessBegun = false;
|
||||
}
|
||||
}
|
||||
ReleaseImportsLocked();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SetTargets(uint64_t token, const AuroraVulkanStereoTarget* targets,
|
||||
uint32_t targetCount) noexcept {
|
||||
if (token == 0 || targets == nullptr || targetCount == 0 ||
|
||||
targetCount > AURORA_VULKAN_STEREO_MAX_TARGETS) {
|
||||
return false;
|
||||
}
|
||||
std::lock_guard lock(m_mutex);
|
||||
if (m_framePending || m_encoded) {
|
||||
return false;
|
||||
}
|
||||
const int64_t auroraFormat = to_vk_format(m_auroraFormat);
|
||||
for (uint32_t eye = 0; eye < targetCount; ++eye) {
|
||||
const auto& target = targets[eye];
|
||||
if (target.buffer == nullptr || target.width == 0 || target.height == 0 ||
|
||||
!same_copy_family(target.vkFormat, auroraFormat)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (uint32_t eye = 0; eye < targetCount; ++eye) {
|
||||
m_targets[eye] = {
|
||||
.buffer = targets[eye].buffer,
|
||||
.width = targets[eye].width,
|
||||
.height = targets[eye].height,
|
||||
.vkFormat = targets[eye].vkFormat,
|
||||
.acquireFenceFd = targets[eye].acquireFenceFd,
|
||||
.acquireImageLayout = targets[eye].acquireImageLayout,
|
||||
};
|
||||
}
|
||||
for (uint32_t eye = targetCount; eye < m_targets.size(); ++eye) {
|
||||
m_targets[eye] = {};
|
||||
}
|
||||
m_frameToken = token;
|
||||
m_targetCount = targetCount;
|
||||
m_framePending = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Encode(wgpu::CommandEncoder& encoder, const stereo::SinkFrame& frame) noexcept {
|
||||
std::lock_guard lock(m_mutex);
|
||||
if (!m_framePending || m_encoded || frame.frameToken != m_frameToken) {
|
||||
return false;
|
||||
}
|
||||
if (EncodeLocked(encoder, frame)) {
|
||||
m_encoded = true;
|
||||
return true;
|
||||
}
|
||||
PublishAndClearFrameLocked(frame.frameToken, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
void Submitted(const stereo::SinkFrame& frame) noexcept {
|
||||
std::lock_guard lock(m_mutex);
|
||||
if (!m_framePending || !m_encoded || frame.frameToken != m_frameToken) {
|
||||
return;
|
||||
}
|
||||
std::array<AuroraVulkanStereoRelease, AURORA_VULKAN_STEREO_MAX_TARGETS> releases{};
|
||||
const bool success = EndAccessLocked(releases);
|
||||
NotifyLocked(frame.frameToken, success, releases);
|
||||
ClearFrameLocked();
|
||||
}
|
||||
|
||||
void CancelPending() noexcept {
|
||||
std::lock_guard lock(m_mutex);
|
||||
if (!m_framePending) {
|
||||
return;
|
||||
}
|
||||
const uint64_t token = m_frameToken;
|
||||
std::array<AuroraVulkanStereoRelease, AURORA_VULKAN_STEREO_MAX_TARGETS> releases{};
|
||||
if (m_encoded) {
|
||||
EndAccessLocked(releases);
|
||||
for (auto& release : releases) {
|
||||
close_fd(release.releaseFenceFd);
|
||||
}
|
||||
}
|
||||
PublishAndClearFrameLocked(token, false);
|
||||
}
|
||||
|
||||
bool CancelBeforeEncode(uint64_t token) noexcept {
|
||||
std::unique_lock lock(m_mutex, std::try_to_lock);
|
||||
if (!lock.owns_lock()) {
|
||||
return false;
|
||||
}
|
||||
if (token == 0 || !m_framePending || m_encoded || token != m_frameToken) {
|
||||
return false;
|
||||
}
|
||||
ClearFrameLocked();
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
Import* EnsureImport(uint32_t eye, const stereo::EyeImage& source) noexcept {
|
||||
const auto& target = m_targets[eye];
|
||||
if (source.texture == nullptr || source.format != m_auroraFormat ||
|
||||
source.size.width != target.width || source.size.height != target.height) {
|
||||
Log.error("Stereo eye {} does not match its OpenXR Vulkan target ({}x{} vs {}x{})", eye,
|
||||
source.size.width, source.size.height, target.width, target.height);
|
||||
return nullptr;
|
||||
}
|
||||
if (auto found = m_imports.find(target.buffer); found != m_imports.end()) {
|
||||
auto& import = found->second;
|
||||
if (import.width == target.width && import.height == target.height &&
|
||||
import.format == source.format) {
|
||||
return &import;
|
||||
}
|
||||
Log.error("AHardwareBuffer for eye {} was re-used with a different geometry", eye);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Import import;
|
||||
import.buffer = target.buffer;
|
||||
AHardwareBuffer_acquire(import.buffer);
|
||||
|
||||
wgpu::SharedTextureMemoryAHardwareBufferDescriptor ahb{};
|
||||
ahb.handle = target.buffer;
|
||||
const wgpu::SharedTextureMemoryDescriptor memoryDescriptor{
|
||||
.nextInChain = &ahb,
|
||||
.label = eye == 0 ? "OpenXR left eye AHardwareBuffer" : "OpenXR right eye AHardwareBuffer",
|
||||
};
|
||||
import.memory = webgpu::g_device.ImportSharedTextureMemory(&memoryDescriptor);
|
||||
if (!import.memory) {
|
||||
Log.error("Dawn rejected the AHardwareBuffer import for eye {}", eye);
|
||||
AHardwareBuffer_release(import.buffer);
|
||||
return nullptr;
|
||||
}
|
||||
wgpu::SharedTextureMemoryProperties properties{};
|
||||
if (import.memory.GetProperties(&properties) != wgpu::Status::Success ||
|
||||
properties.size.width != target.width || properties.size.height != target.height ||
|
||||
(properties.usage & wgpu::TextureUsage::CopyDst) == wgpu::TextureUsage::None) {
|
||||
Log.error("Dawn reported incompatible AHardwareBuffer properties for eye {}", eye);
|
||||
AHardwareBuffer_release(import.buffer);
|
||||
return nullptr;
|
||||
}
|
||||
if (properties.format != source.format) {
|
||||
// The OpenXR side allocates R8G8B8A8_UNORM buffers because that is the only
|
||||
// 8-bit RGBA AHardwareBuffer format; gpu.cpp steers Aurora to RGBA8Unorm
|
||||
// under xrInterop so this mismatch only happens on a driver with no such
|
||||
// surface format, where a copy could never be legal anyway.
|
||||
Log.error("AHardwareBuffer format {} does not match Aurora's {} for eye {}",
|
||||
magic_enum::enum_name(properties.format), magic_enum::enum_name(source.format), eye);
|
||||
AHardwareBuffer_release(import.buffer);
|
||||
return nullptr;
|
||||
}
|
||||
const wgpu::TextureDescriptor textureDescriptor{
|
||||
.label = eye == 0 ? "OpenXR left eye shared texture" : "OpenXR right eye shared texture",
|
||||
.usage = wgpu::TextureUsage::CopyDst,
|
||||
.dimension = wgpu::TextureDimension::e2D,
|
||||
.size = {target.width, target.height, 1},
|
||||
.format = source.format,
|
||||
.mipLevelCount = 1,
|
||||
.sampleCount = 1,
|
||||
};
|
||||
import.texture = import.memory.CreateTexture(&textureDescriptor);
|
||||
if (!import.texture) {
|
||||
Log.error("Dawn could not wrap the AHardwareBuffer for eye {}", eye);
|
||||
AHardwareBuffer_release(import.buffer);
|
||||
return nullptr;
|
||||
}
|
||||
import.format = source.format;
|
||||
import.width = target.width;
|
||||
import.height = target.height;
|
||||
const auto [inserted, ok] = m_imports.emplace(target.buffer, std::move(import));
|
||||
return ok ? &inserted->second : nullptr;
|
||||
}
|
||||
|
||||
bool EncodeLocked(wgpu::CommandEncoder& encoder, const stereo::SinkFrame& frame) noexcept {
|
||||
std::array<Import*, AURORA_VULKAN_STEREO_MAX_TARGETS> imports{};
|
||||
for (uint32_t eye = 0; eye < m_targetCount; ++eye) {
|
||||
imports[eye] = EnsureImport(eye, frame.eyes[eye]);
|
||||
if (imports[eye] == nullptr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (uint32_t eye = 0; eye < m_targetCount; ++eye) {
|
||||
auto& target = m_targets[eye];
|
||||
auto& import = *imports[eye];
|
||||
if (import.accessBegun) {
|
||||
Log.error("AHardwareBuffer for eye {} is still under a previous access", eye);
|
||||
RollbackAccesses(imports, eye);
|
||||
return false;
|
||||
}
|
||||
// The OpenXR side's release barrier leaves the image in acquireImageLayout;
|
||||
// Dawn's acquire barrier must repeat exactly that old/new pair, then it
|
||||
// transitions to its own copy-destination layout. A never-written buffer
|
||||
// has undefined contents and layout, which Dawn treats as uninitialized.
|
||||
const bool haveContents = import.initialized &&
|
||||
target.acquireImageLayout != VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
wgpu::SharedTextureMemoryVkImageLayoutBeginState layout{};
|
||||
layout.oldLayout = haveContents ? target.acquireImageLayout : VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
layout.newLayout = haveContents ? target.acquireImageLayout : VK_IMAGE_LAYOUT_GENERAL;
|
||||
wgpu::SharedFence acquireFence;
|
||||
if (target.acquireFenceFd >= 0) {
|
||||
wgpu::SharedFenceSyncFDDescriptor syncFd{};
|
||||
syncFd.handle = target.acquireFenceFd;
|
||||
const wgpu::SharedFenceDescriptor fenceDescriptor{
|
||||
.nextInChain = &syncFd,
|
||||
.label = "OpenXR eye copy-out fence",
|
||||
};
|
||||
// Dawn duplicates the descriptor; the bridge still owns and closes its copy.
|
||||
acquireFence = webgpu::g_device.ImportSharedFence(&fenceDescriptor);
|
||||
close_fd(target.acquireFenceFd);
|
||||
if (!acquireFence) {
|
||||
Log.error("Dawn could not import the OpenXR copy-out fence for eye {}", eye);
|
||||
RollbackAccesses(imports, eye);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const std::array fences{acquireFence};
|
||||
// A sync file descriptor is binary; Dawn requires signaled value 1 for
|
||||
// SyncFD fences ("signaled value (0) was not 1" otherwise).
|
||||
const std::array<uint64_t, 1> values{1};
|
||||
wgpu::SharedTextureMemoryBeginAccessDescriptor begin{};
|
||||
begin.nextInChain = &layout;
|
||||
begin.concurrentRead = false;
|
||||
begin.initialized = haveContents;
|
||||
if (acquireFence) {
|
||||
begin.fenceCount = 1;
|
||||
begin.fences = fences.data();
|
||||
begin.signaledValueCount = 1;
|
||||
begin.signaledValues = values.data();
|
||||
}
|
||||
if (import.memory.BeginAccess(import.texture, &begin) != wgpu::Status::Success) {
|
||||
Log.error("Dawn BeginAccess failed for stereo eye {}", eye);
|
||||
RollbackAccesses(imports, eye);
|
||||
return false;
|
||||
}
|
||||
import.accessBegun = true;
|
||||
}
|
||||
for (uint32_t eye = 0; eye < m_targetCount; ++eye) {
|
||||
const auto& import = *imports[eye];
|
||||
const wgpu::TexelCopyTextureInfo source{
|
||||
.texture = *frame.eyes[eye].texture,
|
||||
.mipLevel = 0,
|
||||
.origin = {},
|
||||
.aspect = wgpu::TextureAspect::All,
|
||||
};
|
||||
const wgpu::TexelCopyTextureInfo destination{
|
||||
.texture = import.texture,
|
||||
.mipLevel = 0,
|
||||
.origin = {},
|
||||
.aspect = wgpu::TextureAspect::All,
|
||||
};
|
||||
const wgpu::Extent3D extent{import.width, import.height, 1};
|
||||
encoder.CopyTextureToTexture(&source, &destination, &extent);
|
||||
m_encodedImports[eye] = imports[eye];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void RollbackAccesses(const std::array<Import*, AURORA_VULKAN_STEREO_MAX_TARGETS>& imports,
|
||||
uint32_t count) noexcept {
|
||||
for (uint32_t eye = 0; eye < count; ++eye) {
|
||||
if (imports[eye] != nullptr && imports[eye]->accessBegun) {
|
||||
wgpu::SharedTextureMemoryEndAccessState end{};
|
||||
imports[eye]->memory.EndAccess(imports[eye]->texture, &end);
|
||||
imports[eye]->initialized = end.initialized;
|
||||
imports[eye]->accessBegun = false;
|
||||
}
|
||||
}
|
||||
for (auto& target : m_targets) {
|
||||
close_fd(target.acquireFenceFd);
|
||||
}
|
||||
}
|
||||
|
||||
bool EndAccessLocked(
|
||||
std::array<AuroraVulkanStereoRelease, AURORA_VULKAN_STEREO_MAX_TARGETS>& releases) noexcept {
|
||||
bool success = true;
|
||||
for (auto& release : releases) {
|
||||
release = {.releaseFenceFd = -1, .releasedImageLayout = VK_IMAGE_LAYOUT_UNDEFINED};
|
||||
}
|
||||
for (uint32_t eye = 0; eye < m_targetCount; ++eye) {
|
||||
Import* import = m_encodedImports[eye];
|
||||
if (import == nullptr || !import->accessBegun) {
|
||||
success = false;
|
||||
continue;
|
||||
}
|
||||
wgpu::SharedTextureMemoryVkImageLayoutEndState layout{};
|
||||
wgpu::SharedTextureMemoryEndAccessState end{};
|
||||
end.nextInChain = &layout;
|
||||
if (import->memory.EndAccess(import->texture, &end) != wgpu::Status::Success) {
|
||||
Log.error("Dawn EndAccess failed for stereo eye {}", eye);
|
||||
success = false;
|
||||
} else {
|
||||
import->initialized = end.initialized;
|
||||
releases[eye].releasedImageLayout = layout.newLayout;
|
||||
for (size_t i = 0; i < end.fenceCount; ++i) {
|
||||
wgpu::SharedFenceSyncFDExportInfo syncFd{};
|
||||
wgpu::SharedFenceExportInfo info{};
|
||||
info.nextInChain = &syncFd;
|
||||
end.fences[i].ExportInfo(&info);
|
||||
if (info.type == wgpu::SharedFenceType::SyncFD && syncFd.handle >= 0) {
|
||||
// The fence keeps its descriptor; hand the caller an independent one.
|
||||
const int duplicate = ::dup(syncFd.handle);
|
||||
if (duplicate >= 0) {
|
||||
if (releases[eye].releaseFenceFd >= 0) {
|
||||
// Dawn normally returns exactly one fence per access. Both must
|
||||
// be honoured and one descriptor cannot express two fences, so
|
||||
// the earlier one is retired on the CPU before handing over the
|
||||
// latest.
|
||||
Log.warn("Dawn returned several release fences for eye {}; merging on the CPU", eye);
|
||||
wait_sync_fd(releases[eye].releaseFenceFd);
|
||||
close_fd(releases[eye].releaseFenceFd);
|
||||
}
|
||||
releases[eye].releaseFenceFd = duplicate;
|
||||
}
|
||||
} else {
|
||||
Log.error("Dawn returned a non-sync-fd fence for eye {}", eye);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
import->accessBegun = false;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
void ReleaseImportsLocked() noexcept {
|
||||
for (auto& [buffer, import] : m_imports) {
|
||||
import.texture = nullptr;
|
||||
import.memory = nullptr;
|
||||
if (import.buffer != nullptr) {
|
||||
AHardwareBuffer_release(import.buffer);
|
||||
}
|
||||
}
|
||||
m_imports.clear();
|
||||
}
|
||||
|
||||
void ClearFrameLocked() noexcept {
|
||||
for (auto& target : m_targets) {
|
||||
close_fd(target.acquireFenceFd);
|
||||
target = {};
|
||||
}
|
||||
m_encodedImports = {};
|
||||
m_frameToken = 0;
|
||||
m_targetCount = 0;
|
||||
m_framePending = false;
|
||||
m_encoded = false;
|
||||
}
|
||||
|
||||
void PublishAndClearFrameLocked(uint64_t token, bool success) noexcept {
|
||||
std::array<AuroraVulkanStereoRelease, AURORA_VULKAN_STEREO_MAX_TARGETS> releases{};
|
||||
for (auto& release : releases) {
|
||||
release = {.releaseFenceFd = -1, .releasedImageLayout = VK_IMAGE_LAYOUT_UNDEFINED};
|
||||
}
|
||||
NotifyLocked(token, success, releases);
|
||||
ClearFrameLocked();
|
||||
}
|
||||
|
||||
void NotifyLocked(uint64_t token, bool success,
|
||||
const std::array<AuroraVulkanStereoRelease, AURORA_VULKAN_STEREO_MAX_TARGETS>&
|
||||
releases) noexcept {
|
||||
if (m_callback != nullptr) {
|
||||
m_callback(token, success, releases.data(), m_targetCount, m_userdata);
|
||||
} else {
|
||||
for (auto release : releases) {
|
||||
close_fd(release.releaseFenceFd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::mutex m_mutex;
|
||||
std::unordered_map<AHardwareBuffer*, Import> m_imports;
|
||||
std::array<PendingTarget, AURORA_VULKAN_STEREO_MAX_TARGETS> m_targets{};
|
||||
std::array<Import*, AURORA_VULKAN_STEREO_MAX_TARGETS> m_encodedImports{};
|
||||
wgpu::TextureFormat m_auroraFormat = wgpu::TextureFormat::Undefined;
|
||||
AuroraVulkanStereoSubmittedCallback m_callback = nullptr;
|
||||
void* m_userdata = nullptr;
|
||||
uint64_t m_frameToken = 0;
|
||||
uint32_t m_targetCount = 0;
|
||||
bool m_framePending = false;
|
||||
bool m_encoded = false;
|
||||
};
|
||||
|
||||
std::unique_ptr<StereoBridge> g_bridge;
|
||||
|
||||
bool sink_encode(wgpu::CommandEncoder& encoder, const stereo::SinkFrame& frame,
|
||||
void* userdata) noexcept {
|
||||
return static_cast<StereoBridge*>(userdata)->Encode(encoder, frame);
|
||||
}
|
||||
|
||||
void sink_submitted(const stereo::SinkFrame& frame, void* userdata) noexcept {
|
||||
static_cast<StereoBridge*>(userdata)->Submitted(frame);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace aurora::vulkan_interop
|
||||
|
||||
bool aurora_vulkan_get_native_handles(AuroraVulkanNativeHandles* handles) {
|
||||
if (handles == nullptr) {
|
||||
return false;
|
||||
}
|
||||
*handles = {};
|
||||
using namespace aurora::vulkan_interop;
|
||||
if (!aurora::webgpu::g_device || aurora::webgpu::g_backendType != wgpu::BackendType::Vulkan) {
|
||||
return false;
|
||||
}
|
||||
const int64_t format = to_vk_format(aurora::webgpu::g_graphicsConfig.surfaceConfiguration.format);
|
||||
if (format == VK_FORMAT_UNDEFINED) {
|
||||
return false;
|
||||
}
|
||||
*handles = {
|
||||
.colorVkFormat = format,
|
||||
.sharedTextureMemoryAHardwareBuffer =
|
||||
aurora::webgpu::g_device.HasFeature(wgpu::FeatureName::SharedTextureMemoryAHardwareBuffer),
|
||||
.sharedFenceSyncFd = aurora::webgpu::g_device.HasFeature(wgpu::FeatureName::SharedFenceSyncFD),
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
bool aurora_vulkan_enable_stereo_bridge(AuroraVulkanStereoSubmittedCallback submitted,
|
||||
void* userdata) {
|
||||
using namespace aurora::vulkan_interop;
|
||||
if (g_bridge || submitted == nullptr) {
|
||||
return false;
|
||||
}
|
||||
auto bridge = std::make_unique<StereoBridge>(submitted, userdata);
|
||||
if (!bridge->Initialize()) {
|
||||
return false;
|
||||
}
|
||||
aurora::stereo::set_sink(sink_encode, sink_submitted, bridge.get());
|
||||
g_bridge = std::move(bridge);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool aurora_vulkan_set_stereo_targets(uint64_t frameToken,
|
||||
const AuroraVulkanStereoTarget* targets,
|
||||
uint32_t targetCount) {
|
||||
using namespace aurora::vulkan_interop;
|
||||
return g_bridge && g_bridge->SetTargets(frameToken, targets, targetCount);
|
||||
}
|
||||
|
||||
bool aurora_vulkan_cancel_stereo_targets(uint64_t frameToken) {
|
||||
using namespace aurora::vulkan_interop;
|
||||
return g_bridge && g_bridge->CancelBeforeEncode(frameToken);
|
||||
}
|
||||
|
||||
bool aurora_vulkan_disable_stereo_bridge() {
|
||||
using namespace aurora::vulkan_interop;
|
||||
if (!g_bridge) {
|
||||
return true;
|
||||
}
|
||||
aurora::stereo::set_sink(nullptr, nullptr, nullptr);
|
||||
g_bridge->CancelPending();
|
||||
if (!g_bridge->PrepareForDestruction()) {
|
||||
(void)g_bridge.release();
|
||||
return false;
|
||||
}
|
||||
g_bridge.reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
bool aurora_vulkan_get_native_handles(AuroraVulkanNativeHandles* handles) {
|
||||
if (handles != nullptr) {
|
||||
*handles = {};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool aurora_vulkan_enable_stereo_bridge(AuroraVulkanStereoSubmittedCallback, void*) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool aurora_vulkan_set_stereo_targets(uint64_t, const AuroraVulkanStereoTarget*, uint32_t) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool aurora_vulkan_cancel_stereo_targets(uint64_t) { return false; }
|
||||
|
||||
bool aurora_vulkan_disable_stereo_bridge() { return true; }
|
||||
|
||||
#endif
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "input.hpp"
|
||||
#include "internal.hpp"
|
||||
|
||||
#include <aurora/android.h>
|
||||
#include <aurora/aurora.h>
|
||||
#include <aurora/event.h>
|
||||
#include <aurora/gfx.h>
|
||||
@@ -28,12 +29,11 @@
|
||||
|
||||
#if defined(SDL_PLATFORM_ANDROID)
|
||||
#include <jni.h>
|
||||
extern "C" void Android_LockActivityMutex(void);
|
||||
extern "C" void Android_UnlockActivityMutex(void);
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include "dolphin/vi/vi_internal.hpp"
|
||||
@@ -55,6 +55,10 @@ std::atomic_bool g_backgrounded = false;
|
||||
std::atomic_bool g_nativeResizePending = false;
|
||||
std::atomic<AuroraDisplayMode> g_displayMode{AURORA_DISPLAY_MODE_WINDOWED};
|
||||
#if defined(SDL_PLATFORM_ANDROID)
|
||||
// SDL's own activity mutex (Android_LockActivityMutex) is internal and not
|
||||
// exported from libSDL3.so, so SurfaceLock owns an equivalent one. Recursive
|
||||
// because a surface-recreation path can re-enter through refresh_surface().
|
||||
std::recursive_mutex g_surfaceMutex;
|
||||
std::atomic_bool g_surfaceReady = false;
|
||||
#else
|
||||
std::atomic_bool g_surfaceReady = true;
|
||||
@@ -582,10 +586,19 @@ bool is_presentable() noexcept {
|
||||
}
|
||||
|
||||
void pump_events() noexcept {
|
||||
#if defined(SDL_PLATFORM_ANDROID)
|
||||
// Guest fibers run on libco stacks inside the SDL thread. SDL's Android pump
|
||||
// reaches Java (joystick polling, HIDAPI), and ART binds JNI transitions to
|
||||
// the thread's real stack, so pumping here from a fiber can crash the VM.
|
||||
// Events are pumped only by the host through aurora_update(), which the
|
||||
// runtime calls on the scheduler's own stack (KartPad found this on device).
|
||||
return;
|
||||
#else
|
||||
if (g_window != nullptr) {
|
||||
SDL_SyncWindow(g_window);
|
||||
}
|
||||
SDL_PumpEvents();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool native_resize_pending() noexcept { return g_nativeResizePending.load(std::memory_order_acquire); }
|
||||
@@ -605,15 +618,29 @@ bool native_window_size_matches(uint32_t width, uint32_t height) noexcept {
|
||||
|
||||
void set_surface_ready(bool ready) noexcept { g_surfaceReady.store(ready, std::memory_order_release); }
|
||||
|
||||
#if defined(SDL_PLATFORM_ANDROID)
|
||||
// Held across the Java side's surface change so no present can race an
|
||||
// ANativeWindow SDL is destroying or replacing (see aurora/android.h).
|
||||
void begin_surface_mutation() noexcept {
|
||||
g_surfaceMutex.lock();
|
||||
set_surface_ready(false);
|
||||
}
|
||||
|
||||
void end_surface_mutation(bool ready) noexcept {
|
||||
set_surface_ready(ready);
|
||||
g_surfaceMutex.unlock();
|
||||
}
|
||||
#endif
|
||||
|
||||
SurfaceLock::SurfaceLock() noexcept {
|
||||
#if defined(SDL_PLATFORM_ANDROID)
|
||||
Android_LockActivityMutex();
|
||||
g_surfaceMutex.lock();
|
||||
#endif
|
||||
}
|
||||
|
||||
SurfaceLock::~SurfaceLock() {
|
||||
#if defined(SDL_PLATFORM_ANDROID)
|
||||
Android_UnlockActivityMutex();
|
||||
g_surfaceMutex.unlock();
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -815,4 +842,12 @@ extern "C" JNIEXPORT void JNICALL Java_org_libsdl_app_SDLSurface_auroraNativeSet
|
||||
jboolean ready) {
|
||||
aurora::window::set_surface_ready(ready == JNI_TRUE);
|
||||
}
|
||||
|
||||
void aurora_android_begin_surface_mutation(void) {
|
||||
aurora::window::begin_surface_mutation();
|
||||
}
|
||||
|
||||
void aurora_android_end_surface_mutation(bool ready) {
|
||||
aurora::window::end_surface_mutation(ready);
|
||||
}
|
||||
#endif
|
||||
@@ -44,6 +44,10 @@ void pump_events() noexcept;
|
||||
bool native_resize_pending() noexcept;
|
||||
bool native_window_size_matches(uint32_t width, uint32_t height) noexcept;
|
||||
void set_surface_ready(bool ready) noexcept;
|
||||
#if defined(__ANDROID__)
|
||||
void begin_surface_mutation() noexcept;
|
||||
void end_surface_mutation(bool ready) noexcept;
|
||||
#endif
|
||||
void set_title(const char* title);
|
||||
void set_fullscreen(bool fullscreen);
|
||||
bool get_fullscreen();
|
||||
|
||||
@@ -194,6 +194,8 @@ void populate_pipeline_config(PipelineConfig& config, GXPrimitive primitive, GXV
|
||||
GXBindGroups build_bind_groups(const ShaderInfo& info) noexcept { return {}; }
|
||||
void resolve_sampled_textures(const ShaderInfo& info) noexcept {}
|
||||
u8 color_channel(GXChannelID id) noexcept { return 0; }
|
||||
// Desktop behaviour: vertices upload with their packed GX stride.
|
||||
u32 padded_upload_stride(u32 packedStride) noexcept { return packedStride; }
|
||||
u8 comp_type_size(GXAttr attr, GXCompType type) noexcept {
|
||||
if (!s_useRealVertexFormatHelpers) {
|
||||
return 0;
|
||||
@@ -282,6 +284,15 @@ Range push_verts(const uint8_t* data, size_t length) {
|
||||
s_lastPushedVertices.assign(data, data + length);
|
||||
return {};
|
||||
}
|
||||
std::pair<ByteBuffer, Range> map_verts(size_t length) {
|
||||
s_lastPushedVertices.assign(length, 0);
|
||||
return {ByteBuffer{s_lastPushedVertices.data(), s_lastPushedVertices.size()}, Range{}};
|
||||
}
|
||||
std::pair<ByteBuffer, Range> map_storage(size_t length) {
|
||||
static std::vector<uint8_t> storage;
|
||||
storage.assign(length, 0);
|
||||
return {ByteBuffer{storage.data(), storage.size()}, Range{}};
|
||||
}
|
||||
Range push_indices(const uint8_t* data, size_t length) {
|
||||
CHECK(length % sizeof(uint16_t) == 0, "unaligned test index upload");
|
||||
s_lastPushedIndices.resize(length / sizeof(uint16_t));
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
# WiiCompiled VR on Meta Quest (standalone Android)
|
||||
|
||||
This document is the design and build reference for the native Quest build. It
|
||||
complements `OPENXR.md`, which remains the specification for the presentation
|
||||
policy, the virtual screen, the first-person camera and frame interpolation:
|
||||
all of that is shared, unchanged, between the Windows D3D12 product and the
|
||||
Quest Vulkan product. What differs is everything below the stereo replay: the
|
||||
graphics binding, the app shell and the platform glue.
|
||||
|
||||
## Sources of the design
|
||||
|
||||
- **KartPad** (`kartpad-main/`, the `kartpad-android` runtime branch of the
|
||||
WiiCompiled fork) proved that the translated game runs on Android arm64 with
|
||||
Aurora on Dawn/Vulkan under SDL3's `SDLActivity`. Its lessons carried over:
|
||||
the products are shared libraries SDL loads, the activity exports its
|
||||
directories through the environment before native code runs, the
|
||||
Windows-generated blob assembly needs its section syntax rewritten for ELF,
|
||||
and Dawn's android-aarch64 prebuilt package needs one path rewritten.
|
||||
KartPad's Android fiber/JNI split (never calling Java-backed SDL APIs from a
|
||||
guest fiber stack) is respected here by keeping every OpenXR call on the
|
||||
dedicated pacing thread, which is a real `std::thread`.
|
||||
- **DolphinXR** (`Dolphin-OpenXR-2/`, quest flavour) supplied the Quest-side
|
||||
specifics: the loader must be initialised with the *activity* as its
|
||||
context or the session never leaves `IDLE`; `XrInstanceCreateInfoAndroidKHR`
|
||||
must be chained on instance creation; the manifest needs the Khronos runtime
|
||||
broker queries, the `OPENXR_SYSTEM` permission, the `com.oculus.intent.category.VR`
|
||||
intent category and the `XR_ACTIVITY_START_MODE_FULL_SPACE_UNMANAGED`
|
||||
property; `XR_KHR_android_thread_settings` may reject the renderer-worker
|
||||
type on some runtime builds.
|
||||
|
||||
## Architecture
|
||||
|
||||
### One runtime, two graphics bindings
|
||||
|
||||
`runtime/src/vr/openxr_integration.cpp` owns the pacing thread, policy
|
||||
evaluation, the retained-layer protocol and the head-pose maths. It is written
|
||||
against the backend-neutral vocabulary in `runtime/include/vr/openxr_backend.h`
|
||||
(`OpenXRPresentation`, `OpenXRBackendFrame`, `OpenXRBeginStatus`,
|
||||
`OpenXRSubmissionStatus`) and selects one backend class at compile time:
|
||||
|
||||
| Platform | Backend | Binding |
|
||||
| --- | --- | --- |
|
||||
| Windows | `OpenXRD3D12Backend` (`openxr_d3d12.cpp`) | Dawn's own D3D12 device and queue are bound to the session; eyes are copied on that queue. |
|
||||
| Android | `OpenXRVulkanBackend` (`openxr_vulkan.cpp`) | The backend creates its **own** `VkInstance`/`VkDevice` through the OpenXR runtime; Dawn and that device meet on `AHardwareBuffer`s. |
|
||||
|
||||
The D3D12 types keep their old names through aliases, so `openxr_d3d12_replay_tests`
|
||||
and the desktop code did not change.
|
||||
|
||||
### Why a second Vulkan device
|
||||
|
||||
The pinned Dawn package (`v20260603.191052`) exposes only `VkInstance` from its
|
||||
Vulkan backend, no `VkDevice`, `VkQueue` or queue family, and it will not enable
|
||||
the device extensions the OpenXR runtime demands. Binding Dawn's device to the
|
||||
session is therefore impossible without a patched Dawn. Instead:
|
||||
|
||||
1. `xrCreateVulkanInstanceKHR` / `xrCreateVulkanDeviceKHR` (`XR_KHR_vulkan_enable2`,
|
||||
with an `XR_KHR_vulkan_enable` fallback that queries the extension lists)
|
||||
create a small Vulkan device the runtime is happy with.
|
||||
2. Per eye, two `AHardwareBuffer`s (R8G8B8A8_UNORM, or RGBA16F when Aurora
|
||||
renders float) are allocated and imported on that device
|
||||
(`VK_ANDROID_external_memory_android_hardware_buffer`).
|
||||
3. Aurora imports the same buffers as Dawn shared texture memory
|
||||
(`SharedTextureMemoryAHardwareBuffer`) and, inside the frame worker's
|
||||
command buffer, copies each replayed eye into the buffer
|
||||
(`aurora-main/lib/webgpu/vulkan_interop.cpp`, the twin of
|
||||
`d3d12_interop.cpp` and registered through the same stereo sink).
|
||||
4. Ordering across the two devices uses Android sync file descriptors:
|
||||
Dawn's `EndAccess` exports a `SharedFenceSyncFD` the OpenXR device waits on
|
||||
before its `vkCmdCopyImage` into the acquired swapchain image, and the copy
|
||||
signals an exportable semaphore whose sync fd Dawn waits on before it
|
||||
writes that buffer again. Image layouts follow Vulkan's rule that a queue
|
||||
family ownership release and acquire must repeat the same old/new layout
|
||||
pair: Dawn reports its release layout, the OpenXR side acquires with it,
|
||||
transitions for the copy, and hands the buffer back in `GENERAL` with a
|
||||
transition-free release so Dawn's acquire can mirror it.
|
||||
5. The copy runs on the queue bound to the session before
|
||||
`xrReleaseSwapchainImage`, so the compositor sees ordinary same-queue work.
|
||||
|
||||
The cost is one extra GPU copy per eye per frame, a few hundred microseconds
|
||||
at Quest eye resolutions; the benefit is that stock Dawn is used unchanged
|
||||
and the OpenXR device outlives Aurora's, which is exactly the failure DolphinXR
|
||||
hit on Vulkan when a game's device was destroyed under the compositor.
|
||||
|
||||
`gpu.cpp` steers Aurora to an RGBA8 surface format under `xrInterop` on Android
|
||||
(there is no BGRA `AHardwareBuffer` format) and requests the two Dawn features
|
||||
the bridge needs.
|
||||
|
||||
### Controllers
|
||||
|
||||
Quest Touch controllers are not HID gamepads, so `openxr_input.cpp` syncs an
|
||||
OpenXR action set on the pacing thread and feeds a virtual SDL joystick
|
||||
(`SDL_AttachVirtualJoystick`, type gamepad). Aurora opens it like any pad and
|
||||
assigns it to player 1; every existing binding, dead zone and overlay setting
|
||||
applies. Mapping: A/B → South/East, X/Y → West/North, index triggers → trigger
|
||||
axes, grips → shoulders, thumbsticks → sticks (clicks → stick buttons), left
|
||||
menu → Start. Bindings are suggested for `oculus/touch_controller` and
|
||||
`khr/simple_controller`.
|
||||
|
||||
### Android platform glue
|
||||
|
||||
- `runtime/src/vr/openxr_android.cpp`: `xrInitializeLoaderKHR` with the
|
||||
JavaVM and activity SDL already holds, the `XrInstanceCreateInfoAndroidKHR`
|
||||
chain (`OpenXRConfig::instance_create_next`), and the optional thread hint.
|
||||
- `runtime/src/platform/host_platform.cpp` / `runtime_config.h`: the activity
|
||||
exports `MKW_ANDROID_DATA_DIR` (external files dir, user reachable) and
|
||||
`MKW_ANDROID_RESOURCES_DIR` (unpacked `wii_bootstrap/`, `dsp_coef.bin`,
|
||||
`initial_pipeline_cache.db`); the latter stands in for the executable
|
||||
directory so the existing adjacent-file lookups work unchanged.
|
||||
- `main.cpp` includes `SDL_main.h` on Android so `SDLActivity` finds
|
||||
`SDL_main` in `libmain.so`, and passes the resources path to Aurora.
|
||||
- Fibers use the vendored libco AArch64 backend (Bionic is Linux), guest memory
|
||||
uses the Linux `mmap` path, the MPRIS media monitor is compiled out.
|
||||
- **Surface readiness.** Aurora presents only while `g_surfaceReady` is set, and
|
||||
on Android that flag starts false. Stock SDL3 exports neither its activity
|
||||
mutex (`Android_LockActivityMutex`) nor a readiness hook, so the app's
|
||||
`QuestSurface` subclass brackets SDL's `surfaceChanged`/`surfaceDestroyed`
|
||||
with `aurora_android_begin/end_surface_mutation` (`aurora/android.h`), and
|
||||
Aurora's `SurfaceLock` owns its own recursive mutex. This is KartPad's design.
|
||||
- **JNI only on the real thread stack.** Guest threads run on libco stacks
|
||||
inside the SDL thread, and SDL's Android event pump can reach Java (joystick
|
||||
polling, HIDAPI). ART binds JNI transitions to the thread's real stack, so
|
||||
Aurora's `pump_events` is a no-op on Android and `UpdateAuroraAndProcessEvents`
|
||||
defers a poll made from a guest fiber until control is back on the scheduler
|
||||
context (`GuestFiberManager::IsOnSchedulerFiber`). The default guest thread
|
||||
shares the scheduler context, so most polls run immediately. Also KartPad's
|
||||
finding, from device crashes.
|
||||
- Time conversion for frame interpolation uses `XR_KHR_convert_timespec_time`
|
||||
(CLOCK_MONOTONIC, the clock behind `steady_clock` on Bionic).
|
||||
|
||||
### Build system
|
||||
|
||||
- `runtime/CMakeLists.txt` recognises `CMAKE_SYSTEM_NAME=Android` on arm64 as
|
||||
`MKW_PLATFORM_ANDROID`: OpenXR on by default, Dawn from the pinned
|
||||
android-aarch64 package (digest pinned in `AuroraDawnProvider.cmake`, which
|
||||
also rewrites the package's absolute `liblog.so` path and looks the package
|
||||
up with `NO_CMAKE_FIND_ROOT_PATH` so the NDK sysroot rule does not hide it),
|
||||
SDL3 built shared (or `-DAURORA_SDL3_PROVIDER=system` for the AAR prefab),
|
||||
tests off, products built as `libmain.so` / `libmain_retro_rewind.so`,
|
||||
`-mcpu=cortex-a77` (Quest 2's XR2 Gen 1; Quest 3/Pro are supersets).
|
||||
- `runtime/cmake/PublicProducts.cmake` gains `MKW_GENERATED_DIR` so a build
|
||||
configured from a checkout can name the translator output tree, and on
|
||||
Android rewrites the PE/COFF `.section .rdata,"dr"` of a Windows-generated
|
||||
blob `.S` into ELF `.rodata` plus a GNU-stack note. The translator itself
|
||||
also learned `--target-os windows|macos|linux|android` for
|
||||
`generate-data-init` and `translate-mod`, for pipelines that generate on
|
||||
another host.
|
||||
- `android/`: the Gradle project. `app/src/main/cpp/CMakeLists.txt` adds the
|
||||
repository's `runtime/` as a subdirectory with those Android choices;
|
||||
flavours `base` and `retroRewind` pick the product target and library name.
|
||||
|
||||
## Building
|
||||
|
||||
Prerequisites on the Windows host (all already present on the machine this
|
||||
was developed on): JDK 17, Android SDK with platform 34+, NDK `29.0.14206865`,
|
||||
SDK CMake `3.22.1`, `adb`; a translated graph for your own disc (the installer's
|
||||
`BuildWorkspace/generated`, produced by the normal Windows pipeline).
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File android/Prepare-QuestDependencies.ps1 # SDL3 3.4.4 AAR into android/app/libs
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Install # base game, debug-signed
|
||||
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Flavor retroRewind # Retro Rewind (needs translate-mod output)
|
||||
adb push DATA /sdcard/Android/data/org.wiicompiled.quest/files/WiiCompiledOpenXRVR/DATA
|
||||
```
|
||||
|
||||
`Config.toml`, saves and per-run logs live next to `DATA` under
|
||||
`WiiCompiledOpenXRVR`; the activity writes a first `Config.toml` with
|
||||
`[vr] enabled = true` and `paths.dvd_root` set. Logs: `adb logcat -s SDL WiiCompiledQuest`
|
||||
plus the `Logs/<product>_<stamp>_pid<pid>/console.log` folder the runtime writes.
|
||||
|
||||
A CMake-only cross-compile of the native runtime (no game) is the quick
|
||||
compile check and needs no Gradle:
|
||||
|
||||
```powershell
|
||||
cmake -S runtime -B .scratch/android-audit-build -G Ninja `
|
||||
-DCMAKE_TOOLCHAIN_FILE=$env:LOCALAPPDATA/Android/Sdk/ndk/29.0.14206865/build/cmake/android.toolchain.cmake `
|
||||
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-29 -DANDROID_STL=c++_shared `
|
||||
-DCMAKE_BUILD_TYPE=Release -DMKW_BUILD_PRODUCTS=OFF
|
||||
cmake --build .scratch/android-audit-build --target mkw_android_native_compile aurora_core aurora_gx
|
||||
```
|
||||
|
||||
## Validation status
|
||||
|
||||
What has been verified on the development machine (September 2026):
|
||||
|
||||
- Translator: `dotnet test` passes with the new `--target-os` tests (17/17 in
|
||||
the touched suites).
|
||||
- Windows: `mkw_openxr_replay_tests` and `mkw_vr_policy_tests` pass; the
|
||||
`mkw_runtime_common` and `aurora_core` targets compile with the refactored
|
||||
integration; the workspace product rebuild links `WiiCompiled.exe`.
|
||||
- Android: the cross-compile audit passes. `mkw_android_native_compile`,
|
||||
`aurora_core` and `aurora_gx` all build for `aarch64-none-linux-android29`
|
||||
with NDK 29.0.14206865, which covers the whole native runtime including
|
||||
`openxr_vulkan.cpp`, `openxr_android.cpp`, `openxr_input.cpp` and the Aurora
|
||||
AHardwareBuffer bridge. Three Bionic portability fixes came out of it: the
|
||||
`std::min` call in `hle/audio/audio.cpp` needed an explicit type (`int64_t`
|
||||
is `long` on LP64 Android while the clock rep is `long long`),
|
||||
`guest_flat_memory.cpp` needs a `__NR_memfd_create` shim below API 30, and
|
||||
Crypto++'s `cpu.cpp` needs the NDK's `cpu-features` source compiled in.
|
||||
- **Device, 2026-09-16: running on a Quest 3** (HorizonOS 14, API 34). The
|
||||
runtime negotiates `XR_KHR_vulkan_enable2` (Vulkan 1.0 to 1.2), creates
|
||||
1680x1760 `R8G8B8A8_SRGB` (VkFormat 43) swapchains, attaches the controller
|
||||
actions, and the session reaches `FOCUSED`. The game boots through the title
|
||||
movies into the attract race, the policy switches to `immersive-race` with
|
||||
all 189 perspective draws replayed per eye, the first projection layer is
|
||||
submitted, and the menus return to the virtual screen. No WebGPU or OpenXR
|
||||
errors over a 90 second session.
|
||||
|
||||
Bring-up fixes that only a device could reveal:
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
| --- | --- | --- |
|
||||
| Activity crashed with `EACCES` on `Config.toml` | `adb shell mkdir` had created the app's data directory, so the shell user owned it | Let the app create its own directory; `Run-Quest.ps1` launches once before placing DATA |
|
||||
| DATA unreadable by the game | adb-placed files stay owned by the shell user and directories are `2770` | `chmod -R a+rX DATA` as the owning shell user; `run-as` cannot reach shared storage (SELinux) |
|
||||
| "Cannot persist NAND setting.txt" | FUSE storage has no hard links; `link()` fails with `EACCES`, not `EPERM` | Android falls back to exists-check plus `rename` in `nand_settings.h` |
|
||||
| `SharedFence ... signaled value (0) was not 1` | A sync fd is binary; Dawn expects value 1 | `vulkan_interop.cpp` passes 1 |
|
||||
| Link error on `Android_LockActivityMutex` | SDL's activity mutex is not exported | Aurora-owned mutex plus the `QuestSurface` bracket (above) |
|
||||
| Crypto++ `cpu-features.h` not found | The NDK ships cpu-features as source | Compiled into `mkw_cryptopp` on Android |
|
||||
| Exploded racers and menu characters; smeared movie panels in the menus; then, once those were fixed, damaged eyes and slightly misplaced detail on characters | The Adreno 740 driver reads the wrong bytes when the shader multiplies an index by a stride that is not a multiple of 4. That covers the vertex fetch (`ubuf.vtx_start + vidx * stride + offset`) and indexed array reads (`array_start + index * stride`, e.g. 6-byte S16 normals). GX packs both byte-tight, so skinned models (a 1-byte `PNMTXIDX` first, stride 7) broke everywhere | Android pads every uploaded vertex and every indexed-array element to a 4-byte stride (`padded_upload_stride` in `lib/gx/gx.cpp`). Offsets inside a vertex or element are unchanged, and desktop is unchanged. **Fixed, headset-verified 2026-09-16** at character select and a Grand Prix start |
|
||||
|
||||
How the explosion was isolated, so the next Adreno rendering bug starts further
|
||||
ahead:
|
||||
|
||||
- The CPU side was identical to Windows: a per-draw audit of palette indices
|
||||
and matrices matched byte for byte. Menus reach the headset as the mono
|
||||
desktop image, so stereo replay was not involved either.
|
||||
- Shader-side rewrites did **not** help and were removed: constant-index palette
|
||||
matrix selection, shift-free sign extension, replacing `extractBits`, and
|
||||
byte helpers rewritten with constant shifts or integer division. The last two
|
||||
made menus worse, which is what pointed away from any one helper.
|
||||
- Moving every attribute to a 4-byte boundary on the CPU fixed the explosion.
|
||||
Padding only the stride, with offsets still packed, fixed it just as well,
|
||||
which narrows the fault to the `vidx * stride` term. Mario's eyes stayed
|
||||
wrong under both, until indexed arrays got the same element padding. That
|
||||
combined padding is the shipped fix. It costs one copy per vertex and per
|
||||
array element. Peak uploads at a 12-racer race start were about 570 KB of
|
||||
the 3 MB vertex buffer and 710 KB of the 8 MB storage buffer.
|
||||
- KartPad's Android reports of corrupted drivers on Adreno 750 match this
|
||||
symptom. That is plausible but not tested.
|
||||
|
||||
Diagnostics that stay in the build, all read once at launch from system
|
||||
properties. Set them with `adb shell setprop <name> <value>` before starting
|
||||
the app:
|
||||
|
||||
| Property | Effect |
|
||||
| --- | --- |
|
||||
| `debug.wiicompiled.vtxpad 0` | Turns the stride padding off, to re-check a driver update |
|
||||
| `debug.wiicompiled.validation 1` | Keeps WebGPU validation and robustness on in release builds |
|
||||
| `debug.wiicompiled.inject <n>:<button>` | Presses `a`, `b`, `x`, `y`, `start`, `up`, `down`, `left` or `right` for 12 XR frames each time `<n>` changes |
|
||||
|
||||
The injector makes headset tests possible with nobody wearing the headset.
|
||||
Keep the display awake, drive the menus, then take a compositor screenshot:
|
||||
|
||||
```powershell
|
||||
adb shell am broadcast -a com.oculus.vrpowermanager.prox_close
|
||||
adb shell setprop debug.wiicompiled.inject 1:a # title -> licence; bump the number per press
|
||||
adb shell am startservice -n com.oculus.metacam/.capture.CaptureService -a TAKE_SCREENSHOT
|
||||
adb pull /sdcard/Oculus/Screenshots/<newest>.jpg
|
||||
```
|
||||
|
||||
From a cold start, five `a` presses about 5 s apart, starting once the title
|
||||
screen is up, reach Grand Prix character select. A value left over from an
|
||||
earlier run is ignored on the first read. Presses only land while the XR
|
||||
session is `FOCUSED`.
|
||||
|
||||
Open measurement: the attract race advanced 603 game frames in about 14 s,
|
||||
roughly 43 FPS against the game's 60, with an optimized build (`-O3`,
|
||||
translated code `-O2`, `-mcpu=cortex-a77`). Profiling on the XR2 Gen 2 is the
|
||||
first performance task.
|
||||
|
||||
Verified on device since: the menus on the virtual screen, controller input
|
||||
(the user has driven races), and an immersive Grand Prix start with all 12
|
||||
racers rendering correctly. Not yet verified: stereo comfort and scale,
|
||||
lifecycle (Quest menu, guardian, sleep), and a full race to the finish.
|
||||
When diagnosing a new device, the session log should show, in order: the loader log line
|
||||
(`OpenXR Android loader initialized`), the requirements line (which binding
|
||||
extension was negotiated), `OpenXR Vulkan swapchains ready`, the session
|
||||
state reaching `FOCUSED`, `presentation=virtual-screen` for the menus, and
|
||||
`first immersive packet consumed` on race entry. A black headset with a working
|
||||
Android mirror points at the AHardwareBuffer copy (check for `vkImportSemaphoreFdKHR`
|
||||
or `EndAccess` errors); a black mirror too points at Aurora itself.
|
||||
|
||||
## Known gaps and next steps
|
||||
|
||||
- **Device bring-up.** Run on a Quest 3, capture logcat, fix what the runtime
|
||||
rejects. Likely first candidates: the exact `XR_KHR_vulkan_enable2` device
|
||||
extension negotiation, Dawn's begin/end layout reporting for AHardwareBuffer
|
||||
imports, and swapchain format choice (`R8G8B8A8_SRGB` is expected).
|
||||
- **Performance.** The desktop product targets x86-64-v3; nothing has been
|
||||
profiled on the XR2. Expect shader compilation stalls on first run (Aurora's
|
||||
pipeline cache is bundled) and start with `render_scale` below 1.0 if the
|
||||
compositor reports missed frames. `XR_FB_foveation` is not used yet.
|
||||
- **Lifecycle.** Backgrounding (the Quest menu, guardian) pauses the session
|
||||
through the ordinary `STOPPING`/`READY` events; SDL's Android surface loss is
|
||||
handled by Aurora's existing Android paths. Neither has been exercised.
|
||||
- **Input.** D-pad (trick inputs) is not bound; remap in `Config.toml` or bind
|
||||
the thumbstick directions in a follow-up. Haptics are wired but nothing calls
|
||||
them yet.
|
||||
- **Retro Rewind on device** needs the mod's translation and its extracted
|
||||
content pushed next to `DATA`, exactly like the desktop product.
|
||||
- **Release signing and store packaging** are out of scope; `Build-Quest.ps1`
|
||||
produces debug-signed APKs for sideloading.
|
||||
+80
-6
@@ -13,9 +13,15 @@ elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64)$")
|
||||
set(MKW_PLATFORM_MACOS TRUE)
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$")
|
||||
set(MKW_PLATFORM_LINUX TRUE)
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Android" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
|
||||
# Android arm64 through the NDK toolchain file (Meta Quest standalone).
|
||||
# Bionic is Linux underneath, so the libco fibers, the mmap guest memory
|
||||
# and the POSIX networking all follow the Linux branch; only paths, the
|
||||
# SDL entry point and the OpenXR/Vulkan binding are Android-specific.
|
||||
set(MKW_PLATFORM_ANDROID TRUE)
|
||||
else()
|
||||
message(FATAL_ERROR
|
||||
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS arm64")
|
||||
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, Apple Clang on macOS arm64, or the Android NDK on arm64-v8a")
|
||||
endif()
|
||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
message(FATAL_ERROR "WiiCompiled only supports Release builds")
|
||||
@@ -66,7 +72,7 @@ set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF)
|
||||
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
|
||||
# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers
|
||||
# and macOS uses the project's x18-safe AArch64 assembly backend, so this target is Linux-only.
|
||||
if(MKW_PLATFORM_LINUX)
|
||||
if(MKW_PLATFORM_LINUX OR MKW_PLATFORM_ANDROID)
|
||||
add_library(mkw_libco STATIC third_party/libco/libco.c)
|
||||
add_library(mkw::libco ALIAS mkw_libco)
|
||||
target_include_directories(mkw_libco PUBLIC third_party/libco)
|
||||
@@ -98,9 +104,24 @@ if(NOT MKW_NATIVE_PREBUILT_DIR)
|
||||
"/(adhoc|bench[123]|datatest|dlltest|fipsalgt|fipstest|pch|regtest[1234]|simple|test|validat[0-9]+)\\.cpp$")
|
||||
list(FILTER MKW_CRYPTOPP_SOURCES EXCLUDE REGEX
|
||||
"(_avx|_ppc|_simd|_sse)\\.cpp$")
|
||||
if(MKW_PLATFORM_ANDROID)
|
||||
# cpu.cpp reads the ARM feature bits through the NDK's cpu-features
|
||||
# library, which the NDK ships as source rather than a prebuilt (see the
|
||||
# comment above its #include "cpu-features.h"). Compiling it here is the
|
||||
# arrangement upstream's own setenv-android.sh sets up.
|
||||
set(MKW_ANDROID_CPU_FEATURES_DIR "${CMAKE_ANDROID_NDK}/sources/android/cpufeatures")
|
||||
if(NOT EXISTS "${MKW_ANDROID_CPU_FEATURES_DIR}/cpu-features.c")
|
||||
message(FATAL_ERROR
|
||||
"Crypto++ needs the NDK cpu-features source: ${MKW_ANDROID_CPU_FEATURES_DIR}")
|
||||
endif()
|
||||
list(APPEND MKW_CRYPTOPP_SOURCES "${MKW_ANDROID_CPU_FEATURES_DIR}/cpu-features.c")
|
||||
endif()
|
||||
add_library(mkw_cryptopp STATIC ${MKW_CRYPTOPP_SOURCES})
|
||||
add_library(mkw::cryptopp ALIAS mkw_cryptopp)
|
||||
target_include_directories(mkw_cryptopp SYSTEM PUBLIC third_party)
|
||||
if(MKW_PLATFORM_ANDROID)
|
||||
target_include_directories(mkw_cryptopp SYSTEM PRIVATE "${MKW_ANDROID_CPU_FEATURES_DIR}")
|
||||
endif()
|
||||
target_compile_features(mkw_cryptopp PUBLIC cxx_std_17)
|
||||
target_compile_definitions(mkw_cryptopp PUBLIC ${MKW_CRYPTOPP_INTERFACE_DEFINITIONS})
|
||||
target_compile_options(mkw_cryptopp PRIVATE -w)
|
||||
@@ -117,7 +138,7 @@ set(MKW_CPPWINRT_INCLUDE_DIR "" CACHE PATH "Optional self-contained C++/WinRT in
|
||||
# available to explicit developer builds without making a nonfunctional SDK
|
||||
# fetch part of every normal build.
|
||||
set(MKW_ENABLE_OPENXR_DEFAULT OFF)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
if(MKW_PLATFORM_WINDOWS OR MKW_PLATFORM_ANDROID)
|
||||
set(MKW_ENABLE_OPENXR_DEFAULT ON)
|
||||
endif()
|
||||
option(MKW_ENABLE_OPENXR "Build OpenXR VR support" ${MKW_ENABLE_OPENXR_DEFAULT})
|
||||
@@ -149,6 +170,18 @@ else()
|
||||
if(NOT EXISTS ${MKW_AURORA_DIR}/CMakeLists.txt)
|
||||
message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing")
|
||||
endif()
|
||||
if(MKW_PLATFORM_ANDROID)
|
||||
# encounter/dawn-build publishes the same release for android-aarch64;
|
||||
# the provider pins its digest and maps the asset name. SDL3 is built
|
||||
# from source as a shared library because SDLActivity loads libSDL3.so
|
||||
# before libmain.so; a Gradle build may instead pass
|
||||
# -DAURORA_SDL3_PROVIDER=system to use the prefab from the SDL3 AAR.
|
||||
set(AURORA_DAWN_PROVIDER package CACHE STRING "" FORCE)
|
||||
set(AURORA_SDL3_LINKAGE shared CACHE STRING "" FORCE)
|
||||
if(NOT AURORA_SDL3_PROVIDER OR AURORA_SDL3_PROVIDER STREQUAL "auto")
|
||||
set(AURORA_SDL3_PROVIDER vendor CACHE STRING "" FORCE)
|
||||
endif()
|
||||
endif()
|
||||
set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE)
|
||||
@@ -190,8 +223,13 @@ else()
|
||||
# should add it to the Dawn targets the same way.
|
||||
|
||||
# Keep SDL3 lean when aurora is enabled.
|
||||
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
|
||||
set(SDL_STATIC ON CACHE BOOL "" FORCE)
|
||||
if(MKW_PLATFORM_ANDROID)
|
||||
set(SDL_SHARED ON CACHE BOOL "" FORCE)
|
||||
set(SDL_STATIC OFF CACHE BOOL "" FORCE)
|
||||
else()
|
||||
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
|
||||
set(SDL_STATIC ON CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
set(SDL_TEST OFF CACHE BOOL "" FORCE)
|
||||
|
||||
# MinGW/WSL can choke on SDL precompiled headers; disable PCH for aurora/SDL.
|
||||
@@ -250,7 +288,7 @@ endif()
|
||||
|
||||
set(MKW_OPENXR_TARGET "")
|
||||
if(MKW_ENABLE_OPENXR)
|
||||
if(NOT MKW_PLATFORM_WINDOWS AND NOT MKW_PLATFORM_LINUX)
|
||||
if(NOT MKW_PLATFORM_WINDOWS AND NOT MKW_PLATFORM_LINUX AND NOT MKW_PLATFORM_ANDROID)
|
||||
message(WARNING "OpenXR is not wired for this platform; disabling MKW_ENABLE_OPENXR")
|
||||
set(MKW_ENABLE_OPENXR OFF)
|
||||
else()
|
||||
@@ -332,6 +370,15 @@ set_target_properties(mkw_platform PROPERTIES UNITY_BUILD OFF)
|
||||
|
||||
# Keep these independent from Aurora's BUILD_TESTING option: they validate the
|
||||
# project's host-platform contracts, not Aurora's third-party test suite.
|
||||
# A cross-compiled Android tree cannot run them, so it does not build them.
|
||||
if(MKW_PLATFORM_ANDROID)
|
||||
set(MKW_BUILD_TESTS_DEFAULT OFF)
|
||||
else()
|
||||
set(MKW_BUILD_TESTS_DEFAULT ON)
|
||||
endif()
|
||||
option(MKW_BUILD_TESTS "Build the runtime host-contract test executables" ${MKW_BUILD_TESTS_DEFAULT})
|
||||
unset(MKW_BUILD_TESTS_DEFAULT)
|
||||
if(MKW_BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_executable(mkw_frame_interpolation_pacing_tests tests/frame_interpolation_pacing_tests.cpp)
|
||||
target_include_directories(mkw_frame_interpolation_pacing_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
@@ -443,6 +490,7 @@ if(MKW_PLATFORM_MACOS)
|
||||
target_compile_features(mkw_macos_guest_flat_memory_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests)
|
||||
endif()
|
||||
endif() # MKW_BUILD_TESTS
|
||||
|
||||
# The translator emits the complete, content-addressed source graph. Consuming
|
||||
# this one manifest keeps configure independent of the 28k generated function
|
||||
@@ -493,6 +541,32 @@ else()
|
||||
mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
|
||||
endif()
|
||||
if(MKW_PLATFORM_ANDROID)
|
||||
# Compile-only audit of the native runtime for arm64 Android, including
|
||||
# the OpenXR Vulkan backend and the Android platform glue, without a
|
||||
# translated game. Same exclusions as the macOS audit: those two files
|
||||
# include the translator-emitted RuntimeConfig.h.
|
||||
set(MKW_ANDROID_NATIVE_AUDIT_SOURCES ${SOURCES})
|
||||
list(REMOVE_ITEM MKW_ANDROID_NATIVE_AUDIT_SOURCES
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/abi_bridge.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/hle/os/os_alarm.cpp")
|
||||
add_library(mkw_android_native_compile OBJECT ${MKW_ANDROID_NATIVE_AUDIT_SOURCES})
|
||||
target_include_directories(mkw_android_native_compile PRIVATE
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include" "${CMAKE_CURRENT_LIST_DIR}/src"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/.." "${CMAKE_CURRENT_LIST_DIR}/../aurora-main/include")
|
||||
target_compile_features(mkw_android_native_compile PRIVATE cxx_std_20)
|
||||
target_compile_definitions(mkw_android_native_compile PRIVATE TARGET_PC)
|
||||
target_link_libraries(mkw_android_native_compile PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
|
||||
mkw::pugixml mkw::toml11 mkw::cryptopp mkw::libco)
|
||||
if(MKW_ENABLE_OPENXR)
|
||||
target_link_libraries(mkw_android_native_compile PRIVATE ${MKW_OPENXR_TARGET})
|
||||
endif()
|
||||
# Same precompiled header as the product objects: several sources rely on
|
||||
# it for abi_bridge.h / ppc_runtime.h rather than including them directly.
|
||||
target_precompile_headers(mkw_android_native_compile PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include/mkw_pch.h")
|
||||
set_target_properties(mkw_android_native_compile PROPERTIES UNITY_BUILD OFF)
|
||||
endif()
|
||||
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
|
||||
message(STATUS "Translated product targets disabled (MKW_BUILD_PRODUCTS=OFF)")
|
||||
endif()
|
||||
@@ -4,15 +4,38 @@
|
||||
# functions are compiled once into mkw_base_shared; only callers whose direct
|
||||
# ABI differs between profiles receive small base/RR variants.
|
||||
|
||||
set(DATA_INIT_FILE "${MKW_RUNTIME_SOURCE_DIR}/../generated/data_sections_init.cpp")
|
||||
set(DATA_INIT_BLOB_ASM "${MKW_RUNTIME_SOURCE_DIR}/../generated/data_sections_init_blobs.S")
|
||||
# The translator's output tree. It sits next to runtime/ in the installer's
|
||||
# build workspace; a build configured straight from a checkout (the Android
|
||||
# app, the test build dirs) names the workspace copy instead.
|
||||
set(MKW_GENERATED_DIR "${MKW_RUNTIME_SOURCE_DIR}/../generated" CACHE PATH
|
||||
"Translator output directory holding data_sections_init.cpp and RuntimeConfig.h")
|
||||
get_filename_component(MKW_GENERATED_PARENT_DIR "${MKW_GENERATED_DIR}" DIRECTORY)
|
||||
|
||||
set(DATA_INIT_FILE "${MKW_GENERATED_DIR}/data_sections_init.cpp")
|
||||
set(DATA_INIT_BLOB_ASM "${MKW_GENERATED_DIR}/data_sections_init_blobs.S")
|
||||
if(MKW_PLATFORM_ANDROID AND EXISTS "${DATA_INIT_BLOB_ASM}")
|
||||
# The Windows installer generates the blob assembly for PE/COFF. The payload
|
||||
# is byte-identical on every platform (the .S only wraps .incbin
|
||||
# directives), so rewrite the section syntax for ELF here rather than
|
||||
# requiring a second translation pass for the Quest build. A tree generated
|
||||
# with `generate-data-init --target-os android` passes through unchanged.
|
||||
file(READ "${DATA_INIT_BLOB_ASM}" MKW_ANDROID_BLOB_ASM_TEXT)
|
||||
string(REPLACE ".section .rdata,\"dr\"" ".section .rodata,\"a\",@progbits"
|
||||
MKW_ANDROID_BLOB_ASM_ELF "${MKW_ANDROID_BLOB_ASM_TEXT}")
|
||||
if(NOT MKW_ANDROID_BLOB_ASM_ELF STREQUAL MKW_ANDROID_BLOB_ASM_TEXT)
|
||||
string(APPEND MKW_ANDROID_BLOB_ASM_ELF "\n.section .note.GNU-stack,\"\",@progbits\n")
|
||||
set(DATA_INIT_BLOB_ASM "${CMAKE_CURRENT_BINARY_DIR}/data_sections_init_blobs_android.S")
|
||||
file(WRITE "${DATA_INIT_BLOB_ASM}" "${MKW_ANDROID_BLOB_ASM_ELF}")
|
||||
message(STATUS "Rewrote the PE/COFF blob assembly for ELF: ${DATA_INIT_BLOB_ASM}")
|
||||
endif()
|
||||
endif()
|
||||
if(EXISTS "${DATA_INIT_FILE}")
|
||||
list(APPEND SOURCES "${DATA_INIT_FILE}")
|
||||
endif()
|
||||
# Crash-report symbolization table emitted by generate-data-init. The stub
|
||||
# (deliberately outside the globbed src/ tree so it is never picked up twice)
|
||||
# keeps link succeeding when the generated table has not been produced yet.
|
||||
set(GUEST_SYMBOL_TABLE_FILE "${MKW_RUNTIME_SOURCE_DIR}/../generated/guest_symbol_table.cpp")
|
||||
set(GUEST_SYMBOL_TABLE_FILE "${MKW_GENERATED_DIR}/guest_symbol_table.cpp")
|
||||
if(EXISTS "${GUEST_SYMBOL_TABLE_FILE}")
|
||||
list(APPEND SOURCES "${GUEST_SYMBOL_TABLE_FILE}")
|
||||
else()
|
||||
@@ -45,6 +68,7 @@ function(mkw_configure_object_target target)
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/src"
|
||||
# Workspace root, so translator output is spelled "generated/<x>.h"
|
||||
# instead of a ../ chain whose depth depends on the includer.
|
||||
"${MKW_GENERATED_PARENT_DIR}"
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/.."
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/../aurora-main/include")
|
||||
target_compile_definitions(${target} PRIVATE
|
||||
@@ -76,8 +100,10 @@ endfunction()
|
||||
add_library(mkw_runtime_common OBJECT ${SOURCES})
|
||||
mkw_configure_object_target(mkw_runtime_common)
|
||||
target_compile_features(mkw_runtime_common PRIVATE cxx_std_20)
|
||||
# On Android SDL owns the entry point: main.cpp includes SDL_main.h so that
|
||||
# SDLActivity's nativeRunMain finds SDL_main inside libmain.so.
|
||||
target_compile_definitions(mkw_runtime_common PRIVATE
|
||||
SDL_MAIN_HANDLED
|
||||
$<$<NOT:$<PLATFORM_ID:Android>>:SDL_MAIN_HANDLED>
|
||||
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
@@ -91,6 +117,10 @@ elseif(MKW_PLATFORM_LINUX)
|
||||
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
|
||||
# media monitoring). Empty string on glibc >= 2.34 where dl* is in libc.
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS})
|
||||
elseif(MKW_PLATFORM_ANDROID)
|
||||
# libvulkan for the OpenXR-side device, android/log for AHardwareBuffer,
|
||||
# JNI and logcat.
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco android log vulkan ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
if(MKW_CPPWINRT_INCLUDE_DIR)
|
||||
if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
|
||||
@@ -167,6 +197,26 @@ if(MKW_HAVE_RETRO_REWIND)
|
||||
target_precompile_headers(mkw_retro_sensitive REUSE_FROM mkw_base_shared)
|
||||
endif()
|
||||
|
||||
if(MKW_PLATFORM_ANDROID)
|
||||
# Same PE/COFF-to-ELF rewrite as the base data blobs above.
|
||||
set(MKW_ANDROID_RETRO_EXTRA_SOURCES)
|
||||
foreach(source IN LISTS MKW_RETRO_EXTRA_SOURCES)
|
||||
if(source MATCHES "\\.S$")
|
||||
file(READ "${source}" MKW_ANDROID_RETRO_BLOB_TEXT)
|
||||
string(REPLACE ".section .rdata,\"dr\"" ".section .rodata,\"a\",@progbits"
|
||||
MKW_ANDROID_RETRO_BLOB_ELF "${MKW_ANDROID_RETRO_BLOB_TEXT}")
|
||||
if(NOT MKW_ANDROID_RETRO_BLOB_ELF STREQUAL MKW_ANDROID_RETRO_BLOB_TEXT)
|
||||
string(APPEND MKW_ANDROID_RETRO_BLOB_ELF "\n.section .note.GNU-stack,\"\",@progbits\n")
|
||||
get_filename_component(source_name "${source}" NAME_WE)
|
||||
set(source "${CMAKE_CURRENT_BINARY_DIR}/${source_name}_android.S")
|
||||
file(WRITE "${source}" "${MKW_ANDROID_RETRO_BLOB_ELF}")
|
||||
endif()
|
||||
endif()
|
||||
list(APPEND MKW_ANDROID_RETRO_EXTRA_SOURCES "${source}")
|
||||
endforeach()
|
||||
set(MKW_RETRO_EXTRA_SOURCES ${MKW_ANDROID_RETRO_EXTRA_SOURCES})
|
||||
endif()
|
||||
|
||||
set(MKW_RETRO_TRANSLATED_SOURCES ${MKW_RETRO_MOD_SHARDS} ${MKW_RETRO_EXTRA_SOURCES})
|
||||
set(MKW_RETRO_BLOB_OBJECTS)
|
||||
foreach(source IN LISTS MKW_RETRO_EXTRA_SOURCES)
|
||||
@@ -192,10 +242,12 @@ function(mkw_configure_product target)
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/src"
|
||||
# Workspace root, so translator output is spelled "generated/<x>.h"
|
||||
# instead of a ../ chain whose depth depends on the includer.
|
||||
"${MKW_GENERATED_PARENT_DIR}"
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/.."
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/../aurora-main/include")
|
||||
target_compile_definitions(${target} PRIVATE
|
||||
SDL_MAIN_HANDLED _DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION TARGET_PC)
|
||||
$<$<NOT:$<PLATFORM_ID:Android>>:SDL_MAIN_HANDLED>
|
||||
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION TARGET_PC)
|
||||
target_compile_features(${target} PRIVATE cxx_std_20)
|
||||
mkw_apply_common_compile_options(${target})
|
||||
# The dispatch-table and registration shards compile inside the product target itself and
|
||||
@@ -244,6 +296,8 @@ function(mkw_configure_product target)
|
||||
# here for the same reason: music_attenuation.cpp's dlopen(libdbus-1) lives in those
|
||||
# objects (empty string on glibc >= 2.34, where dl* is in libc).
|
||||
target_link_libraries(${target} PRIVATE mkw::libco ${CMAKE_DL_LIBS})
|
||||
elseif(MKW_PLATFORM_ANDROID)
|
||||
target_link_libraries(${target} PRIVATE mkw::libco android log vulkan ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
foreach(runtime_dll libc++.dll libunwind.dll)
|
||||
@@ -296,7 +350,15 @@ function(mkw_configure_product target)
|
||||
"$<TARGET_FILE_DIR:${target}>/initial_pipeline_cache.db")
|
||||
endfunction()
|
||||
|
||||
add_executable(WiiCompiled "${MKW_BASE_PRODUCT_SOURCE}" ${MKW_BASE_REGISTRATION_SOURCES})
|
||||
# Android ships each product as the shared library SDLActivity loads; the base
|
||||
# game keeps SDL's conventional "main" name and Retro Rewind gets its own so a
|
||||
# dual-flavour app can bundle both without a name clash.
|
||||
if(MKW_PLATFORM_ANDROID)
|
||||
add_library(WiiCompiled SHARED "${MKW_BASE_PRODUCT_SOURCE}" ${MKW_BASE_REGISTRATION_SOURCES})
|
||||
set_target_properties(WiiCompiled PROPERTIES OUTPUT_NAME main)
|
||||
else()
|
||||
add_executable(WiiCompiled "${MKW_BASE_PRODUCT_SOURCE}" ${MKW_BASE_REGISTRATION_SOURCES})
|
||||
endif()
|
||||
mkw_configure_product(WiiCompiled)
|
||||
target_precompile_headers(WiiCompiled PRIVATE
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
|
||||
@@ -305,7 +367,12 @@ if(TARGET mkw_base_sensitive)
|
||||
endif()
|
||||
|
||||
if(MKW_HAVE_RETRO_REWIND)
|
||||
add_executable(RetroRewind "${MKW_RETRO_REWIND_PRODUCT_SOURCE}" ${MKW_RETRO_REGISTRATION_SOURCES})
|
||||
if(MKW_PLATFORM_ANDROID)
|
||||
add_library(RetroRewind SHARED "${MKW_RETRO_REWIND_PRODUCT_SOURCE}" ${MKW_RETRO_REGISTRATION_SOURCES})
|
||||
set_target_properties(RetroRewind PROPERTIES OUTPUT_NAME main_retro_rewind)
|
||||
else()
|
||||
add_executable(RetroRewind "${MKW_RETRO_REWIND_PRODUCT_SOURCE}" ${MKW_RETRO_REGISTRATION_SOURCES})
|
||||
endif()
|
||||
mkw_configure_product(RetroRewind)
|
||||
target_precompile_headers(RetroRewind REUSE_FROM WiiCompiled)
|
||||
if(TARGET mkw_retro_sensitive)
|
||||
@@ -327,6 +394,11 @@ endif()
|
||||
# tuning rather than leaving target-specific performance on the table.
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3)
|
||||
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.
|
||||
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)$")
|
||||
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
|
||||
else()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "settings_overlay.h"
|
||||
#include "runtime_config.h"
|
||||
#include "fiber_manager.h"
|
||||
|
||||
#include <aurora/aurora.h>
|
||||
#include <aurora/event.h>
|
||||
@@ -136,7 +137,30 @@ inline bool BeginAuroraFrame() {
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
inline std::atomic_bool g_androidAuroraPollPending{false};
|
||||
#endif
|
||||
|
||||
// Poll Aurora events and update cached window/framebuffer dimensions.
|
||||
inline void UpdateAuroraAndProcessEvents() {
|
||||
#if defined(__ANDROID__)
|
||||
// aurora_update() pumps SDL, which can call into Java; ART only tolerates
|
||||
// that on the thread's real stack. A guest thread on its own fiber stack
|
||||
// leaves the poll for the scheduler to service after the next switch.
|
||||
if (!Fiber::GuestFiberManager::IsOnSchedulerFiber()) {
|
||||
g_androidAuroraPollPending.store(true, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
g_androidAuroraPollPending.store(false, std::memory_order_release);
|
||||
#endif
|
||||
ProcessAuroraEvents(aurora_update());
|
||||
}
|
||||
|
||||
inline void ServicePendingAuroraEventsOnScheduler() {
|
||||
#if defined(__ANDROID__)
|
||||
if (Fiber::GuestFiberManager::IsOnSchedulerFiber() &&
|
||||
g_androidAuroraPollPending.exchange(false, std::memory_order_acq_rel)) {
|
||||
ProcessAuroraEvents(aurora_update());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -59,6 +59,11 @@ public:
|
||||
|
||||
// Check if fiber system is initialized
|
||||
static bool IsInitialized();
|
||||
|
||||
// True when the caller runs on the scheduler's own host context, i.e. the
|
||||
// original thread stack (also true before the fiber system exists). Code
|
||||
// that may enter Java through JNI on Android must only run there.
|
||||
static bool IsOnSchedulerFiber();
|
||||
|
||||
// Create a fiber for a guest thread (called from OSCreateThread HLE).
|
||||
// OSCreateThread's stackSize/priority are not passed: the host fiber models
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
@@ -202,6 +204,17 @@ inline bool Ensure(const std::filesystem::path& root, std::string& error,
|
||||
published = MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_WRITE_THROUGH) != 0;
|
||||
#else
|
||||
published = ::link(temporary.c_str(), path.c_str()) == 0;
|
||||
#if defined(__ANDROID__)
|
||||
// Android's shared storage (the FUSE-backed Android/data tree) has no
|
||||
// hard links; on a Quest 3 link() fails with EACCES, not EPERM, so the
|
||||
// errno is not a usable signal. The no-replace publish exists for
|
||||
// competing desktop launchers; an Android app runs one process, so an
|
||||
// existence check followed by an atomic rename keeps the "never
|
||||
// overwrite" guarantee. A real permission problem fails the rename too.
|
||||
if (!published && !std::filesystem::exists(path)) {
|
||||
published = ::rename(temporary.c_str(), path.c_str()) == 0;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
std::filesystem::remove(temporary, ec);
|
||||
|
||||
@@ -131,7 +131,9 @@ inline std::filesystem::path PathFromUtf8(std::string_view text) {
|
||||
}
|
||||
|
||||
inline constexpr const char* kConfigFileName = "Config.toml";
|
||||
#ifdef _WIN32
|
||||
#if defined(_WIN32) || defined(__ANDROID__)
|
||||
// The Quest activity (android/.../QuestActivity.kt) creates this directory
|
||||
// under the app's external files dir and writes Config.toml into it.
|
||||
inline constexpr const char* kApplicationDirectoryName = "WiiCompiledOpenXRVR";
|
||||
#else
|
||||
inline constexpr const char* kApplicationDirectoryName = "WiiCompiled";
|
||||
@@ -269,7 +271,10 @@ inline std::optional<std::filesystem::path> ExecutableDirectory() {
|
||||
}
|
||||
buffer.resize(buffer.size() * 2);
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
#elif defined(__APPLE__) || defined(__ANDROID__)
|
||||
// Android has no executable next to which assets could sit; the platform
|
||||
// layer answers with the directory the activity unpacked the bundled
|
||||
// runtime resources into, so the adjacent-file lookups below keep working.
|
||||
return RuntimePlatform::ExecutableDirectory();
|
||||
#else
|
||||
// /proc/self/exe is a Linux-specific magic symlink to the running executable; readlink()
|
||||
@@ -328,7 +333,7 @@ inline std::filesystem::path ApplicationDataDirectory() {
|
||||
CoTaskMemFree(rawPath);
|
||||
return directory;
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
#elif defined(__APPLE__) || defined(__ANDROID__)
|
||||
return RuntimePlatform::ApplicationDataDirectory(kApplicationDirectoryName);
|
||||
#else
|
||||
// XDG Base Directory spec equivalent of FOLDERID_LocalAppData: $XDG_DATA_HOME if set and
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#if defined(MKW_ENABLE_OPENXR) && defined(__ANDROID__)
|
||||
|
||||
#include "vr/openxr_runtime.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace mkw::vr {
|
||||
|
||||
// Android-specific OpenXR bootstrap.
|
||||
//
|
||||
// The Khronos loader on Android must be told which JavaVM and Context it lives
|
||||
// in before any other OpenXR entry point is called (xrInitializeLoaderKHR), and
|
||||
// xrCreateInstance must chain an XrInstanceCreateInfoAndroidKHR naming the
|
||||
// activity. Both come from SDL's Android glue (the SDLActivity that hosts the
|
||||
// runtime), so no extra JNI surface is needed. DolphinXR found that the loader
|
||||
// context must be the *activity*, not the application context: with a plain
|
||||
// application context the Quest runtime never leaves XR_SESSION_STATE_IDLE.
|
||||
//
|
||||
// Call order: OpenXRAndroidInitializeLoader() once, before OpenXRRuntime::Initialize;
|
||||
// then pass OpenXRAndroidInstanceCreateNext() as OpenXRConfig::instance_create_next.
|
||||
bool OpenXRAndroidInitializeLoader(OpenXRLogCallback logger, std::string* error);
|
||||
const void* OpenXRAndroidInstanceCreateNext();
|
||||
|
||||
// Optional XR_KHR_android_thread_settings hint for the calling thread. Failure
|
||||
// is not an error: some Quest runtime builds advertise the extension but reject
|
||||
// particular thread types, so the caller only logs the outcome.
|
||||
enum class OpenXRAndroidThreadType {
|
||||
ApplicationMain,
|
||||
ApplicationWorker,
|
||||
RendererMain,
|
||||
RendererWorker,
|
||||
};
|
||||
bool OpenXRAndroidRegisterThread(OpenXRRuntime& runtime, OpenXRAndroidThreadType type);
|
||||
|
||||
} // namespace mkw::vr
|
||||
|
||||
#endif // defined(MKW_ENABLE_OPENXR) && defined(__ANDROID__)
|
||||
@@ -0,0 +1,66 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#if defined(MKW_ENABLE_OPENXR)
|
||||
|
||||
#include "vr/openxr_runtime.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace mkw::vr {
|
||||
|
||||
// Backend-neutral frame vocabulary shared by every graphics binding.
|
||||
//
|
||||
// The OpenXR pacing thread in openxr_integration.cpp is written against these
|
||||
// types and the method surface documented on OpenXRD3D12Backend; each concrete
|
||||
// backend (D3D12 on Windows, Vulkan on Android) implements that same surface so
|
||||
// the pacing, retained-layer and policy logic is compiled once for both.
|
||||
|
||||
enum class OpenXRFrameMode {
|
||||
ImmersiveProjection,
|
||||
VirtualScreen,
|
||||
};
|
||||
|
||||
enum class OpenXRBeginStatus {
|
||||
Ready,
|
||||
SessionNotRunning,
|
||||
ExitRequested,
|
||||
Error,
|
||||
};
|
||||
|
||||
enum class OpenXRSubmissionStatus {
|
||||
Success,
|
||||
Failed,
|
||||
Timeout,
|
||||
ShuttingDown,
|
||||
};
|
||||
|
||||
struct OpenXRPresentation {
|
||||
OpenXRFrameMode mode = OpenXRFrameMode::ImmersiveProjection;
|
||||
|
||||
// Used only by VirtualScreen.
|
||||
float quad_distance_meters = 2.0f;
|
||||
float quad_width_meters = 2.4f;
|
||||
|
||||
// When quad_anchored is set, the quad is placed at quad_pose in the
|
||||
// application reference space and stays put as the player looks around.
|
||||
// Otherwise it falls back to being head-locked in XR_VIEW_SPACE, centered
|
||||
// straight ahead at -Z, which is what happens until tracking has produced a
|
||||
// head pose good enough to anchor against.
|
||||
bool quad_anchored = false;
|
||||
XrPosef quad_pose{{0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}};
|
||||
};
|
||||
|
||||
struct OpenXRBackendFrame {
|
||||
OpenXRFrame xr_frame;
|
||||
OpenXRPresentation presentation;
|
||||
std::array<uint32_t, kOpenXREyeCount> render_width{};
|
||||
std::array<uint32_t, kOpenXREyeCount> render_height{};
|
||||
bool expects_gpu_submission = false;
|
||||
};
|
||||
|
||||
} // namespace mkw::vr
|
||||
|
||||
#endif // defined(MKW_ENABLE_OPENXR)
|
||||
@@ -46,6 +46,10 @@ struct OpenXRConfig {
|
||||
|
||||
// Destruction never waits indefinitely for a runtime to acknowledge an exit.
|
||||
uint32_t shutdown_timeout_ms = 500;
|
||||
|
||||
// Platform structure chained into XrInstanceCreateInfo::next (Android needs
|
||||
// XrInstanceCreateInfoAndroidKHR). Must outlive Initialize(); null elsewhere.
|
||||
const void* instance_create_next = nullptr;
|
||||
};
|
||||
|
||||
} // namespace mkw::vr
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
|
||||
|
||||
#include "vr/openxr_backend.h"
|
||||
#include "vr/openxr_runtime.h"
|
||||
|
||||
#include <array>
|
||||
@@ -13,24 +14,14 @@
|
||||
|
||||
namespace mkw::vr {
|
||||
|
||||
enum class OpenXRD3D12FrameMode {
|
||||
ImmersiveProjection,
|
||||
VirtualScreen,
|
||||
};
|
||||
|
||||
enum class OpenXRD3D12BeginStatus {
|
||||
Ready,
|
||||
SessionNotRunning,
|
||||
ExitRequested,
|
||||
Error,
|
||||
};
|
||||
|
||||
enum class OpenXRD3D12SubmissionStatus {
|
||||
Success,
|
||||
Failed,
|
||||
Timeout,
|
||||
ShuttingDown,
|
||||
};
|
||||
// The frame vocabulary is shared with the Vulkan backend (vr/openxr_backend.h).
|
||||
// These aliases keep the D3D12 spelling that the replay tests and the original
|
||||
// integration code were written against.
|
||||
using OpenXRD3D12FrameMode = OpenXRFrameMode;
|
||||
using OpenXRD3D12BeginStatus = OpenXRBeginStatus;
|
||||
using OpenXRD3D12SubmissionStatus = OpenXRSubmissionStatus;
|
||||
using OpenXRD3D12Presentation = OpenXRPresentation;
|
||||
using OpenXRD3D12Frame = OpenXRBackendFrame;
|
||||
|
||||
struct OpenXRD3D12GraphicsRequirements {
|
||||
uint32_t adapter_luid_low = 0;
|
||||
@@ -38,30 +29,6 @@ struct OpenXRD3D12GraphicsRequirements {
|
||||
uint32_t minimum_feature_level = 0;
|
||||
};
|
||||
|
||||
struct OpenXRD3D12Presentation {
|
||||
OpenXRD3D12FrameMode mode = OpenXRD3D12FrameMode::ImmersiveProjection;
|
||||
|
||||
// Used only by VirtualScreen.
|
||||
float quad_distance_meters = 2.0f;
|
||||
float quad_width_meters = 2.4f;
|
||||
|
||||
// When quad_anchored is set, the quad is placed at quad_pose in the
|
||||
// application reference space and stays put as the player looks around.
|
||||
// Otherwise it falls back to being head-locked in XR_VIEW_SPACE, centered
|
||||
// straight ahead at -Z, which is what happens until tracking has produced a
|
||||
// head pose good enough to anchor against.
|
||||
bool quad_anchored = false;
|
||||
XrPosef quad_pose{{0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f}};
|
||||
};
|
||||
|
||||
struct OpenXRD3D12Frame {
|
||||
OpenXRFrame xr_frame;
|
||||
OpenXRD3D12Presentation presentation;
|
||||
std::array<uint32_t, kOpenXREyeCount> render_width{};
|
||||
std::array<uint32_t, kOpenXREyeCount> render_height{};
|
||||
bool expects_gpu_submission = false;
|
||||
};
|
||||
|
||||
// Same-device Dawn/OpenXR D3D12 backend.
|
||||
//
|
||||
// Startup is deliberately split in two. QueryGraphicsRequirements() runs
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#if defined(MKW_ENABLE_OPENXR)
|
||||
|
||||
#include "vr/openxr_runtime.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace mkw::vr {
|
||||
|
||||
// OpenXR action-based controller input, surfaced to the rest of the runtime as
|
||||
// one ordinary SDL gamepad.
|
||||
//
|
||||
// Quest Touch controllers are not visible to SDL's joystick layer (the OS does
|
||||
// not expose them as HID gamepads), so a standalone headset build would have no
|
||||
// input at all. Rather than adding a second input path through the PAD/WPAD
|
||||
// HLE, this module syncs an OpenXR action set on the pacing thread and feeds a
|
||||
// virtual SDL joystick (SDL_AttachVirtualJoystick) that Aurora's existing
|
||||
// controller code opens, maps and assigns to player 1 exactly like a physical
|
||||
// pad. Every binding the settings overlay already offers keeps working.
|
||||
//
|
||||
// Mapping (Oculus Touch profile; the same actions are also bound for
|
||||
// khr/simple_controller so an unknown runtime still gets A/menu):
|
||||
// right A / B -> gamepad South / East (GameCube A / B)
|
||||
// left X / Y -> gamepad West / North (GameCube X / Y)
|
||||
// index triggers -> left / right trigger axes
|
||||
// grip squeezes -> left / right shoulder buttons
|
||||
// left / right thumbstick-> left / right stick axes, clicks -> stick buttons
|
||||
// left menu -> Start
|
||||
//
|
||||
// Lifetime: OpenXRInputCreate after the session exists (attaches the action
|
||||
// set, which OpenXR permits once per session), OpenXRInputSync once per
|
||||
// xrWaitFrame when the session is focused, OpenXRInputDestroy before the
|
||||
// session is destroyed. All three run on the XR pacing thread; SDL's virtual
|
||||
// joystick setters are internally locked, so the game thread may read the pad
|
||||
// concurrently.
|
||||
class OpenXRInput final {
|
||||
public:
|
||||
explicit OpenXRInput(OpenXRLogCallback logger = {});
|
||||
~OpenXRInput();
|
||||
|
||||
OpenXRInput(const OpenXRInput&) = delete;
|
||||
OpenXRInput& operator=(const OpenXRInput&) = delete;
|
||||
|
||||
// Creates the action set/actions/spaces and attaches them to the session.
|
||||
// Returns false (with LastError set) if the runtime rejects the action set;
|
||||
// the caller continues without controller input rather than failing VR.
|
||||
bool Create(OpenXRRuntime& runtime);
|
||||
void Destroy();
|
||||
|
||||
// xrSyncActions + state reads, then publishes to the virtual gamepad.
|
||||
// predicted_display_time is the frame's XrTime for pose-based lookups.
|
||||
void Sync(XrTime predicted_display_time);
|
||||
|
||||
// Rumble for the given hand (0 = left, 1 = right); amplitude 0..1.
|
||||
void ApplyHaptic(uint32_t hand, float amplitude, XrDuration duration);
|
||||
|
||||
bool IsCreated() const noexcept { return m_created; }
|
||||
bool HasVirtualGamepad() const noexcept { return m_joystick_id != 0; }
|
||||
const std::string& LastError() const noexcept { return m_last_error; }
|
||||
|
||||
private:
|
||||
bool CreateActions();
|
||||
bool SuggestBindings();
|
||||
bool AttachVirtualGamepad();
|
||||
void DetachVirtualGamepad();
|
||||
bool Check(XrResult result, const char* operation);
|
||||
void Log(OpenXRLogLevel level, const std::string& message) const noexcept;
|
||||
|
||||
OpenXRLogCallback m_logger;
|
||||
OpenXRRuntime* m_runtime = nullptr;
|
||||
XrActionSet m_action_set = XR_NULL_HANDLE;
|
||||
XrAction m_thumbstick = XR_NULL_HANDLE;
|
||||
XrAction m_thumbstick_click = XR_NULL_HANDLE;
|
||||
XrAction m_trigger = XR_NULL_HANDLE;
|
||||
XrAction m_squeeze = XR_NULL_HANDLE;
|
||||
XrAction m_button_primary = XR_NULL_HANDLE; // A / X
|
||||
XrAction m_button_secondary = XR_NULL_HANDLE; // B / Y
|
||||
XrAction m_menu = XR_NULL_HANDLE;
|
||||
XrAction m_haptic = XR_NULL_HANDLE;
|
||||
XrPath m_hand_paths[2]{};
|
||||
uint32_t m_joystick_id = 0; // SDL_JoystickID; 0 when detached
|
||||
void* m_joystick = nullptr; // SDL_Joystick*
|
||||
bool m_created = false;
|
||||
bool m_logged_sync_failure = false;
|
||||
std::string m_last_error;
|
||||
};
|
||||
|
||||
} // namespace mkw::vr
|
||||
|
||||
#endif // defined(MKW_ENABLE_OPENXR)
|
||||
@@ -0,0 +1,78 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#if defined(MKW_ENABLE_OPENXR) && defined(__ANDROID__)
|
||||
|
||||
#include "vr/openxr_backend.h"
|
||||
#include "vr/openxr_runtime.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace mkw::vr {
|
||||
|
||||
struct OpenXRVulkanGraphicsRequirements {
|
||||
// XR_MAKE_VERSION-encoded bounds reported by xrGetVulkanGraphicsRequirements2KHR.
|
||||
XrVersion min_api_version = 0;
|
||||
XrVersion max_api_version = 0;
|
||||
// Which binding extension was negotiated: XR_KHR_vulkan_enable2 when the
|
||||
// runtime offers it, otherwise the original XR_KHR_vulkan_enable.
|
||||
bool uses_enable2 = false;
|
||||
};
|
||||
|
||||
// Two-device Dawn/OpenXR Vulkan backend for Android (Meta Quest).
|
||||
//
|
||||
// Method surface and threading rules are identical to OpenXRD3D12Backend so the
|
||||
// pacing thread in openxr_integration.cpp is shared. The difference is behind
|
||||
// BindAurora: instead of binding Dawn's device to the session, this backend
|
||||
// creates its own VkInstance/VkDevice through the OpenXR runtime, owns a small
|
||||
// ring of AHardwareBuffers per eye, and hands them to Aurora's Vulkan stereo
|
||||
// bridge (aurora/vulkan_interop.h). When Aurora reports a completed copy, the
|
||||
// backend copies the buffer into the acquired XrSwapchain image on the
|
||||
// session's queue, so the compositor sees ordinary same-queue rendering.
|
||||
//
|
||||
// QueryGraphicsRequirements() runs after OpenXRRuntime::Initialize() and before
|
||||
// aurora_initialize(); BindAurora() runs afterwards, while Aurora's frame worker
|
||||
// is idle. Everything from BeginFrame() through FinishFrame() belongs to one XR
|
||||
// pacing thread; the Aurora callback only records a small copy on this
|
||||
// backend's private queue and publishes a token.
|
||||
class OpenXRVulkanBackend final {
|
||||
public:
|
||||
explicit OpenXRVulkanBackend(OpenXRLogCallback logger = {});
|
||||
~OpenXRVulkanBackend();
|
||||
|
||||
OpenXRVulkanBackend(const OpenXRVulkanBackend&) = delete;
|
||||
OpenXRVulkanBackend& operator=(const OpenXRVulkanBackend&) = delete;
|
||||
OpenXRVulkanBackend(OpenXRVulkanBackend&&) = delete;
|
||||
OpenXRVulkanBackend& operator=(OpenXRVulkanBackend&&) = delete;
|
||||
|
||||
bool QueryGraphicsRequirements(OpenXRRuntime& runtime);
|
||||
bool BindAurora(OpenXRRuntime& runtime);
|
||||
|
||||
OpenXRBeginStatus BeginFrame(const OpenXRPresentation& presentation, OpenXRBackendFrame& frame);
|
||||
OpenXRSubmissionStatus WaitForSubmission(const OpenXRBackendFrame& frame,
|
||||
uint32_t timeout_ms = UINT32_MAX);
|
||||
bool TryCancelPendingFrame(OpenXRBackendFrame& frame);
|
||||
bool RepeatFrame(const OpenXRBackendFrame& frame);
|
||||
bool FinishFrame(OpenXRBackendFrame& frame, bool submit_layer);
|
||||
|
||||
// Drains this backend's own queue before tearing down. Returns false only
|
||||
// when the private device could not be waited on, in which case the caller
|
||||
// retains the backend and runtime for the process lifetime.
|
||||
bool Shutdown();
|
||||
|
||||
bool IsBound() const;
|
||||
const OpenXRVulkanGraphicsRequirements& GraphicsRequirements() const;
|
||||
int64_t SwapchainFormat() const;
|
||||
const std::string& LastError() const;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> m_impl;
|
||||
};
|
||||
|
||||
} // namespace mkw::vr
|
||||
|
||||
#endif // defined(MKW_ENABLE_OPENXR) && defined(__ANDROID__)
|
||||
@@ -1,4 +1,7 @@
|
||||
#include "fiber_manager.h"
|
||||
#if defined(__ANDROID__)
|
||||
#include "aurora_events.h"
|
||||
#endif
|
||||
#include "memory.h"
|
||||
#include "abi_bridge.h"
|
||||
#include "hle_stubs.h"
|
||||
@@ -219,6 +222,10 @@ void GuestFiberManager::Shutdown() {
|
||||
s_initialized = false;
|
||||
}
|
||||
|
||||
bool GuestFiberManager::IsOnSchedulerFiber() {
|
||||
return !s_initialized || s_schedulerFiber == nullptr || HostContext::IsCurrent(s_schedulerFiber);
|
||||
}
|
||||
|
||||
bool GuestFiberManager::IsInitialized() {
|
||||
return s_initialized;
|
||||
}
|
||||
@@ -414,6 +421,11 @@ void GuestFiberManager::SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu
|
||||
|
||||
// Switch to the target fiber (the target fiber will load its own context)
|
||||
HostContext::Switch(fiberHandle);
|
||||
#if defined(__ANDROID__)
|
||||
// A guest thread running on its own stack may have deferred an SDL event
|
||||
// poll to the scheduler stack; service it now if that is where we are.
|
||||
ServicePendingAuroraEventsOnScheduler();
|
||||
#endif
|
||||
|
||||
// When we return here, the fiber that issued SwitchToThread has resumed.
|
||||
// That does not automatically mean the previous guest thread became runnable
|
||||
|
||||
@@ -33,6 +33,15 @@
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#if defined(__ANDROID__) && __ANDROID_API__ < 30
|
||||
// Bionic only declares memfd_create() from API 30, but every Android kernel
|
||||
// this build can run on (4.x on the Quest line) has had the syscall since 3.17.
|
||||
#include <linux/memfd.h>
|
||||
#include <sys/syscall.h>
|
||||
static int memfd_create(const char* name, unsigned int flags) {
|
||||
return static_cast<int>(syscall(__NR_memfd_create, name, flags));
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace GuestFlat {
|
||||
|
||||
@@ -509,7 +509,8 @@ int64_t ConsumeAudioPollDeltaMicros()
|
||||
// kMaxBlocksPerTick per pass. Never return early on a zero delta: two scheduler passes
|
||||
// can land in the same microsecond and the backlog still needs servicing.
|
||||
constexpr int64_t kMaxPollDeltaMicros = 100'000;
|
||||
return std::min(elapsed, kMaxPollDeltaMicros);
|
||||
// int64_t is `long` on LP64 Android while the clock rep is `long long`; pick one type.
|
||||
return std::min<int64_t>(static_cast<int64_t>(elapsed), static_cast<int64_t>(kMaxPollDeltaMicros));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -70,6 +70,13 @@
|
||||
#include <aurora/aurora.h>
|
||||
#include <aurora/gfx.h>
|
||||
#include <dolphin/gx/GXAurora.h>
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
#include <android/log.h>
|
||||
// Renames main() to SDL_main(), which SDLActivity's nativeRunMain resolves
|
||||
// from libmain.so with dlsym once the Java side has set up the surface.
|
||||
#include <SDL3/SDL_main.h>
|
||||
#endif
|
||||
#include <dolphin/vi.h>
|
||||
|
||||
// Defined in `runtime/src/hle/vi.cpp` (used by GX/VI HLE).
|
||||
@@ -245,6 +252,11 @@ void WriteProcessTranscriptChunk(ProcessTranscriptState& state, const char* data
|
||||
|
||||
void PumpTranscriptPipe(ProcessTranscriptState& state, int readFd, int mirrorFd) {
|
||||
std::array<char, 4096> buffer{};
|
||||
#if defined(__ANDROID__)
|
||||
// Android sends stdout/stderr to /dev/null, so the mirror fd shows nothing;
|
||||
// forward complete lines to logcat as well (tag WiiCompiled).
|
||||
std::string pendingLine;
|
||||
#endif
|
||||
for (;;) {
|
||||
#if defined(_WIN32)
|
||||
const int bytesRead = _read(readFd, buffer.data(), static_cast<unsigned int>(buffer.size()));
|
||||
@@ -275,6 +287,21 @@ void PumpTranscriptPipe(ProcessTranscriptState& state, int readFd, int mirrorFd)
|
||||
}
|
||||
|
||||
WriteProcessTranscriptChunk(state, buffer.data(), static_cast<size_t>(bytesRead));
|
||||
#if defined(__ANDROID__)
|
||||
pendingLine.append(buffer.data(), static_cast<size_t>(bytesRead));
|
||||
size_t lineStart = 0;
|
||||
for (size_t newline = pendingLine.find('\n'); newline != std::string::npos;
|
||||
newline = pendingLine.find('\n', lineStart)) {
|
||||
const std::string line = pendingLine.substr(lineStart, newline - lineStart);
|
||||
__android_log_write(ANDROID_LOG_INFO, "WiiCompiled", line.c_str());
|
||||
lineStart = newline + 1;
|
||||
}
|
||||
pendingLine.erase(0, lineStart);
|
||||
if (pendingLine.size() > 3500) {
|
||||
__android_log_write(ANDROID_LOG_INFO, "WiiCompiled", pendingLine.c_str());
|
||||
pendingLine.clear();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1352,6 +1379,16 @@ int RuntimeMain(int argc, char** argv) {
|
||||
const std::string auroraCachePath = RuntimeConfigFile::PathToUtf8(rendererCacheDirectory);
|
||||
auroraConfig.userPath = auroraUserPath.c_str();
|
||||
auroraConfig.cachePath = auroraCachePath.c_str();
|
||||
#if defined(__ANDROID__)
|
||||
// Aurora defaults resourcesPath to SDL_GetBasePath(), which is empty on
|
||||
// Android; the transferable pipeline cache lives with the other bundled
|
||||
// resources the activity unpacked.
|
||||
std::string auroraResourcesPath;
|
||||
if (const auto resources = RuntimeConfigFile::ExecutableDirectory()) {
|
||||
auroraResourcesPath = RuntimeConfigFile::PathToUtf8(*resources);
|
||||
auroraConfig.resourcesPath = auroraResourcesPath.c_str();
|
||||
}
|
||||
#endif
|
||||
auroraConfig.logCallback = &RuntimeAuroraLogCallback;
|
||||
auroraConfig.logLevel = LOG_DEBUG;
|
||||
const bool configWidescreen = RuntimeConfigFile::WidescreenEnabled(true);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
#include <winrt/Windows.Media.Control.h>
|
||||
#elif defined(__linux__)
|
||||
#elif defined(__linux__) && !defined(__ANDROID__)
|
||||
#include <dlfcn.h>
|
||||
|
||||
#include <string>
|
||||
@@ -196,7 +196,7 @@ void MonitorWindowsMediaSessions() noexcept {
|
||||
<< error.message().c_str() << std::endl;
|
||||
}
|
||||
}
|
||||
#elif defined(__linux__)
|
||||
#elif defined(__linux__) && !defined(__ANDROID__)
|
||||
|
||||
// For linux - watches MPRIS players on the D-Bus session bus and drives the
|
||||
// same g_externalMediaPlaying / g_mediaControl* atomics.
|
||||
@@ -453,7 +453,7 @@ void StartMonitor() noexcept {
|
||||
// The process owns this monitor for its remaining lifetime. Keeping it
|
||||
// detached avoids shutdown ordering between WinRT and static audio state.
|
||||
std::thread(MonitorWindowsMediaSessions).detach();
|
||||
#elif defined(__linux__)
|
||||
#elif defined(__linux__) && !defined(__ANDROID__)
|
||||
// Detached so there is no shutdown ordering to manage against static audio state.
|
||||
std::thread(MonitorLinuxMprisSessions).detach();
|
||||
#else
|
||||
|
||||
@@ -46,6 +46,14 @@ std::optional<std::filesystem::path> ExecutableDirectory() noexcept {
|
||||
std::error_code ec;
|
||||
const auto resolved = std::filesystem::weakly_canonical(path, ec);
|
||||
return (ec ? std::filesystem::path(path) : resolved).parent_path();
|
||||
#elif defined(__ANDROID__)
|
||||
// Bundled read-only runtime resources (wii_bootstrap/, dsp_coef.bin,
|
||||
// initial_pipeline_cache.db) unpacked from the APK by the activity. Stands
|
||||
// in for the executable directory the desktop builds look next to.
|
||||
if (const char* resources = std::getenv("MKW_ANDROID_RESOURCES_DIR"); resources && *resources) {
|
||||
return std::filesystem::path(resources);
|
||||
}
|
||||
return std::nullopt;
|
||||
#else
|
||||
return std::nullopt;
|
||||
#endif
|
||||
@@ -66,6 +74,13 @@ std::filesystem::path ApplicationDataDirectory(std::string_view applicationName)
|
||||
if (const passwd* user = getpwuid(getuid()); user && user->pw_dir && *user->pw_dir) {
|
||||
return std::filesystem::path(user->pw_dir) / "Library" / "Application Support" / applicationName;
|
||||
}
|
||||
#elif defined(__ANDROID__)
|
||||
// The app's external files directory (Android/data/<package>/files) so a
|
||||
// user can push game data and Config.toml with adb or a file browser; the
|
||||
// activity falls back to the private files directory when there is none.
|
||||
if (const char* data = std::getenv("MKW_ANDROID_DATA_DIR"); data && *data) {
|
||||
return std::filesystem::path(data) / applicationName;
|
||||
}
|
||||
#endif
|
||||
return std::filesystem::current_path() / applicationName;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#if defined(MKW_ENABLE_OPENXR) && defined(__ANDROID__)
|
||||
|
||||
#include "vr/openxr_android.h"
|
||||
|
||||
#include <SDL3/SDL_system.h>
|
||||
#include <jni.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define XR_USE_PLATFORM_ANDROID
|
||||
#include <openxr/openxr_platform.h>
|
||||
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
|
||||
namespace mkw::vr {
|
||||
namespace {
|
||||
|
||||
std::mutex g_mutex;
|
||||
JavaVM* g_vm = nullptr;
|
||||
jobject g_activity = nullptr; // global reference, never released: it lives as long as the process
|
||||
bool g_loader_initialized = false;
|
||||
XrInstanceCreateInfoAndroidKHR g_instance_create{XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR};
|
||||
|
||||
void Emit(const OpenXRLogCallback& logger, OpenXRLogLevel level, const std::string& message) noexcept {
|
||||
if (!logger) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
logger(level, message);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
|
||||
bool ResolveAndroidObjectsLocked(std::string* error) {
|
||||
if (g_vm != nullptr && g_activity != nullptr) {
|
||||
return true;
|
||||
}
|
||||
// SDL attaches the calling thread to the VM on demand and hands back the
|
||||
// activity as a local reference the caller owns.
|
||||
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
|
||||
auto activity = static_cast<jobject>(SDL_GetAndroidActivity());
|
||||
if (env == nullptr || activity == nullptr) {
|
||||
if (error != nullptr) {
|
||||
*error = "SDL has not published the Android activity yet";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
JavaVM* vm = nullptr;
|
||||
if (env->GetJavaVM(&vm) != JNI_OK || vm == nullptr) {
|
||||
env->DeleteLocalRef(activity);
|
||||
if (error != nullptr) {
|
||||
*error = "JNIEnv::GetJavaVM failed";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
g_vm = vm;
|
||||
g_activity = env->NewGlobalRef(activity);
|
||||
env->DeleteLocalRef(activity);
|
||||
if (g_activity == nullptr) {
|
||||
if (error != nullptr) {
|
||||
*error = "could not pin the Android activity";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool OpenXRAndroidInitializeLoader(OpenXRLogCallback logger, std::string* error) {
|
||||
std::lock_guard lock(g_mutex);
|
||||
if (g_loader_initialized) {
|
||||
return true;
|
||||
}
|
||||
if (!ResolveAndroidObjectsLocked(error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PFN_xrInitializeLoaderKHR initialize_loader = nullptr;
|
||||
XrResult result = xrGetInstanceProcAddr(
|
||||
XR_NULL_HANDLE, "xrInitializeLoaderKHR",
|
||||
reinterpret_cast<PFN_xrVoidFunction*>(&initialize_loader));
|
||||
if (XR_FAILED(result) || initialize_loader == nullptr) {
|
||||
if (error != nullptr) {
|
||||
std::ostringstream message;
|
||||
message << "xrInitializeLoaderKHR is unavailable (" << static_cast<int>(result) << ')';
|
||||
*error = message.str();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The activity is deliberately used as the loader context (an Activity is
|
||||
// a Context). Meta's runtime keys activity readiness off it; with a plain
|
||||
// application context the session parks in IDLE forever (DolphinXR finding).
|
||||
XrLoaderInitInfoAndroidKHR loader_init{XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR};
|
||||
loader_init.applicationVM = g_vm;
|
||||
loader_init.applicationContext = g_activity;
|
||||
result = initialize_loader(reinterpret_cast<const XrLoaderInitInfoBaseHeaderKHR*>(&loader_init));
|
||||
if (XR_FAILED(result)) {
|
||||
if (error != nullptr) {
|
||||
std::ostringstream message;
|
||||
message << "xrInitializeLoaderKHR failed (" << static_cast<int>(result) << ')';
|
||||
*error = message.str();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
g_instance_create.next = nullptr;
|
||||
g_instance_create.applicationVM = g_vm;
|
||||
g_instance_create.applicationActivity = g_activity;
|
||||
g_loader_initialized = true;
|
||||
Emit(logger, OpenXRLogLevel::Info, "OpenXR Android loader initialized against the SDL activity");
|
||||
return true;
|
||||
}
|
||||
|
||||
const void* OpenXRAndroidInstanceCreateNext() {
|
||||
std::lock_guard lock(g_mutex);
|
||||
return g_loader_initialized ? &g_instance_create : nullptr;
|
||||
}
|
||||
|
||||
bool OpenXRAndroidRegisterThread(OpenXRRuntime& runtime, OpenXRAndroidThreadType type) {
|
||||
if (!runtime.HasSession()) {
|
||||
return false;
|
||||
}
|
||||
const auto& extensions = runtime.EnabledExtensions();
|
||||
bool enabled = false;
|
||||
for (const auto& extension : extensions) {
|
||||
if (extension == XR_KHR_ANDROID_THREAD_SETTINGS_EXTENSION_NAME) {
|
||||
enabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
PFN_xrSetAndroidApplicationThreadKHR set_thread = nullptr;
|
||||
if (!runtime.LoadFunction("xrSetAndroidApplicationThreadKHR", &set_thread) || set_thread == nullptr) {
|
||||
return false;
|
||||
}
|
||||
XrAndroidThreadTypeKHR xr_type = XR_ANDROID_THREAD_TYPE_APPLICATION_WORKER_KHR;
|
||||
switch (type) {
|
||||
case OpenXRAndroidThreadType::ApplicationMain:
|
||||
xr_type = XR_ANDROID_THREAD_TYPE_APPLICATION_MAIN_KHR;
|
||||
break;
|
||||
case OpenXRAndroidThreadType::ApplicationWorker:
|
||||
xr_type = XR_ANDROID_THREAD_TYPE_APPLICATION_WORKER_KHR;
|
||||
break;
|
||||
case OpenXRAndroidThreadType::RendererMain:
|
||||
xr_type = XR_ANDROID_THREAD_TYPE_RENDERER_MAIN_KHR;
|
||||
break;
|
||||
case OpenXRAndroidThreadType::RendererWorker:
|
||||
xr_type = XR_ANDROID_THREAD_TYPE_RENDERER_WORKER_KHR;
|
||||
break;
|
||||
}
|
||||
const auto thread_id = static_cast<uint32_t>(syscall(SYS_gettid));
|
||||
XrResult result = set_thread(runtime.Session(), xr_type, thread_id);
|
||||
if (XR_FAILED(result) && type == OpenXRAndroidThreadType::RendererWorker) {
|
||||
// Some Quest runtime builds advertise the extension but reject the
|
||||
// renderer-worker type; the renderer-main hint still raises priority.
|
||||
result = set_thread(runtime.Session(), XR_ANDROID_THREAD_TYPE_RENDERER_MAIN_KHR, thread_id);
|
||||
}
|
||||
runtime.ObserveResult(result);
|
||||
return XR_SUCCEEDED(result);
|
||||
}
|
||||
|
||||
} // namespace mkw::vr
|
||||
|
||||
#endif // defined(MKW_ENABLE_OPENXR) && defined(__ANDROID__)
|
||||
@@ -0,0 +1,450 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#if defined(MKW_ENABLE_OPENXR)
|
||||
|
||||
#include "vr/openxr_input.h"
|
||||
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <SDL3/SDL_joystick.h>
|
||||
#include <SDL3/SDL_stdinc.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
#include <sys/system_properties.h>
|
||||
#endif
|
||||
|
||||
namespace mkw::vr {
|
||||
namespace {
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
// Debug-only remote button presses for headset experiments driven over adb, so
|
||||
// a menu can be reached without someone wearing the headset:
|
||||
// adb shell setprop debug.wiicompiled.inject <sequence>:<button>
|
||||
// A new sequence number holds the button for kInjectHoldFrames XR frames.
|
||||
// Buttons: a, b, x, y, start, up, down, left, right. The property is unset in
|
||||
// normal use, so this costs one property read every few frames.
|
||||
constexpr uint32_t kInjectHoldFrames = 12;
|
||||
constexpr uint32_t kInjectPollFrames = 4;
|
||||
|
||||
struct InjectedPress {
|
||||
long sequence = -1;
|
||||
std::string button;
|
||||
uint32_t frames_left = 0;
|
||||
uint32_t poll_countdown = 0;
|
||||
};
|
||||
|
||||
InjectedPress& Injection() {
|
||||
static InjectedPress press;
|
||||
return press;
|
||||
}
|
||||
|
||||
void PollInjection() {
|
||||
InjectedPress& press = Injection();
|
||||
if (press.frames_left > 0) {
|
||||
--press.frames_left;
|
||||
}
|
||||
if (press.poll_countdown > 0) {
|
||||
--press.poll_countdown;
|
||||
return;
|
||||
}
|
||||
press.poll_countdown = kInjectPollFrames;
|
||||
char value[PROP_VALUE_MAX]{};
|
||||
if (__system_property_get("debug.wiicompiled.inject", value) <= 0) {
|
||||
return;
|
||||
}
|
||||
char* end = nullptr;
|
||||
const long sequence = std::strtol(value, &end, 10);
|
||||
if (end == value || *end != ':' || sequence == press.sequence) {
|
||||
return;
|
||||
}
|
||||
const bool first_read = press.sequence < 0;
|
||||
press.sequence = sequence;
|
||||
if (first_read) {
|
||||
return; // A value left over from an earlier run is not a new press.
|
||||
}
|
||||
press.button = end + 1;
|
||||
press.frames_left = kInjectHoldFrames;
|
||||
}
|
||||
|
||||
bool Injected(const char* button) {
|
||||
const InjectedPress& press = Injection();
|
||||
return press.frames_left > 0 && press.button == button;
|
||||
}
|
||||
#else
|
||||
void PollInjection() {}
|
||||
bool Injected(const char*) { return false; }
|
||||
#endif
|
||||
|
||||
constexpr uint32_t kHandCount = 2;
|
||||
|
||||
struct Binding {
|
||||
XrAction* action;
|
||||
const char* path;
|
||||
};
|
||||
|
||||
Sint16 ToAxis(float value) noexcept {
|
||||
const float clamped = std::clamp(value, -1.0f, 1.0f);
|
||||
return static_cast<Sint16>(std::lround(clamped * 32767.0f));
|
||||
}
|
||||
|
||||
// Trigger axes are reported by SDL gamepads on the positive half only.
|
||||
Sint16 ToTrigger(float value) noexcept {
|
||||
const float clamped = std::clamp(value, 0.0f, 1.0f);
|
||||
return static_cast<Sint16>(std::lround(clamped * 32767.0f));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
OpenXRInput::OpenXRInput(OpenXRLogCallback logger) : m_logger(std::move(logger)) {}
|
||||
|
||||
OpenXRInput::~OpenXRInput() {
|
||||
Destroy();
|
||||
}
|
||||
|
||||
bool OpenXRInput::Create(OpenXRRuntime& runtime) {
|
||||
m_last_error.clear();
|
||||
if (m_created) {
|
||||
return true;
|
||||
}
|
||||
if (!runtime.IsInitialized() || !runtime.HasSession()) {
|
||||
m_last_error = "OpenXR input needs an initialized runtime with a session";
|
||||
return false;
|
||||
}
|
||||
m_runtime = &runtime;
|
||||
|
||||
if (!Check(xrStringToPath(runtime.Instance(), "/user/hand/left", &m_hand_paths[0]),
|
||||
"xrStringToPath(/user/hand/left)") ||
|
||||
!Check(xrStringToPath(runtime.Instance(), "/user/hand/right", &m_hand_paths[1]),
|
||||
"xrStringToPath(/user/hand/right)")) {
|
||||
Destroy();
|
||||
return false;
|
||||
}
|
||||
if (!CreateActions() || !SuggestBindings()) {
|
||||
Destroy();
|
||||
return false;
|
||||
}
|
||||
|
||||
XrSessionActionSetsAttachInfo attach{XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO};
|
||||
attach.countActionSets = 1;
|
||||
attach.actionSets = &m_action_set;
|
||||
if (!Check(xrAttachSessionActionSets(runtime.Session(), &attach), "xrAttachSessionActionSets")) {
|
||||
Destroy();
|
||||
return false;
|
||||
}
|
||||
m_created = true;
|
||||
if (!AttachVirtualGamepad()) {
|
||||
Log(OpenXRLogLevel::Warning,
|
||||
"SDL refused the virtual gamepad; OpenXR controllers will not reach the game");
|
||||
}
|
||||
Log(OpenXRLogLevel::Info, "OpenXR controller actions attached");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenXRInput::CreateActions() {
|
||||
XrActionSetCreateInfo set_info{XR_TYPE_ACTION_SET_CREATE_INFO};
|
||||
std::strncpy(set_info.actionSetName, "mkw_gameplay", XR_MAX_ACTION_SET_NAME_SIZE - 1);
|
||||
std::strncpy(set_info.localizedActionSetName, "Gameplay", XR_MAX_LOCALIZED_ACTION_SET_NAME_SIZE - 1);
|
||||
set_info.priority = 0;
|
||||
if (!Check(xrCreateActionSet(m_runtime->Instance(), &set_info, &m_action_set), "xrCreateActionSet")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
struct Spec {
|
||||
XrAction* action;
|
||||
const char* name;
|
||||
const char* localized;
|
||||
XrActionType type;
|
||||
};
|
||||
const std::array<Spec, 8> 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},
|
||||
{&m_squeeze, "squeeze", "Grip", XR_ACTION_TYPE_FLOAT_INPUT},
|
||||
{&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_haptic, "haptic", "Haptic", XR_ACTION_TYPE_VIBRATION_OUTPUT},
|
||||
}};
|
||||
for (const Spec& spec : specs) {
|
||||
XrActionCreateInfo info{XR_TYPE_ACTION_CREATE_INFO};
|
||||
info.actionType = spec.type;
|
||||
std::strncpy(info.actionName, spec.name, XR_MAX_ACTION_NAME_SIZE - 1);
|
||||
std::strncpy(info.localizedActionName, spec.localized, XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1);
|
||||
info.countSubactionPaths = kHandCount;
|
||||
info.subactionPaths = m_hand_paths;
|
||||
if (!Check(xrCreateAction(m_action_set, &info, spec.action), spec.name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenXRInput::SuggestBindings() {
|
||||
const auto suggest = [&](const char* profile, const std::vector<Binding>& bindings, bool required) {
|
||||
XrPath profile_path = XR_NULL_PATH;
|
||||
if (!Check(xrStringToPath(m_runtime->Instance(), profile, &profile_path), profile)) {
|
||||
return false;
|
||||
}
|
||||
std::vector<XrActionSuggestedBinding> suggested;
|
||||
suggested.reserve(bindings.size());
|
||||
for (const Binding& binding : bindings) {
|
||||
XrPath path = XR_NULL_PATH;
|
||||
if (XR_FAILED(xrStringToPath(m_runtime->Instance(), binding.path, &path))) {
|
||||
continue;
|
||||
}
|
||||
suggested.push_back({*binding.action, path});
|
||||
}
|
||||
XrInteractionProfileSuggestedBinding info{XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
|
||||
info.interactionProfile = profile_path;
|
||||
info.countSuggestedBindings = static_cast<uint32_t>(suggested.size());
|
||||
info.suggestedBindings = suggested.data();
|
||||
const XrResult result = xrSuggestInteractionProfileBindings(m_runtime->Instance(), &info);
|
||||
m_runtime->ObserveResult(result);
|
||||
if (XR_FAILED(result)) {
|
||||
std::ostringstream message;
|
||||
message << "xrSuggestInteractionProfileBindings(" << profile << ") failed (" << result << ')';
|
||||
if (required) {
|
||||
m_last_error = message.str();
|
||||
Log(OpenXRLogLevel::Error, m_last_error);
|
||||
return false;
|
||||
}
|
||||
Log(OpenXRLogLevel::Warning, message.str());
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Meta Quest Touch controllers (Quest 2 / 3 / Pro all expose this profile).
|
||||
const std::vector<Binding> touch{
|
||||
{&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/left/input/x/click"},
|
||||
{&m_button_primary, "/user/hand/right/input/a/click"},
|
||||
{&m_button_secondary, "/user/hand/left/input/y/click"},
|
||||
{&m_button_secondary, "/user/hand/right/input/b/click"},
|
||||
{&m_menu, "/user/hand/left/input/menu/click"},
|
||||
{&m_haptic, "/user/hand/left/output/haptic"},
|
||||
{&m_haptic, "/user/hand/right/output/haptic"},
|
||||
};
|
||||
if (!suggest("/interaction_profiles/oculus/touch_controller", touch, true)) {
|
||||
return false;
|
||||
}
|
||||
// Minimal fallback so an unfamiliar runtime still offers a select and a menu.
|
||||
const std::vector<Binding> simple{
|
||||
{&m_button_primary, "/user/hand/right/input/select/click"},
|
||||
{&m_button_secondary, "/user/hand/left/input/select/click"},
|
||||
{&m_menu, "/user/hand/left/input/menu/click"},
|
||||
{&m_haptic, "/user/hand/left/output/haptic"},
|
||||
{&m_haptic, "/user/hand/right/output/haptic"},
|
||||
};
|
||||
suggest("/interaction_profiles/khr/simple_controller", simple, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenXRInput::AttachVirtualGamepad() {
|
||||
SDL_VirtualJoystickDesc desc;
|
||||
SDL_INIT_INTERFACE(&desc);
|
||||
desc.type = SDL_JOYSTICK_TYPE_GAMEPAD;
|
||||
desc.naxes = SDL_GAMEPAD_AXIS_COUNT;
|
||||
desc.nbuttons = SDL_GAMEPAD_BUTTON_COUNT;
|
||||
desc.button_mask = (1u << SDL_GAMEPAD_BUTTON_SOUTH) | (1u << SDL_GAMEPAD_BUTTON_EAST) |
|
||||
(1u << SDL_GAMEPAD_BUTTON_WEST) | (1u << SDL_GAMEPAD_BUTTON_NORTH) |
|
||||
(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);
|
||||
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);
|
||||
desc.name = "OpenXR Touch Controllers";
|
||||
const SDL_JoystickID id = SDL_AttachVirtualJoystick(&desc);
|
||||
if (id == 0) {
|
||||
m_last_error = std::string("SDL_AttachVirtualJoystick failed: ") + SDL_GetError();
|
||||
Log(OpenXRLogLevel::Warning, m_last_error);
|
||||
return false;
|
||||
}
|
||||
SDL_Joystick* joystick = SDL_OpenJoystick(id);
|
||||
if (joystick == nullptr) {
|
||||
m_last_error = std::string("SDL_OpenJoystick failed: ") + SDL_GetError();
|
||||
Log(OpenXRLogLevel::Warning, m_last_error);
|
||||
SDL_DetachVirtualJoystick(id);
|
||||
return false;
|
||||
}
|
||||
m_joystick_id = id;
|
||||
m_joystick = joystick;
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenXRInput::DetachVirtualGamepad() {
|
||||
if (m_joystick != nullptr) {
|
||||
SDL_CloseJoystick(static_cast<SDL_Joystick*>(m_joystick));
|
||||
m_joystick = nullptr;
|
||||
}
|
||||
if (m_joystick_id != 0) {
|
||||
SDL_DetachVirtualJoystick(m_joystick_id);
|
||||
m_joystick_id = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void OpenXRInput::Destroy() {
|
||||
DetachVirtualGamepad();
|
||||
if (m_action_set != XR_NULL_HANDLE) {
|
||||
// Destroying the set destroys every action created from it.
|
||||
xrDestroyActionSet(m_action_set);
|
||||
m_action_set = XR_NULL_HANDLE;
|
||||
}
|
||||
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_hand_paths[0] = m_hand_paths[1] = XR_NULL_PATH;
|
||||
m_created = false;
|
||||
m_runtime = nullptr;
|
||||
}
|
||||
|
||||
void OpenXRInput::Sync(XrTime) {
|
||||
if (!m_created || m_runtime == nullptr || !m_runtime->IsSessionFocused()) {
|
||||
return;
|
||||
}
|
||||
XrActiveActionSet active{m_action_set, XR_NULL_PATH};
|
||||
XrActionsSyncInfo sync{XR_TYPE_ACTIONS_SYNC_INFO};
|
||||
sync.countActiveActionSets = 1;
|
||||
sync.activeActionSets = &active;
|
||||
const XrResult result = xrSyncActions(m_runtime->Session(), &sync);
|
||||
m_runtime->ObserveResult(result);
|
||||
if (XR_FAILED(result)) {
|
||||
if (!m_logged_sync_failure) {
|
||||
m_logged_sync_failure = true;
|
||||
std::ostringstream message;
|
||||
message << "xrSyncActions failed (" << result << ')';
|
||||
Log(OpenXRLogLevel::Warning, message.str());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (m_joystick == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto* joystick = static_cast<SDL_Joystick*>(m_joystick);
|
||||
|
||||
const auto boolean = [&](XrAction action, uint32_t hand) {
|
||||
XrActionStateGetInfo info{XR_TYPE_ACTION_STATE_GET_INFO};
|
||||
info.action = action;
|
||||
info.subactionPath = m_hand_paths[hand];
|
||||
XrActionStateBoolean state{XR_TYPE_ACTION_STATE_BOOLEAN};
|
||||
return XR_SUCCEEDED(xrGetActionStateBoolean(m_runtime->Session(), &info, &state)) &&
|
||||
state.isActive == XR_TRUE && state.currentState == XR_TRUE;
|
||||
};
|
||||
const auto scalar = [&](XrAction action, uint32_t hand) {
|
||||
XrActionStateGetInfo info{XR_TYPE_ACTION_STATE_GET_INFO};
|
||||
info.action = action;
|
||||
info.subactionPath = m_hand_paths[hand];
|
||||
XrActionStateFloat state{XR_TYPE_ACTION_STATE_FLOAT};
|
||||
if (XR_FAILED(xrGetActionStateFloat(m_runtime->Session(), &info, &state)) ||
|
||||
state.isActive != XR_TRUE) {
|
||||
return 0.0f;
|
||||
}
|
||||
return state.currentState;
|
||||
};
|
||||
const auto vector = [&](XrAction action, uint32_t hand) {
|
||||
XrActionStateGetInfo info{XR_TYPE_ACTION_STATE_GET_INFO};
|
||||
info.action = action;
|
||||
info.subactionPath = m_hand_paths[hand];
|
||||
XrActionStateVector2f state{XR_TYPE_ACTION_STATE_VECTOR2F};
|
||||
if (XR_FAILED(xrGetActionStateVector2f(m_runtime->Session(), &info, &state)) ||
|
||||
state.isActive != XR_TRUE) {
|
||||
return XrVector2f{0.0f, 0.0f};
|
||||
}
|
||||
return state.currentState;
|
||||
};
|
||||
|
||||
PollInjection();
|
||||
XrVector2f left = vector(m_thumbstick, 0);
|
||||
const XrVector2f right = vector(m_thumbstick, 1);
|
||||
if (Injected("up")) {
|
||||
left.y = 1.0f;
|
||||
} else if (Injected("down")) {
|
||||
left.y = -1.0f;
|
||||
} else if (Injected("left")) {
|
||||
left.x = -1.0f;
|
||||
} else if (Injected("right")) {
|
||||
left.x = 1.0f;
|
||||
}
|
||||
// OpenXR thumbsticks report +Y up; SDL gamepads report +Y down.
|
||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_LEFTX, ToAxis(left.x));
|
||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_LEFTY, ToAxis(-left.y));
|
||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_RIGHTX, ToAxis(right.x));
|
||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_RIGHTY, ToAxis(-right.y));
|
||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, ToTrigger(scalar(m_trigger, 0)));
|
||||
SDL_SetJoystickVirtualAxis(joystick, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, ToTrigger(scalar(m_trigger, 1)));
|
||||
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_SOUTH,
|
||||
boolean(m_button_primary, 1) || Injected("a"));
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_EAST,
|
||||
boolean(m_button_secondary, 1) || Injected("b"));
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_WEST,
|
||||
boolean(m_button_primary, 0) || Injected("x"));
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_NORTH,
|
||||
boolean(m_button_secondary, 0) || Injected("y"));
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_START, boolean(m_menu, 0) || Injected("start"));
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, boolean(m_thumbstick_click, 0));
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, boolean(m_thumbstick_click, 1));
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, scalar(m_squeeze, 0) > 0.5f);
|
||||
SDL_SetJoystickVirtualButton(joystick, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, scalar(m_squeeze, 1) > 0.5f);
|
||||
}
|
||||
|
||||
void OpenXRInput::ApplyHaptic(uint32_t hand, float amplitude, XrDuration duration) {
|
||||
if (!m_created || m_runtime == nullptr || hand >= kHandCount || m_haptic == XR_NULL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
XrHapticVibration vibration{XR_TYPE_HAPTIC_VIBRATION};
|
||||
vibration.amplitude = std::clamp(amplitude, 0.0f, 1.0f);
|
||||
vibration.duration = duration;
|
||||
vibration.frequency = XR_FREQUENCY_UNSPECIFIED;
|
||||
XrHapticActionInfo info{XR_TYPE_HAPTIC_ACTION_INFO};
|
||||
info.action = m_haptic;
|
||||
info.subactionPath = m_hand_paths[hand];
|
||||
if (vibration.amplitude <= 0.0f) {
|
||||
xrStopHapticFeedback(m_runtime->Session(), &info);
|
||||
return;
|
||||
}
|
||||
xrApplyHapticFeedback(m_runtime->Session(), &info,
|
||||
reinterpret_cast<const XrHapticBaseHeader*>(&vibration));
|
||||
}
|
||||
|
||||
bool OpenXRInput::Check(XrResult result, const char* operation) {
|
||||
if (m_runtime != nullptr) {
|
||||
m_runtime->ObserveResult(result);
|
||||
}
|
||||
if (XR_SUCCEEDED(result)) {
|
||||
return true;
|
||||
}
|
||||
std::ostringstream message;
|
||||
message << operation << " failed (" << result << ')';
|
||||
m_last_error = message.str();
|
||||
Log(OpenXRLogLevel::Error, m_last_error);
|
||||
return false;
|
||||
}
|
||||
|
||||
void OpenXRInput::Log(OpenXRLogLevel level, const std::string& message) const noexcept {
|
||||
if (!m_logger) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
m_logger(level, message);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mkw::vr
|
||||
|
||||
#endif // defined(MKW_ENABLE_OPENXR)
|
||||
@@ -27,12 +27,24 @@
|
||||
#include <thread>
|
||||
|
||||
#if defined(MKW_ENABLE_OPENXR)
|
||||
#include "vr/openxr_backend.h"
|
||||
#include "vr/openxr_input.h"
|
||||
#include "vr/openxr_runtime.h"
|
||||
#if defined(_WIN32)
|
||||
#include "vr/openxr_d3d12.h"
|
||||
#define MKW_OPENXR_GRAPHICS_BACKEND 1
|
||||
#elif defined(__ANDROID__)
|
||||
#include "vr/openxr_android.h"
|
||||
#include "vr/openxr_vulkan.h"
|
||||
#include <time.h>
|
||||
#define XR_USE_TIMESPEC
|
||||
#include <openxr/openxr_platform.h>
|
||||
#define MKW_OPENXR_GRAPHICS_BACKEND 1
|
||||
#else
|
||||
#include "vr/openxr_vulkan_backend.h"
|
||||
#define MKW_OPENXR_GRAPHICS_BACKEND 0
|
||||
#endif
|
||||
#else
|
||||
#define MKW_OPENXR_GRAPHICS_BACKEND 0
|
||||
#endif
|
||||
|
||||
namespace mkw::vr {
|
||||
@@ -40,6 +52,14 @@ namespace {
|
||||
|
||||
inline constexpr float kDegreesToRadians = 0.01745329252f;
|
||||
|
||||
// A standalone headset has no desktop to fall back to, so VR is on unless the
|
||||
// user's configuration turns it off. Desktop builds keep the opt-in default.
|
||||
#if defined(__ANDROID__)
|
||||
inline constexpr bool kVrEnabledDefault = true;
|
||||
#else
|
||||
inline constexpr bool kVrEnabledDefault = false;
|
||||
#endif
|
||||
|
||||
void ConfigurePolicy(bool enabled) noexcept {
|
||||
MkwVRPolicyReset();
|
||||
MkwVRPolicyConfig config{};
|
||||
@@ -54,7 +74,15 @@ void ConfigurePolicy(bool enabled) noexcept {
|
||||
MkwVRFirstPersonApplyConfiguredSettings();
|
||||
}
|
||||
|
||||
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
|
||||
#if MKW_OPENXR_GRAPHICS_BACKEND
|
||||
|
||||
#if defined(_WIN32)
|
||||
using GraphicsBackend = OpenXRD3D12Backend;
|
||||
inline constexpr const char* kGraphicsBackendName = "D3D12";
|
||||
#else
|
||||
using GraphicsBackend = OpenXRVulkanBackend;
|
||||
inline constexpr const char* kGraphicsBackendName = "Vulkan";
|
||||
#endif
|
||||
|
||||
struct Quaternion {
|
||||
float x = 0.0f;
|
||||
@@ -240,17 +268,16 @@ public:
|
||||
last_error_.clear();
|
||||
}
|
||||
if (graphics_retained_) {
|
||||
SetError("OpenXR cannot be restarted after an unfenceable D3D12 submission");
|
||||
SetError(std::string("OpenXR cannot be restarted after an unfenceable ") +
|
||||
kGraphicsBackendName + " submission");
|
||||
return OpenXRStartupResult::Unavailable;
|
||||
}
|
||||
requested_ = RuntimeConfigFile::VrEnabled(false);
|
||||
requested_ = RuntimeConfigFile::VrEnabled(kVrEnabledDefault);
|
||||
ConfigurePolicy(requested_);
|
||||
if (!requested_) {
|
||||
return OpenXRStartupResult::Disabled;
|
||||
}
|
||||
if (aurora_config.desiredBackend != BACKEND_AUTO &&
|
||||
aurora_config.desiredBackend != BACKEND_D3D12) {
|
||||
SetError("OpenXR currently requires the D3D12 graphics backend on Windows");
|
||||
if (!BackendMatchesConfiguredGraphicsApi(aurora_config)) {
|
||||
return OpenXRStartupResult::Unavailable;
|
||||
}
|
||||
|
||||
@@ -259,27 +286,56 @@ public:
|
||||
level == OpenXRLogLevel::Warning ? "warning" : "info";
|
||||
RT_LOG(RT_TAG_RUNTIME) << "[openxr::" << name << "] " << message << std::endl;
|
||||
};
|
||||
#if defined(__ANDROID__)
|
||||
{
|
||||
std::string loader_error;
|
||||
if (!OpenXRAndroidInitializeLoader(logger_, &loader_error)) {
|
||||
SetError("OpenXR Android loader initialization failed: " + loader_error);
|
||||
return OpenXRStartupResult::Unavailable;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
runtime_ = std::make_unique<OpenXRRuntime>(logger_);
|
||||
backend_ = std::make_unique<OpenXRD3D12Backend>(logger_);
|
||||
backend_ = std::make_unique<GraphicsBackend>(logger_);
|
||||
|
||||
OpenXRConfig config{};
|
||||
config.application_name = aurora_config.appName != nullptr ? aurora_config.appName
|
||||
: "WiiCompiled";
|
||||
config.engine_name = "Aurora";
|
||||
config.resolution_scale = RuntimeConfigFile::VrRenderScale(1.0f);
|
||||
#if defined(_WIN32)
|
||||
config.required_extensions = {"XR_KHR_D3D12_enable"};
|
||||
config.optional_extensions = {"XR_KHR_win32_convert_performance_counter_time", "XR_FB_display_refresh_rate"};
|
||||
config.optional_extensions = {"XR_KHR_win32_convert_performance_counter_time",
|
||||
"XR_FB_display_refresh_rate"};
|
||||
#else
|
||||
// Either Vulkan binding extension is acceptable; the backend picks
|
||||
// whichever the runtime enabled, preferring enable2.
|
||||
config.required_extensions = {"XR_KHR_android_create_instance"};
|
||||
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"};
|
||||
config.instance_create_next = OpenXRAndroidInstanceCreateNext();
|
||||
#endif
|
||||
if (!runtime_->Initialize(config)) {
|
||||
SetError("OpenXR instance initialization failed: " + runtime_->LastError().message);
|
||||
ResetPreparedObjects();
|
||||
return OpenXRStartupResult::Unavailable;
|
||||
}
|
||||
const auto& extensions = runtime_->EnabledExtensions();
|
||||
if (std::find(extensions.begin(), extensions.end(),
|
||||
"XR_KHR_win32_convert_performance_counter_time") != extensions.end()) {
|
||||
const auto has_extension = [&](const char* name) {
|
||||
return std::find(extensions.begin(), extensions.end(), name) != extensions.end();
|
||||
};
|
||||
#if defined(_WIN32)
|
||||
if (has_extension("XR_KHR_win32_convert_performance_counter_time")) {
|
||||
runtime_->LoadFunction("xrConvertTimeToWin32PerformanceCounterKHR", &convert_display_time_);
|
||||
}
|
||||
if (std::find(extensions.begin(), extensions.end(), "XR_FB_display_refresh_rate") != extensions.end()) {
|
||||
#else
|
||||
if (has_extension("XR_KHR_convert_timespec_time")) {
|
||||
runtime_->LoadFunction("xrConvertTimeToTimespecTimeKHR", &convert_display_time_);
|
||||
}
|
||||
#endif
|
||||
if (has_extension("XR_FB_display_refresh_rate")) {
|
||||
runtime_->LoadFunction("xrGetDisplayRefreshRateFB", &get_display_refresh_rate_);
|
||||
}
|
||||
interpolation_available_.store(convert_display_time_ != nullptr, std::memory_order_release);
|
||||
@@ -289,12 +345,7 @@ public:
|
||||
return OpenXRStartupResult::Unavailable;
|
||||
}
|
||||
|
||||
const auto& requirements = backend_->GraphicsRequirements();
|
||||
aurora_config.desiredBackend = BACKEND_D3D12;
|
||||
aurora_config.xrInterop = true;
|
||||
aurora_config.hasD3D12AdapterLuid = true;
|
||||
aurora_config.d3d12AdapterLuidLow = requirements.adapter_luid_low;
|
||||
aurora_config.d3d12AdapterLuidHigh = requirements.adapter_luid_high;
|
||||
ApplyGraphicsRequirements(aurora_config);
|
||||
prepared_ = true;
|
||||
return OpenXRStartupResult::Prepared;
|
||||
}
|
||||
@@ -303,8 +354,9 @@ public:
|
||||
if (!prepared_ || runtime_ == nullptr || backend_ == nullptr) {
|
||||
return !requested_;
|
||||
}
|
||||
if (active_backend != BACKEND_D3D12) {
|
||||
SetError("Aurora could not create the OpenXR-required D3D12 backend");
|
||||
if (active_backend != kRequiredAuroraBackend) {
|
||||
SetError(std::string("Aurora could not create the OpenXR-required ") +
|
||||
kGraphicsBackendName + " backend");
|
||||
ResetPreparedObjects();
|
||||
return false;
|
||||
}
|
||||
@@ -313,6 +365,12 @@ public:
|
||||
ResetPreparedObjects();
|
||||
return false;
|
||||
}
|
||||
input_ = std::make_unique<OpenXRInput>(logger_);
|
||||
if (!input_->Create(*runtime_)) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "OpenXR controller input unavailable: " << input_->LastError()
|
||||
<< std::endl;
|
||||
input_.reset();
|
||||
}
|
||||
|
||||
stop_.store(false, std::memory_order_release);
|
||||
{
|
||||
@@ -334,7 +392,8 @@ public:
|
||||
ResetPreparedObjects();
|
||||
return false;
|
||||
}
|
||||
RT_LOG(RT_TAG_RUNTIME) << "OpenXR asynchronous D3D12 presentation started" << std::endl;
|
||||
RT_LOG(RT_TAG_RUNTIME) << "OpenXR asynchronous " << kGraphicsBackendName
|
||||
<< " presentation started" << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -426,17 +485,51 @@ private:
|
||||
AuroraStereoFrame frame{};
|
||||
};
|
||||
|
||||
#if defined(_WIN32)
|
||||
static constexpr AuroraBackend kRequiredAuroraBackend = BACKEND_D3D12;
|
||||
#else
|
||||
static constexpr AuroraBackend kRequiredAuroraBackend = BACKEND_VULKAN;
|
||||
#endif
|
||||
|
||||
bool BackendMatchesConfiguredGraphicsApi(const AuroraConfig& aurora_config) {
|
||||
if (aurora_config.desiredBackend == BACKEND_AUTO ||
|
||||
aurora_config.desiredBackend == kRequiredAuroraBackend) {
|
||||
return true;
|
||||
}
|
||||
SetError(std::string("OpenXR requires the ") + kGraphicsBackendName +
|
||||
" graphics backend on this platform");
|
||||
return false;
|
||||
}
|
||||
|
||||
void ApplyGraphicsRequirements(AuroraConfig& aurora_config) {
|
||||
aurora_config.desiredBackend = kRequiredAuroraBackend;
|
||||
aurora_config.xrInterop = true;
|
||||
#if defined(_WIN32)
|
||||
const auto& requirements = backend_->GraphicsRequirements();
|
||||
aurora_config.hasD3D12AdapterLuid = true;
|
||||
aurora_config.d3d12AdapterLuidLow = requirements.adapter_luid_low;
|
||||
aurora_config.d3d12AdapterLuidHigh = requirements.adapter_luid_high;
|
||||
#endif
|
||||
}
|
||||
|
||||
void ResetPreparedObjects() {
|
||||
ShutdownOrRetainGraphicsObjects();
|
||||
input_.reset();
|
||||
backend_.reset();
|
||||
runtime_.reset();
|
||||
prepared_ = false;
|
||||
}
|
||||
|
||||
bool ShutdownOrRetainGraphicsObjects() noexcept {
|
||||
if (input_ != nullptr) {
|
||||
// Actions belong to the session and must go before it does.
|
||||
input_->Destroy();
|
||||
input_.reset();
|
||||
}
|
||||
if (backend_ != nullptr && !backend_->Shutdown()) {
|
||||
RT_LOG(RT_TAG_RUNTIME)
|
||||
<< "OpenXR D3D12 queue completion is unknown; retaining the backend, "
|
||||
<< "OpenXR " << kGraphicsBackendName
|
||||
<< " queue completion is unknown; retaining the backend, "
|
||||
"runtime, session, and graphics resources until process exit"
|
||||
<< std::endl;
|
||||
(void)backend_.release();
|
||||
@@ -468,6 +561,11 @@ private:
|
||||
}
|
||||
|
||||
void PacingThread() noexcept {
|
||||
#if defined(__ANDROID__)
|
||||
if (runtime_ != nullptr) {
|
||||
OpenXRAndroidRegisterThread(*runtime_, OpenXRAndroidThreadType::RendererMain);
|
||||
}
|
||||
#endif
|
||||
bool fatal = false;
|
||||
bool presentation_logged = false;
|
||||
VRPresentationMode logged_presentation = VRPresentationMode::Desktop;
|
||||
@@ -489,7 +587,7 @@ private:
|
||||
}
|
||||
}
|
||||
if (events == OpenXREventStatus::ExitRequested) {
|
||||
SetError("OpenXR runtime requested session exit; continuing on the desktop mirror");
|
||||
SetError("OpenXR runtime requested session exit; continuing on the mirror output");
|
||||
break;
|
||||
}
|
||||
if (events == OpenXREventStatus::Error) {
|
||||
@@ -525,10 +623,10 @@ private:
|
||||
<< ", bindings=0x" << std::hex << policy.available_bindings
|
||||
<< std::dec << std::endl;
|
||||
}
|
||||
OpenXRD3D12Presentation presentation{};
|
||||
OpenXRPresentation presentation{};
|
||||
const bool immersive = policy.presentation == VRPresentationMode::ImmersiveRace;
|
||||
presentation.mode = immersive ? OpenXRD3D12FrameMode::ImmersiveProjection
|
||||
: OpenXRD3D12FrameMode::VirtualScreen;
|
||||
presentation.mode = immersive ? OpenXRFrameMode::ImmersiveProjection
|
||||
: OpenXRFrameMode::VirtualScreen;
|
||||
presentation.quad_distance_meters = policy.config.hud_distance_meters;
|
||||
presentation.quad_width_meters = policy.config.hud_width_meters;
|
||||
|
||||
@@ -537,23 +635,26 @@ private:
|
||||
const uint32_t interpolation_target = frame_interpolation_fps_.load(std::memory_order_relaxed);
|
||||
SetInterpolationActive(immersive && FrameInterpolationAvailable() && interpolation_target != 0);
|
||||
|
||||
OpenXRD3D12Frame frame{};
|
||||
const OpenXRD3D12BeginStatus begin = backend_->BeginFrame(presentation, frame);
|
||||
if (begin == OpenXRD3D12BeginStatus::SessionNotRunning) {
|
||||
OpenXRBackendFrame frame{};
|
||||
const OpenXRBeginStatus begin = backend_->BeginFrame(presentation, frame);
|
||||
if (begin == OpenXRBeginStatus::SessionNotRunning) {
|
||||
MkwVRPolicySetSessionActive(false);
|
||||
continue;
|
||||
}
|
||||
if (begin == OpenXRD3D12BeginStatus::ExitRequested) {
|
||||
SetError("OpenXR runtime requested session exit; continuing on the desktop mirror");
|
||||
if (begin == OpenXRBeginStatus::ExitRequested) {
|
||||
SetError("OpenXR runtime requested session exit; continuing on the mirror output");
|
||||
break;
|
||||
}
|
||||
if (begin == OpenXRD3D12BeginStatus::Error) {
|
||||
if (begin == OpenXRBeginStatus::Error) {
|
||||
SetError(backend_->LastError());
|
||||
fatal = true;
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateFrameTiming(frame.xr_frame);
|
||||
if (input_ != nullptr) {
|
||||
input_->Sync(frame.xr_frame.predicted_display_time);
|
||||
}
|
||||
// Both of these read this frame's located head pose and must run
|
||||
// before FinishFrame submits a layer built from it.
|
||||
ServiceRecenterRequest();
|
||||
@@ -588,16 +689,16 @@ private:
|
||||
}
|
||||
aurora_notify_stereo_frame();
|
||||
|
||||
OpenXRD3D12SubmissionStatus submission = OpenXRD3D12SubmissionStatus::Timeout;
|
||||
OpenXRSubmissionStatus submission = OpenXRSubmissionStatus::Timeout;
|
||||
bool canceled_before_encode = false;
|
||||
const auto cancel_after =
|
||||
std::chrono::steady_clock::now() + std::chrono::milliseconds(50);
|
||||
while (!stop_.load(std::memory_order_acquire) &&
|
||||
submission == OpenXRD3D12SubmissionStatus::Timeout) {
|
||||
submission == OpenXRSubmissionStatus::Timeout) {
|
||||
// Fresh rendering wakes us immediately. A 50 ms keep-alive
|
||||
// protects stalls without issuing eager repeats during GPU work.
|
||||
submission = backend_->WaitForSubmission(frame, 50);
|
||||
if (submission == OpenXRD3D12SubmissionStatus::Timeout) {
|
||||
if (submission == OpenXRSubmissionStatus::Timeout) {
|
||||
// A pause, minimized window, or guest stall may leave no GX
|
||||
// frame to consume this packet. Withdraw it, then cancel the
|
||||
// matching bridge target only if Encode has not taken ownership.
|
||||
@@ -632,12 +733,13 @@ private:
|
||||
if (fatal) {
|
||||
break;
|
||||
}
|
||||
const bool submit = submission == OpenXRD3D12SubmissionStatus::Success;
|
||||
const bool submit = submission == OpenXRSubmissionStatus::Success;
|
||||
if (!backend_->FinishFrame(frame, submit)) {
|
||||
SetError(backend_->LastError());
|
||||
fatal = true;
|
||||
} else if (!submit) {
|
||||
SetError("Aurora's D3D12 stereo copy failed; continuing on the desktop mirror");
|
||||
SetError(std::string("Aurora's ") + kGraphicsBackendName +
|
||||
" stereo copy failed; continuing on the mirror output");
|
||||
fatal = true;
|
||||
} else {
|
||||
++timing_submissions_;
|
||||
@@ -665,7 +767,7 @@ private:
|
||||
ShutdownOrRetainGraphicsObjects();
|
||||
}
|
||||
|
||||
void BuildPublishedFrame(const OpenXRD3D12Frame& source, bool immersive,
|
||||
void BuildPublishedFrame(const OpenXRBackendFrame& source, bool immersive,
|
||||
float units_per_meter, uint64_t content_tag) noexcept {
|
||||
ApplyPendingReferenceSpaceChange(source.xr_frame);
|
||||
auto& destination = published_frame_.frame;
|
||||
@@ -722,8 +824,8 @@ private:
|
||||
// Anchors the menu screen in the application space and holds it there. The
|
||||
// pose is captured once, from the first frame whose head pose is good enough
|
||||
// to place it, and released again by a recenter or an origin change.
|
||||
void UpdateVirtualScreenPose(OpenXRD3D12Frame& frame) noexcept {
|
||||
if (frame.presentation.mode != OpenXRD3D12FrameMode::VirtualScreen) {
|
||||
void UpdateVirtualScreenPose(OpenXRBackendFrame& frame) noexcept {
|
||||
if (frame.presentation.mode != OpenXRFrameMode::VirtualScreen) {
|
||||
return;
|
||||
}
|
||||
constexpr XrViewStateFlags kPoseUsable =
|
||||
@@ -775,17 +877,31 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
// Converts the compositor's predicted display time onto the runtime's
|
||||
// steady clock, which is what Aurora's interpolation deadlines are paced by.
|
||||
uint64_t DisplayTimeNanos(XrTime display_time) noexcept {
|
||||
if (convert_display_time_ == nullptr) return 0;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto now_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(now.time_since_epoch()).count();
|
||||
#if defined(_WIN32)
|
||||
LARGE_INTEGER display_counter{}, counter{}, frequency{};
|
||||
if (XR_FAILED(convert_display_time_(runtime_->Instance(), display_time, &display_counter)) ||
|
||||
!QueryPerformanceFrequency(&frequency) || frequency.QuadPart <= 0 ||
|
||||
!QueryPerformanceCounter(&counter)) return 0;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto now_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(now.time_since_epoch()).count();
|
||||
const auto delta = static_cast<int64_t>(
|
||||
(static_cast<double>(display_counter.QuadPart) - static_cast<double>(counter.QuadPart)) *
|
||||
1.0e9 / static_cast<double>(frequency.QuadPart));
|
||||
#else
|
||||
// XR_KHR_convert_timespec_time yields CLOCK_MONOTONIC, the clock behind
|
||||
// libc++'s steady_clock, so the delta is measured on that clock too.
|
||||
timespec display_spec{};
|
||||
timespec now_spec{};
|
||||
if (XR_FAILED(convert_display_time_(runtime_->Instance(), display_time, &display_spec)) ||
|
||||
clock_gettime(CLOCK_MONOTONIC, &now_spec) != 0) return 0;
|
||||
const int64_t display_ns = static_cast<int64_t>(display_spec.tv_sec) * 1'000'000'000ll + display_spec.tv_nsec;
|
||||
const int64_t monotonic_ns = static_cast<int64_t>(now_spec.tv_sec) * 1'000'000'000ll + now_spec.tv_nsec;
|
||||
const int64_t delta = display_ns - monotonic_ns;
|
||||
#endif
|
||||
return now_ns + delta > 0 ? static_cast<uint64_t>(now_ns + delta) : 0;
|
||||
}
|
||||
|
||||
@@ -810,7 +926,8 @@ private:
|
||||
|
||||
OpenXRLogCallback logger_;
|
||||
std::unique_ptr<OpenXRRuntime> runtime_;
|
||||
std::unique_ptr<OpenXRD3D12Backend> backend_;
|
||||
std::unique_ptr<GraphicsBackend> backend_;
|
||||
std::unique_ptr<OpenXRInput> input_;
|
||||
std::thread pacing_thread_;
|
||||
std::atomic_bool stop_{false};
|
||||
std::atomic_bool running_{false};
|
||||
@@ -827,7 +944,11 @@ private:
|
||||
std::chrono::steady_clock::time_point timing_start_ = std::chrono::steady_clock::now();
|
||||
uint32_t timing_submissions_ = 0;
|
||||
PFN_xrGetDisplayRefreshRateFB get_display_refresh_rate_ = nullptr;
|
||||
#if defined(_WIN32)
|
||||
using ConvertDisplayTime = XrResult (XRAPI_PTR*)(XrInstance, XrTime, LARGE_INTEGER*);
|
||||
#else
|
||||
using ConvertDisplayTime = XrResult (XRAPI_PTR*)(XrInstance, XrTime, struct timespec*);
|
||||
#endif
|
||||
ConvertDisplayTime convert_display_time_ = nullptr;
|
||||
std::atomic<PublishedFrame*> published_{nullptr};
|
||||
PublishedFrame published_frame_{};
|
||||
@@ -849,7 +970,7 @@ private:
|
||||
bool graphics_retained_ = false;
|
||||
};
|
||||
|
||||
#endif // defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
|
||||
#endif // MKW_OPENXR_GRAPHICS_BACKEND
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -857,32 +978,32 @@ OpenXRStartupResult OpenXRPrepareAurora(AuroraConfig& config) {
|
||||
#if !defined(MKW_ENABLE_OPENXR)
|
||||
(void)config;
|
||||
ConfigurePolicy(false);
|
||||
return RuntimeConfigFile::VrEnabled(false) ? OpenXRStartupResult::Unavailable
|
||||
: OpenXRStartupResult::Disabled;
|
||||
#elif defined(_WIN32)
|
||||
return RuntimeConfigFile::VrEnabled(kVrEnabledDefault) ? OpenXRStartupResult::Unavailable
|
||||
: OpenXRStartupResult::Disabled;
|
||||
#elif MKW_OPENXR_GRAPHICS_BACKEND
|
||||
return OpenXRIntegration::Get().Prepare(config);
|
||||
#else
|
||||
ConfigurePolicy(RuntimeConfigFile::VrEnabled(false));
|
||||
if (!RuntimeConfigFile::VrEnabled(false)) {
|
||||
(void)config;
|
||||
ConfigurePolicy(RuntimeConfigFile::VrEnabled(kVrEnabledDefault));
|
||||
if (!RuntimeConfigFile::VrEnabled(kVrEnabledDefault)) {
|
||||
return OpenXRStartupResult::Disabled;
|
||||
}
|
||||
const OpenXRVulkanCapabilityInfo capability = OpenXRVulkanBackend::DawnInteropCapability();
|
||||
RT_LOG(RT_TAG_RUNTIME) << "OpenXR Vulkan unavailable: " << capability.reason << std::endl;
|
||||
RT_LOG(RT_TAG_RUNTIME) << "OpenXR is not wired to a graphics backend on this platform" << std::endl;
|
||||
return OpenXRStartupResult::Unavailable;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool OpenXRStartAfterAurora(AuroraBackend active_backend) {
|
||||
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
|
||||
#if MKW_OPENXR_GRAPHICS_BACKEND
|
||||
return OpenXRIntegration::Get().Start(active_backend);
|
||||
#else
|
||||
(void)active_backend;
|
||||
return !RuntimeConfigFile::VrEnabled(false);
|
||||
return !RuntimeConfigFile::VrEnabled(kVrEnabledDefault);
|
||||
#endif
|
||||
}
|
||||
|
||||
void OpenXRShutdownBeforeAurora() noexcept {
|
||||
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
|
||||
#if MKW_OPENXR_GRAPHICS_BACKEND
|
||||
OpenXRIntegration::Get().Shutdown();
|
||||
#else
|
||||
MkwVRPolicySetSessionActive(false);
|
||||
@@ -890,13 +1011,13 @@ void OpenXRShutdownBeforeAurora() noexcept {
|
||||
}
|
||||
|
||||
void OpenXRServiceProducerFrameBoundary() noexcept {
|
||||
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
|
||||
#if MKW_OPENXR_GRAPHICS_BACKEND
|
||||
OpenXRIntegration::Get().ServiceProducerFrameBoundary();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool OpenXRIsRunning() noexcept {
|
||||
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
|
||||
#if MKW_OPENXR_GRAPHICS_BACKEND
|
||||
return OpenXRIntegration::Get().IsRunning();
|
||||
#else
|
||||
return false;
|
||||
@@ -904,13 +1025,13 @@ bool OpenXRIsRunning() noexcept {
|
||||
}
|
||||
|
||||
void OpenXRRequestRecenter() noexcept {
|
||||
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
|
||||
#if MKW_OPENXR_GRAPHICS_BACKEND
|
||||
OpenXRIntegration::Get().RequestRecenter();
|
||||
#endif
|
||||
}
|
||||
|
||||
void OpenXRSetLeanBackDegrees(float degrees) noexcept {
|
||||
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
|
||||
#if MKW_OPENXR_GRAPHICS_BACKEND
|
||||
OpenXRIntegration::Get().SetLeanBackDegrees(degrees);
|
||||
#else
|
||||
(void)degrees;
|
||||
@@ -918,7 +1039,7 @@ void OpenXRSetLeanBackDegrees(float degrees) noexcept {
|
||||
}
|
||||
|
||||
void OpenXRSetFrameInterpolationFps(uint32_t target) noexcept {
|
||||
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
|
||||
#if MKW_OPENXR_GRAPHICS_BACKEND
|
||||
OpenXRIntegration::Get().SetFrameInterpolationFps(target);
|
||||
#else
|
||||
(void)target;
|
||||
@@ -926,7 +1047,7 @@ void OpenXRSetFrameInterpolationFps(uint32_t target) noexcept {
|
||||
}
|
||||
|
||||
OpenXRFrameTiming OpenXRGetFrameTiming() noexcept {
|
||||
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
|
||||
#if MKW_OPENXR_GRAPHICS_BACKEND
|
||||
return OpenXRIntegration::Get().FrameTiming();
|
||||
#else
|
||||
return {};
|
||||
@@ -934,7 +1055,7 @@ OpenXRFrameTiming OpenXRGetFrameTiming() noexcept {
|
||||
}
|
||||
|
||||
bool OpenXRFrameInterpolationAvailable() noexcept {
|
||||
#if defined(MKW_ENABLE_OPENXR) && defined(_WIN32)
|
||||
#if MKW_OPENXR_GRAPHICS_BACKEND
|
||||
return OpenXRIntegration::Get().FrameInterpolationAvailable();
|
||||
#else
|
||||
return false;
|
||||
@@ -944,10 +1065,10 @@ bool OpenXRFrameInterpolationAvailable() noexcept {
|
||||
std::string OpenXRLastError() {
|
||||
#if !defined(MKW_ENABLE_OPENXR)
|
||||
return "this build was compiled without OpenXR support";
|
||||
#elif defined(_WIN32)
|
||||
#elif MKW_OPENXR_GRAPHICS_BACKEND
|
||||
return OpenXRIntegration::Get().LastError();
|
||||
#else
|
||||
return OpenXRVulkanBackend::DawnInteropCapability().reason;
|
||||
return "OpenXR is not wired to a graphics backend on this platform";
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -215,6 +215,7 @@ bool OpenXRRuntime::CreateInstance() {
|
||||
}
|
||||
|
||||
XrInstanceCreateInfo create_info{XR_TYPE_INSTANCE_CREATE_INFO};
|
||||
create_info.next = m_config.instance_create_next;
|
||||
CopyOpenXRName(create_info.applicationInfo.applicationName,
|
||||
XR_MAX_APPLICATION_NAME_SIZE, m_config.application_name);
|
||||
create_info.applicationInfo.applicationVersion = m_config.application_version;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -166,7 +166,7 @@ return command switch
|
||||
{
|
||||
"help" or "--help" or "-h" or "-?" => ShowGlobalHelp(),
|
||||
"info" or "--info" or "--version" => RunInfo(),
|
||||
"generate-data-init" => RunGenerateDataInit(),
|
||||
"generate-data-init" => RunGenerateDataInit(tail),
|
||||
"translate-recursive" => RunTranslateRecursive(tail),
|
||||
"translate-mod" => RunTranslateMod(tail),
|
||||
"emit-build-shards" => RunEmitBuildShards(tail),
|
||||
@@ -1647,6 +1647,10 @@ void PreserveTranslateModFailureDiagnostics(string stagingDirectory, string publ
|
||||
|
||||
int RunTranslateModCore(string[] argsTail, string? outputDirectoryOverride)
|
||||
{
|
||||
if (ParseTargetOsOption(argsTail) is not { } modTargetOs)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
// Wall-clock accounting mirroring RunTranslateRecursive's LogPhase output.
|
||||
var modPhaseStart = DateTime.Now;
|
||||
var modLastPhaseSeconds = 0d;
|
||||
@@ -1964,7 +1968,8 @@ int RunTranslateModCore(string[] argsTail, string? outputDirectoryOverride)
|
||||
"cpp",
|
||||
"mod_data_patches_blobs"),
|
||||
profile?.Riivolution?.Xml,
|
||||
riivolutionOptions);
|
||||
riivolutionOptions,
|
||||
targetOs: modTargetOs);
|
||||
|
||||
if (emitCpp && cppFailureCount == 0)
|
||||
{
|
||||
@@ -2943,9 +2948,13 @@ int RunEmitBaseManifest(string[] argsTail)
|
||||
return 0;
|
||||
}
|
||||
|
||||
int RunGenerateDataInit()
|
||||
int RunGenerateDataInit(string[] argsTail)
|
||||
{
|
||||
var loadedProject = RequireProject();
|
||||
if (ParseTargetOsOption(argsTail) is not { } dataInitTargetOs)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
var output = loadedProject.Output.DataInitializer;
|
||||
var runtimeConfigOutput = loadedProject.Output.RuntimeConfig;
|
||||
var dolPath = loadedProject.Inputs.Dol.Path;
|
||||
@@ -2978,7 +2987,9 @@ int RunGenerateDataInit()
|
||||
relImage,
|
||||
output,
|
||||
loadedProject.Identity.DisplayName,
|
||||
relPath is null ? "rel_module" : Path.GetFileNameWithoutExtension(relPath));
|
||||
relPath is null ? "rel_module" : Path.GetFileNameWithoutExtension(relPath),
|
||||
targetOs: dataInitTargetOs);
|
||||
Console.WriteLine($"[translator] Blob assembly flavour: {dataInitTargetOs}");
|
||||
|
||||
// Emit the guest symbol table for crash-report symbolization. The function
|
||||
// map is already the project's authoritative name source; the runtime only
|
||||
@@ -3712,7 +3723,7 @@ static (string? Positional, CommandOption[] Options)? CommandSpec(string command
|
||||
{
|
||||
"info" or "--info" or "--version" =>
|
||||
(null, Array.Empty<CommandOption>()),
|
||||
"generate-data-init" => (null, Array.Empty<CommandOption>()),
|
||||
"generate-data-init" => (null, new CommandOption[] { new("--target-os", "windows|macos|linux|android") }),
|
||||
"translate-recursive" => ("<start_addr>", new CommandOption[]
|
||||
{
|
||||
new("--outdir", "path"),
|
||||
@@ -3736,7 +3747,8 @@ static (string? Positional, CommandOption[] Options)? CommandSpec(string command
|
||||
new("--threads", "N"),
|
||||
new("--retro-wfc-payload", "path-or-url"),
|
||||
new("--skip-retro-wfc"),
|
||||
new("--emit-cpp")
|
||||
new("--emit-cpp"),
|
||||
new("--target-os", "windows|macos|linux|android")
|
||||
}),
|
||||
"check-base-mod-awareness" => (null, new CommandOption[]
|
||||
{
|
||||
@@ -3950,6 +3962,27 @@ static int ParseInt(string text) =>
|
||||
? int.Parse(text[2..], NumberStyles.HexNumber, CultureInfo.InvariantCulture)
|
||||
: int.Parse(text, CultureInfo.InvariantCulture);
|
||||
|
||||
/// <summary>
|
||||
/// The object-file flavour for hand-written blob assembly. Absent, the translator's own host is
|
||||
/// assumed (the historical behaviour); a cross-compiled product names its target explicitly.
|
||||
/// Null means the value was present but unrecognised, and the error has already been printed.
|
||||
/// </summary>
|
||||
static AssemblyTargetOs? ParseTargetOsOption(string[] argsTail)
|
||||
{
|
||||
var text = OptionValue(argsTail, "--target-os");
|
||||
if (text is null)
|
||||
{
|
||||
return AssemblyTargetOsSyntax.Host();
|
||||
}
|
||||
if (AssemblyTargetOsSyntax.TryParse(text, out var target))
|
||||
{
|
||||
return target;
|
||||
}
|
||||
Console.Error.WriteLine(
|
||||
$"[translator] --target-os '{text}' is not recognised; expected one of {AssemblyTargetOsSyntax.AcceptedNames}.");
|
||||
return null;
|
||||
}
|
||||
|
||||
static string? OptionValue(string[] argsTail, string name)
|
||||
{
|
||||
var index = Array.IndexOf(argsTail, name);
|
||||
|
||||
@@ -24,7 +24,8 @@ public static class DataSectionGenerator
|
||||
string outputPath,
|
||||
string projectName = "PowerPC DOL",
|
||||
string relName = "rel_module",
|
||||
string? blobReferenceDirectory = null)
|
||||
string? blobReferenceDirectory = null,
|
||||
AssemblyTargetOs? targetOs = null)
|
||||
{
|
||||
var sections = new List<DataSectionEntry>();
|
||||
|
||||
@@ -65,7 +66,8 @@ public static class DataSectionGenerator
|
||||
sections,
|
||||
blobDirectory,
|
||||
blobReferenceDirectory ?? blobDirectory,
|
||||
blobAssemblyPath);
|
||||
blobAssemblyPath,
|
||||
targetOs ?? AssemblyTargetOsSyntax.Host());
|
||||
WriteSourceFile(sections, outputPath, dol.BssAddress, dol.BssSize, projectName);
|
||||
}
|
||||
|
||||
@@ -90,7 +92,8 @@ public static class DataSectionGenerator
|
||||
List<DataSectionEntry> sections,
|
||||
string blobDirectory,
|
||||
string blobReferenceDirectory,
|
||||
string assemblyPath)
|
||||
string assemblyPath,
|
||||
AssemblyTargetOs targetOs)
|
||||
{
|
||||
var blobs = sections.Select(section => new AssemblyBlob(
|
||||
$"{section.Name}.bin",
|
||||
@@ -103,6 +106,7 @@ public static class DataSectionGenerator
|
||||
blobDirectory,
|
||||
blobReferenceDirectory,
|
||||
blobs,
|
||||
targetOs,
|
||||
"// AUTO-GENERATED - DO NOT EDIT",
|
||||
"// Binary DOL/REL section payloads for data_sections_init.cpp.");
|
||||
}
|
||||
|
||||
@@ -16,21 +16,18 @@ internal static class AssemblyBlobWriter
|
||||
string blobDirectory,
|
||||
string blobReferenceDirectory,
|
||||
IReadOnlyList<AssemblyBlob> blobs,
|
||||
AssemblyTargetOs targetOs,
|
||||
params string[] headerLines)
|
||||
{
|
||||
Directory.CreateDirectory(blobDirectory);
|
||||
var expectedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var assembly = new StringBuilder();
|
||||
foreach (var header in headerLines) assembly.AppendLine(header);
|
||||
// PE/COFF (Windows), Mach-O (macOS), and ELF (Linux) spell a read-only data section
|
||||
// differently in GNU-as syntax. The build always targets
|
||||
// whichever platform the translator itself runs on (there is no cross-compilation
|
||||
// support), so that's what this picks the section syntax from.
|
||||
assembly.AppendLine(OperatingSystem.IsWindows()
|
||||
? ".section .rdata,\"dr\""
|
||||
: OperatingSystem.IsMacOS()
|
||||
? ".section __TEXT,__const"
|
||||
: ".section .rodata,\"a\",@progbits");
|
||||
// PE/COFF (Windows), Mach-O (macOS), and ELF (Linux/Android) spell a read-only data
|
||||
// section differently in GNU-as syntax. The caller names the object-file flavour the
|
||||
// product is assembled for; it defaults to the translator's own host, and a cross-compiled
|
||||
// product (the Android build generated on a Windows host) passes its target explicitly.
|
||||
assembly.AppendLine(AssemblyTargetOsSyntax.SectionDirective(targetOs));
|
||||
assembly.AppendLine();
|
||||
|
||||
foreach (var blob in blobs)
|
||||
@@ -41,10 +38,7 @@ internal static class AssemblyBlobWriter
|
||||
var hash = ChecksumUtilities.Sha256Hex(blob.Data.Span);
|
||||
assembly.AppendLine($"// {blob.Comment}; sha256={hash}");
|
||||
assembly.AppendLine(".p2align 4");
|
||||
// C/C++ external symbols carry a leading underscore in Mach-O,
|
||||
// unlike ELF and COFF. The generated C++ still names the symbol
|
||||
// without that ABI decoration, so emit the platform spelling here.
|
||||
var assemblySymbol = OperatingSystem.IsMacOS() ? $"_{blob.Symbol}" : blob.Symbol;
|
||||
var assemblySymbol = AssemblyTargetOsSyntax.SymbolName(targetOs, blob.Symbol);
|
||||
assembly.AppendLine($".globl {assemblySymbol}");
|
||||
assembly.AppendLine($"{assemblySymbol}:");
|
||||
var referencePath = Path.Combine(blobReferenceDirectory, blob.FileName);
|
||||
@@ -56,7 +50,7 @@ internal static class AssemblyBlobWriter
|
||||
{
|
||||
if (!expectedFiles.Contains(Path.GetFullPath(stalePath))) File.Delete(stalePath);
|
||||
}
|
||||
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
|
||||
if (AssemblyTargetOsSyntax.EmitsGnuStackNote(targetOs))
|
||||
{
|
||||
// Absence of a .note.GNU-stack section makes the linker assume the oldest, most
|
||||
// conservative default for this object (an executable stack) and warn about it; this
|
||||
@@ -64,7 +58,7 @@ internal static class AssemblyBlobWriter
|
||||
// object linked into the binary already does (the norm on modern toolchains, just not
|
||||
// producible without an explicit section since this file is hand-assembled, not
|
||||
// compiler-emitted).
|
||||
assembly.AppendLine(".section .note.GNU-stack,\"\",@progbits");
|
||||
assembly.AppendLine(AssemblyTargetOsSyntax.GnuStackNoteDirective);
|
||||
}
|
||||
FileOutput.WriteTextIfChanged(assemblyPath, assembly.ToString());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
|
||||
namespace Translator.Core.IO;
|
||||
|
||||
/// <summary>
|
||||
/// The object-file flavour the hand-written blob assembly is assembled into. PE/COFF (Windows),
|
||||
/// Mach-O (macOS) and ELF (Linux and Android) spell a read-only data section, external symbol
|
||||
/// decoration and the stack-note trailer differently in GNU-as syntax. Historically the writer
|
||||
/// keyed this off the operating system the translator itself ran on; a cross-compiled product
|
||||
/// (the Quest build is generated on a Windows host and assembled by the Android NDK) must be able
|
||||
/// to ask for another flavour explicitly.
|
||||
/// </summary>
|
||||
public enum AssemblyTargetOs
|
||||
{
|
||||
Windows,
|
||||
MacOS,
|
||||
Linux,
|
||||
}
|
||||
|
||||
public static class AssemblyTargetOsSyntax
|
||||
{
|
||||
/// <summary>The flavour matching the host the translator is running on.</summary>
|
||||
public static AssemblyTargetOs Host()
|
||||
{
|
||||
if (OperatingSystem.IsWindows()) return AssemblyTargetOs.Windows;
|
||||
if (OperatingSystem.IsMacOS()) return AssemblyTargetOs.MacOS;
|
||||
return AssemblyTargetOs.Linux;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a user-facing name. "android" is accepted as a spelling of the ELF flavour so a
|
||||
/// build script can name the platform it is actually producing.
|
||||
/// </summary>
|
||||
public static bool TryParse(string? text, out AssemblyTargetOs target)
|
||||
{
|
||||
switch (text?.Trim().ToLowerInvariant())
|
||||
{
|
||||
case "windows":
|
||||
case "win32":
|
||||
case "mingw":
|
||||
target = AssemblyTargetOs.Windows;
|
||||
return true;
|
||||
case "macos":
|
||||
case "darwin":
|
||||
target = AssemblyTargetOs.MacOS;
|
||||
return true;
|
||||
case "linux":
|
||||
case "android":
|
||||
case "elf":
|
||||
target = AssemblyTargetOs.Linux;
|
||||
return true;
|
||||
default:
|
||||
target = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public const string AcceptedNames = "windows, macos, linux, android";
|
||||
|
||||
/// <summary>The directive that opens the read-only payload section.</summary>
|
||||
public static string SectionDirective(AssemblyTargetOs target) => target switch
|
||||
{
|
||||
AssemblyTargetOs.Windows => ".section .rdata,\"dr\"",
|
||||
AssemblyTargetOs.MacOS => ".section __TEXT,__const",
|
||||
_ => ".section .rodata,\"a\",@progbits",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// C/C++ external symbols carry a leading underscore in Mach-O, unlike ELF and COFF. The
|
||||
/// generated C++ still names the symbol without that ABI decoration, so the assembly spelling
|
||||
/// is produced here.
|
||||
/// </summary>
|
||||
public static string SymbolName(AssemblyTargetOs target, string symbol) =>
|
||||
target == AssemblyTargetOs.MacOS ? "_" + symbol : symbol;
|
||||
|
||||
/// <summary>
|
||||
/// Only ELF objects need an explicit non-executable-stack note: without one the linker assumes
|
||||
/// the most conservative default (an executable stack) and warns.
|
||||
/// </summary>
|
||||
public static bool EmitsGnuStackNote(AssemblyTargetOs target) => target == AssemblyTargetOs.Linux;
|
||||
|
||||
public const string GnuStackNoteDirective = ".section .note.GNU-stack,\"\",@progbits";
|
||||
}
|
||||
@@ -26,7 +26,8 @@ public static class ModDataPatchWriter
|
||||
uint? retroWfcInitializerSuccessReturnValue = null,
|
||||
string? publishedBlobDirectory = null,
|
||||
string? riivolutionXml = null,
|
||||
IReadOnlyList<ModRiivolutionOption>? riivolutionOptions = null)
|
||||
IReadOnlyList<ModRiivolutionOption>? riivolutionOptions = null,
|
||||
AssemblyTargetOs? targetOs = null)
|
||||
{
|
||||
var blobAssemblyPath = Path.Combine(
|
||||
Path.GetDirectoryName(path) ?? ".",
|
||||
@@ -46,7 +47,8 @@ public static class ModDataPatchWriter
|
||||
blobAssemblyPath,
|
||||
blobDirectory,
|
||||
publishedBlobDirectory ?? blobDirectory,
|
||||
blobs);
|
||||
blobs,
|
||||
targetOs ?? AssemblyTargetOsSyntax.Host());
|
||||
|
||||
var sb = new StringBuilder();
|
||||
var emitRetroWfcInitializer =
|
||||
@@ -216,7 +218,8 @@ public static class ModDataPatchWriter
|
||||
string assemblyPath,
|
||||
string blobDirectory,
|
||||
string blobReferenceDirectory,
|
||||
IReadOnlyList<ModDataBlob> blobs)
|
||||
IReadOnlyList<ModDataBlob> blobs,
|
||||
AssemblyTargetOs targetOs)
|
||||
{
|
||||
var assemblyBlobs = blobs.Select(blob => new AssemblyBlob(
|
||||
blob.FileName,
|
||||
@@ -229,6 +232,7 @@ public static class ModDataPatchWriter
|
||||
blobDirectory,
|
||||
blobReferenceDirectory,
|
||||
assemblyBlobs,
|
||||
targetOs,
|
||||
"// Generated by translator translate-mod.",
|
||||
"// Binary payloads referenced by mod_data_patches.cpp.");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Translator.Core.CodeGen;
|
||||
using Translator.Core.IO;
|
||||
using Xunit;
|
||||
|
||||
namespace Translator.Tests;
|
||||
|
||||
public class AssemblyTargetOsTests
|
||||
{
|
||||
// AppendLine uses the host newline, so compare whole lines rather than LF-delimited substrings.
|
||||
private static string[] Lines(string text) =>
|
||||
text.Split('\n').Select(line => line.TrimEnd('\r')).ToArray();
|
||||
|
||||
private static string GenerateBlobAssembly(AssemblyTargetOs targetOs, string tag)
|
||||
{
|
||||
var dol = SyntheticDolFactory.Create(
|
||||
0x80004000,
|
||||
sections: [SyntheticDolFactory.Data(5, 0x80008000, 1, 2, 3, 4)]);
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), $"mkw_blob_target_{tag}_{Guid.NewGuid():N}");
|
||||
try
|
||||
{
|
||||
var output = Path.Combine(tempDir, "data_sections_init.cpp");
|
||||
DataSectionGenerator.Generate(dol, rel: null, output, targetOs: targetOs);
|
||||
return File.ReadAllText(Path.Combine(tempDir, "data_sections_init_blobs.S"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(tempDir)) Directory.Delete(tempDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("windows", AssemblyTargetOs.Windows)]
|
||||
[InlineData("MacOS", AssemblyTargetOs.MacOS)]
|
||||
[InlineData("linux", AssemblyTargetOs.Linux)]
|
||||
[InlineData("android", AssemblyTargetOs.Linux)]
|
||||
public void ParsesUserFacingNames(string text, AssemblyTargetOs expected)
|
||||
{
|
||||
Assert.True(AssemblyTargetOsSyntax.TryParse(text, out var parsed));
|
||||
Assert.Equal(expected, parsed);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("quest")]
|
||||
[InlineData(null)]
|
||||
public void RejectsUnknownNames(string? text)
|
||||
{
|
||||
Assert.False(AssemblyTargetOsSyntax.TryParse(text, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HostFlavourMatchesTheRunningOperatingSystem()
|
||||
{
|
||||
var expected = OperatingSystem.IsWindows() ? AssemblyTargetOs.Windows
|
||||
: OperatingSystem.IsMacOS() ? AssemblyTargetOs.MacOS
|
||||
: AssemblyTargetOs.Linux;
|
||||
Assert.Equal(expected, AssemblyTargetOsSyntax.Host());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ElfFlavourEmitsRodataUndecoratedSymbolsAndStackNote()
|
||||
{
|
||||
var assembly = GenerateBlobAssembly(AssemblyTargetOs.Linux, "elf");
|
||||
var lines = Lines(assembly);
|
||||
Assert.Contains(".section .rodata,\"a\",@progbits", lines);
|
||||
Assert.Contains(".globl kData__data", lines);
|
||||
Assert.DoesNotContain(".rdata", assembly, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("__TEXT,__const", assembly, StringComparison.Ordinal);
|
||||
Assert.Contains(".section .note.GNU-stack,\"\",@progbits", lines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WindowsFlavourEmitsRdataWithoutStackNote()
|
||||
{
|
||||
var assembly = GenerateBlobAssembly(AssemblyTargetOs.Windows, "win");
|
||||
var lines = Lines(assembly);
|
||||
Assert.Contains(".section .rdata,\"dr\"", lines);
|
||||
Assert.Contains(".globl kData__data", lines);
|
||||
Assert.DoesNotContain(".note.GNU-stack", assembly, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MacOsFlavourDecoratesSymbols()
|
||||
{
|
||||
var assembly = GenerateBlobAssembly(AssemblyTargetOs.MacOS, "mac");
|
||||
var lines = Lines(assembly);
|
||||
Assert.Contains(".section __TEXT,__const", lines);
|
||||
Assert.Contains(".globl _kData__data", lines);
|
||||
Assert.DoesNotContain(".note.GNU-stack", assembly, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user