mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 06:00:25 +02:00
Merge PR #2 from darthcircuit: Quest 1 support
Adds the modernQuest and quest1 headset flavours (Kryo CPU target, direct-VR library entry on Quest 1), records the CPU target in the game kit and checks it wherever a kit or game package is used, and gates the Quest 1 EFB-copy and pipeline-scheduling workarounds on the monterey device. Resolved docs/quest-port.md by keeping both sides: the foveated rendering section and the Quest 1 renderer compatibility section, and both Build-Quest.ps1 command lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
83805a410e
21 files changed
+431
-55
No files matched your search
@@ -74,7 +74,7 @@ internal sealed class QuestBuildService
|
|||||||
var kitDirectory = Path.Combine(questRoot, "kit");
|
var kitDirectory = Path.Combine(questRoot, "kit");
|
||||||
_reporter.Progress(InstallStages.QuestKit, "Reading the game kit from the Quest app...", 2);
|
_reporter.Progress(InstallStages.QuestKit, "Reading the game kit from the Quest app...", 2);
|
||||||
var kit = ExtractKit(apkPath, kitDirectory, cancellationToken);
|
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))
|
if (!kit.Products.Contains(product))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
@@ -185,7 +185,7 @@ internal sealed class QuestBuildService
|
|||||||
File.Exists(Path.Combine(directory, "sys", "main.dol"));
|
File.Exists(Path.Combine(directory, "sys", "main.dol"));
|
||||||
|
|
||||||
/// <summary>What the extracted kit says it builds: its fingerprint and the products it carries.</summary>
|
/// <summary>What the extracted kit says it builds: its fingerprint and the products it carries.</summary>
|
||||||
internal sealed record Kit(string Fingerprint, IReadOnlyCollection<string> Products);
|
internal sealed record Kit(string Fingerprint, string AndroidCpu, IReadOnlyCollection<string> Products);
|
||||||
|
|
||||||
/// <summary>Extracts <c>assets/game_kit</c> from the Quest app's APK and reads what it is for.</summary>
|
/// <summary>Extracts <c>assets/game_kit</c> from the Quest app's APK and reads what it is for.</summary>
|
||||||
internal static Kit ExtractKit(string apkPath, string destination, CancellationToken cancellationToken)
|
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.");
|
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) &&
|
var products = document.RootElement.TryGetProperty("products", out var value) &&
|
||||||
value.ValueKind == JsonValueKind.Object
|
value.ValueKind == JsonValueKind.Object
|
||||||
? value.EnumerateObject().Select(property => property.Name).ToArray()
|
? value.EnumerateObject().Select(property => property.Name).ToArray()
|
||||||
: throw new InvalidDataException("The Quest app's game kit lists no games it can build.");
|
: 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<Toolchain> EnsureToolchainAsync(string questRoot, CancellationToken cancellationToken)
|
private async Task<Toolchain> EnsureToolchainAsync(string questRoot, CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -384,14 +384,14 @@ internal static class SelfTests
|
|||||||
using var writer = new StreamWriter(zip.CreateEntry(name).Open());
|
using var writer = new StreamWriter(zip.CreateEntry(name).Open());
|
||||||
writer.Write(text);
|
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/game_kit/include/memory.h", "#pragma once");
|
||||||
Add("assets/runtime_resources/dsp_coef.bin", "not the kit");
|
Add("assets/runtime_resources/dsp_coef.bin", "not the kit");
|
||||||
Add("lib/arm64-v8a/libSDL3.so", "not the kit");
|
Add("lib/arm64-v8a/libSDL3.so", "not the kit");
|
||||||
}
|
}
|
||||||
var kit = Path.Combine(temp, "kit");
|
var kit = Path.Combine(temp, "kit");
|
||||||
var extracted = QuestBuildService.ExtractKit(apk, kit, CancellationToken.None);
|
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"))
|
!extracted.Products.Contains("retro_rewind"))
|
||||||
throw new Exception("The kit fingerprint or its games were not read.");
|
throw new Exception("The kit fingerprint or its games were not read.");
|
||||||
if (!File.Exists(Path.Combine(kit, "include", "memory.h")) ||
|
if (!File.Exists(Path.Combine(kit, "include", "memory.h")) ||
|
||||||
|
|||||||
+19
-6
@@ -1,7 +1,7 @@
|
|||||||
# Builds the standalone Meta Quest APK on a Windows host.
|
# Builds the standalone Meta Quest APK on a Windows host.
|
||||||
#
|
#
|
||||||
# powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 [-Generated <dir>]
|
# powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 [-Generated <dir>]
|
||||||
# [-Configuration debug|release] [-Install]
|
# [-Headset modern|quest1] [-Configuration debug|release] [-Install]
|
||||||
#
|
#
|
||||||
# One app offers both games: the APK carries a kit for the base game, and for Retro Rewind too
|
# One app offers both games: the APK carries a kit for the base game, and for Retro Rewind too
|
||||||
# when -Generated holds a translation that includes the mod.
|
# when -Generated holds a translation that includes the mod.
|
||||||
@@ -25,6 +25,7 @@ param(
|
|||||||
[string]$Generated = '',
|
[string]$Generated = '',
|
||||||
[string]$Dependencies = '',
|
[string]$Dependencies = '',
|
||||||
[string]$CMakeDir = '',
|
[string]$CMakeDir = '',
|
||||||
|
[ValidateSet('modern', 'quest1')] [string]$Headset = 'modern',
|
||||||
[ValidateSet('debug', 'release')] [string]$Configuration = 'debug',
|
[ValidateSet('debug', 'release')] [string]$Configuration = 'debug',
|
||||||
[switch]$Install,
|
[switch]$Install,
|
||||||
[switch]$StockDawn
|
[switch]$StockDawn
|
||||||
@@ -79,7 +80,9 @@ if (-not $Dependencies) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$variant = (Get-Culture).TextInfo.ToTitleCase($Configuration)
|
$variant = (Get-Culture).TextInfo.ToTitleCase($Configuration)
|
||||||
$task = "app:assemble$variant"
|
$flavour = if ($Headset -eq 'quest1') { 'Quest1' } else { 'ModernQuest' }
|
||||||
|
$expectedCpu = if ($Headset -eq 'quest1') { 'kryo' } else { 'cortex-a77' }
|
||||||
|
$task = "app:assemble$flavour$variant"
|
||||||
$gradleArgs = @(
|
$gradleArgs = @(
|
||||||
'--project-dir', $root,
|
'--project-dir', $root,
|
||||||
"-PmkwGeneratedDir=$Generated"
|
"-PmkwGeneratedDir=$Generated"
|
||||||
@@ -95,7 +98,8 @@ Write-Host "gradlew $($gradleArgs -join ' ')"
|
|||||||
& (Join-Path $root 'gradlew.bat') @gradleArgs
|
& (Join-Path $root 'gradlew.bat') @gradleArgs
|
||||||
if ($LASTEXITCODE -ne 0) { throw "Gradle failed ($LASTEXITCODE)" }
|
if ($LASTEXITCODE -ne 0) { throw "Gradle failed ($LASTEXITCODE)" }
|
||||||
|
|
||||||
$apkDir = Join-Path $root "app\build\outputs\apk\$Configuration"
|
$flavourDir = if ($Headset -eq 'quest1') { 'quest1' } else { 'modernQuest' }
|
||||||
|
$apkDir = Join-Path $root "app\build\outputs\apk\$flavourDir\$Configuration"
|
||||||
$apk = Get-ChildItem -Path $apkDir -Filter '*.apk' | Select-Object -First 1
|
$apk = Get-ChildItem -Path $apkDir -Filter '*.apk' | Select-Object -First 1
|
||||||
if (-not $apk) { throw "No APK under $apkDir" }
|
if (-not $apk) { throw "No APK under $apkDir" }
|
||||||
Write-Host "APK: $($apk.FullName)"
|
Write-Host "APK: $($apk.FullName)"
|
||||||
@@ -106,16 +110,25 @@ Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|||||||
$archive = [IO.Compression.ZipFile]::OpenRead($apk.FullName)
|
$archive = [IO.Compression.ZipFile]::OpenRead($apk.FullName)
|
||||||
try {
|
try {
|
||||||
$gameLibraries = @($archive.Entries | Where-Object { $_.FullName -match '^lib/[^/]+/libmain[^/]*\.so$' })
|
$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
|
$hasToolchain = @($archive.Entries | Where-Object { $_.FullName -eq 'assets/quest_toolchain/files.zip' }).Count -gt 0
|
||||||
} finally { $archive.Dispose() }
|
} finally { $archive.Dispose() }
|
||||||
if ($gameLibraries.Count -gt 0) { throw "The APK contains a translated game library: $($gameLibraries.FullName -join ', ')" }
|
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 (-not $hasToolchain) { throw 'The APK has no build toolchain (assets/quest_toolchain/files.zip)' }
|
||||||
|
|
||||||
if ($Install) {
|
if ($Install) {
|
||||||
$adb = Join-Path $sdkRoot 'platform-tools\adb.exe'
|
$adb = Join-Path $sdkRoot 'platform-tools\adb.exe'
|
||||||
& $adb install -r $apk.FullName
|
& $adb install -r $apk.FullName
|
||||||
if ($LASTEXITCODE -ne 0) { throw "adb install failed ($LASTEXITCODE)" }
|
if ($LASTEXITCODE -ne 0) { throw "adb install failed ($LASTEXITCODE)" }
|
||||||
Write-Host 'Installed. Build a game with Build on this Quest in the launcher, or on this PC with android/Build-QuestGame.ps1 -Install.'
|
if ($Headset -eq 'quest1') {
|
||||||
|
Write-Host 'Installed. Use WiiCompiled Settings for setup, then launch WiiCompiled VR directly from the library.'
|
||||||
|
} else {
|
||||||
|
Write-Host 'Installed. Build a game with Build on this Quest in the launcher, or on this PC with android/Build-QuestGame.ps1 -Install.'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
# SDK's NDK and ninja:
|
# SDK's NDK and ninja:
|
||||||
#
|
#
|
||||||
# powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 [-Generated <dir>] [-Kit <dir>]
|
# powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 [-Generated <dir>] [-Kit <dir>]
|
||||||
|
# [-Headset modern|quest1] [-Configuration debug|release]
|
||||||
# [-Data <extracted disc dir>] [-Output <file.wcgame>] [-Install]
|
# [-Data <extracted disc dir>] [-Output <file.wcgame>] [-Install]
|
||||||
#
|
#
|
||||||
# WiiCompiled Setup's --build-quest runs the copy staged in an installation's BuildWorkspace\android,
|
# WiiCompiled Setup's --build-quest runs the copy staged in an installation's BuildWorkspace\android,
|
||||||
@@ -32,6 +33,8 @@ param(
|
|||||||
[string]$Sysroot = '',
|
[string]$Sysroot = '',
|
||||||
[string]$Ninja = '',
|
[string]$Ninja = '',
|
||||||
[string]$BuiltBy = 'android/Build-QuestGame.ps1',
|
[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',
|
[ValidateSet('base', 'retro_rewind')] [string]$Product = 'base',
|
||||||
[string]$Mod = '',
|
[string]$Mod = '',
|
||||||
[int]$TranslatedJobs = 0,
|
[int]$TranslatedJobs = 0,
|
||||||
@@ -44,12 +47,13 @@ $root = $PSScriptRoot
|
|||||||
$workspace = (Resolve-Path (Join-Path $root '..')).Path
|
$workspace = (Resolve-Path (Join-Path $root '..')).Path
|
||||||
if (-not $Generated) { $Generated = Join-Path $workspace '.scratch\vr-build-workspace\BuildWorkspace\generated' }
|
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 $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) {
|
if (-not $Kit) {
|
||||||
# The kit the last APK build packaged, so the game matches the app it will be imported into.
|
# Select the requested variant explicitly. Modification time is unsafe now that flavours use
|
||||||
$Kit = Get-ChildItem -Path (Join-Path $root 'app\build\generated') -Filter kit.json -Recurse -ErrorAction SilentlyContinue |
|
# different -mcpu targets and an up-to-date native probe is not necessarily the newest one.
|
||||||
Where-Object { $_.Directory.Name -eq 'game_kit' } |
|
$Kit = Join-Path $root "app\build\generated\assets\questGameKit\$variant\game_kit"
|
||||||
Sort-Object LastWriteTime -Descending | Select-Object -First 1 | ForEach-Object { $_.DirectoryName }
|
|
||||||
}
|
}
|
||||||
if (-not $Kit -or -not (Test-Path (Join-Path $Kit 'kit.json'))) {
|
if (-not $Kit -or -not (Test-Path (Join-Path $Kit 'kit.json'))) {
|
||||||
throw 'No game kit; run Build-Quest.ps1 first, or pass -Kit'
|
throw 'No game kit; run Build-Quest.ps1 first, or pass -Kit'
|
||||||
|
|||||||
@@ -8,10 +8,11 @@
|
|||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)] [string]$CMakeBinaryDir,
|
[Parameter(Mandatory)] [string]$CMakeBinaryDir,
|
||||||
[Parameter(Mandatory)] [string]$OutputDir,
|
[Parameter(Mandatory)] [string]$OutputDir,
|
||||||
[Parameter(Mandatory)] [string]$LlvmStrip
|
[Parameter(Mandatory)] [string]$LlvmStrip,
|
||||||
|
[Parameter(Mandatory)] [string]$AndroidCpu
|
||||||
)
|
)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
Import-Module (Join-Path $PSScriptRoot 'QuestGameKit.psm1') -Force
|
Import-Module (Join-Path $PSScriptRoot 'QuestGameKit.psm1') -Force
|
||||||
$repo = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
$repo = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||||
$kit = Export-QuestGameKit -CMakeBinaryDir $CMakeBinaryDir -OutputDir $OutputDir -RepoRoot $repo -LlvmStrip $LlvmStrip
|
$kit = Export-QuestGameKit -CMakeBinaryDir $CMakeBinaryDir -OutputDir $OutputDir -RepoRoot $repo -LlvmStrip $LlvmStrip -AndroidCpu $AndroidCpu
|
||||||
Write-Host "Quest game kit $($kit.Fingerprint) [$($kit.Products -join ', ')] -> $OutputDir"
|
Write-Host "Quest game kit $($kit.Fingerprint) [$($kit.Products -join ', '), CPU $AndroidCpu] -> $OutputDir"
|
||||||
@@ -268,10 +268,16 @@ function Export-QuestGameKit {
|
|||||||
[Parameter(Mandatory)] [string]$CMakeBinaryDir,
|
[Parameter(Mandatory)] [string]$CMakeBinaryDir,
|
||||||
[Parameter(Mandatory)] [string]$OutputDir,
|
[Parameter(Mandatory)] [string]$OutputDir,
|
||||||
[Parameter(Mandatory)] [string]$RepoRoot,
|
[Parameter(Mandatory)] [string]$RepoRoot,
|
||||||
[Parameter(Mandatory)] [string]$LlvmStrip
|
[Parameter(Mandatory)] [string]$LlvmStrip,
|
||||||
|
[Parameter(Mandatory)] [string]$AndroidCpu
|
||||||
)
|
)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$binary = ConvertTo-ForwardPath $CMakeBinaryDir
|
$binary = ConvertTo-ForwardPath $CMakeBinaryDir
|
||||||
|
$cache = [IO.File]::ReadAllText("$binary/CMakeCache.txt")
|
||||||
|
$configuredCpu = [regex]::Match($cache, '(?m)^MKW_ANDROID_CPU(?::[^=\r\n]*)?=([^\r\n]+)\r?$').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')
|
$runtimeInclude = ConvertTo-ForwardPath (Join-Path $RepoRoot 'runtime/include')
|
||||||
$ninja = [IO.File]::ReadAllText("$binary/build.ninja")
|
$ninja = [IO.File]::ReadAllText("$binary/build.ninja")
|
||||||
$commands = Read-CompileCommands "$binary/compile_commands.json"
|
$commands = Read-CompileCommands "$binary/compile_commands.json"
|
||||||
@@ -405,6 +411,7 @@ function Export-QuestGameKit {
|
|||||||
|
|
||||||
$recipe = [ordered]@{
|
$recipe = [ordered]@{
|
||||||
schema = $script:KitSchema
|
schema = $script:KitSchema
|
||||||
|
androidCpu = $AndroidCpu
|
||||||
runtimeIncludeFingerprint = Get-RuntimeIncludeFingerprint (Join-Path $OutputDir 'include')
|
runtimeIncludeFingerprint = Get-RuntimeIncludeFingerprint (Join-Path $OutputDir 'include')
|
||||||
products = $products
|
products = $products
|
||||||
}
|
}
|
||||||
@@ -445,10 +452,16 @@ function Invoke-QuestGameBuild {
|
|||||||
$workspace = ConvertTo-ForwardPath (Split-Path -Parent $generated)
|
$workspace = ConvertTo-ForwardPath (Split-Path -Parent $generated)
|
||||||
$recipe = [IO.File]::ReadAllText("$kit/kit.json") | ConvertFrom-Json
|
$recipe = [IO.File]::ReadAllText("$kit/kit.json") | ConvertFrom-Json
|
||||||
if ($recipe.schema -ne $script:KitSchema) { throw "Unsupported game kit schema $($recipe.schema)" }
|
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)) {
|
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 ', '))"
|
throw "This Quest app's game kit cannot build $Product (it has: $($recipe.products.PSObject.Properties.Name -join ', '))"
|
||||||
}
|
}
|
||||||
$productRecipe = $recipe.products.$Product
|
$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
|
# 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.
|
# kit's runtime headers and linked with the kit's runtime objects.
|
||||||
$workspaceInclude = Join-Path $workspace 'runtime/include'
|
$workspaceInclude = Join-Path $workspace 'runtime/include'
|
||||||
@@ -460,6 +473,20 @@ function Invoke-QuestGameBuild {
|
|||||||
}
|
}
|
||||||
New-Item -ItemType Directory -Force $BuildDir | Out-Null
|
New-Item -ItemType Directory -Force $BuildDir | Out-Null
|
||||||
$build = ConvertTo-ForwardPath $BuildDir
|
$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) }
|
$expand = { param([string]$s) $s.Replace('{kit}', $kit).Replace('{sysroot}', (ConvertTo-ForwardPath $Sysroot)).Replace('{workspace}', $workspace) }
|
||||||
|
|
||||||
foreach ($kind in 'translated', 'product', 'runtime', 'asm') {
|
foreach ($kind in 'translated', 'product', 'runtime', 'asm') {
|
||||||
@@ -596,6 +623,7 @@ function New-QuestGamePackage {
|
|||||||
dolSha256 = $DolSha256
|
dolSha256 = $DolSha256
|
||||||
relSha256 = $RelSha256
|
relSha256 = $RelSha256
|
||||||
kitFingerprint = $recipe.fingerprint
|
kitFingerprint = $recipe.fingerprint
|
||||||
|
androidCpu = $recipe.androidCpu
|
||||||
library = $recipe.products.$Product.output
|
library = $recipe.products.$Product.output
|
||||||
librarySha256 = Get-Sha256Hex $Library
|
librarySha256 = Get-Sha256Hex $Library
|
||||||
includesData = [bool]$DataDir
|
includesData = [bool]$DataDir
|
||||||
|
|||||||
+4
-2
@@ -1,7 +1,7 @@
|
|||||||
# WiiCompiled VR for Meta Quest (Android)
|
# WiiCompiled VR for Meta Quest (Android)
|
||||||
|
|
||||||
Standalone Android/OpenXR build of the Mario Kart Wii recompilation for Quest 2,
|
Standalone Android/OpenXR build of the Mario Kart Wii recompilation for Quest 1,
|
||||||
Quest 3, Quest 3S and Quest Pro. The full design, build walkthrough and current
|
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
|
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
|
holds the Gradle project, its helper scripts, the game kit tooling
|
||||||
(`QuestGameKit.psm1`, `Build-QuestGame.ps1`), the on-headset build toolchain
|
(`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
|
||||||
powershell -ExecutionPolicy Bypass -File android/Prepare-QuestDependencies.ps1 # stages the SDL3 3.4.4 AAR once
|
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 -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
|
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
|
||||||
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Product base -Install
|
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 <RetroRewind6> -Install
|
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Product retro_rewind -Mod <RetroRewind6> -Install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
# diagnostics for one session.
|
# diagnostics for one session.
|
||||||
#
|
#
|
||||||
# powershell -ExecutionPolicy Bypass -File android/Run-Quest.ps1 [-Apk <path>] [-Data <extracted disc dir>]
|
# powershell -ExecutionPolicy Bypass -File android/Run-Quest.ps1 [-Apk <path>] [-Data <extracted disc dir>]
|
||||||
# [-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/,
|
# -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
|
# disc/ ...). It is pushed once to a staging folder, then moved into the app's
|
||||||
@@ -24,6 +25,8 @@
|
|||||||
param(
|
param(
|
||||||
[string]$Apk = '',
|
[string]$Apk = '',
|
||||||
[string]$Data = '',
|
[string]$Data = '',
|
||||||
|
[ValidateSet('modern', 'quest1')] [string]$Headset = 'modern',
|
||||||
|
[ValidateSet('debug', 'release')] [string]$Configuration = 'debug',
|
||||||
[int]$Seconds = 60,
|
[int]$Seconds = 60,
|
||||||
[switch]$NoLaunch,
|
[switch]$NoLaunch,
|
||||||
[switch]$SkipInstall
|
[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 $SkipInstall) {
|
||||||
if (-not $Apk) {
|
if (-not $Apk) {
|
||||||
$Apk = Get-ChildItem -Path (Join-Path $root 'app\build\outputs\apk') -Recurse -Filter '*.apk' |
|
$flavour = if ($Headset -eq 'quest1') { 'quest1' } else { 'modernQuest' }
|
||||||
Sort-Object LastWriteTime -Descending | Select-Object -First 1 | ForEach-Object FullName
|
$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' }
|
if (-not $Apk -or -not (Test-Path $Apk)) { throw 'No APK found; run Build-Quest.ps1 first or pass -Apk' }
|
||||||
Write-Host "Installing $Apk"
|
Write-Host "Installing $Apk"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import org.gradle.api.provider.Property
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
@@ -145,6 +146,34 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flavorDimensions += "headset"
|
||||||
|
productFlavors {
|
||||||
|
create("modernQuest") {
|
||||||
|
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.
|
||||||
|
arguments += "-DMKW_ANDROID_CPU=cortex-a77"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
create("quest1") {
|
||||||
|
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.
|
||||||
|
arguments += "-DMKW_ANDROID_CPU=kryo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
// aurora-main needs CMake 3.25+, newer than the SDK's bundled 3.22.1;
|
// aurora-main needs CMake 3.25+, newer than the SDK's bundled 3.22.1;
|
||||||
@@ -201,6 +230,9 @@ abstract class ExportQuestGameKit : DefaultTask() {
|
|||||||
@get:Internal
|
@get:Internal
|
||||||
abstract val llvmStrip: RegularFileProperty
|
abstract val llvmStrip: RegularFileProperty
|
||||||
|
|
||||||
|
@get:Input
|
||||||
|
abstract val androidCpu: Property<String>
|
||||||
|
|
||||||
@get:Inject
|
@get:Inject
|
||||||
abstract val execOperations: ExecOperations
|
abstract val execOperations: ExecOperations
|
||||||
|
|
||||||
@@ -212,18 +244,37 @@ abstract class ExportQuestGameKit : DefaultTask() {
|
|||||||
@TaskAction
|
@TaskAction
|
||||||
fun export() {
|
fun export() {
|
||||||
val app = appDir.get().asFile
|
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?)
|
||||||
|
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" }
|
.filter { it.name == "libmkw_quest_kit_probe.so" && it.parentFile.name == "arm64-v8a" }
|
||||||
.maxByOrNull { it.lastModified() }
|
.map { probe ->
|
||||||
?: throw GradleException("No libmkw_quest_kit_probe.so; the native build did not produce the game kit probe")
|
val configuration = probe.parentFile.parentFile.parentFile // <Variant>/<hash>
|
||||||
val configuration = probe.parentFile.parentFile.parentFile // <BuildType>/<hash>
|
val binaryDir = File(app, ".cxx/${configuration.parentFile.name}/${configuration.name}/arm64-v8a")
|
||||||
val binaryDir = File(app, ".cxx/${configuration.parentFile.name}/${configuration.name}/arm64-v8a")
|
val cacheFile = File(binaryDir, "CMakeCache.txt")
|
||||||
if (!File(binaryDir, "build.ninja").isFile) throw GradleException("No CMake tree at $binaryDir")
|
val cache = cacheFile.takeIf { it.isFile }?.readText().orEmpty()
|
||||||
|
Candidate(probe, binaryDir, cacheValue(cache, "MKW_ANDROID_CPU"))
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
val expectedCpu = androidCpu.get()
|
||||||
|
val matching = candidates.filter {
|
||||||
|
it.cpu == expectedCpu && File(it.binaryDir, "build.ninja").isFile
|
||||||
|
}
|
||||||
|
val chosen = matching.maxByOrNull { it.probe.lastModified() }
|
||||||
|
?: throw GradleException(
|
||||||
|
"No CMake tree for Android CPU $expectedCpu. Found: " +
|
||||||
|
candidates.joinToString { "${it.binaryDir} (cpu=${it.cpu})" }
|
||||||
|
)
|
||||||
|
val binaryDir = chosen.binaryDir
|
||||||
val kitDir = File(outputDir.get().asFile, "game_kit")
|
val kitDir = File(outputDir.get().asFile, "game_kit")
|
||||||
execOperations.exec {
|
execOperations.exec {
|
||||||
commandLine(
|
commandLine(
|
||||||
"powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script.get().asFile.path,
|
"powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script.get().asFile.path,
|
||||||
"-CMakeBinaryDir", binaryDir.path, "-OutputDir", kitDir.path, "-LlvmStrip", llvmStrip.get().asFile.path,
|
"-CMakeBinaryDir", binaryDir.path, "-OutputDir", kitDir.path, "-LlvmStrip", llvmStrip.get().asFile.path,
|
||||||
|
"-AndroidCpu", expectedCpu,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,6 +324,8 @@ androidComponents {
|
|||||||
dependsOn("merge${capitalized}NativeLibs")
|
dependsOn("merge${capitalized}NativeLibs")
|
||||||
appDir.set(layout.projectDirectory)
|
appDir.set(layout.projectDirectory)
|
||||||
script.set(rootProject.layout.projectDirectory.file("Export-QuestGameKit.ps1"))
|
script.set(rootProject.layout.projectDirectory.file("Export-QuestGameKit.ps1"))
|
||||||
|
androidCpu.set(if (variant.name.startsWith("quest1", ignoreCase = true)) "kryo" else "cortex-a77")
|
||||||
|
outputDir.set(layout.buildDirectory.dir("generated/assets/questGameKit/${variant.name}"))
|
||||||
llvmStrip.set(sdkComponents.ndkDirectory.map {
|
llvmStrip.set(sdkComponents.ndkDirectory.map {
|
||||||
it.file("toolchains/llvm/prebuilt/$host/bin/llvm-strip" + if (host.startsWith("windows")) ".exe" else "")
|
it.file("toolchains/llvm/prebuilt/$host/bin/llvm-strip" + if (host.startsWith("windows")) ".exe" else "")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -50,12 +50,13 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.WiiCompiled.Quest">
|
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.supportedDevices" android:value="${mkwQuestSupportedDevices}" />
|
||||||
<meta-data android:name="com.oculus.vr.focusaware" android:value="true" />
|
<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. -->
|
<!-- 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" />
|
<meta-data android:name="com.oculus.trade_cpu_for_gpu_amount" android:value="-1" />
|
||||||
|
|
||||||
<!-- The app opens on this 2D panel: Play starts QuestActivity, Settings edits Config.toml. -->
|
<!-- The setup panel: Play starts QuestActivity on modern Horizon OS; the
|
||||||
|
Quest 1 flavour exposes it as a separate Settings library entry. -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".launcher.LauncherActivity"
|
android:name=".launcher.LauncherActivity"
|
||||||
android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|density"
|
android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|density"
|
||||||
@@ -68,10 +69,6 @@
|
|||||||
android:defaultHeight="640dp"
|
android:defaultHeight="640dp"
|
||||||
android:minWidth="760dp"
|
android:minWidth="760dp"
|
||||||
android:minHeight="520dp" />
|
android:minHeight="520dp" />
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- Extracts the player's disc image and imports games built on a PC (launcher process). -->
|
<!-- Extracts the player's disc image and imports games built on a PC (launcher process). -->
|
||||||
@@ -97,11 +94,6 @@
|
|||||||
<property
|
<property
|
||||||
android:name="android.window.PROPERTY_XR_ACTIVITY_START_MODE"
|
android:name="android.window.PROPERTY_XR_ACTIVITY_START_MODE"
|
||||||
android:value="XR_ACTIVITY_START_MODE_FULL_SPACE_UNMANAGED" />
|
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.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ object GameLibrary {
|
|||||||
if (manifest == null || !library(context, profile).isFile) {
|
if (manifest == null || !library(context, profile).isFile) {
|
||||||
return Status.Missing
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,6 +140,9 @@ object GameBuild {
|
|||||||
if (kitJson.optInt("schema") != KIT_SCHEMA) {
|
if (kitJson.optInt("schema") != KIT_SCHEMA) {
|
||||||
return "This app's game kit is version ${kitJson.optInt("schema")}, which this builder does not know."
|
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)
|
val recipe = kitJson.getJSONObject("products").optJSONObject(profile.id)
|
||||||
?: return "This app's game kit cannot build ${profile.id}."
|
?: return "This app's game kit cannot build ${profile.id}."
|
||||||
report(40, Step.Prepare)
|
report(40, Step.Prepare)
|
||||||
@@ -646,6 +649,7 @@ object GameBuild {
|
|||||||
.put("dolSha256", BuildConfig.DISC_DOL_SHA256)
|
.put("dolSha256", BuildConfig.DISC_DOL_SHA256)
|
||||||
.put("relSha256", BuildConfig.DISC_REL_SHA256)
|
.put("relSha256", BuildConfig.DISC_REL_SHA256)
|
||||||
.put("kitFingerprint", kitFingerprint)
|
.put("kitFingerprint", kitFingerprint)
|
||||||
|
.put("androidCpu", BuildConfig.ANDROID_CPU)
|
||||||
.put("library", GameLibrary.LIBRARY_NAME)
|
.put("library", GameLibrary.LIBRARY_NAME)
|
||||||
.put("librarySha256", BuildRecipe.hex(digest.digest()))
|
.put("librarySha256", BuildRecipe.hex(digest.digest()))
|
||||||
.put("includesData", false)
|
.put("includesData", false)
|
||||||
|
|||||||
@@ -157,6 +157,9 @@ object GamePackageImport {
|
|||||||
if (manifest.optString("kitFingerprint") != GameLibrary.kitFingerprint(context)) {
|
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."
|
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")
|
val expected = manifest.optString("librarySha256")
|
||||||
if (librarySha256 == null) {
|
if (librarySha256 == null) {
|
||||||
return "This game file has no game library (libmain.so)."
|
return "This game file has no game library (libmain.so)."
|
||||||
|
|||||||
@@ -815,6 +815,10 @@ class LauncherActivity : Activity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun play() {
|
private fun play() {
|
||||||
|
if (BuildConfig.QUEST1_DIRECT_LAUNCH) {
|
||||||
|
Toast.makeText(this, R.string.home_quest1_launch_from_library, Toast.LENGTH_LONG).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (launching) {
|
if (launching) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">WiiCompiled VR</string>
|
<string name="app_name">WiiCompiled VR</string>
|
||||||
|
<string name="quest1_settings_name">WiiCompiled Settings</string>
|
||||||
<string name="missing_game_data_title">Game data not found</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>
|
<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>
|
||||||
|
|
||||||
@@ -102,6 +103,7 @@
|
|||||||
<string name="home_replace_title">Replace the game files?</string>
|
<string name="home_replace_title">Replace the game files?</string>
|
||||||
<string name="home_replace_message">Your current DATA folder stays until the new disc image has been checked and completely extracted.</string>
|
<string name="home_replace_message">Your current DATA folder stays until the new disc image has been checked and completely extracted.</string>
|
||||||
<string name="home_launch_failed">The game could not be started.</string>
|
<string name="home_launch_failed">The game could not be started.</string>
|
||||||
|
<string name="home_quest1_launch_from_library">Quest 1 cannot switch this settings panel into VR. Close it, then launch WiiCompiled VR from your library.</string>
|
||||||
<string name="home_close_game_first">Close the game first.</string>
|
<string name="home_close_game_first">Close the game first.</string>
|
||||||
<string name="home_reset">Reset installation</string>
|
<string name="home_reset">Reset installation</string>
|
||||||
<string name="home_reset_title">Reset the installation?</string>
|
<string name="home_reset_title">Reset the installation?</string>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application>
|
||||||
|
<!-- Modern Horizon OS can move from this 2D setup panel into immersive VR. -->
|
||||||
|
<activity android:name=".launcher.LauncherActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name=".QuestActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="com.oculus.intent.category.VR" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application>
|
||||||
|
<!-- Quest 1's final Horizon OS cannot promote a running 2D panel to an
|
||||||
|
immersive activity. Put the game itself in the library instead. -->
|
||||||
|
<activity android:name=".QuestActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="com.oculus.intent.category.VR" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter android:priority="1">
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity-alias
|
||||||
|
android:name=".SettingsLauncher"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/quest1_settings_name"
|
||||||
|
android:targetActivity=".launcher.LauncherActivity">
|
||||||
|
<intent-filter android:priority="-1">
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity-alias>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@@ -15,8 +15,25 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#if defined(__ANDROID__)
|
||||||
|
#include <sys/system_properties.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
#if defined(__ANDROID__)
|
||||||
|
bool quest1_direct_display_copy() noexcept {
|
||||||
|
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 bool quest1_direct_display_copy() noexcept { return false; }
|
||||||
|
#endif
|
||||||
|
|
||||||
struct CopyClearState {
|
struct CopyClearState {
|
||||||
bool clearColor = false;
|
bool clearColor = false;
|
||||||
bool clearAlpha = false;
|
bool clearAlpha = false;
|
||||||
@@ -417,7 +434,15 @@ void GXCopyDisp(void* dest, GXBool clear) {
|
|||||||
const auto logicalDstHeight = std::max<u32>(
|
const auto logicalDstHeight = std::max<u32>(
|
||||||
g_gxState.dispCopyDstHeight != 0 ? g_gxState.dispCopyDstHeight : static_cast<u32>(g_gxState.dispCopySrc.height),
|
g_gxState.dispCopyDstHeight != 0 ? g_gxState.dispCopyDstHeight : static_cast<u32>(g_gxState.dispCopySrc.height),
|
||||||
1);
|
1);
|
||||||
const auto [dstWidth, dstHeight] = scale_copy_dst(logicalDstWidth, logicalDstHeight);
|
auto [dstWidth, dstHeight] = scale_copy_dst(logicalDstWidth, logicalDstHeight);
|
||||||
|
if (quest1_direct_display_copy()) {
|
||||||
|
// The Quest 1's Adreno 540 driver renders Aurora's filtered display
|
||||||
|
// copy as uniform black/white. Keeping the destination identical to the
|
||||||
|
// resolved EFB region selects WebGPU's plain CopyTextureToTexture path;
|
||||||
|
// the later presentation pass performs the final screen scaling.
|
||||||
|
dstWidth = static_cast<u32>(std::max(rect.width, 1));
|
||||||
|
dstHeight = static_cast<u32>(std::max(rect.height, 1));
|
||||||
|
}
|
||||||
|
|
||||||
if (!g_gxState.displayCopyTexture || g_gxState.displayCopyWidth != dstWidth ||
|
if (!g_gxState.displayCopyTexture || g_gxState.displayCopyWidth != dstWidth ||
|
||||||
g_gxState.displayCopyHeight != dstHeight) {
|
g_gxState.displayCopyHeight != dstHeight) {
|
||||||
@@ -431,6 +456,15 @@ void GXCopyDisp(void* dest, GXBool clear) {
|
|||||||
if (aurora::g_config.disableCopyFilter) {
|
if (aurora::g_config.disableCopyFilter) {
|
||||||
copyFilter = {0, copyFilter[0] + copyFilter[1] + copyFilter[2], 0};
|
copyFilter = {0, copyFilter[0] + copyFilter[1] + copyFilter[2], 0};
|
||||||
}
|
}
|
||||||
|
if (quest1_direct_display_copy()) {
|
||||||
|
copyFilter = {0, 64, 0};
|
||||||
|
static bool questDirectDisplayCopyLogged = false;
|
||||||
|
if (!questDirectDisplayCopyLogged) {
|
||||||
|
questDirectDisplayCopyLogged = true;
|
||||||
|
std::fprintf(stderr, "[gx] Quest 1 direct display copy: EFB rect %dx%d at %d,%d -> %ux%u\n",
|
||||||
|
rect.width, rect.height, rect.x, rect.y, dstWidth, dstHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
aurora::gfx::resolve_pass(g_gxState.displayCopyTexture, rect, clearState.clearColor, clearState.clearAlpha,
|
aurora::gfx::resolve_pass(g_gxState.displayCopyTexture, rect, clearState.clearColor, clearState.clearAlpha,
|
||||||
clearState.clearDepth, clearState.clearColorValue, aurora::gx::clear_depth_value(),
|
clearState.clearDepth, clearState.clearColorValue, aurora::gx::clear_depth_value(),
|
||||||
GX_TF_RGBA8, nullptr, false, ©Filter, false,
|
GX_TF_RGBA8, nullptr, false, ©Filter, false,
|
||||||
@@ -450,11 +484,21 @@ void GXCopyTex(void* dest, GXBool clear) {
|
|||||||
// Keep guest dimensions for cache identity while preserving scaled GPU detail.
|
// Keep guest dimensions for cache identity while preserving scaled GPU detail.
|
||||||
const auto logicalDstWidth = std::max<u32>(g_gxState.texCopyDstWidth, 1);
|
const auto logicalDstWidth = std::max<u32>(g_gxState.texCopyDstWidth, 1);
|
||||||
const auto logicalDstHeight = std::max<u32>(g_gxState.texCopyDstHeight, 1);
|
const auto logicalDstHeight = std::max<u32>(g_gxState.texCopyDstHeight, 1);
|
||||||
const auto [scaledDstWidth, scaledDstHeight] = scale_copy_dst(logicalDstWidth, logicalDstHeight);
|
auto [scaledDstWidth, scaledDstHeight] = scale_copy_dst(logicalDstWidth, logicalDstHeight);
|
||||||
const auto texCopyFmt = g_gxState.texCopyFmt;
|
const auto texCopyFmt = g_gxState.texCopyFmt;
|
||||||
const bool sourceHasAlpha = aurora::gx::render_target_has_alpha(g_gxState.pixelFmt);
|
const bool sourceHasAlpha = aurora::gx::render_target_has_alpha(g_gxState.pixelFmt);
|
||||||
const bool forceOpaqueAlpha = !sourceHasAlpha && !aurora::gx::is_depth_format(texCopyFmt);
|
const bool forceOpaqueAlpha = !sourceHasAlpha && !aurora::gx::is_depth_format(texCopyFmt);
|
||||||
const auto resolveFmt = texCopyFmt;
|
const bool quest1DirectRgb5a3 = quest1_direct_display_copy() && texCopyFmt == GX_TF_RGB5A3;
|
||||||
|
auto resolveFmt = texCopyFmt;
|
||||||
|
if (quest1DirectRgb5a3) {
|
||||||
|
// The host texture is RGBA8 regardless of the guest cache format. Match
|
||||||
|
// the EFB rectangle and advertise an RGBA8 resolve so this live menu copy
|
||||||
|
// becomes CopyTextureToTexture instead of invoking Adreno's broken
|
||||||
|
// RGB5A3 conversion/scaling shader.
|
||||||
|
scaledDstWidth = static_cast<u32>(std::max(rect.width, 1));
|
||||||
|
scaledDstHeight = static_cast<u32>(std::max(rect.height, 1));
|
||||||
|
resolveFmt = GX_TF_RGBA8;
|
||||||
|
}
|
||||||
|
|
||||||
const aurora::gx::GXState::CopyTextureKey key{
|
const aurora::gx::GXState::CopyTextureKey key{
|
||||||
.dest = dest,
|
.dest = dest,
|
||||||
@@ -518,7 +562,10 @@ void GXCopyTex(void* dest, GXBool clear) {
|
|||||||
if (aurora::gx::render_target_has_alpha(g_gxState.pixelFmt)) {
|
if (aurora::gx::render_target_has_alpha(g_gxState.pixelFmt)) {
|
||||||
clearState.clearAlpha = clear && alphaUpdate;
|
clearState.clearAlpha = clear && alphaUpdate;
|
||||||
}
|
}
|
||||||
const auto copyFilter = combined_copy_filter_coefficients(g_gxState.copyFilterVFilter);
|
auto copyFilter = combined_copy_filter_coefficients(g_gxState.copyFilterVFilter);
|
||||||
|
if (quest1DirectRgb5a3) {
|
||||||
|
copyFilter = {0, 64, 0};
|
||||||
|
}
|
||||||
// Skip only recurring color copies so one-shot copies are never lost.
|
// Skip only recurring color copies so one-shot copies are never lost.
|
||||||
const bool producedConsecutively = handle.revision != 0 && currentFrame - handle.lastProducedFrame <= 1;
|
const bool producedConsecutively = handle.revision != 0 && currentFrame - handle.lastProducedFrame <= 1;
|
||||||
const bool persistentCopy = !aurora::gx::is_depth_format(texCopyFmt) && !producedConsecutively;
|
const bool persistentCopy = !aurora::gx::is_depth_format(texCopyFmt) && !producedConsecutively;
|
||||||
@@ -531,7 +578,8 @@ void GXCopyTex(void* dest, GXBool clear) {
|
|||||||
}
|
}
|
||||||
aurora::gfx::resolve_pass(handle.handle, rect, clearState.clearColor, clearState.clearAlpha, clearState.clearDepth,
|
aurora::gfx::resolve_pass(handle.handle, rect, clearState.clearColor, clearState.clearAlpha, clearState.clearDepth,
|
||||||
clearState.clearColorValue, aurora::gx::clear_depth_value(), resolveFmt,
|
clearState.clearColorValue, aurora::gx::clear_depth_value(), resolveFmt,
|
||||||
&sourceRect.sampleRect, g_gxState.texCopyHalfScale, ©Filter, forceOpaqueAlpha,
|
quest1DirectRgb5a3 ? nullptr : &sourceRect.sampleRect,
|
||||||
|
quest1DirectRgb5a3 ? false : g_gxState.texCopyHalfScale, ©Filter, forceOpaqueAlpha,
|
||||||
sourceRect.sampleRect.w() / std::max<float>(g_gxState.texCopySrc.height, 1.0f),
|
sourceRect.sampleRect.w() / std::max<float>(g_gxState.texCopySrc.height, 1.0f),
|
||||||
(g_gxState.copyClamp & GX_CLAMP_TOP) != 0, (g_gxState.copyClamp & GX_CLAMP_BOTTOM) != 0,
|
(g_gxState.copyClamp & GX_CLAMP_TOP) != 0, (g_gxState.copyClamp & GX_CLAMP_BOTTOM) != 0,
|
||||||
persistentCopy);
|
persistentCopy);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
|
#include <cstring>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
@@ -23,6 +24,12 @@
|
|||||||
#include <fmt/format.h>
|
#include <fmt/format.h>
|
||||||
#include <tracy/Tracy.hpp>
|
#include <tracy/Tracy.hpp>
|
||||||
|
|
||||||
|
#if defined(__ANDROID__)
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <sys/resource.h>
|
||||||
|
#include <sys/system_properties.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace aurora::gfx {
|
namespace aurora::gfx {
|
||||||
static Module Log("aurora::gfx::pipeline_cache");
|
static Module Log("aurora::gfx::pipeline_cache");
|
||||||
|
|
||||||
@@ -66,8 +73,26 @@ constexpr size_t MaxQueuedPipelineBuilds = 256;
|
|||||||
// render and game threads, and cap large hosts to limit driver submissions and memory use.
|
// render and game threads, and cap large hosts to limit driver submissions and memory use.
|
||||||
constexpr size_t ReservedLogicalProcessors = 2;
|
constexpr size_t ReservedLogicalProcessors = 2;
|
||||||
constexpr size_t MaxPipelineWorkers = 22;
|
constexpr size_t MaxPipelineWorkers = 22;
|
||||||
// Cached clear and GX pipelines are prewarmed using the full worker pool.
|
constexpr size_t Quest1MaxPipelineWorkers = 2;
|
||||||
constexpr size_t MaxBackgroundPipelineWorkers = MaxPipelineWorkers;
|
static bool g_quest1PipelineScheduling = false;
|
||||||
|
|
||||||
|
static bool quest1_pipeline_scheduling() noexcept {
|
||||||
|
#if defined(__ANDROID__)
|
||||||
|
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
|
||||||
|
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)
|
// For synchronous pipeline fallback (OpenGL)
|
||||||
#ifdef NDEBUG
|
#ifdef NDEBUG
|
||||||
constexpr size_t BuildPipelinesPerFrame = 5;
|
constexpr size_t BuildPipelinesPerFrame = 5;
|
||||||
@@ -462,6 +487,12 @@ static PipelineRef find_pipeline_impl(ShaderType type, const PipelineConfig& con
|
|||||||
}
|
}
|
||||||
} else if (g_pendingPipelines.contains(hash)) {
|
} else if (g_pendingPipelines.contains(hash)) {
|
||||||
auto* pending = touch_pending_pipeline(hash, g_pipelineFrameActive);
|
auto* pending = touch_pending_pipeline(hash, g_pipelineFrameActive);
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
if (pending != nullptr && firstFrameUsed < pending->firstFrameUsed) {
|
if (pending != nullptr && firstFrameUsed < pending->firstFrameUsed) {
|
||||||
pending->firstFrameUsed = firstFrameUsed;
|
pending->firstFrameUsed = firstFrameUsed;
|
||||||
if (persist) {
|
if (persist) {
|
||||||
@@ -1033,6 +1064,14 @@ static void pipeline_worker() {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
tracy::SetThreadName("Pipeline compilation thread");
|
tracy::SetThreadName("Pipeline compilation thread");
|
||||||
#endif
|
#endif
|
||||||
|
#if defined(__ANDROID__)
|
||||||
|
pthread_setname_np(pthread_self(), "GXPipeline");
|
||||||
|
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) {
|
while (true) {
|
||||||
PendingPipeline pending;
|
PendingPipeline pending;
|
||||||
@@ -1042,7 +1081,7 @@ static void pipeline_worker() {
|
|||||||
g_pipelineCv.wait(lock, [] {
|
g_pipelineCv.wait(lock, [] {
|
||||||
return !g_priorityPipelines.empty() ||
|
return !g_priorityPipelines.empty() ||
|
||||||
(!g_backgroundPipelines.empty() &&
|
(!g_backgroundPipelines.empty() &&
|
||||||
g_activeBackgroundPipelineWorkers < MaxBackgroundPipelineWorkers) ||
|
g_activeBackgroundPipelineWorkers < max_background_pipeline_workers()) ||
|
||||||
g_pipelineThreadEnd;
|
g_pipelineThreadEnd;
|
||||||
});
|
});
|
||||||
if (g_pipelineThreadEnd) {
|
if (g_pipelineThreadEnd) {
|
||||||
@@ -1091,7 +1130,8 @@ static size_t pipeline_worker_count() {
|
|||||||
}
|
}
|
||||||
const size_t availableWorkers =
|
const size_t availableWorkers =
|
||||||
logicalProcessors > ReservedLogicalProcessors ? logicalProcessors - ReservedLogicalProcessors : 1;
|
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 <typename PipelineConfig, typename CreateFn>
|
template <typename PipelineConfig, typename CreateFn>
|
||||||
@@ -1318,6 +1358,7 @@ void initialize_pipeline_cache() {
|
|||||||
g_pipelineFrameActive = false;
|
g_pipelineFrameActive = false;
|
||||||
g_pipelineThreadEnd = false;
|
g_pipelineThreadEnd = false;
|
||||||
g_activeBackgroundPipelineWorkers = 0;
|
g_activeBackgroundPipelineWorkers = 0;
|
||||||
|
g_quest1PipelineScheduling = quest1_pipeline_scheduling();
|
||||||
|
|
||||||
if (webgpu::g_backendType == wgpu::BackendType::OpenGL || webgpu::g_backendType == wgpu::BackendType::OpenGLES ||
|
if (webgpu::g_backendType == wgpu::BackendType::OpenGL || webgpu::g_backendType == wgpu::BackendType::OpenGLES ||
|
||||||
webgpu::g_backendType == wgpu::BackendType::WebGPU) {
|
webgpu::g_backendType == wgpu::BackendType::WebGPU) {
|
||||||
@@ -1330,7 +1371,7 @@ void initialize_pipeline_cache() {
|
|||||||
g_pipelineThreads.emplace_back(pipeline_worker);
|
g_pipelineThreads.emplace_back(pipeline_worker);
|
||||||
}
|
}
|
||||||
Log.info("Enabled {} priority pipeline compilation workers ({} background prewarm)",
|
Log.info("Enabled {} priority pipeline compilation workers ({} background prewarm)",
|
||||||
workerCount, MaxBackgroundPipelineWorkers);
|
workerCount, max_background_pipeline_workers());
|
||||||
}
|
}
|
||||||
|
|
||||||
load_pipeline_cache();
|
load_pipeline_cache();
|
||||||
|
|||||||
@@ -10,6 +10,12 @@
|
|||||||
|
|
||||||
#include <absl/container/flat_hash_map.h>
|
#include <absl/container/flat_hash_map.h>
|
||||||
|
|
||||||
|
#if defined(__ANDROID__)
|
||||||
|
#include <sys/system_properties.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
#include "texture_convert.hpp"
|
#include "texture_convert.hpp"
|
||||||
|
|
||||||
using namespace std::string_literals;
|
using namespace std::string_literals;
|
||||||
@@ -195,6 +201,48 @@ static constexpr std::string_view FragPassthrough = R"(
|
|||||||
}
|
}
|
||||||
)"sv;
|
)"sv;
|
||||||
|
|
||||||
|
// The Quest 1's Adreno compiler misrenders the general copy shader, whose
|
||||||
|
// sample helper contains texture-dimension math and a dynamic filter branch,
|
||||||
|
// even for passthrough RGBA copies. This equivalent shader deliberately keeps
|
||||||
|
// the same bind-group contract while omitting that unused filter machinery.
|
||||||
|
static constexpr std::string_view SimpleBlitShader = R"(
|
||||||
|
@group(0) @binding(0) var src_samp: sampler;
|
||||||
|
@group(0) @binding(1) var src: texture_2d<f32>;
|
||||||
|
|
||||||
|
struct UVTransform {
|
||||||
|
offset: vec2f,
|
||||||
|
scale: vec2f,
|
||||||
|
copy_filter: vec4f,
|
||||||
|
flags: vec4f,
|
||||||
|
};
|
||||||
|
@group(0) @binding(2) var<uniform> uv_xf: UVTransform;
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) pos: vec4f,
|
||||||
|
@location(0) uv: vec2f,
|
||||||
|
};
|
||||||
|
|
||||||
|
var<private> positions: array<vec2f, 3> = array(
|
||||||
|
vec2f(-1.0, 1.0), vec2f(-1.0, -3.0), vec2f(3.0, 1.0));
|
||||||
|
var<private> uvs: array<vec2f, 3> = array(
|
||||||
|
vec2f(0.0, 0.0), vec2f(0.0, 2.0), vec2f(2.0, 0.0));
|
||||||
|
|
||||||
|
@vertex fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
out.pos = vec4f(positions[vi], 0.0, 1.0);
|
||||||
|
out.uv = uvs[vi] * uv_xf.scale + uv_xf.offset;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f {
|
||||||
|
let color = textureSample(src, src_samp, in.uv);
|
||||||
|
if (uv_xf.flags.x != 0.0) {
|
||||||
|
return vec4f(color.rgb, 1.0);
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
)"sv;
|
||||||
|
|
||||||
// GX_TF_I4: 4-bit intensity -> R8Unorm (quantized)
|
// GX_TF_I4: 4-bit intensity -> R8Unorm (quantized)
|
||||||
static constexpr std::string_view FragI4 = R"(
|
static constexpr std::string_view FragI4 = R"(
|
||||||
@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f {
|
@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f {
|
||||||
@@ -375,6 +423,19 @@ static wgpu::Sampler g_nearestSampler;
|
|||||||
static wgpu::Sampler g_linearSampler;
|
static wgpu::Sampler g_linearSampler;
|
||||||
static absl::flat_hash_map<GXTexFmt, wgpu::RenderPipeline> g_pipelines;
|
static absl::flat_hash_map<GXTexFmt, wgpu::RenderPipeline> g_pipelines;
|
||||||
static wgpu::RenderPipeline g_blitPipeline;
|
static wgpu::RenderPipeline g_blitPipeline;
|
||||||
|
static wgpu::RenderPipeline g_quest1BlitPipeline;
|
||||||
|
|
||||||
|
static bool quest1_simple_blit() noexcept {
|
||||||
|
#if defined(__ANDROID__)
|
||||||
|
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
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
static wgpu::RenderPipeline create_pipeline(const ConvPipeline& conv, const std::string_view shaderPreamble,
|
static wgpu::RenderPipeline create_pipeline(const ConvPipeline& conv, const std::string_view shaderPreamble,
|
||||||
const wgpu::BindGroupLayout& bindGroupLayout) {
|
const wgpu::BindGroupLayout& bindGroupLayout) {
|
||||||
@@ -491,6 +552,9 @@ void initialize() {
|
|||||||
g_blitPipeline = create_pipeline(
|
g_blitPipeline = create_pipeline(
|
||||||
{GX_TF_RGBA8, FragPassthrough, webgpu::g_graphicsConfig.surfaceConfiguration.format, "TexCopyConv Blit"},
|
{GX_TF_RGBA8, FragPassthrough, webgpu::g_graphicsConfig.surfaceConfiguration.format, "TexCopyConv Blit"},
|
||||||
ShaderPreamble, g_bindGroupLayout);
|
ShaderPreamble, g_bindGroupLayout);
|
||||||
|
g_quest1BlitPipeline = create_pipeline(
|
||||||
|
{GX_TF_RGBA8, {}, webgpu::g_graphicsConfig.surfaceConfiguration.format, "Quest 1 Simple TexCopy Blit"},
|
||||||
|
SimpleBlitShader, g_bindGroupLayout);
|
||||||
for (const auto& conv : ConvPipelines) {
|
for (const auto& conv : ConvPipelines) {
|
||||||
g_pipelines[conv.fmt] = create_pipeline(conv, ShaderPreamble, g_bindGroupLayout);
|
g_pipelines[conv.fmt] = create_pipeline(conv, ShaderPreamble, g_bindGroupLayout);
|
||||||
if (conv.outputFormat != to_wgpu(conv.fmt)) {
|
if (conv.outputFormat != to_wgpu(conv.fmt)) {
|
||||||
@@ -522,6 +586,7 @@ void initialize() {
|
|||||||
void shutdown() {
|
void shutdown() {
|
||||||
g_pipelines.clear();
|
g_pipelines.clear();
|
||||||
g_blitPipeline = {};
|
g_blitPipeline = {};
|
||||||
|
g_quest1BlitPipeline = {};
|
||||||
g_bindGroupLayout = {};
|
g_bindGroupLayout = {};
|
||||||
g_depthBindGroupLayout = {};
|
g_depthBindGroupLayout = {};
|
||||||
g_nearestSampler = {};
|
g_nearestSampler = {};
|
||||||
@@ -597,6 +662,14 @@ static void execute(const wgpu::CommandEncoder& cmd, const ConvRequest& req, con
|
|||||||
}
|
}
|
||||||
|
|
||||||
void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req) {
|
void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req) {
|
||||||
|
if (quest1_simple_blit() && req.fmt == GX_TF_RGB5A3) {
|
||||||
|
// MKW's character/kart menu previews are live RGB5A3 EFB copies. The
|
||||||
|
// Adreno 540 miscompiles their quantizing conversion shader just like the
|
||||||
|
// filtered display-copy shader, leaving the preview panels black. Preserve
|
||||||
|
// the RGBA source directly; only the Wii-era RGB5A3 quantization is lost.
|
||||||
|
execute(cmd, req, g_quest1BlitPipeline);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const auto it = g_pipelines.find(req.fmt);
|
const auto it = g_pipelines.find(req.fmt);
|
||||||
if (it == g_pipelines.end()) {
|
if (it == g_pipelines.end()) {
|
||||||
Log.fatal("No copy conversion pipeline for format {}", static_cast<int>(req.fmt));
|
Log.fatal("No copy conversion pipeline for format {}", static_cast<int>(req.fmt));
|
||||||
@@ -604,6 +677,8 @@ void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req) {
|
|||||||
execute(cmd, req, it->second);
|
execute(cmd, req, it->second);
|
||||||
}
|
}
|
||||||
|
|
||||||
void blit(const wgpu::CommandEncoder& cmd, const ConvRequest& req) { execute(cmd, req, g_blitPipeline); }
|
void blit(const wgpu::CommandEncoder& cmd, const ConvRequest& req) {
|
||||||
|
execute(cmd, req, quest1_simple_blit() ? g_quest1BlitPipeline : g_blitPipeline);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace aurora::gfx::tex_copy_conv
|
} // namespace aurora::gfx::tex_copy_conv
|
||||||
+46
-1
@@ -182,6 +182,46 @@ density map does not reduce (the stores stay full size for non-subsampled images
|
|||||||
stays off by default; it pays only when `render_scale` makes the eyes pixel-bound, as at 1.3 on
|
stays off by default; it pays only when `render_scale` makes the eyes pixel-bound, as at 1.3 on
|
||||||
Luigi Circuit.
|
Luigi Circuit.
|
||||||
|
|
||||||
|
### Quest 1 renderer compatibility
|
||||||
|
|
||||||
|
Quest 1 identifies itself as Android device `monterey` and uses an Adreno 540
|
||||||
|
driver that misrenders Aurora's general filtered EFB-copy shader. On that
|
||||||
|
device, `GXCopyDisp` keeps the destination equal to the resolved EFB region so
|
||||||
|
WebGPU can use `CopyTextureToTexture`; the later presentation pass still scales
|
||||||
|
the result. Live RGB5A3 menu copies use a minimal RGBA passthrough shader for
|
||||||
|
the same reason. Other Android devices retain the normal filtered and
|
||||||
|
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. 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
|
||||||
|
entries: **WiiCompiled VR** starts the game directly in VR, while
|
||||||
|
**WiiCompiled Settings** opens the setup/settings panel. After changing a
|
||||||
|
setting, close the panel and start WiiCompiled VR from the library. The Play
|
||||||
|
button in the settings panel explains this limitation instead of opening a
|
||||||
|
black panel. Modern Quest builds keep the normal single launcher, whose Play
|
||||||
|
button starts VR directly.
|
||||||
|
|
||||||
|
The workaround was isolated on a physical Quest 1: it changed the boot/menu
|
||||||
|
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 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. 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. Newer Android headsets
|
||||||
|
and desktop retain the normal worker pool and prewarm behavior.
|
||||||
|
|
||||||
### Controllers
|
### Controllers
|
||||||
|
|
||||||
Quest Touch controllers are not HID gamepads, so `openxr_input.cpp` syncs an
|
Quest Touch controllers are not HID gamepads, so `openxr_input.cpp` syncs an
|
||||||
@@ -772,7 +812,10 @@ Android facts this design rests on, all measured on a Quest 3:
|
|||||||
also learned `--target-os windows|macos|linux|android` for
|
also learned `--target-os windows|macos|linux|android` for
|
||||||
`generate-data-init` and `translate-mod`, for pipelines that generate on
|
`generate-data-init` and `translate-mod`, for pipelines that generate on
|
||||||
another host.
|
another host.
|
||||||
- `android/`: the Gradle project, one app with no flavours.
|
- `android/`: the Gradle project, with `modernQuest` (the default script
|
||||||
|
target) and `quest1` headset flavours. They share the application ID and
|
||||||
|
storage, but select the appropriate CPU baseline, supported-device manifest,
|
||||||
|
and launcher behavior.
|
||||||
`app/src/main/cpp/CMakeLists.txt` adds the repository's `runtime/` as a
|
`app/src/main/cpp/CMakeLists.txt` adds the repository's `runtime/` as a
|
||||||
subdirectory with those Android choices and builds both game kit probes
|
subdirectory with those Android choices and builds both game kit probes
|
||||||
(the Retro Rewind one only when the translation includes the mod), which
|
(the Retro Rewind one only when the translation includes the mod), which
|
||||||
@@ -794,8 +837,10 @@ installer's `BuildWorkspace/generated`, produced by the normal Windows pipeline)
|
|||||||
```powershell
|
```powershell
|
||||||
powershell -ExecutionPolicy Bypass -File android/Prepare-QuestDependencies.ps1 # SDL3 3.4.4 AAR into android/app/libs
|
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 # the app, its game kit and toolchain, debug-signed (the first run also builds Dawn from source)
|
powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Install # the app, its game kit and toolchain, debug-signed (the first run also builds Dawn from source)
|
||||||
|
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 -Install # your game, against that kit, into Import (or WheelWizard VR's Build for Quest)
|
||||||
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Product retro_rewind -Mod <RetroRewind6> -Install # the mod and its pack (needs translate-mod output with --retro-wfc-payload)
|
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Product retro_rewind -Mod <RetroRewind6> -Install # the mod and its pack (needs translate-mod output with --retro-wfc-payload)
|
||||||
|
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Headset quest1 -Install # game package from the Quest 1 kit
|
||||||
adb push MarioKart.iso /sdcard/Download/ # then Select disc image in the launcher
|
adb push MarioKart.iso /sdcard/Download/ # then Select disc image in the launcher
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user