mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-13 09:59:47 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d04be19d6 | |||
| c16f1533e5 |
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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/
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Vendored
+50
@@ -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)
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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*;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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));
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user