diff --git a/Launcher/WiiCompiled.Setup.Windows/QuestBuildService.cs b/Launcher/WiiCompiled.Setup.Windows/QuestBuildService.cs
index 5a07e40..93a774c 100644
--- a/Launcher/WiiCompiled.Setup.Windows/QuestBuildService.cs
+++ b/Launcher/WiiCompiled.Setup.Windows/QuestBuildService.cs
@@ -74,7 +74,7 @@ internal sealed class QuestBuildService
var kitDirectory = Path.Combine(questRoot, "kit");
_reporter.Progress(InstallStages.QuestKit, "Reading the game kit from the Quest app...", 2);
var kit = ExtractKit(apkPath, kitDirectory, cancellationToken);
- _reporter.Diagnostic($"Quest game kit {kit.Fingerprint} [{string.Join(", ", kit.Products)}]");
+ _reporter.Diagnostic($"Quest game kit {kit.Fingerprint} [{string.Join(", ", kit.Products)}, CPU {kit.AndroidCpu}]");
if (!kit.Products.Contains(product))
{
throw new InvalidOperationException(
@@ -185,7 +185,7 @@ internal sealed class QuestBuildService
File.Exists(Path.Combine(directory, "sys", "main.dol"));
/// What the extracted kit says it builds: its fingerprint and the products it carries.
- internal sealed record Kit(string Fingerprint, IReadOnlyCollection Products);
+ internal sealed record Kit(string Fingerprint, string AndroidCpu, IReadOnlyCollection Products);
/// Extracts assets/game_kit from the Quest app's APK and reads what it is for.
internal static Kit ExtractKit(string apkPath, string destination, CancellationToken cancellationToken)
@@ -219,11 +219,16 @@ internal sealed class QuestBuildService
{
throw new InvalidDataException("The Quest app's game kit has no fingerprint.");
}
+ if (!document.RootElement.TryGetProperty("androidCpu", out var androidCpu) ||
+ androidCpu.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(androidCpu.GetString()))
+ {
+ throw new InvalidDataException("The Quest app's game kit has no Android CPU target.");
+ }
var products = document.RootElement.TryGetProperty("products", out var value) &&
value.ValueKind == JsonValueKind.Object
? value.EnumerateObject().Select(property => property.Name).ToArray()
: throw new InvalidDataException("The Quest app's game kit lists no games it can build.");
- return new Kit(fingerprint.GetString()!, products);
+ return new Kit(fingerprint.GetString()!, androidCpu.GetString()!, products);
}
private async Task EnsureToolchainAsync(string questRoot, CancellationToken cancellationToken)
diff --git a/Launcher/WiiCompiled.Setup.Windows/SelfTests.cs b/Launcher/WiiCompiled.Setup.Windows/SelfTests.cs
index ed43bc1..beed091 100644
--- a/Launcher/WiiCompiled.Setup.Windows/SelfTests.cs
+++ b/Launcher/WiiCompiled.Setup.Windows/SelfTests.cs
@@ -384,14 +384,14 @@ internal static class SelfTests
using var writer = new StreamWriter(zip.CreateEntry(name).Open());
writer.Write(text);
}
- Add("assets/game_kit/kit.json", "{\"schema\":3,\"fingerprint\":\"abc123\",\"products\":{\"base\":{},\"retro_rewind\":{}}}");
+ Add("assets/game_kit/kit.json", "{\"schema\":3,\"fingerprint\":\"abc123\",\"androidCpu\":\"kryo\",\"products\":{\"base\":{},\"retro_rewind\":{}}}");
Add("assets/game_kit/include/memory.h", "#pragma once");
Add("assets/runtime_resources/dsp_coef.bin", "not the kit");
Add("lib/arm64-v8a/libSDL3.so", "not the kit");
}
var kit = Path.Combine(temp, "kit");
var extracted = QuestBuildService.ExtractKit(apk, kit, CancellationToken.None);
- if (extracted.Fingerprint != "abc123" || !extracted.Products.Contains("base") ||
+ if (extracted.Fingerprint != "abc123" || extracted.AndroidCpu != "kryo" || !extracted.Products.Contains("base") ||
!extracted.Products.Contains("retro_rewind"))
throw new Exception("The kit fingerprint or its games were not read.");
if (!File.Exists(Path.Combine(kit, "include", "memory.h")) ||
diff --git a/android/Build-Quest.ps1 b/android/Build-Quest.ps1
index 772d1a6..fd0e424 100644
--- a/android/Build-Quest.ps1
+++ b/android/Build-Quest.ps1
@@ -75,6 +75,7 @@ if (-not $Dependencies) {
$variant = (Get-Culture).TextInfo.ToTitleCase($Configuration)
$flavour = if ($Headset -eq 'quest1') { 'Quest1' } else { 'ModernQuest' }
+$expectedCpu = if ($Headset -eq 'quest1') { 'kryo' } else { 'cortex-a77' }
$task = "app:assemble$flavour$variant"
$gradleArgs = @(
'--project-dir', $root,
@@ -98,11 +99,16 @@ Add-Type -AssemblyName System.IO.Compression.FileSystem
$archive = [IO.Compression.ZipFile]::OpenRead($apk.FullName)
try {
$gameLibraries = @($archive.Entries | Where-Object { $_.FullName -match '^lib/[^/]+/libmain[^/]*\.so$' })
- $hasKit = @($archive.Entries | Where-Object { $_.FullName -eq 'assets/game_kit/kit.json' }).Count -gt 0
+ $kitEntry = @($archive.Entries | Where-Object { $_.FullName -eq 'assets/game_kit/kit.json' }) | Select-Object -First 1
+ $kitCpu = if ($kitEntry) {
+ $reader = New-Object IO.StreamReader($kitEntry.Open())
+ try { ($reader.ReadToEnd() | ConvertFrom-Json).androidCpu } finally { $reader.Dispose() }
+ } else { '' }
$hasToolchain = @($archive.Entries | Where-Object { $_.FullName -eq 'assets/quest_toolchain/files.zip' }).Count -gt 0
} finally { $archive.Dispose() }
if ($gameLibraries.Count -gt 0) { throw "The APK contains a translated game library: $($gameLibraries.FullName -join ', ')" }
-if (-not $hasKit) { throw 'The APK has no game kit (assets/game_kit/kit.json)' }
+if (-not $kitEntry) { throw 'The APK has no game kit (assets/game_kit/kit.json)' }
+if ($kitCpu -ne $expectedCpu) { throw "The $Headset APK contains a game kit for CPU '$kitCpu', expected '$expectedCpu'" }
if (-not $hasToolchain) { throw 'The APK has no build toolchain (assets/quest_toolchain/files.zip)' }
if ($Install) {
diff --git a/android/Build-QuestGame.ps1 b/android/Build-QuestGame.ps1
index 679d9fb..3dbc754 100644
--- a/android/Build-QuestGame.ps1
+++ b/android/Build-QuestGame.ps1
@@ -6,6 +6,7 @@
# SDK's NDK and ninja:
#
# powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 [-Generated ] [-Kit ]
+# [-Headset modern|quest1] [-Configuration debug|release]
# [-Data ] [-Output ] [-Install]
#
# WiiCompiled Setup's --build-quest runs the copy staged in an installation's BuildWorkspace\android,
@@ -32,6 +33,8 @@ param(
[string]$Sysroot = '',
[string]$Ninja = '',
[string]$BuiltBy = 'android/Build-QuestGame.ps1',
+ [ValidateSet('modern', 'quest1')] [string]$Headset = 'modern',
+ [ValidateSet('debug', 'release')] [string]$Configuration = 'debug',
[ValidateSet('base', 'retro_rewind')] [string]$Product = 'base',
[string]$Mod = '',
[int]$TranslatedJobs = 0,
@@ -44,12 +47,13 @@ $root = $PSScriptRoot
$workspace = (Resolve-Path (Join-Path $root '..')).Path
if (-not $Generated) { $Generated = Join-Path $workspace '.scratch\vr-build-workspace\BuildWorkspace\generated' }
if (-not $Manifest) { $Manifest = Join-Path $workspace 'projects\mkwii\recomp.yml' }
-if (-not $BuildDir) { $BuildDir = Join-Path $root "app\build\questGame\$Product" }
+$variant = if ($Headset -eq 'quest1') { 'quest1' } else { 'modernQuest' }
+$variant += (Get-Culture).TextInfo.ToTitleCase($Configuration)
+if (-not $BuildDir) { $BuildDir = Join-Path $root "app\build\questGame\$variant\$Product" }
if (-not $Kit) {
- # The kit the last APK build packaged, so the game matches the app it will be imported into.
- $Kit = Get-ChildItem -Path (Join-Path $root 'app\build\generated') -Filter kit.json -Recurse -ErrorAction SilentlyContinue |
- Where-Object { $_.Directory.Name -eq 'game_kit' } |
- Sort-Object LastWriteTime -Descending | Select-Object -First 1 | ForEach-Object { $_.DirectoryName }
+ # Select the requested variant explicitly. Modification time is unsafe now that flavours use
+ # different -mcpu targets and an up-to-date native probe is not necessarily the newest one.
+ $Kit = Join-Path $root "app\build\generated\assets\questGameKit\$variant\game_kit"
}
if (-not $Kit -or -not (Test-Path (Join-Path $Kit 'kit.json'))) {
throw 'No game kit; run Build-Quest.ps1 first, or pass -Kit'
diff --git a/android/Export-QuestGameKit.ps1 b/android/Export-QuestGameKit.ps1
index c0b3c0e..5304830 100644
--- a/android/Export-QuestGameKit.ps1
+++ b/android/Export-QuestGameKit.ps1
@@ -8,10 +8,11 @@
param(
[Parameter(Mandatory)] [string]$CMakeBinaryDir,
[Parameter(Mandatory)] [string]$OutputDir,
- [Parameter(Mandatory)] [string]$LlvmStrip
+ [Parameter(Mandatory)] [string]$LlvmStrip,
+ [Parameter(Mandatory)] [string]$AndroidCpu
)
$ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot 'QuestGameKit.psm1') -Force
$repo = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
-$kit = Export-QuestGameKit -CMakeBinaryDir $CMakeBinaryDir -OutputDir $OutputDir -RepoRoot $repo -LlvmStrip $LlvmStrip
-Write-Host "Quest game kit $($kit.Fingerprint) [$($kit.Products -join ', ')] -> $OutputDir"
+$kit = Export-QuestGameKit -CMakeBinaryDir $CMakeBinaryDir -OutputDir $OutputDir -RepoRoot $repo -LlvmStrip $LlvmStrip -AndroidCpu $AndroidCpu
+Write-Host "Quest game kit $($kit.Fingerprint) [$($kit.Products -join ', '), CPU $AndroidCpu] -> $OutputDir"
diff --git a/android/QuestGameKit.psm1 b/android/QuestGameKit.psm1
index ff422a9..5d23a66 100644
--- a/android/QuestGameKit.psm1
+++ b/android/QuestGameKit.psm1
@@ -268,10 +268,16 @@ function Export-QuestGameKit {
[Parameter(Mandatory)] [string]$CMakeBinaryDir,
[Parameter(Mandatory)] [string]$OutputDir,
[Parameter(Mandatory)] [string]$RepoRoot,
- [Parameter(Mandatory)] [string]$LlvmStrip
+ [Parameter(Mandatory)] [string]$LlvmStrip,
+ [Parameter(Mandatory)] [string]$AndroidCpu
)
$ErrorActionPreference = 'Stop'
$binary = ConvertTo-ForwardPath $CMakeBinaryDir
+ $cache = [IO.File]::ReadAllText("$binary/CMakeCache.txt")
+ $configuredCpu = [regex]::Match($cache, '(?m)^MKW_ANDROID_CPU(?::[^=\r\n]*)?=([^\r\n]+)$').Groups[1].Value.Trim()
+ if ($configuredCpu -ne $AndroidCpu) {
+ throw "CMake tree $binary targets Android CPU '$configuredCpu', expected '$AndroidCpu'"
+ }
$runtimeInclude = ConvertTo-ForwardPath (Join-Path $RepoRoot 'runtime/include')
$ninja = [IO.File]::ReadAllText("$binary/build.ninja")
$commands = Read-CompileCommands "$binary/compile_commands.json"
@@ -405,6 +411,7 @@ function Export-QuestGameKit {
$recipe = [ordered]@{
schema = $script:KitSchema
+ androidCpu = $AndroidCpu
runtimeIncludeFingerprint = Get-RuntimeIncludeFingerprint (Join-Path $OutputDir 'include')
products = $products
}
@@ -445,10 +452,16 @@ function Invoke-QuestGameBuild {
$workspace = ConvertTo-ForwardPath (Split-Path -Parent $generated)
$recipe = [IO.File]::ReadAllText("$kit/kit.json") | ConvertFrom-Json
if ($recipe.schema -ne $script:KitSchema) { throw "Unsupported game kit schema $($recipe.schema)" }
+ if ([string]::IsNullOrWhiteSpace($recipe.androidCpu)) { throw 'The game kit does not name its Android CPU target' }
if (-not $recipe.products.PSObject.Properties.Name.Contains($Product)) {
throw "This Quest app's game kit cannot build $Product (it has: $($recipe.products.PSObject.Properties.Name -join ', '))"
}
$productRecipe = $recipe.products.$Product
+ foreach ($kind in 'translated', 'product', 'runtime', 'asm') {
+ if (@($productRecipe.compile.$kind) -notcontains "-mcpu=$($recipe.androidCpu)") {
+ throw "The game kit says CPU '$($recipe.androidCpu)' but its $kind compile flags do not match"
+ }
+ }
# The translation must come from the same release as the kit: its code is compiled against the
# kit's runtime headers and linked with the kit's runtime objects.
$workspaceInclude = Join-Path $workspace 'runtime/include'
@@ -460,6 +473,20 @@ function Invoke-QuestGameBuild {
}
New-Item -ItemType Directory -Force $BuildDir | Out-Null
$build = ConvertTo-ForwardPath $BuildDir
+ # Ninja sees the response-file path in each command, not changes to that file's contents. A
+ # different kit can therefore otherwise reuse objects compiled with another flavour's -mcpu.
+ # Keep an explicit identity next to the build and discard every reusable native output when it
+ # changes. WiiCompiled Setup deliberately reuses one BuildDir across APK selections.
+ $identity = "$($recipe.fingerprint) $Product"
+ $identityFile = "$build/kit-identity.txt"
+ $previousIdentity = if (Test-Path $identityFile) { [IO.File]::ReadAllText($identityFile).Trim() } else { '' }
+ if ($previousIdentity -ne $identity) {
+ foreach ($stale in 'obj', 'libmain.so', '.ninja_deps', '.ninja_log') {
+ $path = "$build/$stale"
+ if (Test-Path $path) { Remove-Item -Recurse -Force $path }
+ }
+ }
+ [IO.File]::WriteAllText($identityFile, $identity, (New-Object Text.UTF8Encoding $false))
$expand = { param([string]$s) $s.Replace('{kit}', $kit).Replace('{sysroot}', (ConvertTo-ForwardPath $Sysroot)).Replace('{workspace}', $workspace) }
foreach ($kind in 'translated', 'product', 'runtime', 'asm') {
@@ -583,6 +610,7 @@ function New-QuestGamePackage {
dolSha256 = $DolSha256
relSha256 = $RelSha256
kitFingerprint = $recipe.fingerprint
+ androidCpu = $recipe.androidCpu
library = $recipe.products.$Product.output
librarySha256 = Get-Sha256Hex $Library
includesData = [bool]$DataDir
diff --git a/android/README.md b/android/README.md
index d78a84a..8c3b3fc 100644
--- a/android/README.md
+++ b/android/README.md
@@ -1,7 +1,7 @@
# 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
+Standalone Android/OpenXR build of the Mario Kart Wii recompilation for Quest 1,
+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, its helper scripts, the game kit tooling
(`QuestGameKit.psm1`, `Build-QuestGame.ps1`), the on-headset build toolchain
@@ -10,6 +10,7 @@ holds the Gradle project, its helper scripts, the game kit tooling
```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 # the app, debug-signed, installs over adb
+powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Headset quest1 -Install # Quest 1 flavour
```
One app offers both games, with a toggle on Home: Mario Kart Wii and, when your
@@ -23,6 +24,7 @@ half an hour) or on a PC and imported:
```powershell
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Product base -Install
+powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Headset quest1 -Product base -Install
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Product retro_rewind -Mod -Install
```
diff --git a/android/Run-Quest.ps1 b/android/Run-Quest.ps1
index 6cf351f..f2c3c40 100644
--- a/android/Run-Quest.ps1
+++ b/android/Run-Quest.ps1
@@ -3,7 +3,8 @@
# diagnostics for one session.
#
# powershell -ExecutionPolicy Bypass -File android/Run-Quest.ps1 [-Apk ] [-Data ]
-# [-Seconds 60] [-NoLaunch] [-SkipInstall]
+# [-Headset modern|quest1] [-Configuration debug|release]
+# [-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
@@ -24,6 +25,8 @@
param(
[string]$Apk = '',
[string]$Data = '',
+ [ValidateSet('modern', 'quest1')] [string]$Headset = 'modern',
+ [ValidateSet('debug', 'release')] [string]$Configuration = 'debug',
[int]$Seconds = 60,
[switch]$NoLaunch,
[switch]$SkipInstall
@@ -49,8 +52,10 @@ if (-not $devices) { throw 'No device in "device" state; check the Quest is conn
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
+ $flavour = if ($Headset -eq 'quest1') { 'quest1' } else { 'modernQuest' }
+ $apkDir = Join-Path $root "app\build\outputs\apk\$flavour\$Configuration"
+ $Apk = Get-ChildItem -Path $apkDir -Filter '*.apk' -ErrorAction SilentlyContinue |
+ Select-Object -First 1 | ForEach-Object FullName
}
if (-not $Apk -or -not (Test-Path $Apk)) { throw 'No APK found; run Build-Quest.ps1 first or pass -Apk' }
Write-Host "Installing $Apk"
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index d61670b..51f7cb5 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -1,4 +1,5 @@
import javax.inject.Inject
+import org.gradle.api.provider.Property
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
@@ -145,6 +146,7 @@ android {
dimension = "headset"
manifestPlaceholders["mkwQuestSupportedDevices"] = "quest2|quest3|quest3s|questpro"
buildConfigField("boolean", "QUEST1_DIRECT_LAUNCH", "false")
+ buildConfigField("String", "ANDROID_CPU", "\"cortex-a77\"")
externalNativeBuild {
cmake {
// Snapdragon XR2 and newer. Keep this as the default build target.
@@ -156,6 +158,7 @@ android {
dimension = "headset"
manifestPlaceholders["mkwQuestSupportedDevices"] = "quest|quest2"
buildConfigField("boolean", "QUEST1_DIRECT_LAUNCH", "true")
+ buildConfigField("String", "ANDROID_CPU", "\"kryo\"")
externalNativeBuild {
cmake {
// Snapdragon 835. cortex-a77 binaries terminate with SIGILL on Quest 1.
@@ -221,6 +224,12 @@ abstract class ExportQuestGameKit : DefaultTask() {
@get:Internal
abstract val llvmStrip: RegularFileProperty
+ @get:Input
+ abstract val androidCpu: Property
+
+ @get:Input
+ abstract val cmakeBuildType: Property
+
@get:Inject
abstract val execOperations: ExecOperations
@@ -232,18 +241,39 @@ abstract class ExportQuestGameKit : DefaultTask() {
@TaskAction
fun export() {
val app = appDir.get().asFile
- val probe = File(app, "build/intermediates/cxx").walkTopDown()
+ data class Candidate(val probe: File, val binaryDir: File, val cpu: String?, val buildType: String?)
+ fun cacheValue(cache: String, name: String): String? =
+ Regex("(?m)^${Regex.escape(name)}(?::[^=\\r\\n]*)?=([^\\r\\n]*)$")
+ .find(cache)?.groupValues?.get(1)?.trim()
+
+ val candidates = File(app, "build/intermediates/cxx").walkTopDown()
.filter { it.name == "libmkw_quest_kit_probe.so" && it.parentFile.name == "arm64-v8a" }
- .maxByOrNull { it.lastModified() }
- ?: throw GradleException("No libmkw_quest_kit_probe.so; the native build did not produce the game kit probe")
- val configuration = probe.parentFile.parentFile.parentFile // /
- val binaryDir = File(app, ".cxx/${configuration.parentFile.name}/${configuration.name}/arm64-v8a")
- if (!File(binaryDir, "build.ninja").isFile) throw GradleException("No CMake tree at $binaryDir")
+ .map { probe ->
+ val configuration = probe.parentFile.parentFile.parentFile // /
+ val binaryDir = File(app, ".cxx/${configuration.parentFile.name}/${configuration.name}/arm64-v8a")
+ val cacheFile = File(binaryDir, "CMakeCache.txt")
+ val cache = cacheFile.takeIf { it.isFile }?.readText().orEmpty()
+ Candidate(probe, binaryDir, cacheValue(cache, "MKW_ANDROID_CPU"), cacheValue(cache, "CMAKE_BUILD_TYPE"))
+ }
+ .toList()
+ val expectedCpu = androidCpu.get()
+ val expectedBuildType = cmakeBuildType.get()
+ val matching = candidates.filter {
+ it.cpu == expectedCpu && it.buildType.equals(expectedBuildType, ignoreCase = true) &&
+ File(it.binaryDir, "build.ninja").isFile
+ }
+ val chosen = matching.maxByOrNull { it.probe.lastModified() }
+ ?: throw GradleException(
+ "No $expectedBuildType CMake tree for Android CPU $expectedCpu. Found: " +
+ candidates.joinToString { "${it.binaryDir} (cpu=${it.cpu}, type=${it.buildType})" }
+ )
+ val binaryDir = chosen.binaryDir
val kitDir = File(outputDir.get().asFile, "game_kit")
execOperations.exec {
commandLine(
"powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script.get().asFile.path,
"-CMakeBinaryDir", binaryDir.path, "-OutputDir", kitDir.path, "-LlvmStrip", llvmStrip.get().asFile.path,
+ "-AndroidCpu", expectedCpu,
)
}
}
@@ -293,6 +323,9 @@ androidComponents {
dependsOn("merge${capitalized}NativeLibs")
appDir.set(layout.projectDirectory)
script.set(rootProject.layout.projectDirectory.file("Export-QuestGameKit.ps1"))
+ androidCpu.set(if (variant.name.startsWith("quest1", ignoreCase = true)) "kryo" else "cortex-a77")
+ cmakeBuildType.set(variant.buildType)
+ outputDir.set(layout.buildDirectory.dir("generated/assets/questGameKit/${variant.name}"))
llvmStrip.set(sdkComponents.ndkDirectory.map {
it.file("toolchains/llvm/prebuilt/$host/bin/llvm-strip" + if (host.startsWith("windows")) ".exe" else "")
})
diff --git a/android/app/src/main/java/org/wiicompiled/quest/GameLibrary.kt b/android/app/src/main/java/org/wiicompiled/quest/GameLibrary.kt
index 0aa9481..ba0c008 100644
--- a/android/app/src/main/java/org/wiicompiled/quest/GameLibrary.kt
+++ b/android/app/src/main/java/org/wiicompiled/quest/GameLibrary.kt
@@ -50,6 +50,8 @@ object GameLibrary {
if (manifest == null || !library(context, profile).isFile) {
return Status.Missing
}
- return if (manifest.optString("kitFingerprint") == kitFingerprint(context)) Status.Ready else Status.Stale
+ return if (manifest.optString("kitFingerprint") == kitFingerprint(context) &&
+ manifest.optString("androidCpu") == BuildConfig.ANDROID_CPU
+ ) Status.Ready else Status.Stale
}
}
diff --git a/android/app/src/main/java/org/wiicompiled/quest/launcher/GameBuild.kt b/android/app/src/main/java/org/wiicompiled/quest/launcher/GameBuild.kt
index 0e1e2e2..cead748 100644
--- a/android/app/src/main/java/org/wiicompiled/quest/launcher/GameBuild.kt
+++ b/android/app/src/main/java/org/wiicompiled/quest/launcher/GameBuild.kt
@@ -133,6 +133,9 @@ object GameBuild {
if (kitJson.optInt("schema") != KIT_SCHEMA) {
return "This app's game kit is version ${kitJson.optInt("schema")}, which this builder does not know."
}
+ if (kitJson.optString("androidCpu") != BuildConfig.ANDROID_CPU) {
+ return "This app contains a game kit for ${kitJson.optString("androidCpu")}, but this build requires ${BuildConfig.ANDROID_CPU}."
+ }
val recipe = kitJson.getJSONObject("products").optJSONObject(profile.id)
?: return "This app's game kit cannot build ${profile.id}."
report(40, Step.Prepare)
@@ -570,6 +573,7 @@ object GameBuild {
.put("dolSha256", BuildConfig.DISC_DOL_SHA256)
.put("relSha256", BuildConfig.DISC_REL_SHA256)
.put("kitFingerprint", kitFingerprint)
+ .put("androidCpu", BuildConfig.ANDROID_CPU)
.put("library", GameLibrary.LIBRARY_NAME)
.put("librarySha256", BuildRecipe.hex(digest.digest()))
.put("includesData", false)
diff --git a/android/app/src/main/java/org/wiicompiled/quest/launcher/GamePackageImport.kt b/android/app/src/main/java/org/wiicompiled/quest/launcher/GamePackageImport.kt
index bf7c01b..eb7229c 100644
--- a/android/app/src/main/java/org/wiicompiled/quest/launcher/GamePackageImport.kt
+++ b/android/app/src/main/java/org/wiicompiled/quest/launcher/GamePackageImport.kt
@@ -157,6 +157,9 @@ object GamePackageImport {
if (manifest.optString("kitFingerprint") != GameLibrary.kitFingerprint(context)) {
return "This game file was built for a different version of this app. Build it again against the version installed on the headset."
}
+ if (manifest.optString("androidCpu") != BuildConfig.ANDROID_CPU) {
+ return "This game file targets ${manifest.optString("androidCpu")}, but this app requires ${BuildConfig.ANDROID_CPU}. Build it again for the correct headset."
+ }
val expected = manifest.optString("librarySha256")
if (librarySha256 == null) {
return "This game file has no game library (libmain.so)."
diff --git a/aurora-main/cmake/AuroraDawnProvider.cmake b/aurora-main/cmake/AuroraDawnProvider.cmake
index 90584d0..2cf1a36 100644
--- a/aurora-main/cmake/AuroraDawnProvider.cmake
+++ b/aurora-main/cmake/AuroraDawnProvider.cmake
@@ -154,13 +154,6 @@ elseif (_aurora_dawn_provider STREQUAL "package")
endif ()
set(AURORA_DAWN_PACKAGE_URL
"https://github.com/theofficialgman/dawn-build/releases/download/${AURORA_DAWN_VERSION}/dawn-${_dawn_system}-${_dawn_arch}.tar.gz")
- # Meta's Quest 1 firmware advertises VK_EXT_debug_utils but rejects it at
- # vkCreateInstance. This pinned Android package disables that extension on
- # Android; it is also the package validated on modern Quest headsets.
- if (_dawn_system STREQUAL "android" AND _dawn_arch STREQUAL "aarch64")
- set(AURORA_DAWN_PACKAGE_URL
- "https://github.com/encounter/dawn-build/releases/download/${AURORA_DAWN_VERSION}/dawn-android-aarch64.tar.gz")
- endif ()
# A release asset is mutable: the same tag has already served two different windows-amd64 archives,
# and a cached extraction is never re-verified. Pin the digest for the combinations we ship.
@@ -188,7 +181,7 @@ elseif (_aurora_dawn_provider STREQUAL "package")
"SHA256=f97701d26fd1f25bbcc260b4c31736ede134c730c12556029e2470fde967f424")
elseif (_dawn_system STREQUAL "android" AND _dawn_arch STREQUAL "aarch64")
set(AURORA_DAWN_PACKAGE_URL_HASH
- "SHA256=27d910dee1201fd1e5b6ac567f0ba2306ebf2135e9f40b6929976c365d38b09b")
+ "SHA256=0e63e8cbf53551f703f582d1306f4257c0380353f66b53369d96952ce6d9f934")
endif ()
endif ()
endif ()
diff --git a/aurora-main/lib/gfx/pipeline_cache.cpp b/aurora-main/lib/gfx/pipeline_cache.cpp
index 9b23d95..9adac95 100644
--- a/aurora-main/lib/gfx/pipeline_cache.cpp
+++ b/aurora-main/lib/gfx/pipeline_cache.cpp
@@ -10,6 +10,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -26,6 +27,7 @@
#if defined(__ANDROID__)
#include
#include
+#include
#endif
namespace aurora::gfx {
@@ -70,26 +72,27 @@ constexpr size_t MaxQueuedPipelineBuilds = 256;
// First-use compilation works best as a short parallel burst. Leave two logical processors for the
// render and game threads, and cap large hosts to limit driver submissions and memory use.
constexpr size_t ReservedLogicalProcessors = 2;
-#if defined(__ANDROID__)
-// Quest reports all eight Kryo cores, but the Adreno Vulkan driver serializes
-// much of vkCreateGraphicsPipelines. Six equal-priority compiler threads crowd
-// out the translated game and XR submission threads without materially
-// shortening first-use compilation. Two background-priority workers keep
-// pipeline discovery asynchronous while preserving frame cadence.
-constexpr size_t MaxPipelineWorkers = 2;
-#else
constexpr size_t MaxPipelineWorkers = 22;
-#endif
-// Cached clear and GX pipelines are normally prewarmed using the full worker
-// pool. Mobile Adreno drivers serialize much of pipeline creation internally;
-// prewarming hundreds of recipes there starves first-use pipelines for over a
-// minute. Keep cached recipes dormant on Android. A recipe is promoted to the
-// priority queue as soon as the game actually requests it.
+constexpr size_t Quest1MaxPipelineWorkers = 2;
+static bool g_quest1PipelineScheduling = false;
+
+static bool quest1_pipeline_scheduling() noexcept {
#if defined(__ANDROID__)
-constexpr size_t MaxBackgroundPipelineWorkers = 0;
+ static const bool enabled = [] {
+ char device[PROP_VALUE_MAX]{};
+ return __system_property_get("ro.product.device", device) > 0 && std::strcmp(device, "monterey") == 0;
+ }();
+ return enabled;
#else
-constexpr size_t MaxBackgroundPipelineWorkers = MaxPipelineWorkers;
+ return false;
#endif
+}
+
+static size_t max_background_pipeline_workers() noexcept {
+ // Quest 1's Adreno driver serializes pipeline creation. Leave cached recipes
+ // dormant until first use there; all newer headsets retain normal prewarm.
+ return g_quest1PipelineScheduling ? 0 : MaxPipelineWorkers;
+}
// For synchronous pipeline fallback (OpenGL)
#ifdef NDEBUG
constexpr size_t BuildPipelinesPerFrame = 5;
@@ -484,8 +487,8 @@ static PipelineRef find_pipeline_impl(ShaderType type, const PipelineConfig& con
}
} else if (g_pendingPipelines.contains(hash)) {
auto* pending = touch_pending_pipeline(hash, g_pipelineFrameActive);
- // A cached Android recipe can sit dormant because background prewarm is
- // disabled. Promoting it to first-use priority must wake a worker before
+ // A cached recipe can sit dormant when background prewarm is disabled.
+ // Promoting it to first-use priority must wake a worker before
// bind_pipeline waits for completion, or both threads sleep forever.
if (g_pipelineFrameActive && deferGxPipeline) {
notifyWorker = true;
@@ -1063,9 +1066,11 @@ static void pipeline_worker() {
#endif
#if defined(__ANDROID__)
pthread_setname_np(pthread_self(), "GXPipeline");
- // setpriority(PRIO_PROCESS, 0, ...) targets the calling Linux thread.
- // Pipeline creation may finish later, but must not preempt gameplay or XR.
- setpriority(PRIO_PROCESS, 0, 5);
+ if (g_quest1PipelineScheduling) {
+ // setpriority(PRIO_PROCESS, 0, ...) targets the calling Linux thread.
+ // Quest 1 pipeline creation must not preempt gameplay or XR.
+ setpriority(PRIO_PROCESS, 0, 5);
+ }
#endif
while (true) {
@@ -1076,7 +1081,7 @@ static void pipeline_worker() {
g_pipelineCv.wait(lock, [] {
return !g_priorityPipelines.empty() ||
(!g_backgroundPipelines.empty() &&
- g_activeBackgroundPipelineWorkers < MaxBackgroundPipelineWorkers) ||
+ g_activeBackgroundPipelineWorkers < max_background_pipeline_workers()) ||
g_pipelineThreadEnd;
});
if (g_pipelineThreadEnd) {
@@ -1125,7 +1130,8 @@ static size_t pipeline_worker_count() {
}
const size_t availableWorkers =
logicalProcessors > ReservedLogicalProcessors ? logicalProcessors - ReservedLogicalProcessors : 1;
- return std::clamp(availableWorkers, size_t{1}, MaxPipelineWorkers);
+ const size_t maximum = g_quest1PipelineScheduling ? Quest1MaxPipelineWorkers : MaxPipelineWorkers;
+ return std::clamp(availableWorkers, size_t{1}, maximum);
}
template
@@ -1286,6 +1292,7 @@ void initialize_pipeline_cache() {
g_pipelineFrameActive = false;
g_pipelineThreadEnd = false;
g_activeBackgroundPipelineWorkers = 0;
+ g_quest1PipelineScheduling = quest1_pipeline_scheduling();
if (webgpu::g_backendType == wgpu::BackendType::OpenGL || webgpu::g_backendType == wgpu::BackendType::OpenGLES ||
webgpu::g_backendType == wgpu::BackendType::WebGPU) {
@@ -1298,7 +1305,7 @@ void initialize_pipeline_cache() {
g_pipelineThreads.emplace_back(pipeline_worker);
}
Log.info("Enabled {} priority pipeline compilation workers ({} background prewarm)",
- workerCount, MaxBackgroundPipelineWorkers);
+ workerCount, max_background_pipeline_workers());
}
load_pipeline_cache();
diff --git a/docs/quest-port.md b/docs/quest-port.md
index 5c3f107..6a020f8 100644
--- a/docs/quest-port.md
+++ b/docs/quest-port.md
@@ -110,9 +110,10 @@ format-converting paths.
Quest 1 must be built with the `quest1` flavour (`Build-Quest.ps1 -Headset
quest1`). It targets the Snapdragon 835's Kryo CPU; the modern flavour targets
`cortex-a77`, whose instructions can terminate the game with `SIGILL` on Quest
-1. Android builds use a pinned Dawn package which does not enable
-`VK_EXT_debug_utils`: the old Quest firmware advertises that extension but
-rejects it when Dawn creates the Vulkan instance.
+1. Each variant's exported game kit records this CPU target, and the APK build,
+PC game build, on-headset build and package import all verify it. This prevents
+a kit left by another flavour from producing a library that can crash with
+`SIGILL`.
The final Quest 1 firmware also cannot reliably promote this app's 2D setup
panel into an immersive activity. Its APK therefore exposes two library
@@ -128,13 +129,13 @@ output from flickering black or white frames to a complete menu with character
and vehicle previews. It does intentionally skip Wii-era RGB5A3 quantization on
that device.
-Pipeline compilation is also scheduled differently on Android. Adreno
+Pipeline compilation is also scheduled differently on Quest 1. Its Adreno
serializes much of `vkCreateGraphicsPipelines`, so a large equal-priority worker
pool starved the translated game and OpenXR pacing threads without shortening
-the compile materially. Android uses two nice-level 5 workers and does not
+the compile materially. Quest 1 uses two nice-level 5 workers and does not
prewarm cached recipes in the background; a cached recipe is promoted and its
-workers are awakened when the game first requests it. Desktop keeps the full
-worker pool and prewarm behavior.
+workers are awakened when the game first requests it. Newer Android headsets
+and desktop retain the normal worker pool and prewarm behavior.
### Controllers
@@ -502,6 +503,7 @@ powershell -ExecutionPolicy Bypass -File android/Prepare-QuestDependencies.ps1
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Install # the app, its game kit and toolchain, debug-signed
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Headset quest1 -Install # Quest 1: Kryo CPU and direct-VR library entry
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Install # your game, against that kit, into Import (or WheelWizard VR's Build for Quest)
+powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Headset quest1 -Install # game package from the Quest 1 kit
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Product retro_rewind -Mod -Install # the mod and its pack (needs translate-mod output)
adb push MarioKart.iso /sdcard/Download/ # then Select disc image in the launcher
```