Added Android Meta Quest Support

This commit is contained in:
iChris4 committed 2026-09-16 22:06:34 +02:00
1 parent 49b8602543
commit e52227baf5
67 files changed
+5662 -176

No files matched your search

+94
View File
@@ -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'
}
+42
View File
@@ -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()
}
+23
View File
@@ -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.
+111
View File
@@ -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 }
+124
View File
@@ -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"))
}
+61
View File
@@ -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>
+58
View File
@@ -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>
+4
View File
@@ -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
}
+5
View File
@@ -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
Binary file not shown.
+8
View File
@@ -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
+248
View File
@@ -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" "$@"
+93
View File
@@ -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
+18
View File
@@ -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")