2 Commits

Author SHA1 Message Date
Michael G 0d04be19d6 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>
2026-09-13 14:07:15 +02:00
Michael G c16f1533e5 (feat) Apple Silicon macOS CI building and Setup.pkg documentation (#118)
* docs(macos): document Setup.pkg installation

Add Apple Silicon macOS CI coverage for runtime configuration, substrate tests, and Setup.pkg packaging alongside the macOS installation instructions.

* macos: pin Apple Silicon deployment target

* fix(macos): restore Retro Rewind local builds
2026-09-05 10:13:55 +02:00
45 changed files with 1266 additions and 150 deletions
+35
View File
@@ -38,3 +38,38 @@ jobs:
- name: Test - name: Test
run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal
macos_substrate:
name: macOS arm64 (configure + substrate tests)
runs-on: macos-14
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Configure native runtime
shell: bash
run: |
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=12.0' \
build-macos/CMakeCache.txt
- name: Build macOS portability targets
shell: bash
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 \
mkw_macos_guest_flat_memory_tests
- name: Test execution substrate
shell: bash
run: ctest --test-dir build-macos --output-on-failure
+127 -6
View File
@@ -1,8 +1,8 @@
name: Package installers name: Package installers
# Builds the per-platform installer/setup tool (WiiCompiled-Setup.exe / # Builds the per-platform installer/setup tool (WiiCompiled-Setup.exe /
# WiiCompiled-Setup-x86_64.AppImage) via Launcher/Build-Installer.ps1 and # WiiCompiled-Setup-x86_64.AppImage / WiiCompiled-Setup.pkg) via the platform packaging scripts -
# Launcher/build-appimage.sh respectively - the same scripts a maintainer runs by hand today to # the same scripts a maintainer runs by hand today to
# produce a GitHub Release asset. This does NOT build the actual translated game executable: # produce a GitHub Release asset. This does NOT build the actual translated game executable:
# that step requires the end user's own Mario Kart Wii dump (Assets/main.dol, Assets/StaticR.rel), # that step requires the end user's own Mario Kart Wii dump (Assets/main.dol, Assets/StaticR.rel),
# which is proprietary and not present in this repository or in CI. # which is proprietary and not present in this repository or in CI.
@@ -11,6 +11,11 @@ on:
tags: tags:
- '*' - '*'
workflow_dispatch: workflow_dispatch:
inputs:
version:
description: Package version
required: true
type: string
permissions: permissions:
contents: read contents: read
@@ -90,14 +95,130 @@ jobs:
if-no-files-found: error if-no-files-found: error
archive: false archive: false
macos-setup-package:
name: macOS (universal Setup.pkg)
runs-on: macos-14
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '8.0.x'
- name: Verify Apple Silicon runner
shell: bash
run: |
test "$(uname -m)" = arm64
xcode-select -p
- name: Download pinned Nod tools
shell: bash
run: |
mkdir -p Launcher/artifacts/macos
nodtool_version=v2.0.0-alpha.10
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_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 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-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
run: |
cmake_version=4.4.3
archive="cmake-${cmake_version}-macos-universal.tar.gz"
base_url="https://github.com/Kitware/CMake/releases/download/v${cmake_version}"
expected_sha256=0c5d65251c14cc884bfa16bdbed3c263ce5bffe2e21c0d0d00962cb0610464fa
curl -fsSL --retry 3 "$base_url/$archive" -o "$archive"
printf '%s %s\n' "$expected_sha256" "$archive" | shasum -a 256 -c -
tar -xzf "$archive"
mv "cmake-${cmake_version}-macos-universal/CMake.app/Contents" Launcher/artifacts/macos/cmake
- name: Build Setup.pkg
env:
PACKAGE_VERSION: ${{ inputs.version }}
TAG_VERSION: ${{ github.ref_name }}
shell: bash
run: |
package_version="$PACKAGE_VERSION"
if [[ -z "$package_version" ]]; then package_version="${TAG_VERSION#v}"; fi
mkdir -p Launcher/dist
Launcher/macos/build-setup-pkg.command \
--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-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 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-universal
path: Launcher/dist/WiiCompiled-Setup.pkg
if-no-files-found: error
archive: false
# Publishes the packaged installers as a GitHub Release whenever a v* tag is pushed. Wheel Wizard # Publishes the packaged installers as a GitHub Release whenever a v* tag is pushed. Wheel Wizard
# discovers updates from these releases, so the contract it relies on is enforced here: a full # discovers updates from these releases, so the contract it relies on is enforced here: a full
# (non-prerelease) release whose tag is v<semver>, carrying an asset named exactly # (non-prerelease) release whose tag is v<semver>, carrying the expected platform assets,
# WiiCompiled-Setup.exe, produced by a setup host that reports that same version. # produced by setup hosts that report that same version.
release: release:
name: Publish GitHub Release name: Publish GitHub Release
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
needs: [linux-appimage, windows-installer, recompilation] needs: [linux-appimage, windows-installer, macos-setup-package, recompilation]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
@@ -143,7 +264,7 @@ jobs:
set -euo pipefail set -euo pipefail
ls -lR artifacts ls -lR artifacts
assets=() assets=()
for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage; do for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage WiiCompiled-Setup.pkg; do
found="$(find artifacts -type f -name "$name" | head -n 1)" found="$(find artifacts -type f -name "$name" | head -n 1)"
[ -n "$found" ] && [ -s "$found" ] || { echo "::error::missing release asset $name"; exit 1; } [ -n "$found" ] && [ -s "$found" ] || { echo "::error::missing release asset $name"; exit 1; }
assets+=("$found") assets+=("$found")
+2
View File
@@ -26,6 +26,8 @@ Code.pul
/build/ /build/
/build-*/ /build-*/
/native-build/ /native-build/
/native-build-macos/
/local-products/
/dist/ /dist/
/out/ /out/
[Bb]in/ [Bb]in/
+1 -1
View File
@@ -281,7 +281,7 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
$manifest = [ordered]@{ $manifest = [ordered]@{
SchemaVersion = 2 SchemaVersion = 2
ProductVersion = '0.2.29' ProductVersion = '0.2.31'
ExpectedGameId = $pins.GameId ExpectedGameId = $pins.GameId
ExpectedDolSha256 = $pins.DolSha256 ExpectedDolSha256 = $pins.DolSha256
ExpectedRelSha256 = $pins.RelSha256 ExpectedRelSha256 = $pins.RelSha256
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace> <RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName> <AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
<Version>0.2.29</Version> <Version>0.2.31</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description> <Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description>
@@ -5,7 +5,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace> <RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName> <AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
<Version>0.2.29</Version> <Version>0.2.31</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description> <Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description>
+1 -1
View File
@@ -3,7 +3,7 @@ namespace WiiCompiled.Setup.Linux;
internal static class ProductInfo internal static class ProductInfo
{ {
public const string Name = "WiiCompiled"; public const string Name = "WiiCompiled";
public const string Version = "0.2.29"; public const string Version = "0.2.31";
} }
/// <summary>One installed product's record inside install-state.json.</summary> /// <summary>One installed product's record inside install-state.json.</summary>
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyName>WiiCompiled.Setup.Linux</AssemblyName> <AssemblyName>WiiCompiled.Setup.Linux</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Linux</RootNamespace> <RootNamespace>WiiCompiled.Setup.Linux</RootNamespace>
<Version>0.2.29</Version> <Version>0.2.31</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled on Linux</Description> <Description>Command-line installer and launcher for WiiCompiled on Linux</Description>
@@ -121,7 +121,7 @@ internal static class PlatformChecks
internal static class ProductInfo internal static class ProductInfo
{ {
public const string Name = "WiiCompiled"; public const string Name = "WiiCompiled";
public const string Version = "0.2.29"; public const string Version = "0.2.31";
/// <summary> /// <summary>
/// The setup executable is copied into the installation under this name. It is the launcher and /// The setup executable is copied into the installation under this name. It is the launcher and
@@ -7,7 +7,7 @@
<AssemblyName>WiiCompiled.Setup</AssemblyName> <AssemblyName>WiiCompiled.Setup</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace> <RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.2.29</Version> <Version>0.2.31</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled</Description> <Description>Command-line installer and launcher for WiiCompiled</Description>
+9 -6
View File
@@ -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-rewind-package-dir DIR RetroRewind6 directory (required for Retro Rewind)
--retro-wfc-offline-dir DIR Directory containing binary/payload.RMCPD00.bin --retro-wfc-offline-dir DIR Directory containing binary/payload.RMCPD00.bin
--skip-retro-wfc-payload Build Retro Rewind without the shared Retro-WFC payload --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 --parallel N Pin translation and build parallelism
--cmake PATH --ninja PATH Override build tools --cmake PATH --ninja PATH Override build tools
--dotnet PATH Override dotnet --dotnet PATH Override dotnet
@@ -56,7 +56,9 @@ while (($#)); do
done done
[[ $(uname -s) == Darwin ]] || fail 'this build script is for macOS only' [[ $(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) workspace=$(cd "$workspace" && pwd)
[[ -n "$output_dir" ]] || fail '--output-dir is required' [[ -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 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" 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" 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' assert_file "$project" 'translation project'
if [[ -n "$game" ]]; then "$script_dir/macos/extract-disc.command" --game "$game" --assets-dir "$assets" --nodtool "$nodtool"; fi 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' assert_file "$assets/main.dol" 'extracted main.dol'; assert_file "$assets/StaticR.rel" 'extracted StaticR.rel'
@@ -131,9 +134,9 @@ if (( builds_retro )); then args+=(--resolved-profile "$mod_out/resolved_dispatc
step emit-build-shards 'Preparing native build shards'; translator "${args[@]}" step emit-build-shards 'Preparing native build shards'; translator "${args[@]}"
step configure-native 'Configuring the native toolchain' step configure-native 'Configuring the native toolchain'
"$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" "$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) 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" 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 [[ "$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"; 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" printf 'MKWCBUILD:OUTPUT=%s\n' "$output_dir"
+66 -18
View File
@@ -9,12 +9,15 @@ fail() { printf 'build-setup-pkg.command: error: %s\n' "$*" >&2; exit 1; }
copy_clean() { DITTONORSRC=1 ditto --norsrc --noqtn "$@"; } copy_clean() { DITTONORSRC=1 ditto --norsrc --noqtn "$@"; }
usage() { usage() {
cat <<'EOF' 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 Creates a game-code-free WiiCompiled Setup.pkg. The supplied tools must be
maintainer-verified, redistributable macOS arm64 artifacts. The resulting pkg maintainer-verified, redistributable macOS artifacts for both arm64 and
is unsigned unless --installer-identity is supplied; releases should sign and x86_64. The setup package selects native tools for its host while the game is
notarize it with a Developer ID Installer certificate. 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) --workspace DIR Repository root (default: script's grandparent)
--version VERSION Bundle/package version (default: 0.1.0) --version VERSION Bundle/package version (default: 0.1.0)
@@ -23,14 +26,17 @@ EOF
} }
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 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 while (($#)); do
case "$1" in case "$1" in
--workspace) workspace=${2:-}; shift 2 ;; --workspace) workspace=${2:-}; shift 2 ;;
--nodtool) nodtool=${2:-}; shift 2 ;; --nodtool-arm64) nodtool_arm64=${2:-}; shift 2 ;;
--translator) translator=${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 ;; --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 ;; --output) output=${2:-}; shift 2 ;;
--version) version=${2:-}; shift 2 ;; --version) version=${2:-}; shift 2 ;;
--installer-identity) identity=${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' [[ "$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" IFS=. read -r version_major version_minor version_patch <<< "$version"
short_version="$version_major.${version_minor:-0}.${version_patch:-0}" 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 for tool in pkgbuild productbuild ditto codesign lipo; do command -v "$tool" >/dev/null || fail "required macOS tool unavailable: $tool"; done
[[ -x "$nodtool" ]] || fail '--nodtool must name an executable' 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 "$translator" ]] || fail '--translator must name an executable'
[[ -x "$cmake_root/bin/cmake" ]] || fail '--cmake-root must contain bin/cmake' [[ -x "$cmake_root/bin/cmake" ]] || fail '--cmake-root must contain bin/cmake'
[[ -x "$ninja" ]] || fail '--ninja must name an executable' require_arch() {
"$nodtool" --version >/dev/null || fail '--nodtool did not run successfully' 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") workspace=$(cd "$workspace" && pwd); output=$(cd "$(dirname "$output")" && pwd)/$(basename "$output")
stage=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-pkg.XXXXXX") stage=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-pkg.XXXXXX")
trap 'rm -rf "$stage"' EXIT trap 'rm -rf "$stage"' EXIT
@@ -64,7 +106,7 @@ cat > "$app/Contents/Info.plist" <<EOF
<key>CFBundlePackageType</key><string>APPL</string> <key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>$short_version</string> <key>CFBundleShortVersionString</key><string>$short_version</string>
<key>CFBundleVersion</key><string>$version</string> <key>CFBundleVersion</key><string>$version</string>
<key>LSMinimumSystemVersion</key><string>14.0</string> <key>LSMinimumSystemVersion</key><string>12.0</string>
</dict></plist> </dict></plist>
EOF EOF
cat > "$app/Contents/MacOS/WiiCompiledSetup" <<'EOF' cat > "$app/Contents/MacOS/WiiCompiledSetup" <<'EOF'
@@ -103,11 +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/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" 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 chmod +x "$resources/workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/macos/"*.command
mkdir -p "$resources/tools/cmake" # setup.command uses this marker to refresh source inputs in an existing user
copy_clean "$nodtool" "$resources/tools/nodtool"; chmod +x "$resources/tools/nodtool" # workspace without replacing extracted game assets or Retro Rewind files.
copy_clean "$translator" "$resources/tools/Translator.Cli"; chmod +x "$resources/tools/Translator.Cli" printf '%s\n' "$version" > "$resources/workspace/.bundle-version"
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 "$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/LICENSE" "$resources/LICENSE"
copy_clean "$workspace/THIRD-PARTY-NOTICES.md" "$resources/THIRD-PARTY-NOTICES.md" copy_clean "$workspace/THIRD-PARTY-NOTICES.md" "$resources/THIRD-PARTY-NOTICES.md"
codesign --force --deep --sign - "$app" codesign --force --deep --sign - "$app"
@@ -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)
+55 -10
View File
@@ -5,28 +5,36 @@ set -euo pipefail
fail() { printf 'publish-app.command: error: %s\n' "$*" >&2; exit 1; } fail() { printf 'publish-app.command: error: %s\n' "$*" >&2; exit 1; }
usage() { usage() {
cat <<'EOF' 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/<product>.app. Copies a locally built product and its runtime assets into OUTPUT-DIR/<product>.app.
It bundles non-system dylibs, rewrites their install names, and ad-hoc signs the 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 result. This is suitable for local use; a release must replace ad-hoc signing
with the project's Developer ID signing and notarization process. 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 EOF
} }
build_dir=""; product=""; output_dir="" build_dir=""; product=""; output_dir=""; architecture=$(uname -m); minimum_system_version=12.0
while (($#)); do while (($#)); do
case "$1" in case "$1" in
--build-dir) build_dir=${2:-}; shift 2 ;; --build-dir) build_dir=${2:-}; shift 2 ;;
--product) product=${2:-}; shift 2 ;; --product) product=${2:-}; shift 2 ;;
--output-dir) output_dir=${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 ;; -h|--help) usage; exit 0 ;;
*) fail "unknown option: $1" ;; *) fail "unknown option: $1" ;;
esac esac
done done
[[ "$product" == WiiCompiled || "$product" == RetroRewind ]] || fail '--product must be WiiCompiled or RetroRewind' [[ "$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" [[ -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 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" app="$output_dir/$product.app"
@@ -47,7 +55,7 @@ cat > "$app/Contents/Info.plist" <<EOF
<key>CFBundlePackageType</key><string>APPL</string> <key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>0.1.0</string> <key>CFBundleShortVersionString</key><string>0.1.0</string>
<key>CFBundleVersion</key><string>1</string> <key>CFBundleVersion</key><string>1</string>
<key>LSMinimumSystemVersion</key><string>14.0</string> <key>LSMinimumSystemVersion</key><string>$minimum_system_version</string>
<key>NSHighResolutionCapable</key><true/> <key>NSHighResolutionCapable</key><true/>
</dict></plist> </dict></plist>
EOF EOF
@@ -57,25 +65,62 @@ for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do
ln -s "../Resources/$asset" "$macos/$asset" ln -s "../Resources/$asset" "$macos/$asset"
done 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") queue=("$macos/$product")
while ((${#queue[@]})); do while ((${#queue[@]})); do
current=${queue[0]} current=${queue[0]}
queue=("${queue[@]:1}") queue=("${queue[@]:1}")
while IFS= read -r dependency; do while IFS= read -r dependency; do
[[ "$dependency" == /opt/homebrew/* || "$dependency" == /usr/local/* ]] || continue dependency_path=$(dependency_path "$current" "$dependency") || continue
[[ -f "$dependency" ]] || continue
name=$(basename "$dependency") name=$(basename "$dependency")
if [[ ! -f "$frameworks/$name" ]]; then if [[ ! -f "$frameworks/$name" ]]; then
ditto "$dependency" "$frameworks/$name" ditto "$dependency_path" "$frameworks/$name"
install_name_tool -id "@rpath/$name" "$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 fi
done < <(otool -L "$current" | tail -n +2 | awk '{print $1}') done < <(otool -L "$current" | tail -n +2 | awk '{print $1}')
done done
while IFS= read -r binary; do while IFS= read -r binary; do
while IFS= read -r old; do while IFS= read -r old; do
[[ "$old" == /opt/homebrew/* || "$old" == /usr/local/* ]] || continue
name=$(basename "$old") name=$(basename "$old")
[[ -f "$frameworks/$name" ]] || continue [[ -f "$frameworks/$name" ]] || continue
if [[ "$binary" == "$macos/$product" ]]; then if [[ "$binary" == "$macos/$product" ]]; then
+34 -6
View File
@@ -5,10 +5,7 @@ set -euo pipefail
resources=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) resources=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace_source="$resources/workspace" workspace_source="$resources/workspace"
nodtool="$resources/tools/nodtool"
translator="$resources/tools/Translator.Cli"
cmake_bin="$resources/tools/cmake/bin/cmake" cmake_bin="$resources/tools/cmake/bin/cmake"
ninja_bin="$resources/tools/ninja"
support_root="$HOME/Library/Application Support/WiiCompiled" support_root="$HOME/Library/Application Support/WiiCompiled"
workspace="$support_root/BuildWorkspace" workspace="$support_root/BuildWorkspace"
products="$support_root/Products" products="$support_root/Products"
@@ -36,6 +33,13 @@ while (($#)); do
done done
[[ "$install_location" == user || "$install_location" == applications ]] || fail '--install-location must be user or applications' [[ "$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 if [[ -z "$game" ]]; then
game=$(/usr/bin/osascript <<'APPLESCRIPT' game=$(/usr/bin/osascript <<'APPLESCRIPT'
set selectedFile to choose file with prompt "Choose your clean Mario Kart Wii PAL (RMCP01) disc image" set selectedFile to choose file with prompt "Choose your clean Mario Kart Wii PAL (RMCP01) disc image"
@@ -60,12 +64,36 @@ if ! /usr/bin/xcode-select -p >/dev/null 2>&1; then
/usr/bin/xcode-select --install || true /usr/bin/xcode-select --install || true
exit 1 exit 1
fi 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" mkdir -p "$support_root" "$products"
if [[ ! -d "$workspace/.git" && ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then source_bundle_version="$workspace_source/.bundle-version"
workspace_bundle_version="$workspace/.bundle-version"
needs_workspace_refresh=0
if [[ ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then
needs_workspace_refresh=1
elif [[ -f "$source_bundle_version" ]] && [[ ! -f "$workspace_bundle_version" || "$(<"$source_bundle_version")" != "$(<"$workspace_bundle_version")" ]]; then
needs_workspace_refresh=1
fi
if (( needs_workspace_refresh )); then
printf 'Preparing the local build workspace...\n' printf 'Preparing the local build workspace...\n'
rm -rf "$workspace" if [[ ! -d "$workspace" ]]; then
/usr/bin/ditto "$workspace_source" "$workspace" /usr/bin/ditto "$workspace_source" "$workspace"
else
# Refresh only packaged source inputs. Assets and the staged Retro
# Rewind package belong to the user and stay in place.
for source in aurora-main projects runtime translator Launcher; do
/usr/bin/ditto "$workspace_source/$source" "$workspace/$source"
done
/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-arm64" "$workspace/native-build-macos-x86_64"
fi
fi fi
profile=base profile=base
+17 -2
View File
@@ -89,8 +89,8 @@ Known limitations of the Wii Remote path:
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher - GPU: GTX 1650 / RX 6400 / Arc A310 or higher
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) 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) - About 20 GB of free disk space during installation (Final game size ~5 GB)
- macOS 14 (Sonoma) or later on Apple Silicon - macOS 12 (Monterey) or later on Apple Silicon (`arm64`) or Intel (`x86_64-v3`); pre-Haswell Intel CPUs are unsupported
- On macOS, Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing) - 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, - A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
GCZ, CISO, WBFS, WIA and RVZ are accepted. GCZ, CISO, WBFS, WIA and RVZ are accepted.
@@ -111,6 +111,21 @@ image under Settings, turn on **WiiCompiled (beta)**, and hit install from the H
Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it. launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
### macOS
Download `WiiCompiled-Setup.pkg` from this repository's Releases page and open it. It requires an
Apple Silicon Mac because its bundled nodtool and Translator.Cli executables are arm64. It installs
**WiiCompiled Setup** in Applications; open that app, choose your clean PAL `RMCP01` disc image,
and select either the base game or Retro Rewind. For Retro Rewind, choose the `RetroRewind6` folder
or its parent folder.
Setup verifies and extracts the image locally, then translates and compiles the native app on your
Mac. On a first run it may ask macOS to install Xcode Command Line Tools; complete Apple's installer,
then open Setup again. When the build completes, Setup asks for administrator approval once to install
`WiiCompiled.app` (and, if selected, `RetroRewind.app`) in `/Applications`.
Setup opens Terminal while it works, so the extraction and build progress—and any error that needs
reporting—remain visible.
> [!CAUTION] > [!CAUTION]
> Only take builds from this repository's > Only take builds from this repository's
+5 -2
View File
@@ -41,7 +41,10 @@ if (_aurora_sdl3_provider STREQUAL "auto")
set(_aurora_sdl3_provider "package") set(_aurora_sdl3_provider "package")
else () else ()
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON) set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
find_package(SDL3 QUIET) # Aurora uses APIs from the SDL version pinned by AURORA_SDL3_VERSION.
# Do not silently select an older system package and fail later while
# compiling its headers.
find_package(SDL3 ${AURORA_SDL3_VERSION} QUIET)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF) set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
if (SDL3_FOUND) if (SDL3_FOUND)
set(_aurora_sdl3_provider "system") set(_aurora_sdl3_provider "system")
@@ -58,7 +61,7 @@ if (_aurora_sdl3_provider STREQUAL "system")
message(STATUS "aurora: Using system SDL3 (provider=system)") message(STATUS "aurora: Using system SDL3 (provider=system)")
if (NOT SDL3_FOUND) if (NOT SDL3_FOUND)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON) set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
find_package(SDL3 REQUIRED) find_package(SDL3 ${AURORA_SDL3_VERSION} REQUIRED)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF) set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
endif () endif ()
_aurora_sdl3_select_target() _aurora_sdl3_select_target()
+3 -1
View File
@@ -41,7 +41,9 @@ if (AURORA_ENABLE_GX)
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_METAL) target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_METAL)
target_sources(aurora_core PRIVATE lib/dawn/MetalBinding.mm) target_sources(aurora_core PRIVATE lib/dawn/MetalBinding.mm)
set_source_files_properties(lib/dawn/MetalBinding.mm PROPERTIES COMPILE_FLAGS -fobjc-arc) 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 () endif ()
if (DAWN_ENABLE_D3D11) if (DAWN_ENABLE_D3D11)
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_D3D11) target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_D3D11)
+50
View File
@@ -16,10 +16,60 @@ function(aurora_find_package_global)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ${_PREV_FIND_PACKAGE_TARGETS_GLOBAL}) set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ${_PREV_FIND_PACKAGE_TARGETS_GLOBAL})
endfunction() 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) if (AURORA_ENABLE_GX)
include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/AuroraDawnProvider.cmake) include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/AuroraDawnProvider.cmake)
endif () 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. # 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. # 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) if (NOT TARGET absl::flat_hash_map OR NOT TARGET absl::btree)
+30 -24
View File
@@ -6,10 +6,10 @@
#include <SDL3/SDL_mouse.h> #include <SDL3/SDL_mouse.h>
#include <SDL3/SDL_joystick.h> #include <SDL3/SDL_joystick.h>
#include <algorithm>
#include <array> #include <array>
#include <atomic> #include <atomic>
#include <sys/stat.h> #include <sys/stat.h>
#include <ranges>
namespace { namespace {
constexpr int32_t k_mappingsFileVersion = 3; constexpr int32_t k_mappingsFileVersion = 3;
@@ -342,7 +342,7 @@ BOOL PADInit() {
} }
g_initialized = true; 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_buttonMapping = g_defaultKeys;
state.m_axisMapping = g_defaultKeyAxis; 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) static Sint16 _get_axis_value(const aurora::input::GameController* controller, // NOLINT(*-reserved-identifier)
PADAxis axis) { PADAxis axis) {
const auto iter = const auto iter = std::find_if(controller->m_axisMapping.begin(), controller->m_axisMapping.end(),
std::ranges::find_if(controller->m_axisMapping, [axis](const auto& pair) { return pair.padAxis == axis; }); [axis](const auto& pair) { return pair.padAxis == axis; });
if (iter == controller->m_axisMapping.end()) { if (iter == controller->m_axisMapping.end()) {
return 0; return 0;
} }
@@ -725,8 +725,8 @@ u32 PADRead(PADStatus* status) {
status[i].err = PAD_ERR_NONE; status[i].err = PAD_ERR_NONE;
if (g_keyboardBindings[i].m_mappingsSet) { if (g_keyboardBindings[i].m_mappingsSet) {
std::ranges::for_each( std::for_each(g_keyboardBindings[i].m_buttonMapping.begin(), g_keyboardBindings[i].m_buttonMapping.end(),
g_keyboardBindings[i].m_buttonMapping, [&kbState, &i, &status](const PADKeyButtonBinding& mapping) { [&kbState, &i, &status](const PADKeyButtonBinding& mapping) {
if (mapping.scancode > PAD_KEY_INVALID && kbState[mapping.scancode]) { if (mapping.scancode > PAD_KEY_INVALID && kbState[mapping.scancode]) {
status[i].button |= mapping.padButton; status[i].button |= mapping.padButton;
} else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) { } 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 leftTriggerSet = false;
bool rightTriggerSet = false; bool rightTriggerSet = false;
std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet, std::for_each(controller->m_buttonMapping.begin(), controller->m_buttonMapping.end(),
&rightTriggerSet](const auto& mapping) { [&controller, &i, &status, &leftTriggerSet, &rightTriggerSet](const auto& mapping) {
if (SDL_GetGamepadButton(controller->m_controller, static_cast<SDL_GamepadButton>(mapping.nativeButton))) { if (SDL_GetGamepadButton(controller->m_controller, static_cast<SDL_GamepadButton>(mapping.nativeButton))) {
status[i].button |= mapping.padButton; status[i].button |= mapping.padButton;
} }
@@ -847,8 +847,8 @@ u32 PADRead(PADStatus* status) {
} }
}); });
std::ranges::for_each(controller->m_altButtonMapping, [&controller, &i, &status, &leftTriggerSet, std::for_each(controller->m_altButtonMapping.begin(), controller->m_altButtonMapping.end(),
&rightTriggerSet](const auto& mapping) { [&controller, &i, &status, &leftTriggerSet, &rightTriggerSet](const auto& mapping) {
if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) { if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) {
return; return;
} }
@@ -1165,8 +1165,8 @@ void PADSetButtonMapping(const u32 port, const PADButtonMapping mapping) {
return; return;
} }
const auto iter = std::ranges::find_if(controller->m_buttonMapping, const auto iter = std::find_if(controller->m_buttonMapping.begin(), controller->m_buttonMapping.end(),
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; }); [mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
if (iter == controller->m_buttonMapping.end()) { if (iter == controller->m_buttonMapping.end()) {
return; return;
} }
@@ -1199,8 +1199,8 @@ void PADSetAltButtonMapping(const u32 port, const PADButtonMapping mapping) {
return; return;
} }
const auto iter = std::ranges::find_if(controller->m_altButtonMapping, const auto iter = std::find_if(controller->m_altButtonMapping.begin(), controller->m_altButtonMapping.end(),
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; }); [mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
if (iter == controller->m_altButtonMapping.end()) { if (iter == controller->m_altButtonMapping.end()) {
return; return;
} }
@@ -1226,8 +1226,8 @@ void PADSetAxisMapping(const u32 port, const PADAxisMapping mapping) {
return; return;
} }
const auto iter = std::ranges::find_if(controller->m_axisMapping, const auto iter = std::find_if(controller->m_axisMapping.begin(), controller->m_axisMapping.end(),
[mapping](const auto& pair) { return mapping.padAxis == pair.padAxis; }); [mapping](const auto& pair) { return mapping.padAxis == pair.padAxis; });
if (iter == controller->m_axisMapping.end()) { if (iter == controller->m_axisMapping.end()) {
return; return;
} }
@@ -1396,9 +1396,10 @@ static void load_keyboard_bindings() {
if (mappingsSet) { if (mappingsSet) {
const bool anyBound = const bool anyBound =
std::ranges::any_of(buttonMapping, std::any_of(buttonMapping.begin(), buttonMapping.end(),
[](const PADKeyButtonBinding& b) { return b.scancode != PAD_KEY_INVALID; }) || [](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(axisMapping.begin(), axisMapping.end(),
[](const PADKeyAxisBinding& b) { return b.scancode != PAD_KEY_INVALID; });
if (!anyBound) { if (!anyBound) {
mappingsSet = false; mappingsSet = false;
} }
@@ -1438,7 +1439,10 @@ void __PADWriteDeadZones(SDL_IOStream* file, // NOLINT(*-reserved-identifier)
void PADSerializeMappings() { void PADSerializeMappings() {
const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath); const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath);
for (auto& controller : aurora::input::g_GameControllers | std::views::values) { // Avoid std::views::values here: older Apple libc++ releases implement the
// C++20 ranges algorithms we use but not this adaptor.
for (auto& entry : aurora::input::g_GameControllers) {
auto& controller = entry.second;
EnsureMappingLoaded(&controller); EnsureMappingLoaded(&controller);
const auto filePath = const auto filePath =
basePath / fmt::format("{}_{:04X}_{:04X}.controller", aurora::input::controller_name(controller.m_index), basePath / fmt::format("{}_{:04X}_{:04X}.controller", aurora::input::controller_name(controller.m_index),
@@ -1540,8 +1544,8 @@ static constexpr std::array<std::pair<PADButton, std::string_view>, PAD_AXIS_COU
const char* PADGetButtonName(const PADButton button) { const char* PADGetButtonName(const PADButton button) {
if (const auto iter = if (const auto iter = std::find_if(skButtonNames.begin(), skButtonNames.end(),
std::ranges::find_if(skButtonNames, [&button](const auto& pair) { return button == pair.first; }); [&button](const auto& pair) { return button == pair.first; });
iter != skButtonNames.end()) { iter != skButtonNames.end()) {
return iter->second.data(); return iter->second.data();
} }
@@ -1554,7 +1558,8 @@ const char* PADGetNativeButtonName(u32 button) {
} }
const char* PADGetAxisName(const PADAxis axis) { 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()) { it != skAxisNames.end()) {
return it->second.data(); return it->second.data();
} }
@@ -1563,7 +1568,8 @@ const char* PADGetAxisName(const PADAxis axis) {
} }
const char* PADGetAxisDirectionLabel(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()) { it != skAxisDirLabels.end()) {
return it->second.data(); return it->second.data();
} }
+2 -3
View File
@@ -22,7 +22,6 @@
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <optional> #include <optional>
#include <ranges>
#include <absl/container/flat_hash_map.h> #include <absl/container/flat_hash_map.h>
#include <magic_enum.hpp> #include <magic_enum.hpp>
@@ -1328,8 +1327,8 @@ static void render_impl(std::vector<RenderPass>& renderPasses, wgpu::CommandEnco
#if defined(AURORA_GFX_DEBUG_GROUPS) #if defined(AURORA_GFX_DEBUG_GROUPS)
if (finalize && !g_debugGroupStack.empty()) { if (finalize && !g_debugGroupStack.empty()) {
for (auto& it : std::ranges::reverse_view(g_debugGroupStack)) { for (auto it = g_debugGroupStack.rbegin(); it != g_debugGroupStack.rend(); ++it) {
Log.warn("Debug group was not popped at end of frame: {}", it); Log.warn("Debug group was not popped at end of frame: {}", *it);
} }
g_debugGroupStack.clear(); g_debugGroupStack.clear();
} }
+1 -1
View File
@@ -553,7 +553,7 @@ void build_index() noexcept {
continue; continue;
} }
s_replacementIndex.try_emplace(*parsed, path); s_replacementIndex.try_emplace(*parsed, ReplacementIndexEntry{path});
} }
Log.info("Indexed {} texture replacements", s_replacementIndex.size()); Log.info("Indexed {} texture replacements", s_replacementIndex.size());
+1 -1
View File
@@ -235,7 +235,7 @@ std::string GetOSVersion() {
constexpr auto name = "iOS"; constexpr auto name = "iOS";
#elif TARGET_OS_TV #elif TARGET_OS_TV
constexpr auto name = "tvOS"; constexpr auto name = "tvOS";
#elif #else
constexpr auto name = Unknown; constexpr auto name = Unknown;
#endif #endif
+82 -27
View File
@@ -1,22 +1,44 @@
cmake_minimum_required(VERSION 3.16) 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) project(mkw_recompiled)
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8) 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") message(FATAL_ERROR "WiiCompiled requires a 64-bit Clang toolchain")
endif() 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)$") if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_PLATFORM_WINDOWS TRUE) set(MKW_PLATFORM_WINDOWS TRUE)
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64)$") elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|arm64|ARM64)$")
# The first native macOS target is Apple Silicon. Intel and universal
# binaries remain future compatibility work; do not silently claim them.
set(MKW_PLATFORM_MACOS TRUE) 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)$") 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) set(MKW_PLATFORM_LINUX TRUE)
else() else()
message(FATAL_ERROR message(FATAL_ERROR
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS arm64") "WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS x86_64/arm64")
endif() endif()
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release") if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
message(FATAL_ERROR "WiiCompiled only supports Release builds") message(FATAL_ERROR "WiiCompiled only supports Release builds")
endif() endif()
@@ -59,14 +81,15 @@ target_include_directories(mkw_pugixml PUBLIC third_party/pugixml)
target_compile_features(mkw_pugixml PUBLIC cxx_std_17) target_compile_features(mkw_pugixml PUBLIC cxx_std_17)
set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF) 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 # 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 # directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
# asymmetric resume/yield coroutine libraries, which would need every call site restructured). # 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 # 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 # 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. # Apple Silicon uses the project's x18-safe AArch64 assembly backend, while Intel macOS uses
if(MKW_PLATFORM_LINUX) # 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 STATIC third_party/libco/libco.c)
add_library(mkw::libco ALIAS mkw_libco) add_library(mkw::libco ALIAS mkw_libco)
target_include_directories(mkw_libco PUBLIC third_party/libco) target_include_directories(mkw_libco PUBLIC third_party/libco)
@@ -243,12 +266,14 @@ endif()
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp") file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
if(MKW_PLATFORM_MACOS) if(MKW_PLATFORM_MACOS)
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory.cpp") list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory.cpp")
# HostContext's Apple Silicon backend is implemented in a small assembly if(MKW_PLATFORM_MACOS_ARM64)
# companion. It must be part of the product runtime as well as the # HostContext's Apple Silicon backend is implemented in a small assembly
# standalone context test; otherwise the final executable is missing # companion. It must be part of the product runtime as well as the
# mkw_co_init/mkw_co_switch at link time. # standalone context test; otherwise the final executable is missing
enable_language(ASM) # mkw_co_init/mkw_co_switch at link time.
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S") enable_language(ASM)
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
endif()
else() else()
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp") list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
endif() endif()
@@ -277,11 +302,23 @@ target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17) target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests) 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") 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_include_directories(mkw_nand_settings_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17) target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests) 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 # The input expression engine is self-contained, so it can be exercised without
# linking the runtime or SDL. # linking the runtime or SDL.
add_executable(mkw_input_expr_tests add_executable(mkw_input_expr_tests
@@ -307,20 +344,31 @@ if(MKW_PLATFORM_LINUX)
endif() endif()
if(MKW_PLATFORM_MACOS) if(MKW_PLATFORM_MACOS)
# Exercise the Apple Silicon context ABI and the public host-memory # Exercise the public host-memory contracts separately from translated products.
# contracts separately from translated products. if(MKW_PLATFORM_MACOS_ARM64)
enable_language(ASM) # Apple Silicon's context ABI is implemented by the local assembly backend.
add_executable(mkw_macos_context_abi_tests enable_language(ASM)
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp" add_executable(mkw_macos_context_abi_tests
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S") "${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17) "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests) 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 add_executable(mkw_macos_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp" "${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp" "${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S") "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include") 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) 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) add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
@@ -379,6 +427,13 @@ else()
target_link_libraries(mkw_macos_native_compile PRIVATE target_link_libraries(mkw_macos_native_compile PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
mkw::pugixml mkw::toml11 mkw::cryptopp) 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) set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
endif() endif()
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform) add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
+6 -4
View File
@@ -1,4 +1,4 @@
# Public WiiCompiled product graph. # Public WiiCompiled product graph.
# #
# The translator owns the translated build graph. Mario Kart's profile-neutral # The translator owns the translated build graph. Mario Kart's profile-neutral
# functions are compiled once into mkw_base_shared; only callers whose direct # 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) target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
if(MKW_PLATFORM_WINDOWS) if(MKW_PLATFORM_WINDOWS)
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp) 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 # ${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}) target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS})
endif() endif()
if(MKW_CPPWINRT_INCLUDE_DIR) if(MKW_CPPWINRT_INCLUDE_DIR)
@@ -206,6 +206,8 @@ function(mkw_configure_product target)
if(MKW_PLATFORM_MACOS) if(MKW_PLATFORM_MACOS)
target_link_libraries(${target} PRIVATE target_link_libraries(${target} PRIVATE
"${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}") "${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}")
target_link_options(${target} PRIVATE
"LINKER:-U,_OBJC_CLASS_$_MTLLogStateDescriptor")
endif() endif()
if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake") if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
include("${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) dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE) 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 # mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o
# files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own # files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own
# target_link_libraries (object libraries don't carry usage requirements to a consumer # target_link_libraries (object libraries don't carry usage requirements to a consumer
+5 -5
View File
@@ -3,11 +3,11 @@
#include <cstddef> #include <cstddef>
// HostContext is the deliberately small boundary between the guest scheduler // HostContext is the deliberately small boundary between the guest scheduler
// and the host's cooperative-context facility. Windows uses native Fibers and // and the host's cooperative-context facility. Windows uses native Fibers;
// Linux uses libco; macOS AArch64 uses the local assembly backend because it // Linux and Intel macOS use libco's System V x86-64 backend. macOS AArch64 uses
// must preserve Darwin's platform-reserved x18 register, which libco's AArch64 // the local assembly backend because it must preserve Darwin's platform-reserved
// backend does not save. Its handles are only valid on the thread that // x18 register, which libco's AArch64 backend does not save. Its handles are
// initialized the scheduler. // only valid on the thread that initialized the scheduler.
namespace HostContext { namespace HostContext {
using Handle = void*; using Handle = void*;
+14 -1
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "runtime_config.h" #include "runtime_config.h"
#include "nand_settings.h"
#include "runtime_log.h" #include "runtime_log.h"
#include "system_bridge.h" #include "system_bridge.h"
@@ -163,7 +164,7 @@ inline std::filesystem::path CreateManagedNandRoot() {
return root; return root;
} }
inline std::filesystem::path DiscoverNandRootPath() { inline std::filesystem::path ResolveNandRootPath() {
const std::string configPath = RuntimeConfigFile::NandRoot(); const std::string configPath = RuntimeConfigFile::NandRoot();
if (!configPath.empty()) { if (!configPath.empty()) {
const auto path = ResolveConfiguredPath(configPath); const auto path = ResolveConfiguredPath(configPath);
@@ -179,4 +180,16 @@ inline std::filesystem::path DiscoverNandRootPath() {
return CreateManagedNandRoot(); 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 } // namespace RuntimeNandPath
+59
View File
@@ -0,0 +1,59 @@
#pragma once
#include <filesystem>
#include <fstream>
#include <istream>
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
+153 -2
View File
@@ -1,6 +1,9 @@
#pragma once #pragma once
#include <array> #include <array>
#include <atomic>
#include <chrono>
#include <ctime>
#include <cstdint> #include <cstdint>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
@@ -9,14 +12,23 @@
#include <string> #include <string>
#include <utility> #include <utility>
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif
namespace RuntimeNandSettings { namespace RuntimeNandSettings {
using Settings = std::map<std::string, std::string>; using Settings = std::map<std::string, std::string>;
inline std::filesystem::path FilePath(const std::filesystem::path& root) {
return root / "title/00000001/00000002/data/setting.txt";
}
// Wii setting.txt is a 256-byte buffer encrypted with a rotating XOR key. // Wii setting.txt is a 256-byte buffer encrypted with a rotating XOR key.
inline std::optional<Settings> Read(const std::filesystem::path& nandRoot) { inline std::optional<Settings> Read(const std::filesystem::path& nandRoot) {
std::ifstream input(nandRoot / "title/00000001/00000002/data/setting.txt", std::ifstream input(FilePath(nandRoot), std::ios::binary);
std::ios::binary);
std::array<uint8_t, 256> bytes{}; std::array<uint8_t, 256> bytes{};
if (!input.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) { if (!input.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) {
return std::nullopt; return std::nullopt;
@@ -66,4 +78,143 @@ inline bool HasIdentity(const Settings& settings) {
return true; return true;
} }
// Dolphin's normal (non-deterministic) first-boot algorithm. It is independent
// of the ES device ID. Matching another NAND requires that NAND's saved serial.
inline std::string GenerateSerial(std::time_t now) {
if (now < 0) {
return {};
}
const auto digits = std::to_string(now % 1000000000);
return std::string(9 - digits.size(), '0') + digits;
}
// This recompilation targets the European disc. These are Dolphin's PAL boot
// defaults; an existing setting.txt always takes precedence, in every region.
inline std::optional<std::array<uint8_t, 256>> EncodeNew(const std::string& serial) {
const Settings identity{{"SERNO", serial}, {"CODE", "LEH"}, {"AREA", "EUR"}, {"GAME", "EU"}};
if (!HasIdentity(identity)) {
return std::nullopt;
}
std::array<uint8_t, 256> bytes{};
size_t position = 0;
uint32_t key = 0x73B5DBFAu;
const auto writeByte = [&](char value) {
bytes[position++] = static_cast<uint8_t>(value) ^ static_cast<uint8_t>(key);
key = (key << 1) | (key >> 31);
};
for (const std::string& line : {std::string("AREA=EUR\r\n"), std::string("MODEL=RVL-001(EUR)\r\n"),
std::string("DVD=0\r\n"), std::string("MPCH=0x7FFE\r\n"), std::string("CODE=LEH\r\n"),
"SERNO=" + serial + "\r\n", std::string("VIDEO=PAL\r\n"), std::string("GAME=EU\r\n")}) {
for (;;) {
if (position + line.size() > bytes.size()) {
return std::nullopt;
}
const auto start = position;
const auto savedKey = key;
bool hasNull = false;
for (const char value : line) {
writeByte(value);
hasNull |= bytes[position - 1] == 0;
}
if (!hasNull) {
break;
}
// Nintendo stops at an encoded NUL. Dolphin inserts an extra LF
// before this line and retries with the shifted encryption key.
position = start;
key = savedKey;
writeByte('\n');
}
}
return bytes; // The unused tail stays raw zero, as in Dolphin.
}
// Atomically claim our own scratch directory. A collision belongs to another
// launch (or a previous crashed launch); leave it untouched and try another name.
inline std::optional<std::filesystem::path> CreateScratchDirectory(
const std::filesystem::path& parent, const std::string& token, std::error_code& ec) {
for (unsigned attempt = 0; attempt < 128; ++attempt) {
const auto candidate = parent / (".setting-init-" + token + "-" + std::to_string(attempt));
ec.clear();
if (std::filesystem::create_directory(candidate, ec)) return candidate;
if (ec && ec != std::errc::file_exists) return std::nullopt;
}
ec = std::make_error_code(std::errc::file_exists);
return std::nullopt;
}
// Never replace an existing file, including an unreadable or damaged one.
// Publish a complete file atomically so simultaneous launches use one identity.
inline bool Ensure(const std::filesystem::path& root, std::string& error,
std::time_t now = std::time(nullptr)) {
const auto path = FilePath(root);
std::error_code ec;
const auto status = std::filesystem::symlink_status(path, ec);
if (ec && ec != std::errc::no_such_file_or_directory) {
error = "Cannot inspect NAND setting.txt: " + ec.message();
return false;
}
if (std::filesystem::exists(status)) {
const auto existing = Read(root);
if (existing && HasIdentity(*existing)) {
return true;
}
error = "Existing NAND setting.txt is unreadable or invalid; restore it from this console's backup";
return false;
}
const auto bytes = EncodeNew(GenerateSerial(now));
if (!bytes) {
error = "Cannot initialize NAND settings: invalid system clock";
return false;
}
ec.clear();
std::filesystem::create_directories(path.parent_path(), ec);
if (ec) {
error = "Cannot create NAND settings directory: " + ec.message();
return false;
}
static std::atomic<unsigned> sequence{0};
#ifdef _WIN32
const auto processId = GetCurrentProcessId();
#else
const auto processId = getpid();
#endif
const auto scratch = CreateScratchDirectory(path.parent_path(),
std::to_string(processId) + "-" + std::to_string(
std::chrono::steady_clock::now().time_since_epoch().count()) + "-" +
std::to_string(sequence++), ec);
if (!scratch) {
error = "Cannot create temporary NAND settings directory: " + ec.message();
return false;
}
const auto temporary = *scratch / "setting.txt";
bool written = false;
{
std::ofstream output(temporary, std::ios::binary);
output.write(reinterpret_cast<const char*>(bytes->data()), bytes->size());
output.close();
written = static_cast<bool>(output);
}
bool published = false;
if (written) {
#ifdef _WIN32
published = MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_WRITE_THROUGH) != 0;
#else
published = ::link(temporary.c_str(), path.c_str()) == 0;
#endif
}
std::filesystem::remove(temporary, ec);
std::filesystem::remove(*scratch, ec);
// A competing launcher may have published its settings first. Always read
// the winner from NAND rather than using our unpersisted candidate serial.
const auto persisted = Read(root);
if (persisted && HasIdentity(*persisted)) {
return true;
}
error = published ? "Cannot read newly initialized NAND setting.txt" :
"Cannot persist NAND setting.txt; check NAND directory permissions";
return false;
}
} // namespace RuntimeNandSettings } // namespace RuntimeNandSettings
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <charconv>
#include <cstddef>
#include <cstdint>
#include <string_view>
#include <system_error>
namespace RuntimeScSerial {
// SCGetProductSN's output is a u32, not a character buffer. DWC loads
// that word and formats it with the product code to construct csnum.
template <typename RangeValidator, typename WordWriter>
uint32_t Write(std::string_view serial, uint32_t address,
RangeValidator&& contains, WordWriter&& write32) {
if (serial.empty() || serial.size() > 9 ||
serial.find_first_not_of("0123456789") != std::string_view::npos) return 0;
uint32_t number = 0;
const auto parsed = std::from_chars(serial.data(), serial.data() + serial.size(), number);
if (parsed.ec != std::errc{} || parsed.ptr != serial.data() + serial.size() ||
!address || !contains(address, sizeof(uint32_t))) return 0;
write32(address, number);
return 1;
}
} // namespace RuntimeScSerial
+4 -6
View File
@@ -1,6 +1,7 @@
#include "hle_stubs.h" #include "hle_stubs.h"
#include "console_identity.h" #include "console_identity.h"
#include "sc_serial_contract.h"
#include <cstdlib> #include <cstdlib>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
@@ -97,12 +98,9 @@ PPC_NATIVE_OVERRIDE(801B2424, SCGetProductCode_HLE, uint32_t, (), ());
extern "C" uint32_t SCGetProductSN_HLE(uint32_t serialAddress) extern "C" uint32_t SCGetProductSN_HLE(uint32_t serialAddress)
{ {
const std::string& serial = RuntimeConsoleIdentity::Current().serial; const std::string& serial = RuntimeConsoleIdentity::Current().serial;
if (!serialAddress || !Memory::Contains(serialAddress, serial.size() + 1)) { return RuntimeScSerial::Write(serial, serialAddress,
return 0; [](uint32_t address, size_t size) { return Memory::Contains(address, size); },
} [](uint32_t address, uint32_t value) { Memory::Write32(address, value); });
std::memcpy(Memory::GetPointer(serialAddress, serial.size() + 1),
serial.c_str(), serial.size() + 1);
return 1;
} }
PPC_NATIVE_OVERRIDE(801B2460, SCGetProductSN_HLE, uint32_t, (uint32_t serialAddress), (serialAddress)); PPC_NATIVE_OVERRIDE(801B2460, SCGetProductSN_HLE, uint32_t, (uint32_t serialAddress), (serialAddress));
+3
View File
@@ -93,6 +93,9 @@ extern "C" int32_t NANDOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint32_t
const std::filesystem::path hostPath = TranslateNandPath(path); const std::filesystem::path hostPath = TranslateNandPath(path);
if (const auto result = NandCheckSystemSaveRead("NANDOpen", hostPath, mode))
return *result;
// Existing-file write opens go through a shadow copy seeded from the original, so a // Existing-file write opens go through a shadow copy seeded from the original, so a
// crash between NANDWrite and NANDClose cannot leave a torn file (the game patches // crash between NANDWrite and NANDClose cannot leave a torn file (the game patches
// sub-ranges, e.g. ghost saves at a non-zero offset). New files still create in place. // sub-ranges, e.g. ghost saves at a non-zero offset). New files still create in place.
+2
View File
@@ -411,6 +411,8 @@ extern "C" int32_t NANDSafeOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint
if (mode == 1) { if (mode == 1) {
// Read-only safe open reads the original in place; the library builds no scratch // Read-only safe open reads the original in place; the library builds no scratch
// copy for this case. // copy for this case.
if (const auto result = NandCheckSystemSaveRead("NANDSafeOpen", hostPath, mode))
return *result;
FILE* file = NandFopen(hostPath, "rb"); FILE* file = NandFopen(hostPath, "rb");
if (!file && IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) { if (!file && IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) {
file = NandFopen(hostPath, "rb"); file = NandFopen(hostPath, "rb");
+20
View File
@@ -411,6 +411,26 @@ bool IsFaceLibResourcePath(const char* path) {
return std::strcmp(path, "/shared2/menu/FaceLib/RFL_Res.dat") == 0; return std::strcmp(path, "/shared2/menu/FaceLib/RFL_Res.dat") == 0;
} }
std::optional<int32_t> NandCheckSystemSaveRead(const char* who,
const std::filesystem::path& hostPath, int mode, bool ios) {
const auto action = RuntimeNandSave::CheckRead(hostPath, mode);
if (action == RuntimeNandSave::ReadAction::Proceed) return std::nullopt;
if (action == RuntimeNandSave::ReadAction::Missing) {
LogNandWarning(who, "treating empty or zero-filled system save '%s' as missing",
HostPathText(hostPath).c_str());
return ios ? ISFS_ENOENT : NAND_RESULT_NOEXISTS;
}
if (action == RuntimeNandSave::ReadAction::RecoveryNeeded) {
LogNandError(who, "system save '%s' is missing or blank but its .nandsafe.tmp contains data; "
"back up both files before attempting recovery",
HostPathText(hostPath).c_str());
} else {
LogNandError(who, "could not inspect system save '%s' or its write shadow; leaving data untouched",
HostPathText(hostPath).c_str());
}
return ios ? ISFS_EIO : NAND_RESULT_UNKNOWN;
}
// Create directories recursively // Create directories recursively
bool CreateDirectoryPath(const std::filesystem::path& path) { bool CreateDirectoryPath(const std::filesystem::path& path) {
if (path.empty()) { if (path.empty()) {
+7
View File
@@ -9,6 +9,7 @@
#include "hle/runtime_parse_helpers.h" #include "hle/runtime_parse_helpers.h"
#include "memory.h" #include "memory.h"
#include "nand_path.h" #include "nand_path.h"
#include "nand_save_probe.h"
#include "hle/net/network.h" #include "hle/net/network.h"
#include "recomp_mod_loader.h" #include "recomp_mod_loader.h"
#include "runtime_config.h" #include "runtime_config.h"
@@ -26,6 +27,7 @@
#include <deque> #include <deque>
#include <map> #include <map>
#include <mutex> #include <mutex>
#include <optional>
#include <vector> #include <vector>
#include <filesystem> #include <filesystem>
#include <string> #include <string>
@@ -56,6 +58,11 @@ constexpr uint32_t kNandTitleIdLo = 0x524D4350; // "RMCP" fallback
void LogNandError(const char* func, const char* fmt, ...); void LogNandError(const char* func, const char* fmt, ...);
void LogNandWarning(const char* func, const char* fmt, ...); void LogNandWarning(const char* func, const char* fmt, ...);
// An empty optional means continue opening normally; otherwise return the
// supplied NAND/IOS error without exposing a failed scan as a missing save.
std::optional<int32_t> NandCheckSystemSaveRead(const char* who,
const std::filesystem::path& hostPath, int mode, bool ios = false);
// ============================================================================ // ============================================================================
// File Descriptor Management // File Descriptor Management
// ============================================================================ // ============================================================================
+3
View File
@@ -392,6 +392,9 @@ extern "C" int32_t NAND_IOS_Open_HLE(uint32_t pathPtr, uint32_t mode) {
// It's a NAND file path // It's a NAND file path
const std::filesystem::path hostPath = TranslateNandPath(path); const std::filesystem::path hostPath = TranslateNandPath(path);
if (const auto result = NandCheckSystemSaveRead("IOS_Open", hostPath, mode, true))
return *result;
// Seed FaceLib resources before the existence check so every open mode can // Seed FaceLib resources before the existence check so every open mode can
// still find them on a fresh managed NAND. // still find them on a fresh managed NAND.
if (!PathExists(hostPath) && IsFaceLibResourcePath(path)) { if (!PathExists(hostPath) && IsFaceLibResourcePath(path)) {
+2 -2
View File
@@ -14,7 +14,7 @@
extern "C" void mkw_co_switch(void** targetSp, void** sourceSp); extern "C" void mkw_co_switch(void** targetSp, void** sourceSp);
extern "C" void* mkw_co_init(void* stackTop, void (*entry)(void*), void* argument); extern "C" void* mkw_co_init(void* stackTop, void (*entry)(void*), void* argument);
#elif defined(__linux__) #elif defined(__linux__) || (defined(__APPLE__) && defined(__x86_64__))
#include <libco.h> #include <libco.h>
#include <cstdlib> #include <cstdlib>
@@ -158,7 +158,7 @@ void Switch(Handle target)
g_current = source; g_current = source;
} }
#elif defined(__linux__) #elif defined(__linux__) || (defined(__APPLE__) && defined(__x86_64__))
namespace { namespace {
struct Context { struct Context {
+11 -2
View File
@@ -44,10 +44,15 @@
#include <signal.h> #include <signal.h>
#if defined(__x86_64__) #if defined(__x86_64__)
// Only the x86 POSIX fault path inspects ucontext_t to recover the page-fault // Only the x86 POSIX fault path inspects ucontext_t to recover the page-fault
// write bit. macOS deprecates ucontext and requires _XOPEN_SOURCE just to // write bit. macOS exposes the signal-handler context through sys/ucontext.h;
// include the header, while the arm64 handler does not use it at all. // avoid ucontext.h itself because its deprecated user-context APIs require
// _XOPEN_SOURCE. The arm64 handler does not inspect a host context at all.
#if defined(__APPLE__)
#include <sys/ucontext.h>
#else
#include <ucontext.h> #include <ucontext.h>
#endif #endif
#endif
#include <unistd.h> #include <unistd.h>
#endif #endif
@@ -1128,7 +1133,11 @@ void PosixMemoryFaultHandler(int sig, siginfo_t* info, void* ucontextVoid) {
// error code x86 pushes on a page fault records whether it was a write. // error code x86 pushes on a page fault records whether it was a write.
if (ucontextVoid != nullptr) { if (ucontextVoid != nullptr) {
auto* uc = static_cast<ucontext_t*>(ucontextVoid); auto* uc = static_cast<ucontext_t*>(ucontextVoid);
#if defined(__APPLE__)
isWrite = uc->uc_mcontext != nullptr && (uc->uc_mcontext->__es.__err & 0x2) != 0;
#else
isWrite = (uc->uc_mcontext.gregs[REG_ERR] & 0x2) != 0; isWrite = (uc->uc_mcontext.gregs[REG_ERR] & 0x2) != 0;
#endif
} }
#endif #endif
+18 -4
View File
@@ -4,9 +4,9 @@
#include <array> #include <array>
#include <atomic> #include <atomic>
#include <bit>
#include <chrono> #include <chrono>
#include <cstdint> #include <cstdint>
#include <cstring>
#include <iostream> #include <iostream>
#include <mutex> #include <mutex>
#include <thread> #include <thread>
@@ -52,6 +52,20 @@ std::array<float, kSoundPlayerCount> g_requestedSoundPlayerVolumes{};
std::array<float, kSoundPlayerCount> g_lastAppliedSoundPlayerVolumes{}; std::array<float, kSoundPlayerCount> g_lastAppliedSoundPlayerVolumes{};
std::array<bool, kSoundPlayerCount> g_haveSoundPlayerVolumes{}; std::array<bool, kSoundPlayerCount> g_haveSoundPlayerVolumes{};
uint32_t FloatBits(float value) noexcept {
static_assert(sizeof(float) == sizeof(uint32_t));
uint32_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
return bits;
}
float BitsFloat(uint32_t bits) noexcept {
static_assert(sizeof(float) == sizeof(uint32_t));
float value = 0.0f;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
float ClampSoundPlayerVolume(float volume) noexcept { float ClampSoundPlayerVolume(float volume) noexcept {
// Match nw4r::snd::SoundPlayer::SetVolume at 0x800A35E0 exactly, // Match nw4r::snd::SoundPlayer::SetVolume at 0x800A35E0 exactly,
// including its NaN behavior (unordered compares select the upper bound). // including its NaN behavior (unordered compares select the upper bound).
@@ -63,7 +77,7 @@ float ClampSoundPlayerVolume(float volume) noexcept {
bool WriteGuestFloat(uint32_t address, float value) noexcept { bool WriteGuestFloat(uint32_t address, float value) noexcept {
try { try {
Memory::Write32(address, std::bit_cast<uint32_t>(value)); Memory::Write32(address, FloatBits(value));
return true; return true;
} catch (const Memory::AccessViolation&) { } catch (const Memory::AccessViolation&) {
return false; return false;
@@ -75,7 +89,7 @@ bool ReadGuestFloat(uint32_t address, float& value) noexcept {
if (!Memory::TryRead32(address, bits)) { if (!Memory::TryRead32(address, bits)) {
return false; return false;
} }
value = std::bit_cast<float>(bits); value = BitsFloat(bits);
return true; return true;
} }
@@ -558,7 +572,7 @@ void SetSoundPlayerVolume(uint32_t soundPlayer, float requestedVolume) {
} }
// Preserve the original function's access semantics. An invalid player is // Preserve the original function's access semantics. An invalid player is
// a guest bug and must not be converted into a silent successful call. // a guest bug and must not be converted into a silent successful call.
Memory::Write32(soundPlayer + kSoundPlayerVolumeOffset, std::bit_cast<uint32_t>(applied)); Memory::Write32(soundPlayer + kSoundPlayerVolumeOffset, FloatBits(applied));
} }
} // namespace MusicAttenuation } // namespace MusicAttenuation
+142
View File
@@ -0,0 +1,142 @@
#include "nand_save_probe.h"
#include <algorithm>
#include <chrono>
#include <iostream>
#include <sstream>
#include <stdexcept>
#ifdef _WIN32
#include <windows.h>
#endif
namespace fs = std::filesystem;
using RuntimeNandSave::ReadAction;
using RuntimeNandSave::Contents;
static void Require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
static void Write(const fs::path& path, const std::string& bytes) {
fs::create_directories(path.parent_path());
std::ofstream output(path, std::ios::binary);
output.write(bytes.data(), bytes.size());
output.close();
Require(static_cast<bool>(output), "Fixture write failed");
}
static std::string Read(const fs::path& path) {
std::ifstream input(path, std::ios::binary);
Require(static_cast<bool>(input), "Fixture read failed");
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
}
// A disk error after zero-filled blocks must not look like a blank file's EOF.
class FailingDisk : public std::streambuf {
int blocks;
public:
explicit FailingDisk(int zeroBlocks) : blocks(zeroBlocks) {}
std::streamsize xsgetn(char* buffer, std::streamsize length) override {
if (blocks-- <= 0) throw std::runtime_error("injected read failure");
std::fill(buffer, buffer + length, '\0');
return length;
}
};
int main() {
const auto root = fs::temp_directory_path() / ("wiicomp-save-scenarios-" +
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
try {
const auto save = root / "title/00010004/524d4350/data/rksys.dat";
const auto shadow = fs::path(save.native() + fs::path(".nandsafe.tmp").native());
// Save inspection must leave unrelated NAND data alone. Settings
// initialization is covered separately by nand_settings_tests.
const auto settingsPath = root / "title/00000001/00000002/data/setting.txt";
const std::string identity(256, '\x5a');
Write(settingsPath, identity);
Require(RuntimeNandSave::CheckRead(save, 1) == ReadAction::Proceed, "Fresh profile follows normal missing-file handling");
Require(!fs::exists(save), "Probing fresh profile must not create a save");
// First launch interrupted before save initialization, including block
// boundaries and a full-sized synthetic zero-filled allocation.
for (const size_t size : {size_t(0), size_t(1), size_t(4095), size_t(4096), size_t(4097), size_t(3 * 1024 * 1024)}) {
const std::string bytes(size, '\0');
Write(save, bytes);
Require(RuntimeNandSave::CheckRead(save, 1) == ReadAction::Missing, "Blank save should be offered first-save recovery");
Require(Read(save) == bytes, "Blank-save detection must not modify the file");
for (int mode : {2, 3}) {
Require(RuntimeNandSave::CheckRead(save, mode) == ReadAction::Proceed, "Write opens must remain available for initialization");
}
}
// Existing saves, imported saves, partial/corrupt saves, and a zero
// prefix with data only in the final byte are all left to the game.
std::string existing(3 * 1024 * 1024, '\0');
existing.replace(0, 8, "RKSD0006");
existing[10000] = 42;
for (const std::string& bytes : {existing, std::string("RKSD"), std::string("damaged-header"),
std::string(8192, '\0') + "x", std::string(8191, '\0') + "x"}) {
Write(save, bytes);
Require(RuntimeNandSave::CheckRead(save, 1) == ReadAction::Proceed, "Never hide a save containing any data");
Require(Read(save) == bytes, "Existing/partial save must be byte-identical after inspection");
}
// Interrupted replacement: retain a committed original regardless of
// whether the shadow is blank, partial, or contains a complete header.
Write(save, existing);
for (const std::string& bytes : {std::string(), std::string(4096, '\0'), std::string("RKSD"), existing}) {
Write(shadow, bytes);
Require(RuntimeNandSave::CheckRead(save, 1) == ReadAction::Proceed, "Committed original takes precedence over write shadow");
Require(Read(save) == existing && Read(shadow) == bytes, "Probe must preserve both sides of an interrupted write");
}
// No usable original: do not let missing-save recovery discard the
// only possible recovery source, and do not auto-promote that shadow.
for (const bool mainExists : {false, true}) {
fs::remove(save);
if (mainExists) Write(save, std::string(4096, '\0'));
Write(shadow, existing);
Require(RuntimeNandSave::CheckRead(save, 1) == ReadAction::RecoveryNeeded, "Preserve recovery candidate when original is missing or blank");
Require(Read(shadow) == existing, "Recovery candidate must remain unchanged");
Require(fs::exists(save) == mainExists, "Do not promote shadow automatically");
}
Write(shadow, std::string(4096, '\0'));
Require(RuntimeNandSave::CheckRead(save, 1) == ReadAction::Missing, "Two blank files may use first-save recovery");
fs::remove(shadow);
for (const char* name : {"rksys.dat.bak", "rksys.dat.backup", "rksys.dat2", "banner.bin", "setting.txt"}) {
const auto unrelated = save.parent_path() / name;
Write(unrelated, std::string(4096, '\0'));
Require(RuntimeNandSave::CheckRead(unrelated, 1) == ReadAction::Proceed, "Do not classify backups or unrelated files as missing saves");
}
for (int blocks : {0, 1, 2}) {
FailingDisk disk(blocks);
std::istream input(&disk);
Require(RuntimeNandSave::InspectStream(input) == Contents::Error, "Read failure must remain an error, including after zero-filled blocks");
}
std::istringstream badEof;
badEof.setstate(std::ios::badbit | std::ios::eofbit);
Require(RuntimeNandSave::InspectStream(badEof) == Contents::Error, "Badbit plus EOF must not imply a blank save");
#ifdef _WIN32
Write(save, existing);
const HANDLE locked = CreateFileW(save.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
Require(locked != INVALID_HANDLE_VALUE, "Could not lock fixture");
const auto lockedResult = RuntimeNandSave::CheckRead(save, 1);
CloseHandle(locked);
Require(lockedResult == ReadAction::Error, "Sharing/access failure must not report a missing save");
Require(Read(save) == existing, "Locked save must survive inspection unchanged");
Require(SetFileAttributesW(save.c_str(), FILE_ATTRIBUTE_READONLY) != 0, "Set fixture read-only");
const auto readOnlyResult = RuntimeNandSave::CheckRead(save, 1);
SetFileAttributesW(save.c_str(), FILE_ATTRIBUTE_NORMAL);
Require(readOnlyResult == ReadAction::Proceed && Read(save) == existing, "Readable read-only save remains available");
#endif
Require(Read(settingsPath) == identity, "Save inspection must not change NAND settings");
fs::remove_all(root);
std::cout << "NAND save startup, preservation, interrupted-write and I/O failure scenarios passed\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << " (fixtures retained at " << root << ")\n";
return 1;
}
}
+120 -2
View File
@@ -3,22 +3,78 @@
#include <chrono> #include <chrono>
#include <iostream> #include <iostream>
#include <stdexcept> #include <stdexcept>
#include <thread>
#include <vector>
static void Require(bool condition) { static void Require(bool condition, const char* message = "NAND settings check failed") {
if (!condition) { if (!condition) {
throw std::runtime_error("NAND settings check failed"); throw std::runtime_error(message);
} }
} }
static std::string ReadBytes(const std::filesystem::path& path) {
std::ifstream input(path, std::ios::binary);
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
}
int main() { int main() {
const auto root = std::filesystem::temp_directory_path() / const auto root = std::filesystem::temp_directory_path() /
("wiicomp-nand-settings-" + std::to_string( ("wiicomp-nand-settings-" + std::to_string(
std::chrono::steady_clock::now().time_since_epoch().count())); std::chrono::steady_clock::now().time_since_epoch().count()));
const auto path = root / "title/00000001/00000002/data/setting.txt"; const auto path = root / "title/00000001/00000002/data/setting.txt";
try { try {
using namespace RuntimeNandSettings;
Require(GenerateSerial(1800000123) == "800000123", "Dolphin timestamp modulo");
Require(GenerateSerial(1000000001) == "000000001", "Dolphin leading zero padding");
Require(GenerateSerial(-1).empty(), "Invalid clock must not supply an identity");
// Golden bytes generated by Dolphin's unmodified SettingsHandler.cpp
// (upstream 2026-09-06), PAL boot fields and synthetic serial 000000001.
// Everything after this prefix is raw zero padding to 256 bytes.
const std::string goldenHex =
"bba6ac929a0bc96b7eed83d27f33a1e7e73d9b836d8b47c59ee23df6b275baab"
"bec9d9dead03cc7a3bdafee50c30ab9fb86194e119fe4ba19eff62d5ec3aacb3"
"b5c9d9e3977eac0943d7ff903120a49ef024eafe1cf77be79cf6229a823aabf0f0";
std::array<uint8_t, 256> golden{};
for (size_t i = 0; i < goldenHex.size() / 2; ++i) {
golden[i] = static_cast<uint8_t>(std::stoul(goldenHex.substr(i * 2, 2), nullptr, 16));
}
Require(EncodeNew("000000001") == golden, "Exact Dolphin writer golden fixture");
std::string error;
Require(!RuntimeNandSettings::Read(root)); Require(!RuntimeNandSettings::Read(root));
Require(!std::filesystem::exists(root)); Require(!std::filesystem::exists(root));
std::filesystem::create_directories(path.parent_path()); std::filesystem::create_directories(path.parent_path());
const auto scratchParent = root / "scratch-collisions";
std::filesystem::create_directories(scratchParent / ".setting-init-fixed-0");
const auto sentinel = scratchParent / ".setting-init-fixed-0" / "setting.txt";
{ std::ofstream output(sentinel); output << "another launch owns this"; }
const auto occupiedFile = scratchParent / ".setting-init-fixed-1";
{ std::ofstream output(occupiedFile); output << "leave this file alone"; }
std::error_code scratchError;
const auto claimed = CreateScratchDirectory(scratchParent, "fixed", scratchError);
Require(claimed && *claimed == scratchParent / ".setting-init-fixed-2" && !scratchError,
"Retry collisions with both existing directories and files");
Require(ReadBytes(sentinel) == "another launch owns this" &&
ReadBytes(occupiedFile) == "leave this file alone", "Never modify another launch's scratch data");
Require(!CreateScratchDirectory(occupiedFile / "not-a-directory", "fixed", scratchError) && scratchError,
"Real filesystem errors must fail rather than retry indefinitely");
// Force all claimants to use the same token; this deterministically
// exercises the collision path even when host clock precision is high.
std::array<std::optional<std::filesystem::path>, 16> claims;
std::vector<std::thread> claimants;
for (size_t i = 0; i < claims.size(); ++i) {
claimants.emplace_back([&, i] {
std::error_code ec;
claims[i] = CreateScratchDirectory(scratchParent, "shared", ec);
});
}
for (auto& claimant : claimants) claimant.join();
for (size_t i = 0; i < claims.size(); ++i) {
Require(claims[i].has_value(), "Every concurrent claimant must acquire a scratch directory");
for (size_t j = 0; j < i; ++j) {
Require(claims[i] != claims[j], "Concurrent claimants must own different scratch directories");
}
}
const std::string plain = "AREA=USA\r\n\nCODE=LU\r\nSERNO=987654321\r\nGAME=US\r\n"; const std::string plain = "AREA=USA\r\n\nCODE=LU\r\nSERNO=987654321\r\nGAME=US\r\n";
std::array<uint8_t, 256> fixture{}; std::array<uint8_t, 256> fixture{};
for (size_t i = 0; i < fixture.size(); ++i) { for (size_t i = 0; i < fixture.size(); ++i) {
@@ -35,6 +91,7 @@ int main() {
Require(settings && RuntimeNandSettings::HasIdentity(*settings)); Require(settings && RuntimeNandSettings::HasIdentity(*settings));
Require(settings->at("SERNO") == "987654321" && settings->at("CODE") == "LU"); Require(settings->at("SERNO") == "987654321" && settings->at("CODE") == "LU");
Require(settings->at("AREA") == "USA" && settings->at("GAME") == "US"); Require(settings->at("AREA") == "USA" && settings->at("GAME") == "US");
Require(Ensure(root, error, 1800000123), "Existing imported NAND must work");
std::array<uint8_t, 256> after{}; std::array<uint8_t, 256> after{};
{ {
std::ifstream input(path, std::ios::binary); std::ifstream input(path, std::ios::binary);
@@ -54,6 +111,67 @@ int main() {
Require(!RuntimeNandSettings::HasIdentity(*settings)); Require(!RuntimeNandSettings::HasIdentity(*settings));
std::filesystem::resize_file(path, 128); std::filesystem::resize_file(path, 128);
Require(!RuntimeNandSettings::Read(root)); Require(!RuntimeNandSettings::Read(root));
const auto damaged = ReadBytes(path);
Require(!Ensure(root, error, 1800000123), "Do not replace a truncated identity");
Require(ReadBytes(path) == damaged, "Damaged file must remain untouched");
const auto fresh = root / "fresh";
Require(Ensure(fresh, error, 1800000123), "Missing setting.txt must initialize");
const auto generated = Read(fresh);
Require(generated && HasIdentity(*generated), "Generated file must be readable");
Require(generated->at("SERNO") == "800000123", "Persist Dolphin-generated serial");
Require(generated->at("CODE") == "LEH" && generated->at("AREA") == "EUR" &&
generated->at("GAME") == "EU", "PAL first-boot fields");
Require(generated->at("MODEL") == "RVL-001(EUR)" && generated->at("VIDEO") == "PAL" &&
generated->at("DVD") == "0" && generated->at("MPCH") == "0x7FFE",
"Complete Dolphin boot settings");
const auto firstBoot = ReadBytes(FilePath(fresh));
Require(firstBoot.size() == 256 && firstBoot.back() == 0, "Dolphin buffer size and raw zero padding");
Require(Ensure(fresh, error, 1900000999), "Second boot");
Require(ReadBytes(FilePath(fresh)) == firstBoot, "Second boot must not change any bytes");
const auto blocked = root / "blocked";
{ std::ofstream output(blocked); output << "file obstructing NAND directory"; }
Require(!Ensure(blocked, error, 1800000123), "Write failure must not return an ephemeral identity");
Require(!Ensure(root / "bad-clock", error, -1), "Clock failure must not initialize");
const auto concurrent = root / "concurrent";
std::array<bool, 16> results{};
std::vector<std::thread> workers;
for (size_t i = 0; i < results.size(); ++i) {
workers.emplace_back([&, i] {
std::string detail;
results[i] = Ensure(concurrent, detail, 1800000001 + i);
});
}
for (auto& worker : workers) worker.join();
for (const bool result : results) Require(result, "Concurrent boot must read the persisted winner");
const auto winner = ReadBytes(FilePath(concurrent));
Require(Read(concurrent) && HasIdentity(*Read(concurrent)), "Concurrent boot must persist valid settings");
Require(Ensure(concurrent, error, 1900000999), "Boot after concurrent initialization");
Require(ReadBytes(FilePath(concurrent)) == winner, "Concurrent winner must remain stable");
// Independently decode as Nintendo does: stop at the first encoded NUL.
// Exercise serials that force Dolphin's extra-LF escaping, not only
// values that happen to work with a plain rotating-XOR encoder.
bool sawExtraLf = false;
for (int serial = 1; serial <= 10000; ++serial) {
const auto number = GenerateSerial(1000000000 + serial);
const auto encoded = EncodeNew(number);
Require(encoded.has_value(), "Serial encoding must fit");
std::string decoded;
for (size_t i = 0; i < encoded->size() && (*encoded)[i] != 0; ++i) {
const unsigned shift = i % 32;
const uint32_t key = shift == 0 ? 0x73B5DBFAu :
(0x73B5DBFAu << shift) | (0x73B5DBFAu >> (32 - shift));
decoded += static_cast<char>((*encoded)[i] ^ static_cast<uint8_t>(key));
}
Require(decoded.find("SERNO=" + number + "\r\n") != std::string::npos &&
decoded.find("GAME=EU\r\n") != std::string::npos,
"Encoded NUL must not truncate settings");
sawExtraLf |= decoded.find("\r\n\n") != std::string::npos;
}
Require(sawExtraLf, "Exercise Dolphin LF escape path");
std::filesystem::remove_all(root); std::filesystem::remove_all(root);
std::cout << "NAND settings checks passed\n"; std::cout << "NAND settings checks passed\n";
return 0; return 0;
+81
View File
@@ -0,0 +1,81 @@
#include "sc_serial_contract.h"
#include "nand_settings.h"
#include <algorithm>
#include <array>
#include <iostream>
#include <iomanip>
#include <stdexcept>
#include <string>
static void Require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
static uint32_t ReadWord(const unsigned char* bytes) {
return (uint32_t(bytes[0]) << 24) | (uint32_t(bytes[1]) << 16) |
(uint32_t(bytes[2]) << 8) | uint32_t(bytes[3]);
}
int main(int argc, char** argv) {
try {
// Feed real generator + SC ABI outputs to the upstream bot decoder.
// Usage: mkw_sc_serial_tests --timestamp-vectors <unix-seconds> ...
if (argc > 1 && std::string(argv[1]) == "--timestamp-vectors") {
for (int i = 2; i < argc; ++i) {
const auto timestamp = std::stoll(argv[i]);
const auto serial = RuntimeNandSettings::GenerateSerial(static_cast<std::time_t>(timestamp));
std::array<unsigned char, 4> output{};
Require(RuntimeScSerial::Write(serial, 4,
[](uint32_t address, size_t size) { return address == 4 && size == 4; },
[&](uint32_t, uint32_t value) {
for (unsigned j = 0; j < 4; ++j)
output[j] = static_cast<unsigned char>(value >> (24 - 8 * j));
}) == 1, "Generated serial must pass SC ABI");
std::cout << timestamp << '\t' << serial << "\tLEH"
<< std::setfill('0') << std::setw(9) << ReadWord(output.data()) << '\n';
}
return 0;
}
// Reproduce the reported csnums from the old string-writing override.
const unsigned char old7886[] = {'7', '8', '8', '6'};
const unsigned char old7618[] = {'7', '6', '1', '8'};
Require(ReadWord(old7886) == 926431286, "Reproduce shared LEH926431286");
Require(ReadWord(old7618) == 926298424, "Reproduce shared LEH926298424");
std::array<unsigned char, 16> memory;
size_t available = 4;
unsigned writes = 0;
const auto contains = [&](uint32_t address, size_t size) {
return address == 4 && size <= available;
};
const auto write32 = [&](uint32_t address, uint32_t value) {
++writes;
for (unsigned i = 0; i < 4; ++i)
memory[address + i] = static_cast<unsigned char>(value >> (24 - 8 * i));
};
for (const auto& pair : {std::pair{"788600001", 788600001u}, {"788699999", 788699999u},
{"761800001", 761800001u}, {"761899999", 761899999u},
{"012345678", 12345678u}, {"000000001", 1u}, {"999999999", 999999999u}}) {
memory.fill(0xa5);
writes = 0;
Require(RuntimeScSerial::Write(pair.first, 4, contains, write32) == 1, "Accept an exactly four-byte output buffer");
Require(writes == 1 && ReadWord(memory.data() + 4) == pair.second, "Return full numeric serial, including digits after common prefix");
for (size_t i = 0; i < memory.size(); ++i)
if (i < 4 || i >= 8) Require(memory[i] == 0xa5, "Do not overwrite adjacent guest stack data");
}
for (const char* serial : {"", "1234567890", "7886x1234", "-12345678", "+12345678"}) {
writes = 0;
Require(RuntimeScSerial::Write(serial, 4, contains, write32) == 0 && writes == 0, "Reject malformed serial without a write");
}
writes = 0;
Require(RuntimeScSerial::Write("788600001", 0, contains, write32) == 0 && writes == 0, "Reject null output");
available = 3;
Require(RuntimeScSerial::Write("788600001", 4, contains, write32) == 0 && writes == 0, "Reject undersized output");
std::cout << "SC serial collision reproduction, numeric output and memory-boundary tests passed\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
+19 -5
View File
@@ -97,15 +97,29 @@ int main() {
Check(!Pressed(pulse), "pulse expires"); Check(!Pressed(pulse), "pulse expires");
// The timing-window idiom seen in shared Dolphin configs. // The timing-window idiom seen in shared Dolphin configs.
auto window = Compile("!pulse(`W`, 0.05) & pulse(`W`, 0.15)"); auto window = Compile("!pulse(`W`, 0.05) & pulse(`W`, 0.35)");
g_inputs["W"] = 0.0; g_inputs["W"] = 0.0;
window.Evaluate(Source()); window.Evaluate(Source());
g_inputs["W"] = 1.0; g_inputs["W"] = 1.0;
Check(!Pressed(window), "window closed before its start"); Check(!Pressed(window), "window closed before its start");
Sleep(90); bool windowOpened = false;
Check(Pressed(window), "window open between the two pulses"); for (int i = 0; i < 40; ++i) {
Sleep(90); Sleep(10);
Check(!Pressed(window), "window closed after its end"); if (Pressed(window)) {
windowOpened = true;
break;
}
}
Check(windowOpened, "window open between the two pulses");
bool windowClosed = false;
for (int i = 0; i < 50; ++i) {
Sleep(10);
if (!Pressed(window)) {
windowClosed = true;
break;
}
}
Check(windowClosed, "window closed after its end");
// timer ramps 0..1 and wraps, so a threshold turns it into a square wave. // timer ramps 0..1 and wraps, so a threshold turns it into a square wave.
auto timer = Compile("`X` & timer(0.1)"); auto timer = Compile("`X` & timer(0.1)");
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@@ -378,7 +378,7 @@ public sealed partial class CxxLinearCodeGenerator
instructionContinuationLabels.TryGetValue(trace.Address, out var continuationLabel)) instructionContinuationLabels.TryGetValue(trace.Address, out var continuationLabel))
{ {
RecordEmittedLocalLabel(continuationLabel); RecordEmittedLocalLabel(continuationLabel);
body.AppendLine($"{continuationLabel}:"); body.AppendLine($"{continuationLabel}: ;");
} }
var localFallthroughLr = TryGetLocalFallthroughLr(block.Instructions, i, nonReturningCallTargets, lrContinuationCallTargets); var localFallthroughLr = TryGetLocalFallthroughLr(block.Instructions, i, nonReturningCallTargets, lrContinuationCallTargets);
// State-free bodies are cloned after register caching and lose their // State-free bodies are cloned after register caching and lose their
@@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using Translator.Core.Analysis.Representation; using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis.Ssa; using Translator.Core.Analysis.Ssa;
using Translator.Core.CodeGen; using Translator.Core.CodeGen;
@@ -153,4 +153,35 @@ public class EmittedOutputShapeTests
Assert.Contains("f3.d = MemoryInline::FlatReadFloat32((r4 + 16));", code, StringComparison.Ordinal); Assert.Contains("f3.d = MemoryInline::FlatReadFloat32((r4 + 16));", code, StringComparison.Ordinal);
Assert.Contains("f4.d = MemoryInline::FlatReadFloat64((r4 + 24));", code, StringComparison.Ordinal); Assert.Contains("f4.d = MemoryInline::FlatReadFloat64((r4 + 24));", code, StringComparison.Ordinal);
} }
[Fact]
public void ContinuationLabelAtBlockEndEmitsValidCxx17Statement()
{
var function = new IrFunction("continuation_at_block_end", "0x800E7798", new[]
{
new IrBasicBlock("0x800E7798", new IrInstruction[]
{
new IrCall(string.Empty, "0x8179B000", System.Array.Empty<IrValue>()),
new IrTracePpc(0x800E77A0u, "nop", "0x60000000"),
new IrJump("0x800E77A4")
}),
new IrBasicBlock("0x800E77A4", new IrInstruction[]
{
new IrReturn(null)
})
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>());
var code = new CxxLinearCodeGenerator().Emit(
0x800E7798,
new SsaTransformer().Convert(function),
new FunctionAbiClassification(function.Name, ValueRepresentation.Void),
types,
lrContinuationCallTargets: new HashSet<uint> { 0x8179B000u });
// In C++17, a label before a closing brace is invalid without an intervening statement.
Assert.Contains("loc_800E77A0: ;", code, StringComparison.Ordinal);
Assert.DoesNotContain("loc_800E77A0:\n}", code.Replace("\r\n", "\n"), StringComparison.Ordinal);
}
} }