From 0d04be19d6ed5cba7d6841c17bd7e9199e59871f Mon Sep 17 00:00:00 2001
From: Michael G <10155689+DarthMDev@users.noreply.github.com>
Date: Sun, 13 Sep 2026 08:07:15 -0400
Subject: [PATCH] macos: add x86_64 Intel support (#122)
* macos: add x86_64 Intel support
* aurora: construct texture replacement index entries
* Switch back to LLVM 22 so build can succeed on at least some systems (#141)
two upstream LLVM bugs currently prevent building on some of the newest distros. There is no current LLVM release that works on them so we are pending fixes from LLVM
https://github.com/patchzyy/Wiicompiled/issues/136
* Fix GC Pocket+ rumble stop handling (#148)
* macos: package universal setup tools
* ci: build macOS input expression tests
* runtime: Do not force 14.0 minimum anymore
new minimum is 12.0
* Update network_socket.cpp
* fix(macos): increase guest fiber stack size (#154)
* v0.2.28
* Update settings_overlay.cpp
* macos: support older libc++ algorithms
* Load console identity from NAND setting.txt (#164)
* Add synthetic recompilation CI workflow (#161)
* version
* macos: allow undefined MTLLogStateDescriptor for older SDKs
* tests: deflake input expression timing window test
* import setting.txt (#169)
* import setting.txt
* coderabbit ugh
* Treat empty MKW save as missing rather than corrupt (#168)
* treat empty mkw save as missing
first-run format zero-fills rksys.dat before any real save; a quit before
the first save left an all-zero file that read back as corrupt and trapped
the user in a delete/recreate loop. read opens now treat an all-zero
rksys.dat as absent (a real save always begins with the RKSD0006 header),
so the game recreates it from scratch. also ignore native build output.
* shorten
* I dont really want to change this to be honest.
* extra safety
---------
Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
* version 0.2.30
* csnum fix
* update
* build: Fix mac tests
* translator: emit null statement after continuation labels for C++17 compatibility
* review fixes
---------
Co-authored-by: theofficialgman <28281419+theofficialgman@users.noreply.github.com>
Co-authored-by: Cristian Boehm <75502884+GalaxisBeast@users.noreply.github.com>
Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
Co-authored-by: Wubbzee <41394708+JGM01@users.noreply.github.com>
---
.github/workflows/build.yml | 10 +-
.github/workflows/package.yml | 75 ++++--
.github/workflows/recomp-test.yml | 41 ++++
.gitignore | 2 +
Launcher/Build-Installer.ps1 | 2 +-
Launcher/Prepare-NativePrebuilt.sh | 2 +-
Launcher/Test-Recompilation.ps1 | 146 ++++++++++++
.../WiiCompiled.Setup.Common.Cli.csproj | 2 +-
.../WiiCompiled.Setup.Common.csproj | 2 +-
Launcher/WiiCompiled.Setup.Linux/Models.cs | 2 +-
.../WiiCompiled.Setup.Linux.csproj | 2 +-
Launcher/WiiCompiled.Setup.Windows/Program.cs | 2 +-
.../WiiCompiled.Setup.Windows.csproj | 2 +-
Launcher/local-build-macos.command | 17 +-
Launcher/macos/build-setup-pkg.command | 83 +++++--
Launcher/macos/macos-x86_64-toolchain.cmake | 11 +
Launcher/macos/publish-app.command | 65 +++++-
Launcher/macos/setup.command | 16 +-
Launcher/prepare-portable-tools.sh | 38 +--
README.md | 4 +-
aurora-main/cmake/aurora_core.cmake | 4 +-
aurora-main/extern/CMakeLists.txt | 50 ++++
aurora-main/lib/dolphin/pad/pad.cpp | 49 ++--
aurora-main/lib/gfx/common.cpp | 5 +-
aurora-main/lib/gfx/texture_replacement.cpp | 2 +-
aurora-main/lib/input.cpp | 9 +
aurora-main/lib/input.hpp | 1 +
runtime/CMakeLists.txt | 122 +++++++---
runtime/cmake/PublicProducts.cmake | 10 +-
runtime/include/console_identity.h | 109 ++-------
runtime/include/host_context.h | 10 +-
runtime/include/nand_path.h | 15 +-
runtime/include/nand_save_probe.h | 59 +++++
runtime/include/nand_settings.h | 220 ++++++++++++++++++
runtime/include/sc_serial_contract.h | 26 +++
runtime/src/fiber_manager.cpp | 8 +-
runtime/src/hle/net/network_socket.cpp | 2 +-
runtime/src/hle/sc.cpp | 54 +++--
runtime/src/hle/storage/nand_api.cpp | 3 +
runtime/src/hle/storage/nand_async.cpp | 2 +
runtime/src/hle/storage/nand_fs.cpp | 20 ++
runtime/src/hle/storage/nand_internal.h | 7 +
runtime/src/hle/storage/nand_isfs.cpp | 3 +
runtime/src/host_context.cpp | 4 +-
runtime/src/main.cpp | 13 +-
runtime/src/music_attenuation.cpp | 22 +-
runtime/src/settings_overlay.cpp | 5 -
runtime/tests/nand_save_tests.cpp | 142 +++++++++++
runtime/tests/nand_settings_tests.cpp | 183 +++++++++++++++
runtime/tests/sc_serial_tests.cpp | 81 +++++++
runtime/tests/test_expr.cpp | 24 +-
.../CodeGen/CxxLinearCodeGenerator.cs | 4 +-
.../EmittedOutputShapeTests.cs | 33 ++-
53 files changed, 1526 insertions(+), 299 deletions(-)
create mode 100644 .github/workflows/recomp-test.yml
create mode 100644 Launcher/Test-Recompilation.ps1
create mode 100644 Launcher/macos/macos-x86_64-toolchain.cmake
create mode 100644 runtime/include/nand_save_probe.h
create mode 100644 runtime/include/nand_settings.h
create mode 100644 runtime/include/sc_serial_contract.h
create mode 100644 runtime/tests/nand_save_tests.cpp
create mode 100644 runtime/tests/nand_settings_tests.cpp
create mode 100644 runtime/tests/sc_serial_tests.cpp
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