Added Retro Rewind Quest Support

This commit is contained in:
iChris4 committed 2026-09-17 21:36:00 +02:00
1 parent 0bf2675db1
commit 58c0890983
18 files changed
+368 -158

No files matched your search

@@ -72,8 +72,16 @@ internal sealed class QuestBuildService
var questRoot = Path.Combine(installation.Root, QuestDirectoryName); var questRoot = Path.Combine(installation.Root, QuestDirectoryName);
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 kitFingerprint = ExtractKit(apkPath, kitDirectory, cancellationToken); var kit = ExtractKit(apkPath, kitDirectory, cancellationToken);
_reporter.Diagnostic($"Quest game kit {kitFingerprint}"); _reporter.Diagnostic($"Quest game kit {kit.Fingerprint} ({kit.Product})");
// A Retro Rewind app needs this installation's Retro Rewind translation, which only exists
// once the mod has been built here at least once.
if (kit.Product == "retro_rewind" && !HasRetroRewindShards(generated))
{
throw new InvalidOperationException(
"This Quest app is the Retro Rewind one, and this installation has no Retro Rewind " +
"translation. Install or repair Retro Rewind, then build for Quest again.");
}
var toolchain = await EnsureToolchainAsync(questRoot, cancellationToken); var toolchain = await EnsureToolchainAsync(questRoot, cancellationToken);
@@ -116,7 +124,7 @@ internal sealed class QuestBuildService
TryDelete(temporaryOutput); TryDelete(temporaryOutput);
throw; throw;
} }
return new Result(outputPath, kitFingerprint, includeGameFiles, new FileInfo(outputPath).Length); return new Result(outputPath, kit.Fingerprint, includeGameFiles, new FileInfo(outputPath).Length);
} }
private void TryDelete(string path) private void TryDelete(string path)
@@ -147,13 +155,24 @@ internal sealed class QuestBuildService
} }
} }
/// <summary>Whether emit-build-shards recorded a Retro Rewind translation in this workspace.</summary>
private static bool HasRetroRewindShards(string generated)
{
var shards = Path.Combine(generated, "build_shards", "shards.cmake");
return File.Exists(shards) &&
File.ReadAllText(shards).Contains("set(MKW_HAVE_RETRO_REWIND_SHARDS ON)", StringComparison.Ordinal);
}
private static bool IsGameData(string directory) => private static bool IsGameData(string directory) =>
Directory.Exists(Path.Combine(directory, "files")) && Directory.Exists(Path.Combine(directory, "files")) &&
File.Exists(Path.Combine(directory, "sys", "fst.bin")) && File.Exists(Path.Combine(directory, "sys", "fst.bin")) &&
File.Exists(Path.Combine(directory, "sys", "main.dol")); File.Exists(Path.Combine(directory, "sys", "main.dol"));
/// <summary>Extracts <c>assets/game_kit</c> from the Quest app's APK and returns its fingerprint.</summary> /// <summary>What the extracted kit says it builds: its fingerprint and product ("base" or "retro_rewind").</summary>
internal static string ExtractKit(string apkPath, string destination, CancellationToken cancellationToken) internal sealed record Kit(string Fingerprint, string Product);
/// <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)
{ {
const string prefix = "assets/game_kit/"; const string prefix = "assets/game_kit/";
if (Directory.Exists(destination)) Directory.Delete(destination, recursive: true); if (Directory.Exists(destination)) Directory.Delete(destination, recursive: true);
@@ -179,10 +198,16 @@ internal sealed class QuestBuildService
throw new InvalidDataException( throw new InvalidDataException(
"This APK is not a WiiCompiled Quest app with a game kit. Choose the Quest app installed on the headset."); "This APK is not a WiiCompiled Quest app with a game kit. Choose the Quest app installed on the headset.");
using var document = JsonDocument.Parse(File.ReadAllText(kitJson)); using var document = JsonDocument.Parse(File.ReadAllText(kitJson));
return document.RootElement.TryGetProperty("fingerprint", out var fingerprint) && if (!document.RootElement.TryGetProperty("fingerprint", out var fingerprint) ||
fingerprint.ValueKind == JsonValueKind.String fingerprint.ValueKind != JsonValueKind.String)
? fingerprint.GetString()! {
: throw new InvalidDataException("The Quest app's game kit has no fingerprint."); throw new InvalidDataException("The Quest app's game kit has no fingerprint.");
}
var product = document.RootElement.TryGetProperty("product", out var value) &&
value.ValueKind == JsonValueKind.String
? value.GetString()!
: "base";
return new Kit(fingerprint.GetString()!, product);
} }
private async Task<Toolchain> EnsureToolchainAsync(string questRoot, CancellationToken cancellationToken) private async Task<Toolchain> EnsureToolchainAsync(string questRoot, CancellationToken cancellationToken)
@@ -369,14 +369,15 @@ 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\":1,\"fingerprint\":\"abc123\"}"); Add("assets/game_kit/kit.json", "{\"schema\":2,\"product\":\"retro_rewind\",\"fingerprint\":\"abc123\"}");
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");
if (QuestBuildService.ExtractKit(apk, kit, CancellationToken.None) != "abc123") var extracted = QuestBuildService.ExtractKit(apk, kit, CancellationToken.None);
throw new Exception("The kit fingerprint was not read."); if (extracted.Fingerprint != "abc123" || extracted.Product != "retro_rewind")
throw new Exception("The kit fingerprint or product was not read.");
if (!File.Exists(Path.Combine(kit, "include", "memory.h")) || if (!File.Exists(Path.Combine(kit, "include", "memory.h")) ||
Directory.GetFiles(kit, "*", SearchOption.AllDirectories).Length != 2) Directory.GetFiles(kit, "*", SearchOption.AllDirectories).Length != 2)
throw new Exception("The kit extraction took the wrong files."); throw new Exception("The kit extraction took the wrong files.");
+14 -10
View File
@@ -87,23 +87,27 @@ $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)"
# The base APK must be distributable: no translated game inside, only the game kit. # Every APK must be distributable: no translated game inside, only the game kit. The toolchain
if ($Flavor -eq 'base') { # that builds one on the headset travels in the base APK only.
Add-Type -AssemblyName System.IO.Compression.FileSystem 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 $hasKit = @($archive.Entries | Where-Object { $_.FullName -eq 'assets/game_kit/kit.json' }).Count -gt 0
$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 $hasKit) { throw 'The APK has no game kit (assets/game_kit/kit.json)' }
if (-not $hasToolchain) { throw 'The APK has no build toolchain (assets/quest_toolchain/files.zip)' } if ($Flavor -eq 'base' -and -not $hasToolchain) { throw 'The APK has no build toolchain (assets/quest_toolchain/files.zip)' }
} if ($Flavor -ne 'base' -and $hasToolchain) { throw 'Only the base APK carries the on-headset build toolchain' }
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)" }
if ($Flavor -eq 'base') {
Write-Host 'Installed. Build the game with Build on this Quest in the launcher, or on this PC with android/Build-QuestGame.ps1 -Install.' Write-Host 'Installed. Build the game with Build on this Quest in the launcher, or on this PC with android/Build-QuestGame.ps1 -Install.'
} else {
Write-Host "Installed. Build its game on this PC with android/Build-QuestGame.ps1 -Flavor $Flavor -Install."
}
} }
+13 -7
View File
@@ -13,7 +13,8 @@
# downloaded, the toolkit's ninja and the workspace's recomp.yml. # downloaded, the toolkit's ninja and the workspace's recomp.yml.
# #
# -Generated is the translator output (the dev workspace's generated/ by default). -Kit is the game # -Generated is the translator output (the dev workspace's generated/ by default). -Kit is the game
# kit the Quest app carries; it defaults to the one the last APK build exported. -Data adds the # kit the Quest app carries; it defaults to the one the last APK build of -Flavor (base or
# retroRewind, whose translation must include the mod) exported. -Data adds the
# extracted disc (the directory holding sys/ and files/) so the headset needs nothing else. # extracted disc (the directory holding sys/ and files/) so the headset needs nothing else.
# -Install pushes the package into the app's Import folder over adb, where the launcher picks it # -Install pushes the package into the app's Import folder over adb, where the launcher picks it
# up the next time it opens. # up the next time it opens.
@@ -30,6 +31,7 @@ param(
[string]$Sysroot = '', [string]$Sysroot = '',
[string]$Ninja = '', [string]$Ninja = '',
[string]$BuiltBy = 'android/Build-QuestGame.ps1', [string]$BuiltBy = 'android/Build-QuestGame.ps1',
[ValidateSet('base', 'retroRewind')] [string]$Flavor = 'base',
[int]$TranslatedJobs = 0, [int]$TranslatedJobs = 0,
[switch]$Install [switch]$Install
) )
@@ -40,15 +42,18 @@ $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' } if (-not $BuildDir) { $BuildDir = Join-Path $root "app\build\questGame\$Flavor" }
if (-not $Kit) { if (-not $Kit) {
# The kit the last APK build packaged (the exportBase*QuestGameKit tasks), so the game # The kit this flavour's last APK build packaged (its export<Flavor>*QuestGameKit task), so the
# matches the app it will be imported into. # game matches the app it will be imported into.
$exportTask = 'export' + $Flavor.Substring(0, 1).ToUpper() + $Flavor.Substring(1)
$Kit = Get-ChildItem -Path (Join-Path $root 'app\build\generated') -Filter kit.json -Recurse -ErrorAction SilentlyContinue | $Kit = Get-ChildItem -Path (Join-Path $root 'app\build\generated') -Filter kit.json -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.Directory.Name -eq 'game_kit' } | Where-Object { $_.Directory.Name -eq 'game_kit' -and $_.FullName -like "*$exportTask*" } |
Sort-Object LastWriteTime -Descending | Select-Object -First 1 | ForEach-Object { $_.DirectoryName } Sort-Object LastWriteTime -Descending | Select-Object -First 1 | ForEach-Object { $_.DirectoryName }
} }
if (-not $Kit -or -not (Test-Path (Join-Path $Kit 'kit.json'))) { throw 'No game kit; run Build-Quest.ps1 first or pass -Kit' } if (-not $Kit -or -not (Test-Path (Join-Path $Kit 'kit.json'))) {
throw "No $Flavor game kit; run Build-Quest.ps1 -Flavor $Flavor first, or pass -Kit"
}
if (-not (Test-Path (Join-Path $Generated 'build_shards\shards.cmake'))) { throw "No translated graph at $Generated" } if (-not (Test-Path (Join-Path $Generated 'build_shards\shards.cmake'))) { throw "No translated graph at $Generated" }
$sdkRoot = if ($env:ANDROID_HOME) { $env:ANDROID_HOME } elseif ($env:ANDROID_SDK_ROOT) { $env:ANDROID_SDK_ROOT } else { Join-Path $env:LOCALAPPDATA 'Android\Sdk' } $sdkRoot = if ($env:ANDROID_HOME) { $env:ANDROID_HOME } elseif ($env:ANDROID_SDK_ROOT) { $env:ANDROID_SDK_ROOT } else { Join-Path $env:LOCALAPPDATA 'Android\Sdk' }
@@ -91,7 +96,8 @@ Write-Host "Game package: $Output (kit $($game.kitFingerprint))"
if ($Install) { if ($Install) {
$adb = Join-Path $sdkRoot 'platform-tools\adb.exe' $adb = Join-Path $sdkRoot 'platform-tools\adb.exe'
$import = '/sdcard/Android/data/org.wiicompiled.quest/files/WiiCompiledOpenXRVR/Import' $package = if ($Flavor -eq 'retroRewind') { 'org.wiicompiled.quest.retrorewind' } else { 'org.wiicompiled.quest' }
$import = "/sdcard/Android/data/$package/files/WiiCompiledOpenXRVR/Import"
# The launcher creates Import itself and must own it to remove imported packages (see # The launcher creates Import itself and must own it to remove imported packages (see
# Run-Quest.ps1 on adb-created directories), so only push into one that exists. # Run-Quest.ps1 on adb-created directories), so only push into one that exists.
$exists = ((& $adb shell "test -d '$import' && echo yes") | Out-String).Trim() $exists = ((& $adb shell "test -d '$import' && echo yes") | Out-String).Trim()
+6 -5
View File
@@ -1,16 +1,17 @@
# Exports the Quest game kit (see QuestGameKit.psm1) from a configured and built Android CMake # Exports the Quest game kit (see QuestGameKit.psm1) from a configured and built Android CMake
# tree. android/app/build.gradle.kts runs this after the native build and packages the result as # tree. android/app/build.gradle.kts runs this after the native build and packages the result as
# the APK's game_kit assets. # the APK's game_kit assets, once per app flavour.
# #
# powershell -ExecutionPolicy Bypass -File android/Export-QuestGameKit.ps1 -CMakeBinaryDir <dir> -OutputDir <dir> -LlvmStrip <llvm-strip> # powershell -ExecutionPolicy Bypass -File android/Export-QuestGameKit.ps1 -CMakeBinaryDir <dir> -OutputDir <dir> -LlvmStrip <llvm-strip> [-Product base|retro_rewind]
[CmdletBinding()] [CmdletBinding()]
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,
[ValidateSet('base', 'retro_rewind')] [string]$Product = 'base'
) )
$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
$fingerprint = Export-QuestGameKit -CMakeBinaryDir $CMakeBinaryDir -OutputDir $OutputDir -RepoRoot $repo -LlvmStrip $LlvmStrip $fingerprint = Export-QuestGameKit -CMakeBinaryDir $CMakeBinaryDir -OutputDir $OutputDir -RepoRoot $repo -LlvmStrip $LlvmStrip -Product $Product
Write-Host "Quest game kit $fingerprint -> $OutputDir" Write-Host "Quest game kit ($Product) $fingerprint -> $OutputDir"
+5
View File
@@ -271,6 +271,11 @@ function Publish-Translator([string]$Destination, [string]$Work) {
& (Join-Path $env:SystemRoot 'System32\robocopy.exe') (Join-Path $repo $tree) (Join-Path $stage $tree) /E /XD bin obj /NFL /NDL /NJH /NJS /NP | Out-Null & (Join-Path $env:SystemRoot 'System32\robocopy.exe') (Join-Path $repo $tree) (Join-Path $stage $tree) /E /XD bin obj /NFL /NDL /NJH /NJS /NP | Out-Null
if ($LASTEXITCODE -ge 8) { throw "Staging $tree failed (robocopy $LASTEXITCODE)" } if ($LASTEXITCODE -ge 8) { throw "Staging $tree failed (robocopy $LASTEXITCODE)" }
} }
# Build output the checkout may hold for another target framework would be picked up here and
# make the retargeted publish resolve the wrong references.
Get-ChildItem -Recurse -Directory $stage |
Where-Object { $_.Name -eq 'bin' -or $_.Name -eq 'obj' } |
ForEach-Object { if (Test-Path $_.FullName) { Remove-Item -Recurse -Force $_.FullName } }
foreach ($project in Get-ChildItem -Recurse -Filter *.csproj $stage) { foreach ($project in Get-ChildItem -Recurse -Filter *.csproj $stage) {
$text = [IO.File]::ReadAllText($project.FullName) $text = [IO.File]::ReadAllText($project.FullName)
$retargeted = $text.Replace('<TargetFramework>net8.0</TargetFramework>', '<TargetFramework>net10.0</TargetFramework>') $retargeted = $text.Replace('<TargetFramework>net8.0</TargetFramework>', '<TargetFramework>net10.0</TargetFramework>')
+113 -54
View File
@@ -8,12 +8,14 @@
# exported from CMake's own build graph so its flags and link order cannot drift from a normal # exported from CMake's own build graph so its flags and link order cannot drift from a normal
# build: # build:
# #
# * the link line of mkw_quest_kit_probe (runtime/cmake/PublicProducts.cmake), which is # * the link line of the flavour's probe (runtime/cmake/PublicProducts.cmake), which is that
# WiiCompiled without the game, becomes kit.json's link inputs, with {game:*} markers where # product without any translated code, becomes kit.json's link inputs, with {game:*} markers
# the game's objects sat in WiiCompiled's own link; # where the game's objects sat in the product's own link;
# * the compile commands CMake recorded for one source of each generated kind (translated shard, # * the compile commands CMake recorded for one source of each generated kind (translated shard,
# registration shard, disc-generated runtime source, blob assembly) become kit.json's # registration shard, disc-generated runtime source, blob assembly) become kit.json's
# compile flags, with include paths reduced to the kit's copy of runtime/include. # compile flags, with include paths reduced to the kit's copy of runtime/include;
# * kit.json's "sources" says which of the translator's build_shards lists fill each slot, so a
# builder never has to know which flavour it is building.
# #
# Placeholders in kit.json: {kit} the extracted kit, {sysroot} the NDK sysroot the compiler # Placeholders in kit.json: {kit} the extracted kit, {sysroot} the NDK sysroot the compiler
# uses, {workspace} the directory holding the translator's generated/ tree. # uses, {workspace} the directory holding the translator's generated/ tree.
@@ -22,9 +24,42 @@
Set-StrictMode -Version Latest Set-StrictMode -Version Latest
$script:KitSchema = 1 $script:KitSchema = 2
$script:GameSchema = 1 $script:GameSchema = 1
# What each app flavour's kit is made of. Slots name the game's objects in the probe's link line,
# and Sources says which of the translator's build_shards lists ("@NAME") or generated files fill
# each of them, in link order. Retro Rewind adds a slot: the mod's own shards and the
# profile-sensitive base shards it replaces the base flavour's with, linked as objects rather
# than through the base archive.
$script:KitProfiles = @{
base = @{
Probe = 'libmkw_quest_kit_probe'
ProductObjectPattern = '/base_product\.cpp\.o$'
RegistrationPattern = '/build_shards/base_registration/[^/]+\.cpp$'
Slots = @('runtime', 'product', 'translated')
Sources = [ordered]@{
runtime = @('{workspace}/generated/data_sections_init.cpp', '{workspace}/generated/guest_symbol_table.cpp',
'{workspace}/generated/data_sections_init_blobs.S')
product = @('@MKW_BASE_REGISTRATION_SOURCES')
translated = @('@MKW_BASE_COMMON_SHARDS', '@MKW_BASE_PORTABLE_SENSITIVE_SHARDS')
}
}
retro_rewind = @{
Probe = 'libmkw_quest_kit_probe_retro'
ProductObjectPattern = '/retro_rewind_product\.cpp\.o$'
RegistrationPattern = '/build_shards/retro_rewind_registration/[^/]+\.cpp$'
Slots = @('runtime', 'product', 'mod', 'translated')
Sources = [ordered]@{
runtime = @('{workspace}/generated/data_sections_init.cpp', '{workspace}/generated/guest_symbol_table.cpp',
'{workspace}/generated/data_sections_init_blobs.S')
product = @('@MKW_RETRO_REGISTRATION_SOURCES')
mod = @('@MKW_RETRO_PORTABLE_SENSITIVE_SHARDS', '@MKW_RETRO_MOD_SHARDS', '@MKW_RETRO_EXTRA_SOURCES')
translated = @('@MKW_BASE_COMMON_SHARDS')
}
}
}
function ConvertTo-ForwardPath([string]$Path) { function ConvertTo-ForwardPath([string]$Path) {
return [IO.Path]::GetFullPath($Path).Replace('\', '/').TrimEnd('/') return [IO.Path]::GetFullPath($Path).Replace('\', '/').TrimEnd('/')
} }
@@ -165,7 +200,7 @@ function Split-DriverCommand([string]$Line) {
# sysroot/ and lib/clang/), {output}, and {game:runtime}, {game:product}, {game:translated}, each # sysroot/ and lib/clang/), {output}, and {game:runtime}, {game:product}, {game:translated}, each
# replaced by its object files (the translated ones already sit inside --start-lib/--end-lib). # replaced by its object files (the translated ones already sit inside --start-lib/--end-lib).
function Get-KitLldArguments { function Get-KitLldArguments {
param([string]$ClangCxx, [string]$KitDir, [string[]]$Flags, [string[]]$Inputs) param([string]$ClangCxx, [string]$KitDir, [string[]]$Flags, [string[]]$Inputs, [string[]]$Slots)
$kit = ConvertTo-ForwardPath $KitDir $kit = ConvertTo-ForwardPath $KitDir
$ndk = ConvertTo-ForwardPath (Join-Path (Split-Path -Parent $ClangCxx) '..') $ndk = ConvertTo-ForwardPath (Join-Path (Split-Path -Parent $ClangCxx) '..')
# Forward slashes throughout: clang reads response files with GNU quoting, where '\' escapes. # Forward slashes throughout: clang reads response files with GNU quoting, where '\' escapes.
@@ -173,7 +208,7 @@ function Get-KitLldArguments {
New-Item -ItemType Directory $probe | Out-Null New-Item -ItemType Directory $probe | Out-Null
try { try {
$markers = [ordered]@{} $markers = [ordered]@{}
foreach ($slot in 'runtime', 'product', 'translated') { foreach ($slot in $Slots) {
$markers[$slot] = ConvertTo-ForwardPath (Join-Path $probe "game_$slot.o") $markers[$slot] = ConvertTo-ForwardPath (Join-Path $probe "game_$slot.o")
[IO.File]::WriteAllBytes($markers[$slot], [byte[]]@()) [IO.File]::WriteAllBytes($markers[$slot], [byte[]]@())
} }
@@ -182,12 +217,12 @@ function Get-KitLldArguments {
foreach ($flag in $Flags) { $arguments.Add((& $real $flag)) } foreach ($flag in $Flags) { $arguments.Add((& $real $flag)) }
$arguments.Add('-o'); $arguments.Add("$probe/libmain.so") $arguments.Add('-o'); $arguments.Add("$probe/libmain.so")
foreach ($linkInput in $Inputs) { foreach ($linkInput in $Inputs) {
switch ($linkInput) { $slot = [regex]::Match($linkInput, '^\{game:(\w+)\}$').Groups[1].Value
'{game:runtime}' { $arguments.Add($markers.runtime) } if (-not $slot) { $arguments.Add((& $real $linkInput)); continue }
'{game:product}' { $arguments.Add($markers.product) } # Only the base archive's slot keeps archive semantics; the rest are plain objects.
'{game:translated}' { $arguments.Add('-Wl,--start-lib'); $arguments.Add($markers.translated); $arguments.Add('-Wl,--end-lib') } if ($slot -eq 'translated') { $arguments.Add('-Wl,--start-lib') }
default { $arguments.Add((& $real $linkInput)) } $arguments.Add($markers[$slot])
} if ($slot -eq 'translated') { $arguments.Add('-Wl,--end-lib') }
} }
$rsp = Join-Path $probe 'link.rsp' $rsp = Join-Path $probe 'link.rsp'
[IO.File]::WriteAllText($rsp, (($arguments | ForEach-Object { ConvertTo-QuotedArgument $_ }) -join "`n"), (New-Object Text.UTF8Encoding $false)) [IO.File]::WriteAllText($rsp, (($arguments | ForEach-Object { ConvertTo-QuotedArgument $_ }) -join "`n"), (New-Object Text.UTF8Encoding $false))
@@ -218,7 +253,7 @@ function Get-KitLldArguments {
if ($argument -match '[A-Za-z]:/') { throw "The kit link names a path outside the kit and the NDK: $argument" } if ($argument -match '[A-Za-z]:/') { throw "The kit link names a path outside the kit and the NDK: $argument" }
$lld.Add($argument) $lld.Add($argument)
} }
foreach ($required in '{output}', '{game:runtime}', '{game:product}', '{game:translated}') { foreach ($required in @('{output}') + @($Slots | ForEach-Object { "{game:$_}" })) {
if (-not $lld.Contains($required)) { throw "The expanded kit link lacks $required" } if (-not $lld.Contains($required)) { throw "The expanded kit link lacks $required" }
} }
return , $lld.ToArray() return , $lld.ToArray()
@@ -233,16 +268,18 @@ 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,
[ValidateSet('base', 'retro_rewind')] [string]$Product = 'base'
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$binary = ConvertTo-ForwardPath $CMakeBinaryDir $binary = ConvertTo-ForwardPath $CMakeBinaryDir
$runtimeInclude = ConvertTo-ForwardPath (Join-Path $RepoRoot 'runtime/include') $runtimeInclude = ConvertTo-ForwardPath (Join-Path $RepoRoot 'runtime/include')
$kitProfile = $script:KitProfiles[$Product]
# The probe's link edge: explicit inputs are the objects, LINK_LIBRARIES the archives. # The probe's link edge: explicit inputs are the objects, LINK_LIBRARIES the archives.
$ninja = [IO.File]::ReadAllText("$binary/build.ninja") $ninja = [IO.File]::ReadAllText("$binary/build.ninja")
$edge = [regex]::Match($ninja, '(?m)^build (\S*libmkw_quest_kit_probe\.so): (\S+) ([^\r\n]*)\r?\n((?: [^\r\n]*\r?\n)+)') $edge = [regex]::Match($ninja, "(?m)^build (\S*$([regex]::Escape($kitProfile.Probe))\.so): (\S+) ([^\r\n]*)\r?\n((?: [^\r\n]*\r?\n)+)")
if (-not $edge.Success) { throw "No mkw_quest_kit_probe link edge in $binary/build.ninja" } if (-not $edge.Success) { throw "No $($kitProfile.Probe) link edge in $binary/build.ninja" }
$explicit = ($edge.Groups[3].Value.Replace('$ ', '<ninja-space>') -split ' \|')[0] $explicit = ($edge.Groups[3].Value.Replace('$ ', '<ninja-space>') -split ' \|')[0]
$objects = @($explicit -split ' ' | Where-Object { $_ } | ForEach-Object { ConvertFrom-NinjaPath $_.Replace('<ninja-space>', '$ ') }) $objects = @($explicit -split ' ' | Where-Object { $_ } | ForEach-Object { ConvertFrom-NinjaPath $_.Replace('<ninja-space>', '$ ') })
$vars = @{} $vars = @{}
@@ -272,7 +309,7 @@ function Export-QuestGameKit {
} }
$compile = [ordered]@{ $compile = [ordered]@{
translated = & $flagsFor $translatedEntry translated = & $flagsFor $translatedEntry
product = & $flagsFor (Find-Command '/build_shards/base_registration/[^/]+\.cpp$') product = & $flagsFor (Find-Command $kitProfile.RegistrationPattern)
runtime = & $flagsFor (Find-Command '/generated/data_sections_init\.cpp$') runtime = & $flagsFor (Find-Command '/generated/data_sections_init\.cpp$')
asm = & $flagsFor (Find-Command 'data_sections_init_blobs[^/]*\.S$') asm = & $flagsFor (Find-Command 'data_sections_init_blobs[^/]*\.S$')
} }
@@ -306,11 +343,17 @@ function Export-QuestGameKit {
} }
for ($i = 0; $i -lt $objects.Count; $i++) { for ($i = 0; $i -lt $objects.Count; $i++) {
$inputs.Add((& $copy $objects[$i] 'objects')) $inputs.Add((& $copy $objects[$i] 'objects'))
if ($i -eq $lastRuntime) { $inputs.Add('{game:runtime}') } if ($objects[$i] -match $kitProfile.ProductObjectPattern) { $inputs.Add('{game:product}') }
if ($objects[$i] -match '/base_product\.cpp\.o$') { $inputs.Add('{game:product}') } if ($i -eq $lastRuntime) {
$inputs.Add('{game:runtime}')
# Retro Rewind links the mod's own objects after the runtime's, as its CMake target does.
if ($kitProfile.Slots -contains 'mod') { $inputs.Add('{game:mod}') }
}
}
foreach ($slot in $kitProfile.Slots) {
if ($slot -ne 'translated' -and -not $inputs.Contains("{game:$slot}")) {
throw "The probe link line lacks the objects the {game:$slot} slot follows"
} }
if (-not $inputs.Contains('{game:runtime}') -or -not $inputs.Contains('{game:product}')) {
throw 'The probe link line lacks the runtime or product objects the game slots follow'
} }
foreach ($token in ((ConvertFrom-NinjaPath $vars['LINK_LIBRARIES']) -split ' ' | Where-Object { $_ })) { foreach ($token in ((ConvertFrom-NinjaPath $vars['LINK_LIBRARIES']) -split ' ' | Where-Object { $_ })) {
if ($token.StartsWith('-')) { $inputs.Add($token); continue } if ($token.StartsWith('-')) { $inputs.Add($token); continue }
@@ -334,7 +377,7 @@ function Export-QuestGameKit {
} }
$linkFlags.Add('-Wl,-soname,libmain.so') $linkFlags.Add('-Wl,-soname,libmain.so')
$lld = Get-KitLldArguments -ClangCxx (Join-Path (Split-Path -Parent $LlvmStrip) 'clang++.exe') -KitDir $OutputDir ` $lld = Get-KitLldArguments -ClangCxx (Join-Path (Split-Path -Parent $LlvmStrip) 'clang++.exe') -KitDir $OutputDir `
-Flags $linkFlags.ToArray() -Inputs $inputs.ToArray() -Flags $linkFlags.ToArray() -Inputs $inputs.ToArray() -Slots $kitProfile.Slots
# What the headset's translator needs besides the disc: the game manifest and symbol map, and # What the headset's translator needs besides the disc: the game manifest and symbol map, and
# the runtime sources it indexes for native registrations, which must be the ones compiled into # the runtime sources it indexes for native registrations, which must be the ones compiled into
@@ -346,11 +389,12 @@ function Export-QuestGameKit {
$recipe = [ordered]@{ $recipe = [ordered]@{
schema = $script:KitSchema schema = $script:KitSchema
product = 'base' product = $Product
output = 'libmain.so' output = 'libmain.so'
target = $target target = $target
runtimeIncludeFingerprint = Get-RuntimeIncludeFingerprint (Join-Path $OutputDir 'include') runtimeIncludeFingerprint = Get-RuntimeIncludeFingerprint (Join-Path $OutputDir 'include')
compile = $compile compile = $compile
sources = $kitProfile.Sources
link = [ordered]@{ flags = $linkFlags.ToArray(); inputs = $inputs.ToArray(); lld = $lld } link = [ordered]@{ flags = $linkFlags.ToArray(); inputs = $inputs.ToArray(); lld = $lld }
} }
$recipeJson = $recipe | ConvertTo-Json -Depth 8 -Compress $recipeJson = $recipe | ConvertTo-Json -Depth 8 -Compress
@@ -409,21 +453,37 @@ function Invoke-QuestGameBuild {
$linkLines = @($recipe.link.flags | ForEach-Object { ConvertTo-QuotedArgument (& $expand $_) }) $linkLines = @($recipe.link.flags | ForEach-Object { ConvertTo-QuotedArgument (& $expand $_) })
[IO.File]::WriteAllText("$build/link.rsp", ($linkLines -join "`n"), (New-Object Text.UTF8Encoding $false)) [IO.File]::WriteAllText("$build/link.rsp", ($linkLines -join "`n"), (New-Object Text.UTF8Encoding $false))
# The Windows installer generates the blob assembly for PE/COFF; rewrite it for ELF exactly # The Windows installer generates blob assembly for PE/COFF; every one of them is rewritten for
# as runtime/cmake/PublicProducts.cmake does. # ELF exactly as runtime/cmake/PublicProducts.cmake does, into the build directory.
$blob = [IO.File]::ReadAllText("$generated/data_sections_init_blobs.S") $toElf = {
$elf = $blob.Replace('.section .rdata,"dr"', '.section .rodata,"a",@progbits') param([string]$Source)
if ($elf -ne $blob) { $elf += "`n.section .note.GNU-stack,`"`",@progbits`n" } $text = [IO.File]::ReadAllText($Source)
[IO.File]::WriteAllText("$build/data_sections_init_blobs_android.S", $elf, (New-Object Text.UTF8Encoding $false)) $elf = $text.Replace('.section .rdata,"dr"', '.section .rodata,"a",@progbits')
if ($elf -ne $text) { $elf += "`n.section .note.GNU-stack,`"`",@progbits`n" }
$rewritten = "$build/$([IO.Path]::GetFileNameWithoutExtension($Source))_android.S"
[IO.File]::WriteAllText($rewritten, $elf, (New-Object Text.UTF8Encoding $false))
return $rewritten
}
# Each slot's sources, as the recipe names them: a build_shards list ("@NAME") or one generated
# file.
$shards = "$generated/build_shards/shards.cmake" $shards = "$generated/build_shards/shards.cmake"
$sources = [ordered]@{ $sources = [ordered]@{}
runtime = @("$generated/data_sections_init.cpp", "$generated/guest_symbol_table.cpp") foreach ($slot in $recipe.sources.PSObject.Properties.Name) {
runtimeAsm = @("$build/data_sections_init_blobs_android.S") $slotSources = New-Object System.Collections.Generic.List[string]
product = @(Read-ShardList $shards 'MKW_BASE_REGISTRATION_SOURCES') foreach ($entry in $recipe.sources.$slot) {
translated = @((Read-ShardList $shards 'MKW_BASE_COMMON_SHARDS') + (Read-ShardList $shards 'MKW_BASE_PORTABLE_SENSITIVE_SHARDS')) if ($entry.StartsWith('@')) {
Read-ShardList $shards $entry.Substring(1) | ForEach-Object { $slotSources.Add($_) }
} else {
$slotSources.Add((& $expand $entry))
}
}
$sources[$slot] = @($slotSources | ForEach-Object { if ($_.EndsWith('.S')) { & $toElf $_ } else { $_ } })
} }
if ($sources.translated.Count -eq 0) { throw "No translated shards in $shards" } if ($sources.translated.Count -eq 0) { throw "No translated shards in $shards" }
foreach ($slot in $sources.Keys) {
if ($sources[$slot].Count -eq 0) { throw "The $slot sources of a $($recipe.product) game are missing from $shards" }
}
$ninjaText = New-Object System.Text.StringBuilder $ninjaText = New-Object System.Text.StringBuilder
$escape = { param([string]$p) $p.Replace('$', '$$').Replace(':', '$:').Replace(' ', '$ ') } $escape = { param([string]$p) $p.Replace('$', '$$').Replace(':', '$:').Replace(' ', '$ ') }
@@ -432,37 +492,36 @@ function Invoke-QuestGameBuild {
[void]$ninjaText.AppendLine("rule cxx`n command = $(ConvertTo-QuotedArgument $ClangCxx) @`$flags -c `$in -o `$out`n description = Compiling `$in") [void]$ninjaText.AppendLine("rule cxx`n command = $(ConvertTo-QuotedArgument $ClangCxx) @`$flags -c `$in -o `$out`n description = Compiling `$in")
[void]$ninjaText.AppendLine("rule asm`n command = $(ConvertTo-QuotedArgument $ClangC) @`$flags -c `$in -o `$out`n description = Assembling `$in") [void]$ninjaText.AppendLine("rule asm`n command = $(ConvertTo-QuotedArgument $ClangC) @`$flags -c `$in -o `$out`n description = Assembling `$in")
[void]$ninjaText.AppendLine("rule link`n command = $(ConvertTo-QuotedArgument $ClangCxx) @`$flags -o `$out @`$out.rsp`n rspfile = `$out.rsp`n rspfile_content = `$inputs`n description = Linking `$out") [void]$ninjaText.AppendLine("rule link`n command = $(ConvertTo-QuotedArgument $ClangCxx) @`$flags -o `$out @`$out.rsp`n rspfile = `$out.rsp`n rspfile_content = `$inputs`n description = Linking `$out")
$objectsOf = @{ runtime = @(); product = @(); translated = @() } $objectsOf = @{}
$seen = @{} $seen = @{}
foreach ($kind in 'runtime', 'runtimeAsm', 'product', 'translated') { foreach ($slot in $sources.Keys) {
foreach ($source in $sources[$kind]) { $objectsOf[$slot] = @()
foreach ($source in $sources[$slot]) {
$stem = [IO.Path]::GetFileNameWithoutExtension($source) $stem = [IO.Path]::GetFileNameWithoutExtension($source)
$object = "obj/$stem.o" $object = "obj/$stem.o"
if ($seen.ContainsKey($object)) { throw "Two generated sources share the object name $object" } if ($seen.ContainsKey($object)) { throw "Two generated sources share the object name $object" }
$seen[$object] = $true $seen[$object] = $true
$rule = if ($kind -eq 'runtimeAsm') { 'asm' } else { 'cxx' } # Assembly is assembled; a mod shard compiles with the same flags as a translated one.
$flags = switch ($kind) { 'runtimeAsm' { 'asm.rsp' } default { "$kind.rsp" } } $assembly = $source.EndsWith('.S')
$rule = if ($assembly) { 'asm' } else { 'cxx' }
$flags = if ($assembly) { 'asm.rsp' } elseif ($slot -eq 'mod') { 'translated.rsp' } else { "$slot.rsp" }
[void]$ninjaText.AppendLine("build $(& $escape $object): $rule $(& $escape $source)`n flags = $(& $escape "$build/$flags")") [void]$ninjaText.AppendLine("build $(& $escape $object): $rule $(& $escape $source)`n flags = $(& $escape "$build/$flags")")
if ($kind -eq 'translated') { [void]$ninjaText.AppendLine(' pool = translated') } if ($slot -eq 'translated' -or $slot -eq 'mod') { [void]$ninjaText.AppendLine(' pool = translated') }
$slot = if ($kind -eq 'runtimeAsm') { 'runtime' } else { $kind }
$objectsOf[$slot] += "$build/$object" $objectsOf[$slot] += "$build/$object"
} }
} }
$linkInputs = New-Object System.Collections.Generic.List[string] $linkInputs = New-Object System.Collections.Generic.List[string]
foreach ($linkInput in $recipe.link.inputs) { foreach ($linkInput in $recipe.link.inputs) {
switch ($linkInput) { $slot = [regex]::Match($linkInput, '^\{game:(\w+)\}$').Groups[1].Value
'{game:runtime}' { $objectsOf.runtime | ForEach-Object { $linkInputs.Add($_) } } if (-not $slot) { $linkInputs.Add((& $expand $linkInput)); continue }
'{game:product}' { $objectsOf.product | ForEach-Object { $linkInputs.Add($_) } } if (-not $objectsOf.ContainsKey($slot)) { throw "The kit recipe links a {game:$slot} slot it names no sources for" }
'{game:translated}' { # The base shards keep the archive semantics libmkw_base_shared.a has in a CMake build;
# Archive semantics, as libmkw_base_shared.a has in a CMake build. # every other slot is linked as plain objects, as the product's own CMake target does.
$linkInputs.Add('-Wl,--start-lib') if ($slot -eq 'translated') { $linkInputs.Add('-Wl,--start-lib') }
$objectsOf.translated | ForEach-Object { $linkInputs.Add($_) } $objectsOf[$slot] | ForEach-Object { $linkInputs.Add($_) }
$linkInputs.Add('-Wl,--end-lib') if ($slot -eq 'translated') { $linkInputs.Add('-Wl,--end-lib') }
} }
default { $linkInputs.Add((& $expand $linkInput)) } $allObjects = @($sources.Keys | ForEach-Object { $objectsOf[$_] } | ForEach-Object { & $escape $_.Substring($build.Length + 1) })
}
}
$allObjects = @($objectsOf.runtime + $objectsOf.product + $objectsOf.translated | ForEach-Object { & $escape $_.Substring($build.Length + 1) })
$rspInputs = ($linkInputs | ForEach-Object { ConvertTo-QuotedArgument $_ }) -join ' ' $rspInputs = ($linkInputs | ForEach-Object { ConvertTo-QuotedArgument $_ }) -join ' '
[void]$ninjaText.AppendLine("build libmain.so: link $($allObjects -join ' ')`n flags = $(& $escape "$build/link.rsp")`n inputs = $($rspInputs.Replace('$', '$$'))") [void]$ninjaText.AppendLine("build libmain.so: link $($allObjects -join ' ')`n flags = $(& $escape "$build/link.rsp")`n inputs = $($rspInputs.Replace('$', '$$'))")
[IO.File]::WriteAllText("$build/build.ninja", $ninjaText.ToString(), (New-Object Text.UTF8Encoding $false)) [IO.File]::WriteAllText("$build/build.ninja", $ninjaText.ToString(), (New-Object Text.UTF8Encoding $false))
+35 -22
View File
@@ -130,6 +130,8 @@ android {
// The APK carries the game kit, not the game: the player's libmain.so is built from // The APK carries the game kit, not the game: the player's libmain.so is built from
// their own disc and loaded from private storage (see android/QuestGameKit.psm1). // their own disc and loaded from private storage (see android/QuestGameKit.psm1).
buildConfigField("boolean", "EXTERNAL_GAME", "true") buildConfigField("boolean", "EXTERNAL_GAME", "true")
// And it carries the toolchain that builds it on the headset (Prepare-QuestToolchain.ps1).
buildConfigField("boolean", "ON_DEVICE_BUILD", "true")
externalNativeBuild { cmake { targets += "mkw_quest_kit_probe" } } externalNativeBuild { cmake { targets += "mkw_quest_kit_probe" } }
} }
create("retroRewind") { create("retroRewind") {
@@ -138,10 +140,11 @@ android {
versionNameSuffix = "-retro-rewind" versionNameSuffix = "-retro-rewind"
buildConfigField("String", "MAIN_LIBRARY", "\"main_retro_rewind\"") buildConfigField("String", "MAIN_LIBRARY", "\"main_retro_rewind\"")
buildConfigField("String", "PROFILE", "\"retro_rewind\"") buildConfigField("String", "PROFILE", "\"retro_rewind\"")
// Not moved to the game kit yet: this flavour still bundles its translated library, buildConfigField("boolean", "EXTERNAL_GAME", "true")
// so a Retro Rewind APK is for local use only. // Retro Rewind is built on a PC and imported: building it on the headset would also
buildConfigField("boolean", "EXTERNAL_GAME", "false") // need the mod's Code.pul and its content there, which the launcher cannot fetch yet.
externalNativeBuild { cmake { targets += "RetroRewind" } } buildConfigField("boolean", "ON_DEVICE_BUILD", "false")
externalNativeBuild { cmake { targets += "mkw_quest_kit_probe_retro" } }
} }
} }
@@ -170,8 +173,8 @@ android {
packaging { packaging {
jniLibs { jniLibs {
useLegacyPackaging = false useLegacyPackaging = false
// Built only to produce the game kit; its objects travel as assets/game_kit. // Built only to produce each flavour's game kit; their objects travel as assets/game_kit.
excludes += "**/libmkw_quest_kit_probe.so" excludes += "**/libmkw_quest_kit_probe*.so"
} }
} }
lint { lint {
@@ -201,6 +204,10 @@ abstract class ExportQuestGameKit : DefaultTask() {
@get:Internal @get:Internal
abstract val llvmStrip: RegularFileProperty abstract val llvmStrip: RegularFileProperty
/** "base" or "retro_rewind": which product's kit this flavour carries. */
@get:Internal
abstract val product: Property<String>
@get:Inject @get:Inject
abstract val execOperations: ExecOperations abstract val execOperations: ExecOperations
@@ -212,10 +219,11 @@ abstract class ExportQuestGameKit : DefaultTask() {
@TaskAction @TaskAction
fun export() { fun export() {
val app = appDir.get().asFile val app = appDir.get().asFile
val library = if (product.get() == "base") "libmkw_quest_kit_probe.so" else "libmkw_quest_kit_probe_retro.so"
val probe = File(app, "build/intermediates/cxx").walkTopDown() val probe = File(app, "build/intermediates/cxx").walkTopDown()
.filter { it.name == "libmkw_quest_kit_probe.so" && it.parentFile.name == "arm64-v8a" } .filter { it.name == library && it.parentFile.name == "arm64-v8a" }
.maxByOrNull { it.lastModified() } .maxByOrNull { it.lastModified() }
?: throw GradleException("No libmkw_quest_kit_probe.so; the native build did not produce the game kit probe") ?: throw GradleException("No $library; the native build did not produce the game kit probe")
val configuration = probe.parentFile.parentFile.parentFile // <BuildType>/<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")
if (!File(binaryDir, "build.ninja").isFile) throw GradleException("No CMake tree at $binaryDir") if (!File(binaryDir, "build.ninja").isFile) throw GradleException("No CMake tree at $binaryDir")
@@ -224,6 +232,7 @@ abstract class ExportQuestGameKit : DefaultTask() {
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,
"-Product", product.get(),
) )
} }
} }
@@ -263,32 +272,36 @@ abstract class PrepareQuestToolchain : DefaultTask() {
} }
androidComponents { androidComponents {
onVariants(selector().withFlavor("profile" to "base")) { variant -> onVariants { variant ->
val prepareToolchain = tasks.register<PrepareQuestToolchain>(
"prepare${variant.name.replaceFirstChar { it.uppercase() }}QuestToolchain"
) {
script.set(rootProject.layout.projectDirectory.file("Prepare-QuestToolchain.ps1"))
sources.from(script, rootProject.layout.projectDirectory.dir("toolchain"))
sources.from(fileTree(File(mkwRepoRoot, "translator/src")) { exclude("**/bin/**", "**/obj/**") })
sources.from(fileTree(File(mkwRepoRoot, "Launcher/WiiCompiled.Setup.Common")) { exclude("**/bin/**", "**/obj/**") })
val host = if (System.getProperty("os.name").startsWith("Windows")) "windows-x86_64" else "linux-x86_64" val host = if (System.getProperty("os.name").startsWith("Windows")) "windows-x86_64" else "linux-x86_64"
ndkLlvm.set(sdkComponents.ndkDirectory.map { it.dir("toolchains/llvm/prebuilt/$host") })
}
variant.sources.assets?.addGeneratedSourceDirectory(prepareToolchain, PrepareQuestToolchain::outputDir)
val capitalized = variant.name.replaceFirstChar { it.uppercase() } val capitalized = variant.name.replaceFirstChar { it.uppercase() }
// AGP packages every library in the CMake output directory, so a game library left over // Both flavours ship a kit instead of the game. AGP packages every library in the CMake
// from a build that still linked the game would otherwise ship in the APK. // output directory, so a game library left over from an earlier build would otherwise
// travel in the APK.
variant.packaging.jniLibs.excludes.add("**/libmain*.so") variant.packaging.jniLibs.excludes.add("**/libmain*.so")
val export = tasks.register<ExportQuestGameKit>("export${capitalized}QuestGameKit") { val export = tasks.register<ExportQuestGameKit>("export${capitalized}QuestGameKit") {
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"))
val host = if (System.getProperty("os.name").startsWith("Windows")) "windows-x86_64" else "linux-x86_64" product.set(if (variant.flavorName == "retroRewind") "retro_rewind" else "base")
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 "")
}) })
} }
variant.sources.assets?.addGeneratedSourceDirectory(export, ExportQuestGameKit::outputDir) variant.sources.assets?.addGeneratedSourceDirectory(export, ExportQuestGameKit::outputDir)
// Only the base flavour builds the game on the headset, so only it carries the toolchain:
// Retro Rewind also needs the mod's own content and translation, which is a PC build for now.
if (variant.flavorName != "retroRewind") {
val prepareToolchain = tasks.register<PrepareQuestToolchain>("prepare${capitalized}QuestToolchain") {
script.set(rootProject.layout.projectDirectory.file("Prepare-QuestToolchain.ps1"))
sources.from(script, rootProject.layout.projectDirectory.dir("toolchain"))
sources.from(fileTree(File(mkwRepoRoot, "translator/src")) { exclude("**/bin/**", "**/obj/**") })
sources.from(fileTree(File(mkwRepoRoot, "Launcher/WiiCompiled.Setup.Common")) { exclude("**/bin/**", "**/obj/**") })
ndkLlvm.set(sdkComponents.ndkDirectory.map { it.dir("toolchains/llvm/prebuilt/$host") })
}
variant.sources.assets?.addGeneratedSourceDirectory(prepareToolchain, PrepareQuestToolchain::outputDir)
}
} }
} }
+1 -1
View File
@@ -51,7 +51,7 @@ add_subdirectory("${MKW_REPO_ROOT}/runtime" wiicompiled-runtime)
# App-owned JNI glue lives in each product library SDLActivity loads, and in the game kit # App-owned JNI glue lives in each product library SDLActivity loads, and in the game kit
# probe (runtime/cmake/PublicProducts.cmake) whose objects the player's game is linked with. # probe (runtime/cmake/PublicProducts.cmake) whose objects the player's game is linked with.
foreach(product IN ITEMS WiiCompiled RetroRewind mkw_quest_kit_probe) foreach(product IN ITEMS WiiCompiled RetroRewind mkw_quest_kit_probe mkw_quest_kit_probe_retro)
if(TARGET ${product}) if(TARGET ${product})
target_sources(${product} PRIVATE "${CMAKE_CURRENT_LIST_DIR}/quest_surface_jni.cpp") target_sources(${product} PRIVATE "${CMAKE_CURRENT_LIST_DIR}/quest_surface_jni.cpp")
target_include_directories(${product} PRIVATE "${MKW_REPO_ROOT}/aurora-main/include") target_include_directories(${product} PRIVATE "${MKW_REPO_ROOT}/aurora-main/include")
@@ -17,6 +17,8 @@ object GameStorage {
// Must match kApplicationDirectoryName in runtime/include/runtime_config.h. // Must match kApplicationDirectoryName in runtime/include/runtime_config.h.
const val APP_DIRECTORY = "WiiCompiledOpenXRVR" const val APP_DIRECTORY = "WiiCompiledOpenXRVR"
const val DISC_DIRECTORY = "DATA" const val DISC_DIRECTORY = "DATA"
/** The Retro Rewind pack the modded product reads (`[paths] retro_rewind_root`). */
const val MOD_DIRECTORY = "RetroRewind6"
enum class DiscStatus { Missing, Incomplete, Ready } enum class DiscStatus { Missing, Incomplete, Ready }
@@ -26,6 +28,12 @@ object GameStorage {
fun discDirectory(context: Context): File = File(gameRoot(context), DISC_DIRECTORY) fun discDirectory(context: Context): File = File(gameRoot(context), DISC_DIRECTORY)
fun modDirectory(context: Context): File = File(gameRoot(context), MOD_DIRECTORY)
/** Whether this app needs the Retro Rewind pack and has it. Always true for the base game. */
fun modContentReady(context: Context): Boolean =
BuildConfig.PROFILE != "retro_rewind" || File(modDirectory(context), "Binaries/Code.pul").isFile
fun configFile(context: Context): File = File(gameRoot(context), "Config.toml") fun configFile(context: Context): File = File(gameRoot(context), "Config.toml")
fun logsDirectory(context: Context): File = File(gameRoot(context), "Logs") fun logsDirectory(context: Context): File = File(gameRoot(context), "Logs")
@@ -60,22 +68,28 @@ object GameStorage {
importDirectory(context).mkdirs() importDirectory(context).mkdirs()
val config = configFile(context) val config = configFile(context)
if (!config.isFile) { if (!config.isFile) {
val disc = discDirectory(context).absolutePath // Written line by line: trimIndent runs after interpolation, so an interpolated line
config.writeText( // would take the indent off every other one.
""" val lines = mutableListOf(
# WiiCompiled Quest configuration. Edit with the launcher, the in-game panel or adb pull/push. "# WiiCompiled Quest configuration. Edit with the launcher, the in-game panel or adb pull/push.",
[paths] "[paths]",
dvd_root = "$disc" "dvd_root = \"${discDirectory(context).absolutePath}\"",
[video]
widescreen = true
resolution_multiplier = 1.0
[vr]
enabled = true
render_scale = 1.0
""".trimIndent() + "\n",
) )
// The modded product reads its pack from retro_rewind_root, which sits next to DATA here.
if (BuildConfig.PROFILE == "retro_rewind") {
lines += "retro_rewind_root = \"${modDirectory(context).absolutePath}\""
}
lines += listOf(
"",
"[video]",
"widescreen = true",
"resolution_multiplier = 1.0",
"",
"[vr]",
"enabled = true",
"render_scale = 1.0",
)
config.writeText(lines.joinToString("\n", postfix = "\n"))
} }
return gameRoot return gameRoot
} }
@@ -9,10 +9,6 @@ import java.security.MessageDigest
*/ */
object BuildRecipe { object BuildRecipe {
/** The translated source lists emit-build-shards writes into shards.cmake, in link order. */
val TRANSLATED_LISTS = listOf("MKW_BASE_COMMON_SHARDS", "MKW_BASE_PORTABLE_SENSITIVE_SHARDS")
const val REGISTRATION_LIST = "MKW_BASE_REGISTRATION_SOURCES"
/** /**
* One argument of a response file for clang or lld on Android, which read them with GNU * One argument of a response file for clang or lld on Android, which read them with GNU
* quoting: a backslash escapes the next character. * quoting: a backslash escapes the next character.
@@ -53,6 +53,7 @@ object GameBuild {
} }
private const val TAG = "WiiCompiledLauncher" private const val TAG = "WiiCompiledLauncher"
private const val KIT_SCHEMA = 2
private const val TOOLCHAIN_ASSETS = "quest_toolchain" private const val TOOLCHAIN_ASSETS = "quest_toolchain"
private const val KIT_ASSETS = "game_kit" private const val KIT_ASSETS = "game_kit"
private const val MARKER = ".complete" private const val MARKER = ".complete"
@@ -66,6 +67,9 @@ object GameBuild {
/** Null on success, otherwise the message to show. */ /** Null on success, otherwise the message to show. */
fun run(context: Context, reporter: Reporter, cancelled: () -> Boolean, finishing: () -> Unit): String? { fun run(context: Context, reporter: Reporter, cancelled: () -> Boolean, finishing: () -> Unit): String? {
if (!BuildConfig.ON_DEVICE_BUILD) {
return "This app cannot build its game on the headset. Build it on a computer and use Import from computer."
}
val disc = GameStorage.discDirectory(context) val disc = GameStorage.discDirectory(context)
if (GameStorage.discStatus(context) != GameStorage.DiscStatus.Ready) { if (GameStorage.discStatus(context) != GameStorage.DiscStatus.Ready) {
return "The game files (DATA) are needed to build the game. Select your disc image first." return "The game files (DATA) are needed to build the game. Select your disc image first."
@@ -120,6 +124,9 @@ object GameBuild {
val kitFingerprint = GameLibrary.kitFingerprint(context) ?: return "This app carries no game kit." val kitFingerprint = GameLibrary.kitFingerprint(context) ?: return "This app carries no game kit."
unpackKit(kitFingerprint) unpackKit(kitFingerprint)
val recipe = JSONObject(File(kit, "kit.json").readText()) val recipe = JSONObject(File(kit, "kit.json").readText())
if (recipe.optInt("schema") != KIT_SCHEMA) {
return "This app's game kit is version ${recipe.optInt("schema")}, which this builder does not know."
}
report(40, Step.Prepare) report(40, Step.Prepare)
downloadNdk(JSONObject(context.assets.open("$TOOLCHAIN_ASSETS/${toolchainManifest.getString("ndk")}").reader().use { it.readText() })) downloadNdk(JSONObject(context.assets.open("$TOOLCHAIN_ASSETS/${toolchainManifest.getString("ndk")}").reader().use { it.readText() }))
@@ -336,21 +343,42 @@ object GameBuild {
stamp.writeText(identity) stamp.writeText(identity)
} }
val blob = File(generated, "data_sections_init_blobs.S") // Blob assembly generated on Windows is rewritten for ELF, as PublicProducts.cmake does.
val elfBlob = File(work, "data_sections_init_blobs_android.S") fun elfAssembly(source: File): File {
val blobText = blob.readText() val rewritten = File(work, source.nameWithoutExtension + "_android.S")
val elfText = BuildRecipe.elfBlobAssembly(blobText) val text = BuildRecipe.elfBlobAssembly(source.readText())
if (!elfBlob.isFile || elfBlob.readText() != elfText) elfBlob.writeText(elfText) if (!rewritten.isFile || rewritten.readText() != text) rewritten.writeText(text)
return rewritten
}
// Which sources fill each link slot is the recipe's to say, so this builds either flavour.
data class Job(val kind: String, val source: File, val slot: String) data class Job(val kind: String, val source: File, val slot: String)
val jobs = ArrayList<Job>() val jobs = ArrayList<Job>()
jobs += Job("runtime", File(generated, "data_sections_init.cpp"), "runtime") val sources = recipe.getJSONObject("sources")
jobs += Job("runtime", File(generated, "guest_symbol_table.cpp"), "runtime") for (slot in sources.keys()) {
jobs += Job("asm", elfBlob, "runtime") val entries = sources.getJSONArray(slot)
BuildRecipe.sourceList(shards, BuildRecipe.REGISTRATION_LIST).forEach { jobs += Job("product", File(it), "product") } val slotSources = ArrayList<File>()
val translated = BuildRecipe.TRANSLATED_LISTS.flatMap { BuildRecipe.sourceList(shards, it) } for (i in 0 until entries.length()) {
if (translated.isEmpty()) throw IOException("The translation produced no game code.") val entry = entries.getString(i)
translated.forEach { jobs += Job("translated", File(it), "translated") } when {
entry.startsWith("@") -> BuildRecipe.sourceList(shards, entry.substring(1)).forEach { slotSources += File(it) }
else -> slotSources += File(BuildRecipe.expand(entry, mapOf("workspace" to workspace.absolutePath)))
}
}
if (slotSources.isEmpty()) {
throw IOException("The translation has no $slot sources for a ${recipe.getString("product")} game.")
}
// Assembly is assembled; a mod shard compiles with the same flags as a translated one.
slotSources.forEach { source ->
val assembly = source.name.endsWith(".S")
val kind = when {
assembly -> "asm"
slot == "mod" -> "translated"
else -> slot
}
jobs += Job(kind, if (assembly) elfAssembly(source) else source, slot)
}
}
if (jobs.map { BuildRecipe.objectName(it.source.path) }.toSet().size != jobs.size) { if (jobs.map { BuildRecipe.objectName(it.source.path) }.toSet().size != jobs.size) {
throw IOException("Two generated sources share an object name.") throw IOException("Two generated sources share an object name.")
} }
@@ -361,7 +389,7 @@ object GameBuild {
"workspace" to workspace.absolutePath, "workspace" to workspace.absolutePath,
) )
val compileFlags = recipe.getJSONObject("compile") val compileFlags = recipe.getJSONObject("compile")
for (kind in listOf("runtime", "asm", "product", "translated")) { for (kind in jobs.map { it.kind }.distinct()) {
val flags = compileFlags.getJSONArray(kind).let { array -> (0 until array.length()).map { BuildRecipe.expand(array.getString(it), values) } } val flags = compileFlags.getJSONArray(kind).let { array -> (0 until array.length()).map { BuildRecipe.expand(array.getString(it), values) } }
File(work, "$kind.rsp").writeText(BuildRecipe.responseFile(flags)) File(work, "$kind.rsp").writeText(BuildRecipe.responseFile(flags))
} }
@@ -211,11 +211,12 @@ class LauncherActivity : Activity() {
mainAction = when { mainAction = when {
running -> Action.Resume running -> Action.Resume
discStatus != GameStorage.DiscStatus.Ready -> Action.SelectDisc discStatus != GameStorage.DiscStatus.Ready -> Action.SelectDisc
gameStatus != GameLibrary.Status.Ready -> Action.BuildGame gameStatus != GameLibrary.Status.Ready -> if (BuildConfig.ON_DEVICE_BUILD) Action.BuildGame else Action.ImportGame
else -> Action.Play else -> Action.Play
} }
secondaryAction = when { secondaryAction = when {
settingUp || running -> null settingUp || running -> null
mainAction == Action.ImportGame -> null
gameStatus != GameLibrary.Status.Ready || mainAction == Action.SelectDisc -> Action.ImportGame gameStatus != GameLibrary.Status.Ready || mainAction == Action.SelectDisc -> Action.ImportGame
else -> null else -> null
} }
@@ -279,10 +280,14 @@ class LauncherActivity : Activity() {
setup is GameSetup.State.Failed && building -> showBanner(getString(R.string.home_build_failed, setup.message), warning = true) setup is GameSetup.State.Failed && building -> showBanner(getString(R.string.home_build_failed, setup.message), warning = true)
setup is GameSetup.State.Failed -> showBanner(getString(R.string.home_setup_failed, setup.message), warning = true) setup is GameSetup.State.Failed -> showBanner(getString(R.string.home_setup_failed, setup.message), warning = true)
discStatus == GameStorage.DiscStatus.Incomplete -> showBanner(getString(R.string.home_data_incomplete, disc), warning = true) discStatus == GameStorage.DiscStatus.Incomplete -> showBanner(getString(R.string.home_data_incomplete, disc), warning = true)
gameStatus == GameLibrary.Status.Stale -> showBanner(getString(R.string.home_game_stale), warning = true) !GameStorage.modContentReady(this) ->
showBanner(getString(R.string.home_mod_missing, GameStorage.modDirectory(this).absolutePath), warning = true)
gameStatus == GameLibrary.Status.Stale ->
showBanner(getString(if (BuildConfig.ON_DEVICE_BUILD) R.string.home_game_stale else R.string.home_game_stale_pc), warning = true)
gameStatus == GameLibrary.Status.Missing && discStatus == GameStorage.DiscStatus.Missing -> gameStatus == GameLibrary.Status.Missing && discStatus == GameStorage.DiscStatus.Missing ->
showBanner(getString(R.string.home_setup_intro), warning = false) showBanner(getString(if (BuildConfig.ON_DEVICE_BUILD) R.string.home_setup_intro else R.string.home_setup_intro_pc), warning = false)
gameStatus == GameLibrary.Status.Missing -> showBanner(getString(R.string.home_game_missing), warning = false) gameStatus == GameLibrary.Status.Missing ->
showBanner(getString(if (BuildConfig.ON_DEVICE_BUILD) R.string.home_game_missing else R.string.home_game_missing_pc), warning = false)
discStatus == GameStorage.DiscStatus.Missing -> showBanner(getString(R.string.home_data_missing, disc), warning = false) discStatus == GameStorage.DiscStatus.Missing -> showBanner(getString(R.string.home_data_missing, disc), warning = false)
else -> showBanner(null) else -> showBanner(null)
} }
@@ -322,7 +327,7 @@ class LauncherActivity : Activity() {
/** Builds the game on this headset from DATA, after saying what that takes. */ /** Builds the game on this headset from DATA, after saying what that takes. */
private fun buildGame() { private fun buildGame() {
if (GameSetup.isRunning || GameStorage.discStatus(this) != GameStorage.DiscStatus.Ready) return if (!BuildConfig.ON_DEVICE_BUILD || GameSetup.isRunning || GameStorage.discStatus(this) != GameStorage.DiscStatus.Ready) return
val message = if (GameLibrary.status(this) == GameLibrary.Status.Ready) R.string.home_build_replace_message else R.string.home_build_message val message = if (GameLibrary.status(this) == GameLibrary.Status.Ready) R.string.home_build_replace_message else R.string.home_build_message
confirm(R.string.home_build_title, message, R.string.home_build) { confirm(R.string.home_build_title, message, R.string.home_build) {
GameSetupService.startBuild(this) GameSetupService.startBuild(this)
@@ -282,12 +282,14 @@ class SettingsPage(
GameLibrary.Status.Missing -> activity.getString(R.string.about_game_missing) GameLibrary.Status.Missing -> activity.getString(R.string.about_game_missing)
} }
info(R.string.about_game, gameText, stacked = true) info(R.string.about_game, gameText, stacked = true)
if (BuildConfig.ON_DEVICE_BUILD) {
action( action(
R.string.home_build, R.string.about_build_helper, R.string.home_build, R.string.home_build, R.string.about_build_helper, R.string.home_build,
enabled = !GameSetup.isRunning && GameStorage.discStatus(activity) == GameStorage.DiscStatus.Ready, enabled = !GameSetup.isRunning && GameStorage.discStatus(activity) == GameStorage.DiscStatus.Ready,
) { ) {
buildGame() buildGame()
} }
}
action(R.string.home_import, R.string.about_import_helper, R.string.home_import, enabled = !GameSetup.isRunning) { action(R.string.home_import, R.string.about_import_helper, R.string.home_import, enabled = !GameSetup.isRunning) {
importGame() importGame()
} }
@@ -24,6 +24,9 @@
<string name="home_setup_intro">Nothing from Mario Kart Wii comes with this app: the game is built from your own PAL (RMCP01) disc.\n\nOn this headset: copy your disc image (ISO, WBFS, RVZ…) here, for example into Download, press Select disc image, then Build on this Quest.\n\nWith a computer: in the WiiCompiled VR launcher, use Build for Quest with your disc image, copy the .wcgame file it makes to this headset, then press Import from computer.</string> <string name="home_setup_intro">Nothing from Mario Kart Wii comes with this app: the game is built from your own PAL (RMCP01) disc.\n\nOn this headset: copy your disc image (ISO, WBFS, RVZ…) here, for example into Download, press Select disc image, then Build on this Quest.\n\nWith a computer: in the WiiCompiled VR launcher, use Build for Quest with your disc image, copy the .wcgame file it makes to this headset, then press Import from computer.</string>
<string name="home_game_missing">The game files are ready. Now build the game from them: Build on this Quest takes about 30–40 minutes on a Quest 3. With a computer, use Build for Quest in the WiiCompiled VR launcher instead, copy the .wcgame file to this headset and press Import from computer.</string> <string name="home_game_missing">The game files are ready. Now build the game from them: Build on this Quest takes about 30–40 minutes on a Quest 3. With a computer, use Build for Quest in the WiiCompiled VR launcher instead, copy the .wcgame file to this headset and press Import from computer.</string>
<string name="home_game_stale">This app was updated, so the game must be built again for it: press Build on this Quest, or use Build for Quest in the WiiCompiled VR launcher again and Import from computer.</string> <string name="home_game_stale">This app was updated, so the game must be built again for it: press Build on this Quest, or use Build for Quest in the WiiCompiled VR launcher again and Import from computer.</string>
<string name="home_setup_intro_pc">Nothing from Mario Kart Wii or Retro Rewind comes with this app: the game is built from your own PAL (RMCP01) disc.\n\nCopy your disc image (ISO, WBFS, RVZ…) here, for example into Download, and press Select disc image. Then, on a computer, use Build for Quest in the WiiCompiled VR launcher with this app’s APK, copy the .wcgame file it makes to this headset and press Import from computer.</string>
<string name="home_game_missing_pc">The game files are ready. The game itself is built on a computer: in the WiiCompiled VR launcher, use Build for Quest with this app’s APK, copy the .wcgame file to this headset, then press Import from computer.</string>
<string name="home_game_stale_pc">This app was updated, so the game must be built again for it: use Build for Quest in the WiiCompiled VR launcher again, then Import from computer.</string>
<string name="home_build">Build on this Quest</string> <string name="home_build">Build on this Quest</string>
<string name="home_build_preparing">Preparing…</string> <string name="home_build_preparing">Preparing…</string>
<string name="home_building">Building %1$d%%</string> <string name="home_building">Building %1$d%%</string>
@@ -46,6 +49,7 @@
<string name="home_replace_game_title">Replace the game?</string> <string name="home_replace_game_title">Replace the game?</string>
<string name="home_replace_game_message">The game installed now stays until the new game file has been checked and completely imported.</string> <string name="home_replace_game_message">The game installed now stays until the new game file has been checked and completely imported.</string>
<string name="home_data_incomplete">DATA is there but incomplete or unreadable. Select your disc image to extract it again, or copy the whole extracted partition (sys/ and files/, not just files/) to:\n%1$s</string> <string name="home_data_incomplete">DATA is there but incomplete or unreadable. Select your disc image to extract it again, or copy the whole extracted partition (sys/ and files/, not just files/) to:\n%1$s</string>
<string name="home_mod_missing">The Retro Rewind pack is missing, so the game cannot start. Copy the RetroRewind6 folder (the one with Binaries, Tracks and Assets in it) to:\n%1$s</string>
<string name="home_checking">Checking disc…</string> <string name="home_checking">Checking disc…</string>
<string name="home_extracting">Extracting %1$d%%</string> <string name="home_extracting">Extracting %1$d%%</string>
<string name="home_finishing">Finishing…</string> <string name="home_finishing">Finishing…</string>
@@ -22,6 +22,17 @@ class BuildRecipeTest {
assertThrows(IllegalArgumentException::class.java) { BuildRecipe.expand("-I{workspace}", values) } assertThrows(IllegalArgumentException::class.java) { BuildRecipe.expand("-I{workspace}", values) }
} }
@Test
fun expandsAnyLinkSlotTheRecipeNames() {
// Retro Rewind adds a mod slot; a builder only has to expand what the recipe lists.
val arguments = BuildRecipe.linkArguments(
listOf("{game:product}", "{game:mod}", "--start-lib", "{game:translated}", "--end-lib"),
emptyMap(),
mapOf("product" to listOf("/o/reg.o"), "mod" to listOf("/o/m1.o", "/o/m2.o"), "translated" to listOf("/o/base.o")),
)
assertEquals(listOf("/o/reg.o", "/o/m1.o", "/o/m2.o", "--start-lib", "/o/base.o", "--end-lib"), arguments)
}
@Test @Test
fun expandsTheLinkSlotsInPlace() { fun expandsTheLinkSlotsInPlace() {
val template = listOf("-o", "{output}", "{kit}/objects/001_a.o", "{game:runtime}", "--start-lib", "{game:translated}", "--end-lib", "-lm") val template = listOf("-o", "{output}", "{kit}/objects/001_a.o", "{game:runtime}", "--start-lib", "{game:translated}", "--end-lib", "-lm")
@@ -59,7 +70,10 @@ class BuildRecipeTest {
) )
assertEquals(emptyList<String>(), BuildRecipe.sourceList(shards, "MKW_BASE_PORTABLE_SENSITIVE_SHARDS")) assertEquals(emptyList<String>(), BuildRecipe.sourceList(shards, "MKW_BASE_PORTABLE_SENSITIVE_SHARDS"))
assertEquals(emptyList<String>(), BuildRecipe.sourceList(shards, "MKW_RETRO_MOD_SHARDS")) assertEquals(emptyList<String>(), BuildRecipe.sourceList(shards, "MKW_RETRO_MOD_SHARDS"))
assertEquals(listOf("C:/w/generated/build_shards/base_registration/registration_00.cpp"), BuildRecipe.sourceList(shards, BuildRecipe.REGISTRATION_LIST)) assertEquals(
listOf("C:/w/generated/build_shards/base_registration/registration_00.cpp"),
BuildRecipe.sourceList(shards, "MKW_BASE_REGISTRATION_SOURCES"),
)
} }
@Test @Test
+33 -8
View File
@@ -234,11 +234,23 @@ The kit is exported from CMake's own build graph, so its flags cannot drift from
this way had the same 61,975 defined and 785 undefined dynamic symbols, 29,995 translated this way had the same 61,975 defined and 785 undefined dynamic symbols, 29,995 translated
functions, `NEEDED` list and soname as the CMake-built one, in 2.4 minutes. functions, `NEEDED` list and soname as the CMake-built one, in 2.4 minutes.
`Build-Quest.ps1` refuses a base APK that contains any `libmain*.so` or lacks the kit, and the `Build-Quest.ps1` refuses an APK that contains any `libmain*.so` or lacks the kit, and every
base variant excludes `**/libmain*.so` from packaging, since AGP packages every library left in variant excludes `**/libmain*.so` and the probes from packaging, since AGP packages every library
the CMake output directory. The `func_8…` symbols the kit's runtime objects define are left in the CMake output directory. The `func_8…` symbols the kit's runtime objects define are
hand-written HLE overrides (`PPC_NATIVE_OVERRIDE_*` in `hle_stubs.h`), not translated code. hand-written HLE overrides (`PPC_NATIVE_OVERRIDE_*` in `hle_stubs.h`), not translated code.
The Retro Rewind flavour has not moved to the kit yet and still bundles its library.
**Retro Rewind has its own kit** (`mkw_quest_kit_probe_retro`, RetroRewind without any translated
code), so that APK ships no game either. It needs one more link slot than the base flavour, because
the modded product links more kinds of translated code: `{game:runtime}` (the disc-generated
sources), `{game:product}` (the mod's registration and dispatch shards), `{game:mod}` (the mod's own
shards, the profile-sensitive base shards it replaces, and the mod's data patches) and
`{game:translated}` (the shared base shards, with the archive semantics `libmkw_base_shared.a` has).
Rather than teach each builder which flavour it is building, `kit.json` now carries a `sources`
map naming the `shards.cmake` list behind each slot, and the builders just follow it (kit schema 2).
Its game is built on a PC for now: building it on the headset would also need the mod's `Code.pul`
and its 2 GB pack there, so only the base APK carries the on-device toolchain
(`BuildConfig.ON_DEVICE_BUILD`). The Retro Rewind app writes `[paths] retro_rewind_root` into its
first `Config.toml` and Home says so when that pack is missing.
### Game packages (.wcgame) and Import from computer ### Game packages (.wcgame) and Import from computer
@@ -365,7 +377,8 @@ Android facts this design rests on, all measured on a Quest 3:
another host. another host.
- `android/`: the Gradle project. `app/src/main/cpp/CMakeLists.txt` adds the - `android/`: the Gradle project. `app/src/main/cpp/CMakeLists.txt` adds the
repository's `runtime/` as a subdirectory with those Android choices; repository's `runtime/` as a subdirectory with those Android choices;
flavours `base` and `retroRewind` pick the product target and library name. flavours `base` and `retroRewind` each build their own game kit probe, and only `base` carries
the on-headset toolchain.
- `android/nod-jni`: Gradle's `buildNodJni` task runs `cargo build --release - `android/nod-jni`: Gradle's `buildNodJni` task runs `cargo build --release
--locked --target aarch64-linux-android` with the NDK's clang as linker and C --locked --target aarch64-linux-android` with the NDK's clang as linker and C
compiler. `stageNodJni` puts `libnod_jni.so` into the APK's `arm64-v8a` compiler. `stageNodJni` puts `libnod_jni.so` into the APK's `arm64-v8a`
@@ -384,7 +397,8 @@ installer's `BuildWorkspace/generated`, produced by the normal Windows pipeline)
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 powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Install # the app, its game kit and toolchain, debug-signed
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-Quest.ps1 -Flavor retroRewind # Retro Rewind (needs translate-mod output) powershell -ExecutionPolicy Bypass -File android/Build-Quest.ps1 -Flavor retroRewind # Retro Rewind app and its kit (needs translate-mod output)
powershell -ExecutionPolicy Bypass -File android/Build-QuestGame.ps1 -Flavor retroRewind -Data <dir> -Install # its game, with the disc files
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
``` ```
@@ -446,6 +460,14 @@ What has been verified on the development machine (September 2026):
byte-identical to one the same toolchain built from a shell, and against the PC-built library it byte-identical to one the same toolchain built from a shell, and against the PC-built library it
has the same soname, `NEEDED` list, 785 undefined symbols and 29,995 translated functions has the same soname, `NEEDED` list, 785 undefined symbols and 29,995 translated functions
(61,974 defined against 61,975: the PC's older clang keeps one inline helper out of line). (61,974 defined against 61,975: the PC's older clang keeps one inline helper out of line).
- **Device, 2026-09-17: Retro Rewind from its own kit.** The Retro Rewind APK carries a kit and no
game (66 MB). Its game built from that kit on the PC in 2.9 minutes (166 generated sources:
72 base shards, 24 profile-sensitive, 48 mod shards, the mod's data patches, 17 registration
shards and the 3 disc-generated sources), linked to a 157.6 MB `libmain.so` with the same import
list as the base one, 67,872 defined symbols against the base library's 61,975, and 5,905 symbols
the base library does not have. Packaged with the disc files (2.55 GB), imported on the Quest 3 in
under two minutes, and the mod runs: its own title screen, "Press the A Button", and the licence
menu, with the pack read from `retro_rewind_root`.
Bring-up fixes that only a device could reveal: Bring-up fixes that only a device could reveal:
@@ -556,7 +578,10 @@ or `EndAccess` errors); a black mirror too points at Aurora itself.
- **Input.** D-pad (trick inputs) is not bound; remap in `Config.toml` or bind - **Input.** D-pad (trick inputs) is not bound; remap in `Config.toml` or bind
the thumbstick directions in a follow-up. Haptics are wired but nothing calls the thumbstick directions in a follow-up. Haptics are wired but nothing calls
them yet. them yet.
- **Retro Rewind on device** needs the mod's translation and its extracted - **Retro Rewind on the headset** runs from a kit-built library (below), but its game must be built
content pushed next to `DATA`, exactly like the desktop product. on a PC and its pack copied next to `DATA` by hand. `adb push` cannot create directories inside
an app's external files directory (`secure_mkdirs failed`), so push the pack to `Download` and
copy it over on the device, then `chmod -R a+rX` it. The launcher does not fetch or update the
pack, and cannot build the mod on the headset.
- **Release signing and store packaging** are out of scope; `Build-Quest.ps1` - **Release signing and store packaging** are out of scope; `Build-Quest.ps1`
produces debug-signed APKs for sideloading. produces debug-signed APKs for sideloading.
+9 -1
View File
@@ -415,6 +415,14 @@ if(MKW_HAVE_RETRO_REWIND)
if(MKW_RETRO_BLOB_OBJECTS) if(MKW_RETRO_BLOB_OBJECTS)
target_sources(RetroRewind PRIVATE ${MKW_RETRO_BLOB_OBJECTS}) target_sources(RetroRewind PRIVATE ${MKW_RETRO_BLOB_OBJECTS})
endif() endif()
if(MKW_PLATFORM_ANDROID)
# Retro Rewind's game kit, the same idea as the base probe above: RetroRewind without any
# translated code, so the APK carries neither the game nor the mod.
add_library(mkw_quest_kit_probe_retro SHARED "${MKW_RETRO_REWIND_PRODUCT_SOURCE}")
mkw_configure_product(mkw_quest_kit_probe_retro WITHOUT_GAME)
target_precompile_headers(mkw_quest_kit_probe_retro REUSE_FROM WiiCompiled)
target_link_options(mkw_quest_kit_probe_retro PRIVATE "-Wl,--unresolved-symbols=ignore-all")
endif()
add_custom_target(mkw_release DEPENDS WiiCompiled RetroRewind) add_custom_target(mkw_release DEPENDS WiiCompiled RetroRewind)
else() else()
add_custom_target(mkw_release DEPENDS WiiCompiled) add_custom_target(mkw_release DEPENDS WiiCompiled)
@@ -440,7 +448,7 @@ endif()
set(MKW_ALL_BUILD_TARGETS set(MKW_ALL_BUILD_TARGETS
mkw_runtime_common mkw_base_shared mkw_base_sensitive mkw_retro_sensitive mkw_runtime_common mkw_base_shared mkw_base_sensitive mkw_retro_sensitive
mkw_retro_rewind_functions WiiCompiled RetroRewind mkw_quest_kit_probe) mkw_retro_rewind_functions WiiCompiled RetroRewind mkw_quest_kit_probe mkw_quest_kit_probe_retro)
foreach(target IN LISTS MKW_ALL_BUILD_TARGETS) foreach(target IN LISTS MKW_ALL_BUILD_TARGETS)
if(TARGET ${target} AND MKW_BASELINE_ARCH_FLAG) if(TARGET ${target} AND MKW_BASELINE_ARCH_FLAG)
target_compile_options(${target} PRIVATE ${MKW_BASELINE_ARCH_FLAG}) target_compile_options(${target} PRIVATE ${MKW_BASELINE_ARCH_FLAG})