diff --git a/Launcher/WiiCompiled.Setup.Common/RetroWfcPayload.cs b/Launcher/WiiCompiled.Setup.Common/RetroWfcPayload.cs index f100171..22d1c23 100644 --- a/Launcher/WiiCompiled.Setup.Common/RetroWfcPayload.cs +++ b/Launcher/WiiCompiled.Setup.Common/RetroWfcPayload.cs @@ -53,9 +53,22 @@ public static class RetroWfcPayload var root = Path.GetFullPath(stagedDirectory); var payload = Path.Combine(root, RetroWfcOfflinePayloadFile); - if (!File.Exists(payload)) + try + { + if ((File.GetAttributes(payload) & FileAttributes.Directory) != 0) + throw new InvalidDataException( + "The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin."); + } + catch (FileNotFoundException) + { throw new InvalidDataException( "The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin."); + } + catch (DirectoryNotFoundException) + { + throw new InvalidDataException( + "The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin."); + } ValidateRetroWfcPayloadFile(payload, signingKey); return root; } diff --git a/Launcher/local-build-macos.command b/Launcher/local-build-macos.command new file mode 100755 index 0000000..1f91b0c --- /dev/null +++ b/Launcher/local-build-macos.command @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# Native macOS build automation: optionally extract -> translate -> compile -> publish .app. +set -euo pipefail + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +default_workspace=$(cd "$script_dir/.." && pwd) + +fail() { printf 'local-build-macos.command: error: %s\n' "$*" >&2; exit 1; } +step() { printf 'MKWCBUILD:STEP:%s %s\n' "$1" "$2"; } +assert_file() { [[ -f "$1" ]] || fail "$2 is missing: $1"; } +sha256() { shasum -a 256 "$1" | awk '{ print $1 }'; } + +usage() { + cat <<'EOF' +Usage: local-build-macos.command --output-dir DIR [options] + + --workspace DIR Repository root (default: this script's parent directory) + --profile {base|retro-rewind|both} Product to build (default: base) + --output-dir DIR Output .app directory (required; Retro Rewind for both) + --base-output-dir DIR Base .app directory (required with --profile both) + --game IMAGE --nodtool PATH Extract and verify a clean PAL RMCP01 disc image first + --retro-rewind-package-dir DIR RetroRewind6 directory (required for Retro Rewind) + --retro-wfc-offline-dir DIR Directory containing binary/payload.RMCPD00.bin + --skip-retro-wfc-payload Build Retro Rewind without the shared Retro-WFC payload + --force-clean-build Delete local generated and native-build-macos caches + --parallel N Pin translation and build parallelism + --cmake PATH --ninja PATH Override build tools + --dotnet PATH Override dotnet + --translator-bin PATH Use a self-contained Translator.Cli executable +EOF +} + +workspace="$default_workspace"; profile=base; output_dir=""; base_output_dir="" +game=""; nodtool=""; retro_root=""; retro_wfc=""; skip_retro_wfc=0; force_clean=0 +parallel=0; cmake_bin=cmake; ninja_bin=ninja; dotnet_bin=dotnet; translator_bin="" +while (($#)); do + case "$1" in + --workspace) workspace=${2:-}; shift 2 ;; + --profile) profile=${2:-}; shift 2 ;; + --output-dir) output_dir=${2:-}; shift 2 ;; + --base-output-dir) base_output_dir=${2:-}; shift 2 ;; + --game) game=${2:-}; shift 2 ;; + --nodtool) nodtool=${2:-}; shift 2 ;; + --retro-rewind-package-dir) retro_root=${2:-}; shift 2 ;; + --retro-wfc-offline-dir) retro_wfc=${2:-}; shift 2 ;; + --skip-retro-wfc-payload) skip_retro_wfc=1; shift ;; + --force-clean-build) force_clean=1; shift ;; + --parallel) parallel=${2:-}; shift 2 ;; + --cmake) cmake_bin=${2:-}; shift 2 ;; + --ninja) ninja_bin=${2:-}; shift 2 ;; + --dotnet) dotnet_bin=${2:-}; shift 2 ;; + --translator-bin) translator_bin=${2:-}; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) fail "unknown option: $1" ;; + esac +done + +[[ $(uname -s) == Darwin ]] || fail 'this build script is for macOS only' +[[ $(uname -m) == arm64 ]] || fail 'the current macOS product target is Apple Silicon only' +workspace=$(cd "$workspace" && pwd) +[[ -n "$output_dir" ]] || fail '--output-dir is required' +case "$profile" in base|retro-rewind|both) ;; *) fail '--profile must be base, retro-rewind, or both' ;; esac +builds_retro=0; [[ "$profile" != base ]] && builds_retro=1 +if [[ "$profile" == both && -z "$base_output_dir" ]]; then fail '--base-output-dir is required with --profile both'; fi +if [[ "$profile" != both && -n "$base_output_dir" ]]; then fail '--base-output-dir is valid only with --profile both'; fi +if [[ -n "$game" || -n "$nodtool" ]]; then [[ -n "$game" && -n "$nodtool" ]] || fail '--game and --nodtool must be supplied together'; fi +if (( builds_retro )); then + [[ -n "$retro_root" ]] || fail '--retro-rewind-package-dir is required for Retro Rewind' + [[ -n "$retro_wfc" ]] && (( skip_retro_wfc )) && fail 'choose only one Retro-WFC mode' + [[ -n "$retro_wfc" || $skip_retro_wfc -eq 1 ]] || fail 'choose a Retro-WFC payload directory or --skip-retro-wfc-payload' +fi +for tool in "$cmake_bin" "$ninja_bin" clang clang++ shasum; do command -v "$tool" >/dev/null || fail "required tool not found: $tool"; done + +project="$workspace/projects/mkwii/recomp.yml"; assets="$workspace/Assets"; generated="$workspace/generated" +functions="$generated/functions"; metadata="$generated/base_translation_output.json"; manifest_dir="$workspace/build/base" +manifest="$manifest_dir/mkwii_base_manifest.json"; shards="$generated/build_shards"; native_build="$workspace/native-build-macos" +assert_file "$project" 'translation project' +if [[ -n "$game" ]]; then "$script_dir/macos/extract-disc.command" --game "$game" --assets-dir "$assets" --nodtool "$nodtool"; fi +assert_file "$assets/main.dol" 'extracted main.dol'; assert_file "$assets/StaticR.rel" 'extracted StaticR.rel' + +if (( force_clean )); then + step force-clean 'Discarding translation and native build caches' + rm -rf "$generated" "$manifest_dir" "$native_build" +fi + +if [[ -n "$translator_bin" ]]; then + assert_file "$translator_bin" 'Translator.Cli executable' + translator() { "$translator_bin" "$@"; } +else + command -v "$dotnet_bin" >/dev/null || fail "required tool not found: $dotnet_bin" + translator_dll="$workspace/translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll" + if [[ ! -f "$translator_dll" ]]; then + step build-translator 'Building the translator' + "$dotnet_bin" build "$workspace/translator/src/Translator.Cli/Translator.Cli.csproj" -c Release + fi + translator() { "$dotnet_bin" "$translator_dll" "$@"; } +fi + +entry_point=$(awk '/^translation:/{inside=1} inside && /^[[:space:]]*-[[:space:]]*0[xX][0-9a-fA-F]+[[:space:]]*$/{gsub(/^[[:space:]]*-[[:space:]]*/, ""); print; exit}' "$project") +[[ -n "$entry_point" ]] || fail 'could not find the translation entry point' +cpu=$(sysctl -n hw.ncpu); mem_gib=$(( $(sysctl -n hw.memsize) / 1024 / 1024 / 1024 )); (( mem_gib < 1 )) && mem_gib=1 +if (( parallel > 0 )); then translator_threads=$parallel; translated_jobs=$parallel; global_jobs=$parallel +else translator_threads=$(( cpu < 16 ? cpu : 16 )); translated_jobs=$(( cpu < mem_gib / 2 ? cpu : mem_gib / 2 )); (( translated_jobs < 1 )) && translated_jobs=1; global_jobs=$cpu; fi + +if (( builds_retro )); then + retro_root=$(cd "$retro_root" && pwd) + if [[ ! -f "$retro_root/Binaries/Code.pul" && -f "$retro_root/RetroRewind6/Binaries/Code.pul" ]]; then + retro_root="$retro_root/RetroRewind6" + fi + assert_file "$retro_root/Binaries/Code.pul" 'Retro Rewind Code.pul' + stage="$workspace/PulsarPacks/completed/RetroRewind/RetroRewind6/Binaries" + mkdir -p "$stage" + if [[ ! "$retro_root/Binaries/Code.pul" -ef "$stage/Code.pul" ]]; then + cp -f "$retro_root/Binaries/Code.pul" "$stage/Code.pul" + fi +fi + +step translate-base 'Translating the user-owned base game' +rm -rf "$functions" "$metadata" "$manifest_dir"; mkdir -p "$functions" "$manifest_dir" +translator translate-recursive "$entry_point" --project "$project" --outdir "$functions" --output-metadata "$metadata" --production-source-bundle "$generated/base_translation_sources.bin" --no-function-files --prune-stale --threads "$translator_threads" +step emit-base-manifest 'Creating the local base translation manifest' +translator emit-base-manifest --project "$project" --out "$manifest_dir" --functions-dir "$functions" --translation-output-metadata "$metadata" --region P +if (( builds_retro )); then + mod_out="$workspace/build/mods/retro_rewind_full_cpp"; args=(translate-mod --project "$project" --profile retro-rewind --base-manifest "$manifest" --base-translation-output-metadata "$metadata" --code-pul "$retro_root/Binaries/Code.pul" --mod-root "$retro_root" --mod-name 'Retro Rewind' --region P --out "$mod_out" --prefer-cached-inputs --emit-cpp --threads "$translator_threads") + if (( skip_retro_wfc )); then args+=(--skip-retro-wfc); else offline_payload="$retro_wfc/binary/payload.RMCPD00.bin"; assert_file "$offline_payload" 'Offline Retro-WFC payload'; args+=(--retro-wfc-payload "$offline_payload"); fi + step translate-mod 'Translating Retro Rewind'; translator "${args[@]}" +fi +step generate-data-init 'Generating local game data initialization'; translator generate-data-init --project "$project" +args=(emit-build-shards --project "$project" --base-metadata "$metadata" --base-functions-dir "$functions" --native-source-dir "$workspace/runtime/src" --out "$shards") +if (( builds_retro )); then args+=(--resolved-profile "$mod_out/resolved_dispatch_profile.json" --retro-cpp-dir "$mod_out/cpp"); fi +step emit-build-shards 'Preparing native build shards'; translator "${args[@]}" + +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" +targets=(); [[ "$profile" != retro-rewind ]] && targets+=(WiiCompiled); [[ "$profile" != base ]] && targets+=(RetroRewind) +step compile "Compiling ${targets[*]} locally"; "$cmake_bin" --build "$native_build" --target "${targets[@]}" --parallel "$global_jobs" +if [[ "$profile" != retro-rewind ]]; then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product WiiCompiled --output-dir "${base_output_dir:-$output_dir}"; fi +if (( builds_retro )); then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product RetroRewind --output-dir "$output_dir"; fi +printf 'MKWCBUILD:OUTPUT=%s\n' "$output_dir" diff --git a/Launcher/macos/build-setup-pkg.command b/Launcher/macos/build-setup-pkg.command new file mode 100755 index 0000000..50082c9 --- /dev/null +++ b/Launcher/macos/build-setup-pkg.command @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# Maintainer release builder. It packages setup/source/tooling only -- never a +# translated executable, extracted DATA tree, disc image, or Retro Rewind data. +set -euo pipefail + +fail() { printf 'build-setup-pkg.command: error: %s\n' "$*" >&2; exit 1; } +# Release payloads must not inherit Finder metadata, resource forks, or a +# downloaded-file quarantine bit from a maintainer's working volume. +copy_clean() { DITTONORSRC=1 ditto --norsrc --noqtn "$@"; } +usage() { + cat <<'EOF' +Usage: build-setup-pkg.command --nodtool PATH --translator PATH --cmake-root DIR --ninja PATH --output PKG [options] + +Creates a game-code-free WiiCompiled Setup.pkg. The supplied tools must be +maintainer-verified, redistributable macOS arm64 artifacts. The resulting pkg +is unsigned unless --installer-identity is supplied; releases should sign and +notarize it with a Developer ID Installer certificate. + + --workspace DIR Repository root (default: script's grandparent) + --version VERSION Bundle/package version (default: 0.1.0) + --installer-identity NAME Developer ID Installer identity for productbuild +EOF +} + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +workspace=$(cd "$script_dir/../.." && pwd); nodtool=""; translator=""; cmake_root=""; ninja=""; output=""; version=0.1.0; identity="" +while (($#)); do + case "$1" in + --workspace) workspace=${2:-}; shift 2 ;; + --nodtool) nodtool=${2:-}; shift 2 ;; + --translator) translator=${2:-}; shift 2 ;; + --cmake-root) cmake_root=${2:-}; shift 2 ;; + --ninja) ninja=${2:-}; shift 2 ;; + --output) output=${2:-}; shift 2 ;; + --version) version=${2:-}; shift 2 ;; + --installer-identity) identity=${2:-}; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) fail "unknown option: $1" ;; + esac +done +version=${version#v} +[[ "$version" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]] || fail '--version must contain one to three period-separated integers' +IFS=. read -r version_major version_minor version_patch <<< "$version" +short_version="$version_major.${version_minor:-0}.${version_patch:-0}" +for tool in pkgbuild productbuild ditto codesign; do command -v "$tool" >/dev/null || fail "required macOS tool unavailable: $tool"; done +[[ -x "$nodtool" ]] || fail '--nodtool must name an executable' +[[ -x "$translator" ]] || fail '--translator must name an executable' +[[ -x "$cmake_root/bin/cmake" ]] || fail '--cmake-root must contain bin/cmake' +[[ -x "$ninja" ]] || fail '--ninja must name an executable' +"$nodtool" --version >/dev/null || fail '--nodtool did not run successfully' +workspace=$(cd "$workspace" && pwd); output=$(cd "$(dirname "$output")" && pwd)/$(basename "$output") +stage=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-pkg.XXXXXX") +trap 'rm -rf "$stage"' EXIT +app="$stage/root/Applications/WiiCompiled Setup.app" +resources="$app/Contents/Resources" +mkdir -p "$app/Contents/MacOS" "$resources/tools" +cat > "$app/Contents/Info.plist" < + + +CFBundleExecutableWiiCompiledSetup +CFBundleIdentifierorg.wiicompiled.setup +CFBundleNameWiiCompiled Setup +CFBundlePackageTypeAPPL +CFBundleShortVersionString$short_version +CFBundleVersion$version +LSMinimumSystemVersion14.0 + +EOF +cat > "$app/Contents/MacOS/WiiCompiledSetup" <<'EOF' +#!/usr/bin/env bash +resources="$(cd "$(dirname "$0")/../Resources" && pwd)" + +# Finder launches an app with no terminal attached. The setup work deliberately +# writes human-readable build progress to stdout, so run its .command entry +# point in Terminal instead of discarding that output behind an inert app icon. +exec /usr/bin/osascript - "$resources/setup.command" "$@" <<'APPLESCRIPT' +on run argv + set commandLine to quoted form of (item 1 of argv) + if (count of argv) > 1 then + repeat with argumentIndex from 2 to (count of argv) + set commandLine to commandLine & " " & quoted form of (item argumentIndex of argv) + end repeat + end if + tell application "Terminal" + activate + do script commandLine + end tell +end run +APPLESCRIPT +EOF +chmod +x "$app/Contents/MacOS/WiiCompiledSetup" +copy_clean "$script_dir/setup.command" "$resources/setup.command"; chmod +x "$resources/setup.command" +# Copy only the build inputs. This deliberately avoids a maintainer's ignored +# output directories, local disc extraction, and developer-only packaging. +mkdir -p "$resources/workspace" +for source in aurora-main projects runtime translator; do + [[ -d "$workspace/$source" ]] || fail "required workspace directory is missing: $source" + copy_clean "$workspace/$source" "$resources/workspace/$source" +done +mkdir -p "$resources/workspace/Launcher/macos" +copy_clean "$workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/local-build-macos.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" +chmod +x "$resources/workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/macos/"*.command +mkdir -p "$resources/tools/cmake" +copy_clean "$nodtool" "$resources/tools/nodtool"; chmod +x "$resources/tools/nodtool" +copy_clean "$translator" "$resources/tools/Translator.Cli"; chmod +x "$resources/tools/Translator.Cli" +copy_clean "$cmake_root" "$resources/tools/cmake" +copy_clean "$ninja" "$resources/tools/ninja"; chmod +x "$resources/tools/ninja" +copy_clean "$workspace/LICENSE" "$resources/LICENSE" +copy_clean "$workspace/THIRD-PARTY-NOTICES.md" "$resources/THIRD-PARTY-NOTICES.md" +codesign --force --deep --sign - "$app" +pkg="$stage/WiiCompiled-Setup-unsigned.pkg" +DITTONORSRC=1 COPYFILE_DISABLE=1 pkgbuild --root "$stage/root" --identifier org.wiicompiled.setup --version "$version" --install-location / "$pkg" +if [[ -n "$identity" ]]; then productbuild --sign "$identity" --package "$pkg" "$output"; else ditto "$pkg" "$output"; fi +printf 'Created game-code-free package: %s\n' "$output" diff --git a/Launcher/macos/extract-disc.command b/Launcher/macos/extract-disc.command new file mode 100755 index 0000000..663b542 --- /dev/null +++ b/Launcher/macos/extract-disc.command @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# Extract a user-owned Mario Kart Wii PAL (RMCP01) disc image for a local build. +# This script deliberately contains no game data and is intended for the macOS +# setup application and for maintainers testing that setup path. +set -euo pipefail + +readonly EXPECTED_DOL_SHA256=80d18895b39c63bd80f457398bfcbb91b7d16ac116a41a88967e954080155b05 +readonly EXPECTED_REL_SHA256=16d9d146112541fefea701ecb5bc1a496f9d50e4a752fbb5b6778e7c6399f67d + +fail() { printf 'extract-disc.command: error: %s\n' "$*" >&2; exit 1; } +sha256() { shasum -a 256 "$1" | awk '{ print $1 }'; } + +usage() { + cat <<'EOF' +Usage: extract-disc.command --game IMAGE --assets-dir DIR --nodtool PATH + +Extracts a user-owned Mario Kart Wii PAL RMCP01 image into DIR. The final +layout is DIR/main.dol, DIR/StaticR.rel, and DIR/DATA. Existing data is left +untouched unless the complete new extraction passes both content hash checks. +EOF +} + +game="" +assets_dir="" +nodtool="" +while (($#)); do + case "$1" in + --game) game=${2:-}; shift 2 ;; + --assets-dir) assets_dir=${2:-}; shift 2 ;; + --nodtool) nodtool=${2:-}; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) fail "unknown option: $1" ;; + esac +done + +[[ -f "$game" ]] || fail "disc image does not exist: $game" +[[ -n "$assets_dir" ]] || fail "--assets-dir is required" +[[ -x "$nodtool" ]] || fail "nodtool is not executable: $nodtool" + +assets_dir=$(mkdir -p "$assets_dir" && cd "$assets_dir" && pwd) +scratch=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-disc.XXXXXX") +cleanup() { rm -rf "$scratch"; } +trap cleanup EXIT + +printf 'MKWCBUILD:STEP:validate-disc Checking the selected disc image\n' +"$nodtool" info "$game" >/dev/null +printf 'MKWCBUILD:STEP:extract-disc Extracting the user-owned disc image\n' +"$nodtool" extract "$game" "$scratch/extracted" + +dol=$(find "$scratch/extracted" -type f -path '*/sys/main.dol' -print -quit) +rel=$(find "$scratch/extracted" -type f -path '*/files/rel/StaticR.rel' -print -quit) +[[ -n "$dol" ]] || fail 'nodtool extraction did not contain sys/main.dol' +[[ -n "$rel" ]] || fail 'nodtool extraction did not contain files/rel/StaticR.rel' +[[ $(sha256 "$dol") == "$EXPECTED_DOL_SHA256" ]] || fail 'disc is not the supported clean PAL RMCP01 Mario Kart Wii image' +[[ $(sha256 "$rel") == "$EXPECTED_REL_SHA256" ]] || fail 'disc has an unexpected StaticR.rel; use a clean PAL RMCP01 image' + +data_root=$(dirname "$(dirname "$dol")") +[[ -d "$data_root/files" ]] || fail 'nodtool extraction did not contain the Wii files directory' + +# Stage beside the destination so the final replacement stays on one volume. +stage="$assets_dir/.extract-stage-$$" +rm -rf "$stage" +mkdir -p "$stage" +ditto "$data_root" "$stage/DATA" +ditto "$dol" "$stage/main.dol" +ditto "$rel" "$stage/StaticR.rel" + +backup="$assets_dir/.previous-extraction-$(date +%Y%m%d-%H%M%S)" +if [[ -e "$assets_dir/DATA" || -e "$assets_dir/main.dol" || -e "$assets_dir/StaticR.rel" ]]; then + mkdir -p "$backup" + for item in DATA main.dol StaticR.rel; do + [[ -e "$assets_dir/$item" ]] && mv "$assets_dir/$item" "$backup/$item" + done +fi +mv "$stage/DATA" "$assets_dir/DATA" +mv "$stage/main.dol" "$assets_dir/main.dol" +mv "$stage/StaticR.rel" "$assets_dir/StaticR.rel" +rmdir "$stage" +rm -rf "$backup" + +printf 'MKWCBUILD:STEP:disc-ready Verified and extracted clean PAL RMCP01 game assets\n' diff --git a/Launcher/macos/publish-app.command b/Launcher/macos/publish-app.command new file mode 100755 index 0000000..b384a55 --- /dev/null +++ b/Launcher/macos/publish-app.command @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# Turn one locally compiled macOS product into a self-contained .app bundle. +set -euo pipefail + +fail() { printf 'publish-app.command: error: %s\n' "$*" >&2; exit 1; } +usage() { + cat <<'EOF' +Usage: publish-app.command --build-dir DIR --product {WiiCompiled|RetroRewind} --output-dir DIR + +Copies a locally built product and its runtime assets into OUTPUT-DIR/.app. +It bundles non-system dylibs, rewrites their install names, and ad-hoc signs the +result. This is suitable for local use; a release must replace ad-hoc signing +with the project's Developer ID signing and notarization process. +EOF +} + +build_dir=""; product=""; output_dir="" +while (($#)); do + case "$1" in + --build-dir) build_dir=${2:-}; shift 2 ;; + --product) product=${2:-}; shift 2 ;; + --output-dir) output_dir=${2:-}; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) fail "unknown option: $1" ;; + esac +done +[[ "$product" == WiiCompiled || "$product" == RetroRewind ]] || fail '--product must be WiiCompiled or RetroRewind' +for tool in codesign ditto install_name_tool otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done +[[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product" +for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done + +app="$output_dir/$product.app" +macos="$app/Contents/MacOS" +frameworks="$app/Contents/Frameworks" +resources="$app/Contents/Resources" +rm -rf "$app" +mkdir -p "$macos" "$frameworks" "$resources" +cat > "$app/Contents/Info.plist" < + + + CFBundleDevelopmentRegionen + CFBundleExecutable$product + CFBundleIdentifierorg.wiicompiled.$product + CFBundleInfoDictionaryVersion6.0 + CFBundleName$product + CFBundlePackageTypeAPPL + CFBundleShortVersionString0.1.0 + CFBundleVersion1 + LSMinimumSystemVersion14.0 + NSHighResolutionCapable + +EOF +ditto "$build_dir/$product" "$macos/$product" +for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do + ditto "$build_dir/$asset" "$resources/$asset" + ln -s "../Resources/$asset" "$macos/$asset" +done + +# Build a closure of Homebrew dylibs. System libraries remain system references. +queue=("$macos/$product") +while ((${#queue[@]})); do + current=${queue[0]} + queue=("${queue[@]:1}") + while IFS= read -r dependency; do + [[ "$dependency" == /opt/homebrew/* || "$dependency" == /usr/local/* ]] || continue + [[ -f "$dependency" ]] || continue + name=$(basename "$dependency") + if [[ ! -f "$frameworks/$name" ]]; then + ditto "$dependency" "$frameworks/$name" + install_name_tool -id "@rpath/$name" "$frameworks/$name" + queue+=("$frameworks/$name") + fi + done < <(otool -L "$current" | tail -n +2 | awk '{print $1}') +done +while IFS= read -r binary; do + while IFS= read -r old; do + [[ "$old" == /opt/homebrew/* || "$old" == /usr/local/* ]] || continue + name=$(basename "$old") + [[ -f "$frameworks/$name" ]] || continue + if [[ "$binary" == "$macos/$product" ]]; then + install_name_tool -change "$old" "@executable_path/../Frameworks/$name" "$binary" + else + install_name_tool -change "$old" "@loader_path/$name" "$binary" + fi + done < <(otool -L "$binary" | tail -n +2 | awk '{print $1}') +done < <(find "$frameworks" -type f -print; printf '%s\n' "$macos/$product") + +find "$frameworks" -type f -exec codesign --force --sign - {} + +codesign --force --deep --sign - "$app" +codesign --verify --deep --strict "$app" +printf 'MKWCBUILD:APP=%s\n' "$app" diff --git a/Launcher/macos/setup.command b/Launcher/macos/setup.command new file mode 100755 index 0000000..8676e7a --- /dev/null +++ b/Launcher/macos/setup.command @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +# Entry point bundled in WiiCompiled Setup.app. The package contains source and +# tools only; a user's own verified disc is extracted into Application Support. +set -euo pipefail + +resources=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +workspace_source="$resources/workspace" +nodtool="$resources/tools/nodtool" +translator="$resources/tools/Translator.Cli" +cmake_bin="$resources/tools/cmake/bin/cmake" +ninja_bin="$resources/tools/ninja" +support_root="$HOME/Library/Application Support/WiiCompiled" +workspace="$support_root/BuildWorkspace" +products="$support_root/Products" + +fail() { printf 'WiiCompiled Setup: %s\n' "$*" >&2; exit 1; } +notice() { /usr/bin/osascript -e "display dialog \"${1//\"/\\\"}\" buttons {\"OK\"} default button \"OK\" with icon caution" >/dev/null; } +usage() { + cat <<'EOF' +Usage: setup.command --game IMAGE [--retro-dir DIR] [--install-location {user|applications}] + +Without arguments this script opens file pickers. It is normally launched by +WiiCompiled Setup.app, not run directly. +EOF +} + +game=""; retro_dir=""; install_location=applications +while (($#)); do + case "$1" in + --game) game=${2:-}; shift 2 ;; + --retro-dir) retro_dir=${2:-}; shift 2 ;; + --install-location) install_location=${2:-}; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) fail "unknown option: $1" ;; + esac +done +[[ "$install_location" == user || "$install_location" == applications ]] || fail '--install-location must be user or applications' + +if [[ -z "$game" ]]; then + game=$(/usr/bin/osascript <<'APPLESCRIPT' +set selectedFile to choose file with prompt "Choose your clean Mario Kart Wii PAL (RMCP01) disc image" +POSIX path of selectedFile +APPLESCRIPT +) || exit 0 + choice=$(/usr/bin/osascript -e 'button returned of (display dialog "Would you like to build Retro Rewind too?" buttons {"Base game only", "Choose Retro Rewind folder"} default button "Base game only")') + if [[ "$choice" == 'Choose Retro Rewind folder' ]]; then + retro_dir=$(/usr/bin/osascript <<'APPLESCRIPT' +set selectedFolder to choose folder with prompt "Choose the RetroRewind6 folder (or its parent folder)" +POSIX path of selectedFolder +APPLESCRIPT +) || exit 0 + fi +fi + +[[ -x "$nodtool" ]] || fail 'the packaged nodtool is missing or not executable' +[[ -x "$translator" ]] || fail 'the packaged Translator.Cli is missing or not executable' +[[ -x "$cmake_bin" && -x "$ninja_bin" ]] || fail 'the packaged CMake or Ninja tool is missing' +if ! /usr/bin/xcode-select -p >/dev/null 2>&1; then + notice 'Xcode Command Line Tools are required once to compile WiiCompiled. Click OK, complete the Apple installer, then run WiiCompiled Setup again.' + /usr/bin/xcode-select --install || true + exit 1 +fi + +mkdir -p "$support_root" "$products" +if [[ ! -d "$workspace/.git" && ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then + printf 'Preparing the local build workspace...\n' + rm -rf "$workspace" + /usr/bin/ditto "$workspace_source" "$workspace" +fi + +profile=base +build_args=(--workspace "$workspace" --game "$game" --nodtool "$nodtool" --output-dir "$products") +if [[ -n "$retro_dir" ]]; then + profile=both + # Online play needs the shared Retro-WFC payload. Keep it in the per-user + # support directory rather than the packaged app or build workspace, then + # verify its pinned signature before publishing it into the local cache. + retro_wfc_dir="$support_root/RetroWfcPayload" + retro_wfc_payload="$retro_wfc_dir/binary/payload.RMCPD00.bin" + if [[ -f "$retro_wfc_payload" ]] && ! "$translator" validate-retro-wfc-payload --directory "$retro_wfc_dir"; then + printf 'Discarding an invalid cached Retro-WFC payload...\n' >&2 + rm -f "$retro_wfc_payload" + fi + if [[ ! -f "$retro_wfc_payload" ]]; then + printf 'Downloading the Retro-WFC payload needed for online play...\n' + mkdir -p "$retro_wfc_dir" + payload_stage=$(mktemp -d "$retro_wfc_dir/.payload-download.XXXXXX") + temporary_payload="$payload_stage/binary/payload.RMCPD00.bin" + mkdir -p "$(dirname "$temporary_payload")" + trap 'rm -rf "$payload_stage"' EXIT + /usr/bin/curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \ + --retry 1 --output "$temporary_payload" \ + 'http://nas.play.rwfc.net/payload?g=RMCPD00' || fail 'could not download the Retro-WFC payload needed for online play' + "$translator" validate-retro-wfc-payload --directory "$payload_stage" || \ + fail 'downloaded Retro-WFC payload failed signature validation' + mkdir -p "$retro_wfc_dir/binary" + mv "$temporary_payload" "$retro_wfc_payload" + rmdir "$payload_stage/binary" "$payload_stage" + trap - EXIT + fi + build_args+=(--profile both --base-output-dir "$products" --retro-rewind-package-dir "$retro_dir" --retro-wfc-offline-dir "$retro_wfc_dir") +fi +"$workspace/Launcher/local-build-macos.command" "${build_args[@]}" --profile "$profile" --cmake "$cmake_bin" --ninja "$ninja_bin" --translator-bin "$translator" + +config="$support_root/Config.toml" +toml_string() { printf '%s' "$1" | sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g'; } +set_path() { + local key=$1 value=$2 encoded line temporary + encoded=$(toml_string "$value"); line="$key = \"$encoded\""; temporary="$config.tmp" + touch "$config" + if grep -q '^[[:space:]]*\[paths\][[:space:]]*$' "$config"; then + awk -v key="$key" -v line="$line" ' + /^[[:space:]]*\[paths\][[:space:]]*$/ { print; print line; inside = 1; next } + inside && /^[[:space:]]*\[/ { inside = 0 } + inside && $0 ~ "^[[:space:]]*" key "[[:space:]]*=" { next } + { print } + ' "$config" > "$temporary" + else + { cat "$config"; printf '\n[paths]\n%s\n' "$line"; } > "$temporary" + fi + mv "$temporary" "$config" +} +set_path dvd_root "$workspace/Assets/DATA" +[[ -n "$retro_dir" ]] && set_path retro_rewind_root "$retro_dir" + +destination="$HOME/Applications" +if [[ "$install_location" == applications ]]; then destination=/Applications; fi +install_app() { + local app=$1 + [[ -d "$products/$app" ]] || return 0 + if [[ "$destination" == /Applications ]]; then + command="mkdir -p /Applications && rm -rf '/Applications/$app' && ditto '$products/$app' '/Applications/$app'" + /usr/bin/osascript -e "do shell script \"$command\" with administrator privileges" + else + mkdir -p "$destination"; rm -rf "$destination/$app"; /usr/bin/ditto "$products/$app" "$destination/$app" + fi +} +install_app WiiCompiled.app +[[ "$profile" == both ]] && install_app RetroRewind.app +notice "Installation complete. Your apps are in $destination." diff --git a/README.md b/README.md index 8dd8426..29d6679 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,8 @@ adapter must be switched to the WinUSB driver once (Zadig). - GPU: GTX 1650 / RX 6400 / Arc A310 or higher - CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher - About 20 GB of free disk space during installation (Final game size ~5 GB) +- macOS 14 (Sonoma) or later on Apple Silicon +- On macOS, Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing) - A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM, GCZ, CISO, WBFS, WIA and RVZ are accepted. @@ -86,6 +88,7 @@ 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 launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it. + > [!CAUTION] > Only take builds from this repository's > [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an diff --git a/aurora-main/lib/aurora.cpp b/aurora-main/lib/aurora.cpp index b679e32..bf76d94 100644 --- a/aurora-main/lib/aurora.cpp +++ b/aurora-main/lib/aurora.cpp @@ -255,6 +255,14 @@ FrameWorkerState g_frameWorker; bool frame_worker_requested() noexcept { #ifdef AURORA_ENABLE_GX static const bool enabled = [] { +#if defined(__APPLE__) + // ImGui's SDL backend may raise an SDL window from ImGui::NewFrame(). On + // macOS that reaches AppKit, whose window operations are main-thread-only; + // doing it on the frame worker terminates the process with EXC_BREAKPOINT. + // Keep all SDL/ImGui work on the calling thread until the worker no longer + // owns frame preparation on Apple platforms. + return false; +#endif #if defined(_WIN32) // RenderDoc's D3D12 layer is injected before Aurora starts and needs device and command // ownership on one thread, so keep frame submission synchronous there. @@ -1516,6 +1524,15 @@ std::vector encode_sealed_frame(gfx::SealedFrame& sealedFrame, // Phase 3: hand the encoded group to whoever owns presentation. void publish_presentations(std::vector&& presentationJobs, bool interpolationActive) { +#if defined(__APPLE__) + (void)interpolationActive; + // Presenting reaches SDL/AppKit, whose window operations must stay on the + // main thread. Interpolation normally starts the presenter worker, so keep + // its jobs synchronous on Apple platforms. + for (const auto& job : presentationJobs) { + present_presentation_job(job); + } +#else // Keep presentation on the presenter whenever the async frame worker runs, even with // interpolation off, so every mode shares one surface/resize path. RenderDoc keeps the sync path. if (frame_worker_requested() || interpolationActive || @@ -1526,6 +1543,7 @@ void publish_presentations(std::vector&& presentationJobs, bool present_presentation_job(job); } } +#endif } void record_frame_telemetry() { diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 532da95..6fd79e2 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -1,18 +1,28 @@ cmake_minimum_required(VERSION 3.16) project(mkw_recompiled) -if((NOT (WIN32 AND MINGW)) AND (NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")) - message(FATAL_ERROR "WiiCompiled requires Windows (LLVM-MinGW) or native Linux") +if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8) + message(FATAL_ERROR "WiiCompiled requires a 64-bit Clang toolchain") endif() -if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR - NOT CMAKE_SIZEOF_VOID_P EQUAL 8 OR - NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$") - message(FATAL_ERROR "WiiCompiled requires 64-bit Clang targeting x86_64 or aarch64") + +if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$") + set(MKW_PLATFORM_WINDOWS TRUE) +elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64)$") + # The first native macOS target is Apple Silicon. Intel and universal + # binaries remain future compatibility work; do not silently claim them. + set(MKW_PLATFORM_MACOS TRUE) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$") + set(MKW_PLATFORM_LINUX TRUE) +else() + message(FATAL_ERROR + "WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS arm64") endif() if(NOT CMAKE_BUILD_TYPE STREQUAL "Release") message(FATAL_ERROR "WiiCompiled only supports Release builds") endif() +option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON) + # Preprocessor definitions that belong to this project's own code (the runtime, # the translated shards and the product glue) and to nothing else. They are # applied directory-scoped, immediately after the aurora add_subdirectory() call, @@ -49,17 +59,14 @@ target_include_directories(mkw_pugixml PUBLIC third_party/pugixml) target_compile_features(mkw_pugixml PUBLIC cxx_std_17) set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF) -# Non-Windows guest-fiber scheduling (runtime/src/fiber_manager.cpp) needs a symmetric +# Linux guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric # stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers # directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike # asymmetric resume/yield coroutine libraries, which would need every call site restructured). # Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is -# separately BSD-style licensed, see third_party/libco/LICENSE) - all of libco's non-Windows -# CPU-architecture backends are kept, even though libco.c's own preprocessor dispatch -# (__amd64__/__i386__/__arm__/__aarch64__/etc.) only ever selects amd64.c for this project's -# x86_64-only target (see the platform/arch check above). Windows keeps using native Fibers -# untouched, so this target is never built there. -if(NOT WIN32) +# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers +# and macOS uses the project's x18-safe AArch64 assembly backend, so this target is Linux-only. +if(MKW_PLATFORM_LINUX) add_library(mkw_libco STATIC third_party/libco/libco.c) add_library(mkw::libco ALIAS mkw_libco) target_include_directories(mkw_libco PUBLIC third_party/libco) @@ -130,31 +137,35 @@ else() message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing") endif() set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE) - if(WIN32) + if(MKW_PLATFORM_WINDOWS) set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE) + set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE) + set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE) set(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "" FORCE) - set(DAWN_USE_WINDOWS_UI OFF CACHE BOOL "" FORCE) - else() - # Non-Windows (Linux): mirrors aurora-main's own - # _aurora_dawn_set_platform_backends() choice for this platform - Vulkan only, no - # D3D/HLSL. Kept in sync here because this project's own CMake FORCEs these cache - # variables before aurora-main's add_subdirectory() runs, which pre-empts aurora's - # auto-detection (CACHE ... INTERNAL "" without FORCE never overrides an existing value). + elseif(MKW_PLATFORM_MACOS) set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_VULKAN OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_METAL ON CACHE BOOL "" FORCE) + set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE) + else() + set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE) + set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE) set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE) endif() - set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE) set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE) set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE) # Provide a tiny stub for DXProgrammableCapture when the SDK/PIX headers are # missing (common on MinGW). Dawn only includes the header; no symbols are # referenced when PIX isn't present. - set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs") - if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h") - file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR}) - file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h" -"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX is absent.\n") + if(MKW_PLATFORM_WINDOWS) + set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs") + if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h") + file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR}) + file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h" +"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX isn't present.\n") + endif() endif() # Deliberately NOT injected project-wide. Only a from-source Dawn build ever # includes DXProgrammableCapture.h, and this tree consumes Dawn as a prebuilt @@ -183,7 +194,9 @@ else() if(TARGET ${t}) set_target_properties(${t} PROPERTIES UNITY_BUILD OFF) target_compile_options(${t} PRIVATE -O3 -ffast-math -w -pipe) - target_include_directories(${t} PRIVATE ${MKW_DX_STUB_DIR}) + if(MKW_PLATFORM_WINDOWS) + target_include_directories(${t} PRIVATE ${MKW_DX_STUB_DIR}) + endif() # Aurora's own sources include Windows headers and call std::min/max; # they relied on the old project-wide NOMINMAX that no longer leaks # into this subtree, so the define is applied per target here. @@ -228,6 +241,18 @@ endif() # a registration file is silently never compiled and never errors. The stale-glob # failure mode is worth far more than the milliseconds. file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp") +if(MKW_PLATFORM_MACOS) + list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory.cpp") + # HostContext's Apple Silicon backend is implemented in a small assembly + # companion. It must be part of the product runtime as well as the + # standalone context test; otherwise the final executable is missing + # mkw_co_init/mkw_co_switch at link time. + enable_language(ASM) + list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S") +else() + list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp") +endif() +set(MKW_PLATFORM_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/platform/host_platform.cpp") set(MKW_BASE_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/base_product.cpp") set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro_rewind_product.cpp") # The host ISA guard is the one translation unit that must not receive the @@ -235,31 +260,113 @@ set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro # mkw_runtime_common. See cmake/PublicProducts.cmake and the file's own header. set(MKW_CPU_BASELINE_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/host_cpu_baseline.cpp") list(REMOVE_ITEM SOURCES ${MKW_BASE_PRODUCT_SOURCE} ${MKW_RETRO_REWIND_PRODUCT_SOURCE} - ${MKW_CPU_BASELINE_SOURCE}) + ${MKW_CPU_BASELINE_SOURCE} ${MKW_PLATFORM_SOURCE}) + +# This deliberately small library contains host services that are safe to +# validate before guest memory and fiber work makes a full runtime build viable. +add_library(mkw_platform STATIC "${MKW_PLATFORM_SOURCE}") +target_include_directories(mkw_platform PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include") +target_compile_features(mkw_platform PUBLIC cxx_std_17) +set_target_properties(mkw_platform PROPERTIES UNITY_BUILD OFF) + +# Keep these independent from Aurora's BUILD_TESTING option: they validate the +# project's host-platform contracts, not Aurora's third-party test suite. +enable_testing() +add_executable(mkw_platform_paths_tests "${CMAKE_CURRENT_LIST_DIR}/tests/platform_paths_tests.cpp") +target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform) +target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17) +add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests) + +# HostContext deliberately keeps the platform-specific context primitive out +# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future +# refactors cannot silently remove its headers, implementation, or link edge. +if(MKW_PLATFORM_LINUX) + add_executable(mkw_linux_host_context_tests + "${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp" + "${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp") + target_include_directories(mkw_linux_host_context_tests PRIVATE + "${CMAKE_CURRENT_LIST_DIR}/include" + "${CMAKE_CURRENT_LIST_DIR}/third_party/libco") + target_compile_features(mkw_linux_host_context_tests PRIVATE cxx_std_17) + target_link_libraries(mkw_linux_host_context_tests PRIVATE mkw::libco) + add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests) +endif() + +if(MKW_PLATFORM_MACOS) + # Exercise the Apple Silicon context ABI and the public host-memory + # contracts separately from translated products. + enable_language(ASM) + add_executable(mkw_macos_context_abi_tests + "${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp" + "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S") + target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17) + add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests) + + add_executable(mkw_macos_host_context_tests + "${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp" + "${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp" + "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S") + target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include") + 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_executable(mkw_macos_guest_flat_memory_tests + "${CMAKE_CURRENT_LIST_DIR}/tests/macos_guest_flat_memory_tests.cpp" + "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp") + target_include_directories(mkw_macos_guest_flat_memory_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include") + target_compile_features(mkw_macos_guest_flat_memory_tests PRIVATE cxx_std_17) + add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests) +endif() # The translator emits the complete, content-addressed source graph. Consuming # this one manifest keeps configure independent of the 28k generated function # files and of optional Retro Rewind artifacts such as code.map. -set(MKW_TRANSLATED_SHARD_MANIFEST - "${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake" - CACHE FILEPATH "Translator-owned aggregate shard manifest") -# The prebuilt export only needs the aurora/third-party closure configured above, so a -# packaging machine without a translation stops here instead of failing. -if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}") - message(STATUS "No translator shard manifest; configuring the native prebuilt export only") - return() -endif() -if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}") - message(FATAL_ERROR - "Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. " - "Run Translator.Cli emit-build-shards first; see translator/README.md.") -endif() -include("${MKW_TRANSLATED_SHARD_MANIFEST}") -set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS}) -message(STATUS - "Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; " - "${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; " - "${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions") +if(MKW_BUILD_PRODUCTS) + set(MKW_TRANSLATED_SHARD_MANIFEST + "${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake" + CACHE FILEPATH "Translator-owned aggregate shard manifest") + # The prebuilt export only needs the aurora/third-party closure configured above, so a + # packaging machine without a translation stops here instead of failing. + if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}") + message(STATUS "No translator shard manifest; configuring the native prebuilt export only") + return() + endif() + if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}") + message(FATAL_ERROR + "Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. " + "Run Translator.Cli emit-build-shards first; see translator/README.md.") + endif() + include("${MKW_TRANSLATED_SHARD_MANIFEST}") + set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS}) + message(STATUS + "Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; " + "${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; " + "${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions") -set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}") -include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake") + set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}") + include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake") +else() + if(MKW_PLATFORM_MACOS) + # Compile-only audit of native runtime sources. It deliberately avoids + # translated products until their host dependencies are portable. + # These sources depend on generated/RuntimeConfig.h, which is emitted for + # a particular game by the translator and is intentionally unavailable + # in this platform-only configuration. + set(MKW_MACOS_NATIVE_AUDIT_SOURCES ${SOURCES}) + list(REMOVE_ITEM MKW_MACOS_NATIVE_AUDIT_SOURCES + "${CMAKE_CURRENT_LIST_DIR}/src/abi_bridge.cpp" + "${CMAKE_CURRENT_LIST_DIR}/src/hle/os/os_alarm.cpp") + add_library(mkw_macos_native_compile OBJECT ${MKW_MACOS_NATIVE_AUDIT_SOURCES}) + target_include_directories(mkw_macos_native_compile PRIVATE + "${CMAKE_CURRENT_LIST_DIR}/include" "${CMAKE_CURRENT_LIST_DIR}/src" + "${CMAKE_CURRENT_LIST_DIR}/.." "${CMAKE_CURRENT_LIST_DIR}/../aurora-main/include") + target_compile_features(mkw_macos_native_compile PRIVATE cxx_std_20) + target_compile_definitions(mkw_macos_native_compile PRIVATE SDL_MAIN_HANDLED TARGET_PC) + target_link_libraries(mkw_macos_native_compile PRIVATE + aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx + mkw::pugixml mkw::toml11 mkw::cryptopp) + set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF) + endif() + add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform) + message(STATUS "Translated product targets disabled (MKW_BUILD_PRODUCTS=OFF)") +endif() diff --git a/runtime/cmake/PublicProducts.cmake b/runtime/cmake/PublicProducts.cmake index 1987cd5..ad2a535 100644 --- a/runtime/cmake/PublicProducts.cmake +++ b/runtime/cmake/PublicProducts.cmake @@ -25,6 +25,11 @@ if(EXISTS "${DATA_INIT_BLOB_ASM}") endif() list(REMOVE_DUPLICATES SOURCES) +if(MKW_PLATFORM_MACOS) + find_library(MKW_IOKIT_FRAMEWORK IOKit REQUIRED) + find_library(MKW_COREFOUNDATION_FRAMEWORK CoreFoundation REQUIRED) +endif() + function(mkw_apply_common_compile_options target) target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe) endfunction() @@ -76,10 +81,10 @@ target_compile_definitions(mkw_runtime_common PRIVATE _DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION) target_link_libraries(mkw_runtime_common PRIVATE aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx) -target_link_libraries(mkw_runtime_common PRIVATE mkw::pugixml mkw::toml11 mkw::cryptopp) -if(WIN32) +target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp) +if(MKW_PLATFORM_WINDOWS) target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp) -else() +elseif(MKW_PLATFORM_LINUX) # ${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. target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS}) @@ -127,16 +132,16 @@ set_target_properties(mkw_runtime_common PROPERTIES UNITY_BUILD ON UNITY_BUILD_M target_precompile_headers(mkw_runtime_common PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h") mkw_apply_common_compile_options(mkw_runtime_common) -# Host ISA guard. Everything in MKW_ALL_BUILD_TARGETS below is compiled with -# -march=x86-64-v3; this object library deliberately is not, which -# is the whole point of keeping it out of mkw_runtime_common. It runs a CPUID -# check from a C initializer so an unsupported machine gets a readable error -# instead of an illegal-instruction crash. Excluded from the unity build and the -# precompiled header because both are produced with the owning target's flags. -add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}") -target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17) -set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF) -target_compile_options(mkw_cpu_baseline PRIVATE -w) +# Host ISA guard. Windows and Linux x86_64 product targets use x86-64-v3, so +# this object deliberately keeps the plain baseline ISA and checks the CPU +# before any AVX2/FMA code can execute. AArch64 has no equivalent optional ISA +# floor to probe: NEON/FMA are architectural requirements. +if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$") + add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}") + target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17) + set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF) + target_compile_options(mkw_cpu_baseline PRIVATE -w) +endif() if(NOT MKW_BASE_COMMON_SHARDS) message(FATAL_ERROR "Translator build graph contains no shared base shards") @@ -176,7 +181,9 @@ function(mkw_configure_product target) target_sources(${target} PRIVATE $) # Startup CPU check. Must stay a separate object library so it keeps the # plain baseline ISA while everything around it is built for x86-64-v3. - target_sources(${target} PRIVATE $) + if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$") + target_sources(${target} PRIVATE $) + endif() target_include_directories(${target} PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include" "${MKW_RUNTIME_SOURCE_DIR}/src" @@ -192,10 +199,14 @@ function(mkw_configure_product target) # include the same fat translated headers; bound them by the same pool. mkw_bound_translated_compiles(${target}) target_link_libraries(${target} PRIVATE - mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp) + mkw_platform mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp) target_link_libraries(${target} PRIVATE aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx) + if(MKW_PLATFORM_MACOS) + target_link_libraries(${target} PRIVATE + "${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}") + endif() if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake") include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake") aurora_copy_runtime_dlls(${target}) @@ -210,12 +221,12 @@ function(mkw_configure_product target) $ $) endif() - if(WIN32) + if(MKW_PLATFORM_WINDOWS) target_link_libraries(${target} PRIVATE dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp) set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE) - else() + elseif(MKW_PLATFORM_LINUX) # mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o # files via $, which does not propagate mkw_runtime_common's own # target_link_libraries (object libraries don't carry usage requirements to a consumer @@ -226,7 +237,7 @@ function(mkw_configure_product target) # objects (empty string on glibc >= 2.34, where dl* is in libc). target_link_libraries(${target} PRIVATE mkw::libco ${CMAKE_DL_LIBS}) endif() - if(WIN32) + if(MKW_PLATFORM_WINDOWS) foreach(runtime_dll libc++.dll libunwind.dll) execute_process( COMMAND "${CMAKE_CXX_COMPILER}" "--print-file-name=${runtime_dll}" @@ -302,13 +313,10 @@ else() message(STATUS "RetroRewind target disabled (run translate-mod and emit-build-shards)") endif() -# x86-64-v3 (SSE3/SSSE3/SSE4.1/FMA/AVX2/BMI2) is the baseline runtime/src/host_cpu_baseline.cpp -# guards against - a fixed, portable floor since an x86_64 build may run on a different machine -# than the one that built it. AArch64 has no such redistribution path here: every build this -# project produces runs only on the machine that built it (local-build.sh, and the AppImage which -# wraps it, always build from source on the target), so -mcpu=native is safe and strictly better - -# real per-core tuning (scheduling, whatever NEON/atomic extensions that exact CPU actually has) -# instead of the generic armv8-a baseline Clang would otherwise assume. +# Windows and Linux x86_64 share the x86-64-v3 floor that the CPU baseline +# object above checks. AArch64 builds are compiled locally for the host that +# will run them, so both Linux and Apple Silicon use the compiler's native CPU +# tuning rather than leaving target-specific performance on the table. if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$") set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3) elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$") diff --git a/runtime/include/fiber_manager.h b/runtime/include/fiber_manager.h index bdefcf3..cad48c4 100644 --- a/runtime/include/fiber_manager.h +++ b/runtime/include/fiber_manager.h @@ -6,6 +6,7 @@ #include #include #include +#include #if defined(_WIN32) #ifndef NOMINMAX @@ -22,7 +23,7 @@ // Forward declarations struct CpuContext; -// GuestFiberManager: each guest OSThread maps to a Windows Fiber. A scheduler fiber picks +// GuestFiberManager: each guest OSThread maps to a host context. A scheduler context picks // which guest fiber runs; a real timer thread queues VI retraces at the VI cadence. Guest // threads only switch at explicit yield points (OSSleepThread, OSYieldThread, ...), matching // Wii cooperative semantics exactly. @@ -39,7 +40,7 @@ enum class ThreadState : uint32_t { // Information about a guest fiber struct GuestFiber { - void* fiber = nullptr; // Windows fiber handle + void* fiber = nullptr; // Host context handle uint32_t entryPoint = 0; // Thread entry function uint32_t entryArg = 0; // Argument to entry function CpuContext cpuContext{}; // Saved CPU context for this fiber @@ -102,10 +103,6 @@ private: static void CALLBACK FiberProc(void* param); #else static void FiberProc(void* param); - // libco's co_create() entry points take no argument (unlike CreateFiber's FiberProc(void*)), - // so this trampoline reads the guest thread address staged by CreateGuestFiber() and forwards - // into the (platform-neutral-bodied) FiberProc above. See fiber_manager.cpp. - static void FiberProcTrampoline(); #endif // Switch from whichever fiber is currently active straight to the scheduler fiber, without // the SwitchToThread bookkeeping (CPU context save/restore, s_currentGuestThread). Used for @@ -117,9 +114,8 @@ private: static std::mutex s_mutex; static std::unordered_map s_fibers; static std::vector s_fibersPendingDelete; - // The scheduler's own "fiber": a Windows HFIBER, or (non-Windows) libco's cothread_t for - // whichever native call stack first called GuestFiberManager::Initialize() - both are - // plain void* handles, so one field serves both platforms. + // The scheduler's own host context. Its opaque handle is supplied by the + // active HostContext backend, so one field serves every supported host. static void* s_schedulerFiber; static uint32_t s_currentGuestThread; static bool s_initialized; @@ -135,4 +131,3 @@ private: extern std::atomic g_viRetracePendingCount; } // namespace Fiber - diff --git a/runtime/include/guest_flat_memory.h b/runtime/include/guest_flat_memory.h index 0973ae7..6c0bdd3 100644 --- a/runtime/include/guest_flat_memory.h +++ b/runtime/include/guest_flat_memory.h @@ -14,10 +14,17 @@ namespace GuestFlat { // Fixed base so the emitted access is `[reg + imm64-in-register]` with no load // of a global. inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull; +inline constexpr size_t kGuestPageSize = 0x1000; #if defined(__x86_64__) // 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap // placement. inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'1000'0000'0000ull; +#elif defined(__aarch64__) && defined(__APPLE__) +// Keep this well above the low address ranges that Darwin's ASLR may use for +// a PIE executable and its shared cache. Apple Silicon's user VA is wider +// than Linux's 39-bit minimum, so this 512 GiB region is available while the +// Linux AArch64 target retains its 64 GiB placement below. +inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0080'0000'0000ull; #elif defined(__aarch64__) // 16 TiB (this arch's x86_64 sibling value) is unreachable on any AArch64 // kernel configured for 39-bit virtual addresses (512 GiB ceiling) - common on @@ -58,6 +65,25 @@ struct FaultCounters { // True once the reservation exists and translated code may use the flat path. bool IsActive(); +// True when a host VM page covers more than one 4 KiB Wii page. In that +// configuration, guest-view page protection cannot safely represent per-Wii- +// page MMIO, deferred-read, or executable-write state, so general translated +// accesses must use the checked Memory::* path. +// Windows user mode and x86-64 always use a 4 KiB base page, so those builds +// fold this to a compile-time false: it appears in every flat access and must +// not become a hot-path load. Only AArch64, where the page size is a kernel +// configuration (4/16/64 KiB), has to probe it at runtime. +#if defined(_WIN32) || defined(__x86_64__) +#define MKW_GUEST_FLAT_FIXED_PAGE_SIZE 1 +#endif + +#if defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE) +inline constexpr bool RequiresCheckedAccess() noexcept { return false; } +#else +extern bool g_requiresCheckedAccess; +inline bool RequiresCheckedAccess() noexcept { return g_requiresCheckedAccess; } +#endif + // Reserves the 4 GiB space (once per process) and maps every requested region // into both views. Throws std::runtime_error with a precise diagnosis when the // reservation, the section objects or a view cannot be created - a silent diff --git a/runtime/include/host_context.h b/runtime/include/host_context.h new file mode 100644 index 0000000..4d7355f --- /dev/null +++ b/runtime/include/host_context.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +// HostContext is the deliberately small boundary between the guest scheduler +// and the host's cooperative-context facility. Windows uses native Fibers and +// Linux uses libco; macOS AArch64 uses the local assembly backend because it +// must preserve Darwin's platform-reserved x18 register, which libco's AArch64 +// backend does not save. Its handles are only valid on the thread that +// initialized the scheduler. +namespace HostContext { + +using Handle = void*; +using Entry = void (*)(void*); + +bool InitializeScheduler(Handle* scheduler); +void ShutdownScheduler(Handle scheduler); + +Handle Create(std::size_t stackSize, Entry entry, void* argument); +void Destroy(Handle context); +bool IsCurrent(Handle context); +void Switch(Handle target); + +} // namespace HostContext diff --git a/runtime/include/isa/ppc_isa_quantized.h b/runtime/include/isa/ppc_isa_quantized.h index 9f5c807..bc22f01 100644 --- a/runtime/include/isa/ppc_isa_quantized.h +++ b/runtime/include/isa/ppc_isa_quantized.h @@ -264,6 +264,8 @@ inline void PpcWritePairPsqInline(uint32_t addr, T first, T second) // reading stale bytes, and unmapped pages commit on demand, same as MemoryInline::Flat* loads. MKW_PPC_FORCE_INLINE const uint8_t* PpcTryGetPsqReadableHostInline(uint32_t addr) { + if (GuestFlat::RequiresCheckedAccess()) [[unlikely]] + return nullptr; return MKW_FLAT_GUEST_BASE + addr; } @@ -274,6 +276,8 @@ MKW_PPC_FORCE_INLINE const uint8_t* PpcTryGetPsqReadableHostInline(uint32_t addr // executable, and unmapped pages still trap. MKW_PPC_FORCE_INLINE uint8_t* PpcTryGetPsqWritableHostInline(uint32_t addr) { + if (GuestFlat::RequiresCheckedAccess()) [[unlikely]] + return nullptr; if (addr > UINT32_MAX - 7u) [[unlikely]] return nullptr; if (MemoryInline::FlatWriteNeedsPolicy(addr) || diff --git a/runtime/include/memory_access.h b/runtime/include/memory_access.h index 8b19e02..7ac535d 100644 --- a/runtime/include/memory_access.h +++ b/runtime/include/memory_access.h @@ -231,6 +231,13 @@ MKW_MEMORY_FORCE_INLINE uint8_t* ResolveRangeHost(uint32_t base, int32_t minOffs (void)needsRead; const uint32_t guestStart = base + static_cast(minOffset); if (length == 0 || length > kPageSize || guestStart > UINT32_MAX - (length - 1)) return nullptr; + if (GuestFlat::RequiresCheckedAccess()) { + // A host page can cover multiple independently-special Wii pages. + // Returning null keeps resolved accesses on the checked Memory::* + // path, which materializes deferred reads and applies write policy. + (void)needsWrite; + return nullptr; + } if (needsWrite && (FlatWriteNeedsPolicy(guestStart) || FlatWriteNeedsPolicy(guestStart + (length - 1)))) [[unlikely]] return nullptr; @@ -522,6 +529,13 @@ MKW_MEMORY_FORCE_INLINE void WriteResolvedFloat64(uint8_t* r, uint32_t o, uint32 // around `*(T*)(base + addr)`, no page-table load or limit check (interception model documented // in guest_flat_memory.h). The one exception kept inline is the MMIO write policy, since the // written value can't be recovered from a fault record. +// +// When a host VM page is larger than a 4 KiB Wii page, guest-view protections +// cannot distinguish adjacent special Wii pages. The general FlatRead*/ +// FlatWrite* helpers then use the checked page-table path, which materializes +// deferred reads and applies executable-write/MMIO policy before touching RAM. +// FlatWriteRam* remains direct because the translator emits it only for +// addresses it has proven are ordinary RAM. template MKW_MEMORY_FORCE_INLINE T FlatLoad(uint32_t address) { @@ -536,47 +550,62 @@ MKW_MEMORY_FORCE_INLINE void FlatStore(uint32_t address, T value) { std::memcpy(MKW_FLAT_GUEST_BASE + address, &swapped, sizeof(T)); } -MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) { return FlatLoad(address); } -MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) { return FlatLoad(address); } -MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) { return FlatLoad(address); } +MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) { + if (GuestFlat::RequiresCheckedAccess()) return Memory::Read8(address); + return FlatLoad(address); +} +MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) { + if (GuestFlat::RequiresCheckedAccess()) return Memory::Read16(address); + return FlatLoad(address); +} +MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) { + if (GuestFlat::RequiresCheckedAccess()) return Memory::Read32(address); + return FlatLoad(address); +} MKW_MEMORY_FORCE_INLINE float FlatReadFloat32(uint32_t address) { - const uint32_t bits = FlatLoad(address); + const uint32_t bits = FlatRead32(address); float value = 0.0f; std::memcpy(&value, &bits, sizeof(value)); return value; } MKW_MEMORY_FORCE_INLINE double FlatReadFloat64(uint32_t address) { - const uint64_t bits = FlatLoad(address); + const uint64_t bits = GuestFlat::RequiresCheckedAccess() + ? Memory::Read64(address) : FlatLoad(address); double value = 0.0; std::memcpy(&value, &bits, sizeof(value)); return value; } MKW_MEMORY_FORCE_INLINE void FlatWrite8(uint32_t address, uint8_t value) { + if (GuestFlat::RequiresCheckedAccess()) { Memory::Write8(address, value); return; } if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write8Slow(address, value); return; } FlatStore(address, value); } MKW_MEMORY_FORCE_INLINE void FlatWrite16(uint32_t address, uint16_t value) { + if (GuestFlat::RequiresCheckedAccess()) { Memory::Write16(address, value); return; } if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write16Slow(address, value); return; } FlatStore(address, value); } MKW_MEMORY_FORCE_INLINE void FlatWrite32(uint32_t address, uint32_t value) { + if (GuestFlat::RequiresCheckedAccess()) { Memory::Write32(address, value); return; } if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write32Slow(address, value); return; } FlatStore(address, value); } MKW_MEMORY_FORCE_INLINE void FlatWriteFloat32(uint32_t address, double value) { + if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat32(address, value); return; } const uint32_t bits = ConvertPpcDoubleToSingleBits(value); if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat32Slow(address, value); return; } FlatStore(address, bits); } MKW_MEMORY_FORCE_INLINE void FlatWriteFloat64(uint32_t address, double value) { + if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat64(address, value); return; } uint64_t bits = 0; std::memcpy(&bits, &value, sizeof(bits)); if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat64Slow(address, value); return; } diff --git a/runtime/include/platform/host_platform.h b/runtime/include/platform/host_platform.h new file mode 100644 index 0000000..1fd961e --- /dev/null +++ b/runtime/include/platform/host_platform.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include + +// Small host-services boundary for functionality that must not leak Win32 +// assumptions into runtime or game code. Guest execution, virtual memory, and +// cooperative contexts remain outside this layer until dedicated macOS +// prototypes establish a safe abstraction. +namespace RuntimePlatform { + +std::optional ExecutableDirectory() noexcept; + +// Returns the platform's conventional per-user application-data directory. +// It does not create the directory, leaving that policy to the caller. +std::filesystem::path ApplicationDataDirectory(std::string_view applicationName); + +// The root for per-run diagnostics. Keeping this here ensures log placement +// follows the same host convention as configuration and other user data. +std::filesystem::path LogDirectory(std::string_view applicationName); + +uint64_t CurrentProcessId() noexcept; + +} // namespace RuntimePlatform diff --git a/runtime/include/runtime_config.h b/runtime/include/runtime_config.h index c7a93c4..620a043 100644 --- a/runtime/include/runtime_config.h +++ b/runtime/include/runtime_config.h @@ -17,6 +17,7 @@ #include #include #include +#include "platform/host_platform.h" #ifdef _WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN @@ -141,7 +142,14 @@ inline bool IsSupportedResolutionMultiplier(float value) { // Must stay in step with the backend table in main.cpp, which is what actually // maps these to AuroraBackend. inline bool IsSupportedGraphicsApi(std::string_view value) { +#if defined(__APPLE__) + static constexpr std::array values{"auto", "metal"}; +// only vulkan for linux +#elif defined(__linux__) + static constexpr std::array values{"auto", "vulkan"}; +#elif defined(_WIN32) static constexpr std::array values{"auto", "d3d12", "vulkan"}; +#endif return std::find(values.begin(), values.end(), value) != values.end(); } @@ -172,6 +180,8 @@ inline std::optional ExecutableDirectory() { } buffer.resize(buffer.size() * 2); } +#elif defined(__APPLE__) + return RuntimePlatform::ExecutableDirectory(); #else // /proc/self/exe is a Linux-specific magic symlink to the running executable; readlink() // does not NUL-terminate and silently truncates if the buffer is too small, so this grows @@ -229,6 +239,8 @@ inline std::filesystem::path ApplicationDataDirectory() { CoTaskMemFree(rawPath); return directory; } +#elif defined(__APPLE__) + return RuntimePlatform::ApplicationDataDirectory(kApplicationDirectoryName); #else // XDG Base Directory spec equivalent of FOLDERID_LocalAppData: $XDG_DATA_HOME if set and // non-empty, otherwise its default of $HOME/.local/share. diff --git a/runtime/include/system_bridge.h b/runtime/include/system_bridge.h index 1f57f0d..a2d4cf6 100644 --- a/runtime/include/system_bridge.h +++ b/runtime/include/system_bridge.h @@ -36,6 +36,7 @@ extern thread_local uint32_t g_sehLastAccessType; void WriteFatalLog(std::string_view reason); void SetRuntimeExitCode(int code); +void MarkFatalErrorReported(); // Centralized crash reporting (defined in main.cpp). Every fatal path funnels // through these so the per-run log folder always receives the same artifact diff --git a/runtime/src/fiber_manager.cpp b/runtime/src/fiber_manager.cpp index 8e15c1a..fb4575d 100644 --- a/runtime/src/fiber_manager.cpp +++ b/runtime/src/fiber_manager.cpp @@ -2,6 +2,7 @@ #include "memory.h" #include "abi_bridge.h" #include "hle_stubs.h" +#include "host_context.h" #include "runtime_log.h" // Defined in hle/os/os_sleep.cpp; the sleep-timer table is file-local there. @@ -13,24 +14,8 @@ #include #include -#if !defined(_WIN32) -#include "libco.h" -#endif - namespace Fiber { -#if !defined(_WIN32) -namespace { -// libco's co_create() entry points take no argument, unlike CreateFiber(size, FiberProc, param). -// CreateGuestFiber() stages the guest thread address here immediately before the first co_switch -// into a freshly created cothread; FiberProcTrampoline reads it exactly once, at the top of the -// fiber's very first activation. Safe because guest fibers are strictly cooperative on a single -// OS thread: nothing else can run (and so nothing else can overwrite this) between the staging -// write and the trampoline's read of it. -thread_local uint32_t s_pendingFiberArg = 0; -} // namespace -#endif - std::mutex GuestFiberManager::s_mutex; std::unordered_map GuestFiberManager::s_fibers; std::vector GuestFiberManager::s_fibersPendingDelete; @@ -45,21 +30,11 @@ void GuestFiberManager::PurgePendingFibers() { std::lock_guard lock(s_mutex); toDelete.swap(s_fibersPendingDelete); } -#if defined(_WIN32) - const void* current = GetCurrentFiber(); for (void* f : toDelete) { - if (f && f != current) { - DeleteFiber(f); + if (f && !HostContext::IsCurrent(f)) { + HostContext::Destroy(f); } } -#else - const void* current = co_active(); - for (void* f : toDelete) { - if (f && f != current) { - co_delete(static_cast(f)); - } - } -#endif } // Global VI retrace counter @@ -212,27 +187,13 @@ void GuestFiberManager::Initialize() { return; } -#if defined(_WIN32) - // Convert the main thread to a fiber (the scheduler fiber) - s_schedulerFiber = ConvertThreadToFiber(nullptr); - if (!s_schedulerFiber) { - // May already be a fiber - s_schedulerFiber = GetCurrentFiber(); - if (!s_schedulerFiber) { - RT_LOG(RT_TAG_OS) << "FATAL: Failed to initialize scheduler fiber!" << std::endl; - ShowRuntimeFatalPopup("guest scheduler initialization failed", - "Windows could not create the scheduler fiber required to run guest threads."); - std::abort(); - } + if (!HostContext::InitializeScheduler(&s_schedulerFiber)) { + RT_LOG(RT_TAG_OS) << "FATAL: Failed to initialize scheduler context!" << std::endl; + ShowRuntimeFatalPopup("guest scheduler initialization failed", + "The host could not create the scheduler context required to run guest threads."); + std::abort(); } -#else - // co_active() returns a handle for whichever native stack is currently running, creating one - // on first call if needed - the libco analogue of ConvertThreadToFiber(nullptr): it converts - // this call's own stack into a switchable target without altering control flow. - s_schedulerFiber = co_active(); -#endif - s_currentGuestThread = 0; s_initialized = true; } @@ -240,32 +201,18 @@ void GuestFiberManager::Initialize() { void GuestFiberManager::Shutdown() { std::lock_guard lock(s_mutex); -#if defined(_WIN32) for (auto& [addr, fiber] : s_fibers) { if (fiber.fiber && !fiber.isSchedulerFiber) { - DeleteFiber(fiber.fiber); + HostContext::Destroy(fiber.fiber); fiber.fiber = nullptr; } } s_fibers.clear(); - // Convert scheduler fiber back to thread if (s_schedulerFiber) { - ConvertFiberToThread(); + HostContext::ShutdownScheduler(s_schedulerFiber); s_schedulerFiber = nullptr; } -#else - for (auto& [addr, fiber] : s_fibers) { - if (fiber.fiber && !fiber.isSchedulerFiber) { - co_delete(static_cast(fiber.fiber)); - fiber.fiber = nullptr; - } - } - s_fibers.clear(); - // Unlike ConvertFiberToThread, libco has no "undo" for co_active(): the scheduler's own - // stack was never separately allocated, so there is nothing to release here. - s_schedulerFiber = nullptr; -#endif s_initialized = false; } @@ -288,11 +235,7 @@ bool GuestFiberManager::CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entr if (existingIt != s_fibers.end()) { // Delete the old fiber if it exists and is not the scheduler fiber if (existingIt->second.fiber && !existingIt->second.isSchedulerFiber) { -#if defined(_WIN32) - DeleteFiber(existingIt->second.fiber); -#else - co_delete(static_cast(existingIt->second.fiber)); -#endif + HostContext::Destroy(existingIt->second.fiber); } s_fibers.erase(existingIt); } @@ -312,31 +255,17 @@ bool GuestFiberManager::CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entr gf.cpuContext.pc = entryPoint; gf.cpuContext.srr0 = entryPoint; -#if defined(_WIN32) - // Create Windows fiber with reasonable stack size - // Use host stack size (64KB should be plenty for translated code) + // The host stack models only translated host calls; the guest stack starts + // at stackBase in the CPU context above. constexpr size_t kHostStackSize = 64 * 1024; - gf.fiber = CreateFiber(kHostStackSize, FiberProc, reinterpret_cast(static_cast(guestThreadAddr))); + gf.fiber = HostContext::Create(kHostStackSize, FiberProc, + reinterpret_cast(static_cast(guestThreadAddr))); if (!gf.fiber) { - DWORD err = GetLastError(); - RT_LOG(RT_TAG_OS) << "CreateFiber failed for thread 0x" - << std::hex << guestThreadAddr - << " error=" << std::dec << err << std::endl; - return false; - } -#else - // libco's co_create() entry point takes no argument; SwitchToThread() stages guestThreadAddr - // into s_pendingFiberArg immediately before the co_switch that first activates this handle. - constexpr unsigned int kHostStackSize = 64 * 1024; - gf.fiber = co_create(kHostStackSize, &FiberProcTrampoline); - - if (!gf.fiber) { - RT_LOG(RT_TAG_OS) << "co_create failed for thread 0x" + RT_LOG(RT_TAG_OS) << "Failed to create host context for thread 0x" << std::hex << guestThreadAddr << std::dec << std::endl; return false; } -#endif s_fibers[guestThreadAddr] = gf; @@ -390,24 +319,11 @@ void GuestFiberManager::ExitGuestThread(uint32_t guestThreadAddr, ThreadState fi } if (it->second.fiber && !it->second.isSchedulerFiber) { -#if defined(_WIN32) - const void* current = GetCurrentFiber(); - if (it->second.fiber == current) { + if (HostContext::IsCurrent(it->second.fiber)) { s_fibersPendingDelete.push_back(it->second.fiber); } else { - DeleteFiber(it->second.fiber); + HostContext::Destroy(it->second.fiber); } -#else - const void* current = co_active(); - if (it->second.fiber == current) { - // Deleting the coroutine we're currently executing on would free the very stack - // this call is running on; defer it (PurgePendingFibers) until some other fiber is - // active, exactly like the Windows branch above. - s_fibersPendingDelete.push_back(it->second.fiber); - } else { - co_delete(static_cast(it->second.fiber)); - } -#endif it->second.fiber = nullptr; } } @@ -474,12 +390,7 @@ void GuestFiberManager::SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu // Check if we're already on the target fiber (e.g., switching to main thread // when we're already on the scheduler fiber) -#if defined(_WIN32) - void* currentFiber = GetCurrentFiber(); -#else - void* currentFiber = co_active(); -#endif - if (currentFiber == fiberHandle) { + if (HostContext::IsCurrent(fiberHandle)) { // Already executing on the target host fiber. This is common for the // default guest thread, which also owns the scheduler fiber. Keep the // live CPU context instead of restoring a possibly stale saved copy @@ -496,15 +407,7 @@ void GuestFiberManager::SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu } // Switch to the target fiber (the target fiber will load its own context) -#if defined(_WIN32) - SwitchToFiber(fiberHandle); -#else - // Staged for FiberProcTrampoline's first (and only) read; a no-op for a fiber that has - // already started, since resuming it re-enters mid-function rather than through the - // trampoline's entry point. - s_pendingFiberArg = guestThreadAddr; - co_switch(static_cast(fiberHandle)); -#endif + HostContext::Switch(fiberHandle); // When we return here, the fiber that issued SwitchToThread has resumed. // That does not automatically mean the previous guest thread became runnable @@ -618,14 +521,6 @@ void GuestFiberManager::ProcessTimerEvents(CpuContext* cpu) { } } -void GuestFiberManager::SwitchToScheduler() { -#if defined(_WIN32) - SwitchToFiber(s_schedulerFiber); -#else - co_switch(static_cast(s_schedulerFiber)); -#endif -} - #if defined(_WIN32) void CALLBACK GuestFiberManager::FiberProc(void* param) #else @@ -634,6 +529,7 @@ void GuestFiberManager::FiberProc(void* param) { uint32_t guestThreadAddr = static_cast(reinterpret_cast(param)); + // Get our fiber info GuestFiber* fiber = nullptr; uint32_t entryPoint = 0; @@ -644,7 +540,7 @@ void GuestFiberManager::FiberProc(void* param) auto it = s_fibers.find(guestThreadAddr); if (it == s_fibers.end()) { RT_LOG(RT_TAG_OS) << "FiberProc: fiber not found!" << std::endl; - SwitchToScheduler(); + HostContext::Switch(s_schedulerFiber); return; } fiber = &it->second; @@ -707,7 +603,7 @@ void GuestFiberManager::FiberProc(void* param) << ", fn=0x" << startFn << ") after retries; continuing anyway." << std::dec << std::endl; break; } - SwitchToScheduler(); + HostContext::Switch(s_schedulerFiber); } // The deferral loop above yields to the scheduler and therefore can resume @@ -764,18 +660,7 @@ void GuestFiberManager::FiberProc(void* param) } // Return to scheduler - SwitchToScheduler(); + HostContext::Switch(s_schedulerFiber); } -#if !defined(_WIN32) -void GuestFiberManager::FiberProcTrampoline() { - const uint32_t guestThreadAddr = s_pendingFiberArg; - FiberProc(reinterpret_cast(static_cast(guestThreadAddr))); - // FiberProc always calls SwitchToScheduler() on every exit path and never falls off its own - // end; this is only a safety net in case that ever changes; falling off co_create's entry - // function is otherwise undefined behavior (libco's own crash() fallback aborts instead). - SwitchToScheduler(); -} -#endif - } // namespace Fiber diff --git a/runtime/src/guest_flat_memory.cpp b/runtime/src/guest_flat_memory.cpp index 17780d9..06d1898 100644 --- a/runtime/src/guest_flat_memory.cpp +++ b/runtime/src/guest_flat_memory.cpp @@ -36,6 +36,9 @@ #endif namespace GuestFlat { +#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE) +bool g_requiresCheckedAccess = false; +#endif namespace { #if defined(_WIN32) @@ -48,6 +51,16 @@ constexpr DWORD kMemPreservePlaceholder = 0x00000002; constexpr size_t kAllocationGranularity = 0x10000; // 64 KiB constexpr size_t kHostPageSize = 0x1000; +// Only hosts that can expose a page larger than 4 KiB need to discover their +// size at runtime; see RequiresCheckedAccess() in guest_flat_memory.h. +#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE) +size_t HostPageSize() +{ + const long size = sysconf(_SC_PAGESIZE); + return size > 0 ? static_cast(size) : kGuestPageSize; +} +#endif + // Named, platform-neutral protection modes so every fault-interception call site below (the // MMIO window, the executable-write guard, deferred-EFB-read protection, the on-demand // unmapped-block commit) can stay identical text on both platforms; only ProtectRange() and @@ -349,7 +362,7 @@ bool IsMmio(uint32_t address) { return MemoryInline::IsMmioAddress(address); } bool IsGpuFifo(uint32_t address) { return MemoryInline::IsGpuFifoAddress(address); } void ApplyExecutableProtectionLocked() { - if (g_base == nullptr) return; + if (g_base == nullptr || RequiresCheckedAccess()) return; auto& protectedPages = ExecutableProtectedPages(); for (const auto& range : ExecutableRanges()) { // Only pages fully inside the range are protected: edge pages often share a page with data @@ -497,6 +510,10 @@ bool IsActive() { void Initialize(const std::vector& regions) { std::lock_guard lock(StateMutex()); +#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE) + g_requiresCheckedAccess = HostPageSize() > kGuestPageSize; +#endif + if (g_initialized) { if (!SameLayout(g_activeRegions, regions)) { throw std::runtime_error( @@ -615,7 +632,7 @@ uint8_t* HostPointer(uint32_t guestAddress) { } void ProtectDeferredRange(uint32_t address, size_t length) { - if (!g_initialized || length == 0) return; + if (RequiresCheckedAccess() || !g_initialized || length == 0) return; const uint64_t end = static_cast(address) + length; if (end > kGuestSpaceSize) return; std::lock_guard lock(StateMutex()); @@ -630,7 +647,7 @@ void ProtectDeferredRange(uint32_t address, size_t length) { } void UnprotectDeferredRange(uint32_t address, size_t length) { - if (!g_initialized || length == 0) return; + if (RequiresCheckedAccess() || !g_initialized || length == 0) return; std::lock_guard lock(StateMutex()); auto& ranges = DeferredRanges(); const uint64_t end = static_cast(address) + length; @@ -645,7 +662,7 @@ void UnprotectDeferredRange(uint32_t address, size_t length) { } void RegisterExecutableRange(uint32_t start, uint32_t end) { - if (end <= start) return; + if (RequiresCheckedAccess() || end <= start) return; std::lock_guard lock(StateMutex()); auto& ranges = ExecutableRanges(); if (std::any_of(ranges.begin(), ranges.end(), [&](const GuardedRange& range) { diff --git a/runtime/src/guest_flat_memory_macos.cpp b/runtime/src/guest_flat_memory_macos.cpp new file mode 100644 index 0000000..7a628e8 --- /dev/null +++ b/runtime/src/guest_flat_memory_macos.cpp @@ -0,0 +1,80 @@ +#include "guest_flat_memory.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace GuestFlat { +bool g_requiresCheckedAccess = false; +namespace { +struct Mapping { uint32_t base; uint64_t size; uint8_t* host; }; +std::mutex g_mutex; +std::vector g_mappings; +std::vector g_layout; +uint8_t* g_base = nullptr; +bool g_active = false; + +uint64_t Offset(const RegionRequest& r) { + if (r.backing == Backing::Mem1) return r.base & 0x1fffffffu; + if (r.backing == Backing::Mem2) return (r.base & 0x1fffffffu) - 0x10000000u; + return 0; +} +bool Same(const std::vector& a, const std::vector& b) { + return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), + [](const auto& x, const auto& y) { return x.base == y.base && x.size == y.size && x.backing == y.backing; }); +} +int BackingFile(size_t size) { + char name[] = "/tmp/wiicompiled-guest-XXXXXX"; + const int fd = mkstemp(name); + if (fd >= 0) { unlink(name); if (ftruncate(fd, static_cast(size)) != 0) { close(fd); return -1; } } + return fd; +} +} // namespace + +bool IsActive() { return g_active; } +void Initialize(const std::vector& regions) { + std::lock_guard lock(g_mutex); + g_requiresCheckedAccess = static_cast(getpagesize()) > kGuestPageSize; + if (g_active) { if (!Same(g_layout, regions)) throw std::runtime_error("flat guest layout cannot be remapped"); return; } + mach_vm_address_t address = kFixedFlatGuestBase; + if (mach_vm_allocate(mach_task_self(), &address, kGuestSpaceSize, VM_FLAGS_FIXED) != KERN_SUCCESS || address != kFixedFlatGuestBase) + throw std::runtime_error("unable to reserve fixed 4 GiB macOS guest address space"); + g_base = reinterpret_cast(address); + struct Store { Backing kind; uint32_t owned; uint64_t size; int fd; }; + std::vector stores; + for (const auto& r : regions) { + if (!r.size) continue; + const uint32_t owned = r.backing == Backing::Owned ? r.base : 0; + auto it = std::find_if(stores.begin(), stores.end(), [&](const Store& s) { return s.kind == r.backing && s.owned == owned; }); + const uint64_t need = Offset(r) + r.size; + if (it == stores.end()) stores.push_back({r.backing, owned, need, -1}); else it->size = std::max(it->size, need); + } + for (auto& s : stores) { s.fd = BackingFile(s.size); if (s.fd < 0) throw std::runtime_error("unable to create macOS guest backing store"); } + for (const auto& r : regions) { + if (!r.size) continue; + const uint32_t owned = r.backing == Backing::Owned ? r.base : 0; + const auto& s = *std::find_if(stores.begin(), stores.end(), [&](const Store& x) { return x.kind == r.backing && x.owned == owned; }); + auto* host = static_cast(mmap(nullptr, r.size, PROT_READ | PROT_WRITE, MAP_SHARED, s.fd, Offset(r))); + auto* guest = mmap(g_base + r.base, r.size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, s.fd, Offset(r)); + if (host == MAP_FAILED || guest != g_base + r.base) throw std::runtime_error("unable to map macOS guest alias"); + g_mappings.push_back({r.base, r.size, host}); + } + for (auto& s : stores) close(s.fd); + g_layout = regions; g_active = true; +} +uint8_t* HostPointer(uint32_t a) { for (const auto& m : g_mappings) if (a >= m.base && uint64_t(a - m.base) < m.size) return m.host + (a - m.base); return nullptr; } +void ProtectDeferredRange(uint32_t, size_t) {} +void UnprotectDeferredRange(uint32_t, size_t) {} +void RegisterExecutableRange(uint32_t, uint32_t) {} +FaultCounters Counters() { return {}; } +void LogFaultSummary() noexcept {} +bool HandleAccessViolation(void*, bool) noexcept { return false; } +} // namespace GuestFlat diff --git a/runtime/src/hle/gx/gx_internal.h b/runtime/src/hle/gx/gx_internal.h index 25e9e6d..8656dba 100644 --- a/runtime/src/hle/gx/gx_internal.h +++ b/runtime/src/hle/gx/gx_internal.h @@ -394,3 +394,4 @@ extern std::mutex g_tlutObjMutex; // that happens on another key: unordered_map keeps references valid across a // rehash, an open-addressed table would not. extern std::unordered_map g_TexObjMeta; +extern std::map g_TlutObjMeta; diff --git a/runtime/src/hle/gx/gx_utils.cpp b/runtime/src/hle/gx/gx_utils.cpp index aed1e5d..81d7c07 100644 --- a/runtime/src/hle/gx/gx_utils.cpp +++ b/runtime/src/hle/gx/gx_utils.cpp @@ -33,6 +33,15 @@ void WriteGuestFloat(uint32_t addr, float value, const char* label) { void* GuestToHostPtr(uint32_t addr, size_t len) { if (addr == 0) return nullptr; +#if defined(__APPLE__) + // The macOS flat guest map exposes separate host aliases for cached, + // uncached, and physical MEM1/MEM2 addresses. GX resources are identified + // by their host pointer, so all aliases of one guest allocation must use + // the same physical mapping before they reach Aurora. Kept macOS-only: + // changing which alias the other hosts hand out would re-key their existing + // GX resource identity. + addr = CanonicalizeGxMainRamAddress(addr); +#endif try { return Memory::GetPointer(addr, len); } catch (const Memory::AccessViolation& e) { LogMemoryError(RT_TAG_GX, "GX guest pointer", e); return nullptr; } } diff --git a/runtime/src/hle/vi.cpp b/runtime/src/hle/vi.cpp index 52fda5c..7ee76b5 100644 --- a/runtime/src/hle/vi.cpp +++ b/runtime/src/hle/vi.cpp @@ -6,6 +6,7 @@ #include "aurora_events.h" #include "settings_overlay.h" #include "fiber_manager.h" +#include "platform/host_platform.h" #include "runtime_log.h" #include @@ -20,18 +21,15 @@ #include #include -#include - #if defined(_WIN32) -#ifndef NOMINMAX -#define NOMINMAX -#endif #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #endif +#include + // Forward declaration for OSWakeupThread - used to wake threads on VI retrace queue extern "C" void OSWakeupThread_HLE_801aaaa4(CpuContext* ctx); diff --git a/runtime/src/host_context.cpp b/runtime/src/host_context.cpp new file mode 100644 index 0000000..e23c1c0 --- /dev/null +++ b/runtime/src/host_context.cpp @@ -0,0 +1,270 @@ +#include "host_context.h" + +#if defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#elif defined(__APPLE__) && defined(__aarch64__) +#include +#include + +extern "C" void mkw_co_switch(void** targetSp, void** sourceSp); +extern "C" void* mkw_co_init(void* stackTop, void (*entry)(void*), void* argument); +#elif defined(__linux__) +#include + +#include +#include +#else +#error "HostContext needs a supported cooperative-context backend" +#endif + +namespace HostContext { + +#if defined(_WIN32) + +namespace { +thread_local bool g_convertedScheduler = false; +} + +bool InitializeScheduler(Handle* scheduler) +{ + void* context = ConvertThreadToFiber(nullptr); + g_convertedScheduler = context != nullptr; + if (!context) { + context = GetCurrentFiber(); + } + *scheduler = context; + return context != nullptr; +} + +void ShutdownScheduler(Handle scheduler) +{ + if (scheduler && g_convertedScheduler) { + ConvertFiberToThread(); + } + g_convertedScheduler = false; +} + +Handle Create(std::size_t stackSize, Entry entry, void* argument) +{ + return CreateFiber(stackSize, entry, argument); +} + +void Destroy(Handle context) +{ + if (context) { + DeleteFiber(context); + } +} + +bool IsCurrent(Handle context) +{ + return context != nullptr && GetCurrentFiber() == context; +} + +void Switch(Handle target) +{ + SwitchToFiber(target); +} + +#elif defined(__APPLE__) && defined(__aarch64__) + +namespace { +struct Context { + void* savedStackPointer = nullptr; + void* stack = nullptr; + std::size_t stackSize = 0; +}; + +// Guest scheduling is confined to the initialized main host thread. Keeping +// this as ordinary process state also avoids relying on Darwin TLS internals +// while executing on a manually managed stack. +Context* g_current = nullptr; +} + +bool InitializeScheduler(Handle* scheduler) +{ + auto* context = new Context(); + g_current = context; + *scheduler = context; + return true; +} + +void ShutdownScheduler(Handle scheduler) +{ + auto* context = static_cast(scheduler); + if (g_current == context) { + g_current = nullptr; + } + delete context; +} + +Handle Create(std::size_t stackSize, Entry entry, void* argument) +{ + auto* context = new Context(); + const std::size_t guardSize = static_cast(getpagesize()); + const std::size_t totalSize = stackSize + guardSize; + context->stack = mmap(nullptr, totalSize, PROT_READ | PROT_WRITE, + MAP_ANON | MAP_PRIVATE, -1, 0); + if (context->stack == MAP_FAILED) { + delete context; + return nullptr; + } + // Fault on stack overflow instead of corrupting the preceding mapping. + if (mprotect(context->stack, guardSize, PROT_NONE) != 0) { + munmap(context->stack, totalSize); + delete context; + return nullptr; + } + context->stackSize = totalSize; + + auto* stackTop = static_cast(context->stack) + totalSize; + context->savedStackPointer = mkw_co_init(stackTop, entry, argument); + return context; +} + +void Destroy(Handle context) +{ + auto* nativeContext = static_cast(context); + if (!nativeContext) { + return; + } + if (nativeContext->stack) { + munmap(nativeContext->stack, nativeContext->stackSize); + } + delete nativeContext; +} + +bool IsCurrent(Handle context) +{ + return context != nullptr && context == g_current; +} + +void Switch(Handle target) +{ + auto* destination = static_cast(target); + Context* source = g_current; + if (!destination || destination == source) { + return; + } + + g_current = destination; + mkw_co_switch(&destination->savedStackPointer, &source->savedStackPointer); + g_current = source; +} + +#elif defined(__linux__) + +namespace { +struct Context { + cothread_t native = nullptr; + Entry entry = nullptr; + void* argument = nullptr; + bool ownsNative = false; +}; + +thread_local Context* g_current = nullptr; +thread_local std::unordered_map g_contexts; + +void ContextEntry() +{ + const auto found = g_contexts.find(co_active()); + if (found == g_contexts.end() || !found->second || !found->second->entry) { + std::abort(); + } + + Context* context = found->second; + g_current = context; + context->entry(context->argument); + + // A guest fiber must return through FiberProc's scheduler handoff. There + // is no valid native caller to return to from libco's entry trampoline. + std::abort(); +} +} // namespace + +bool InitializeScheduler(Handle* scheduler) +{ + auto* context = new Context(); + context->native = co_active(); + if (!context->native) { + delete context; + return false; + } + + g_current = context; + g_contexts.emplace(context->native, context); + *scheduler = context; + return true; +} + +void ShutdownScheduler(Handle scheduler) +{ + auto* context = static_cast(scheduler); + if (!context) { + return; + } + + g_contexts.erase(context->native); + if (g_current == context) { + g_current = nullptr; + } + delete context; +} + +Handle Create(std::size_t stackSize, Entry entry, void* argument) +{ + auto* context = new Context(); + context->entry = entry; + context->argument = argument; + context->native = co_create(static_cast(stackSize), ContextEntry); + context->ownsNative = context->native != nullptr; + if (!context->native) { + delete context; + return nullptr; + } + + g_contexts.emplace(context->native, context); + return context; +} + +void Destroy(Handle context) +{ + auto* nativeContext = static_cast(context); + if (!nativeContext) { + return; + } + + g_contexts.erase(nativeContext->native); + if (nativeContext->ownsNative) { + co_delete(nativeContext->native); + } + delete nativeContext; +} + +bool IsCurrent(Handle context) +{ + return context != nullptr && context == g_current; +} + +void Switch(Handle target) +{ + auto* destination = static_cast(target); + Context* source = g_current; + if (!destination || destination == source) { + return; + } + + g_current = destination; + co_switch(destination->native); + g_current = source; +} + +#endif + +} // namespace HostContext diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index f10a290..98b0334 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -23,6 +23,10 @@ #include #include +#if !defined(_WIN32) +#include +#endif + #if defined(_WIN32) #ifndef NOMINMAX #define NOMINMAX @@ -38,7 +42,12 @@ #include #else #include +#if defined(__x86_64__) +// 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 +// include the header, while the arm64 handler does not use it at all. #include +#endif #include #endif @@ -1370,9 +1379,21 @@ int RuntimeMain(int argc, char** argv) { const char* configName; AuroraBackend backend; }; +#if defined(__APPLE__) + static constexpr std::array kGraphicsBackends{{ + {"auto", BACKEND_AUTO}, {"metal", BACKEND_METAL}, + }}; +// only vulkan for linux +#elif defined(__linux__) + static constexpr std::array kGraphicsBackends{{ + {"auto", BACKEND_AUTO}, {"vulkan", BACKEND_VULKAN}, + }}; +#elif defined(_WIN32) static constexpr std::array kGraphicsBackends{{ {"auto", BACKEND_AUTO}, {"d3d12", BACKEND_D3D12}, {"vulkan", BACKEND_VULKAN}, }}; + +#endif const auto backendDisplayName = [](AuroraBackend value) -> const char* { for (const auto& entry : kGraphicsBackends) { if (entry.backend == value) { diff --git a/runtime/src/platform/host_platform.cpp b/runtime/src/platform/host_platform.cpp new file mode 100644 index 0000000..c0caa95 --- /dev/null +++ b/runtime/src/platform/host_platform.cpp @@ -0,0 +1,85 @@ +#include "platform/host_platform.h" + +#include + +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#else +#include +#endif + +#if defined(__APPLE__) +#include +#include +#endif + +namespace RuntimePlatform { + +std::optional ExecutableDirectory() noexcept { +#if defined(_WIN32) + std::wstring buffer(MAX_PATH, L'\0'); + for (;;) { + const DWORD length = GetModuleFileNameW(nullptr, buffer.data(), static_cast(buffer.size())); + if (length == 0) { + return std::nullopt; + } + if (length < buffer.size() - 1) { + buffer.resize(length); + return std::filesystem::path(buffer).parent_path(); + } + buffer.resize(buffer.size() * 2); + } +#elif defined(__APPLE__) + uint32_t size = 0; + if (_NSGetExecutablePath(nullptr, &size) != -1 || size == 0) { + return std::nullopt; + } + std::string path(size, '\0'); + if (_NSGetExecutablePath(path.data(), &size) != 0) { + return std::nullopt; + } + path.resize(std::char_traits::length(path.c_str())); + std::error_code ec; + const auto resolved = std::filesystem::weakly_canonical(path, ec); + return (ec ? std::filesystem::path(path) : resolved).parent_path(); +#else + return std::nullopt; +#endif +} + +std::filesystem::path ApplicationDataDirectory(std::string_view applicationName) { +#if defined(_WIN32) + PWSTR rawPath = nullptr; + if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_CREATE, nullptr, &rawPath)) && rawPath) { + const std::filesystem::path directory = std::filesystem::path(rawPath) / applicationName; + CoTaskMemFree(rawPath); + return directory; + } +#elif defined(__APPLE__) + if (const char* home = std::getenv("HOME"); home && *home) { + return std::filesystem::path(home) / "Library" / "Application Support" / applicationName; + } + if (const passwd* user = getpwuid(getuid()); user && user->pw_dir && *user->pw_dir) { + return std::filesystem::path(user->pw_dir) / "Library" / "Application Support" / applicationName; + } +#endif + return std::filesystem::current_path() / applicationName; +} + +std::filesystem::path LogDirectory(std::string_view applicationName) { + return ApplicationDataDirectory(applicationName) / "Logs"; +} + +uint64_t CurrentProcessId() noexcept { +#if defined(_WIN32) + return static_cast(::GetCurrentProcessId()); +#else + return static_cast(::getpid()); +#endif +} + +} // namespace RuntimePlatform diff --git a/runtime/src/platform/macos/co_switch.S b/runtime/src/platform/macos/co_switch.S new file mode 100644 index 0000000..cdd2b50 --- /dev/null +++ b/runtime/src/platform/macos/co_switch.S @@ -0,0 +1,59 @@ +.text +.align 2 + +// AArch64 Darwin cooperative context frame (240 bytes): x18-x30, then v8-v15. +// x18 is platform-reserved on Darwin and is needed by code that accesses TLS. +// x0 = address holding the target frame pointer; x1 = address to receive the +// current frame pointer. This is intentionally leaf-only: it never calls C++. +.globl _mkw_co_switch +_mkw_co_switch: + sub sp, sp, #240 + str x18, [sp, #0] + stp x19, x20, [sp, #16] + stp x21, x22, [sp, #32] + stp x23, x24, [sp, #48] + stp x25, x26, [sp, #64] + stp x27, x28, [sp, #80] + stp x29, x30, [sp, #96] + stp q8, q9, [sp, #112] + stp q10, q11, [sp, #144] + stp q12, q13, [sp, #176] + stp q14, q15, [sp, #208] + mov x2, sp + str x2, [x1] + + ldr x2, [x0] + mov sp, x2 + ldr x18, [sp, #0] + ldp x19, x20, [sp, #16] + ldp x21, x22, [sp, #32] + ldp x23, x24, [sp, #48] + ldp x25, x26, [sp, #64] + ldp x27, x28, [sp, #80] + ldp x29, x30, [sp, #96] + ldp q8, q9, [sp, #112] + ldp q10, q11, [sp, #144] + ldp q12, q13, [sp, #176] + ldp q14, q15, [sp, #208] + add sp, sp, #240 + ret + +// Creates a frame compatible with mkw_co_switch and returns its saved SP. +// x0 = one-past-end stack pointer, x1 = entry(void*), x2 = entry argument. +.globl _mkw_co_init +_mkw_co_init: + bic x0, x0, #0xf + sub x0, x0, #240 + str x18, [x0, #0] // Darwin platform register / TLS base + str x1, [x0, #16] // x19: entry + str x2, [x0, #24] // x20: argument + str xzr, [x0, #96] // x29 + adrp x3, _mkw_co_entry_trampoline@PAGE + add x3, x3, _mkw_co_entry_trampoline@PAGEOFF + str x3, [x0, #104] // x30 + ret + +_mkw_co_entry_trampoline: + mov x0, x20 + blr x19 + brk #0 diff --git a/runtime/tests/host_context_tests.cpp b/runtime/tests/host_context_tests.cpp new file mode 100644 index 0000000..f6b457d --- /dev/null +++ b/runtime/tests/host_context_tests.cpp @@ -0,0 +1,49 @@ +#include "host_context.h" + +#include + +namespace { +// The worker yields twice; each return to the scheduler must preserve both +// context identities and the worker's continuation point. +HostContext::Handle g_scheduler = nullptr; +HostContext::Handle g_worker = nullptr; +int g_steps = 0; + +void Worker(void*) +{ + if (!HostContext::IsCurrent(g_worker)) { + std::abort(); + } + ++g_steps; + HostContext::Switch(g_scheduler); + if (!HostContext::IsCurrent(g_worker)) { + std::abort(); + } + ++g_steps; + HostContext::Switch(g_scheduler); +} +} // namespace + +int main() +{ + if (!HostContext::InitializeScheduler(&g_scheduler) || + !HostContext::IsCurrent(g_scheduler)) { + return 1; + } + g_worker = HostContext::Create(64 * 1024, Worker, nullptr); + if (!g_worker) { + return 1; + } + + HostContext::Switch(g_worker); + if (g_steps != 1 || !HostContext::IsCurrent(g_scheduler)) { + return 1; + } + HostContext::Switch(g_worker); + if (g_steps != 2 || !HostContext::IsCurrent(g_scheduler)) { + return 1; + } + + HostContext::Destroy(g_worker); + HostContext::ShutdownScheduler(g_scheduler); +} diff --git a/runtime/tests/macos_context_abi_tests.cpp b/runtime/tests/macos_context_abi_tests.cpp new file mode 100644 index 0000000..a1fc6f7 --- /dev/null +++ b/runtime/tests/macos_context_abi_tests.cpp @@ -0,0 +1,46 @@ +#include +#include +#include +#include +#include + +extern "C" void mkw_co_switch(void** targetSp, void** sourceSp); +extern "C" void* mkw_co_init(void* stackTop, void (*entry)(void*), void* argument); + +namespace { +// Exercise the raw AArch64 context ABI independently of HostContext so a +// callee-saved-register or stack-frame regression is localized to this layer. + +std::array g_workerStack{}; +void* g_schedulerSp = nullptr; +void* g_workerSp = nullptr; +std::vector g_events; + +void Worker(void*) { + g_events.push_back(1); + mkw_co_switch(&g_schedulerSp, &g_workerSp); + g_events.push_back(2); + mkw_co_switch(&g_schedulerSp, &g_workerSp); + std::abort(); +} + +} // namespace + +int main() { + g_workerSp = mkw_co_init(g_workerStack.data() + g_workerStack.size(), Worker, nullptr); + if (!g_workerSp) { + std::cerr << "failed to create AArch64 context frame\n"; + return 1; + } + mkw_co_switch(&g_workerSp, &g_schedulerSp); + if (g_events != std::vector{1}) { + std::cerr << "worker did not yield to scheduler\n"; + return 1; + } + mkw_co_switch(&g_workerSp, &g_schedulerSp); + if (g_events != std::vector{1, 2}) { + std::cerr << "worker did not resume from saved context\n"; + return 1; + } + return 0; +} diff --git a/runtime/tests/macos_guest_flat_memory_tests.cpp b/runtime/tests/macos_guest_flat_memory_tests.cpp new file mode 100644 index 0000000..116b687 --- /dev/null +++ b/runtime/tests/macos_guest_flat_memory_tests.cpp @@ -0,0 +1,66 @@ +#include "guest_flat_memory.h" + +#include +#include +#include +#include + +int main() { + GuestFlat::Initialize({ + {0x00000000u, 0x4000u, GuestFlat::Backing::Mem1}, + {0x80000000u, 0x4000u, GuestFlat::Backing::Mem1}, + {0x10000000u, 0x4000u, GuestFlat::Backing::Mem2}, + {0x90000000u, 0x4000u, GuestFlat::Backing::Mem2}, + }); + if (!GuestFlat::IsActive()) { + std::cerr << "guest address space did not become active\n"; + return 1; + } + if (GuestFlat::RequiresCheckedAccess() != + (static_cast(getpagesize()) > GuestFlat::kGuestPageSize)) { + std::cerr << "guest access mode does not reflect the host page size\n"; + return 1; + } + auto* mem1Physical = GuestFlat::HostPointer(0x00000000u); + auto* mem1Cached = GuestFlat::HostPointer(0x80000000u); + auto* mem2Physical = GuestFlat::HostPointer(0x10000000u); + auto* mem2Cached = GuestFlat::HostPointer(0x90000000u); + if (!mem1Physical || !mem1Cached || !mem2Physical || !mem2Cached) { + std::cerr << "missing host alias\n"; + return 1; + } + if (GuestFlat::HostPointer(0x4000u) != nullptr || + GuestFlat::HostPointer(0xa0000000u) != nullptr) { + std::cerr << "unmapped guest address resolved to host memory\n"; + return 1; + } + mem1Physical[7] = 0x5a; + mem2Cached[9] = 0xa5; + const auto* guest = reinterpret_cast(GuestFlat::kFixedFlatGuestBase); + if (mem1Cached[7] != 0x5a || guest[0x80000007u] != 0x5a || + mem2Physical[9] != 0xa5 || guest[0x10000009u] != 0xa5) { + std::cerr << "guest aliases are not coherent\n"; + return 1; + } + auto* guestWritable = reinterpret_cast(GuestFlat::kFixedFlatGuestBase); + guestWritable[0x80000008u] = 0x3c; + guestWritable[0x1000000au] = 0xc3; + if (mem1Physical[8] != 0x3c || mem2Cached[10] != 0xc3) { + std::cerr << "guest writes were not visible through host aliases\n"; + return 1; + } + + GuestFlat::Initialize({ + {0x00000000u, 0x4000u, GuestFlat::Backing::Mem1}, + {0x80000000u, 0x4000u, GuestFlat::Backing::Mem1}, + {0x10000000u, 0x4000u, GuestFlat::Backing::Mem2}, + {0x90000000u, 0x4000u, GuestFlat::Backing::Mem2}, + }); + try { + GuestFlat::Initialize({{0x00000000u, 0x8000u, GuestFlat::Backing::Mem1}}); + std::cerr << "guest address space accepted a different layout\n"; + return 1; + } catch (const std::runtime_error&) { + } + return 0; +} diff --git a/runtime/tests/platform_paths_tests.cpp b/runtime/tests/platform_paths_tests.cpp new file mode 100644 index 0000000..66cf79a --- /dev/null +++ b/runtime/tests/platform_paths_tests.cpp @@ -0,0 +1,30 @@ +#include "platform/host_platform.h" + +#include + +int main() { + if (!RuntimePlatform::ExecutableDirectory()) { + std::cerr << "unable to resolve the current executable directory\n"; + return 1; + } + + const auto userData = RuntimePlatform::ApplicationDataDirectory("WiiCompiledPlatformPathsTest"); + if (userData.filename() != "WiiCompiledPlatformPathsTest") { + std::cerr << "application-data directory lost its application name: " << userData << '\n'; + return 1; + } + if (RuntimePlatform::LogDirectory("WiiCompiledPlatformPathsTest") != userData / "Logs") { + std::cerr << "log directory is not derived from application data\n"; + return 1; + } +#if defined(__APPLE__) + if (userData.parent_path().filename() != "Application Support" || + userData.parent_path().parent_path().filename() != "Library") { + std::cerr << "macOS application-data directory is not under ~/Library/Application Support: " + << userData << '\n'; + return 1; + } +#endif + + return 0; +} diff --git a/translator/src/Translator.Cli/Program.cs b/translator/src/Translator.Cli/Program.cs index c8f7fe4..9eee788 100644 --- a/translator/src/Translator.Cli/Program.cs +++ b/translator/src/Translator.Cli/Program.cs @@ -166,6 +166,7 @@ return command switch "emit-build-shards" => RunEmitBuildShards(tail), "emit-base-manifest" => RunEmitBaseManifest(tail), "check-base-mod-awareness" => RunCheckBaseModAwareness(tail), + "validate-retro-wfc-payload" => RunValidateRetroWfcPayload(tail), _ => ShowHelp(command) }; @@ -354,6 +355,33 @@ int RunInfo() return 0; } +int RunValidateRetroWfcPayload(string[] argsTail) +{ + var directory = OptionValue(argsTail, "--directory"); + if (string.IsNullOrWhiteSpace(directory)) + { + Console.Error.WriteLine("--directory is required."); + return 1; + } + + try + { + WiiCompiled.Setup.Common.RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(directory); + Console.WriteLine("[translator] Retro WFC payload signature validated."); + return 0; + } + catch (InvalidDataException ex) + { + Console.Error.WriteLine($"[translator] Retro WFC payload validation failed: {ex.Message}"); + return 2; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + Console.Error.WriteLine($"[translator] Could not read Retro WFC payload: {ex.Message}"); + return 1; + } +} + int RunTranslateRecursive(string[] argsTail) { if (argsTail.Length == 0 || argsTail[0].StartsWith("--", StringComparison.Ordinal)) @@ -3639,7 +3667,8 @@ static string[] KnownCommands() => new[] "translate-mod", "emit-base-manifest", "emit-build-shards", - "check-base-mod-awareness" + "check-base-mod-awareness", + "validate-retro-wfc-payload" }; /// @@ -3682,6 +3711,10 @@ static (string? Positional, CommandOption[] Options)? CommandSpec(string command new("--translation-output-metadata", "path"), new("--code-pul", "path") }), + "validate-retro-wfc-payload" => (null, new CommandOption[] + { + new("--directory", "directory", Required: true) + }), "emit-base-manifest" => (null, new CommandOption[] { new("--out", "path"), @@ -4061,4 +4094,3 @@ sealed record ResolvedDispatchEntry( bool MustRemainDynamicallyDispatchable, string SourceFile); - diff --git a/translator/src/Translator.Cli/Translator.Cli.csproj b/translator/src/Translator.Cli/Translator.Cli.csproj index d6f98c9..6543c9f 100644 --- a/translator/src/Translator.Cli/Translator.Cli.csproj +++ b/translator/src/Translator.Cli/Translator.Cli.csproj @@ -15,6 +15,7 @@ + diff --git a/translator/src/Translator.Core/IO/AssemblyBlobWriter.cs b/translator/src/Translator.Core/IO/AssemblyBlobWriter.cs index 3a295f0..2201e54 100644 --- a/translator/src/Translator.Core/IO/AssemblyBlobWriter.cs +++ b/translator/src/Translator.Core/IO/AssemblyBlobWriter.cs @@ -22,14 +22,15 @@ internal static class AssemblyBlobWriter var expectedFiles = new HashSet(StringComparer.OrdinalIgnoreCase); var assembly = new StringBuilder(); foreach (var header in headerLines) assembly.AppendLine(header); - // PE/COFF (Windows) and ELF (Linux) spell a read-only data section differently in GNU-as - // syntax - COFF section flags ("dr" = data, read-only) versus an ELF section needing an - // allocatable-only flag plus an explicit @progbits type. The build always targets + // PE/COFF (Windows), Mach-O (macOS), and ELF (Linux) spell a read-only data section + // differently in GNU-as syntax. The build always targets // whichever platform the translator itself runs on (there is no cross-compilation // support), so that's what this picks the section syntax from. assembly.AppendLine(OperatingSystem.IsWindows() ? ".section .rdata,\"dr\"" - : ".section .rodata,\"a\",@progbits"); + : OperatingSystem.IsMacOS() + ? ".section __TEXT,__const" + : ".section .rodata,\"a\",@progbits"); assembly.AppendLine(); foreach (var blob in blobs) @@ -40,8 +41,12 @@ internal static class AssemblyBlobWriter var hash = ChecksumUtilities.Sha256Hex(blob.Data.Span); assembly.AppendLine($"// {blob.Comment}; sha256={hash}"); assembly.AppendLine(".p2align 4"); - assembly.AppendLine($".globl {blob.Symbol}"); - assembly.AppendLine($"{blob.Symbol}:"); + // C/C++ external symbols carry a leading underscore in Mach-O, + // unlike ELF and COFF. The generated C++ still names the symbol + // without that ABI decoration, so emit the platform spelling here. + var assemblySymbol = OperatingSystem.IsMacOS() ? $"_{blob.Symbol}" : blob.Symbol; + assembly.AppendLine($".globl {assemblySymbol}"); + assembly.AppendLine($"{assemblySymbol}:"); var referencePath = Path.Combine(blobReferenceDirectory, blob.FileName); assembly.AppendLine($".incbin \"{SanitizeAssemblyPath(referencePath)}\""); assembly.AppendLine(); @@ -51,7 +56,7 @@ internal static class AssemblyBlobWriter { if (!expectedFiles.Contains(Path.GetFullPath(stalePath))) File.Delete(stalePath); } - if (!OperatingSystem.IsWindows()) + if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS()) { // Absence of a .note.GNU-stack section makes the linker assume the oldest, most // conservative default for this object (an executable stack) and warn about it; this diff --git a/translator/tests/Translator.Tests/ModDataPatchWriterTests.cs b/translator/tests/Translator.Tests/ModDataPatchWriterTests.cs index bc7d2a6..29da247 100644 --- a/translator/tests/Translator.Tests/ModDataPatchWriterTests.cs +++ b/translator/tests/Translator.Tests/ModDataPatchWriterTests.cs @@ -28,8 +28,10 @@ public sealed class ModDataPatchWriterTests var assemblyPath = Path.Combine(root, "cpp", "mod_data_patches_blobs.S"); var assembly = File.ReadAllText(assemblyPath); - Assert.Contains(".globl kModuleImage", assembly, StringComparison.Ordinal); - Assert.Contains(".globl kKamekCodeSha1Digest", assembly, StringComparison.Ordinal); + var symbolPrefix = OperatingSystem.IsMacOS() ? "_" : ""; + Assert.Contains($".globl {symbolPrefix}kModuleImage", assembly, StringComparison.Ordinal); + Assert.Contains($".globl {symbolPrefix}kKamekCodeSha1Digest", assembly, StringComparison.Ordinal); + Assert.DoesNotContain("__APPLE__", assembly, StringComparison.Ordinal); Assert.Contains(".incbin", assembly, StringComparison.Ordinal); var timestamps = new[] { cppPath, assemblyPath }