feature: add apple silicon native macOS support (#81)

* feature: add apple silicon native macOS support - #81

* (macos): Fix crash

This fixes a crash when viewing the rear camera

* fix(macos): keep interpolated presentation on main thread

* fix(macos): supply Retro-WFC payload during setup

* perf(windows): compile out flat-memory fallback check

* remove duplicate smoke test

* test(macos): name and focus host platform tests

* fix(macos): validate Retro-WFC payload cache

* fix(payload): preserve staged file access failures

* Limit flat-page checks to variable-page hosts

---------

Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
This commit is contained in:
Michael Gandpatchzyy authored and GitHub committed 2026-09-01 18:57:15 +02:00
1 parent ae3096c89b
commit 5c76e2b0df
36 files changed
+1742 -251

No files matched your search

@@ -53,9 +53,22 @@ public static class RetroWfcPayload
var root = Path.GetFullPath(stagedDirectory); var root = Path.GetFullPath(stagedDirectory);
var payload = Path.Combine(root, RetroWfcOfflinePayloadFile); 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( throw new InvalidDataException(
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin."); "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); ValidateRetroWfcPayloadFile(payload, signingKey);
return root; return root;
} }
+139
View File
@@ -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"
+117
View File
@@ -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" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleExecutable</key><string>WiiCompiledSetup</string>
<key>CFBundleIdentifier</key><string>org.wiicompiled.setup</string>
<key>CFBundleName</key><string>WiiCompiled Setup</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>$short_version</string>
<key>CFBundleVersion</key><string>$version</string>
<key>LSMinimumSystemVersion</key><string>14.0</string>
</dict></plist>
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"
+81
View File
@@ -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'
+92
View File
@@ -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/<product>.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" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleDevelopmentRegion</key><string>en</string>
<key>CFBundleExecutable</key><string>$product</string>
<key>CFBundleIdentifier</key><string>org.wiicompiled.$product</string>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
<key>CFBundleName</key><string>$product</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>0.1.0</string>
<key>CFBundleVersion</key><string>1</string>
<key>LSMinimumSystemVersion</key><string>14.0</string>
<key>NSHighResolutionCapable</key><true/>
</dict></plist>
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"
+140
View File
@@ -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."
+3
View File
@@ -66,6 +66,8 @@ adapter must be switched to the WinUSB driver once (Zadig).
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher - GPU: GTX 1650 / RX 6400 / Arc A310 or higher
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher - CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
- About 20 GB of free disk space during installation (Final game size ~5 GB) - About 20 GB of free disk space during installation (Final game size ~5 GB)
- macOS 14 (Sonoma) or later on Apple Silicon
- 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, - A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
GCZ, CISO, WBFS, WIA and RVZ are accepted. GCZ, CISO, WBFS, WIA and RVZ are accepted.
@@ -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 Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it. launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
> [!CAUTION] > [!CAUTION]
> Only take builds from this repository's > Only take builds from this repository's
> [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an > [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an
+18
View File
@@ -255,6 +255,14 @@ FrameWorkerState g_frameWorker;
bool frame_worker_requested() noexcept { bool frame_worker_requested() noexcept {
#ifdef AURORA_ENABLE_GX #ifdef AURORA_ENABLE_GX
static const bool enabled = [] { 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) #if defined(_WIN32)
// RenderDoc's D3D12 layer is injected before Aurora starts and needs device and command // RenderDoc's D3D12 layer is injected before Aurora starts and needs device and command
// ownership on one thread, so keep frame submission synchronous there. // ownership on one thread, so keep frame submission synchronous there.
@@ -1516,6 +1524,15 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
// Phase 3: hand the encoded group to whoever owns presentation. // Phase 3: hand the encoded group to whoever owns presentation.
void publish_presentations(std::vector<PresentationJob>&& presentationJobs, bool interpolationActive) { void publish_presentations(std::vector<PresentationJob>&& 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 // 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. // interpolation off, so every mode shares one surface/resize path. RenderDoc keeps the sync path.
if (frame_worker_requested() || interpolationActive || if (frame_worker_requested() || interpolationActive ||
@@ -1526,6 +1543,7 @@ void publish_presentations(std::vector<PresentationJob>&& presentationJobs, bool
present_presentation_job(job); present_presentation_job(job);
} }
} }
#endif
} }
void record_frame_telemetry() { void record_frame_telemetry() {
+158 -51
View File
@@ -1,18 +1,28 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(mkw_recompiled) project(mkw_recompiled)
if((NOT (WIN32 AND MINGW)) AND (NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")) if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
message(FATAL_ERROR "WiiCompiled requires Windows (LLVM-MinGW) or native Linux") message(FATAL_ERROR "WiiCompiled requires a 64-bit Clang toolchain")
endif() endif()
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR
NOT CMAKE_SIZEOF_VOID_P EQUAL 8 OR if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$") set(MKW_PLATFORM_WINDOWS TRUE)
message(FATAL_ERROR "WiiCompiled requires 64-bit Clang targeting x86_64 or aarch64") 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() endif()
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release") if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
message(FATAL_ERROR "WiiCompiled only supports Release builds") message(FATAL_ERROR "WiiCompiled only supports Release builds")
endif() endif()
option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON)
# Preprocessor definitions that belong to this project's own code (the runtime, # 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 # the translated shards and the product glue) and to nothing else. They are
# applied directory-scoped, immediately after the aurora add_subdirectory() call, # 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) target_compile_features(mkw_pugixml PUBLIC cxx_std_17)
set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF) set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF)
# 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 # stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike # directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
# asymmetric resume/yield coroutine libraries, which would need every call site restructured). # asymmetric resume/yield coroutine libraries, which would need every call site restructured).
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is # Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
# separately BSD-style licensed, see third_party/libco/LICENSE) - all of libco's non-Windows # separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers
# CPU-architecture backends are kept, even though libco.c's own preprocessor dispatch # and macOS uses the project's x18-safe AArch64 assembly backend, so this target is Linux-only.
# (__amd64__/__i386__/__arm__/__aarch64__/etc.) only ever selects amd64.c for this project's if(MKW_PLATFORM_LINUX)
# 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)
add_library(mkw_libco STATIC third_party/libco/libco.c) add_library(mkw_libco STATIC third_party/libco/libco.c)
add_library(mkw::libco ALIAS mkw_libco) add_library(mkw::libco ALIAS mkw_libco)
target_include_directories(mkw_libco PUBLIC third_party/libco) target_include_directories(mkw_libco PUBLIC third_party/libco)
@@ -130,31 +137,35 @@ else()
message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing") message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing")
endif() endif()
set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE) 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_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(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "" FORCE)
set(DAWN_USE_WINDOWS_UI OFF CACHE BOOL "" FORCE) elseif(MKW_PLATFORM_MACOS)
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).
set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE) 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) set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE)
endif() endif()
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE) set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE)
set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE)
# Provide a tiny stub for DXProgrammableCapture when the SDK/PIX headers are # Provide a tiny stub for DXProgrammableCapture when the SDK/PIX headers are
# missing (common on MinGW). Dawn only includes the header; no symbols are # missing (common on MinGW). Dawn only includes the header; no symbols are
# referenced when PIX isn't present. # referenced when PIX isn't present.
set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs") if(MKW_PLATFORM_WINDOWS)
if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h") set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs")
file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR}) if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h")
file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h" file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR})
"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX is absent.\n") 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() endif()
# Deliberately NOT injected project-wide. Only a from-source Dawn build ever # Deliberately NOT injected project-wide. Only a from-source Dawn build ever
# includes DXProgrammableCapture.h, and this tree consumes Dawn as a prebuilt # includes DXProgrammableCapture.h, and this tree consumes Dawn as a prebuilt
@@ -183,7 +194,9 @@ else()
if(TARGET ${t}) if(TARGET ${t})
set_target_properties(${t} PROPERTIES UNITY_BUILD OFF) set_target_properties(${t} PROPERTIES UNITY_BUILD OFF)
target_compile_options(${t} PRIVATE -O3 -ffast-math -w -pipe) 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; # Aurora's own sources include Windows headers and call std::min/max;
# they relied on the old project-wide NOMINMAX that no longer leaks # they relied on the old project-wide NOMINMAX that no longer leaks
# into this subtree, so the define is applied per target here. # 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 # a registration file is silently never compiled and never errors. The stale-glob
# failure mode is worth far more than the milliseconds. # failure mode is worth far more than the milliseconds.
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp") 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_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") 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 # 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. # 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") 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} 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 # The translator emits the complete, content-addressed source graph. Consuming
# this one manifest keeps configure independent of the 28k generated function # this one manifest keeps configure independent of the 28k generated function
# files and of optional Retro Rewind artifacts such as code.map. # files and of optional Retro Rewind artifacts such as code.map.
set(MKW_TRANSLATED_SHARD_MANIFEST if(MKW_BUILD_PRODUCTS)
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake" set(MKW_TRANSLATED_SHARD_MANIFEST
CACHE FILEPATH "Translator-owned aggregate shard manifest") "${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
# The prebuilt export only needs the aurora/third-party closure configured above, so a CACHE FILEPATH "Translator-owned aggregate shard manifest")
# packaging machine without a translation stops here instead of failing. # The prebuilt export only needs the aurora/third-party closure configured above, so a
if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}") # packaging machine without a translation stops here instead of failing.
message(STATUS "No translator shard manifest; configuring the native prebuilt export only") if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
return() message(STATUS "No translator shard manifest; configuring the native prebuilt export only")
endif() return()
if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}") endif()
message(FATAL_ERROR if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
"Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. " message(FATAL_ERROR
"Run Translator.Cli emit-build-shards first; see translator/README.md.") "Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. "
endif() "Run Translator.Cli emit-build-shards first; see translator/README.md.")
include("${MKW_TRANSLATED_SHARD_MANIFEST}") endif()
set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS}) include("${MKW_TRANSLATED_SHARD_MANIFEST}")
message(STATUS set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS})
"Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; " message(STATUS
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; " "Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; "
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions") "${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}") set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake") 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()
+33 -25
View File
@@ -25,6 +25,11 @@ if(EXISTS "${DATA_INIT_BLOB_ASM}")
endif() endif()
list(REMOVE_DUPLICATES SOURCES) 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) function(mkw_apply_common_compile_options target)
target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe) target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe)
endfunction() endfunction()
@@ -76,10 +81,10 @@ target_compile_definitions(mkw_runtime_common PRIVATE
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION) _DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
target_link_libraries(mkw_runtime_common PRIVATE target_link_libraries(mkw_runtime_common PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx) aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
target_link_libraries(mkw_runtime_common PRIVATE mkw::pugixml mkw::toml11 mkw::cryptopp) target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
if(WIN32) if(MKW_PLATFORM_WINDOWS)
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp) 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 # ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
# media monitoring). Empty string on glibc >= 2.34 where dl* is in libc. # media monitoring). Empty string on glibc >= 2.34 where dl* is in libc.
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS}) 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") target_precompile_headers(mkw_runtime_common PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
mkw_apply_common_compile_options(mkw_runtime_common) mkw_apply_common_compile_options(mkw_runtime_common)
# Host ISA guard. Everything in MKW_ALL_BUILD_TARGETS below is compiled with # Host ISA guard. Windows and Linux x86_64 product targets use x86-64-v3, so
# -march=x86-64-v3; this object library deliberately is not, which # this object deliberately keeps the plain baseline ISA and checks the CPU
# is the whole point of keeping it out of mkw_runtime_common. It runs a CPUID # before any AVX2/FMA code can execute. AArch64 has no equivalent optional ISA
# check from a C initializer so an unsupported machine gets a readable error # floor to probe: NEON/FMA are architectural requirements.
# instead of an illegal-instruction crash. Excluded from the unity build and the if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
# precompiled header because both are produced with the owning target's flags. add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}")
add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}") target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17)
target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17) set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF)
set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF) target_compile_options(mkw_cpu_baseline PRIVATE -w)
target_compile_options(mkw_cpu_baseline PRIVATE -w) endif()
if(NOT MKW_BASE_COMMON_SHARDS) if(NOT MKW_BASE_COMMON_SHARDS)
message(FATAL_ERROR "Translator build graph contains no shared base 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 $<TARGET_OBJECTS:mkw_runtime_common>) target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_runtime_common>)
# Startup CPU check. Must stay a separate object library so it keeps the # 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. # plain baseline ISA while everything around it is built for x86-64-v3.
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_cpu_baseline>) if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_cpu_baseline>)
endif()
target_include_directories(${target} PRIVATE target_include_directories(${target} PRIVATE
"${MKW_RUNTIME_SOURCE_DIR}/include" "${MKW_RUNTIME_SOURCE_DIR}/include"
"${MKW_RUNTIME_SOURCE_DIR}/src" "${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. # include the same fat translated headers; bound them by the same pool.
mkw_bound_translated_compiles(${target}) mkw_bound_translated_compiles(${target})
target_link_libraries(${target} PRIVATE 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 target_link_libraries(${target} PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx) 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") if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake") include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
aurora_copy_runtime_dlls(${target}) aurora_copy_runtime_dlls(${target})
@@ -210,12 +221,12 @@ function(mkw_configure_product target)
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>) $<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
endif() endif()
if(WIN32) if(MKW_PLATFORM_WINDOWS)
target_link_libraries(${target} PRIVATE target_link_libraries(${target} PRIVATE
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp) dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE) set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
else() elseif(MKW_PLATFORM_LINUX)
# mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o # mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o
# files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own # files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own
# target_link_libraries (object libraries don't carry usage requirements to a consumer # target_link_libraries (object libraries don't carry usage requirements to a consumer
@@ -226,7 +237,7 @@ function(mkw_configure_product target)
# objects (empty string on glibc >= 2.34, where dl* is in libc). # objects (empty string on glibc >= 2.34, where dl* is in libc).
target_link_libraries(${target} PRIVATE mkw::libco ${CMAKE_DL_LIBS}) target_link_libraries(${target} PRIVATE mkw::libco ${CMAKE_DL_LIBS})
endif() endif()
if(WIN32) if(MKW_PLATFORM_WINDOWS)
foreach(runtime_dll libc++.dll libunwind.dll) foreach(runtime_dll libc++.dll libunwind.dll)
execute_process( execute_process(
COMMAND "${CMAKE_CXX_COMPILER}" "--print-file-name=${runtime_dll}" 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)") message(STATUS "RetroRewind target disabled (run translate-mod and emit-build-shards)")
endif() endif()
# x86-64-v3 (SSE3/SSSE3/SSE4.1/FMA/AVX2/BMI2) is the baseline runtime/src/host_cpu_baseline.cpp # Windows and Linux x86_64 share the x86-64-v3 floor that the CPU baseline
# guards against - a fixed, portable floor since an x86_64 build may run on a different machine # object above checks. AArch64 builds are compiled locally for the host that
# than the one that built it. AArch64 has no such redistribution path here: every build this # will run them, so both Linux and Apple Silicon use the compiler's native CPU
# project produces runs only on the machine that built it (local-build.sh, and the AppImage which # tuning rather than leaving target-specific performance on the table.
# 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.
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$") if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3) set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$") elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
+5 -10
View File
@@ -6,6 +6,7 @@
#include <mutex> #include <mutex>
#include <thread> #include <thread>
#include <unordered_map> #include <unordered_map>
#include <vector>
#if defined(_WIN32) #if defined(_WIN32)
#ifndef NOMINMAX #ifndef NOMINMAX
@@ -22,7 +23,7 @@
// Forward declarations // Forward declarations
struct CpuContext; 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 // 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 // threads only switch at explicit yield points (OSSleepThread, OSYieldThread, ...), matching
// Wii cooperative semantics exactly. // Wii cooperative semantics exactly.
@@ -39,7 +40,7 @@ enum class ThreadState : uint32_t {
// Information about a guest fiber // Information about a guest fiber
struct GuestFiber { struct GuestFiber {
void* fiber = nullptr; // Windows fiber handle void* fiber = nullptr; // Host context handle
uint32_t entryPoint = 0; // Thread entry function uint32_t entryPoint = 0; // Thread entry function
uint32_t entryArg = 0; // Argument to entry function uint32_t entryArg = 0; // Argument to entry function
CpuContext cpuContext{}; // Saved CPU context for this fiber CpuContext cpuContext{}; // Saved CPU context for this fiber
@@ -102,10 +103,6 @@ private:
static void CALLBACK FiberProc(void* param); static void CALLBACK FiberProc(void* param);
#else #else
static void FiberProc(void* param); 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 #endif
// Switch from whichever fiber is currently active straight to the scheduler fiber, without // Switch from whichever fiber is currently active straight to the scheduler fiber, without
// the SwitchToThread bookkeeping (CPU context save/restore, s_currentGuestThread). Used for // the SwitchToThread bookkeeping (CPU context save/restore, s_currentGuestThread). Used for
@@ -117,9 +114,8 @@ private:
static std::mutex s_mutex; static std::mutex s_mutex;
static std::unordered_map<uint32_t, GuestFiber> s_fibers; static std::unordered_map<uint32_t, GuestFiber> s_fibers;
static std::vector<void*> s_fibersPendingDelete; static std::vector<void*> s_fibersPendingDelete;
// The scheduler's own "fiber": a Windows HFIBER, or (non-Windows) libco's cothread_t for // The scheduler's own host context. Its opaque handle is supplied by the
// whichever native call stack first called GuestFiberManager::Initialize() - both are // active HostContext backend, so one field serves every supported host.
// plain void* handles, so one field serves both platforms.
static void* s_schedulerFiber; static void* s_schedulerFiber;
static uint32_t s_currentGuestThread; static uint32_t s_currentGuestThread;
static bool s_initialized; static bool s_initialized;
@@ -135,4 +131,3 @@ private:
extern std::atomic<uint32_t> g_viRetracePendingCount; extern std::atomic<uint32_t> g_viRetracePendingCount;
} // namespace Fiber } // namespace Fiber
+26
View File
@@ -14,10 +14,17 @@ namespace GuestFlat {
// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load // Fixed base so the emitted access is `[reg + imm64-in-register]` with no load
// of a global. // of a global.
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull; inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
inline constexpr size_t kGuestPageSize = 0x1000;
#if defined(__x86_64__) #if defined(__x86_64__)
// 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap // 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap
// placement. // placement.
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'1000'0000'0000ull; 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__) #elif defined(__aarch64__)
// 16 TiB (this arch's x86_64 sibling value) is unreachable on any 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 // 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. // True once the reservation exists and translated code may use the flat path.
bool IsActive(); 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 // 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 // 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 // reservation, the section objects or a view cannot be created - a silent
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <cstddef>
// 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
+4
View File
@@ -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. // 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) MKW_PPC_FORCE_INLINE const uint8_t* PpcTryGetPsqReadableHostInline(uint32_t addr)
{ {
if (GuestFlat::RequiresCheckedAccess()) [[unlikely]]
return nullptr;
return MKW_FLAT_GUEST_BASE + addr; 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. // executable, and unmapped pages still trap.
MKW_PPC_FORCE_INLINE uint8_t* PpcTryGetPsqWritableHostInline(uint32_t addr) MKW_PPC_FORCE_INLINE uint8_t* PpcTryGetPsqWritableHostInline(uint32_t addr)
{ {
if (GuestFlat::RequiresCheckedAccess()) [[unlikely]]
return nullptr;
if (addr > UINT32_MAX - 7u) [[unlikely]] if (addr > UINT32_MAX - 7u) [[unlikely]]
return nullptr; return nullptr;
if (MemoryInline::FlatWriteNeedsPolicy(addr) || if (MemoryInline::FlatWriteNeedsPolicy(addr) ||
+34 -5
View File
@@ -231,6 +231,13 @@ MKW_MEMORY_FORCE_INLINE uint8_t* ResolveRangeHost(uint32_t base, int32_t minOffs
(void)needsRead; (void)needsRead;
const uint32_t guestStart = base + static_cast<uint32_t>(minOffset); const uint32_t guestStart = base + static_cast<uint32_t>(minOffset);
if (length == 0 || length > kPageSize || guestStart > UINT32_MAX - (length - 1)) return nullptr; 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 && if (needsWrite &&
(FlatWriteNeedsPolicy(guestStart) || FlatWriteNeedsPolicy(guestStart + (length - 1)))) (FlatWriteNeedsPolicy(guestStart) || FlatWriteNeedsPolicy(guestStart + (length - 1))))
[[unlikely]] return nullptr; [[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 // 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 // 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. // 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 <typename T> template <typename T>
MKW_MEMORY_FORCE_INLINE T FlatLoad(uint32_t address) { 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)); std::memcpy(MKW_FLAT_GUEST_BASE + address, &swapped, sizeof(T));
} }
MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) { return FlatLoad<uint8_t>(address); } MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) {
MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) { return FlatLoad<uint16_t>(address); } if (GuestFlat::RequiresCheckedAccess()) return Memory::Read8(address);
MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) { return FlatLoad<uint32_t>(address); } return FlatLoad<uint8_t>(address);
}
MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) {
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read16(address);
return FlatLoad<uint16_t>(address);
}
MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) {
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read32(address);
return FlatLoad<uint32_t>(address);
}
MKW_MEMORY_FORCE_INLINE float FlatReadFloat32(uint32_t address) { MKW_MEMORY_FORCE_INLINE float FlatReadFloat32(uint32_t address) {
const uint32_t bits = FlatLoad<uint32_t>(address); const uint32_t bits = FlatRead32(address);
float value = 0.0f; float value = 0.0f;
std::memcpy(&value, &bits, sizeof(value)); std::memcpy(&value, &bits, sizeof(value));
return value; return value;
} }
MKW_MEMORY_FORCE_INLINE double FlatReadFloat64(uint32_t address) { MKW_MEMORY_FORCE_INLINE double FlatReadFloat64(uint32_t address) {
const uint64_t bits = FlatLoad<uint64_t>(address); const uint64_t bits = GuestFlat::RequiresCheckedAccess()
? Memory::Read64(address) : FlatLoad<uint64_t>(address);
double value = 0.0; double value = 0.0;
std::memcpy(&value, &bits, sizeof(value)); std::memcpy(&value, &bits, sizeof(value));
return value; return value;
} }
MKW_MEMORY_FORCE_INLINE void FlatWrite8(uint32_t address, uint8_t 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; } if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write8Slow(address, value); return; }
FlatStore<uint8_t>(address, value); FlatStore<uint8_t>(address, value);
} }
MKW_MEMORY_FORCE_INLINE void FlatWrite16(uint32_t address, uint16_t 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; } if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write16Slow(address, value); return; }
FlatStore<uint16_t>(address, value); FlatStore<uint16_t>(address, value);
} }
MKW_MEMORY_FORCE_INLINE void FlatWrite32(uint32_t address, uint32_t 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; } if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write32Slow(address, value); return; }
FlatStore<uint32_t>(address, value); FlatStore<uint32_t>(address, value);
} }
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat32(uint32_t address, double 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); const uint32_t bits = ConvertPpcDoubleToSingleBits(value);
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat32Slow(address, value); return; } if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat32Slow(address, value); return; }
FlatStore<uint32_t>(address, bits); FlatStore<uint32_t>(address, bits);
} }
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat64(uint32_t address, double value) { MKW_MEMORY_FORCE_INLINE void FlatWriteFloat64(uint32_t address, double value) {
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat64(address, value); return; }
uint64_t bits = 0; uint64_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits)); std::memcpy(&bits, &value, sizeof(bits));
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat64Slow(address, value); return; } if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat64Slow(address, value); return; }
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <cstdint>
#include <filesystem>
#include <optional>
#include <string_view>
// 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<std::filesystem::path> 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
+12
View File
@@ -17,6 +17,7 @@
#include <utility> #include <utility>
#include <vector> #include <vector>
#include <toml.hpp> #include <toml.hpp>
#include "platform/host_platform.h"
#ifdef _WIN32 #ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN #ifndef WIN32_LEAN_AND_MEAN
#define 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 // Must stay in step with the backend table in main.cpp, which is what actually
// maps these to AuroraBackend. // maps these to AuroraBackend.
inline bool IsSupportedGraphicsApi(std::string_view value) { inline bool IsSupportedGraphicsApi(std::string_view value) {
#if defined(__APPLE__)
static constexpr std::array<std::string_view, 2> values{"auto", "metal"};
// only vulkan for linux
#elif defined(__linux__)
static constexpr std::array<std::string_view, 2> values{"auto", "vulkan"};
#elif defined(_WIN32)
static constexpr std::array<std::string_view, 3> values{"auto", "d3d12", "vulkan"}; static constexpr std::array<std::string_view, 3> values{"auto", "d3d12", "vulkan"};
#endif
return std::find(values.begin(), values.end(), value) != values.end(); return std::find(values.begin(), values.end(), value) != values.end();
} }
@@ -172,6 +180,8 @@ inline std::optional<std::filesystem::path> ExecutableDirectory() {
} }
buffer.resize(buffer.size() * 2); buffer.resize(buffer.size() * 2);
} }
#elif defined(__APPLE__)
return RuntimePlatform::ExecutableDirectory();
#else #else
// /proc/self/exe is a Linux-specific magic symlink to the running executable; readlink() // /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 // 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); CoTaskMemFree(rawPath);
return directory; return directory;
} }
#elif defined(__APPLE__)
return RuntimePlatform::ApplicationDataDirectory(kApplicationDirectoryName);
#else #else
// XDG Base Directory spec equivalent of FOLDERID_LocalAppData: $XDG_DATA_HOME if set and // XDG Base Directory spec equivalent of FOLDERID_LocalAppData: $XDG_DATA_HOME if set and
// non-empty, otherwise its default of $HOME/.local/share. // non-empty, otherwise its default of $HOME/.local/share.
+1
View File
@@ -36,6 +36,7 @@ extern thread_local uint32_t g_sehLastAccessType;
void WriteFatalLog(std::string_view reason); void WriteFatalLog(std::string_view reason);
void SetRuntimeExitCode(int code); void SetRuntimeExitCode(int code);
void MarkFatalErrorReported();
// Centralized crash reporting (defined in main.cpp). Every fatal path funnels // Centralized crash reporting (defined in main.cpp). Every fatal path funnels
// through these so the per-run log folder always receives the same artifact // through these so the per-run log folder always receives the same artifact
+24 -139
View File
@@ -2,6 +2,7 @@
#include "memory.h" #include "memory.h"
#include "abi_bridge.h" #include "abi_bridge.h"
#include "hle_stubs.h" #include "hle_stubs.h"
#include "host_context.h"
#include "runtime_log.h" #include "runtime_log.h"
// Defined in hle/os/os_sleep.cpp; the sleep-timer table is file-local there. // Defined in hle/os/os_sleep.cpp; the sleep-timer table is file-local there.
@@ -13,24 +14,8 @@
#include <iomanip> #include <iomanip>
#include <sstream> #include <sstream>
#if !defined(_WIN32)
#include "libco.h"
#endif
namespace Fiber { 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::mutex GuestFiberManager::s_mutex;
std::unordered_map<uint32_t, GuestFiber> GuestFiberManager::s_fibers; std::unordered_map<uint32_t, GuestFiber> GuestFiberManager::s_fibers;
std::vector<void*> GuestFiberManager::s_fibersPendingDelete; std::vector<void*> GuestFiberManager::s_fibersPendingDelete;
@@ -45,21 +30,11 @@ void GuestFiberManager::PurgePendingFibers() {
std::lock_guard<std::mutex> lock(s_mutex); std::lock_guard<std::mutex> lock(s_mutex);
toDelete.swap(s_fibersPendingDelete); toDelete.swap(s_fibersPendingDelete);
} }
#if defined(_WIN32)
const void* current = GetCurrentFiber();
for (void* f : toDelete) { for (void* f : toDelete) {
if (f && f != current) { if (f && !HostContext::IsCurrent(f)) {
DeleteFiber(f); HostContext::Destroy(f);
} }
} }
#else
const void* current = co_active();
for (void* f : toDelete) {
if (f && f != current) {
co_delete(static_cast<cothread_t>(f));
}
}
#endif
} }
// Global VI retrace counter // Global VI retrace counter
@@ -212,27 +187,13 @@ void GuestFiberManager::Initialize() {
return; return;
} }
#if defined(_WIN32) if (!HostContext::InitializeScheduler(&s_schedulerFiber)) {
// Convert the main thread to a fiber (the scheduler fiber) RT_LOG(RT_TAG_OS) << "FATAL: Failed to initialize scheduler context!" << std::endl;
s_schedulerFiber = ConvertThreadToFiber(nullptr); ShowRuntimeFatalPopup("guest scheduler initialization failed",
if (!s_schedulerFiber) { "The host could not create the scheduler context required to run guest threads.");
// May already be a fiber std::abort();
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();
}
} }
#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_currentGuestThread = 0;
s_initialized = true; s_initialized = true;
} }
@@ -240,32 +201,18 @@ void GuestFiberManager::Initialize() {
void GuestFiberManager::Shutdown() { void GuestFiberManager::Shutdown() {
std::lock_guard<std::mutex> lock(s_mutex); std::lock_guard<std::mutex> lock(s_mutex);
#if defined(_WIN32)
for (auto& [addr, fiber] : s_fibers) { for (auto& [addr, fiber] : s_fibers) {
if (fiber.fiber && !fiber.isSchedulerFiber) { if (fiber.fiber && !fiber.isSchedulerFiber) {
DeleteFiber(fiber.fiber); HostContext::Destroy(fiber.fiber);
fiber.fiber = nullptr; fiber.fiber = nullptr;
} }
} }
s_fibers.clear(); s_fibers.clear();
// Convert scheduler fiber back to thread
if (s_schedulerFiber) { if (s_schedulerFiber) {
ConvertFiberToThread(); HostContext::ShutdownScheduler(s_schedulerFiber);
s_schedulerFiber = nullptr; s_schedulerFiber = nullptr;
} }
#else
for (auto& [addr, fiber] : s_fibers) {
if (fiber.fiber && !fiber.isSchedulerFiber) {
co_delete(static_cast<cothread_t>(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; s_initialized = false;
} }
@@ -288,11 +235,7 @@ bool GuestFiberManager::CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entr
if (existingIt != s_fibers.end()) { if (existingIt != s_fibers.end()) {
// Delete the old fiber if it exists and is not the scheduler fiber // Delete the old fiber if it exists and is not the scheduler fiber
if (existingIt->second.fiber && !existingIt->second.isSchedulerFiber) { if (existingIt->second.fiber && !existingIt->second.isSchedulerFiber) {
#if defined(_WIN32) HostContext::Destroy(existingIt->second.fiber);
DeleteFiber(existingIt->second.fiber);
#else
co_delete(static_cast<cothread_t>(existingIt->second.fiber));
#endif
} }
s_fibers.erase(existingIt); s_fibers.erase(existingIt);
} }
@@ -312,31 +255,17 @@ bool GuestFiberManager::CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entr
gf.cpuContext.pc = entryPoint; gf.cpuContext.pc = entryPoint;
gf.cpuContext.srr0 = entryPoint; gf.cpuContext.srr0 = entryPoint;
#if defined(_WIN32) // The host stack models only translated host calls; the guest stack starts
// Create Windows fiber with reasonable stack size // at stackBase in the CPU context above.
// Use host stack size (64KB should be plenty for translated code)
constexpr size_t kHostStackSize = 64 * 1024; constexpr size_t kHostStackSize = 64 * 1024;
gf.fiber = CreateFiber(kHostStackSize, FiberProc, reinterpret_cast<void*>(static_cast<uintptr_t>(guestThreadAddr))); gf.fiber = HostContext::Create(kHostStackSize, FiberProc,
reinterpret_cast<void*>(static_cast<uintptr_t>(guestThreadAddr)));
if (!gf.fiber) { if (!gf.fiber) {
DWORD err = GetLastError(); RT_LOG(RT_TAG_OS) << "Failed to create host context for thread 0x"
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"
<< std::hex << guestThreadAddr << std::dec << std::endl; << std::hex << guestThreadAddr << std::dec << std::endl;
return false; return false;
} }
#endif
s_fibers[guestThreadAddr] = gf; s_fibers[guestThreadAddr] = gf;
@@ -390,24 +319,11 @@ void GuestFiberManager::ExitGuestThread(uint32_t guestThreadAddr, ThreadState fi
} }
if (it->second.fiber && !it->second.isSchedulerFiber) { if (it->second.fiber && !it->second.isSchedulerFiber) {
#if defined(_WIN32) if (HostContext::IsCurrent(it->second.fiber)) {
const void* current = GetCurrentFiber();
if (it->second.fiber == current) {
s_fibersPendingDelete.push_back(it->second.fiber); s_fibersPendingDelete.push_back(it->second.fiber);
} else { } 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<cothread_t>(it->second.fiber));
}
#endif
it->second.fiber = nullptr; 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 // Check if we're already on the target fiber (e.g., switching to main thread
// when we're already on the scheduler fiber) // when we're already on the scheduler fiber)
#if defined(_WIN32) if (HostContext::IsCurrent(fiberHandle)) {
void* currentFiber = GetCurrentFiber();
#else
void* currentFiber = co_active();
#endif
if (currentFiber == fiberHandle) {
// Already executing on the target host fiber. This is common for the // Already executing on the target host fiber. This is common for the
// default guest thread, which also owns the scheduler fiber. Keep the // default guest thread, which also owns the scheduler fiber. Keep the
// live CPU context instead of restoring a possibly stale saved copy // 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) // Switch to the target fiber (the target fiber will load its own context)
#if defined(_WIN32) HostContext::Switch(fiberHandle);
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<cothread_t>(fiberHandle));
#endif
// When we return here, the fiber that issued SwitchToThread has resumed. // When we return here, the fiber that issued SwitchToThread has resumed.
// That does not automatically mean the previous guest thread became runnable // 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<cothread_t>(s_schedulerFiber));
#endif
}
#if defined(_WIN32) #if defined(_WIN32)
void CALLBACK GuestFiberManager::FiberProc(void* param) void CALLBACK GuestFiberManager::FiberProc(void* param)
#else #else
@@ -634,6 +529,7 @@ void GuestFiberManager::FiberProc(void* param)
{ {
uint32_t guestThreadAddr = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(param)); uint32_t guestThreadAddr = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(param));
// Get our fiber info // Get our fiber info
GuestFiber* fiber = nullptr; GuestFiber* fiber = nullptr;
uint32_t entryPoint = 0; uint32_t entryPoint = 0;
@@ -644,7 +540,7 @@ void GuestFiberManager::FiberProc(void* param)
auto it = s_fibers.find(guestThreadAddr); auto it = s_fibers.find(guestThreadAddr);
if (it == s_fibers.end()) { if (it == s_fibers.end()) {
RT_LOG(RT_TAG_OS) << "FiberProc: fiber not found!" << std::endl; RT_LOG(RT_TAG_OS) << "FiberProc: fiber not found!" << std::endl;
SwitchToScheduler(); HostContext::Switch(s_schedulerFiber);
return; return;
} }
fiber = &it->second; fiber = &it->second;
@@ -707,7 +603,7 @@ void GuestFiberManager::FiberProc(void* param)
<< ", fn=0x" << startFn << ") after retries; continuing anyway." << std::dec << std::endl; << ", fn=0x" << startFn << ") after retries; continuing anyway." << std::dec << std::endl;
break; break;
} }
SwitchToScheduler(); HostContext::Switch(s_schedulerFiber);
} }
// The deferral loop above yields to the scheduler and therefore can resume // The deferral loop above yields to the scheduler and therefore can resume
@@ -764,18 +660,7 @@ void GuestFiberManager::FiberProc(void* param)
} }
// Return to scheduler // Return to scheduler
SwitchToScheduler(); HostContext::Switch(s_schedulerFiber);
} }
#if !defined(_WIN32)
void GuestFiberManager::FiberProcTrampoline() {
const uint32_t guestThreadAddr = s_pendingFiberArg;
FiberProc(reinterpret_cast<void*>(static_cast<uintptr_t>(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 } // namespace Fiber
+21 -4
View File
@@ -36,6 +36,9 @@
#endif #endif
namespace GuestFlat { namespace GuestFlat {
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
bool g_requiresCheckedAccess = false;
#endif
namespace { namespace {
#if defined(_WIN32) #if defined(_WIN32)
@@ -48,6 +51,16 @@ constexpr DWORD kMemPreservePlaceholder = 0x00000002;
constexpr size_t kAllocationGranularity = 0x10000; // 64 KiB constexpr size_t kAllocationGranularity = 0x10000; // 64 KiB
constexpr size_t kHostPageSize = 0x1000; 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_t>(size) : kGuestPageSize;
}
#endif
// Named, platform-neutral protection modes so every fault-interception call site below (the // 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 // 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 // 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); } bool IsGpuFifo(uint32_t address) { return MemoryInline::IsGpuFifoAddress(address); }
void ApplyExecutableProtectionLocked() { void ApplyExecutableProtectionLocked() {
if (g_base == nullptr) return; if (g_base == nullptr || RequiresCheckedAccess()) return;
auto& protectedPages = ExecutableProtectedPages(); auto& protectedPages = ExecutableProtectedPages();
for (const auto& range : ExecutableRanges()) { for (const auto& range : ExecutableRanges()) {
// Only pages fully inside the range are protected: edge pages often share a page with data // 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<RegionRequest>& regions) { void Initialize(const std::vector<RegionRequest>& regions) {
std::lock_guard<std::mutex> lock(StateMutex()); std::lock_guard<std::mutex> lock(StateMutex());
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
g_requiresCheckedAccess = HostPageSize() > kGuestPageSize;
#endif
if (g_initialized) { if (g_initialized) {
if (!SameLayout(g_activeRegions, regions)) { if (!SameLayout(g_activeRegions, regions)) {
throw std::runtime_error( throw std::runtime_error(
@@ -615,7 +632,7 @@ uint8_t* HostPointer(uint32_t guestAddress) {
} }
void ProtectDeferredRange(uint32_t address, size_t length) { 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<uint64_t>(address) + length; const uint64_t end = static_cast<uint64_t>(address) + length;
if (end > kGuestSpaceSize) return; if (end > kGuestSpaceSize) return;
std::lock_guard<std::mutex> lock(StateMutex()); std::lock_guard<std::mutex> lock(StateMutex());
@@ -630,7 +647,7 @@ void ProtectDeferredRange(uint32_t address, size_t length) {
} }
void UnprotectDeferredRange(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<std::mutex> lock(StateMutex()); std::lock_guard<std::mutex> lock(StateMutex());
auto& ranges = DeferredRanges(); auto& ranges = DeferredRanges();
const uint64_t end = static_cast<uint64_t>(address) + length; const uint64_t end = static_cast<uint64_t>(address) + length;
@@ -645,7 +662,7 @@ void UnprotectDeferredRange(uint32_t address, size_t length) {
} }
void RegisterExecutableRange(uint32_t start, uint32_t end) { void RegisterExecutableRange(uint32_t start, uint32_t end) {
if (end <= start) return; if (RequiresCheckedAccess() || end <= start) return;
std::lock_guard<std::mutex> lock(StateMutex()); std::lock_guard<std::mutex> lock(StateMutex());
auto& ranges = ExecutableRanges(); auto& ranges = ExecutableRanges();
if (std::any_of(ranges.begin(), ranges.end(), [&](const GuardedRange& range) { if (std::any_of(ranges.begin(), ranges.end(), [&](const GuardedRange& range) {
+80
View File
@@ -0,0 +1,80 @@
#include "guest_flat_memory.h"
#include <mach/mach.h>
#include <mach/mach_vm.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <algorithm>
#include <cstdio>
#include <mutex>
#include <stdexcept>
#include <vector>
namespace GuestFlat {
bool g_requiresCheckedAccess = false;
namespace {
struct Mapping { uint32_t base; uint64_t size; uint8_t* host; };
std::mutex g_mutex;
std::vector<Mapping> g_mappings;
std::vector<RegionRequest> 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<RegionRequest>& a, const std::vector<RegionRequest>& 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<off_t>(size)) != 0) { close(fd); return -1; } }
return fd;
}
} // namespace
bool IsActive() { return g_active; }
void Initialize(const std::vector<RegionRequest>& regions) {
std::lock_guard lock(g_mutex);
g_requiresCheckedAccess = static_cast<size_t>(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<uint8_t*>(address);
struct Store { Backing kind; uint32_t owned; uint64_t size; int fd; };
std::vector<Store> 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<uint8_t*>(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
+1
View File
@@ -394,3 +394,4 @@ extern std::mutex g_tlutObjMutex;
// that happens on another key: unordered_map keeps references valid across a // that happens on another key: unordered_map keeps references valid across a
// rehash, an open-addressed table would not. // rehash, an open-addressed table would not.
extern std::unordered_map<uint32_t, TexObjSlot> g_TexObjMeta; extern std::unordered_map<uint32_t, TexObjSlot> g_TexObjMeta;
extern std::map<uint32_t, TlutObjMeta> g_TlutObjMeta;
+9
View File
@@ -33,6 +33,15 @@ void WriteGuestFloat(uint32_t addr, float value, const char* label) {
void* GuestToHostPtr(uint32_t addr, size_t len) { void* GuestToHostPtr(uint32_t addr, size_t len) {
if (addr == 0) return nullptr; 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; } try { return Memory::GetPointer(addr, len); } catch (const Memory::AccessViolation& e) { LogMemoryError(RT_TAG_GX, "GX guest pointer", e); return nullptr; }
} }
+3 -5
View File
@@ -6,6 +6,7 @@
#include "aurora_events.h" #include "aurora_events.h"
#include "settings_overlay.h" #include "settings_overlay.h"
#include "fiber_manager.h" #include "fiber_manager.h"
#include "platform/host_platform.h"
#include "runtime_log.h" #include "runtime_log.h"
#include <dolphin/vi.h> #include <dolphin/vi.h>
@@ -20,18 +21,15 @@
#include <mutex> #include <mutex>
#include <thread> #include <thread>
#include <aurora/aurora.h>
#if defined(_WIN32) #if defined(_WIN32)
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN #ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#endif #endif
#include <windows.h> #include <windows.h>
#endif #endif
#include <aurora/aurora.h>
// Forward declaration for OSWakeupThread - used to wake threads on VI retrace queue // Forward declaration for OSWakeupThread - used to wake threads on VI retrace queue
extern "C" void OSWakeupThread_HLE_801aaaa4(CpuContext* ctx); extern "C" void OSWakeupThread_HLE_801aaaa4(CpuContext* ctx);
+270
View File
@@ -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 <windows.h>
#elif defined(__APPLE__) && defined(__aarch64__)
#include <sys/mman.h>
#include <unistd.h>
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 <libco.h>
#include <cstdlib>
#include <unordered_map>
#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<Context*>(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<std::size_t>(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<char*>(context->stack) + totalSize;
context->savedStackPointer = mkw_co_init(stackTop, entry, argument);
return context;
}
void Destroy(Handle context)
{
auto* nativeContext = static_cast<Context*>(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<Context*>(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<cothread_t, Context*> 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<Context*>(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<unsigned int>(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*>(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<Context*>(target);
Context* source = g_current;
if (!destination || destination == source) {
return;
}
g_current = destination;
co_switch(destination->native);
g_current = source;
}
#endif
} // namespace HostContext
+21
View File
@@ -23,6 +23,10 @@
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#if !defined(_WIN32)
#include <unistd.h>
#endif
#if defined(_WIN32) #if defined(_WIN32)
#ifndef NOMINMAX #ifndef NOMINMAX
#define NOMINMAX #define NOMINMAX
@@ -38,7 +42,12 @@
#include <dbghelp.h> #include <dbghelp.h>
#else #else
#include <signal.h> #include <signal.h>
#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 <ucontext.h> #include <ucontext.h>
#endif
#include <unistd.h> #include <unistd.h>
#endif #endif
@@ -1370,9 +1379,21 @@ int RuntimeMain(int argc, char** argv) {
const char* configName; const char* configName;
AuroraBackend backend; AuroraBackend backend;
}; };
#if defined(__APPLE__)
static constexpr std::array<GraphicsBackendEntry, 2> kGraphicsBackends{{
{"auto", BACKEND_AUTO}, {"metal", BACKEND_METAL},
}};
// only vulkan for linux
#elif defined(__linux__)
static constexpr std::array<GraphicsBackendEntry, 2> kGraphicsBackends{{
{"auto", BACKEND_AUTO}, {"vulkan", BACKEND_VULKAN},
}};
#elif defined(_WIN32)
static constexpr std::array<GraphicsBackendEntry, 3> kGraphicsBackends{{ static constexpr std::array<GraphicsBackendEntry, 3> kGraphicsBackends{{
{"auto", BACKEND_AUTO}, {"d3d12", BACKEND_D3D12}, {"vulkan", BACKEND_VULKAN}, {"auto", BACKEND_AUTO}, {"d3d12", BACKEND_D3D12}, {"vulkan", BACKEND_VULKAN},
}}; }};
#endif
const auto backendDisplayName = [](AuroraBackend value) -> const char* { const auto backendDisplayName = [](AuroraBackend value) -> const char* {
for (const auto& entry : kGraphicsBackends) { for (const auto& entry : kGraphicsBackends) {
if (entry.backend == value) { if (entry.backend == value) {
+85
View File
@@ -0,0 +1,85 @@
#include "platform/host_platform.h"
#include <cstdlib>
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <shlobj.h>
#else
#include <unistd.h>
#endif
#if defined(__APPLE__)
#include <mach-o/dyld.h>
#include <pwd.h>
#endif
namespace RuntimePlatform {
std::optional<std::filesystem::path> ExecutableDirectory() noexcept {
#if defined(_WIN32)
std::wstring buffer(MAX_PATH, L'\0');
for (;;) {
const DWORD length = GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(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<char>::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<uint64_t>(::GetCurrentProcessId());
#else
return static_cast<uint64_t>(::getpid());
#endif
}
} // namespace RuntimePlatform
+59
View File
@@ -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
+49
View File
@@ -0,0 +1,49 @@
#include "host_context.h"
#include <cstdlib>
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);
}
+46
View File
@@ -0,0 +1,46 @@
#include <array>
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <vector>
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<std::byte, 64 * 1024> g_workerStack{};
void* g_schedulerSp = nullptr;
void* g_workerSp = nullptr;
std::vector<int> 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<int>{1}) {
std::cerr << "worker did not yield to scheduler\n";
return 1;
}
mkw_co_switch(&g_workerSp, &g_schedulerSp);
if (g_events != std::vector<int>{1, 2}) {
std::cerr << "worker did not resume from saved context\n";
return 1;
}
return 0;
}
@@ -0,0 +1,66 @@
#include "guest_flat_memory.h"
#include <cstdint>
#include <iostream>
#include <stdexcept>
#include <unistd.h>
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<size_t>(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<const uint8_t*>(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<uint8_t*>(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;
}
+30
View File
@@ -0,0 +1,30 @@
#include "platform/host_platform.h"
#include <iostream>
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;
}
+34 -2
View File
@@ -166,6 +166,7 @@ return command switch
"emit-build-shards" => RunEmitBuildShards(tail), "emit-build-shards" => RunEmitBuildShards(tail),
"emit-base-manifest" => RunEmitBaseManifest(tail), "emit-base-manifest" => RunEmitBaseManifest(tail),
"check-base-mod-awareness" => RunCheckBaseModAwareness(tail), "check-base-mod-awareness" => RunCheckBaseModAwareness(tail),
"validate-retro-wfc-payload" => RunValidateRetroWfcPayload(tail),
_ => ShowHelp(command) _ => ShowHelp(command)
}; };
@@ -354,6 +355,33 @@ int RunInfo()
return 0; 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) int RunTranslateRecursive(string[] argsTail)
{ {
if (argsTail.Length == 0 || argsTail[0].StartsWith("--", StringComparison.Ordinal)) if (argsTail.Length == 0 || argsTail[0].StartsWith("--", StringComparison.Ordinal))
@@ -3639,7 +3667,8 @@ static string[] KnownCommands() => new[]
"translate-mod", "translate-mod",
"emit-base-manifest", "emit-base-manifest",
"emit-build-shards", "emit-build-shards",
"check-base-mod-awareness" "check-base-mod-awareness",
"validate-retro-wfc-payload"
}; };
/// <summary> /// <summary>
@@ -3682,6 +3711,10 @@ static (string? Positional, CommandOption[] Options)? CommandSpec(string command
new("--translation-output-metadata", "path"), new("--translation-output-metadata", "path"),
new("--code-pul", "path") new("--code-pul", "path")
}), }),
"validate-retro-wfc-payload" => (null, new CommandOption[]
{
new("--directory", "directory", Required: true)
}),
"emit-base-manifest" => (null, new CommandOption[] "emit-base-manifest" => (null, new CommandOption[]
{ {
new("--out", "path"), new("--out", "path"),
@@ -4061,4 +4094,3 @@ sealed record ResolvedDispatchEntry(
bool MustRemainDynamicallyDispatchable, bool MustRemainDynamicallyDispatchable,
string SourceFile); string SourceFile);
@@ -15,6 +15,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Translator.Core\Translator.Core.csproj" /> <ProjectReference Include="..\Translator.Core\Translator.Core.csproj" />
<ProjectReference Include="..\..\..\Launcher\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -22,14 +22,15 @@ internal static class AssemblyBlobWriter
var expectedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var expectedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var assembly = new StringBuilder(); var assembly = new StringBuilder();
foreach (var header in headerLines) assembly.AppendLine(header); foreach (var header in headerLines) assembly.AppendLine(header);
// PE/COFF (Windows) and ELF (Linux) spell a read-only data section differently in GNU-as // PE/COFF (Windows), Mach-O (macOS), and ELF (Linux) spell a read-only data section
// syntax - COFF section flags ("dr" = data, read-only) versus an ELF section needing an // differently in GNU-as syntax. The build always targets
// allocatable-only flag plus an explicit @progbits type. The build always targets
// whichever platform the translator itself runs on (there is no cross-compilation // whichever platform the translator itself runs on (there is no cross-compilation
// support), so that's what this picks the section syntax from. // support), so that's what this picks the section syntax from.
assembly.AppendLine(OperatingSystem.IsWindows() assembly.AppendLine(OperatingSystem.IsWindows()
? ".section .rdata,\"dr\"" ? ".section .rdata,\"dr\""
: ".section .rodata,\"a\",@progbits"); : OperatingSystem.IsMacOS()
? ".section __TEXT,__const"
: ".section .rodata,\"a\",@progbits");
assembly.AppendLine(); assembly.AppendLine();
foreach (var blob in blobs) foreach (var blob in blobs)
@@ -40,8 +41,12 @@ internal static class AssemblyBlobWriter
var hash = ChecksumUtilities.Sha256Hex(blob.Data.Span); var hash = ChecksumUtilities.Sha256Hex(blob.Data.Span);
assembly.AppendLine($"// {blob.Comment}; sha256={hash}"); assembly.AppendLine($"// {blob.Comment}; sha256={hash}");
assembly.AppendLine(".p2align 4"); assembly.AppendLine(".p2align 4");
assembly.AppendLine($".globl {blob.Symbol}"); // C/C++ external symbols carry a leading underscore in Mach-O,
assembly.AppendLine($"{blob.Symbol}:"); // 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); var referencePath = Path.Combine(blobReferenceDirectory, blob.FileName);
assembly.AppendLine($".incbin \"{SanitizeAssemblyPath(referencePath)}\""); assembly.AppendLine($".incbin \"{SanitizeAssemblyPath(referencePath)}\"");
assembly.AppendLine(); assembly.AppendLine();
@@ -51,7 +56,7 @@ internal static class AssemblyBlobWriter
{ {
if (!expectedFiles.Contains(Path.GetFullPath(stalePath))) File.Delete(stalePath); 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 // 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 // conservative default for this object (an executable stack) and warn about it; this
@@ -28,8 +28,10 @@ public sealed class ModDataPatchWriterTests
var assemblyPath = Path.Combine(root, "cpp", "mod_data_patches_blobs.S"); var assemblyPath = Path.Combine(root, "cpp", "mod_data_patches_blobs.S");
var assembly = File.ReadAllText(assemblyPath); var assembly = File.ReadAllText(assemblyPath);
Assert.Contains(".globl kModuleImage", assembly, StringComparison.Ordinal); var symbolPrefix = OperatingSystem.IsMacOS() ? "_" : "";
Assert.Contains(".globl kKamekCodeSha1Digest", assembly, StringComparison.Ordinal); 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); Assert.Contains(".incbin", assembly, StringComparison.Ordinal);
var timestamps = new[] { cppPath, assemblyPath } var timestamps = new[] { cppPath, assemblyPath }