diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index bf83009..fe8dc86 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -14,6 +14,10 @@ concurrency:
cancel-in-progress: true
jobs:
+ recompilation:
+ name: Recompilation test
+ uses: ./.github/workflows/recomp-test.yml
+
translator:
name: Translator (build + test)
runs-on: windows-latest
@@ -49,7 +53,7 @@ jobs:
test "$(uname -m)" = arm64
cmake -S runtime -B build-macos -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DMKW_BUILD_PRODUCTS=OFF
- grep -qx 'CMAKE_OSX_DEPLOYMENT_TARGET:STRING=14.0' \
+ grep -qx 'CMAKE_OSX_DEPLOYMENT_TARGET:STRING=12.0' \
build-macos/CMakeCache.txt
- name: Build macOS portability targets
@@ -57,6 +61,10 @@ jobs:
run: |
cmake --build build-macos --target \
mkw_platform_paths_tests \
+ mkw_nand_save_tests \
+ mkw_nand_settings_tests \
+ mkw_sc_serial_tests \
+ mkw_input_expr_tests \
mkw_macos_native_compile \
mkw_macos_context_abi_tests \
mkw_macos_host_context_tests \
diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml
index e65abd6..63f40b5 100644
--- a/.github/workflows/package.yml
+++ b/.github/workflows/package.yml
@@ -25,6 +25,10 @@ concurrency:
cancel-in-progress: true
jobs:
+ recompilation:
+ name: Recompilation test
+ uses: ./.github/workflows/recomp-test.yml
+
linux-appimage:
name: Linux (AppImage, ${{ matrix.arch }})
strategy:
@@ -92,7 +96,7 @@ jobs:
archive: false
macos-setup-package:
- name: macOS (Setup.pkg, Apple Silicon)
+ name: macOS (universal Setup.pkg)
runs-on: macos-14
steps:
- uses: actions/checkout@v7
@@ -103,35 +107,52 @@ jobs:
with:
dotnet-version: '8.0.x'
- - name: Verify Apple Silicon runner tools
+ - name: Verify Apple Silicon runner
shell: bash
run: |
test "$(uname -m)" = arm64
xcode-select -p
- command -v ninja
- file "$(command -v ninja)" | grep -q arm64
- - name: Download pinned nodtool release
+ - name: Download pinned Nod tools
shell: bash
run: |
mkdir -p Launcher/artifacts/macos
nodtool_version=v2.0.0-alpha.10
- nodtool_asset=nodtool-macos-arm64
- nodtool_sha256=e23ca466999b720c55e6d29c9683fce8cc74451ba64ead2e543d50129f24528a
+ nodtool_arm64_asset=nodtool-macos-arm64
+ nodtool_arm64_sha256=e23ca466999b720c55e6d29c9683fce8cc74451ba64ead2e543d50129f24528a
+ nodtool_x86_64_asset=nodtool-macos-x86_64
+ nodtool_x86_64_sha256=f68f504dc2b72694b468ca78b6a24142c7aa5c8800f77564297f4143682e6575
curl -fsSL --retry 3 \
- "https://github.com/encounter/nod/releases/download/${nodtool_version}/${nodtool_asset}" \
- -o Launcher/artifacts/macos/nodtool
- printf '%s %s\n' "$nodtool_sha256" Launcher/artifacts/macos/nodtool | shasum -a 256 -c -
- chmod +x Launcher/artifacts/macos/nodtool
- Launcher/artifacts/macos/nodtool --version
+ "https://github.com/encounter/nod/releases/download/${nodtool_version}/${nodtool_arm64_asset}" \
+ -o Launcher/artifacts/macos/nodtool-arm64
+ curl -fsSL --retry 3 \
+ "https://github.com/encounter/nod/releases/download/${nodtool_version}/${nodtool_x86_64_asset}" \
+ -o Launcher/artifacts/macos/nodtool-x86_64
+ printf '%s %s\n' "$nodtool_arm64_sha256" Launcher/artifacts/macos/nodtool-arm64 | shasum -a 256 -c -
+ printf '%s %s\n' "$nodtool_x86_64_sha256" Launcher/artifacts/macos/nodtool-x86_64 | shasum -a 256 -c -
+ chmod +x Launcher/artifacts/macos/nodtool-arm64 Launcher/artifacts/macos/nodtool-x86_64
- - name: Publish self-contained translator
+ - name: Publish self-contained Translator tools
shell: bash
run: |
dotnet publish translator/src/Translator.Cli/Translator.Cli.csproj \
-c Release -r osx-arm64 --self-contained true \
-p:PublishSingleFile=true \
- -o Launcher/artifacts/macos/translator
+ -o Launcher/artifacts/macos/translator-arm64
+ dotnet publish translator/src/Translator.Cli/Translator.Cli.csproj \
+ -c Release -r osx-x64 --self-contained true \
+ -p:PublishSingleFile=true \
+ -o Launcher/artifacts/macos/translator-x86_64
+
+ - name: Download pinned universal Ninja
+ shell: bash
+ run: |
+ ninja_version=1.13.2
+ ninja_sha256=c99048673aa765960a99cf10c6ddb9f1fad506099ff0a0e137ad8960a88f321b
+ curl -fsSL --retry 3 "https://github.com/ninja-build/ninja/releases/download/v${ninja_version}/ninja-mac.zip" -o ninja-mac.zip
+ printf '%s %s\n' "$ninja_sha256" ninja-mac.zip | shasum -a 256 -c -
+ unzip -q ninja-mac.zip -d Launcher/artifacts/macos/ninja
+ chmod +x Launcher/artifacts/macos/ninja/ninja
- name: Download pinned portable CMake
shell: bash
@@ -155,23 +176,37 @@ jobs:
if [[ -z "$package_version" ]]; then package_version="${TAG_VERSION#v}"; fi
mkdir -p Launcher/dist
Launcher/macos/build-setup-pkg.command \
- --nodtool Launcher/artifacts/macos/nodtool \
- --translator Launcher/artifacts/macos/translator/Translator.Cli \
+ --nodtool-arm64 Launcher/artifacts/macos/nodtool-arm64 \
+ --nodtool-x86_64 Launcher/artifacts/macos/nodtool-x86_64 \
+ --translator-arm64 Launcher/artifacts/macos/translator-arm64/Translator.Cli \
+ --translator-x86_64 Launcher/artifacts/macos/translator-x86_64/Translator.Cli \
--cmake-root Launcher/artifacts/macos/cmake \
- --ninja "$(command -v ninja)" \
+ --ninja-arm64 Launcher/artifacts/macos/ninja/ninja \
+ --ninja-x86_64 Launcher/artifacts/macos/ninja/ninja \
--output Launcher/dist/WiiCompiled-Setup.pkg \
--version "$package_version"
- - name: Verify package boundary
+ - name: Verify package layout and architecture-specific tools
shell: bash
run: |
pkgutil --check-signature Launcher/dist/WiiCompiled-Setup.pkg
! pkgutil --payload-files Launcher/dist/WiiCompiled-Setup.pkg | \
grep -E '/(Assets|generated|PulsarPacks|WiiCompiled.app|RetroRewind.app)(/|$)'
+ expanded="$RUNNER_TEMP/wiicompiled-setup-expanded"
+ pkgutil --expand-full Launcher/dist/WiiCompiled-Setup.pkg "$expanded"
+ resources="$expanded/Payload/Applications/WiiCompiled Setup.app/Contents/Resources"
+ for arch in arm64 x86_64; do
+ for tool in nodtool Translator.Cli ninja; do
+ lipo "$resources/tools/$arch/$tool" -verify_arch "$arch"
+ done
+ done
+ lipo "$resources/tools/cmake/bin/cmake" -verify_arch arm64 x86_64
+ bash "$resources/setup.command" --help
+ /usr/bin/arch -x86_64 /bin/bash "$resources/setup.command" --help
- uses: actions/upload-artifact@v7
with:
- name: WiiCompiled-Setup-macos-arm64
+ name: WiiCompiled-Setup-macos-universal
path: Launcher/dist/WiiCompiled-Setup.pkg
if-no-files-found: error
archive: false
@@ -183,7 +218,7 @@ jobs:
release:
name: Publish GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
- needs: [linux-appimage, windows-installer, macos-setup-package]
+ needs: [linux-appimage, windows-installer, macos-setup-package, recompilation]
runs-on: ubuntu-latest
permissions:
contents: write
diff --git a/.github/workflows/recomp-test.yml b/.github/workflows/recomp-test.yml
new file mode 100644
index 0000000..0908d15
--- /dev/null
+++ b/.github/workflows/recomp-test.yml
@@ -0,0 +1,41 @@
+name: Synthetic recompilation
+
+on:
+ workflow_call:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ windows:
+ name: Windows runtime (synthetic DOL)
+ runs-on: windows-latest
+ timeout-minutes: 60
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ persist-credentials: false
+
+ - uses: actions/setup-dotnet@v6
+ with:
+ dotnet-version: '8.0.x'
+
+ # Cache downloads only. Preparation still validates pins, and every run
+ # compiles current Aurora, runtime, and generated sources from scratch.
+ - uses: actions/cache@v5
+ with:
+ path: Launcher/artifacts/downloads
+ key: windows-recomp-downloads-${{ hashFiles('Launcher/Prepare-PortableTools.ps1', 'Launcher/Prepare-Dependencies.ps1') }}
+
+ - name: Prepare the shipped Windows toolchain
+ shell: pwsh
+ run: ./Launcher/Prepare-PortableTools.ps1
+
+ - name: Prepare pinned native dependencies
+ shell: pwsh
+ run: ./Launcher/Prepare-Dependencies.ps1
+
+ - name: Translate, compile the full runtime, and link
+ shell: pwsh
+ run: ./Launcher/Test-Recompilation.ps1 -Parallel 3
diff --git a/.gitignore b/.gitignore
index 855cfed..59542aa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,8 @@ Code.pul
/build/
/build-*/
/native-build/
+/native-build-macos/
+/local-products/
/dist/
/out/
[Bb]in/
diff --git a/Launcher/Build-Installer.ps1 b/Launcher/Build-Installer.ps1
index 27058cb..eb70930 100644
--- a/Launcher/Build-Installer.ps1
+++ b/Launcher/Build-Installer.ps1
@@ -281,7 +281,7 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
$manifest = [ordered]@{
SchemaVersion = 2
- ProductVersion = '0.2.27'
+ ProductVersion = '0.2.31'
ExpectedGameId = $pins.GameId
ExpectedDolSha256 = $pins.DolSha256
ExpectedRelSha256 = $pins.RelSha256
diff --git a/Launcher/Prepare-NativePrebuilt.sh b/Launcher/Prepare-NativePrebuilt.sh
index df2e100..3d4e6ed 100755
--- a/Launcher/Prepare-NativePrebuilt.sh
+++ b/Launcher/Prepare-NativePrebuilt.sh
@@ -88,7 +88,7 @@ assert_file "$ninja_bin" "Portable Ninja"
assert_file "$cc" "Portable C compiler"
assert_file "$cxx" "Portable C++ compiler"
assert_dir "$aurora_source" "aurora-main source tree"
-clang_binary=$(normalize "$toolchain_dir/bin/clang-23")
+clang_binary=$(normalize "$toolchain_dir/bin/clang-22")
assert_file "$clang_binary" "Portable clang driver binary"
(( parallel > 0 )) || parallel=$(nproc)
diff --git a/Launcher/Test-Recompilation.ps1 b/Launcher/Test-Recompilation.ps1
new file mode 100644
index 0000000..9a8d8a2
--- /dev/null
+++ b/Launcher/Test-Recompilation.ps1
@@ -0,0 +1,146 @@
+# Build the real Windows runtime with translated, entirely synthetic PowerPC code.
+# No game dump, game symbol map, REL, mod download, or existing generated/ output is used.
+[CmdletBinding()]
+param(
+ [string]$PortableToolsDirectory = 'Launcher/artifacts/portable-tools',
+ [string]$DependencySourceDirectory = 'Launcher/artifacts/dependencies',
+ [string]$StageDirectory = 'build/recomp-test',
+ [ValidateRange(1, 64)] [int]$Parallel = 3
+)
+
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version 3.0
+. (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1')
+
+$repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
+function Full([string]$Path) {
+ if ([IO.Path]::IsPathRooted($Path)) { return [IO.Path]::GetFullPath($Path) }
+ return [IO.Path]::GetFullPath((Join-Path $repoRoot $Path))
+}
+$portableTools = Full $PortableToolsDirectory
+$dependencies = Full $DependencySourceDirectory
+$stage = Full $StageDirectory
+$dotnet = (Get-Command dotnet -CommandType Application).Source
+$cmake = Join-Path $portableTools 'CMake/bin/cmake.exe'
+$ninja = Join-Path $portableTools 'Ninja/ninja.exe'
+$compilerBin = Join-Path $portableTools 'llvm-mingw/bin'
+Assert-File $cmake 'Pinned CMake (run Prepare-PortableTools.ps1 first)'
+Assert-File $ninja 'Pinned Ninja'
+Assert-File (Join-Path $dependencies 'cppwinrt/winrt/base.h') 'Pinned dependencies (run Prepare-Dependencies.ps1 first)'
+
+# Refuse reuse so a developer's game translation or an earlier build cannot make
+# the test pass. Keep the staging tree after the run for diagnostics.
+if (Test-Path -LiteralPath $stage) { throw "Test stage already exists; choose a fresh -StageDirectory: $stage" }
+[IO.Directory]::CreateDirectory($stage) | Out-Null
+Write-Host "Synthetic recompilation workspace: $stage"
+
+# Copy current sources, including uncommitted edits, but no ignored build output.
+# An isolated workspace preserves the developer's real generated/ directory.
+$sourceFiles = & git -C $repoRoot -c core.quotepath=false ls-files --cached --others --exclude-standard -- runtime aurora-main
+if ($LASTEXITCODE -ne 0) { throw 'Could not enumerate runtime and Aurora sources.' }
+foreach ($relative in $sourceFiles | Sort-Object -Unique) {
+ $destination = Join-Path $stage $relative
+ [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination)) | Out-Null
+ Copy-Item -LiteralPath (Join-Path $repoRoot $relative) -Destination $destination
+}
+
+# One synthetic text section: li r3,40; addi r3,r3,2; nop; blr.
+# Native HLE wrappers also call these eight guest symbols directly. Give each
+# its own generated blr function so the real product can link without game code.
+# Keep this list explicit: a new unresolved guest dependency must fail the test.
+[uint32]$entry = 0x80001000L
+[uint32[]]$guestCallbacks = @(
+ 0x8012B830L, 0x801A0620L, 0x801A1ED8L, 0x801A961CL,
+ 0x801AADE0L, 0x801D8D30L, 0x801D9E94L, 0x8055531CL
+)
+$textSize = [int]($guestCallbacks[-1] - $entry + 4)
+$dataOffset = 0x100 + $textSize
+$dol = [byte[]]::new($dataOffset + 4)
+function Write-BigEndian32([int]$Offset, [uint32]$Value) {
+ $dol[$Offset] = [byte](($Value -shr 24) -band 255)
+ $dol[$Offset + 1] = [byte](($Value -shr 16) -band 255)
+ $dol[$Offset + 2] = [byte](($Value -shr 8) -band 255)
+ $dol[$Offset + 3] = [byte]($Value -band 255)
+}
+Write-BigEndian32 0x00 0x100 # text[0] file offset
+Write-BigEndian32 0x48 $entry # text[0] guest address
+Write-BigEndian32 0x90 $textSize # text[0] length (unreachable gaps are zero)
+Write-BigEndian32 0x1C $dataOffset # data[0] file offset
+Write-BigEndian32 0x64 0x80600000L # data[0] guest address
+Write-BigEndian32 0xAC 4 # data[0] length
+Write-BigEndian32 0xD8 0x80601000L # BSS address
+Write-BigEndian32 0xDC 32 # BSS length
+Write-BigEndian32 0xE0 $entry # entry point
+Write-BigEndian32 0x100 0x38600028 # li r3,40
+Write-BigEndian32 0x104 0x38630002 # addi r3,r3,2
+Write-BigEndian32 0x108 0x60000000 # nop
+Write-BigEndian32 0x10C 0x4E800020 # blr
+foreach ($address in $guestCallbacks) {
+ Write-BigEndian32 ([int](0x100 + $address - $entry)) 0x4E800020
+}
+Write-BigEndian32 $dataOffset 0x12345678
+[IO.File]::WriteAllBytes((Join-Path $stage 'synthetic.dol'), $dol)
+$entryPoints = (@($entry) + $guestCallbacks | ForEach-Object { '0x{0:X8}' -f $_ }) -join ', '
+$functionMap = (@($entry) + $guestCallbacks | ForEach-Object { '{0:X8} func_{0:X8}' -f $_ }) -join "`n"
+[IO.File]::WriteAllText((Join-Path $stage 'synthetic-functions.txt'), $functionMap)
+$manifest = Join-Path $stage 'recomp.yml'
+[IO.File]::WriteAllText($manifest, @"
+schema_version: 1
+workspace_root: .
+project:
+ id: ci-synthetic-dol
+ display_name: CI Synthetic DOL
+memory:
+ base: 0x80000000
+ size: 0x01800000
+ sda_base: 0x80600000
+ sda2_base: 0x80600000
+inputs:
+ dol:
+ path: synthetic.dol
+translation:
+ entry_points: [$entryPoints]
+ function_map:
+ path: synthetic-functions.txt
+ allow_unsupported_instructions: false
+runtime:
+ native_abi_directories: []
+ native_registration_root: runtime/src
+output:
+ root: generated
+"@)
+
+$translatorProject = Join-Path $repoRoot 'translator/src/Translator.Cli/Translator.Cli.csproj'
+Invoke-Checked $dotnet @('build', $translatorProject, '-c', 'Release', '--disable-build-servers') 'Building the translator'
+$translator = Join-Path $repoRoot 'translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll'
+$metadata = Join-Path $stage 'generated/base_translation_output.json'
+Invoke-Checked $dotnet @($translator, 'translate-recursive', '0x80001000', '--project', $manifest,
+ '--output-metadata', $metadata, '--threads', "$Parallel") `
+ 'Translating the synthetic DOL'
+# Function-map seeds can be skipped by discovery; do not accept a partial fixture.
+$translated = Get-Content -LiteralPath $metadata -Raw | ConvertFrom-Json
+foreach ($address in @($entry) + $guestCallbacks) {
+ if ($address -notin $translated.functions.entryPoint) {
+ throw ('Synthetic function 0x{0:X8} was not translated.' -f $address)
+ }
+}
+Invoke-Checked $dotnet @($translator, 'generate-data-init', '--project', $manifest) 'Generating synthetic data and runtime configuration'
+Invoke-Checked $dotnet @($translator, 'emit-build-shards', '--project', $manifest) 'Emitting the production build graph'
+
+$nativeBuild = Join-Path $stage 'native-build'
+$oldPath = $env:PATH
+try {
+ $env:PATH = Get-MkwToolchainPath $portableTools
+ $configure = Get-MkwNativeConfigureArguments -SourceDirectory (Join-Path $stage 'runtime') -BuildDirectory $nativeBuild `
+ -Ninja $ninja -CCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang.exe') `
+ -CxxCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang++.exe') `
+ -ResourceCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-windres.exe') `
+ -DependenciesDirectory $dependencies -AdditionalArguments @('-DMKW_BUILD_PRODUCTS=ON')
+ Invoke-Checked $cmake $configure 'Configuring the production Windows runtime'
+ Invoke-Checked $cmake @('--build', $nativeBuild, '--target', 'WiiCompiled', '--parallel', "$Parallel") `
+ 'Compiling and linking the synthetic product with the full runtime'
+ Assert-File (Join-Path $nativeBuild 'WiiCompiled.exe') 'Linked synthetic product'
+} finally {
+ $env:PATH = $oldPath
+}
+Write-Host 'Synthetic recompilation passed (translation, data generation, runtime compilation, and product link).'
diff --git a/Launcher/WiiCompiled.Setup.Common.Cli/WiiCompiled.Setup.Common.Cli.csproj b/Launcher/WiiCompiled.Setup.Common.Cli/WiiCompiled.Setup.Common.Cli.csproj
index b22d680..97e8ecb 100644
--- a/Launcher/WiiCompiled.Setup.Common.Cli/WiiCompiled.Setup.Common.Cli.csproj
+++ b/Launcher/WiiCompiled.Setup.Common.Cli/WiiCompiled.Setup.Common.Cli.csproj
@@ -6,7 +6,7 @@
enable
WiiCompiled.Setup.Common.Cli
WiiCompiled.Setup.Common.Cli
- 0.2.22
+ 0.2.31
patchzy
WiiCompiled
Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1
diff --git a/Launcher/WiiCompiled.Setup.Common/WiiCompiled.Setup.Common.csproj b/Launcher/WiiCompiled.Setup.Common/WiiCompiled.Setup.Common.csproj
index 91fbef7..aebdc1b 100644
--- a/Launcher/WiiCompiled.Setup.Common/WiiCompiled.Setup.Common.csproj
+++ b/Launcher/WiiCompiled.Setup.Common/WiiCompiled.Setup.Common.csproj
@@ -5,7 +5,7 @@
enable
WiiCompiled.Setup.Common
WiiCompiled.Setup.Common
- 0.2.22
+ 0.2.31
patchzy
WiiCompiled
Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers
diff --git a/Launcher/WiiCompiled.Setup.Linux/Models.cs b/Launcher/WiiCompiled.Setup.Linux/Models.cs
index 4c8a4b9..b8f8ea6 100644
--- a/Launcher/WiiCompiled.Setup.Linux/Models.cs
+++ b/Launcher/WiiCompiled.Setup.Linux/Models.cs
@@ -3,7 +3,7 @@ namespace WiiCompiled.Setup.Linux;
internal static class ProductInfo
{
public const string Name = "WiiCompiled";
- public const string Version = "0.2.27";
+ public const string Version = "0.2.31";
}
/// One installed product's record inside install-state.json.
diff --git a/Launcher/WiiCompiled.Setup.Linux/WiiCompiled.Setup.Linux.csproj b/Launcher/WiiCompiled.Setup.Linux/WiiCompiled.Setup.Linux.csproj
index 7424382..84d3269 100644
--- a/Launcher/WiiCompiled.Setup.Linux/WiiCompiled.Setup.Linux.csproj
+++ b/Launcher/WiiCompiled.Setup.Linux/WiiCompiled.Setup.Linux.csproj
@@ -6,7 +6,7 @@
enable
WiiCompiled.Setup.Linux
WiiCompiled.Setup.Linux
- 0.2.22
+ 0.2.31
patchzy
WiiCompiled
Command-line installer and launcher for WiiCompiled on Linux
diff --git a/Launcher/WiiCompiled.Setup.Windows/Program.cs b/Launcher/WiiCompiled.Setup.Windows/Program.cs
index 4f59f7a..7995f17 100644
--- a/Launcher/WiiCompiled.Setup.Windows/Program.cs
+++ b/Launcher/WiiCompiled.Setup.Windows/Program.cs
@@ -121,7 +121,7 @@ internal static class PlatformChecks
internal static class ProductInfo
{
public const string Name = "WiiCompiled";
- public const string Version = "0.2.27";
+ public const string Version = "0.2.31";
///
/// The setup executable is copied into the installation under this name. It is the launcher and
diff --git a/Launcher/WiiCompiled.Setup.Windows/WiiCompiled.Setup.Windows.csproj b/Launcher/WiiCompiled.Setup.Windows/WiiCompiled.Setup.Windows.csproj
index 48fb916..0efaa73 100644
--- a/Launcher/WiiCompiled.Setup.Windows/WiiCompiled.Setup.Windows.csproj
+++ b/Launcher/WiiCompiled.Setup.Windows/WiiCompiled.Setup.Windows.csproj
@@ -7,7 +7,7 @@
WiiCompiled.Setup
WiiCompiled.Setup.Windows
app.manifest
- 0.2.27
+ 0.2.31
patchzy
WiiCompiled
Command-line installer and launcher for WiiCompiled
diff --git a/Launcher/local-build-macos.command b/Launcher/local-build-macos.command
index 2b2ec91..e86e58a 100755
--- a/Launcher/local-build-macos.command
+++ b/Launcher/local-build-macos.command
@@ -22,7 +22,7 @@ Usage: local-build-macos.command --output-dir DIR [options]
--retro-rewind-package-dir DIR RetroRewind6 directory (required for Retro Rewind)
--retro-wfc-offline-dir DIR Directory containing binary/payload.RMCPD00.bin
--skip-retro-wfc-payload Build Retro Rewind without the shared Retro-WFC payload
- --force-clean-build Delete local generated and native-build-macos caches
+ --force-clean-build Delete local generated and current-architecture native build caches
--parallel N Pin translation and build parallelism
--cmake PATH --ninja PATH Override build tools
--dotnet PATH Override dotnet
@@ -56,7 +56,9 @@ while (($#)); do
done
[[ $(uname -s) == Darwin ]] || fail 'this build script is for macOS only'
-[[ $(uname -m) == arm64 ]] || fail 'the current macOS product target is Apple Silicon only'
+macos_arch=$(uname -m)
+case "$macos_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $macos_arch" ;; esac
+macos_deployment_target=12.0
workspace=$(cd "$workspace" && pwd)
[[ -n "$output_dir" ]] || fail '--output-dir is required'
case "$profile" in base|retro-rewind|both) ;; *) fail '--profile must be base, retro-rewind, or both' ;; esac
@@ -73,7 +75,8 @@ for tool in "$cmake_bin" "$ninja_bin" clang clang++ shasum; do command -v "$tool
project="$workspace/projects/mkwii/recomp.yml"; assets="$workspace/Assets"; generated="$workspace/generated"
functions="$generated/functions"; metadata="$generated/base_translation_output.json"; manifest_dir="$workspace/build/base"
-manifest="$manifest_dir/mkwii_base_manifest.json"; shards="$generated/build_shards"; native_build="$workspace/native-build-macos"
+manifest="$manifest_dir/mkwii_base_manifest.json"; shards="$generated/build_shards"
+native_build="$workspace/native-build-macos-$macos_arch"
assert_file "$project" 'translation project'
if [[ -n "$game" ]]; then "$script_dir/macos/extract-disc.command" --game "$game" --assets-dir "$assets" --nodtool "$nodtool"; fi
assert_file "$assets/main.dol" 'extracted main.dol'; assert_file "$assets/StaticR.rel" 'extracted StaticR.rel'
@@ -131,11 +134,9 @@ if (( builds_retro )); then args+=(--resolved-profile "$mod_out/resolved_dispatc
step emit-build-shards 'Preparing native build shards'; translator "${args[@]}"
step configure-native 'Configuring the native toolchain'
-# Use Aurora's pinned SDL3 source on macOS. A system SDL3 can be older than
-# Aurora's required API even when find_package() succeeds.
-"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs" -DAURORA_SDL3_PROVIDER=vendor
+"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DCMAKE_OSX_ARCHITECTURES="$macos_arch" -DCMAKE_OSX_DEPLOYMENT_TARGET="$macos_deployment_target" -DAURORA_SDL3_PROVIDER=vendor -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs"
targets=(); [[ "$profile" != retro-rewind ]] && targets+=(WiiCompiled); [[ "$profile" != base ]] && targets+=(RetroRewind)
step compile "Compiling ${targets[*]} locally"; "$cmake_bin" --build "$native_build" --target "${targets[@]}" --parallel "$global_jobs"
-if [[ "$profile" != retro-rewind ]]; then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product WiiCompiled --output-dir "${base_output_dir:-$output_dir}"; fi
-if (( builds_retro )); then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product RetroRewind --output-dir "$output_dir"; fi
+if [[ "$profile" != retro-rewind ]]; then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product WiiCompiled --output-dir "${base_output_dir:-$output_dir}" --architecture "$macos_arch" --minimum-system-version "$macos_deployment_target"; fi
+if (( builds_retro )); then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product RetroRewind --output-dir "$output_dir" --architecture "$macos_arch" --minimum-system-version "$macos_deployment_target"; fi
printf 'MKWCBUILD:OUTPUT=%s\n' "$output_dir"
diff --git a/Launcher/macos/build-setup-pkg.command b/Launcher/macos/build-setup-pkg.command
index 22efb2b..6cbe610 100755
--- a/Launcher/macos/build-setup-pkg.command
+++ b/Launcher/macos/build-setup-pkg.command
@@ -9,12 +9,15 @@ fail() { printf 'build-setup-pkg.command: error: %s\n' "$*" >&2; exit 1; }
copy_clean() { DITTONORSRC=1 ditto --norsrc --noqtn "$@"; }
usage() {
cat <<'EOF'
-Usage: build-setup-pkg.command --nodtool PATH --translator PATH --cmake-root DIR --ninja PATH --output PKG [options]
+Usage: build-setup-pkg.command --nodtool-arm64 PATH --nodtool-x86_64 PATH --translator-arm64 PATH --translator-x86_64 PATH --cmake-root DIR --ninja-arm64 PATH --ninja-x86_64 PATH --output PKG [options]
Creates a game-code-free WiiCompiled Setup.pkg. The supplied tools must be
-maintainer-verified, redistributable macOS arm64 artifacts. The resulting pkg
-is unsigned unless --installer-identity is supplied; releases should sign and
-notarize it with a Developer ID Installer certificate.
+maintainer-verified, redistributable macOS artifacts for both arm64 and
+x86_64. The setup package selects native tools for its host while the game is
+compiled locally for that host architecture. CMake must be universal2. The
+resulting pkg is unsigned unless
+--installer-identity is supplied; releases should sign and notarize it with a
+Developer ID Installer certificate.
--workspace DIR Repository root (default: script's grandparent)
--version VERSION Bundle/package version (default: 0.1.0)
@@ -23,14 +26,17 @@ EOF
}
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
-workspace=$(cd "$script_dir/../.." && pwd); nodtool=""; translator=""; cmake_root=""; ninja=""; output=""; version=0.1.0; identity=""
+workspace=$(cd "$script_dir/../.." && pwd); nodtool_arm64=""; nodtool_x86_64=""; translator_arm64=""; translator_x86_64=""; cmake_root=""; ninja_arm64=""; ninja_x86_64=""; output=""; version=0.1.0; identity=""
while (($#)); do
case "$1" in
--workspace) workspace=${2:-}; shift 2 ;;
- --nodtool) nodtool=${2:-}; shift 2 ;;
- --translator) translator=${2:-}; shift 2 ;;
+ --nodtool-arm64) nodtool_arm64=${2:-}; shift 2 ;;
+ --nodtool-x86_64) nodtool_x86_64=${2:-}; shift 2 ;;
+ --translator-arm64) translator_arm64=${2:-}; shift 2 ;;
+ --translator-x86_64) translator_x86_64=${2:-}; shift 2 ;;
--cmake-root) cmake_root=${2:-}; shift 2 ;;
- --ninja) ninja=${2:-}; shift 2 ;;
+ --ninja-arm64) ninja_arm64=${2:-}; shift 2 ;;
+ --ninja-x86_64) ninja_x86_64=${2:-}; shift 2 ;;
--output) output=${2:-}; shift 2 ;;
--version) version=${2:-}; shift 2 ;;
--installer-identity) identity=${2:-}; shift 2 ;;
@@ -42,12 +48,48 @@ version=${version#v}
[[ "$version" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]] || fail '--version must contain one to three period-separated integers'
IFS=. read -r version_major version_minor version_patch <<< "$version"
short_version="$version_major.${version_minor:-0}.${version_patch:-0}"
-for tool in pkgbuild productbuild ditto codesign; do command -v "$tool" >/dev/null || fail "required macOS tool unavailable: $tool"; done
-[[ -x "$nodtool" ]] || fail '--nodtool must name an executable'
-[[ -x "$translator" ]] || fail '--translator must name an executable'
+for tool in pkgbuild productbuild ditto codesign lipo; do command -v "$tool" >/dev/null || fail "required macOS tool unavailable: $tool"; done
+for tool_path in "$nodtool_arm64" "$nodtool_x86_64" "$translator_arm64" "$translator_x86_64" "$ninja_arm64" "$ninja_x86_64"; do [[ -x "$tool_path" ]] || fail 'each architecture-specific tool must name an executable'; done
[[ -x "$cmake_root/bin/cmake" ]] || fail '--cmake-root must contain bin/cmake'
-[[ -x "$ninja" ]] || fail '--ninja must name an executable'
-"$nodtool" --version >/dev/null || fail '--nodtool did not run successfully'
+require_arch() {
+ local artifact=$1 arch=$2 label=$3
+ lipo "$artifact" -verify_arch "$arch" >/dev/null 2>&1 || fail "$label must contain a $arch slice: $artifact"
+}
+require_arch "$nodtool_arm64" arm64 '--nodtool-arm64'; require_arch "$nodtool_x86_64" x86_64 '--nodtool-x86_64'
+require_arch "$translator_arm64" arm64 '--translator-arm64'; require_arch "$translator_x86_64" x86_64 '--translator-x86_64'
+require_arch "$ninja_arm64" arm64 '--ninja-arm64'; require_arch "$ninja_x86_64" x86_64 '--ninja-x86_64'
+lipo "$cmake_root/bin/cmake" -verify_arch arm64 x86_64 >/dev/null 2>&1 || fail '--cmake-root/bin/cmake must be universal2'
+
+# Slice checks above prevent accidental cross-architecture packaging. Exercise
+# each supplied executable as well: an incorrectly bundled runtime can have a
+# valid Mach-O header but still fail before the setup app can use it. Apple
+# Silicon maintainers validate Intel tools through Rosetta when it is present.
+host_arch=$(uname -m)
+run_for_arch() {
+ local arch=$1 label=$2
+ shift 2
+ if [[ "$arch" == "$host_arch" ]]; then
+ "$@" >/dev/null || fail "$label did not run successfully"
+ elif [[ "$host_arch" == arm64 && "$arch" == x86_64 ]] && /usr/bin/arch -x86_64 /usr/bin/true >/dev/null 2>&1; then
+ /usr/bin/arch -x86_64 "$@" >/dev/null || fail "$label did not run successfully under Rosetta"
+ else
+ # Intel hosts cannot execute arm64 binaries. The slice remains checked
+ # above; CI or an Apple Silicon maintainer must execute that tool set.
+ printf 'build-setup-pkg.command: warning: unable to execute %s on %s; architecture slice was verified, but run it in %s CI before release\n' \
+ "$label" "$host_arch" "$arch" >&2
+ fi
+}
+for arch in arm64 x86_64; do
+ if [[ "$arch" == arm64 ]]; then
+ nodtool=$nodtool_arm64; translator=$translator_arm64; ninja=$ninja_arm64
+ else
+ nodtool=$nodtool_x86_64; translator=$translator_x86_64; ninja=$ninja_x86_64
+ fi
+ run_for_arch "$arch" "--nodtool-$arch" "$nodtool" --version
+ run_for_arch "$arch" "--translator-$arch" "$translator" --help
+ run_for_arch "$arch" "--ninja-$arch" "$ninja" --version
+done
+run_for_arch "$host_arch" '--cmake-root/bin/cmake' "$cmake_root/bin/cmake" --version
workspace=$(cd "$workspace" && pwd); output=$(cd "$(dirname "$output")" && pwd)/$(basename "$output")
stage=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-pkg.XXXXXX")
trap 'rm -rf "$stage"' EXIT
@@ -64,7 +106,7 @@ cat > "$app/Contents/Info.plist" <CFBundlePackageTypeAPPL
CFBundleShortVersionString$short_version
CFBundleVersion$version
-LSMinimumSystemVersion14.0
+LSMinimumSystemVersion12.0
EOF
cat > "$app/Contents/MacOS/WiiCompiledSetup" <<'EOF'
@@ -103,14 +145,17 @@ copy_clean "$workspace/Launcher/local-build-macos.command" "$resources/workspace
copy_clean "$workspace/Launcher/macos/extract-disc.command" "$resources/workspace/Launcher/macos/extract-disc.command"
copy_clean "$workspace/Launcher/macos/publish-app.command" "$resources/workspace/Launcher/macos/publish-app.command"
chmod +x "$resources/workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/macos/"*.command
-# setup.command uses this marker to update source inputs in an existing user
+# setup.command uses this marker to refresh source inputs in an existing user
# workspace without replacing extracted game assets or Retro Rewind files.
printf '%s\n' "$version" > "$resources/workspace/.bundle-version"
-mkdir -p "$resources/tools/cmake"
-copy_clean "$nodtool" "$resources/tools/nodtool"; chmod +x "$resources/tools/nodtool"
-copy_clean "$translator" "$resources/tools/Translator.Cli"; chmod +x "$resources/tools/Translator.Cli"
+mkdir -p "$resources/tools/cmake" "$resources/tools/arm64" "$resources/tools/x86_64"
+copy_clean "$nodtool_arm64" "$resources/tools/arm64/nodtool"; chmod +x "$resources/tools/arm64/nodtool"
+copy_clean "$nodtool_x86_64" "$resources/tools/x86_64/nodtool"; chmod +x "$resources/tools/x86_64/nodtool"
+copy_clean "$translator_arm64" "$resources/tools/arm64/Translator.Cli"; chmod +x "$resources/tools/arm64/Translator.Cli"
+copy_clean "$translator_x86_64" "$resources/tools/x86_64/Translator.Cli"; chmod +x "$resources/tools/x86_64/Translator.Cli"
copy_clean "$cmake_root" "$resources/tools/cmake"
-copy_clean "$ninja" "$resources/tools/ninja"; chmod +x "$resources/tools/ninja"
+copy_clean "$ninja_arm64" "$resources/tools/arm64/ninja"; chmod +x "$resources/tools/arm64/ninja"
+copy_clean "$ninja_x86_64" "$resources/tools/x86_64/ninja"; chmod +x "$resources/tools/x86_64/ninja"
copy_clean "$workspace/LICENSE" "$resources/LICENSE"
copy_clean "$workspace/THIRD-PARTY-NOTICES.md" "$resources/THIRD-PARTY-NOTICES.md"
codesign --force --deep --sign - "$app"
diff --git a/Launcher/macos/macos-x86_64-toolchain.cmake b/Launcher/macos/macos-x86_64-toolchain.cmake
new file mode 100644
index 0000000..f367498
--- /dev/null
+++ b/Launcher/macos/macos-x86_64-toolchain.cmake
@@ -0,0 +1,11 @@
+# Cross-compile a thin x86_64 macOS build from an Apple Silicon Mac.
+# Pass this file on the first configure with:
+# -DCMAKE_TOOLCHAIN_FILE=/absolute/path/to/macos-x86_64-toolchain.cmake
+#
+# CMAKE_SYSTEM_PROCESSOR is deliberately declared here rather than inferred
+# from CMAKE_OSX_ARCHITECTURES, so target-aware CMake dependencies select their
+# x86_64 artifacts.
+set(CMAKE_SYSTEM_NAME Darwin)
+set(CMAKE_SYSTEM_PROCESSOR x86_64)
+set(CMAKE_OSX_ARCHITECTURES x86_64 CACHE STRING
+ "Target macOS architectures" FORCE)
diff --git a/Launcher/macos/publish-app.command b/Launcher/macos/publish-app.command
index b384a55..a284ed0 100755
--- a/Launcher/macos/publish-app.command
+++ b/Launcher/macos/publish-app.command
@@ -5,28 +5,36 @@ set -euo pipefail
fail() { printf 'publish-app.command: error: %s\n' "$*" >&2; exit 1; }
usage() {
cat <<'EOF'
-Usage: publish-app.command --build-dir DIR --product {WiiCompiled|RetroRewind} --output-dir DIR
+Usage: publish-app.command --build-dir DIR --product {WiiCompiled|RetroRewind} --output-dir DIR [options]
Copies a locally built product and its runtime assets into OUTPUT-DIR/.app.
It bundles non-system dylibs, rewrites their install names, and ad-hoc signs the
result. This is suitable for local use; a release must replace ad-hoc signing
with the project's Developer ID signing and notarization process.
+
+ --architecture {arm64|x86_64} Required architecture of the compiled product (default: host)
+ --minimum-system-version VERSION App bundle minimum macOS version (default: 12.0)
EOF
}
-build_dir=""; product=""; output_dir=""
+build_dir=""; product=""; output_dir=""; architecture=$(uname -m); minimum_system_version=12.0
while (($#)); do
case "$1" in
--build-dir) build_dir=${2:-}; shift 2 ;;
--product) product=${2:-}; shift 2 ;;
--output-dir) output_dir=${2:-}; shift 2 ;;
+ --architecture) architecture=${2:-}; shift 2 ;;
+ --minimum-system-version) minimum_system_version=${2:-}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown option: $1" ;;
esac
done
[[ "$product" == WiiCompiled || "$product" == RetroRewind ]] || fail '--product must be WiiCompiled or RetroRewind'
-for tool in codesign ditto install_name_tool otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
+[[ "$architecture" == arm64 || "$architecture" == x86_64 ]] || fail '--architecture must be arm64 or x86_64'
+[[ "$minimum_system_version" =~ ^[0-9]+(\.[0-9]+){1,2}$ ]] || fail '--minimum-system-version must contain two or three period-separated integers'
+for tool in codesign ditto install_name_tool lipo otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
[[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product"
+lipo "$build_dir/$product" -verify_arch "$architecture" || fail "compiled product is not $architecture: $build_dir/$product"
for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done
app="$output_dir/$product.app"
@@ -47,7 +55,7 @@ cat > "$app/Contents/Info.plist" <CFBundlePackageTypeAPPL
CFBundleShortVersionString0.1.0
CFBundleVersion1
- LSMinimumSystemVersion14.0
+ LSMinimumSystemVersion$minimum_system_version
NSHighResolutionCapable
EOF
@@ -57,25 +65,62 @@ for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do
ln -s "../Resources/$asset" "$macos/$asset"
done
-# Build a closure of Homebrew dylibs. System libraries remain system references.
+# Resolve a non-system dependency from the build product's rpaths. This covers
+# both traditional Homebrew dylibs and the vendored dylibs CMake emits under
+# the local build directory for a cross-architecture build.
+dependency_path() {
+ local current=$1 dependency=$2 name rpath candidate
+ case "$dependency" in
+ /opt/homebrew/*|/usr/local/*)
+ [[ -f "$dependency" ]] && { printf '%s\n' "$dependency"; return 0; }
+ ;;
+ @rpath/*)
+ name=${dependency##*/}
+ while IFS= read -r rpath; do
+ case "$rpath" in
+ @loader_path/*) rpath="$(dirname "$current")/${rpath#@loader_path/}" ;;
+ @executable_path/*) rpath="$macos/${rpath#@executable_path/}" ;;
+ esac
+ candidate="$rpath/$name"
+ [[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; }
+ done < <(otool -l "$current" | awk '
+ /LC_RPATH/ { rpath = 1; next }
+ rpath && /^[[:space:]]*path / {
+ sub(/^[[:space:]]*path /, "")
+ sub(/ \(offset [0-9]+\)$/, "")
+ print
+ rpath = 0
+ }
+ ')
+ ;;
+ @loader_path/*)
+ candidate="$(dirname "$current")/${dependency#@loader_path/}"
+ [[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; }
+ ;;
+ esac
+ return 1
+}
+
+# Build a closure of non-system dylibs. System libraries remain system
+# references, while every resolved dependency is copied beside the executable.
queue=("$macos/$product")
while ((${#queue[@]})); do
current=${queue[0]}
queue=("${queue[@]:1}")
while IFS= read -r dependency; do
- [[ "$dependency" == /opt/homebrew/* || "$dependency" == /usr/local/* ]] || continue
- [[ -f "$dependency" ]] || continue
+ dependency_path=$(dependency_path "$current" "$dependency") || continue
name=$(basename "$dependency")
if [[ ! -f "$frameworks/$name" ]]; then
- ditto "$dependency" "$frameworks/$name"
+ ditto "$dependency_path" "$frameworks/$name"
install_name_tool -id "@rpath/$name" "$frameworks/$name"
- queue+=("$frameworks/$name")
+ # Resolve transitive @loader_path dependencies relative to the
+ # original dylib, not its copied Frameworks location.
+ queue+=("$dependency_path")
fi
done < <(otool -L "$current" | tail -n +2 | awk '{print $1}')
done
while IFS= read -r binary; do
while IFS= read -r old; do
- [[ "$old" == /opt/homebrew/* || "$old" == /usr/local/* ]] || continue
name=$(basename "$old")
[[ -f "$frameworks/$name" ]] || continue
if [[ "$binary" == "$macos/$product" ]]; then
diff --git a/Launcher/macos/setup.command b/Launcher/macos/setup.command
index 5694a57..8a68342 100755
--- a/Launcher/macos/setup.command
+++ b/Launcher/macos/setup.command
@@ -5,10 +5,7 @@ set -euo pipefail
resources=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace_source="$resources/workspace"
-nodtool="$resources/tools/nodtool"
-translator="$resources/tools/Translator.Cli"
cmake_bin="$resources/tools/cmake/bin/cmake"
-ninja_bin="$resources/tools/ninja"
support_root="$HOME/Library/Application Support/WiiCompiled"
workspace="$support_root/BuildWorkspace"
products="$support_root/Products"
@@ -36,6 +33,13 @@ while (($#)); do
done
[[ "$install_location" == user || "$install_location" == applications ]] || fail '--install-location must be user or applications'
+host_arch=$(uname -m)
+case "$host_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $host_arch" ;; esac
+host_tools="$resources/tools/$host_arch"
+nodtool="$host_tools/nodtool"
+translator="$host_tools/Translator.Cli"
+ninja_bin="$host_tools/ninja"
+
if [[ -z "$game" ]]; then
game=$(/usr/bin/osascript <<'APPLESCRIPT'
set selectedFile to choose file with prompt "Choose your clean Mario Kart Wii PAL (RMCP01) disc image"
@@ -60,6 +64,10 @@ if ! /usr/bin/xcode-select -p >/dev/null 2>&1; then
/usr/bin/xcode-select --install || true
exit 1
fi
+for tool in "$nodtool" "$translator" "$cmake_bin" "$ninja_bin"; do
+ /usr/bin/lipo "$tool" -verify_arch "$host_arch" >/dev/null 2>&1 || \
+ fail "the packaged $(basename "$tool") does not support $host_arch"
+done
mkdir -p "$support_root" "$products"
source_bundle_version="$workspace_source/.bundle-version"
@@ -84,7 +92,7 @@ if (( needs_workspace_refresh )); then
/usr/bin/ditto "$source_bundle_version" "$workspace_bundle_version"
# A dependency provider can be cached in this directory, so make the
# refreshed sources configure from a clean native build tree.
- rm -rf "$workspace/native-build-macos"
+ rm -rf "$workspace/native-build-macos-arm64" "$workspace/native-build-macos-x86_64"
fi
fi
diff --git a/Launcher/prepare-portable-tools.sh b/Launcher/prepare-portable-tools.sh
index fe7b42a..69ea00d 100755
--- a/Launcher/prepare-portable-tools.sh
+++ b/Launcher/prepare-portable-tools.sh
@@ -25,7 +25,7 @@ set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace=$(cd "$script_dir/.." && pwd)
-llvm_version=23.1.0
+llvm_version=22.1.8
cmake_version=4.3.3
ninja_version=1.13.2
destination="$script_dir/artifacts/portable-tools"
@@ -52,13 +52,13 @@ done
case "$arch" in
x86_64) llvm_release_arch=X64; target_triple=x86_64-unknown-linux-gnu
- llvm_release_sha256=18da30f77f475688a18f7704d23f9f155ae007ed9922dbed6850a9419d9fec8c
+ llvm_release_sha256=df0e1ecf16caf3489a272a5eea4eec9b0d82878f6477fa309504f918a0006384
cmake_release_arch=x86_64
cmake_sha256=927b2368a946c37269c3a66225ab00544e756459cdd0b5d0da438694fb9ff802
ninja_asset=ninja-linux.zip
ninja_sha256=5749cbc4e668273514150a80e387a957f933c6ed3f5f11e03fb30955e2bbead6 ;;
aarch64) llvm_release_arch=ARM64; target_triple=aarch64-unknown-linux-gnu
- llvm_release_sha256=cfb31bfc713ef453248bf5bd026312f838ad6c52c25623e987cb6a340f3050d4
+ llvm_release_sha256=805efad2bb91cb4967fa569e0881d10c0f69c04461cf671cccbae19f547acc34
cmake_release_arch=aarch64
cmake_sha256=9ea38356dbd3e32e51029a3e09a0f2f8e117ef4fbcaad7a21ffb36409bbd5cb4
ninja_asset=ninja-linux-aarch64.zip
@@ -120,9 +120,9 @@ echo "prepare-portable-tools.sh: pruning to the minimal compile+link toolchain..
# clang: the real driver executable plus the clang/clang++ symlinks CMake/local-build.sh invoke.
# Stripped: debug symbols are dead weight for a bundled compiler nobody will debug.
-cp -a "$src/bin/clang-23" "$work/bin/"
-strip "$work/bin/clang-23"
-ln -s clang-23 "$work/bin/clang"
+cp -a "$src/bin/clang-22" "$work/bin/"
+strip "$work/bin/clang-22"
+ln -s clang-22 "$work/bin/clang"
ln -s clang "$work/bin/clang++"
# lld: linked via -fuse-ld=lld, which clang resolves by looking for ld.lld next to itself first -
@@ -201,10 +201,10 @@ Ninja $ninja_version
Apache License 2.0
EOF
-echo "prepare-portable-tools.sh: smoke-testing the toolchain..."
-smoke_dir=$(mktemp -d)
-trap 'rm -rf "$smoke_dir"' EXIT
-cat > "$smoke_dir/t.cpp" <<'EOF'
+echo "prepare-portable-tools.sh: testing the toolchain..."
+test_dir=$(mktemp -d)
+trap 'rm -rf "$test_dir"' EXIT
+cat > "$test_dir/t.cpp" <<'EOF'
#include
#include
int main() {
@@ -214,23 +214,23 @@ int main() {
return sum == 6 ? 0 : 1;
}
EOF
-"$work/bin/clang++" -std=c++20 -fuse-ld=lld "$smoke_dir/t.cpp" -o "$smoke_dir/t"
-"$smoke_dir/t"
+"$work/bin/clang++" -std=c++20 -fuse-ld=lld "$test_dir/t.cpp" -o "$test_dir/t"
+"$test_dir/t"
# Also exercised together through CMake+Ninja, exactly how local-build.sh drives them - a plain
# clang++ invocation above would not catch a broken CMAKE_ROOT (Modules/Templates) or a Ninja that
# can't find the compiler.
-cat > "$smoke_dir/CMakeLists.txt" <<'EOF'
+cat > "$test_dir/CMakeLists.txt" <<'EOF'
cmake_minimum_required(VERSION 3.16)
-project(smoke CXX)
-add_executable(smoke t.cpp)
+project(test CXX)
+add_executable(test t.cpp)
EOF
-"$work/bin/cmake" -S "$smoke_dir" -B "$smoke_dir/build" -G Ninja \
+"$work/bin/cmake" -S "$test_dir" -B "$test_dir/build" -G Ninja \
-DCMAKE_MAKE_PROGRAM="$work/bin/ninja" -DCMAKE_CXX_COMPILER="$work/bin/clang++" >/dev/null
-"$work/bin/cmake" --build "$smoke_dir/build" >/dev/null
-"$smoke_dir/build/smoke"
+"$work/bin/cmake" --build "$test_dir/build" >/dev/null
+"$test_dir/build/test"
-rm -rf "$smoke_dir"
+rm -rf "$test_dir"
trap - EXIT
mv "$work" "$toolchain_dir"
diff --git a/README.md b/README.md
index 4a7be5f..8a6403f 100644
--- a/README.md
+++ b/README.md
@@ -89,8 +89,8 @@ Known limitations of the Wii Remote path:
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
- About 20 GB of free disk space during installation (Final game size ~5 GB)
-- macOS 14 (Sonoma) or later on Apple Silicon
-- On macOS, Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
+- macOS 12 (Monterey) or later on Apple Silicon (`arm64`) or Intel (`x86_64-v3`); pre-Haswell Intel CPUs are unsupported
+- On macOS, a Metal-capable GPU and Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
GCZ, CISO, WBFS, WIA and RVZ are accepted.
diff --git a/aurora-main/cmake/aurora_core.cmake b/aurora-main/cmake/aurora_core.cmake
index 3e53a62..b17cb42 100644
--- a/aurora-main/cmake/aurora_core.cmake
+++ b/aurora-main/cmake/aurora_core.cmake
@@ -41,7 +41,9 @@ if (AURORA_ENABLE_GX)
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_METAL)
target_sources(aurora_core PRIVATE lib/dawn/MetalBinding.mm)
set_source_files_properties(lib/dawn/MetalBinding.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
- target_link_options(aurora_core PUBLIC "LINKER:-weak_framework,Metal")
+ target_link_options(aurora_core PUBLIC
+ "LINKER:-weak_framework,Metal"
+ "LINKER:-U,_OBJC_CLASS_$_MTLLogStateDescriptor")
endif ()
if (DAWN_ENABLE_D3D11)
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_D3D11)
diff --git a/aurora-main/extern/CMakeLists.txt b/aurora-main/extern/CMakeLists.txt
index 9d77bbb..1612b4f 100644
--- a/aurora-main/extern/CMakeLists.txt
+++ b/aurora-main/extern/CMakeLists.txt
@@ -16,10 +16,60 @@ function(aurora_find_package_global)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ${_PREV_FIND_PACKAGE_TARGETS_GLOBAL})
endfunction()
+# A macOS build for another architecture must not discover Homebrew packages
+# built for its physical host. CMAKE_CROSSCOMPILING is not sufficient here:
+# Apple Clang can target another architecture through CMAKE_OSX_ARCHITECTURES
+# without CMake considering the configure a cross-build.
+set(_AURORA_EXCLUDE_HOST_HOMEBREW FALSE)
+if (APPLE AND CMAKE_OSX_ARCHITECTURES)
+ list(LENGTH CMAKE_OSX_ARCHITECTURES _AURORA_OSX_ARCH_COUNT)
+ if (_AURORA_OSX_ARCH_COUNT EQUAL 1)
+ list(GET CMAKE_OSX_ARCHITECTURES 0 _AURORA_TARGET_ARCH)
+ string(TOLOWER "${_AURORA_TARGET_ARCH}" _AURORA_TARGET_ARCH)
+ # hw.optional.arm64 identifies Apple Silicon even when CMake itself runs
+ # through Rosetta, where CMAKE_HOST_SYSTEM_PROCESSOR reports x86_64.
+ execute_process(
+ COMMAND /usr/sbin/sysctl -n hw.optional.arm64
+ RESULT_VARIABLE _AURORA_ARM64_PROBE_RESULT
+ OUTPUT_VARIABLE _AURORA_ARM64_PROBE
+ ERROR_QUIET
+ OUTPUT_STRIP_TRAILING_WHITESPACE)
+ if (_AURORA_ARM64_PROBE_RESULT EQUAL 0 AND _AURORA_ARM64_PROBE STREQUAL "1")
+ set(_AURORA_HOST_ARCH arm64)
+ else ()
+ execute_process(
+ COMMAND /usr/bin/uname -m
+ OUTPUT_VARIABLE _AURORA_HOST_ARCH
+ OUTPUT_STRIP_TRAILING_WHITESPACE)
+ string(TOLOWER "${_AURORA_HOST_ARCH}" _AURORA_HOST_ARCH)
+ endif ()
+ if (_AURORA_TARGET_ARCH STREQUAL "x86_64" AND _AURORA_HOST_ARCH MATCHES "^(arm64|aarch64)$")
+ list(APPEND CMAKE_IGNORE_PREFIX_PATH "/opt/homebrew")
+ set(_AURORA_EXCLUDE_HOST_HOMEBREW TRUE)
+ elseif (_AURORA_TARGET_ARCH MATCHES "^(arm64|aarch64)$" AND _AURORA_HOST_ARCH MATCHES "^(x86_64|amd64)$")
+ list(APPEND CMAKE_IGNORE_PREFIX_PATH "/usr/local")
+ set(_AURORA_EXCLUDE_HOST_HOMEBREW TRUE)
+ endif ()
+ endif ()
+endif ()
+
+if (_AURORA_EXCLUDE_HOST_HOMEBREW)
+ list(REMOVE_DUPLICATES CMAKE_IGNORE_PREFIX_PATH)
+ message(STATUS "aurora: cross-architecture macOS build; ignoring host Homebrew prefixes")
+endif ()
+
if (AURORA_ENABLE_GX)
include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/AuroraDawnProvider.cmake)
endif ()
+# SDL's pkg-config probe can bypass CMake's prefix exclusion. macOS does not
+# need libusb for the supported SDL input paths, so keep that host-only library
+# out of a cross-architecture configure.
+if (_AURORA_EXCLUDE_HOST_HOMEBREW)
+ set(SDL_HIDAPI_LIBUSB OFF CACHE BOOL "" FORCE)
+ set(SDL_HIDAPI_LIBUSB_SHARED OFF CACHE BOOL "" FORCE)
+endif ()
+
# Abseil is needed for core libraries. It normally comes via Dawn's vendor build.
# Otherwise prefer a system package and only fetch it as a last resort.
if (NOT TARGET absl::flat_hash_map OR NOT TARGET absl::btree)
diff --git a/aurora-main/lib/dolphin/pad/pad.cpp b/aurora-main/lib/dolphin/pad/pad.cpp
index b06c489..0ba55f1 100644
--- a/aurora-main/lib/dolphin/pad/pad.cpp
+++ b/aurora-main/lib/dolphin/pad/pad.cpp
@@ -6,10 +6,10 @@
#include
#include
+#include
#include
#include
#include
-#include
namespace {
constexpr int32_t k_mappingsFileVersion = 3;
@@ -342,7 +342,7 @@ BOOL PADInit() {
}
g_initialized = true;
- std::ranges::for_each(g_keyboardBindings, [](auto& state) {
+ std::for_each(g_keyboardBindings.begin(), g_keyboardBindings.end(), [](auto& state) {
state.m_buttonMapping = g_defaultKeys;
state.m_axisMapping = g_defaultKeyAxis;
});
@@ -634,8 +634,8 @@ static void EnsureMappingLoaded(aurora::input::GameController* controller) {
static Sint16 _get_axis_value(const aurora::input::GameController* controller, // NOLINT(*-reserved-identifier)
PADAxis axis) {
- const auto iter =
- std::ranges::find_if(controller->m_axisMapping, [axis](const auto& pair) { return pair.padAxis == axis; });
+ const auto iter = std::find_if(controller->m_axisMapping.begin(), controller->m_axisMapping.end(),
+ [axis](const auto& pair) { return pair.padAxis == axis; });
if (iter == controller->m_axisMapping.end()) {
return 0;
}
@@ -725,8 +725,8 @@ u32 PADRead(PADStatus* status) {
status[i].err = PAD_ERR_NONE;
if (g_keyboardBindings[i].m_mappingsSet) {
- std::ranges::for_each(
- g_keyboardBindings[i].m_buttonMapping, [&kbState, &i, &status](const PADKeyButtonBinding& mapping) {
+ std::for_each(g_keyboardBindings[i].m_buttonMapping.begin(), g_keyboardBindings[i].m_buttonMapping.end(),
+ [&kbState, &i, &status](const PADKeyButtonBinding& mapping) {
if (mapping.scancode > PAD_KEY_INVALID && kbState[mapping.scancode]) {
status[i].button |= mapping.padButton;
} else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) {
@@ -833,8 +833,8 @@ u32 PADRead(PADStatus* status) {
bool leftTriggerSet = false;
bool rightTriggerSet = false;
- std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet,
- &rightTriggerSet](const auto& mapping) {
+ std::for_each(controller->m_buttonMapping.begin(), controller->m_buttonMapping.end(),
+ [&controller, &i, &status, &leftTriggerSet, &rightTriggerSet](const auto& mapping) {
if (SDL_GetGamepadButton(controller->m_controller, static_cast(mapping.nativeButton))) {
status[i].button |= mapping.padButton;
}
@@ -847,8 +847,8 @@ u32 PADRead(PADStatus* status) {
}
});
- std::ranges::for_each(controller->m_altButtonMapping, [&controller, &i, &status, &leftTriggerSet,
- &rightTriggerSet](const auto& mapping) {
+ std::for_each(controller->m_altButtonMapping.begin(), controller->m_altButtonMapping.end(),
+ [&controller, &i, &status, &leftTriggerSet, &rightTriggerSet](const auto& mapping) {
if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) {
return;
}
@@ -1165,8 +1165,8 @@ void PADSetButtonMapping(const u32 port, const PADButtonMapping mapping) {
return;
}
- const auto iter = std::ranges::find_if(controller->m_buttonMapping,
- [mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
+ const auto iter = std::find_if(controller->m_buttonMapping.begin(), controller->m_buttonMapping.end(),
+ [mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
if (iter == controller->m_buttonMapping.end()) {
return;
}
@@ -1199,8 +1199,8 @@ void PADSetAltButtonMapping(const u32 port, const PADButtonMapping mapping) {
return;
}
- const auto iter = std::ranges::find_if(controller->m_altButtonMapping,
- [mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
+ const auto iter = std::find_if(controller->m_altButtonMapping.begin(), controller->m_altButtonMapping.end(),
+ [mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
if (iter == controller->m_altButtonMapping.end()) {
return;
}
@@ -1226,8 +1226,8 @@ void PADSetAxisMapping(const u32 port, const PADAxisMapping mapping) {
return;
}
- const auto iter = std::ranges::find_if(controller->m_axisMapping,
- [mapping](const auto& pair) { return mapping.padAxis == pair.padAxis; });
+ const auto iter = std::find_if(controller->m_axisMapping.begin(), controller->m_axisMapping.end(),
+ [mapping](const auto& pair) { return mapping.padAxis == pair.padAxis; });
if (iter == controller->m_axisMapping.end()) {
return;
}
@@ -1396,9 +1396,10 @@ static void load_keyboard_bindings() {
if (mappingsSet) {
const bool anyBound =
- std::ranges::any_of(buttonMapping,
- [](const PADKeyButtonBinding& b) { return b.scancode != PAD_KEY_INVALID; }) ||
- std::ranges::any_of(axisMapping, [](const PADKeyAxisBinding& b) { return b.scancode != PAD_KEY_INVALID; });
+ std::any_of(buttonMapping.begin(), buttonMapping.end(),
+ [](const PADKeyButtonBinding& b) { return b.scancode != PAD_KEY_INVALID; }) ||
+ std::any_of(axisMapping.begin(), axisMapping.end(),
+ [](const PADKeyAxisBinding& b) { return b.scancode != PAD_KEY_INVALID; });
if (!anyBound) {
mappingsSet = false;
}
@@ -1543,8 +1544,8 @@ static constexpr std::array, PAD_AXIS_COU
const char* PADGetButtonName(const PADButton button) {
- if (const auto iter =
- std::ranges::find_if(skButtonNames, [&button](const auto& pair) { return button == pair.first; });
+ if (const auto iter = std::find_if(skButtonNames.begin(), skButtonNames.end(),
+ [&button](const auto& pair) { return button == pair.first; });
iter != skButtonNames.end()) {
return iter->second.data();
}
@@ -1557,7 +1558,8 @@ const char* PADGetNativeButtonName(u32 button) {
}
const char* PADGetAxisName(const PADAxis axis) {
- if (const auto it = std::ranges::find_if(skAxisNames, [&axis](const auto& pair) { return axis == pair.first; });
+ if (const auto it = std::find_if(skAxisNames.begin(), skAxisNames.end(),
+ [&axis](const auto& pair) { return axis == pair.first; });
it != skAxisNames.end()) {
return it->second.data();
}
@@ -1566,7 +1568,8 @@ const char* PADGetAxisName(const PADAxis axis) {
}
const char* PADGetAxisDirectionLabel(const PADAxis axis) {
- if (const auto it = std::ranges::find_if(skAxisDirLabels, [&axis](const auto& pair) { return axis == pair.first; });
+ if (const auto it = std::find_if(skAxisDirLabels.begin(), skAxisDirLabels.end(),
+ [&axis](const auto& pair) { return axis == pair.first; });
it != skAxisDirLabels.end()) {
return it->second.data();
}
diff --git a/aurora-main/lib/gfx/common.cpp b/aurora-main/lib/gfx/common.cpp
index 1bb1bb4..9f4ee8b 100644
--- a/aurora-main/lib/gfx/common.cpp
+++ b/aurora-main/lib/gfx/common.cpp
@@ -22,7 +22,6 @@
#include
#include
#include
-#include
#include
#include
@@ -1328,8 +1327,8 @@ static void render_impl(std::vector& renderPasses, wgpu::CommandEnco
#if defined(AURORA_GFX_DEBUG_GROUPS)
if (finalize && !g_debugGroupStack.empty()) {
- for (auto& it : std::ranges::reverse_view(g_debugGroupStack)) {
- Log.warn("Debug group was not popped at end of frame: {}", it);
+ for (auto it = g_debugGroupStack.rbegin(); it != g_debugGroupStack.rend(); ++it) {
+ Log.warn("Debug group was not popped at end of frame: {}", *it);
}
g_debugGroupStack.clear();
}
diff --git a/aurora-main/lib/gfx/texture_replacement.cpp b/aurora-main/lib/gfx/texture_replacement.cpp
index 6791381..e8d0fbf 100644
--- a/aurora-main/lib/gfx/texture_replacement.cpp
+++ b/aurora-main/lib/gfx/texture_replacement.cpp
@@ -553,7 +553,7 @@ void build_index() noexcept {
continue;
}
- s_replacementIndex.try_emplace(*parsed, path);
+ s_replacementIndex.try_emplace(*parsed, ReplacementIndexEntry{path});
}
Log.info("Indexed {} texture replacements", s_replacementIndex.size());
diff --git a/aurora-main/lib/input.cpp b/aurora-main/lib/input.cpp
index 8884f1a..6c9418b 100644
--- a/aurora-main/lib/input.cpp
+++ b/aurora-main/lib/input.cpp
@@ -401,6 +401,8 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
return -1;
}
controller.m_isGameCube = controller.m_vid == 0x057E && controller.m_pid == 0x0337;
+ const char* serial = SDL_GetGamepadSerial(ctrl);
+ controller.m_gameCubeUseOrdinaryStop = controller.m_isGameCube && serial && "GCP+"sv == serial;
if (controller.m_isGameCube ||
(SDL_GetGamepadType(ctrl) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO && controller.m_pid == 0x2073)) {
controller.m_deadZones.emulateTriggers = false;
@@ -481,6 +483,13 @@ bool controller_has_rumble(Uint32 instance) noexcept {
void controller_rumble(uint32_t instance, uint16_t low_freq_intensity, uint16_t high_freq_intensity,
uint16_t duration_ms) noexcept {
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
+ // GC Pocket+ has been observed continuing to vibrate after a hard stop;
+ // an ordinary stop cleared it. With GAMECUBE_RUMBLE_BRAKE enabled, SDL
+ // encodes (0, 1) as adapter command 0, whereas (0, 0) sends command 2.
+ // Apply the workaround here so shutdown uses the same stop as PAD calls.
+ if (it->second.m_gameCubeUseOrdinaryStop && low_freq_intensity == 0 && high_freq_intensity == 0) {
+ high_freq_intensity = 1;
+ }
SDL_RumbleGamepad(it->second.m_controller, low_freq_intensity, high_freq_intensity, duration_ms);
}
}
diff --git a/aurora-main/lib/input.hpp b/aurora-main/lib/input.hpp
index 1c6f24e..ae083b1 100644
--- a/aurora-main/lib/input.hpp
+++ b/aurora-main/lib/input.hpp
@@ -17,6 +17,7 @@ extern Module Log;
struct GameController {
SDL_Gamepad* m_controller = nullptr;
bool m_isGameCube = false;
+ bool m_gameCubeUseOrdinaryStop = false;
Sint32 m_index = -1;
Sint32 m_playerIndex = -1;
bool m_hasRumble = false;
diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt
index a6d383b..45e442c 100644
--- a/runtime/CMakeLists.txt
+++ b/runtime/CMakeLists.txt
@@ -1,30 +1,42 @@
cmake_minimum_required(VERSION 3.16)
+
+# Dawn's pinned macOS artifacts target 12.0. Set the same floor before project()
+# initializes the Apple toolchain so direct developer CMake invocations cannot
+# accidentally inherit the running SDK's deployment version. This cache entry
+# is harmless on non-Apple platforms and remains overridable by a caller.
+if(NOT CMAKE_OSX_DEPLOYMENT_TARGET)
+ set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING
+ "Minimum macOS version supported by WiiCompiled" FORCE)
+endif()
project(mkw_recompiled)
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
message(FATAL_ERROR "WiiCompiled requires a 64-bit Clang toolchain")
endif()
+if(APPLE AND CMAKE_OSX_ARCHITECTURES)
+ list(LENGTH CMAKE_OSX_ARCHITECTURES MKW_OSX_ARCHITECTURE_COUNT)
+ if(MKW_OSX_ARCHITECTURE_COUNT GREATER 1)
+ message(FATAL_ERROR
+ "WiiCompiled supports one macOS architecture per build directory; "
+ "configure separate arm64 and x86_64 build directories")
+ endif()
+endif()
+
if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_PLATFORM_WINDOWS TRUE)
-elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64)$")
- # The first native macOS target is Apple Silicon. Intel and universal
- # binaries remain future compatibility work; do not silently claim them.
+elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|arm64|ARM64)$")
set(MKW_PLATFORM_MACOS TRUE)
+ if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
+ set(MKW_PLATFORM_MACOS_X86_64 TRUE)
+ else()
+ set(MKW_PLATFORM_MACOS_ARM64 TRUE)
+ endif()
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$")
set(MKW_PLATFORM_LINUX TRUE)
else()
message(FATAL_ERROR
- "WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS arm64")
-endif()
-
-# Do not inherit the host SDK's deployment target (macOS 26 on current
-# toolchains). The supported Apple Silicon release is macOS 14 and the app
-# bundle advertises that same minimum. This remains arm64-only until the Intel
-# support work is merged.
-if(MKW_PLATFORM_MACOS)
- set(CMAKE_OSX_DEPLOYMENT_TARGET "14.0" CACHE STRING
- "Minimum macOS version for the Apple Silicon build" FORCE)
+ "WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS x86_64/arm64")
endif()
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
@@ -69,14 +81,15 @@ target_include_directories(mkw_pugixml PUBLIC third_party/pugixml)
target_compile_features(mkw_pugixml PUBLIC cxx_std_17)
set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF)
-# Linux guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric
+# POSIX x86-64 guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric
# stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
# asymmetric resume/yield coroutine libraries, which would need every call site restructured).
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
-# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers
-# and macOS uses the project's x18-safe AArch64 assembly backend, so this target is Linux-only.
-if(MKW_PLATFORM_LINUX)
+# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers;
+# Apple Silicon uses the project's x18-safe AArch64 assembly backend, while Intel macOS uses
+# libco's existing System V AMD64 backend.
+if(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
add_library(mkw_libco STATIC third_party/libco/libco.c)
add_library(mkw::libco ALIAS mkw_libco)
target_include_directories(mkw_libco PUBLIC third_party/libco)
@@ -253,12 +266,14 @@ endif()
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
if(MKW_PLATFORM_MACOS)
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory.cpp")
- # HostContext's Apple Silicon backend is implemented in a small assembly
- # companion. It must be part of the product runtime as well as the
- # standalone context test; otherwise the final executable is missing
- # mkw_co_init/mkw_co_switch at link time.
- enable_language(ASM)
- list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
+ if(MKW_PLATFORM_MACOS_ARM64)
+ # HostContext's Apple Silicon backend is implemented in a small assembly
+ # companion. It must be part of the product runtime as well as the
+ # standalone context test; otherwise the final executable is missing
+ # mkw_co_init/mkw_co_switch at link time.
+ enable_language(ASM)
+ list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
+ endif()
else()
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
endif()
@@ -287,6 +302,23 @@ target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
+add_executable(mkw_nand_save_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_save_tests.cpp")
+target_include_directories(mkw_nand_save_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
+target_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17)
+add_test(NAME mkw_nand_save_tests COMMAND mkw_nand_save_tests)
+
+add_executable(mkw_nand_settings_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_settings_tests.cpp")
+find_package(Threads REQUIRED)
+target_link_libraries(mkw_nand_settings_tests PRIVATE Threads::Threads)
+target_include_directories(mkw_nand_settings_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
+target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
+add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
+
+add_executable(mkw_sc_serial_tests "${CMAKE_CURRENT_LIST_DIR}/tests/sc_serial_tests.cpp")
+target_include_directories(mkw_sc_serial_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
+target_compile_features(mkw_sc_serial_tests PRIVATE cxx_std_17)
+add_test(NAME mkw_sc_serial_tests COMMAND mkw_sc_serial_tests)
+
# The input expression engine is self-contained, so it can be exercised without
# linking the runtime or SDL.
add_executable(mkw_input_expr_tests
@@ -312,20 +344,31 @@ if(MKW_PLATFORM_LINUX)
endif()
if(MKW_PLATFORM_MACOS)
- # Exercise the Apple Silicon context ABI and the public host-memory
- # contracts separately from translated products.
- enable_language(ASM)
- add_executable(mkw_macos_context_abi_tests
- "${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
- "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
- target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
- add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
+ # Exercise the public host-memory contracts separately from translated products.
+ if(MKW_PLATFORM_MACOS_ARM64)
+ # Apple Silicon's context ABI is implemented by the local assembly backend.
+ enable_language(ASM)
+ add_executable(mkw_macos_context_abi_tests
+ "${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
+ "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
+ target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
+ add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
- add_executable(mkw_macos_host_context_tests
- "${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
- "${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp"
- "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
- target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
+ add_executable(mkw_macos_host_context_tests
+ "${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
+ "${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp"
+ "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
+ target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
+ else()
+ # Intel macOS follows the same System V AMD64 libco path as Linux.
+ add_executable(mkw_macos_host_context_tests
+ "${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
+ "${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp")
+ target_include_directories(mkw_macos_host_context_tests PRIVATE
+ "${CMAKE_CURRENT_LIST_DIR}/include"
+ "${CMAKE_CURRENT_LIST_DIR}/third_party/libco")
+ target_link_libraries(mkw_macos_host_context_tests PRIVATE mkw::libco)
+ endif()
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
@@ -384,6 +427,13 @@ else()
target_link_libraries(mkw_macos_native_compile PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
mkw::pugixml mkw::toml11 mkw::cryptopp)
+ if(MKW_PLATFORM_MACOS_X86_64)
+ target_link_libraries(mkw_macos_native_compile PRIVATE mkw::libco)
+ # Keep this compile-only audit on the same Haswell-era x86-64-v3
+ # baseline as the translated product. PPC paired FMA helpers use
+ # FMA intrinsics and intentionally cannot compile for plain x86-64.
+ target_compile_options(mkw_macos_native_compile PRIVATE -march=x86-64-v3)
+ endif()
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
endif()
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
diff --git a/runtime/cmake/PublicProducts.cmake b/runtime/cmake/PublicProducts.cmake
index ad2a535..98ce39a 100644
--- a/runtime/cmake/PublicProducts.cmake
+++ b/runtime/cmake/PublicProducts.cmake
@@ -1,4 +1,4 @@
-# Public WiiCompiled product graph.
+# Public WiiCompiled product graph.
#
# The translator owns the translated build graph. Mario Kart's profile-neutral
# functions are compiled once into mkw_base_shared; only callers whose direct
@@ -84,9 +84,9 @@ target_link_libraries(mkw_runtime_common PRIVATE
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
if(MKW_PLATFORM_WINDOWS)
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
-elseif(MKW_PLATFORM_LINUX)
+elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
- # media monitoring). Empty string on glibc >= 2.34 where dl* is in libc.
+ # media monitoring). Empty on platforms where dl* is already in libc/libSystem.
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS})
endif()
if(MKW_CPPWINRT_INCLUDE_DIR)
@@ -206,6 +206,8 @@ function(mkw_configure_product target)
if(MKW_PLATFORM_MACOS)
target_link_libraries(${target} PRIVATE
"${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}")
+ target_link_options(${target} PRIVATE
+ "LINKER:-U,_OBJC_CLASS_$_MTLLogStateDescriptor")
endif()
if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
@@ -226,7 +228,7 @@ function(mkw_configure_product target)
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
- elseif(MKW_PLATFORM_LINUX)
+ elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
# mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o
# files via $, which does not propagate mkw_runtime_common's own
# target_link_libraries (object libraries don't carry usage requirements to a consumer
diff --git a/runtime/include/console_identity.h b/runtime/include/console_identity.h
index ef46679..c07fc30 100644
--- a/runtime/include/console_identity.h
+++ b/runtime/include/console_identity.h
@@ -1,38 +1,28 @@
#pragma once
-#include "runtime_config.h"
+#include "nand_path.h"
+#include "nand_settings.h"
-#include
#include
-#include
#include
#include
-#include
-#include
#include
-#include
-#include
#include
#include
-#include
#include
namespace RuntimeConsoleIdentity {
struct Identity {
std::string serial;
+ std::string productCode;
+ std::string area;
+ std::string gameRegion;
std::array mac;
};
-inline bool IsValidSerial(const std::string& serial) {
- return serial.size() == 9 &&
- serial != "000000000" &&
- std::all_of(serial.begin(), serial.end(),
- [](unsigned char value) { return std::isdigit(value) != 0; });
-}
-
inline Identity FromSerial(std::string serial) {
- // Keep Nintendo's Wii OUI. The suffix is derived from the persisted serial
+ // Keep Nintendo's Wii OUI. The suffix is derived from the NAND serial
// so every API exposes one coherent, stable virtual-console identity.
uint32_t hash = 2166136261u;
for (const unsigned char value : serial) {
@@ -46,6 +36,7 @@ inline Identity FromSerial(std::string serial) {
return {
std::move(serial),
+ {}, {}, {},
{
0x00,
0x09,
@@ -57,83 +48,23 @@ inline Identity FromSerial(std::string serial) {
};
}
-inline std::optional ReadSerial(const std::filesystem::path& path) {
- std::ifstream input(path);
- std::string line;
- if (!input || !std::getline(input, line)) {
- return std::nullopt;
+inline Identity LoadFromNand() {
+ const auto root = RuntimeNandPath::DiscoverNandRootPath();
+ const auto settings = RuntimeNandSettings::Read(root);
+ if (!settings || !RuntimeNandSettings::HasIdentity(*settings)) {
+ RuntimeNandPath::FailNandRoot(
+ "NAND setting.txt is missing or has invalid console identity fields (SERNO, CODE, AREA, GAME)",
+ root / "title/00000001/00000002/data/setting.txt");
}
- constexpr std::string_view prefix = "serial=";
- if (line.rfind(prefix, 0) != 0) {
- return std::nullopt;
- }
- std::string serial = line.substr(prefix.size());
- if (!IsValidSerial(serial)) {
- return std::nullopt;
- }
- return serial;
-}
-
-inline bool WriteSerial(const std::filesystem::path& path, const std::string& serial) {
- std::error_code ec;
- std::filesystem::create_directories(path.parent_path(), ec);
- if (ec) {
- return false;
- }
-
- std::filesystem::path temporary = path;
- temporary += ".tmp";
- {
- std::ofstream output(temporary, std::ios::trunc);
- if (!output) {
- return false;
- }
- output << "serial=" << serial << '\n';
- output.close();
- if (!output) {
- return false;
- }
- }
-
- std::filesystem::rename(temporary, path, ec);
- if (!ec) {
- return true;
- }
- std::filesystem::remove(temporary, ec);
- return false;
-}
-
-inline std::string GenerateSerial() {
- std::random_device entropy;
- std::seed_seq seed{
- entropy(),
- entropy(),
- entropy(),
- entropy(),
- };
- std::mt19937 generator(seed);
- std::uniform_int_distribution distribution(100000000u, 999999999u);
- return std::to_string(distribution(generator));
-}
-
-inline Identity LoadOrCreate(const std::filesystem::path& path) {
- if (const auto serial = ReadSerial(path)) {
- return FromSerial(*serial);
- }
-
- const std::string generated = GenerateSerial();
- if (WriteSerial(path, generated)) {
- return FromSerial(generated);
- }
-
- // Remain operational in a read-only environment. This fallback matches
- // Dolphin's deterministic serial while keeping the same valid identity shape.
- return FromSerial("123456789");
+ Identity identity = FromSerial(settings->at("SERNO"));
+ identity.productCode = settings->at("CODE");
+ identity.area = settings->at("AREA");
+ identity.gameRegion = settings->at("GAME");
+ return identity;
}
inline const Identity& Current() {
- static const Identity identity =
- LoadOrCreate(RuntimeConfigFile::ApplicationDataDirectory() / "ConsoleIdentity.txt");
+ static const Identity identity = LoadFromNand();
return identity;
}
diff --git a/runtime/include/host_context.h b/runtime/include/host_context.h
index 4d7355f..5ea6bfb 100644
--- a/runtime/include/host_context.h
+++ b/runtime/include/host_context.h
@@ -3,11 +3,11 @@
#include
// HostContext is the deliberately small boundary between the guest scheduler
-// and the host's cooperative-context facility. Windows uses native Fibers and
-// Linux uses libco; macOS AArch64 uses the local assembly backend because it
-// must preserve Darwin's platform-reserved x18 register, which libco's AArch64
-// backend does not save. Its handles are only valid on the thread that
-// initialized the scheduler.
+// and the host's cooperative-context facility. Windows uses native Fibers;
+// Linux and Intel macOS use libco's System V x86-64 backend. macOS AArch64 uses
+// the local assembly backend because it must preserve Darwin's platform-reserved
+// x18 register, which libco's AArch64 backend does not save. Its handles are
+// only valid on the thread that initialized the scheduler.
namespace HostContext {
using Handle = void*;
diff --git a/runtime/include/nand_path.h b/runtime/include/nand_path.h
index 709e4af..eb0fda8 100644
--- a/runtime/include/nand_path.h
+++ b/runtime/include/nand_path.h
@@ -1,6 +1,7 @@
#pragma once
#include "runtime_config.h"
+#include "nand_settings.h"
#include "runtime_log.h"
#include "system_bridge.h"
@@ -163,7 +164,7 @@ inline std::filesystem::path CreateManagedNandRoot() {
return root;
}
-inline std::filesystem::path DiscoverNandRootPath() {
+inline std::filesystem::path ResolveNandRootPath() {
const std::string configPath = RuntimeConfigFile::NandRoot();
if (!configPath.empty()) {
const auto path = ResolveConfiguredPath(configPath);
@@ -179,4 +180,16 @@ inline std::filesystem::path DiscoverNandRootPath() {
return CreateManagedNandRoot();
}
+inline std::filesystem::path DiscoverNandRootPath() {
+ static const auto root = [] {
+ const auto resolved = ResolveNandRootPath();
+ std::string error;
+ if (!RuntimeNandSettings::Ensure(resolved, error)) {
+ FailNandRoot(error.c_str(), RuntimeNandSettings::FilePath(resolved));
+ }
+ return resolved;
+ }();
+ return root;
+}
+
} // namespace RuntimeNandPath
diff --git a/runtime/include/nand_save_probe.h b/runtime/include/nand_save_probe.h
new file mode 100644
index 0000000..b00b7d8
--- /dev/null
+++ b/runtime/include/nand_save_probe.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include
+#include
+#include
+
+namespace RuntimeNandSave {
+
+enum class Contents { Missing, Blank, Nonzero, Error };
+enum class ReadAction { Proceed, Missing, Error, RecoveryNeeded };
+
+// A failed read is not evidence that a save is blank. Check badbit before EOF:
+// an I/O failure may set both, whereas a successful short final read sets EOF.
+inline Contents InspectStream(std::istream& input) {
+ if (!input) return Contents::Error;
+ char block[4096];
+ for (;;) {
+ input.read(block, sizeof(block));
+ if (input.bad() || (input.fail() && !input.eof())) return Contents::Error;
+ for (std::streamsize i = 0; i < input.gcount(); ++i) {
+ if (block[i] != 0) return Contents::Nonzero;
+ }
+ if (input.eof()) return Contents::Blank;
+ }
+}
+
+inline Contents InspectFile(const std::filesystem::path& path) {
+ std::error_code ec;
+ const auto status = std::filesystem::symlink_status(path, ec);
+ if (ec && ec != std::errc::no_such_file_or_directory) return Contents::Error;
+ if (!std::filesystem::exists(status)) return Contents::Missing;
+ if (!std::filesystem::is_regular_file(path, ec) || ec) return Contents::Error;
+ std::ifstream input(path, std::ios::binary);
+ return InspectStream(input);
+}
+
+// Probe only read-only opens of the actual save and its exact write shadow.
+// No probe writes, removes, or repairs data, and backups are not save aliases.
+inline ReadAction CheckRead(const std::filesystem::path& path, int mode) {
+ const auto name = path.filename();
+ const bool isMain = name == "rksys.dat";
+ if (mode != 1 || (!isMain && name != "rksys.dat.nandsafe.tmp")) return ReadAction::Proceed;
+ const auto contents = InspectFile(path);
+ if (contents == Contents::Error) return ReadAction::Error;
+ if (contents == Contents::Nonzero) return ReadAction::Proceed;
+ if (isMain) {
+ auto shadow = path;
+ shadow += ".nandsafe.tmp";
+ const auto shadowContents = InspectFile(shadow);
+ if (shadowContents == Contents::Error) return ReadAction::Error;
+ // The next write normally discards an old shadow. Preserve a possible
+ // recovery source when there is no usable original, without promoting
+ // an uncommitted (and potentially incomplete) shadow to the real save.
+ if (shadowContents == Contents::Nonzero) return ReadAction::RecoveryNeeded;
+ }
+ return contents == Contents::Blank ? ReadAction::Missing : ReadAction::Proceed;
+}
+
+} // namespace RuntimeNandSave
diff --git a/runtime/include/nand_settings.h b/runtime/include/nand_settings.h
new file mode 100644
index 0000000..055f4c2
--- /dev/null
+++ b/runtime/include/nand_settings.h
@@ -0,0 +1,220 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include