mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-11 01:23:15 -04:00
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:
@@ -53,9 +53,22 @@ public static class RetroWfcPayload
|
||||
|
||||
var root = Path.GetFullPath(stagedDirectory);
|
||||
var payload = Path.Combine(root, RetroWfcOfflinePayloadFile);
|
||||
if (!File.Exists(payload))
|
||||
try
|
||||
{
|
||||
if ((File.GetAttributes(payload) & FileAttributes.Directory) != 0)
|
||||
throw new InvalidDataException(
|
||||
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
|
||||
}
|
||||
ValidateRetroWfcPayloadFile(payload, signingKey);
|
||||
return root;
|
||||
}
|
||||
|
||||
Executable
+139
@@ -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"
|
||||
Executable
+117
@@ -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"
|
||||
Executable
+81
@@ -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'
|
||||
Executable
+92
@@ -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"
|
||||
Executable
+140
@@ -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."
|
||||
@@ -66,6 +66,8 @@ adapter must be switched to the WinUSB driver once (Zadig).
|
||||
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher
|
||||
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
|
||||
- About 20 GB of free disk space during installation (Final game size ~5 GB)
|
||||
- macOS 14 (Sonoma) or later on Apple Silicon
|
||||
- On macOS, Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
|
||||
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
|
||||
GCZ, CISO, WBFS, WIA and RVZ are accepted.
|
||||
|
||||
@@ -86,6 +88,7 @@ image under Settings, turn on **WiiCompiled (beta)**, and hit install from the H
|
||||
Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and
|
||||
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
|
||||
|
||||
|
||||
> [!CAUTION]
|
||||
> Only take builds from this repository's
|
||||
> [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an
|
||||
|
||||
@@ -255,6 +255,14 @@ FrameWorkerState g_frameWorker;
|
||||
bool frame_worker_requested() noexcept {
|
||||
#ifdef AURORA_ENABLE_GX
|
||||
static const bool enabled = [] {
|
||||
#if defined(__APPLE__)
|
||||
// ImGui's SDL backend may raise an SDL window from ImGui::NewFrame(). On
|
||||
// macOS that reaches AppKit, whose window operations are main-thread-only;
|
||||
// doing it on the frame worker terminates the process with EXC_BREAKPOINT.
|
||||
// Keep all SDL/ImGui work on the calling thread until the worker no longer
|
||||
// owns frame preparation on Apple platforms.
|
||||
return false;
|
||||
#endif
|
||||
#if defined(_WIN32)
|
||||
// RenderDoc's D3D12 layer is injected before Aurora starts and needs device and command
|
||||
// ownership on one thread, so keep frame submission synchronous there.
|
||||
@@ -1516,6 +1524,15 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
|
||||
|
||||
// Phase 3: hand the encoded group to whoever owns presentation.
|
||||
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
|
||||
// interpolation off, so every mode shares one surface/resize path. RenderDoc keeps the sync path.
|
||||
if (frame_worker_requested() || interpolationActive ||
|
||||
@@ -1526,6 +1543,7 @@ void publish_presentations(std::vector<PresentationJob>&& presentationJobs, bool
|
||||
present_presentation_job(job);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void record_frame_telemetry() {
|
||||
|
||||
+158
-51
@@ -1,18 +1,28 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(mkw_recompiled)
|
||||
|
||||
if((NOT (WIN32 AND MINGW)) AND (NOT CMAKE_SYSTEM_NAME STREQUAL "Linux"))
|
||||
message(FATAL_ERROR "WiiCompiled requires Windows (LLVM-MinGW) or native Linux")
|
||||
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
message(FATAL_ERROR "WiiCompiled requires a 64-bit Clang toolchain")
|
||||
endif()
|
||||
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR
|
||||
NOT CMAKE_SIZEOF_VOID_P EQUAL 8 OR
|
||||
NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$")
|
||||
message(FATAL_ERROR "WiiCompiled requires 64-bit Clang targeting x86_64 or aarch64")
|
||||
|
||||
if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
set(MKW_PLATFORM_WINDOWS TRUE)
|
||||
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64)$")
|
||||
# The first native macOS target is Apple Silicon. Intel and universal
|
||||
# binaries remain future compatibility work; do not silently claim them.
|
||||
set(MKW_PLATFORM_MACOS TRUE)
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$")
|
||||
set(MKW_PLATFORM_LINUX TRUE)
|
||||
else()
|
||||
message(FATAL_ERROR
|
||||
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS arm64")
|
||||
endif()
|
||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
message(FATAL_ERROR "WiiCompiled only supports Release builds")
|
||||
endif()
|
||||
|
||||
option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON)
|
||||
|
||||
# Preprocessor definitions that belong to this project's own code (the runtime,
|
||||
# the translated shards and the product glue) and to nothing else. They are
|
||||
# applied directory-scoped, immediately after the aurora add_subdirectory() call,
|
||||
@@ -49,17 +59,14 @@ target_include_directories(mkw_pugixml PUBLIC third_party/pugixml)
|
||||
target_compile_features(mkw_pugixml PUBLIC cxx_std_17)
|
||||
set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF)
|
||||
|
||||
# Non-Windows guest-fiber scheduling (runtime/src/fiber_manager.cpp) needs a symmetric
|
||||
# Linux guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric
|
||||
# stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
|
||||
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
|
||||
# asymmetric resume/yield coroutine libraries, which would need every call site restructured).
|
||||
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
|
||||
# separately BSD-style licensed, see third_party/libco/LICENSE) - all of libco's non-Windows
|
||||
# CPU-architecture backends are kept, even though libco.c's own preprocessor dispatch
|
||||
# (__amd64__/__i386__/__arm__/__aarch64__/etc.) only ever selects amd64.c for this project's
|
||||
# x86_64-only target (see the platform/arch check above). Windows keeps using native Fibers
|
||||
# untouched, so this target is never built there.
|
||||
if(NOT WIN32)
|
||||
# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers
|
||||
# and macOS uses the project's x18-safe AArch64 assembly backend, so this target is Linux-only.
|
||||
if(MKW_PLATFORM_LINUX)
|
||||
add_library(mkw_libco STATIC third_party/libco/libco.c)
|
||||
add_library(mkw::libco ALIAS mkw_libco)
|
||||
target_include_directories(mkw_libco PUBLIC third_party/libco)
|
||||
@@ -130,31 +137,35 @@ else()
|
||||
message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing")
|
||||
endif()
|
||||
set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE)
|
||||
if(WIN32)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE)
|
||||
set(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_USE_WINDOWS_UI OFF CACHE BOOL "" FORCE)
|
||||
else()
|
||||
# Non-Windows (Linux): mirrors aurora-main's own
|
||||
# _aurora_dawn_set_platform_backends() choice for this platform - Vulkan only, no
|
||||
# D3D/HLSL. Kept in sync here because this project's own CMake FORCEs these cache
|
||||
# variables before aurora-main's add_subdirectory() runs, which pre-empts aurora's
|
||||
# auto-detection (CACHE ... INTERNAL "" without FORCE never overrides an existing value).
|
||||
elseif(MKW_PLATFORM_MACOS)
|
||||
set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_VULKAN OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_METAL ON CACHE BOOL "" FORCE)
|
||||
set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE)
|
||||
else()
|
||||
set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE)
|
||||
set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
|
||||
# Provide a tiny stub for DXProgrammableCapture when the SDK/PIX headers are
|
||||
# missing (common on MinGW). Dawn only includes the header; no symbols are
|
||||
# referenced when PIX isn't present.
|
||||
set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs")
|
||||
if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h")
|
||||
file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR})
|
||||
file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h"
|
||||
"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX is absent.\n")
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs")
|
||||
if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h")
|
||||
file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR})
|
||||
file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h"
|
||||
"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX isn't present.\n")
|
||||
endif()
|
||||
endif()
|
||||
# Deliberately NOT injected project-wide. Only a from-source Dawn build ever
|
||||
# includes DXProgrammableCapture.h, and this tree consumes Dawn as a prebuilt
|
||||
@@ -183,7 +194,9 @@ else()
|
||||
if(TARGET ${t})
|
||||
set_target_properties(${t} PROPERTIES UNITY_BUILD OFF)
|
||||
target_compile_options(${t} PRIVATE -O3 -ffast-math -w -pipe)
|
||||
target_include_directories(${t} PRIVATE ${MKW_DX_STUB_DIR})
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_include_directories(${t} PRIVATE ${MKW_DX_STUB_DIR})
|
||||
endif()
|
||||
# Aurora's own sources include Windows headers and call std::min/max;
|
||||
# they relied on the old project-wide NOMINMAX that no longer leaks
|
||||
# into this subtree, so the define is applied per target here.
|
||||
@@ -228,6 +241,18 @@ endif()
|
||||
# a registration file is silently never compiled and never errors. The stale-glob
|
||||
# failure mode is worth far more than the milliseconds.
|
||||
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory.cpp")
|
||||
# HostContext's Apple Silicon backend is implemented in a small assembly
|
||||
# companion. It must be part of the product runtime as well as the
|
||||
# standalone context test; otherwise the final executable is missing
|
||||
# mkw_co_init/mkw_co_switch at link time.
|
||||
enable_language(ASM)
|
||||
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
|
||||
else()
|
||||
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
|
||||
endif()
|
||||
set(MKW_PLATFORM_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/platform/host_platform.cpp")
|
||||
set(MKW_BASE_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/base_product.cpp")
|
||||
set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro_rewind_product.cpp")
|
||||
# The host ISA guard is the one translation unit that must not receive the
|
||||
@@ -235,31 +260,113 @@ set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro
|
||||
# mkw_runtime_common. See cmake/PublicProducts.cmake and the file's own header.
|
||||
set(MKW_CPU_BASELINE_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/host_cpu_baseline.cpp")
|
||||
list(REMOVE_ITEM SOURCES ${MKW_BASE_PRODUCT_SOURCE} ${MKW_RETRO_REWIND_PRODUCT_SOURCE}
|
||||
${MKW_CPU_BASELINE_SOURCE})
|
||||
${MKW_CPU_BASELINE_SOURCE} ${MKW_PLATFORM_SOURCE})
|
||||
|
||||
# This deliberately small library contains host services that are safe to
|
||||
# validate before guest memory and fiber work makes a full runtime build viable.
|
||||
add_library(mkw_platform STATIC "${MKW_PLATFORM_SOURCE}")
|
||||
target_include_directories(mkw_platform PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_platform PUBLIC cxx_std_17)
|
||||
set_target_properties(mkw_platform PROPERTIES UNITY_BUILD OFF)
|
||||
|
||||
# Keep these independent from Aurora's BUILD_TESTING option: they validate the
|
||||
# project's host-platform contracts, not Aurora's third-party test suite.
|
||||
enable_testing()
|
||||
add_executable(mkw_platform_paths_tests "${CMAKE_CURRENT_LIST_DIR}/tests/platform_paths_tests.cpp")
|
||||
target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
|
||||
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
|
||||
|
||||
# HostContext deliberately keeps the platform-specific context primitive out
|
||||
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
|
||||
# refactors cannot silently remove its headers, implementation, or link edge.
|
||||
if(MKW_PLATFORM_LINUX)
|
||||
add_executable(mkw_linux_host_context_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp")
|
||||
target_include_directories(mkw_linux_host_context_tests PRIVATE
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/third_party/libco")
|
||||
target_compile_features(mkw_linux_host_context_tests PRIVATE cxx_std_17)
|
||||
target_link_libraries(mkw_linux_host_context_tests PRIVATE mkw::libco)
|
||||
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
|
||||
endif()
|
||||
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
# Exercise the Apple Silicon context ABI and the public host-memory
|
||||
# contracts separately from translated products.
|
||||
enable_language(ASM)
|
||||
add_executable(mkw_macos_context_abi_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
|
||||
target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
|
||||
|
||||
add_executable(mkw_macos_host_context_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
|
||||
target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
|
||||
|
||||
add_executable(mkw_macos_guest_flat_memory_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_guest_flat_memory_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
|
||||
target_include_directories(mkw_macos_guest_flat_memory_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_macos_guest_flat_memory_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests)
|
||||
endif()
|
||||
|
||||
# The translator emits the complete, content-addressed source graph. Consuming
|
||||
# this one manifest keeps configure independent of the 28k generated function
|
||||
# files and of optional Retro Rewind artifacts such as code.map.
|
||||
set(MKW_TRANSLATED_SHARD_MANIFEST
|
||||
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
|
||||
CACHE FILEPATH "Translator-owned aggregate shard manifest")
|
||||
# The prebuilt export only needs the aurora/third-party closure configured above, so a
|
||||
# packaging machine without a translation stops here instead of failing.
|
||||
if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
message(STATUS "No translator shard manifest; configuring the native prebuilt export only")
|
||||
return()
|
||||
endif()
|
||||
if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
message(FATAL_ERROR
|
||||
"Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. "
|
||||
"Run Translator.Cli emit-build-shards first; see translator/README.md.")
|
||||
endif()
|
||||
include("${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS})
|
||||
message(STATUS
|
||||
"Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; "
|
||||
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; "
|
||||
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions")
|
||||
if(MKW_BUILD_PRODUCTS)
|
||||
set(MKW_TRANSLATED_SHARD_MANIFEST
|
||||
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
|
||||
CACHE FILEPATH "Translator-owned aggregate shard manifest")
|
||||
# The prebuilt export only needs the aurora/third-party closure configured above, so a
|
||||
# packaging machine without a translation stops here instead of failing.
|
||||
if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
message(STATUS "No translator shard manifest; configuring the native prebuilt export only")
|
||||
return()
|
||||
endif()
|
||||
if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
message(FATAL_ERROR
|
||||
"Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. "
|
||||
"Run Translator.Cli emit-build-shards first; see translator/README.md.")
|
||||
endif()
|
||||
include("${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS})
|
||||
message(STATUS
|
||||
"Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; "
|
||||
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; "
|
||||
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions")
|
||||
|
||||
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake")
|
||||
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake")
|
||||
else()
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
# Compile-only audit of native runtime sources. It deliberately avoids
|
||||
# translated products until their host dependencies are portable.
|
||||
# These sources depend on generated/RuntimeConfig.h, which is emitted for
|
||||
# a particular game by the translator and is intentionally unavailable
|
||||
# in this platform-only configuration.
|
||||
set(MKW_MACOS_NATIVE_AUDIT_SOURCES ${SOURCES})
|
||||
list(REMOVE_ITEM MKW_MACOS_NATIVE_AUDIT_SOURCES
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/abi_bridge.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/hle/os/os_alarm.cpp")
|
||||
add_library(mkw_macos_native_compile OBJECT ${MKW_MACOS_NATIVE_AUDIT_SOURCES})
|
||||
target_include_directories(mkw_macos_native_compile PRIVATE
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include" "${CMAKE_CURRENT_LIST_DIR}/src"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/.." "${CMAKE_CURRENT_LIST_DIR}/../aurora-main/include")
|
||||
target_compile_features(mkw_macos_native_compile PRIVATE cxx_std_20)
|
||||
target_compile_definitions(mkw_macos_native_compile PRIVATE SDL_MAIN_HANDLED TARGET_PC)
|
||||
target_link_libraries(mkw_macos_native_compile PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
|
||||
mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
|
||||
endif()
|
||||
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
|
||||
message(STATUS "Translated product targets disabled (MKW_BUILD_PRODUCTS=OFF)")
|
||||
endif()
|
||||
|
||||
@@ -25,6 +25,11 @@ if(EXISTS "${DATA_INIT_BLOB_ASM}")
|
||||
endif()
|
||||
list(REMOVE_DUPLICATES SOURCES)
|
||||
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
find_library(MKW_IOKIT_FRAMEWORK IOKit REQUIRED)
|
||||
find_library(MKW_COREFOUNDATION_FRAMEWORK CoreFoundation REQUIRED)
|
||||
endif()
|
||||
|
||||
function(mkw_apply_common_compile_options target)
|
||||
target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe)
|
||||
endfunction()
|
||||
@@ -76,10 +81,10 @@ target_compile_definitions(mkw_runtime_common PRIVATE
|
||||
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
if(WIN32)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
|
||||
else()
|
||||
elseif(MKW_PLATFORM_LINUX)
|
||||
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
|
||||
# media monitoring). Empty string on glibc >= 2.34 where dl* is in libc.
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS})
|
||||
@@ -127,16 +132,16 @@ set_target_properties(mkw_runtime_common PROPERTIES UNITY_BUILD ON UNITY_BUILD_M
|
||||
target_precompile_headers(mkw_runtime_common PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
|
||||
mkw_apply_common_compile_options(mkw_runtime_common)
|
||||
|
||||
# Host ISA guard. Everything in MKW_ALL_BUILD_TARGETS below is compiled with
|
||||
# -march=x86-64-v3; this object library deliberately is not, which
|
||||
# is the whole point of keeping it out of mkw_runtime_common. It runs a CPUID
|
||||
# check from a C initializer so an unsupported machine gets a readable error
|
||||
# instead of an illegal-instruction crash. Excluded from the unity build and the
|
||||
# precompiled header because both are produced with the owning target's flags.
|
||||
add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}")
|
||||
target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17)
|
||||
set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF)
|
||||
target_compile_options(mkw_cpu_baseline PRIVATE -w)
|
||||
# Host ISA guard. Windows and Linux x86_64 product targets use x86-64-v3, so
|
||||
# this object deliberately keeps the plain baseline ISA and checks the CPU
|
||||
# before any AVX2/FMA code can execute. AArch64 has no equivalent optional ISA
|
||||
# floor to probe: NEON/FMA are architectural requirements.
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}")
|
||||
target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17)
|
||||
set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF)
|
||||
target_compile_options(mkw_cpu_baseline PRIVATE -w)
|
||||
endif()
|
||||
|
||||
if(NOT MKW_BASE_COMMON_SHARDS)
|
||||
message(FATAL_ERROR "Translator build graph contains no shared base shards")
|
||||
@@ -176,7 +181,9 @@ function(mkw_configure_product target)
|
||||
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_runtime_common>)
|
||||
# Startup CPU check. Must stay a separate object library so it keeps the
|
||||
# plain baseline ISA while everything around it is built for x86-64-v3.
|
||||
target_sources(${target} PRIVATE $<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
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/include"
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/src"
|
||||
@@ -192,10 +199,14 @@ function(mkw_configure_product target)
|
||||
# include the same fat translated headers; bound them by the same pool.
|
||||
mkw_bound_translated_compiles(${target})
|
||||
target_link_libraries(${target} PRIVATE
|
||||
mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
mkw_platform mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
|
||||
target_link_libraries(${target} PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
target_link_libraries(${target} PRIVATE
|
||||
"${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}")
|
||||
endif()
|
||||
if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
|
||||
include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
|
||||
aurora_copy_runtime_dlls(${target})
|
||||
@@ -210,12 +221,12 @@ function(mkw_configure_product target)
|
||||
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_link_libraries(${target} PRIVATE
|
||||
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
|
||||
|
||||
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
else()
|
||||
elseif(MKW_PLATFORM_LINUX)
|
||||
# mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o
|
||||
# files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own
|
||||
# target_link_libraries (object libraries don't carry usage requirements to a consumer
|
||||
@@ -226,7 +237,7 @@ function(mkw_configure_product target)
|
||||
# objects (empty string on glibc >= 2.34, where dl* is in libc).
|
||||
target_link_libraries(${target} PRIVATE mkw::libco ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
if(WIN32)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
foreach(runtime_dll libc++.dll libunwind.dll)
|
||||
execute_process(
|
||||
COMMAND "${CMAKE_CXX_COMPILER}" "--print-file-name=${runtime_dll}"
|
||||
@@ -302,13 +313,10 @@ else()
|
||||
message(STATUS "RetroRewind target disabled (run translate-mod and emit-build-shards)")
|
||||
endif()
|
||||
|
||||
# x86-64-v3 (SSE3/SSSE3/SSE4.1/FMA/AVX2/BMI2) is the baseline runtime/src/host_cpu_baseline.cpp
|
||||
# guards against - a fixed, portable floor since an x86_64 build may run on a different machine
|
||||
# than the one that built it. AArch64 has no such redistribution path here: every build this
|
||||
# project produces runs only on the machine that built it (local-build.sh, and the AppImage which
|
||||
# wraps it, always build from source on the target), so -mcpu=native is safe and strictly better -
|
||||
# real per-core tuning (scheduling, whatever NEON/atomic extensions that exact CPU actually has)
|
||||
# instead of the generic armv8-a baseline Clang would otherwise assume.
|
||||
# Windows and Linux x86_64 share the x86-64-v3 floor that the CPU baseline
|
||||
# object above checks. AArch64 builds are compiled locally for the host that
|
||||
# will run them, so both Linux and Apple Silicon use the compiler's native CPU
|
||||
# tuning rather than leaving target-specific performance on the table.
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3)
|
||||
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#ifndef NOMINMAX
|
||||
@@ -22,7 +23,7 @@
|
||||
// Forward declarations
|
||||
struct CpuContext;
|
||||
|
||||
// GuestFiberManager: each guest OSThread maps to a Windows Fiber. A scheduler fiber picks
|
||||
// GuestFiberManager: each guest OSThread maps to a host context. A scheduler context picks
|
||||
// which guest fiber runs; a real timer thread queues VI retraces at the VI cadence. Guest
|
||||
// threads only switch at explicit yield points (OSSleepThread, OSYieldThread, ...), matching
|
||||
// Wii cooperative semantics exactly.
|
||||
@@ -39,7 +40,7 @@ enum class ThreadState : uint32_t {
|
||||
|
||||
// Information about a guest fiber
|
||||
struct GuestFiber {
|
||||
void* fiber = nullptr; // Windows fiber handle
|
||||
void* fiber = nullptr; // Host context handle
|
||||
uint32_t entryPoint = 0; // Thread entry function
|
||||
uint32_t entryArg = 0; // Argument to entry function
|
||||
CpuContext cpuContext{}; // Saved CPU context for this fiber
|
||||
@@ -102,10 +103,6 @@ private:
|
||||
static void CALLBACK FiberProc(void* param);
|
||||
#else
|
||||
static void FiberProc(void* param);
|
||||
// libco's co_create() entry points take no argument (unlike CreateFiber's FiberProc(void*)),
|
||||
// so this trampoline reads the guest thread address staged by CreateGuestFiber() and forwards
|
||||
// into the (platform-neutral-bodied) FiberProc above. See fiber_manager.cpp.
|
||||
static void FiberProcTrampoline();
|
||||
#endif
|
||||
// Switch from whichever fiber is currently active straight to the scheduler fiber, without
|
||||
// the SwitchToThread bookkeeping (CPU context save/restore, s_currentGuestThread). Used for
|
||||
@@ -117,9 +114,8 @@ private:
|
||||
static std::mutex s_mutex;
|
||||
static std::unordered_map<uint32_t, GuestFiber> s_fibers;
|
||||
static std::vector<void*> s_fibersPendingDelete;
|
||||
// The scheduler's own "fiber": a Windows HFIBER, or (non-Windows) libco's cothread_t for
|
||||
// whichever native call stack first called GuestFiberManager::Initialize() - both are
|
||||
// plain void* handles, so one field serves both platforms.
|
||||
// The scheduler's own host context. Its opaque handle is supplied by the
|
||||
// active HostContext backend, so one field serves every supported host.
|
||||
static void* s_schedulerFiber;
|
||||
static uint32_t s_currentGuestThread;
|
||||
static bool s_initialized;
|
||||
@@ -135,4 +131,3 @@ private:
|
||||
extern std::atomic<uint32_t> g_viRetracePendingCount;
|
||||
|
||||
} // namespace Fiber
|
||||
|
||||
|
||||
@@ -14,10 +14,17 @@ namespace GuestFlat {
|
||||
// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load
|
||||
// of a global.
|
||||
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
|
||||
inline constexpr size_t kGuestPageSize = 0x1000;
|
||||
#if defined(__x86_64__)
|
||||
// 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap
|
||||
// placement.
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'1000'0000'0000ull;
|
||||
#elif defined(__aarch64__) && defined(__APPLE__)
|
||||
// Keep this well above the low address ranges that Darwin's ASLR may use for
|
||||
// a PIE executable and its shared cache. Apple Silicon's user VA is wider
|
||||
// than Linux's 39-bit minimum, so this 512 GiB region is available while the
|
||||
// Linux AArch64 target retains its 64 GiB placement below.
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0080'0000'0000ull;
|
||||
#elif defined(__aarch64__)
|
||||
// 16 TiB (this arch's x86_64 sibling value) is unreachable on any AArch64
|
||||
// kernel configured for 39-bit virtual addresses (512 GiB ceiling) - common on
|
||||
@@ -58,6 +65,25 @@ struct FaultCounters {
|
||||
// True once the reservation exists and translated code may use the flat path.
|
||||
bool IsActive();
|
||||
|
||||
// True when a host VM page covers more than one 4 KiB Wii page. In that
|
||||
// configuration, guest-view page protection cannot safely represent per-Wii-
|
||||
// page MMIO, deferred-read, or executable-write state, so general translated
|
||||
// accesses must use the checked Memory::* path.
|
||||
// Windows user mode and x86-64 always use a 4 KiB base page, so those builds
|
||||
// fold this to a compile-time false: it appears in every flat access and must
|
||||
// not become a hot-path load. Only AArch64, where the page size is a kernel
|
||||
// configuration (4/16/64 KiB), has to probe it at runtime.
|
||||
#if defined(_WIN32) || defined(__x86_64__)
|
||||
#define MKW_GUEST_FLAT_FIXED_PAGE_SIZE 1
|
||||
#endif
|
||||
|
||||
#if defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
|
||||
inline constexpr bool RequiresCheckedAccess() noexcept { return false; }
|
||||
#else
|
||||
extern bool g_requiresCheckedAccess;
|
||||
inline bool RequiresCheckedAccess() noexcept { return g_requiresCheckedAccess; }
|
||||
#endif
|
||||
|
||||
// Reserves the 4 GiB space (once per process) and maps every requested region
|
||||
// into both views. Throws std::runtime_error with a precise diagnosis when the
|
||||
// reservation, the section objects or a view cannot be created - a silent
|
||||
|
||||
@@ -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
|
||||
@@ -264,6 +264,8 @@ inline void PpcWritePairPsqInline(uint32_t addr, T first, T second)
|
||||
// reading stale bytes, and unmapped pages commit on demand, same as MemoryInline::Flat* loads.
|
||||
MKW_PPC_FORCE_INLINE const uint8_t* PpcTryGetPsqReadableHostInline(uint32_t addr)
|
||||
{
|
||||
if (GuestFlat::RequiresCheckedAccess()) [[unlikely]]
|
||||
return nullptr;
|
||||
return MKW_FLAT_GUEST_BASE + addr;
|
||||
}
|
||||
|
||||
@@ -274,6 +276,8 @@ MKW_PPC_FORCE_INLINE const uint8_t* PpcTryGetPsqReadableHostInline(uint32_t addr
|
||||
// executable, and unmapped pages still trap.
|
||||
MKW_PPC_FORCE_INLINE uint8_t* PpcTryGetPsqWritableHostInline(uint32_t addr)
|
||||
{
|
||||
if (GuestFlat::RequiresCheckedAccess()) [[unlikely]]
|
||||
return nullptr;
|
||||
if (addr > UINT32_MAX - 7u) [[unlikely]]
|
||||
return nullptr;
|
||||
if (MemoryInline::FlatWriteNeedsPolicy(addr) ||
|
||||
|
||||
@@ -231,6 +231,13 @@ MKW_MEMORY_FORCE_INLINE uint8_t* ResolveRangeHost(uint32_t base, int32_t minOffs
|
||||
(void)needsRead;
|
||||
const uint32_t guestStart = base + static_cast<uint32_t>(minOffset);
|
||||
if (length == 0 || length > kPageSize || guestStart > UINT32_MAX - (length - 1)) return nullptr;
|
||||
if (GuestFlat::RequiresCheckedAccess()) {
|
||||
// A host page can cover multiple independently-special Wii pages.
|
||||
// Returning null keeps resolved accesses on the checked Memory::*
|
||||
// path, which materializes deferred reads and applies write policy.
|
||||
(void)needsWrite;
|
||||
return nullptr;
|
||||
}
|
||||
if (needsWrite &&
|
||||
(FlatWriteNeedsPolicy(guestStart) || FlatWriteNeedsPolicy(guestStart + (length - 1))))
|
||||
[[unlikely]] return nullptr;
|
||||
@@ -522,6 +529,13 @@ MKW_MEMORY_FORCE_INLINE void WriteResolvedFloat64(uint8_t* r, uint32_t o, uint32
|
||||
// around `*(T*)(base + addr)`, no page-table load or limit check (interception model documented
|
||||
// in guest_flat_memory.h). The one exception kept inline is the MMIO write policy, since the
|
||||
// written value can't be recovered from a fault record.
|
||||
//
|
||||
// When a host VM page is larger than a 4 KiB Wii page, guest-view protections
|
||||
// cannot distinguish adjacent special Wii pages. The general FlatRead*/
|
||||
// FlatWrite* helpers then use the checked page-table path, which materializes
|
||||
// deferred reads and applies executable-write/MMIO policy before touching RAM.
|
||||
// FlatWriteRam* remains direct because the translator emits it only for
|
||||
// addresses it has proven are ordinary RAM.
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE T FlatLoad(uint32_t address) {
|
||||
@@ -536,47 +550,62 @@ MKW_MEMORY_FORCE_INLINE void FlatStore(uint32_t address, T value) {
|
||||
std::memcpy(MKW_FLAT_GUEST_BASE + address, &swapped, sizeof(T));
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) { return FlatLoad<uint8_t>(address); }
|
||||
MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) { return FlatLoad<uint16_t>(address); }
|
||||
MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) { return FlatLoad<uint32_t>(address); }
|
||||
MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read8(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) {
|
||||
const uint32_t bits = FlatLoad<uint32_t>(address);
|
||||
const uint32_t bits = FlatRead32(address);
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE double FlatReadFloat64(uint32_t address) {
|
||||
const uint64_t bits = FlatLoad<uint64_t>(address);
|
||||
const uint64_t bits = GuestFlat::RequiresCheckedAccess()
|
||||
? Memory::Read64(address) : FlatLoad<uint64_t>(address);
|
||||
double value = 0.0;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite8(uint32_t address, uint8_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write8(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write8Slow(address, value); return; }
|
||||
FlatStore<uint8_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite16(uint32_t address, uint16_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write16(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write16Slow(address, value); return; }
|
||||
FlatStore<uint16_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite32(uint32_t address, uint32_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write32(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write32Slow(address, value); return; }
|
||||
FlatStore<uint32_t>(address, value);
|
||||
}
|
||||
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat32(uint32_t address, double value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat32(address, value); return; }
|
||||
const uint32_t bits = ConvertPpcDoubleToSingleBits(value);
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat32Slow(address, value); return; }
|
||||
FlatStore<uint32_t>(address, bits);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat64(uint32_t address, double value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat64(address, value); return; }
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat64Slow(address, value); return; }
|
||||
|
||||
@@ -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
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <toml.hpp>
|
||||
#include "platform/host_platform.h"
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
@@ -141,7 +142,14 @@ inline bool IsSupportedResolutionMultiplier(float value) {
|
||||
// Must stay in step with the backend table in main.cpp, which is what actually
|
||||
// maps these to AuroraBackend.
|
||||
inline bool IsSupportedGraphicsApi(std::string_view value) {
|
||||
#if defined(__APPLE__)
|
||||
static constexpr std::array<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"};
|
||||
#endif
|
||||
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);
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
return RuntimePlatform::ExecutableDirectory();
|
||||
#else
|
||||
// /proc/self/exe is a Linux-specific magic symlink to the running executable; readlink()
|
||||
// does not NUL-terminate and silently truncates if the buffer is too small, so this grows
|
||||
@@ -229,6 +239,8 @@ inline std::filesystem::path ApplicationDataDirectory() {
|
||||
CoTaskMemFree(rawPath);
|
||||
return directory;
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
return RuntimePlatform::ApplicationDataDirectory(kApplicationDirectoryName);
|
||||
#else
|
||||
// XDG Base Directory spec equivalent of FOLDERID_LocalAppData: $XDG_DATA_HOME if set and
|
||||
// non-empty, otherwise its default of $HOME/.local/share.
|
||||
|
||||
@@ -36,6 +36,7 @@ extern thread_local uint32_t g_sehLastAccessType;
|
||||
|
||||
void WriteFatalLog(std::string_view reason);
|
||||
void SetRuntimeExitCode(int code);
|
||||
void MarkFatalErrorReported();
|
||||
|
||||
// Centralized crash reporting (defined in main.cpp). Every fatal path funnels
|
||||
// through these so the per-run log folder always receives the same artifact
|
||||
|
||||
+24
-139
@@ -2,6 +2,7 @@
|
||||
#include "memory.h"
|
||||
#include "abi_bridge.h"
|
||||
#include "hle_stubs.h"
|
||||
#include "host_context.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
// Defined in hle/os/os_sleep.cpp; the sleep-timer table is file-local there.
|
||||
@@ -13,24 +14,8 @@
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#include "libco.h"
|
||||
#endif
|
||||
|
||||
namespace Fiber {
|
||||
|
||||
#if !defined(_WIN32)
|
||||
namespace {
|
||||
// libco's co_create() entry points take no argument, unlike CreateFiber(size, FiberProc, param).
|
||||
// CreateGuestFiber() stages the guest thread address here immediately before the first co_switch
|
||||
// into a freshly created cothread; FiberProcTrampoline reads it exactly once, at the top of the
|
||||
// fiber's very first activation. Safe because guest fibers are strictly cooperative on a single
|
||||
// OS thread: nothing else can run (and so nothing else can overwrite this) between the staging
|
||||
// write and the trampoline's read of it.
|
||||
thread_local uint32_t s_pendingFiberArg = 0;
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
std::mutex GuestFiberManager::s_mutex;
|
||||
std::unordered_map<uint32_t, GuestFiber> GuestFiberManager::s_fibers;
|
||||
std::vector<void*> GuestFiberManager::s_fibersPendingDelete;
|
||||
@@ -45,21 +30,11 @@ void GuestFiberManager::PurgePendingFibers() {
|
||||
std::lock_guard<std::mutex> lock(s_mutex);
|
||||
toDelete.swap(s_fibersPendingDelete);
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
const void* current = GetCurrentFiber();
|
||||
for (void* f : toDelete) {
|
||||
if (f && f != current) {
|
||||
DeleteFiber(f);
|
||||
if (f && !HostContext::IsCurrent(f)) {
|
||||
HostContext::Destroy(f);
|
||||
}
|
||||
}
|
||||
#else
|
||||
const void* current = co_active();
|
||||
for (void* f : toDelete) {
|
||||
if (f && f != current) {
|
||||
co_delete(static_cast<cothread_t>(f));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Global VI retrace counter
|
||||
@@ -212,27 +187,13 @@ void GuestFiberManager::Initialize() {
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
// Convert the main thread to a fiber (the scheduler fiber)
|
||||
s_schedulerFiber = ConvertThreadToFiber(nullptr);
|
||||
if (!s_schedulerFiber) {
|
||||
// May already be a fiber
|
||||
s_schedulerFiber = GetCurrentFiber();
|
||||
if (!s_schedulerFiber) {
|
||||
RT_LOG(RT_TAG_OS) << "FATAL: Failed to initialize scheduler fiber!" << std::endl;
|
||||
ShowRuntimeFatalPopup("guest scheduler initialization failed",
|
||||
"Windows could not create the scheduler fiber required to run guest threads.");
|
||||
std::abort();
|
||||
}
|
||||
if (!HostContext::InitializeScheduler(&s_schedulerFiber)) {
|
||||
RT_LOG(RT_TAG_OS) << "FATAL: Failed to initialize scheduler context!" << std::endl;
|
||||
ShowRuntimeFatalPopup("guest scheduler initialization failed",
|
||||
"The host could not create the scheduler context required to run guest threads.");
|
||||
std::abort();
|
||||
}
|
||||
|
||||
#else
|
||||
// co_active() returns a handle for whichever native stack is currently running, creating one
|
||||
// on first call if needed - the libco analogue of ConvertThreadToFiber(nullptr): it converts
|
||||
// this call's own stack into a switchable target without altering control flow.
|
||||
s_schedulerFiber = co_active();
|
||||
#endif
|
||||
|
||||
s_currentGuestThread = 0;
|
||||
s_initialized = true;
|
||||
}
|
||||
@@ -240,32 +201,18 @@ void GuestFiberManager::Initialize() {
|
||||
void GuestFiberManager::Shutdown() {
|
||||
std::lock_guard<std::mutex> lock(s_mutex);
|
||||
|
||||
#if defined(_WIN32)
|
||||
for (auto& [addr, fiber] : s_fibers) {
|
||||
if (fiber.fiber && !fiber.isSchedulerFiber) {
|
||||
DeleteFiber(fiber.fiber);
|
||||
HostContext::Destroy(fiber.fiber);
|
||||
fiber.fiber = nullptr;
|
||||
}
|
||||
}
|
||||
s_fibers.clear();
|
||||
|
||||
// Convert scheduler fiber back to thread
|
||||
if (s_schedulerFiber) {
|
||||
ConvertFiberToThread();
|
||||
HostContext::ShutdownScheduler(s_schedulerFiber);
|
||||
s_schedulerFiber = nullptr;
|
||||
}
|
||||
#else
|
||||
for (auto& [addr, fiber] : s_fibers) {
|
||||
if (fiber.fiber && !fiber.isSchedulerFiber) {
|
||||
co_delete(static_cast<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;
|
||||
}
|
||||
@@ -288,11 +235,7 @@ bool GuestFiberManager::CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entr
|
||||
if (existingIt != s_fibers.end()) {
|
||||
// Delete the old fiber if it exists and is not the scheduler fiber
|
||||
if (existingIt->second.fiber && !existingIt->second.isSchedulerFiber) {
|
||||
#if defined(_WIN32)
|
||||
DeleteFiber(existingIt->second.fiber);
|
||||
#else
|
||||
co_delete(static_cast<cothread_t>(existingIt->second.fiber));
|
||||
#endif
|
||||
HostContext::Destroy(existingIt->second.fiber);
|
||||
}
|
||||
s_fibers.erase(existingIt);
|
||||
}
|
||||
@@ -312,31 +255,17 @@ bool GuestFiberManager::CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entr
|
||||
gf.cpuContext.pc = entryPoint;
|
||||
gf.cpuContext.srr0 = entryPoint;
|
||||
|
||||
#if defined(_WIN32)
|
||||
// Create Windows fiber with reasonable stack size
|
||||
// Use host stack size (64KB should be plenty for translated code)
|
||||
// The host stack models only translated host calls; the guest stack starts
|
||||
// at stackBase in the CPU context above.
|
||||
constexpr size_t kHostStackSize = 64 * 1024;
|
||||
gf.fiber = CreateFiber(kHostStackSize, FiberProc, reinterpret_cast<void*>(static_cast<uintptr_t>(guestThreadAddr)));
|
||||
gf.fiber = HostContext::Create(kHostStackSize, FiberProc,
|
||||
reinterpret_cast<void*>(static_cast<uintptr_t>(guestThreadAddr)));
|
||||
|
||||
if (!gf.fiber) {
|
||||
DWORD err = GetLastError();
|
||||
RT_LOG(RT_TAG_OS) << "CreateFiber failed for thread 0x"
|
||||
<< std::hex << guestThreadAddr
|
||||
<< " error=" << std::dec << err << std::endl;
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
// libco's co_create() entry point takes no argument; SwitchToThread() stages guestThreadAddr
|
||||
// into s_pendingFiberArg immediately before the co_switch that first activates this handle.
|
||||
constexpr unsigned int kHostStackSize = 64 * 1024;
|
||||
gf.fiber = co_create(kHostStackSize, &FiberProcTrampoline);
|
||||
|
||||
if (!gf.fiber) {
|
||||
RT_LOG(RT_TAG_OS) << "co_create failed for thread 0x"
|
||||
RT_LOG(RT_TAG_OS) << "Failed to create host context for thread 0x"
|
||||
<< std::hex << guestThreadAddr << std::dec << std::endl;
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
s_fibers[guestThreadAddr] = gf;
|
||||
|
||||
@@ -390,24 +319,11 @@ void GuestFiberManager::ExitGuestThread(uint32_t guestThreadAddr, ThreadState fi
|
||||
}
|
||||
|
||||
if (it->second.fiber && !it->second.isSchedulerFiber) {
|
||||
#if defined(_WIN32)
|
||||
const void* current = GetCurrentFiber();
|
||||
if (it->second.fiber == current) {
|
||||
if (HostContext::IsCurrent(it->second.fiber)) {
|
||||
s_fibersPendingDelete.push_back(it->second.fiber);
|
||||
} else {
|
||||
DeleteFiber(it->second.fiber);
|
||||
HostContext::Destroy(it->second.fiber);
|
||||
}
|
||||
#else
|
||||
const void* current = co_active();
|
||||
if (it->second.fiber == current) {
|
||||
// Deleting the coroutine we're currently executing on would free the very stack
|
||||
// this call is running on; defer it (PurgePendingFibers) until some other fiber is
|
||||
// active, exactly like the Windows branch above.
|
||||
s_fibersPendingDelete.push_back(it->second.fiber);
|
||||
} else {
|
||||
co_delete(static_cast<cothread_t>(it->second.fiber));
|
||||
}
|
||||
#endif
|
||||
it->second.fiber = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -474,12 +390,7 @@ void GuestFiberManager::SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu
|
||||
|
||||
// Check if we're already on the target fiber (e.g., switching to main thread
|
||||
// when we're already on the scheduler fiber)
|
||||
#if defined(_WIN32)
|
||||
void* currentFiber = GetCurrentFiber();
|
||||
#else
|
||||
void* currentFiber = co_active();
|
||||
#endif
|
||||
if (currentFiber == fiberHandle) {
|
||||
if (HostContext::IsCurrent(fiberHandle)) {
|
||||
// Already executing on the target host fiber. This is common for the
|
||||
// default guest thread, which also owns the scheduler fiber. Keep the
|
||||
// live CPU context instead of restoring a possibly stale saved copy
|
||||
@@ -496,15 +407,7 @@ void GuestFiberManager::SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu
|
||||
}
|
||||
|
||||
// Switch to the target fiber (the target fiber will load its own context)
|
||||
#if defined(_WIN32)
|
||||
SwitchToFiber(fiberHandle);
|
||||
#else
|
||||
// Staged for FiberProcTrampoline's first (and only) read; a no-op for a fiber that has
|
||||
// already started, since resuming it re-enters mid-function rather than through the
|
||||
// trampoline's entry point.
|
||||
s_pendingFiberArg = guestThreadAddr;
|
||||
co_switch(static_cast<cothread_t>(fiberHandle));
|
||||
#endif
|
||||
HostContext::Switch(fiberHandle);
|
||||
|
||||
// When we return here, the fiber that issued SwitchToThread has resumed.
|
||||
// That does not automatically mean the previous guest thread became runnable
|
||||
@@ -618,14 +521,6 @@ void GuestFiberManager::ProcessTimerEvents(CpuContext* cpu) {
|
||||
}
|
||||
}
|
||||
|
||||
void GuestFiberManager::SwitchToScheduler() {
|
||||
#if defined(_WIN32)
|
||||
SwitchToFiber(s_schedulerFiber);
|
||||
#else
|
||||
co_switch(static_cast<cothread_t>(s_schedulerFiber));
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
void CALLBACK GuestFiberManager::FiberProc(void* param)
|
||||
#else
|
||||
@@ -634,6 +529,7 @@ void GuestFiberManager::FiberProc(void* param)
|
||||
{
|
||||
uint32_t guestThreadAddr = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(param));
|
||||
|
||||
|
||||
// Get our fiber info
|
||||
GuestFiber* fiber = nullptr;
|
||||
uint32_t entryPoint = 0;
|
||||
@@ -644,7 +540,7 @@ void GuestFiberManager::FiberProc(void* param)
|
||||
auto it = s_fibers.find(guestThreadAddr);
|
||||
if (it == s_fibers.end()) {
|
||||
RT_LOG(RT_TAG_OS) << "FiberProc: fiber not found!" << std::endl;
|
||||
SwitchToScheduler();
|
||||
HostContext::Switch(s_schedulerFiber);
|
||||
return;
|
||||
}
|
||||
fiber = &it->second;
|
||||
@@ -707,7 +603,7 @@ void GuestFiberManager::FiberProc(void* param)
|
||||
<< ", fn=0x" << startFn << ") after retries; continuing anyway." << std::dec << std::endl;
|
||||
break;
|
||||
}
|
||||
SwitchToScheduler();
|
||||
HostContext::Switch(s_schedulerFiber);
|
||||
}
|
||||
|
||||
// The deferral loop above yields to the scheduler and therefore can resume
|
||||
@@ -764,18 +660,7 @@ void GuestFiberManager::FiberProc(void* param)
|
||||
}
|
||||
|
||||
// Return to scheduler
|
||||
SwitchToScheduler();
|
||||
HostContext::Switch(s_schedulerFiber);
|
||||
}
|
||||
|
||||
#if !defined(_WIN32)
|
||||
void GuestFiberManager::FiberProcTrampoline() {
|
||||
const uint32_t guestThreadAddr = s_pendingFiberArg;
|
||||
FiberProc(reinterpret_cast<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
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
#endif
|
||||
|
||||
namespace GuestFlat {
|
||||
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
|
||||
bool g_requiresCheckedAccess = false;
|
||||
#endif
|
||||
namespace {
|
||||
|
||||
#if defined(_WIN32)
|
||||
@@ -48,6 +51,16 @@ constexpr DWORD kMemPreservePlaceholder = 0x00000002;
|
||||
constexpr size_t kAllocationGranularity = 0x10000; // 64 KiB
|
||||
constexpr size_t kHostPageSize = 0x1000;
|
||||
|
||||
// Only hosts that can expose a page larger than 4 KiB need to discover their
|
||||
// size at runtime; see RequiresCheckedAccess() in guest_flat_memory.h.
|
||||
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
|
||||
size_t HostPageSize()
|
||||
{
|
||||
const long size = sysconf(_SC_PAGESIZE);
|
||||
return size > 0 ? static_cast<size_t>(size) : kGuestPageSize;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Named, platform-neutral protection modes so every fault-interception call site below (the
|
||||
// MMIO window, the executable-write guard, deferred-EFB-read protection, the on-demand
|
||||
// unmapped-block commit) can stay identical text on both platforms; only ProtectRange() and
|
||||
@@ -349,7 +362,7 @@ bool IsMmio(uint32_t address) { return MemoryInline::IsMmioAddress(address); }
|
||||
bool IsGpuFifo(uint32_t address) { return MemoryInline::IsGpuFifoAddress(address); }
|
||||
|
||||
void ApplyExecutableProtectionLocked() {
|
||||
if (g_base == nullptr) return;
|
||||
if (g_base == nullptr || RequiresCheckedAccess()) return;
|
||||
auto& protectedPages = ExecutableProtectedPages();
|
||||
for (const auto& range : ExecutableRanges()) {
|
||||
// Only pages fully inside the range are protected: edge pages often share a page with data
|
||||
@@ -497,6 +510,10 @@ bool IsActive() {
|
||||
void Initialize(const std::vector<RegionRequest>& regions) {
|
||||
std::lock_guard<std::mutex> lock(StateMutex());
|
||||
|
||||
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
|
||||
g_requiresCheckedAccess = HostPageSize() > kGuestPageSize;
|
||||
#endif
|
||||
|
||||
if (g_initialized) {
|
||||
if (!SameLayout(g_activeRegions, regions)) {
|
||||
throw std::runtime_error(
|
||||
@@ -615,7 +632,7 @@ uint8_t* HostPointer(uint32_t guestAddress) {
|
||||
}
|
||||
|
||||
void ProtectDeferredRange(uint32_t address, size_t length) {
|
||||
if (!g_initialized || length == 0) return;
|
||||
if (RequiresCheckedAccess() || !g_initialized || length == 0) return;
|
||||
const uint64_t end = static_cast<uint64_t>(address) + length;
|
||||
if (end > kGuestSpaceSize) return;
|
||||
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) {
|
||||
if (!g_initialized || length == 0) return;
|
||||
if (RequiresCheckedAccess() || !g_initialized || length == 0) return;
|
||||
std::lock_guard<std::mutex> lock(StateMutex());
|
||||
auto& ranges = DeferredRanges();
|
||||
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) {
|
||||
if (end <= start) return;
|
||||
if (RequiresCheckedAccess() || end <= start) return;
|
||||
std::lock_guard<std::mutex> lock(StateMutex());
|
||||
auto& ranges = ExecutableRanges();
|
||||
if (std::any_of(ranges.begin(), ranges.end(), [&](const GuardedRange& range) {
|
||||
|
||||
@@ -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
|
||||
@@ -394,3 +394,4 @@ extern std::mutex g_tlutObjMutex;
|
||||
// that happens on another key: unordered_map keeps references valid across a
|
||||
// rehash, an open-addressed table would not.
|
||||
extern std::unordered_map<uint32_t, TexObjSlot> g_TexObjMeta;
|
||||
extern std::map<uint32_t, TlutObjMeta> g_TlutObjMeta;
|
||||
|
||||
@@ -33,6 +33,15 @@ void WriteGuestFloat(uint32_t addr, float value, const char* label) {
|
||||
|
||||
void* GuestToHostPtr(uint32_t addr, size_t len) {
|
||||
if (addr == 0) return nullptr;
|
||||
#if defined(__APPLE__)
|
||||
// The macOS flat guest map exposes separate host aliases for cached,
|
||||
// uncached, and physical MEM1/MEM2 addresses. GX resources are identified
|
||||
// by their host pointer, so all aliases of one guest allocation must use
|
||||
// the same physical mapping before they reach Aurora. Kept macOS-only:
|
||||
// changing which alias the other hosts hand out would re-key their existing
|
||||
// GX resource identity.
|
||||
addr = CanonicalizeGxMainRamAddress(addr);
|
||||
#endif
|
||||
try { return Memory::GetPointer(addr, len); } catch (const Memory::AccessViolation& e) { LogMemoryError(RT_TAG_GX, "GX guest pointer", e); return nullptr; }
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "aurora_events.h"
|
||||
#include "settings_overlay.h"
|
||||
#include "fiber_manager.h"
|
||||
#include "platform/host_platform.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
#include <dolphin/vi.h>
|
||||
@@ -20,18 +21,15 @@
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include <aurora/aurora.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
#include <aurora/aurora.h>
|
||||
|
||||
// Forward declaration for OSWakeupThread - used to wake threads on VI retrace queue
|
||||
extern "C" void OSWakeupThread_HLE_801aaaa4(CpuContext* ctx);
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -23,6 +23,10 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32)
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
@@ -38,7 +42,12 @@
|
||||
#include <dbghelp.h>
|
||||
#else
|
||||
#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>
|
||||
#endif
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
@@ -1370,9 +1379,21 @@ int RuntimeMain(int argc, char** argv) {
|
||||
const char* configName;
|
||||
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{{
|
||||
{"auto", BACKEND_AUTO}, {"d3d12", BACKEND_D3D12}, {"vulkan", BACKEND_VULKAN},
|
||||
}};
|
||||
|
||||
#endif
|
||||
const auto backendDisplayName = [](AuroraBackend value) -> const char* {
|
||||
for (const auto& entry : kGraphicsBackends) {
|
||||
if (entry.backend == value) {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -166,6 +166,7 @@ return command switch
|
||||
"emit-build-shards" => RunEmitBuildShards(tail),
|
||||
"emit-base-manifest" => RunEmitBaseManifest(tail),
|
||||
"check-base-mod-awareness" => RunCheckBaseModAwareness(tail),
|
||||
"validate-retro-wfc-payload" => RunValidateRetroWfcPayload(tail),
|
||||
_ => ShowHelp(command)
|
||||
};
|
||||
|
||||
@@ -354,6 +355,33 @@ int RunInfo()
|
||||
return 0;
|
||||
}
|
||||
|
||||
int RunValidateRetroWfcPayload(string[] argsTail)
|
||||
{
|
||||
var directory = OptionValue(argsTail, "--directory");
|
||||
if (string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
Console.Error.WriteLine("--directory is required.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
WiiCompiled.Setup.Common.RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(directory);
|
||||
Console.WriteLine("[translator] Retro WFC payload signature validated.");
|
||||
return 0;
|
||||
}
|
||||
catch (InvalidDataException ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[translator] Retro WFC payload validation failed: {ex.Message}");
|
||||
return 2;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
Console.Error.WriteLine($"[translator] Could not read Retro WFC payload: {ex.Message}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
int RunTranslateRecursive(string[] argsTail)
|
||||
{
|
||||
if (argsTail.Length == 0 || argsTail[0].StartsWith("--", StringComparison.Ordinal))
|
||||
@@ -3639,7 +3667,8 @@ static string[] KnownCommands() => new[]
|
||||
"translate-mod",
|
||||
"emit-base-manifest",
|
||||
"emit-build-shards",
|
||||
"check-base-mod-awareness"
|
||||
"check-base-mod-awareness",
|
||||
"validate-retro-wfc-payload"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -3682,6 +3711,10 @@ static (string? Positional, CommandOption[] Options)? CommandSpec(string command
|
||||
new("--translation-output-metadata", "path"),
|
||||
new("--code-pul", "path")
|
||||
}),
|
||||
"validate-retro-wfc-payload" => (null, new CommandOption[]
|
||||
{
|
||||
new("--directory", "directory", Required: true)
|
||||
}),
|
||||
"emit-base-manifest" => (null, new CommandOption[]
|
||||
{
|
||||
new("--out", "path"),
|
||||
@@ -4061,4 +4094,3 @@ sealed record ResolvedDispatchEntry(
|
||||
bool MustRemainDynamicallyDispatchable,
|
||||
string SourceFile);
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Translator.Core\Translator.Core.csproj" />
|
||||
<ProjectReference Include="..\..\..\Launcher\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -22,14 +22,15 @@ internal static class AssemblyBlobWriter
|
||||
var expectedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var assembly = new StringBuilder();
|
||||
foreach (var header in headerLines) assembly.AppendLine(header);
|
||||
// PE/COFF (Windows) and ELF (Linux) spell a read-only data section differently in GNU-as
|
||||
// syntax - COFF section flags ("dr" = data, read-only) versus an ELF section needing an
|
||||
// allocatable-only flag plus an explicit @progbits type. The build always targets
|
||||
// PE/COFF (Windows), Mach-O (macOS), and ELF (Linux) spell a read-only data section
|
||||
// differently in GNU-as syntax. The build always targets
|
||||
// whichever platform the translator itself runs on (there is no cross-compilation
|
||||
// support), so that's what this picks the section syntax from.
|
||||
assembly.AppendLine(OperatingSystem.IsWindows()
|
||||
? ".section .rdata,\"dr\""
|
||||
: ".section .rodata,\"a\",@progbits");
|
||||
: OperatingSystem.IsMacOS()
|
||||
? ".section __TEXT,__const"
|
||||
: ".section .rodata,\"a\",@progbits");
|
||||
assembly.AppendLine();
|
||||
|
||||
foreach (var blob in blobs)
|
||||
@@ -40,8 +41,12 @@ internal static class AssemblyBlobWriter
|
||||
var hash = ChecksumUtilities.Sha256Hex(blob.Data.Span);
|
||||
assembly.AppendLine($"// {blob.Comment}; sha256={hash}");
|
||||
assembly.AppendLine(".p2align 4");
|
||||
assembly.AppendLine($".globl {blob.Symbol}");
|
||||
assembly.AppendLine($"{blob.Symbol}:");
|
||||
// C/C++ external symbols carry a leading underscore in Mach-O,
|
||||
// unlike ELF and COFF. The generated C++ still names the symbol
|
||||
// without that ABI decoration, so emit the platform spelling here.
|
||||
var assemblySymbol = OperatingSystem.IsMacOS() ? $"_{blob.Symbol}" : blob.Symbol;
|
||||
assembly.AppendLine($".globl {assemblySymbol}");
|
||||
assembly.AppendLine($"{assemblySymbol}:");
|
||||
var referencePath = Path.Combine(blobReferenceDirectory, blob.FileName);
|
||||
assembly.AppendLine($".incbin \"{SanitizeAssemblyPath(referencePath)}\"");
|
||||
assembly.AppendLine();
|
||||
@@ -51,7 +56,7 @@ internal static class AssemblyBlobWriter
|
||||
{
|
||||
if (!expectedFiles.Contains(Path.GetFullPath(stalePath))) File.Delete(stalePath);
|
||||
}
|
||||
if (!OperatingSystem.IsWindows())
|
||||
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
|
||||
{
|
||||
// Absence of a .note.GNU-stack section makes the linker assume the oldest, most
|
||||
// conservative default for this object (an executable stack) and warn about it; this
|
||||
|
||||
@@ -28,8 +28,10 @@ public sealed class ModDataPatchWriterTests
|
||||
|
||||
var assemblyPath = Path.Combine(root, "cpp", "mod_data_patches_blobs.S");
|
||||
var assembly = File.ReadAllText(assemblyPath);
|
||||
Assert.Contains(".globl kModuleImage", assembly, StringComparison.Ordinal);
|
||||
Assert.Contains(".globl kKamekCodeSha1Digest", assembly, StringComparison.Ordinal);
|
||||
var symbolPrefix = OperatingSystem.IsMacOS() ? "_" : "";
|
||||
Assert.Contains($".globl {symbolPrefix}kModuleImage", assembly, StringComparison.Ordinal);
|
||||
Assert.Contains($".globl {symbolPrefix}kKamekCodeSha1Digest", assembly, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("__APPLE__", assembly, StringComparison.Ordinal);
|
||||
Assert.Contains(".incbin", assembly, StringComparison.Ordinal);
|
||||
|
||||
var timestamps = new[] { cppPath, assemblyPath }
|
||||
|
||||
Reference in New Issue
Block a user