Compare commits

...

3 Commits

Author SHA1 Message Date
Michael G 6fa24737d3 feat(macos): back guest flat memory with anonymous vm_remap aliases (#252)
Replace /tmp unlinked temporary files and file-backed mmap with anonymous host pages and Mach mach_vm_remap shared aliases, eliminating disk I/O, APFS churn, and SSD write amplification on macOS.

Adapted from KartPad (chrissotraidis/kartpad).
2026-10-05 23:47:14 +02:00
theofficialgman 279ce8328f Macos support into main (#228)
* macos: add x86_64 Intel support

* (feat) Apple Silicon macOS CI building and Setup.pkg documentation (#118)

* docs(macos): document Setup.pkg installation

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

* macos: pin Apple Silicon deployment target

* fix(macos): restore Retro Rewind local builds

* macos: package universal setup tools

* ci: build macOS input expression tests

* runtime: Do not force 14.0 minimum anymore

new minimum is 12.0

* macos: support older libc++ algorithms

* macos: allow undefined MTLLogStateDescriptor for older SDKs

* tests: deflake input expression timing window test

* build: Fix mac tests

* translator: emit null statement after continuation labels for C++17 compatibility

* feat(macos): add MetalFX spatial upscaling

* Fix automatic music muting on macOS

Co-Authored-By: Michael G <10155689+DarthMDev@users.noreply.github.com>
Co-Authored-By: Daan Vervacke <23398694+DaanVervacke@users.noreply.github.com>

* Address CodeRabbit review comments and integrate upstream TLS cmake

* Remove version requirement for running package CI

Co-Authored-By: Michael G <10155689+DarthMDev@users.noreply.github.com>

* publish-app: fix dependency discovery from build dir, handle spaces in paths, and fail on missing deps

* publish-app: preserve subdirectory suffix during @rpath dependency lookup

* cmake: synchronize CMAKE_SYSTEM_PROCESSOR with CMAKE_OSX_ARCHITECTURES on macOS

* fix rpaths

---------

Co-authored-by: DarthM <mgracer48@yahoo.com>
Co-authored-by: Michael G <10155689+DarthMDev@users.noreply.github.com>
Co-authored-by: Daan Vervacke <23398694+DaanVervacke@users.noreply.github.com>
Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
2026-10-03 11:07:15 +02:00
patchzyy 9d182f8316 Split PSQ fallbacks and add PSQ ISA tests (#278) 2026-10-02 10:34:51 +02:00
52 changed files with 2841 additions and 290 deletions
+41
View File
@@ -46,3 +46,44 @@ jobs:
- name: Test - name: Test
run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal
macos_substrate:
name: macOS arm64 (configure + substrate tests)
runs-on: macos-14
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Test macOS app dependency packaging
shell: bash
run: bash Launcher/macos/test-publish-app.command
- name: Configure native runtime
shell: bash
run: |
test "$(uname -m)" = arm64
cmake -S runtime -B build-macos -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DMKW_BUILD_PRODUCTS=OFF
grep -qx 'CMAKE_OSX_DEPLOYMENT_TARGET:STRING=12.0' \
build-macos/CMakeCache.txt
- name: Build macOS portability targets
shell: bash
run: |
cmake --build build-macos --target \
mkw_platform_paths_tests \
mkw_runtime_config_tests \
mkw_nand_save_tests \
mkw_nand_settings_tests \
mkw_sc_serial_tests \
mkw_input_expr_tests \
mkw_macos_native_compile \
mkw_macos_context_abi_tests \
mkw_macos_host_context_tests \
mkw_macos_guest_flat_memory_tests \
mkw_macos_external_audio_tests
- name: Test execution substrate
shell: bash
run: ctest --test-dir build-macos --output-on-failure
+128 -6
View File
@@ -1,8 +1,8 @@
name: Package installers name: Package installers
# Builds the per-platform installer/setup tool (WiiCompiled-Setup.exe / # Builds the per-platform installer/setup tool (WiiCompiled-Setup.exe /
# WiiCompiled-Setup-x86_64.AppImage) via Launcher/Build-Installer.ps1 and # WiiCompiled-Setup-x86_64.AppImage / WiiCompiled-Setup.pkg) via the platform packaging scripts -
# Launcher/build-appimage.sh respectively - the same scripts a maintainer runs by hand today to # the same scripts a maintainer runs by hand today to
# produce a GitHub Release asset. This does NOT build the actual translated game executable: # produce a GitHub Release asset. This does NOT build the actual translated game executable:
# that step requires the end user's own Mario Kart Wii dump (Assets/main.dol, Assets/StaticR.rel), # that step requires the end user's own Mario Kart Wii dump (Assets/main.dol, Assets/StaticR.rel),
# which is proprietary and not present in this repository or in CI. # which is proprietary and not present in this repository or in CI.
@@ -90,14 +90,136 @@ jobs:
if-no-files-found: error if-no-files-found: error
archive: false archive: false
macos-setup-package:
name: macOS (universal Setup.pkg)
runs-on: macos-14
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '8.0.x'
- name: Verify Apple Silicon runner
shell: bash
run: |
test "$(uname -m)" = arm64
xcode-select -p
- name: Download pinned Nod tools
shell: bash
run: |
mkdir -p Launcher/artifacts/macos
nodtool_version=v2.0.0-alpha.10
nodtool_arm64_asset=nodtool-macos-arm64
nodtool_arm64_sha256=e23ca466999b720c55e6d29c9683fce8cc74451ba64ead2e543d50129f24528a
nodtool_x86_64_asset=nodtool-macos-x86_64
nodtool_x86_64_sha256=f68f504dc2b72694b468ca78b6a24142c7aa5c8800f77564297f4143682e6575
curl -fsSL --retry 3 \
"https://github.com/encounter/nod/releases/download/${nodtool_version}/${nodtool_arm64_asset}" \
-o Launcher/artifacts/macos/nodtool-arm64
curl -fsSL --retry 3 \
"https://github.com/encounter/nod/releases/download/${nodtool_version}/${nodtool_x86_64_asset}" \
-o Launcher/artifacts/macos/nodtool-x86_64
printf '%s %s\n' "$nodtool_arm64_sha256" Launcher/artifacts/macos/nodtool-arm64 | shasum -a 256 -c -
printf '%s %s\n' "$nodtool_x86_64_sha256" Launcher/artifacts/macos/nodtool-x86_64 | shasum -a 256 -c -
chmod +x Launcher/artifacts/macos/nodtool-arm64 Launcher/artifacts/macos/nodtool-x86_64
- name: Publish self-contained Translator tools
shell: bash
run: |
dotnet publish translator/src/Translator.Cli/Translator.Cli.csproj \
-c Release -r osx-arm64 --self-contained true \
-p:PublishSingleFile=true \
-o Launcher/artifacts/macos/translator-arm64
dotnet publish translator/src/Translator.Cli/Translator.Cli.csproj \
-c Release -r osx-x64 --self-contained true \
-p:PublishSingleFile=true \
-o Launcher/artifacts/macos/translator-x86_64
- name: Download pinned universal Ninja
shell: bash
run: |
ninja_version=1.13.2
ninja_sha256=c99048673aa765960a99cf10c6ddb9f1fad506099ff0a0e137ad8960a88f321b
curl -fsSL --retry 3 "https://github.com/ninja-build/ninja/releases/download/v${ninja_version}/ninja-mac.zip" -o ninja-mac.zip
printf '%s %s\n' "$ninja_sha256" ninja-mac.zip | shasum -a 256 -c -
unzip -q ninja-mac.zip -d Launcher/artifacts/macos/ninja
chmod +x Launcher/artifacts/macos/ninja/ninja
- name: Download pinned portable CMake
shell: bash
run: |
cmake_version=4.4.3
archive="cmake-${cmake_version}-macos-universal.tar.gz"
base_url="https://github.com/Kitware/CMake/releases/download/v${cmake_version}"
expected_sha256=0c5d65251c14cc884bfa16bdbed3c263ce5bffe2e21c0d0d00962cb0610464fa
curl -fsSL --retry 3 "$base_url/$archive" -o "$archive"
printf '%s %s\n' "$expected_sha256" "$archive" | shasum -a 256 -c -
tar -xzf "$archive"
mv "cmake-${cmake_version}-macos-universal/CMake.app/Contents" Launcher/artifacts/macos/cmake
- name: Build Setup.pkg
env:
TAG_VERSION: ${{ github.ref_name }}
shell: bash
run: |
package_version=""
if [[ "${TAG_VERSION:-}" =~ ^v?[0-9] ]]; then
package_version="${TAG_VERSION#v}"
elif [[ -f "Launcher/Directory.Build.props" ]]; then
package_version=$(grep -m1 '<Version>' Launcher/Directory.Build.props | sed -E 's/.*<Version>([^<]+)<\/Version>.*/\1/')
fi
mkdir -p Launcher/dist
Launcher/macos/build-setup-pkg.command \
--nodtool-arm64 Launcher/artifacts/macos/nodtool-arm64 \
--nodtool-x86_64 Launcher/artifacts/macos/nodtool-x86_64 \
--translator-arm64 Launcher/artifacts/macos/translator-arm64/Translator.Cli \
--translator-x86_64 Launcher/artifacts/macos/translator-x86_64/Translator.Cli \
--cmake-root Launcher/artifacts/macos/cmake \
--ninja-arm64 Launcher/artifacts/macos/ninja/ninja \
--ninja-x86_64 Launcher/artifacts/macos/ninja/ninja \
--output Launcher/dist/WiiCompiled-Setup.pkg \
${package_version:+--version "$package_version"}
- name: Verify package layout and architecture-specific tools
shell: bash
run: |
pkgutil --check-signature Launcher/dist/WiiCompiled-Setup.pkg || true
if pkgutil --payload-files Launcher/dist/WiiCompiled-Setup.pkg | \
grep -E '/(Assets|generated|PulsarPacks|WiiCompiled.app|RetroRewind.app)(/|$)'; then
echo "::error::Setup.pkg contains a forbidden payload"
exit 1
fi
expanded="$RUNNER_TEMP/wiicompiled-setup-expanded"
pkgutil --expand-full Launcher/dist/WiiCompiled-Setup.pkg "$expanded"
resources="$expanded/Payload/Applications/WiiCompiled Setup.app/Contents/Resources"
for arch in arm64 x86_64; do
for tool in nodtool Translator.Cli ninja; do
lipo "$resources/tools/$arch/$tool" -verify_arch "$arch"
done
done
lipo "$resources/tools/cmake/bin/cmake" -verify_arch arm64 x86_64
bash "$resources/setup.command" --help
/usr/bin/arch -x86_64 /bin/bash "$resources/setup.command" --help
- uses: actions/upload-artifact@v7
with:
name: WiiCompiled-Setup-macos-universal
path: Launcher/dist/WiiCompiled-Setup.pkg
if-no-files-found: error
archive: false
# Publishes the packaged installers as a GitHub Release whenever a v* tag is pushed. Wheel Wizard # Publishes the packaged installers as a GitHub Release whenever a v* tag is pushed. Wheel Wizard
# discovers updates from these releases, so the contract it relies on is enforced here: a full # discovers updates from these releases, so the contract it relies on is enforced here: a full
# (non-prerelease) release whose tag is v<semver>, carrying an asset named exactly # (non-prerelease) release whose tag is v<semver>, carrying the expected platform assets,
# WiiCompiled-Setup.exe, produced by a setup host that reports that same version. # produced by setup hosts that report that same version.
release: release:
name: Publish GitHub Release name: Publish GitHub Release
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
needs: [linux-appimage, windows-installer, recompilation] needs: [linux-appimage, windows-installer, macos-setup-package, recompilation]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
@@ -141,7 +263,7 @@ jobs:
set -euo pipefail set -euo pipefail
ls -lR artifacts ls -lR artifacts
assets=() assets=()
for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage; do for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage WiiCompiled-Setup.pkg; do
found="$(find artifacts -type f -name "$name" | head -n 1)" found="$(find artifacts -type f -name "$name" | head -n 1)"
[ -n "$found" ] && [ -s "$found" ] || { echo "::error::missing release asset $name"; exit 1; } [ -n "$found" ] && [ -s "$found" ] || { echo "::error::missing release asset $name"; exit 1; }
assets+=("$found") assets+=("$found")
+11 -6
View File
@@ -22,7 +22,7 @@ Usage: local-build-macos.command --output-dir DIR [options]
--retro-rewind-package-dir DIR RetroRewind6 directory (required for Retro Rewind) --retro-rewind-package-dir DIR RetroRewind6 directory (required for Retro Rewind)
--retro-wfc-offline-dir DIR Directory containing binary/payload.RMCPD00.bin --retro-wfc-offline-dir DIR Directory containing binary/payload.RMCPD00.bin
--skip-retro-wfc-payload Build Retro Rewind without the shared Retro-WFC payload --skip-retro-wfc-payload Build Retro Rewind without the shared Retro-WFC payload
--force-clean-build Delete local generated and native-build-macos caches --force-clean-build Delete local generated and current-architecture native build caches
--parallel N Pin translation and build parallelism --parallel N Pin translation and build parallelism
--cmake PATH --ninja PATH Override build tools --cmake PATH --ninja PATH Override build tools
--dotnet PATH Override dotnet --dotnet PATH Override dotnet
@@ -56,7 +56,9 @@ while (($#)); do
done done
[[ $(uname -s) == Darwin ]] || fail 'this build script is for macOS only' [[ $(uname -s) == Darwin ]] || fail 'this build script is for macOS only'
[[ $(uname -m) == arm64 ]] || fail 'the current macOS product target is Apple Silicon only' macos_arch=$(uname -m)
case "$macos_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $macos_arch" ;; esac
macos_deployment_target=12.0
workspace=$(cd "$workspace" && pwd) workspace=$(cd "$workspace" && pwd)
[[ -n "$output_dir" ]] || fail '--output-dir is required' [[ -n "$output_dir" ]] || fail '--output-dir is required'
case "$profile" in base|retro-rewind|both) ;; *) fail '--profile must be base, retro-rewind, or both' ;; esac case "$profile" in base|retro-rewind|both) ;; *) fail '--profile must be base, retro-rewind, or both' ;; esac
@@ -73,7 +75,8 @@ for tool in "$cmake_bin" "$ninja_bin" clang clang++ shasum; do command -v "$tool
project="$workspace/projects/mkwii/recomp.yml"; assets="$workspace/Assets"; generated="$workspace/generated" project="$workspace/projects/mkwii/recomp.yml"; assets="$workspace/Assets"; generated="$workspace/generated"
functions="$generated/functions"; metadata="$generated/base_translation_output.json"; manifest_dir="$workspace/build/base" functions="$generated/functions"; metadata="$generated/base_translation_output.json"; manifest_dir="$workspace/build/base"
manifest="$manifest_dir/mkwii_base_manifest.json"; shards="$generated/build_shards"; native_build="$workspace/native-build-macos" manifest="$manifest_dir/mkwii_base_manifest.json"; shards="$generated/build_shards"
native_build="$workspace/native-build-macos-$macos_arch"
assert_file "$project" 'translation project' assert_file "$project" 'translation project'
if [[ -n "$game" ]]; then "$script_dir/macos/extract-disc.command" --game "$game" --assets-dir "$assets" --nodtool "$nodtool"; fi if [[ -n "$game" ]]; then "$script_dir/macos/extract-disc.command" --game "$game" --assets-dir "$assets" --nodtool "$nodtool"; fi
assert_file "$assets/main.dol" 'extracted main.dol'; assert_file "$assets/StaticR.rel" 'extracted StaticR.rel' assert_file "$assets/main.dol" 'extracted main.dol'; assert_file "$assets/StaticR.rel" 'extracted StaticR.rel'
@@ -131,9 +134,11 @@ if (( builds_retro )); then args+=(--resolved-profile "$mod_out/resolved_dispatc
step emit-build-shards 'Preparing native build shards'; translator "${args[@]}" step emit-build-shards 'Preparing native build shards'; translator "${args[@]}"
step configure-native 'Configuring the native toolchain' step configure-native 'Configuring the native toolchain'
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs" # Use Aurora's pinned SDL3 source on macOS. A system SDL3 can be older than
# Aurora's required API even when find_package() succeeds.
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DCMAKE_OSX_ARCHITECTURES="$macos_arch" -DCMAKE_OSX_DEPLOYMENT_TARGET="$macos_deployment_target" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs" -DAURORA_SDL3_PROVIDER=vendor
targets=(); [[ "$profile" != retro-rewind ]] && targets+=(WiiCompiled); [[ "$profile" != base ]] && targets+=(RetroRewind) targets=(); [[ "$profile" != retro-rewind ]] && targets+=(WiiCompiled); [[ "$profile" != base ]] && targets+=(RetroRewind)
step compile "Compiling ${targets[*]} locally"; "$cmake_bin" --build "$native_build" --target "${targets[@]}" --parallel "$global_jobs" step compile "Compiling ${targets[*]} locally"; "$cmake_bin" --build "$native_build" --target "${targets[@]}" --parallel "$global_jobs"
if [[ "$profile" != retro-rewind ]]; then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product WiiCompiled --output-dir "${base_output_dir:-$output_dir}"; fi if [[ "$profile" != retro-rewind ]]; then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product WiiCompiled --output-dir "${base_output_dir:-$output_dir}" --architecture "$macos_arch" --minimum-system-version "$macos_deployment_target"; fi
if (( builds_retro )); then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product RetroRewind --output-dir "$output_dir"; fi if (( builds_retro )); then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product RetroRewind --output-dir "$output_dir" --architecture "$macos_arch" --minimum-system-version "$macos_deployment_target"; fi
printf 'MKWCBUILD:OUTPUT=%s\n' "$output_dir" printf 'MKWCBUILD:OUTPUT=%s\n' "$output_dir"
+75 -18
View File
@@ -9,12 +9,15 @@ fail() { printf 'build-setup-pkg.command: error: %s\n' "$*" >&2; exit 1; }
copy_clean() { DITTONORSRC=1 ditto --norsrc --noqtn "$@"; } copy_clean() { DITTONORSRC=1 ditto --norsrc --noqtn "$@"; }
usage() { usage() {
cat <<'EOF' cat <<'EOF'
Usage: build-setup-pkg.command --nodtool PATH --translator PATH --cmake-root DIR --ninja PATH --output PKG [options] Usage: build-setup-pkg.command --nodtool-arm64 PATH --nodtool-x86_64 PATH --translator-arm64 PATH --translator-x86_64 PATH --cmake-root DIR --ninja-arm64 PATH --ninja-x86_64 PATH --output PKG [options]
Creates a game-code-free WiiCompiled Setup.pkg. The supplied tools must be Creates a game-code-free WiiCompiled Setup.pkg. The supplied tools must be
maintainer-verified, redistributable macOS arm64 artifacts. The resulting pkg maintainer-verified, redistributable macOS artifacts for both arm64 and
is unsigned unless --installer-identity is supplied; releases should sign and x86_64. The setup package selects native tools for its host while the game is
notarize it with a Developer ID Installer certificate. compiled locally for that host architecture. CMake must be universal2. The
resulting pkg is unsigned unless
--installer-identity is supplied; releases should sign and notarize it with a
Developer ID Installer certificate.
--workspace DIR Repository root (default: script's grandparent) --workspace DIR Repository root (default: script's grandparent)
--version VERSION Bundle/package version (default: 0.1.0) --version VERSION Bundle/package version (default: 0.1.0)
@@ -23,14 +26,17 @@ EOF
} }
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace=$(cd "$script_dir/../.." && pwd); nodtool=""; translator=""; cmake_root=""; ninja=""; output=""; version=0.1.0; identity="" workspace=$(cd "$script_dir/../.." && pwd); nodtool_arm64=""; nodtool_x86_64=""; translator_arm64=""; translator_x86_64=""; cmake_root=""; ninja_arm64=""; ninja_x86_64=""; output=""; version=0.1.0; identity=""
while (($#)); do while (($#)); do
case "$1" in case "$1" in
--workspace) workspace=${2:-}; shift 2 ;; --workspace) workspace=${2:-}; shift 2 ;;
--nodtool) nodtool=${2:-}; shift 2 ;; --nodtool-arm64) nodtool_arm64=${2:-}; shift 2 ;;
--translator) translator=${2:-}; shift 2 ;; --nodtool-x86_64) nodtool_x86_64=${2:-}; shift 2 ;;
--translator-arm64) translator_arm64=${2:-}; shift 2 ;;
--translator-x86_64) translator_x86_64=${2:-}; shift 2 ;;
--cmake-root) cmake_root=${2:-}; shift 2 ;; --cmake-root) cmake_root=${2:-}; shift 2 ;;
--ninja) ninja=${2:-}; shift 2 ;; --ninja-arm64) ninja_arm64=${2:-}; shift 2 ;;
--ninja-x86_64) ninja_x86_64=${2:-}; shift 2 ;;
--output) output=${2:-}; shift 2 ;; --output) output=${2:-}; shift 2 ;;
--version) version=${2:-}; shift 2 ;; --version) version=${2:-}; shift 2 ;;
--installer-identity) identity=${2:-}; shift 2 ;; --installer-identity) identity=${2:-}; shift 2 ;;
@@ -39,15 +45,60 @@ while (($#)); do
esac esac
done done
version=${version#v} version=${version#v}
if [[ -z "$version" || "$version" == "0.1.0" ]]; then
local_csproj="$workspace/Launcher/Directory.Build.props"
if [[ -f "$local_csproj" ]]; then
detected=$(grep -m1 '<Version>' "$local_csproj" | sed -E 's/.*<Version>([^<]+)<\/Version>.*/\1/' || true)
if [[ -n "$detected" ]]; then
version="$detected"
fi
fi
fi
[[ "$version" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]] || fail '--version must contain one to three period-separated integers' [[ "$version" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]] || fail '--version must contain one to three period-separated integers'
IFS=. read -r version_major version_minor version_patch <<< "$version" IFS=. read -r version_major version_minor version_patch <<< "$version"
short_version="$version_major.${version_minor:-0}.${version_patch:-0}" short_version="$version_major.${version_minor:-0}.${version_patch:-0}"
for tool in pkgbuild productbuild ditto codesign; do command -v "$tool" >/dev/null || fail "required macOS tool unavailable: $tool"; done for tool in pkgbuild productbuild ditto codesign lipo; do command -v "$tool" >/dev/null || fail "required macOS tool unavailable: $tool"; done
[[ -x "$nodtool" ]] || fail '--nodtool must name an executable' for tool_path in "$nodtool_arm64" "$nodtool_x86_64" "$translator_arm64" "$translator_x86_64" "$ninja_arm64" "$ninja_x86_64"; do [[ -x "$tool_path" ]] || fail 'each architecture-specific tool must name an executable'; done
[[ -x "$translator" ]] || fail '--translator must name an executable'
[[ -x "$cmake_root/bin/cmake" ]] || fail '--cmake-root must contain bin/cmake' [[ -x "$cmake_root/bin/cmake" ]] || fail '--cmake-root must contain bin/cmake'
[[ -x "$ninja" ]] || fail '--ninja must name an executable' require_arch() {
"$nodtool" --version >/dev/null || fail '--nodtool did not run successfully' local artifact=$1 arch=$2 label=$3
lipo "$artifact" -verify_arch "$arch" >/dev/null 2>&1 || fail "$label must contain a $arch slice: $artifact"
}
require_arch "$nodtool_arm64" arm64 '--nodtool-arm64'; require_arch "$nodtool_x86_64" x86_64 '--nodtool-x86_64'
require_arch "$translator_arm64" arm64 '--translator-arm64'; require_arch "$translator_x86_64" x86_64 '--translator-x86_64'
require_arch "$ninja_arm64" arm64 '--ninja-arm64'; require_arch "$ninja_x86_64" x86_64 '--ninja-x86_64'
lipo "$cmake_root/bin/cmake" -verify_arch arm64 x86_64 >/dev/null 2>&1 || fail '--cmake-root/bin/cmake must be universal2'
# Slice checks above prevent accidental cross-architecture packaging. Exercise
# each supplied executable as well: an incorrectly bundled runtime can have a
# valid Mach-O header but still fail before the setup app can use it. Apple
# Silicon maintainers validate Intel tools through Rosetta when it is present.
host_arch=$(uname -m)
run_for_arch() {
local arch=$1 label=$2
shift 2
if [[ "$arch" == "$host_arch" ]]; then
"$@" >/dev/null || fail "$label did not run successfully"
elif [[ "$host_arch" == arm64 && "$arch" == x86_64 ]] && /usr/bin/arch -x86_64 /usr/bin/true >/dev/null 2>&1; then
/usr/bin/arch -x86_64 "$@" >/dev/null || fail "$label did not run successfully under Rosetta"
else
# Intel hosts cannot execute arm64 binaries. The slice remains checked
# above; CI or an Apple Silicon maintainer must execute that tool set.
printf 'build-setup-pkg.command: warning: unable to execute %s on %s; architecture slice was verified, but run it in %s CI before release\n' \
"$label" "$host_arch" "$arch" >&2
fi
}
for arch in arm64 x86_64; do
if [[ "$arch" == arm64 ]]; then
nodtool=$nodtool_arm64; translator=$translator_arm64; ninja=$ninja_arm64
else
nodtool=$nodtool_x86_64; translator=$translator_x86_64; ninja=$ninja_x86_64
fi
run_for_arch "$arch" "--nodtool-$arch" "$nodtool" --version
run_for_arch "$arch" "--translator-$arch" "$translator" --help
run_for_arch "$arch" "--ninja-$arch" "$ninja" --version
done
run_for_arch "$host_arch" '--cmake-root/bin/cmake' "$cmake_root/bin/cmake" --version
workspace=$(cd "$workspace" && pwd); output=$(cd "$(dirname "$output")" && pwd)/$(basename "$output") workspace=$(cd "$workspace" && pwd); output=$(cd "$(dirname "$output")" && pwd)/$(basename "$output")
stage=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-pkg.XXXXXX") stage=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-pkg.XXXXXX")
trap 'rm -rf "$stage"' EXIT trap 'rm -rf "$stage"' EXIT
@@ -64,7 +115,7 @@ cat > "$app/Contents/Info.plist" <<EOF
<key>CFBundlePackageType</key><string>APPL</string> <key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>$short_version</string> <key>CFBundleShortVersionString</key><string>$short_version</string>
<key>CFBundleVersion</key><string>$version</string> <key>CFBundleVersion</key><string>$version</string>
<key>LSMinimumSystemVersion</key><string>14.0</string> <key>LSMinimumSystemVersion</key><string>12.0</string>
</dict></plist> </dict></plist>
EOF EOF
cat > "$app/Contents/MacOS/WiiCompiledSetup" <<'EOF' cat > "$app/Contents/MacOS/WiiCompiledSetup" <<'EOF'
@@ -103,11 +154,17 @@ copy_clean "$workspace/Launcher/local-build-macos.command" "$resources/workspace
copy_clean "$workspace/Launcher/macos/extract-disc.command" "$resources/workspace/Launcher/macos/extract-disc.command" copy_clean "$workspace/Launcher/macos/extract-disc.command" "$resources/workspace/Launcher/macos/extract-disc.command"
copy_clean "$workspace/Launcher/macos/publish-app.command" "$resources/workspace/Launcher/macos/publish-app.command" copy_clean "$workspace/Launcher/macos/publish-app.command" "$resources/workspace/Launcher/macos/publish-app.command"
chmod +x "$resources/workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/macos/"*.command chmod +x "$resources/workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/macos/"*.command
mkdir -p "$resources/tools/cmake" # setup.command uses this marker to update source inputs in an existing user
copy_clean "$nodtool" "$resources/tools/nodtool"; chmod +x "$resources/tools/nodtool" # workspace without replacing extracted game assets or Retro Rewind files.
copy_clean "$translator" "$resources/tools/Translator.Cli"; chmod +x "$resources/tools/Translator.Cli" printf '%s\n' "$version" > "$resources/workspace/.bundle-version"
mkdir -p "$resources/tools/cmake" "$resources/tools/arm64" "$resources/tools/x86_64"
copy_clean "$nodtool_arm64" "$resources/tools/arm64/nodtool"; chmod +x "$resources/tools/arm64/nodtool"
copy_clean "$nodtool_x86_64" "$resources/tools/x86_64/nodtool"; chmod +x "$resources/tools/x86_64/nodtool"
copy_clean "$translator_arm64" "$resources/tools/arm64/Translator.Cli"; chmod +x "$resources/tools/arm64/Translator.Cli"
copy_clean "$translator_x86_64" "$resources/tools/x86_64/Translator.Cli"; chmod +x "$resources/tools/x86_64/Translator.Cli"
copy_clean "$cmake_root" "$resources/tools/cmake" copy_clean "$cmake_root" "$resources/tools/cmake"
copy_clean "$ninja" "$resources/tools/ninja"; chmod +x "$resources/tools/ninja" copy_clean "$ninja_arm64" "$resources/tools/arm64/ninja"; chmod +x "$resources/tools/arm64/ninja"
copy_clean "$ninja_x86_64" "$resources/tools/x86_64/ninja"; chmod +x "$resources/tools/x86_64/ninja"
copy_clean "$workspace/LICENSE" "$resources/LICENSE" copy_clean "$workspace/LICENSE" "$resources/LICENSE"
copy_clean "$workspace/THIRD-PARTY-NOTICES.md" "$resources/THIRD-PARTY-NOTICES.md" copy_clean "$workspace/THIRD-PARTY-NOTICES.md" "$resources/THIRD-PARTY-NOTICES.md"
codesign --force --deep --sign - "$app" codesign --force --deep --sign - "$app"
@@ -0,0 +1,11 @@
# Cross-compile a thin x86_64 macOS build from an Apple Silicon Mac.
# Pass this file on the first configure with:
# -DCMAKE_TOOLCHAIN_FILE=/absolute/path/to/macos-x86_64-toolchain.cmake
#
# CMAKE_SYSTEM_PROCESSOR is deliberately declared here rather than inferred
# from CMAKE_OSX_ARCHITECTURES, so target-aware CMake dependencies select their
# x86_64 artifacts.
set(CMAKE_SYSTEM_NAME Darwin)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_OSX_ARCHITECTURES x86_64 CACHE STRING
"Target macOS architectures" FORCE)
+93 -14
View File
@@ -5,28 +5,36 @@ set -euo pipefail
fail() { printf 'publish-app.command: error: %s\n' "$*" >&2; exit 1; } fail() { printf 'publish-app.command: error: %s\n' "$*" >&2; exit 1; }
usage() { usage() {
cat <<'EOF' cat <<'EOF'
Usage: publish-app.command --build-dir DIR --product {WiiCompiled|RetroRewind} --output-dir DIR Usage: publish-app.command --build-dir DIR --product {WiiCompiled|RetroRewind} --output-dir DIR [options]
Copies a locally built product and its runtime assets into OUTPUT-DIR/<product>.app. Copies a locally built product and its runtime assets into OUTPUT-DIR/<product>.app.
It bundles non-system dylibs, rewrites their install names, and ad-hoc signs the It bundles non-system dylibs, rewrites their install names, and ad-hoc signs the
result. This is suitable for local use; a release must replace ad-hoc signing result. This is suitable for local use; a release must replace ad-hoc signing
with the project's Developer ID signing and notarization process. with the project's Developer ID signing and notarization process.
--architecture {arm64|x86_64} Required architecture of the compiled product (default: host)
--minimum-system-version VERSION App bundle minimum macOS version (default: 12.0)
EOF EOF
} }
build_dir=""; product=""; output_dir="" build_dir=""; product=""; output_dir=""; architecture=$(uname -m); minimum_system_version=12.0
while (($#)); do while (($#)); do
case "$1" in case "$1" in
--build-dir) build_dir=${2:-}; shift 2 ;; --build-dir) build_dir=${2:-}; shift 2 ;;
--product) product=${2:-}; shift 2 ;; --product) product=${2:-}; shift 2 ;;
--output-dir) output_dir=${2:-}; shift 2 ;; --output-dir) output_dir=${2:-}; shift 2 ;;
--architecture) architecture=${2:-}; shift 2 ;;
--minimum-system-version) minimum_system_version=${2:-}; shift 2 ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
*) fail "unknown option: $1" ;; *) fail "unknown option: $1" ;;
esac esac
done done
[[ "$product" == WiiCompiled || "$product" == RetroRewind ]] || fail '--product must be WiiCompiled or RetroRewind' [[ "$product" == WiiCompiled || "$product" == RetroRewind ]] || fail '--product must be WiiCompiled or RetroRewind'
for tool in codesign ditto install_name_tool otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done [[ "$architecture" == arm64 || "$architecture" == x86_64 ]] || fail '--architecture must be arm64 or x86_64'
[[ "$minimum_system_version" =~ ^[0-9]+(\.[0-9]+){1,2}$ ]] || fail '--minimum-system-version must contain two or three period-separated integers'
for tool in codesign ditto install_name_tool lipo otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
[[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product" [[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product"
lipo "$build_dir/$product" -verify_arch "$architecture" || fail "compiled product is not $architecture: $build_dir/$product"
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done
app="$output_dir/$product.app" app="$output_dir/$product.app"
@@ -47,7 +55,7 @@ cat > "$app/Contents/Info.plist" <<EOF
<key>CFBundlePackageType</key><string>APPL</string> <key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>0.1.0</string> <key>CFBundleShortVersionString</key><string>0.1.0</string>
<key>CFBundleVersion</key><string>1</string> <key>CFBundleVersion</key><string>1</string>
<key>LSMinimumSystemVersion</key><string>14.0</string> <key>LSMinimumSystemVersion</key><string>$minimum_system_version</string>
<key>NSHighResolutionCapable</key><true/> <key>NSHighResolutionCapable</key><true/>
</dict></plist> </dict></plist>
EOF EOF
@@ -57,25 +65,96 @@ for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do
ln -s "../Resources/$asset" "$macos/$asset" ln -s "../Resources/$asset" "$macos/$asset"
done done
# Build a closure of Homebrew dylibs. System libraries remain system references. # Expand each image's rpaths before inheriting them, so @loader_path stays
queue=("$macos/$product") # relative to the image that declared it rather than a descendant library.
expanded_rpaths() {
local target=$1 rpath
while IFS= read -r rpath; do
case "$rpath" in
@loader_path/*) rpath="$(dirname "$target")/${rpath#@loader_path/}" ;;
@loader_path) rpath="$(dirname "$target")" ;;
@executable_path/*) rpath="$build_dir/${rpath#@executable_path/}" ;;
@executable_path) rpath="$build_dir" ;;
esac
printf '%s\n' "$rpath"
done < <(otool -l "$target" | awk '
/LC_RPATH/ { rpath = 1; next }
rpath && /^[[:space:]]*path / {
sub(/^[[:space:]]*path[[:space:]]+/, "");
sub(/[[:space:]]+\(offset[[:space:]]+[0-9]+\)$/, "");
print;
rpath = 0;
}')
}
# Resolve a non-system dependency using the current image's rpaths followed
# by the inherited loader stack, matching dyld's dependency-chain search.
dependency_path() {
local current=$1 dependency=$2 search_rpaths=$3 rpath candidate
case "$dependency" in
/System/Library/*|/usr/lib/*)
return 1
;;
/*)
[[ -f "$dependency" ]] && { printf '%s\n' "$dependency"; return 0; }
;;
@loader_path/*)
candidate="$(dirname "$current")/${dependency#@loader_path/}"
[[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; }
;;
@executable_path/*)
candidate="$build_dir/${dependency#@executable_path/}"
[[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; }
;;
@rpath/*|*.dylib)
local subpath
if [[ "$dependency" == @rpath/* ]]; then
subpath="${dependency#@rpath/}"
else
subpath="$dependency"
fi
while IFS= read -r rpath; do
[[ -n "$rpath" ]] || continue
candidate="$rpath/$subpath"
[[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; }
done <<< "$search_rpaths"
;;
esac
return 1
}
# Build a closure of non-system dylibs. System libraries remain system
# references, while every resolved dependency is copied beside the executable.
queue=("$build_dir/$product")
queue_rpaths=("")
while ((${#queue[@]})); do while ((${#queue[@]})); do
current=${queue[0]} current=${queue[0]}
inherited_rpaths=${queue_rpaths[0]}
queue=("${queue[@]:1}") queue=("${queue[@]:1}")
queue_rpaths=("${queue_rpaths[@]:1}")
current_rpaths=$(expanded_rpaths "$current")
if [[ -n "$inherited_rpaths" ]]; then
current_rpaths="${current_rpaths}${current_rpaths:+$'\n'}$inherited_rpaths"
fi
self_id=$(otool -D "$current" 2>/dev/null | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]*$/\1/p' || true)
while IFS= read -r dependency; do while IFS= read -r dependency; do
[[ "$dependency" == /opt/homebrew/* || "$dependency" == /usr/local/* ]] || continue [[ -z "$dependency" ]] && continue
[[ -f "$dependency" ]] || continue [[ -n "$self_id" && "$dependency" == "$self_id" ]] && continue
name=$(basename "$dependency") case "$dependency" in
/System/Library/*|/usr/lib/*) continue ;;
esac
dep_path=$(dependency_path "$current" "$dependency" "$current_rpaths") || fail "unresolved non-system dependency: '$dependency' needed by '$current'"
name=$(basename "$dep_path")
if [[ ! -f "$frameworks/$name" ]]; then if [[ ! -f "$frameworks/$name" ]]; then
ditto "$dependency" "$frameworks/$name" ditto "$dep_path" "$frameworks/$name"
install_name_tool -id "@rpath/$name" "$frameworks/$name" install_name_tool -id "@rpath/$name" "$frameworks/$name"
queue+=("$frameworks/$name") queue+=("$dep_path")
queue_rpaths+=("$current_rpaths")
fi fi
done < <(otool -L "$current" | tail -n +2 | awk '{print $1}') done < <(otool -L "$current" | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]+\(compatibility version .*/\1/p')
done done
while IFS= read -r binary; do while IFS= read -r binary; do
while IFS= read -r old; do while IFS= read -r old; do
[[ "$old" == /opt/homebrew/* || "$old" == /usr/local/* ]] || continue
name=$(basename "$old") name=$(basename "$old")
[[ -f "$frameworks/$name" ]] || continue [[ -f "$frameworks/$name" ]] || continue
if [[ "$binary" == "$macos/$product" ]]; then if [[ "$binary" == "$macos/$product" ]]; then
@@ -83,7 +162,7 @@ while IFS= read -r binary; do
else else
install_name_tool -change "$old" "@loader_path/$name" "$binary" install_name_tool -change "$old" "@loader_path/$name" "$binary"
fi fi
done < <(otool -L "$binary" | tail -n +2 | awk '{print $1}') done < <(otool -L "$binary" | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]+\(compatibility version .*/\1/p')
done < <(find "$frameworks" -type f -print; printf '%s\n' "$macos/$product") done < <(find "$frameworks" -type f -print; printf '%s\n' "$macos/$product")
find "$frameworks" -type f -exec codesign --force --sign - {} + find "$frameworks" -type f -exec codesign --force --sign - {} +
+35 -6
View File
@@ -5,10 +5,7 @@ set -euo pipefail
resources=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) resources=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace_source="$resources/workspace" workspace_source="$resources/workspace"
nodtool="$resources/tools/nodtool"
translator="$resources/tools/Translator.Cli"
cmake_bin="$resources/tools/cmake/bin/cmake" cmake_bin="$resources/tools/cmake/bin/cmake"
ninja_bin="$resources/tools/ninja"
support_root="$HOME/Library/Application Support/WiiCompiled" support_root="$HOME/Library/Application Support/WiiCompiled"
workspace="$support_root/BuildWorkspace" workspace="$support_root/BuildWorkspace"
products="$support_root/Products" products="$support_root/Products"
@@ -36,6 +33,13 @@ while (($#)); do
done done
[[ "$install_location" == user || "$install_location" == applications ]] || fail '--install-location must be user or applications' [[ "$install_location" == user || "$install_location" == applications ]] || fail '--install-location must be user or applications'
host_arch=$(uname -m)
case "$host_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $host_arch" ;; esac
host_tools="$resources/tools/$host_arch"
nodtool="$host_tools/nodtool"
translator="$host_tools/Translator.Cli"
ninja_bin="$host_tools/ninja"
if [[ -z "$game" ]]; then if [[ -z "$game" ]]; then
game=$(/usr/bin/osascript <<'APPLESCRIPT' game=$(/usr/bin/osascript <<'APPLESCRIPT'
set selectedFile to choose file with prompt "Choose your clean Mario Kart Wii PAL (RMCP01) disc image" set selectedFile to choose file with prompt "Choose your clean Mario Kart Wii PAL (RMCP01) disc image"
@@ -60,12 +64,37 @@ if ! /usr/bin/xcode-select -p >/dev/null 2>&1; then
/usr/bin/xcode-select --install || true /usr/bin/xcode-select --install || true
exit 1 exit 1
fi fi
for tool in "$nodtool" "$translator" "$cmake_bin" "$ninja_bin"; do
/usr/bin/lipo "$tool" -verify_arch "$host_arch" >/dev/null 2>&1 || \
fail "the packaged $(basename "$tool") does not support $host_arch"
done
mkdir -p "$support_root" "$products" mkdir -p "$support_root" "$products"
if [[ ! -d "$workspace/.git" && ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then source_bundle_version="$workspace_source/.bundle-version"
workspace_bundle_version="$workspace/.bundle-version"
needs_workspace_refresh=0
if [[ ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then
needs_workspace_refresh=1
elif [[ -f "$source_bundle_version" ]] && [[ ! -f "$workspace_bundle_version" || "$(<"$source_bundle_version")" != "$(<"$workspace_bundle_version")" ]]; then
needs_workspace_refresh=1
fi
if (( needs_workspace_refresh )); then
printf 'Preparing the local build workspace...\n' printf 'Preparing the local build workspace...\n'
rm -rf "$workspace" if [[ ! -d "$workspace" ]]; then
/usr/bin/ditto "$workspace_source" "$workspace" /usr/bin/ditto "$workspace_source" "$workspace"
else
# Refresh only packaged source inputs. Assets and the staged Retro
# Rewind package belong to the user and stay in place.
for source in aurora-main projects runtime translator Launcher; do
rm -rf "$workspace/$source"
/usr/bin/ditto "$workspace_source/$source" "$workspace/$source"
done
/usr/bin/ditto "$source_bundle_version" "$workspace_bundle_version"
# A dependency provider can be cached in this directory, so make the
# refreshed sources configure from a clean native build tree.
rm -rf "$workspace/native-build-macos-arm64" "$workspace/native-build-macos-x86_64"
fi
fi fi
profile=base profile=base
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# Exercise a real dyld dependency chain before and after app packaging.
set -euo pipefail
[[ $(uname -s) == Darwin ]] || { printf 'This test requires macOS.\n' >&2; exit 1; }
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
temp_root=$(cd "${TMPDIR:-/tmp}" && pwd)
test_root=$(mktemp -d "$temp_root/wiicompiled-publish-test.XXXXXX")
[[ "$test_root" == "$temp_root"/wiicompiled-publish-test.* ]] || exit 1
trap 'rm -rf "$test_root"' EXIT
build_dir="$test_root/build with spaces"
output_dir="$test_root/output with spaces"
mkdir -p "$build_dir/A/b" "$build_dir/A/c" "$build_dir/wii_bootstrap" "$output_dir"
cat > "$test_root/c.c" <<'EOF'
int value_c(void) { return 7; }
EOF
cat > "$test_root/b.c" <<'EOF'
extern int value_c(void);
int value_b(void) { return 2 * value_c(); }
EOF
cat > "$test_root/a.c" <<'EOF'
extern int value_b(void);
int value_a(void) { return 1 + value_b(); }
EOF
cat > "$test_root/main.c" <<'EOF'
#include <stdio.h>
extern int value_a(void);
int main(void) { printf("%d\n", value_a()); return 0; }
EOF
clang -dynamiclib "$test_root/c.c" -o "$build_dir/A/c/libC.dylib" \
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libC.dylib
clang -dynamiclib "$test_root/b.c" -o "$build_dir/A/b/libB.dylib" \
-L "$build_dir/A/c" -lC \
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libB.dylib
clang -dynamiclib "$test_root/a.c" -o "$build_dir/A/libA.dylib" \
-L "$build_dir/A/b" -lB \
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libA.dylib \
-Wl,-rpath,@loader_path/b -Wl,-rpath,@loader_path/c
clang "$test_root/main.c" -o "$build_dir/WiiCompiled" \
-L "$build_dir/A" -lA \
-Wl,-headerpad_max_install_names -Wl,-rpath,@executable_path/A
# B has no rpaths: its C dependency must inherit A's loader-relative path.
[[ $("$build_dir/WiiCompiled") == 15 ]]
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem; do
: > "$build_dir/$asset"
done
bash "$script_dir/publish-app.command" --build-dir "$build_dir" \
--product WiiCompiled --output-dir "$output_dir" --architecture "$(uname -m)"
for name in libA.dylib libB.dylib libC.dylib; do
[[ -f "$output_dir/WiiCompiled.app/Contents/Frameworks/$name" ]]
done
# Remove access to the original paths and relocate the app before executing.
mv "$build_dir" "$test_root/hidden build"
mkdir "$test_root/relocated app"
mv "$output_dir/WiiCompiled.app" "$test_root/relocated app/WiiCompiled.app"
[[ $("$test_root/relocated app/WiiCompiled.app/Contents/MacOS/WiiCompiled") == 15 ]]
printf 'publish-app dependency-chain test passed\n'
+22 -3
View File
@@ -52,12 +52,16 @@ Play at several times the console's resolution.
**Music ducking.** **Music ducking.**
Start playing something else, Spotify, a YouTube video, and Start playing something else, Spotify, a YouTube video, and
the game automatically mutes its own music until the other audio stops. Optional, if you'd the game automatically mutes its own music until the other audio stops. Optional, if you'd
rather it didn't. All audio that shows in your display media controls on your windows pc fall under this. rather it didn't. Windows uses system media controls and Linux uses MPRIS players.
On macOS 14.2 or later, this detects other apps with active audio output and excludes
the game's own audio. Apps that keep an output stream running silently can keep
game music muted even when nothing is audible.
**An in-game settings bar.** **An in-game settings bar.**
Press **F10** while the game window has focus: Press **F10** while the game window has focus:
- Internal resolution - Internal resolution
- FPS counter - FPS counter
- MetalFX spatial upscaling on supported macOS GPUs
- Controller assignment for all four ports - Controller assignment for all four ports
- Full per-controller button mapping, including the bumpers - Full per-controller button mapping, including the bumpers
- Dolphin-syntax input expressions and GCPadNew.ini import - Dolphin-syntax input expressions and GCPadNew.ini import
@@ -96,8 +100,8 @@ Known limitations of the Wii Remote path:
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher - GPU: GTX 1650 / RX 6400 / Arc A310 or higher
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher - CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
- About 20 GB of free disk space during installation (Final game size ~5 GB) - About 20 GB of free disk space during installation (Final game size ~5 GB)
- macOS 14 (Sonoma) or later on Apple Silicon - macOS 12 (Monterey) or later on Apple Silicon (`arm64`) or Intel (`x86_64-v3`); pre-Haswell Intel CPUs are unsupported
- On macOS, Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing) - On macOS, a Metal-capable GPU and Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM, - A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
GCZ, CISO, WBFS, WIA and RVZ are accepted. GCZ, CISO, WBFS, WIA and RVZ are accepted.
@@ -118,6 +122,21 @@ image under Settings, turn on **WiiCompiled (beta)**, and hit install from the H
Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it. launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
### macOS
Download `WiiCompiled-Setup.pkg` from this repository's Releases page and open it. The universal
package selects the appropriate bundled tools for the host architecture, supporting both Apple Silicon (`arm64`)
and Intel (`x86_64`) Macs. It installs **WiiCompiled Setup** in Applications; open that app, choose
your clean PAL `RMCP01` disc image, and select either the base game or Retro Rewind. For Retro Rewind, choose the `RetroRewind6` folder
or its parent folder.
Setup verifies and extracts the image locally, then translates and compiles the native app on your
Mac. On a first run it may ask macOS to install Xcode Command Line Tools; complete Apple's installer,
then open Setup again. When the build completes, Setup asks for administrator approval once to install
`WiiCompiled.app` (and, if selected, `RetroRewind.app`) in `/Applications`.
Setup opens Terminal while it works, so the extraction and build progress—and any error that needs
reporting—remain visible.
> [!CAUTION] > [!CAUTION]
> Only take builds from this repository's > Only take builds from this repository's
+8
View File
@@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.25)
project(aurora LANGUAGES C CXX) project(aurora LANGUAGES C CXX)
if (APPLE) if (APPLE)
enable_language(OBJC) enable_language(OBJC)
enable_language(OBJCXX)
endif() endif()
set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
@@ -83,3 +84,10 @@ if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND NOT CMAKE_CROSSCOMPIL
enable_testing() enable_testing()
add_subdirectory(tests) add_subdirectory(tests)
endif () endif ()
option(AURORA_BUILD_METALFX_PRESENTATION_TEST "Build the macOS MetalFX presentation test" OFF)
if (AURORA_BUILD_METALFX_PRESENTATION_TEST AND APPLE AND AURORA_ENABLE_GX AND DAWN_ENABLE_METAL AND AURORA_METALFX_FRAMEWORK)
add_executable(metalfx_presentation_test tests/metalfx_interop/presentation_test.cpp)
target_include_directories(metalfx_presentation_test PRIVATE lib)
target_link_libraries(metalfx_presentation_test PRIVATE aurora::core aurora::gx aurora::vi dawn::webgpu_dawn)
endif ()
+5 -1
View File
@@ -48,7 +48,7 @@ if (_aurora_dawn_provider STREQUAL "auto")
set(_has_package TRUE) set(_has_package TRUE)
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|aarch64)$") elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|aarch64)$")
set(_has_package TRUE) set(_has_package TRUE)
elseif (APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|x86_64)$") elseif (APPLE AND (CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|x86_64)$" OR CMAKE_OSX_ARCHITECTURES MATCHES "^(arm64|x86_64)$"))
set(_has_package TRUE) set(_has_package TRUE)
endif () endif ()
@@ -143,6 +143,10 @@ elseif (_aurora_dawn_provider STREQUAL "package")
if (NOT AURORA_DAWN_PACKAGE_URL) if (NOT AURORA_DAWN_PACKAGE_URL)
string(TOLOWER "${CMAKE_SYSTEM_NAME}" _dawn_system) string(TOLOWER "${CMAKE_SYSTEM_NAME}" _dawn_system)
string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _dawn_arch) string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _dawn_arch)
if (APPLE AND CMAKE_OSX_ARCHITECTURES)
list(GET CMAKE_OSX_ARCHITECTURES 0 _dawn_osx_arch)
string(TOLOWER "${_dawn_osx_arch}" _dawn_arch)
endif ()
if (_dawn_system STREQUAL "windows") if (_dawn_system STREQUAL "windows")
if (_dawn_arch STREQUAL "x86_64") if (_dawn_arch STREQUAL "x86_64")
set(_dawn_arch "amd64") set(_dawn_arch "amd64")
+5 -2
View File
@@ -41,7 +41,10 @@ if (_aurora_sdl3_provider STREQUAL "auto")
set(_aurora_sdl3_provider "package") set(_aurora_sdl3_provider "package")
else () else ()
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON) set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
find_package(SDL3 QUIET) # Aurora uses APIs from the SDL version pinned by AURORA_SDL3_VERSION.
# Do not silently select an older system package and fail later while
# compiling its headers.
find_package(SDL3 ${AURORA_SDL3_VERSION} QUIET)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF) set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
if (SDL3_FOUND) if (SDL3_FOUND)
set(_aurora_sdl3_provider "system") set(_aurora_sdl3_provider "system")
@@ -58,7 +61,7 @@ if (_aurora_sdl3_provider STREQUAL "system")
message(STATUS "aurora: Using system SDL3 (provider=system)") message(STATUS "aurora: Using system SDL3 (provider=system)")
if (NOT SDL3_FOUND) if (NOT SDL3_FOUND)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON) set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
find_package(SDL3 REQUIRED) find_package(SDL3 ${AURORA_SDL3_VERSION} REQUIRED)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF) set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
endif () endif ()
_aurora_sdl3_select_target() _aurora_sdl3_select_target()
+14 -1
View File
@@ -34,6 +34,17 @@ if (AURORA_ENABLE_GX)
target_compile_definitions(aurora_core PUBLIC AURORA_ENABLE_GX WEBGPU_DAWN) target_compile_definitions(aurora_core PUBLIC AURORA_ENABLE_GX WEBGPU_DAWN)
target_sources(aurora_core PRIVATE lib/webgpu/gpu.cpp lib/webgpu/gpu_cache.cpp lib/dawn/BackendBinding.cpp) target_sources(aurora_core PRIVATE lib/webgpu/gpu.cpp lib/webgpu/gpu_cache.cpp lib/dawn/BackendBinding.cpp)
target_link_libraries(aurora_core PRIVATE dawn::webgpu_dawn) target_link_libraries(aurora_core PRIVATE dawn::webgpu_dawn)
if (APPLE AND DAWN_ENABLE_METAL)
find_library(AURORA_METALFX_FRAMEWORK MetalFX)
endif ()
if (APPLE AND DAWN_ENABLE_METAL AND AURORA_METALFX_FRAMEWORK)
target_sources(aurora_core PRIVATE lib/webgpu/metalfx.mm)
set_source_files_properties(lib/webgpu/metalfx.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
target_link_options(aurora_core PUBLIC "LINKER:-weak_framework,MetalFX")
target_link_libraries(aurora_core PRIVATE "-framework IOSurface")
else ()
target_sources(aurora_core PRIVATE lib/webgpu/metalfx_stub.cpp)
endif ()
if (DAWN_ENABLE_VULKAN) if (DAWN_ENABLE_VULKAN)
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_VULKAN) target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_VULKAN)
endif () endif ()
@@ -41,7 +52,9 @@ if (AURORA_ENABLE_GX)
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_METAL) target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_METAL)
target_sources(aurora_core PRIVATE lib/dawn/MetalBinding.mm) target_sources(aurora_core PRIVATE lib/dawn/MetalBinding.mm)
set_source_files_properties(lib/dawn/MetalBinding.mm PROPERTIES COMPILE_FLAGS -fobjc-arc) set_source_files_properties(lib/dawn/MetalBinding.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
target_link_options(aurora_core PUBLIC "LINKER:-weak_framework,Metal") target_link_options(aurora_core PUBLIC
"LINKER:-weak_framework,Metal"
"LINKER:-U,_OBJC_CLASS_$_MTLLogStateDescriptor")
endif () endif ()
if (DAWN_ENABLE_D3D11) if (DAWN_ENABLE_D3D11)
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_D3D11) target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_D3D11)
+50
View File
@@ -16,10 +16,60 @@ function(aurora_find_package_global)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ${_PREV_FIND_PACKAGE_TARGETS_GLOBAL}) set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ${_PREV_FIND_PACKAGE_TARGETS_GLOBAL})
endfunction() endfunction()
# A macOS build for another architecture must not discover Homebrew packages
# built for its physical host. CMAKE_CROSSCOMPILING is not sufficient here:
# Apple Clang can target another architecture through CMAKE_OSX_ARCHITECTURES
# without CMake considering the configure a cross-build.
set(_AURORA_EXCLUDE_HOST_HOMEBREW FALSE)
if (APPLE AND CMAKE_OSX_ARCHITECTURES)
list(LENGTH CMAKE_OSX_ARCHITECTURES _AURORA_OSX_ARCH_COUNT)
if (_AURORA_OSX_ARCH_COUNT EQUAL 1)
list(GET CMAKE_OSX_ARCHITECTURES 0 _AURORA_TARGET_ARCH)
string(TOLOWER "${_AURORA_TARGET_ARCH}" _AURORA_TARGET_ARCH)
# hw.optional.arm64 identifies Apple Silicon even when CMake itself runs
# through Rosetta, where CMAKE_HOST_SYSTEM_PROCESSOR reports x86_64.
execute_process(
COMMAND /usr/sbin/sysctl -n hw.optional.arm64
RESULT_VARIABLE _AURORA_ARM64_PROBE_RESULT
OUTPUT_VARIABLE _AURORA_ARM64_PROBE
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE)
if (_AURORA_ARM64_PROBE_RESULT EQUAL 0 AND _AURORA_ARM64_PROBE STREQUAL "1")
set(_AURORA_HOST_ARCH arm64)
else ()
execute_process(
COMMAND /usr/bin/uname -m
OUTPUT_VARIABLE _AURORA_HOST_ARCH
OUTPUT_STRIP_TRAILING_WHITESPACE)
string(TOLOWER "${_AURORA_HOST_ARCH}" _AURORA_HOST_ARCH)
endif ()
if (_AURORA_TARGET_ARCH STREQUAL "x86_64" AND _AURORA_HOST_ARCH MATCHES "^(arm64|aarch64)$")
list(APPEND CMAKE_IGNORE_PREFIX_PATH "/opt/homebrew")
set(_AURORA_EXCLUDE_HOST_HOMEBREW TRUE)
elseif (_AURORA_TARGET_ARCH MATCHES "^(arm64|aarch64)$" AND _AURORA_HOST_ARCH MATCHES "^(x86_64|amd64)$")
list(APPEND CMAKE_IGNORE_PREFIX_PATH "/usr/local")
set(_AURORA_EXCLUDE_HOST_HOMEBREW TRUE)
endif ()
endif ()
endif ()
if (_AURORA_EXCLUDE_HOST_HOMEBREW)
list(REMOVE_DUPLICATES CMAKE_IGNORE_PREFIX_PATH)
message(STATUS "aurora: cross-architecture macOS build; ignoring host Homebrew prefixes")
endif ()
if (AURORA_ENABLE_GX) if (AURORA_ENABLE_GX)
include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/AuroraDawnProvider.cmake) include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/AuroraDawnProvider.cmake)
endif () endif ()
# SDL's pkg-config probe can bypass CMake's prefix exclusion. macOS does not
# need libusb for the supported SDL input paths, so keep that host-only library
# out of a cross-architecture configure.
if (_AURORA_EXCLUDE_HOST_HOMEBREW)
set(SDL_HIDAPI_LIBUSB OFF CACHE BOOL "" FORCE)
set(SDL_HIDAPI_LIBUSB_SHARED OFF CACHE BOOL "" FORCE)
endif ()
# Abseil is needed for core libraries. It normally comes via Dawn's vendor build. # Abseil is needed for core libraries. It normally comes via Dawn's vendor build.
# Otherwise prefer a system package and only fetch it as a last resort. # Otherwise prefer a system package and only fetch it as a last resort.
if (NOT TARGET absl::flat_hash_map OR NOT TARGET absl::btree) if (NOT TARGET absl::flat_hash_map OR NOT TARGET absl::btree)
+15
View File
@@ -170,6 +170,21 @@ void aurora_set_background_input(bool value);
void aurora_set_display_mode(AuroraDisplayMode mode); void aurora_set_display_mode(AuroraDisplayMode mode);
AuroraDisplayMode aurora_get_display_mode(); AuroraDisplayMode aurora_get_display_mode();
typedef enum {
AURORA_METALFX_DISABLED,
AURORA_METALFX_UNSUPPORTED,
AURORA_METALFX_NOT_UPSCALING,
AURORA_METALFX_ACTIVE,
AURORA_METALFX_ERROR,
} AuroraMetalFXStatus;
// Changes are consumed at the next sealed frame boundary. MetalFX only applies
// when both source dimensions are smaller than the aspect-fitted output.
void aurora_set_metalfx_spatial(bool enabled);
bool aurora_get_metalfx_spatial();
bool aurora_is_metalfx_spatial_supported();
AuroraMetalFXStatus aurora_get_metalfx_status();
AuroraBackend aurora_get_backend(); AuroraBackend aurora_get_backend();
const AuroraBackend* aurora_get_available_backends(size_t* count); const AuroraBackend* aurora_get_available_backends(size_t* count);
+150 -9
View File
@@ -7,6 +7,7 @@
#include "gx/shader_info.hpp" #include "gx/shader_info.hpp"
#include "imgui.hpp" #include "imgui.hpp"
#include "webgpu/gpu.hpp" #include "webgpu/gpu.hpp"
#include "webgpu/metalfx.hpp"
#include <webgpu/webgpu_cpp.h> #include <webgpu/webgpu_cpp.h>
#endif #endif
@@ -60,6 +61,9 @@ std::atomic<AuroraFrameWorkerWaitCallback> g_frameWorkerWaitCallback{nullptr};
// deadlines derived from it, so the presenter cannot drift. Zero means present when ready. // deadlines derived from it, so the presenter cannot drift. Zero means present when ready.
std::atomic<uint64_t> g_presentScheduleBaseNanos{0}; std::atomic<uint64_t> g_presentScheduleBaseNanos{0};
std::atomic<uint64_t> g_presentScheduleIntervalNanos{0}; std::atomic<uint64_t> g_presentScheduleIntervalNanos{0};
std::atomic<bool> g_metalfxRequested{false};
std::atomic<bool> g_metalfxSupported{false};
std::atomic<AuroraMetalFXStatus> g_metalfxStatus{AURORA_METALFX_DISABLED};
namespace { namespace {
Module Log("aurora"); Module Log("aurora");
@@ -765,6 +769,9 @@ AuroraInfo initialize(int argc, char* argv[], const AuroraConfig& config) noexce
#ifdef AURORA_ENABLE_GX #ifdef AURORA_ENABLE_GX
gfx::initialize(); gfx::initialize();
g_metalfxSupported.store(webgpu::metalfx::supported(g_device, webgpu::g_backendType));
g_metalfxStatus.store(AURORA_METALFX_DISABLED);
imgui::create_context(); imgui::create_context();
#endif #endif
const auto size = window::get_window_size(); const auto size = window::get_window_size();
@@ -1187,12 +1194,114 @@ void stop_presenter() noexcept {
g_presenterStarted.store(false, std::memory_order_release); g_presenterStarted.store(false, std::memory_order_release);
} }
struct MetalFXSlot {
webgpu::metalfx::Size size{};
std::unique_ptr<webgpu::metalfx::SpatialScaler> scaler;
wgpu::BindGroup bindGroup;
};
std::array<MetalFXSlot, gx::MaxInterpolatedFrames + 1> g_metalfxSlots;
size_t g_metalfxNextSlot = 0;
webgpu::metalfx::SpatialScaler* g_metalfxPendingOutput = nullptr;
bool g_metalfxFailed = false;
void metalfx_failed(const std::string& reason) {
Log.warn("MetalFX spatial upscaling disabled: {}; using normal presentation", reason);
g_metalfxFailed = true;
g_metalfxStatus.store(AURORA_METALFX_ERROR);
g_metalfxPendingOutput = nullptr;
g_metalfxSlots = {};
}
wgpu::BindGroup upscale_presentation(wgpu::CommandEncoder& encoder,
const webgpu::PresentSource& source,
const webgpu::Viewport& viewport, bool enabled) {
if (!enabled) {
g_metalfxSlots = {};
g_metalfxFailed = false;
g_metalfxStatus.store(AURORA_METALFX_DISABLED);
return {};
}
if (!g_metalfxSupported.load()) {
g_metalfxStatus.store(AURORA_METALFX_UNSUPPORTED);
return {};
}
if (g_metalfxFailed) return {};
const webgpu::metalfx::Size size{
source.size.width, source.size.height,
static_cast<uint32_t>(viewport.width), static_cast<uint32_t>(viewport.height),
webgpu::g_graphicsConfig.surfaceConfiguration.format,
};
// The existing copy path samples perceptual values from unorm game images.
// Do not introduce implicit sRGB decoding or downscaling into MetalFX.
if (!size.inputWidth || !size.inputHeight || size.inputWidth >= size.outputWidth ||
size.inputHeight >= size.outputHeight ||
(source.format != wgpu::TextureFormat::RGBA8Unorm && source.format != wgpu::TextureFormat::BGRA8Unorm)) {
g_metalfxStatus.store(AURORA_METALFX_NOT_UPSCALING);
return {};
}
auto& slot = g_metalfxSlots[g_metalfxNextSlot++ % g_metalfxSlots.size()];
if (!slot.scaler || !(slot.size == size)) {
slot = {};
std::string error;
slot.scaler = webgpu::metalfx::create(g_instance, g_device, size, error);
if (!slot.scaler) {
if (error.empty()) g_metalfxStatus.store(AURORA_METALFX_NOT_UPSCALING);
else metalfx_failed(error);
return {};
}
slot.size = size;
wgpu::SamplerDescriptor samplerDescriptor{};
samplerDescriptor.magFilter = wgpu::FilterMode::Linear;
samplerDescriptor.minFilter = wgpu::FilterMode::Linear;
slot.bindGroup = webgpu::create_copy_bind_group(slot.scaler->output_view(),
g_device.CreateSampler(&samplerDescriptor));
Log.info("MetalFX spatial slot: {}x{} -> {}x{}", size.inputWidth, size.inputHeight,
size.outputWidth, size.outputHeight);
}
if (!slot.scaler->begin_input()) {
metalfx_failed(slot.scaler->error());
return {};
}
const wgpu::RenderPassColorAttachment attachment{
.view = slot.scaler->input_view(),
.loadOp = wgpu::LoadOp::Clear,
.storeOp = wgpu::StoreOp::Store,
};
const wgpu::RenderPassDescriptor descriptor{
.label = "MetalFX input copy",
.colorAttachmentCount = 1,
.colorAttachments = &attachment,
};
auto pass = encoder.BeginRenderPass(&descriptor);
pass.SetPipeline(webgpu::g_CopyPipeline);
pass.SetBindGroup(0, source.bindGroup);
pass.SetViewport(0, 0, static_cast<float>(size.inputWidth), static_cast<float>(size.inputHeight), 0, 1);
pass.Draw(3);
pass.End();
// Submit the sealed scene and input copy before crossing to the native queue.
// The replacement encoder composites the upscaled image and ImGui normally.
auto buffer = encoder.Finish();
{
std::lock_guard submitLock(g_queueSubmitMutex);
g_queue.Submit(1, &buffer);
}
encoder = g_device.CreateCommandEncoder();
if (!slot.scaler->upscale()) {
metalfx_failed(slot.scaler->error());
return {};
}
g_metalfxPendingOutput = slot.scaler.get();
g_metalfxStatus.store(AURORA_METALFX_ACTIVE);
return slot.bindGroup;
}
// `presentSource` is latched in the seal prologue: by the time this encodes, the producer's next // `presentSource` is latched in the seal prologue: by the time this encodes, the producer's next
// gfx::begin_frame() may already have cleared the display-copy override. // gfx::begin_frame() may already have cleared the display-copy override.
void encode_presentation_snapshot(const wgpu::CommandEncoder& encoder, wgpu::BindGroup encode_presentation_snapshot(wgpu::CommandEncoder& encoder,
const webgpu::PresentSource& presentSource, const webgpu::PresentSource& presentSource,
const PresentationImage& image, const PresentationImage& image,
bool includeImGui) { bool includeImGui, bool metalfxEnabled,
const wgpu::BindGroup* cachedMetalFXOutput = nullptr) {
ZoneScoped; ZoneScoped;
auto viewport = webgpu::calculate_present_viewport( auto viewport = webgpu::calculate_present_viewport(
image.texture.size.width, image.texture.size.height, presentSource.size.width, image.texture.size.width, image.texture.size.height, presentSource.size.width,
@@ -1203,6 +1312,13 @@ void encode_presentation_snapshot(const wgpu::CommandEncoder& encoder,
image.texture.size.width, image.texture.size.height, presentAspect); image.texture.size.width, image.texture.size.height, presentAspect);
} }
wgpu::BindGroup presentBindGroup = presentSource.bindGroup; wgpu::BindGroup presentBindGroup = presentSource.bindGroup;
wgpu::BindGroup newMetalFXOutput;
if (cachedMetalFXOutput && *cachedMetalFXOutput) {
presentBindGroup = *cachedMetalFXOutput;
} else if (auto upscaled = upscale_presentation(encoder, presentSource, viewport, metalfxEnabled)) {
presentBindGroup = std::move(upscaled);
newMetalFXOutput = presentBindGroup;
}
{ {
const std::array attachments{ const std::array attachments{
wgpu::RenderPassColorAttachment{ wgpu::RenderPassColorAttachment{
@@ -1243,6 +1359,7 @@ void encode_presentation_snapshot(const wgpu::CommandEncoder& encoder,
imgui::render(pass); imgui::render(pass);
pass.End(); pass.End();
} }
return newMetalFXOutput;
} }
#endif #endif
@@ -1251,6 +1368,13 @@ void shutdown() noexcept {
#ifdef AURORA_ENABLE_GX #ifdef AURORA_ENABLE_GX
stop_presenter(); stop_presenter();
g_presentationImagePools = {}; g_presentationImagePools = {};
g_metalfxSlots = {};
g_metalfxPendingOutput = nullptr;
g_metalfxNextSlot = 0;
g_metalfxFailed = false;
g_metalfxRequested.store(false);
g_metalfxSupported.store(false);
g_metalfxStatus.store(AURORA_METALFX_DISABLED);
imgui::shutdown(); imgui::shutdown();
gfx::shutdown(); gfx::shutdown();
webgpu::shutdown(); webgpu::shutdown();
@@ -1363,6 +1487,7 @@ struct SealedFrameContext {
uint32_t logicalFrame = 0; uint32_t logicalFrame = 0;
bool interpolationActive = false; bool interpolationActive = false;
bool replayInterpolatedFrames = false; bool replayInterpolatedFrames = false;
bool metalfxEnabled = false;
}; };
// Phase 1: everything that touches producer-shared renderer state. Needs g_rendererGpuMutex and // Phase 1: everything that touches producer-shared renderer state. Needs g_rendererGpuMutex and
@@ -1390,6 +1515,7 @@ void seal_frame_locked(gfx::SealedFrame& sealedFrame, SealedFrameContext& ctx) {
ctx.snapshotWidth = (std::max)(windowSize.native_fb_width, 1u); ctx.snapshotWidth = (std::max)(windowSize.native_fb_width, 1u);
ctx.snapshotHeight = (std::max)(windowSize.native_fb_height, 1u); ctx.snapshotHeight = (std::max)(windowSize.native_fb_height, 1u);
ctx.logicalFrame = gfx::current_frame(); ctx.logicalFrame = gfx::current_frame();
ctx.metalfxEnabled = g_metalfxRequested.load();
// Latched before webgpu::clear_present_source_override() in the producer's // Latched before webgpu::clear_present_source_override() in the producer's
// next gfx::begin_frame(). // next gfx::begin_frame().
ctx.presentSource = webgpu::current_present_source(); ctx.presentSource = webgpu::current_present_source();
@@ -1437,10 +1563,14 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
const wgpu::CommandBufferDescriptor cmdBufDescriptor{ const wgpu::CommandBufferDescriptor cmdBufDescriptor{
.label = "Presentation slot command buffer", .label = "Presentation slot command buffer",
}; };
const auto submitEncodedSlot = [&](wgpu::CommandEncoder& target) { const auto submitEncodedSlot = [&](wgpu::CommandEncoder& target, bool releaseMetalFXOutput = true) {
const auto buffer = target.Finish(&cmdBufDescriptor); const auto buffer = target.Finish(&cmdBufDescriptor);
std::lock_guard submitLock(g_queueSubmitMutex); std::lock_guard submitLock(g_queueSubmitMutex);
g_queue.Submit(1, &buffer); g_queue.Submit(1, &buffer);
if (releaseMetalFXOutput && g_metalfxPendingOutput) {
if (!g_metalfxPendingOutput->end_output()) metalfx_failed(g_metalfxPendingOutput->error());
g_metalfxPendingOutput = nullptr;
}
}; };
if (ctx.replayInterpolatedFrames) { if (ctx.replayInterpolatedFrames) {
@@ -1449,7 +1579,7 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
gfx::render(sealedFrame, encoder, static_cast<int32_t>(interpolatedFrame), false); gfx::render(sealedFrame, encoder, static_cast<int32_t>(interpolatedFrame), false);
auto image = auto image =
acquire_presentation_image(interpolatedFrame, ctx.snapshotWidth, ctx.snapshotHeight); acquire_presentation_image(interpolatedFrame, ctx.snapshotWidth, ctx.snapshotHeight);
encode_presentation_snapshot(encoder, ctx.presentSource, *image, true); encode_presentation_snapshot(encoder, ctx.presentSource, *image, true, ctx.metalfxEnabled);
presentationJobs.push_back({ presentationJobs.push_back({
.image = std::move(image), .image = std::move(image),
.logicalFrame = ctx.logicalFrame, .logicalFrame = ctx.logicalFrame,
@@ -1467,12 +1597,18 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
// The copy targets now hold this frame's resolves, so queue their readbacks on the same encoder; // The copy targets now hold this frame's resolves, so queue their readbacks on the same encoder;
// completion is harvested in gfx::after_submit, never waited on here. // completion is harvested in gfx::after_submit, never waited on here.
gfx::efb_ram::encode_async_downloads(encoder); gfx::efb_ram::encode_async_downloads(encoder);
wgpu::BindGroup duplicatedMetalFXOutput;
if (!ctx.replayInterpolatedFrames) { if (!ctx.replayInterpolatedFrames) {
for (uint32_t interpolatedFrame = 0; interpolatedFrame < ctx.interpolatedFrameCount; for (uint32_t interpolatedFrame = 0; interpolatedFrame < ctx.interpolatedFrameCount;
++interpolatedFrame) { ++interpolatedFrame) {
auto image = auto image =
acquire_presentation_image(interpolatedFrame, ctx.snapshotWidth, ctx.snapshotHeight); acquire_presentation_image(interpolatedFrame, ctx.snapshotWidth, ctx.snapshotHeight);
encode_presentation_snapshot(encoder, ctx.presentSource, *image, true); const auto newMetalFXOutput = encode_presentation_snapshot(
encoder, ctx.presentSource, *image, true, ctx.metalfxEnabled,
duplicatedMetalFXOutput ? &duplicatedMetalFXOutput : nullptr);
if (!duplicatedMetalFXOutput && newMetalFXOutput) {
duplicatedMetalFXOutput = newMetalFXOutput;
}
presentationJobs.push_back({ presentationJobs.push_back({
.image = std::move(image), .image = std::move(image),
.logicalFrame = ctx.logicalFrame, .logicalFrame = ctx.logicalFrame,
@@ -1480,13 +1616,14 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
.interpolated = true, .interpolated = true,
.duplicated = true, .duplicated = true,
}); });
submitEncodedSlot(encoder); submitEncodedSlot(encoder, false);
encoder = g_device.CreateCommandEncoder(&encoderDescriptor); encoder = g_device.CreateCommandEncoder(&encoderDescriptor);
} }
} }
auto finalImage = auto finalImage =
acquire_presentation_image(ctx.interpolatedFrameCount, ctx.snapshotWidth, ctx.snapshotHeight); acquire_presentation_image(ctx.interpolatedFrameCount, ctx.snapshotWidth, ctx.snapshotHeight);
encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true); encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true, ctx.metalfxEnabled,
duplicatedMetalFXOutput ? &duplicatedMetalFXOutput : nullptr);
auto pendingFrameCapture = encode_frame_capture(encoder, ctx.presentSource); auto pendingFrameCapture = encode_frame_capture(encoder, ctx.presentSource);
presentationJobs.push_back({ presentationJobs.push_back({
.image = std::move(finalImage), .image = std::move(finalImage),
@@ -1962,3 +2099,7 @@ void aurora_set_background_input(bool value) {
} }
void aurora_set_display_mode(AuroraDisplayMode mode) { aurora::window::set_display_mode(mode); } void aurora_set_display_mode(AuroraDisplayMode mode) { aurora::window::set_display_mode(mode); }
AuroraDisplayMode aurora_get_display_mode() { return aurora::window::get_display_mode(); } AuroraDisplayMode aurora_get_display_mode() { return aurora::window::get_display_mode(); }
void aurora_set_metalfx_spatial(bool enabled) { aurora::g_metalfxRequested.store(enabled); }
bool aurora_get_metalfx_spatial() { return aurora::g_metalfxRequested.load(); }
bool aurora_is_metalfx_spatial_supported() { return aurora::g_metalfxSupported.load(); }
AuroraMetalFXStatus aurora_get_metalfx_status() { return aurora::g_metalfxStatus.load(); }
+30 -24
View File
@@ -6,10 +6,10 @@
#include <SDL3/SDL_mouse.h> #include <SDL3/SDL_mouse.h>
#include <SDL3/SDL_joystick.h> #include <SDL3/SDL_joystick.h>
#include <algorithm>
#include <array> #include <array>
#include <atomic> #include <atomic>
#include <sys/stat.h> #include <sys/stat.h>
#include <ranges>
namespace { namespace {
constexpr int32_t k_mappingsFileVersion = 3; constexpr int32_t k_mappingsFileVersion = 3;
@@ -354,7 +354,7 @@ BOOL PADInit() {
} }
g_initialized = true; g_initialized = true;
std::ranges::for_each(g_keyboardBindings, [](auto& state) { std::for_each(g_keyboardBindings.begin(), g_keyboardBindings.end(), [](auto& state) {
state.m_buttonMapping = g_defaultKeys; state.m_buttonMapping = g_defaultKeys;
state.m_axisMapping = g_defaultKeyAxis; state.m_axisMapping = g_defaultKeyAxis;
}); });
@@ -647,8 +647,8 @@ static void EnsureMappingLoaded(aurora::input::GameController* controller) {
static Sint16 _get_axis_value(const aurora::input::GameController* controller, // NOLINT(*-reserved-identifier) static Sint16 _get_axis_value(const aurora::input::GameController* controller, // NOLINT(*-reserved-identifier)
PADAxis axis) { PADAxis axis) {
const auto iter = const auto iter = std::find_if(controller->m_axisMapping.begin(), controller->m_axisMapping.end(),
std::ranges::find_if(controller->m_axisMapping, [axis](const auto& pair) { return pair.padAxis == axis; }); [axis](const auto& pair) { return pair.padAxis == axis; });
if (iter == controller->m_axisMapping.end()) { if (iter == controller->m_axisMapping.end()) {
return 0; return 0;
} }
@@ -738,8 +738,8 @@ u32 PADRead(PADStatus* status) {
status[i].err = PAD_ERR_NONE; status[i].err = PAD_ERR_NONE;
if (g_keyboardBindings[i].m_mappingsSet && SDL_GetKeyboardFocus() != nullptr) { if (g_keyboardBindings[i].m_mappingsSet && SDL_GetKeyboardFocus() != nullptr) {
std::ranges::for_each( std::for_each(g_keyboardBindings[i].m_buttonMapping.begin(), g_keyboardBindings[i].m_buttonMapping.end(),
g_keyboardBindings[i].m_buttonMapping, [&kbState, &numKeys, &i, &status](const PADKeyButtonBinding& mapping) { [&kbState, &numKeys, &i, &status](const PADKeyButtonBinding& mapping) {
if (mapping.scancode > PAD_KEY_INVALID && mapping.scancode < numKeys && kbState[mapping.scancode]) { if (mapping.scancode > PAD_KEY_INVALID && mapping.scancode < numKeys && kbState[mapping.scancode]) {
status[i].button |= mapping.padButton; status[i].button |= mapping.padButton;
} else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) { } else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) {
@@ -846,8 +846,8 @@ u32 PADRead(PADStatus* status) {
bool leftTriggerSet = false; bool leftTriggerSet = false;
bool rightTriggerSet = false; bool rightTriggerSet = false;
std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet, std::for_each(controller->m_buttonMapping.begin(), controller->m_buttonMapping.end(),
&rightTriggerSet](const auto& mapping) { [&controller, &i, &status, &leftTriggerSet, &rightTriggerSet](const auto& mapping) {
if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) { if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) {
status[i].button |= mapping.padButton; status[i].button |= mapping.padButton;
} }
@@ -860,8 +860,8 @@ u32 PADRead(PADStatus* status) {
} }
}); });
std::ranges::for_each(controller->m_altButtonMapping, [&controller, &i, &status, &leftTriggerSet, std::for_each(controller->m_altButtonMapping.begin(), controller->m_altButtonMapping.end(),
&rightTriggerSet](const auto& mapping) { [&controller, &i, &status, &leftTriggerSet, &rightTriggerSet](const auto& mapping) {
if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) { if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) {
return; return;
} }
@@ -1190,8 +1190,8 @@ void PADSetButtonMapping(const u32 port, const PADButtonMapping mapping) {
return; return;
} }
const auto iter = std::ranges::find_if(controller->m_buttonMapping, const auto iter = std::find_if(controller->m_buttonMapping.begin(), controller->m_buttonMapping.end(),
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; }); [mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
if (iter == controller->m_buttonMapping.end()) { if (iter == controller->m_buttonMapping.end()) {
return; return;
} }
@@ -1224,8 +1224,8 @@ void PADSetAltButtonMapping(const u32 port, const PADButtonMapping mapping) {
return; return;
} }
const auto iter = std::ranges::find_if(controller->m_altButtonMapping, const auto iter = std::find_if(controller->m_altButtonMapping.begin(), controller->m_altButtonMapping.end(),
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; }); [mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
if (iter == controller->m_altButtonMapping.end()) { if (iter == controller->m_altButtonMapping.end()) {
return; return;
} }
@@ -1251,8 +1251,8 @@ void PADSetAxisMapping(const u32 port, const PADAxisMapping mapping) {
return; return;
} }
const auto iter = std::ranges::find_if(controller->m_axisMapping, const auto iter = std::find_if(controller->m_axisMapping.begin(), controller->m_axisMapping.end(),
[mapping](const auto& pair) { return mapping.padAxis == pair.padAxis; }); [mapping](const auto& pair) { return mapping.padAxis == pair.padAxis; });
if (iter == controller->m_axisMapping.end()) { if (iter == controller->m_axisMapping.end()) {
return; return;
} }
@@ -1426,9 +1426,10 @@ static void load_keyboard_bindings() {
if (mappingsSet) { if (mappingsSet) {
const bool anyBound = const bool anyBound =
std::ranges::any_of(buttonMapping, std::any_of(buttonMapping.begin(), buttonMapping.end(),
[](const PADKeyButtonBinding& b) { return b.scancode != PAD_KEY_INVALID; }) || [](const PADKeyButtonBinding& b) { return b.scancode != PAD_KEY_INVALID; }) ||
std::ranges::any_of(axisMapping, [](const PADKeyAxisBinding& b) { return b.scancode != PAD_KEY_INVALID; }); std::any_of(axisMapping.begin(), axisMapping.end(),
[](const PADKeyAxisBinding& b) { return b.scancode != PAD_KEY_INVALID; });
if (!anyBound) { if (!anyBound) {
mappingsSet = false; mappingsSet = false;
} }
@@ -1468,7 +1469,10 @@ void __PADWriteDeadZones(SDL_IOStream* file, // NOLINT(*-reserved-identifier)
void PADSerializeMappings() { void PADSerializeMappings() {
const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath); const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath);
for (auto& controller : aurora::input::g_GameControllers | std::views::values) { // Avoid std::views::values here: older Apple libc++ releases implement the
// C++20 ranges algorithms we use but not this adaptor.
for (auto& entry : aurora::input::g_GameControllers) {
auto& controller = entry.second;
EnsureMappingLoaded(&controller); EnsureMappingLoaded(&controller);
const auto filePath = const auto filePath =
basePath / fmt::format("{}_{:04X}_{:04X}.controller", aurora::input::controller_name(controller.m_index), basePath / fmt::format("{}_{:04X}_{:04X}.controller", aurora::input::controller_name(controller.m_index),
@@ -1570,8 +1574,8 @@ static constexpr std::array<std::pair<PADButton, std::string_view>, PAD_AXIS_COU
const char* PADGetButtonName(const PADButton button) { const char* PADGetButtonName(const PADButton button) {
if (const auto iter = if (const auto iter = std::find_if(skButtonNames.begin(), skButtonNames.end(),
std::ranges::find_if(skButtonNames, [&button](const auto& pair) { return button == pair.first; }); [&button](const auto& pair) { return button == pair.first; });
iter != skButtonNames.end()) { iter != skButtonNames.end()) {
return iter->second.data(); return iter->second.data();
} }
@@ -1584,7 +1588,8 @@ const char* PADGetNativeButtonName(u32 button) {
} }
const char* PADGetAxisName(const PADAxis axis) { const char* PADGetAxisName(const PADAxis axis) {
if (const auto it = std::ranges::find_if(skAxisNames, [&axis](const auto& pair) { return axis == pair.first; }); if (const auto it = std::find_if(skAxisNames.begin(), skAxisNames.end(),
[&axis](const auto& pair) { return axis == pair.first; });
it != skAxisNames.end()) { it != skAxisNames.end()) {
return it->second.data(); return it->second.data();
} }
@@ -1593,7 +1598,8 @@ const char* PADGetAxisName(const PADAxis axis) {
} }
const char* PADGetAxisDirectionLabel(const PADAxis axis) { const char* PADGetAxisDirectionLabel(const PADAxis axis) {
if (const auto it = std::ranges::find_if(skAxisDirLabels, [&axis](const auto& pair) { return axis == pair.first; }); if (const auto it = std::find_if(skAxisDirLabels.begin(), skAxisDirLabels.end(),
[&axis](const auto& pair) { return axis == pair.first; });
it != skAxisDirLabels.end()) { it != skAxisDirLabels.end()) {
return it->second.data(); return it->second.data();
} }
+2 -3
View File
@@ -23,7 +23,6 @@
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <optional> #include <optional>
#include <ranges>
#include <absl/container/flat_hash_map.h> #include <absl/container/flat_hash_map.h>
#include <magic_enum.hpp> #include <magic_enum.hpp>
@@ -1448,8 +1447,8 @@ static void render_impl(std::vector<RenderPass>& renderPasses, wgpu::CommandEnco
#if defined(AURORA_GFX_DEBUG_GROUPS) #if defined(AURORA_GFX_DEBUG_GROUPS)
if (finalize && !debugFrame.groups.empty()) { if (finalize && !debugFrame.groups.empty()) {
for (auto& it : std::ranges::reverse_view(debugFrame.groups)) { for (auto it = debugFrame.groups.rbegin(); it != debugFrame.groups.rend(); ++it) {
Log.warn("Debug group was not popped at end of frame: {}", it); Log.warn("Debug group was not popped at end of frame: {}", *it);
} }
debugFrame.groups.clear(); debugFrame.groups.clear();
} }
+1 -1
View File
@@ -235,7 +235,7 @@ std::string GetOSVersion() {
constexpr auto name = "iOS"; constexpr auto name = "iOS";
#elif TARGET_OS_TV #elif TARGET_OS_TV
constexpr auto name = "tvOS"; constexpr auto name = "tvOS";
#elif #else
constexpr auto name = Unknown; constexpr auto name = Unknown;
#endif #endif
+8
View File
@@ -642,6 +642,14 @@ bool initialize(AuroraBackend auroraBackend) {
requiredLimits.maxDynamicStorageBuffersPerPipelineLayout, requiredLimits.maxStorageBuffersPerShaderStage, requiredLimits.maxDynamicStorageBuffersPerPipelineLayout, requiredLimits.maxStorageBuffersPerShaderStage,
requiredLimits.minUniformBufferOffsetAlignment, requiredLimits.minStorageBufferOffsetAlignment); requiredLimits.minUniformBufferOffsetAlignment, requiredLimits.minStorageBufferOffsetAlignment);
std::vector<wgpu::FeatureName> requiredFeatures; std::vector<wgpu::FeatureName> requiredFeatures;
// Optional native sharing for MetalFX. Devices without either feature keep
// the normal renderer; the upscaler checks the enabled pair at runtime.
if (backend == wgpu::BackendType::Metal &&
g_adapter.HasFeature(wgpu::FeatureName::SharedTextureMemoryIOSurface) &&
g_adapter.HasFeature(wgpu::FeatureName::SharedFenceMTLSharedEvent)) {
requiredFeatures.push_back(wgpu::FeatureName::SharedTextureMemoryIOSurface);
requiredFeatures.push_back(wgpu::FeatureName::SharedFenceMTLSharedEvent);
}
bool implicitDeviceSynchronizationSupported = false; bool implicitDeviceSynchronizationSupported = false;
wgpu::SupportedFeatures supportedFeatures; wgpu::SupportedFeatures supportedFeatures;
g_adapter.GetFeatures(&supportedFeatures); g_adapter.GetFeatures(&supportedFeatures);
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <webgpu/webgpu_cpp.h>
#include <memory>
#include <string>
namespace aurora::webgpu::metalfx {
struct Size {
uint32_t inputWidth;
uint32_t inputHeight;
uint32_t outputWidth;
uint32_t outputHeight;
wgpu::TextureFormat format;
bool operator==(const Size&) const = default;
};
// All methods except supported() belong to the serialized frame encoder.
// GPU ownership is explicit: begin_input -> submit input -> upscale ->
// submit output consumption -> end_output. Neither texture may be used by
// Dawn outside its access interval. Destruction retires in-flight resources.
class SpatialScaler {
public:
virtual ~SpatialScaler() = default;
virtual const wgpu::TextureView& input_view() const = 0;
virtual const wgpu::TextureView& output_view() const = 0;
virtual const wgpu::Texture& output_texture() const = 0;
virtual bool begin_input() = 0;
virtual bool upscale() = 0;
virtual bool end_output() = 0;
virtual const std::string& error() const = 0;
};
bool supported(const wgpu::Device& device, wgpu::BackendType backend);
// A null result with no error means the bounded retirement pool is busy;
// skip upscaling for this frame and retry at a later frame boundary.
std::unique_ptr<SpatialScaler> create(const wgpu::Instance& instance,
const wgpu::Device& device, const Size& size,
std::string& error);
} // namespace aurora::webgpu::metalfx
+326
View File
@@ -0,0 +1,326 @@
#include "metalfx.hpp"
#import <Foundation/Foundation.h>
#import <IOSurface/IOSurface.h>
#import <Metal/Metal.h>
#import <MetalFX/MetalFX.h>
#include <dawn/native/MetalBackend.h>
#include <atomic>
#include <string_view>
namespace aurora::webgpu::metalfx {
namespace {
constexpr uint64_t kScheduleTimeoutNs = 1'000'000'000;
// Four current slots plus at most four retiring slots during resize. A busy
// GPU must not allow resize events to allocate unbounded full-resolution images.
constexpr unsigned kMaxLiveResources = 8;
std::atomic<unsigned> g_liveResources{0};
struct SharedImage {
IOSurfaceRef surface = nullptr;
id<MTLTexture> metal;
wgpu::SharedTextureMemory memory;
wgpu::Texture texture;
wgpu::TextureView view;
~SharedImage() { if (surface) CFRelease(surface); }
bool create(const wgpu::Device& device, id<MTLDevice> native, uint32_t width,
uint32_t height, wgpu::TextureFormat format, MTLTextureUsage nativeUsage,
wgpu::TextureUsage usage) {
const bool bgra = format == wgpu::TextureFormat::BGRA8Unorm;
const size_t rowBytes = IOSurfaceAlignProperty(kIOSurfaceBytesPerRow, size_t(width) * 4);
NSDictionary* properties = @{
(id)kIOSurfaceWidth: @(width), (id)kIOSurfaceHeight: @(height),
(id)kIOSurfaceBytesPerElement: @4, (id)kIOSurfaceBytesPerRow: @(rowBytes),
(id)kIOSurfaceAllocSize: @(rowBytes * height),
(id)kIOSurfacePixelFormat: @(bgra ? 0x42475241u : 0x52474241u)
};
surface = IOSurfaceCreate((__bridge CFDictionaryRef)properties);
if (!surface) return false;
auto descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:
bgra ? MTLPixelFormatBGRA8Unorm : MTLPixelFormatRGBA8Unorm
width:width height:height mipmapped:NO];
descriptor.storageMode = MTLStorageModeShared;
descriptor.usage = nativeUsage;
metal = [native newTextureWithDescriptor:descriptor iosurface:surface plane:0];
if (!metal) return false;
wgpu::SharedTextureMemoryIOSurfaceDescriptor io{};
io.ioSurface = surface;
io.allowStorageBinding = false;
wgpu::SharedTextureMemoryDescriptor importDescriptor{};
importDescriptor.nextInChain = &io;
memory = device.ImportSharedTextureMemory(&importDescriptor);
wgpu::SharedTextureMemoryProperties actual{};
if (!memory || memory.GetProperties(&actual) != wgpu::Status::Success ||
actual.format != format || actual.size.width != width || actual.size.height != height ||
(actual.usage & usage) != usage) return false;
wgpu::TextureDescriptor textureDescriptor{};
textureDescriptor.label = "MetalFX shared texture";
textureDescriptor.size = {width, height, 1};
textureDescriptor.format = format;
textureDescriptor.usage = usage;
texture = memory.CreateTexture(&textureDescriptor);
if (!texture) return false;
view = texture.CreateView();
return view != nullptr;
}
};
struct API_AVAILABLE(macos(13.0)) Resources {
SharedImage input, output;
id<MTLFXSpatialScaler> scaler;
id<MTLTexture> privateOutput;
id<MTLCommandQueue> nativeQueue;
id<MTLSharedEvent> event;
wgpu::SharedFence fence;
std::atomic<bool> failed{false};
Resources() { ++g_liveResources; }
~Resources() { --g_liveResources; }
};
class API_AVAILABLE(macos(13.0)) MetalSpatialScaler final : public SpatialScaler {
wgpu::Instance m_instance;
wgpu::Queue m_queue;
std::shared_ptr<Resources> m_resources;
wgpu::SharedTextureMemoryEndAccessState m_outputReleased{};
wgpu::Future m_outputScheduled{};
uint64_t m_value = 0;
std::string m_error;
bool fail(const char* reason) {
m_error = reason;
m_resources->failed = true;
return false;
}
bool wait_scheduled(wgpu::Future future) {
return m_instance.WaitAny(future, kScheduleTimeoutNs) == wgpu::WaitStatus::Success ||
fail("Timed out scheduling MetalFX GPU work");
}
bool end_access(SharedImage& image, wgpu::SharedTextureMemoryEndAccessState& state,
wgpu::Future& scheduled) {
wgpu::SharedTextureMemoryMetalEndAccessState metal{};
state.nextInChain = &metal;
const auto status = image.memory.EndAccess(image.texture, &state);
state.nextInChain = nullptr;
scheduled = metal.commandsScheduledFuture;
return status == wgpu::Status::Success || fail("MetalFX Dawn EndAccess failed");
}
bool begin_access(SharedImage& image, bool initialized, uint64_t value) {
wgpu::SharedTextureMemoryBeginAccessDescriptor access{};
access.initialized = initialized;
if (value) {
access.fenceCount = 1;
access.fences = &m_resources->fence;
access.signaledValueCount = 1;
access.signaledValues = &value;
}
return image.memory.BeginAccess(image.texture, &access) == wgpu::Status::Success ||
fail("MetalFX Dawn BeginAccess failed");
}
bool encode_waits(id<MTLCommandBuffer> commands,
const wgpu::SharedTextureMemoryEndAccessState& state) {
for (size_t i = 0; i < state.fenceCount; ++i) {
wgpu::SharedFenceMTLSharedEventExportInfo metal{};
wgpu::SharedFenceExportInfo info{};
info.nextInChain = &metal;
state.fences[i].ExportInfo(&info);
if (info.type != wgpu::SharedFenceType::MTLSharedEvent || !metal.sharedEvent)
return fail("Dawn did not export a MetalFX shared-event dependency");
[commands encodeWaitForEvent:(__bridge id<MTLSharedEvent>)metal.sharedEvent
value:state.signaledValues[i]];
}
return true;
}
void retain_until_dawn_done() {
// A resize/toggle can destroy this wrapper immediately. The last submitted
// Dawn consumer keeps the IOSurfaces/scaler alive independently of the cache.
m_queue.OnSubmittedWorkDone(wgpu::CallbackMode::AllowSpontaneous,
[resources = m_resources](wgpu::QueueWorkDoneStatus status, wgpu::StringView) {
if (status != wgpu::QueueWorkDoneStatus::Success) resources->failed = true;
});
}
public:
MetalSpatialScaler(const wgpu::Instance& instance, const wgpu::Device& device,
std::shared_ptr<Resources> resources)
: m_instance(instance), m_queue(device.GetQueue()), m_resources(std::move(resources)) {}
const wgpu::TextureView& input_view() const override { return m_resources->input.view; }
const wgpu::TextureView& output_view() const override { return m_resources->output.view; }
const wgpu::Texture& output_texture() const override { return m_resources->output.texture; }
const std::string& error() const override { return m_error; }
bool begin_input() override {
if (m_resources->failed) return fail("Previous MetalFX GPU work failed");
return begin_access(m_resources->input, m_value != 0, m_value);
}
bool upscale() override {
@autoreleasepool {
// Input has already been submitted. Retain it even if an export or native
// allocation fails and the caller immediately falls back to normal copy.
retain_until_dawn_done();
wgpu::SharedTextureMemoryEndAccessState inputReleased{};
wgpu::Future inputScheduled{};
if (!end_access(m_resources->input, inputReleased, inputScheduled) ||
!wait_scheduled(inputScheduled)) return false;
if (m_value && !wait_scheduled(m_outputScheduled)) return false;
id<MTLCommandBuffer> commands = [m_resources->nativeQueue commandBuffer];
if (!commands) return fail("Could not allocate a MetalFX command buffer");
commands.label = @"MetalFX spatial upscale and return to Dawn";
if (!encode_waits(commands, inputReleased) || !encode_waits(commands, m_outputReleased))
return false;
[m_resources->scaler encodeToCommandBuffer:commands];
id<MTLBlitCommandEncoder> blit = [commands blitCommandEncoder];
if (!blit) return fail("Could not allocate the MetalFX output blit");
[blit copyFromTexture:m_resources->privateOutput sourceSlice:0 sourceLevel:0
sourceOrigin:MTLOriginMake(0, 0, 0)
sourceSize:MTLSizeMake(m_resources->privateOutput.width, m_resources->privateOutput.height, 1)
toTexture:m_resources->output.metal destinationSlice:0 destinationLevel:0
destinationOrigin:MTLOriginMake(0, 0, 0)];
[blit endEncoding];
++m_value;
[commands encodeSignalEvent:m_resources->event value:m_value];
const auto resources = m_resources;
[commands addCompletedHandler:^(id<MTLCommandBuffer> completed) {
if (completed.status == MTLCommandBufferStatusError) resources->failed = true;
}];
[commands commit];
// Scheduling is required to order independent Metal queues. Completion
// remains asynchronous; resource reuse is guarded by shared GPU events.
[commands waitUntilScheduled];
if (commands.status == MTLCommandBufferStatusError)
return fail("MetalFX command buffer failed");
return begin_access(m_resources->output, true, m_value);
}
}
bool end_output() override {
retain_until_dawn_done();
m_outputReleased = {};
return end_access(m_resources->output, m_outputReleased, m_outputScheduled);
}
};
// Allocation failures must be caught here rather than reaching Aurora's fatal
// uncaptured-error callback. Scope callbacks own their strings even on timeout.
bool pop_scope(const wgpu::Instance& instance, const wgpu::Device& device, std::string& error) {
auto message = std::make_shared<std::string>();
auto future = device.PopErrorScope(wgpu::CallbackMode::WaitAnyOnly,
[message](wgpu::PopErrorScopeStatus status, wgpu::ErrorType type, wgpu::StringView text) {
if (status != wgpu::PopErrorScopeStatus::Success || type != wgpu::ErrorType::NoError) {
const std::string_view detail{text};
*message = detail.empty() ? "MetalFX texture allocation failed" : std::string(detail);
}
});
if (instance.WaitAny(future, kScheduleTimeoutNs) != wgpu::WaitStatus::Success) {
error = "Timed out checking MetalFX texture allocation";
return false;
}
if (!message->empty()) { error = *message; return false; }
return true;
}
} // namespace
bool supported(const wgpu::Device& device, wgpu::BackendType backend) {
if (@available(macOS 13.0, *)) {
if (!device || backend != wgpu::BackendType::Metal ||
!device.HasFeature(wgpu::FeatureName::SharedTextureMemoryIOSurface) ||
!device.HasFeature(wgpu::FeatureName::SharedFenceMTLSharedEvent)) return false;
auto native = dawn::native::metal::GetMTLDevice(device.Get());
return native && [MTLFXSpatialScalerDescriptor supportsDevice:native];
}
return false;
}
std::unique_ptr<SpatialScaler> create(const wgpu::Instance& instance,
const wgpu::Device& device, const Size& size,
std::string& error) {
error.clear();
if (@available(macOS 13.0, *)) {
@autoreleasepool {
if (!supported(device, wgpu::BackendType::Metal)) {
error = "MetalFX spatial scaling is unsupported";
return {};
}
wgpu::Limits limits{};
device.GetLimits(&limits);
if (!size.inputWidth || !size.inputHeight || size.inputWidth >= size.outputWidth ||
size.inputHeight >= size.outputHeight || size.outputWidth > limits.maxTextureDimension2D ||
size.outputHeight > limits.maxTextureDimension2D ||
(size.format != wgpu::TextureFormat::RGBA8Unorm && size.format != wgpu::TextureFormat::BGRA8Unorm)) {
error = "MetalFX requires smaller input dimensions and an RGBA8/BGRA8 unorm target";
return {};
}
if (g_liveResources.load() >= kMaxLiveResources) {
return {};
}
auto native = dawn::native::metal::GetMTLDevice(device.Get());
auto resources = std::make_shared<Resources>();
auto descriptor = [MTLFXSpatialScalerDescriptor new];
descriptor.inputWidth = size.inputWidth;
descriptor.inputHeight = size.inputHeight;
descriptor.outputWidth = size.outputWidth;
descriptor.outputHeight = size.outputHeight;
descriptor.colorTextureFormat = size.format == wgpu::TextureFormat::BGRA8Unorm
? MTLPixelFormatBGRA8Unorm : MTLPixelFormatRGBA8Unorm;
descriptor.outputTextureFormat = descriptor.colorTextureFormat;
descriptor.colorProcessingMode = MTLFXSpatialScalerColorProcessingModePerceptual;
resources->scaler = [descriptor newSpatialScalerWithDevice:native];
resources->nativeQueue = [native newCommandQueue];
resources->event = [native newSharedEvent];
if (!resources->scaler || !resources->nativeQueue || !resources->event) {
error = "Could not create MetalFX spatial resources";
return {};
}
auto outputDescriptor = [MTLTextureDescriptor
texture2DDescriptorWithPixelFormat:descriptor.outputTextureFormat
width:size.outputWidth height:size.outputHeight mipmapped:NO];
outputDescriptor.storageMode = MTLStorageModePrivate;
outputDescriptor.usage = resources->scaler.outputTextureUsage;
resources->privateOutput = [native newTextureWithDescriptor:outputDescriptor];
if (!resources->privateOutput) { error = "Could not allocate MetalFX private output"; return {}; }
device.PushErrorScope(wgpu::ErrorFilter::Validation);
device.PushErrorScope(wgpu::ErrorFilter::OutOfMemory);
device.PushErrorScope(wgpu::ErrorFilter::Internal);
bool allocated = resources->input.create(device, native, size.inputWidth, size.inputHeight,
size.format, resources->scaler.colorTextureUsage, wgpu::TextureUsage::RenderAttachment);
allocated = allocated && resources->output.create(device, native, size.outputWidth, size.outputHeight,
size.format, MTLTextureUsageShaderRead, wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc);
if (allocated) {
wgpu::SharedFenceMTLSharedEventDescriptor event{};
event.sharedEvent = (__bridge void*)resources->event;
wgpu::SharedFenceDescriptor fence{};
fence.nextInChain = &event;
resources->fence = device.ImportSharedFence(&fence);
allocated = resources->fence != nullptr;
}
for (int i = 0; i < 3; ++i) {
if (!pop_scope(instance, device, error)) allocated = false;
}
if (!allocated) {
if (error.empty()) error = "Could not import MetalFX IOSurface textures into Dawn";
return {};
}
resources->scaler.colorTexture = resources->input.metal;
resources->scaler.outputTexture = resources->privateOutput;
resources->scaler.inputContentWidth = size.inputWidth;
resources->scaler.inputContentHeight = size.inputHeight;
return std::make_unique<MetalSpatialScaler>(instance, device, std::move(resources));
}
}
error = "MetalFX requires macOS 13 or newer";
return {};
}
} // namespace aurora::webgpu::metalfx
+11
View File
@@ -0,0 +1,11 @@
#include "metalfx.hpp"
namespace aurora::webgpu::metalfx {
bool supported(const wgpu::Device&, wgpu::BackendType) { return false; }
std::unique_ptr<SpatialScaler> create(const wgpu::Instance&, const wgpu::Device&,
const Size&, std::string& error) {
error = "MetalFX is not available in this build";
return {};
}
} // namespace aurora::webgpu::metalfx
@@ -0,0 +1,28 @@
cmake_minimum_required(VERSION 3.25)
project(metalfx_interop LANGUAGES CXX)
if(NOT APPLE)
message(FATAL_ERROR "The MetalFX interoperability probe requires macOS")
endif()
enable_language(OBJCXX)
set(CMAKE_OBJCXX_STANDARD 20)
set(CMAKE_OBJCXX_STANDARD_REQUIRED ON)
set(CMAKE_OSX_DEPLOYMENT_TARGET 13.0)
# Use the same Dawn package as Aurora; no separate download or renderer build.
find_package(Threads REQUIRED)
find_package(Dawn CONFIG REQUIRED)
add_executable(metalfx_interop main.mm ../../lib/webgpu/metalfx.mm)
target_include_directories(metalfx_interop PRIVATE ../../lib)
target_compile_options(metalfx_interop PRIVATE -fobjc-arc -Wall -Wextra)
target_link_libraries(metalfx_interop PRIVATE dawn::webgpu_dawn
"-framework MetalFX" "-framework Metal" "-framework IOSurface" "-framework Foundation")
enable_testing()
add_test(NAME metalfx_interop COMMAND metalfx_interop)
set_tests_properties(metalfx_interop PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
add_executable(metalfx_stub_test stub_test.cpp ../../lib/webgpu/metalfx_stub.cpp)
target_include_directories(metalfx_stub_test PRIVATE ../../lib)
target_compile_features(metalfx_stub_test PRIVATE cxx_std_20)
target_link_libraries(metalfx_stub_test PRIVATE dawn::webgpu_dawn)
add_test(NAME metalfx_stub COMMAND metalfx_stub_test)
+124
View File
@@ -0,0 +1,124 @@
# MetalFX spatial upscaling: renderer integration and tests
Aurora can now upscale the completed game image with MetalFX before aspect-fit
presentation and ImGui composition. It is opt-in and requires macOS 13+, a
supported Metal device, and Dawn IOSurface/shared-event support. Other backends
and builds without the MetalFX SDK use a stub and the existing presentation path.
The MetalFX framework is weak-linked; the game's deployment target is unchanged.
## Trying the renderer integration
Use F10 → Graphics → MetalFX spatial upscaling. Then use the existing
Resolution control to render below the output viewport's size.
Both source dimensions must be smaller than the output dimensions. Equal-size
rendering, supersampling, and unsupported source formats bypass MetalFX. In
particular, Auto (window size) generally offers no upscaling opportunity.
The F10 toggle is saved in `Config.toml` as
`video.metalfx_spatial_upscaling`. It uses these thread-safe Aurora entry points:
- `aurora_set_metalfx_spatial(bool)` requests a change at the next sealed frame.
- `aurora_get_metalfx_spatial()` returns the requested setting.
- `aurora_is_metalfx_spatial_supported()` reports device/build support.
- `aurora_get_metalfx_status()` distinguishes Disabled, Unsupported,
Not Upscaling, Active, and Error. A busy resize-retirement pool temporarily
bypasses upscaling and retries on a later frame. Other upscaler errors log a
reason and use normal presentation until a disabled frame resets the error.
The game's HUD is part of the source image and is upscaled. ImGui/F10/FPS overlays
are composed afterward at output resolution. Existing source-frame captures
still capture the original source image. The interpolation snapshot call sites
all use the same upscaling hook; game-specific interpolation remains untested.
## GPU path and ownership
1. Request Dawn's `SharedTextureMemoryIOSurface` and `SharedFenceMTLSharedEvent`
features when the Metal adapter supports both. Use that Dawn device's native
`MTLDevice`, not a separately selected default device.
2. Cache MaxInterpolatedFrames + 1 upscaling slots with IOSurface-backed input and output textures,
a spatial scaler, a private MetalFX output, and shared-event dependencies.
Check texture formats, dimensions, usages, and device size limits on creation.
3. Begin Dawn input access, copy the completed game image at its source size,
and submit the scene plus copy. End input access and wait for Dawn's
`commandsScheduledFuture` before submitting dependent native Metal work.
4. On the native queue, wait for input rendering and any prior Dawn consumption
of the shared output. Encode MetalFX into its required **private** output
texture, then GPU-blit the result into the output IOSurface and signal an event.
5. After native scheduling, begin Dawn output access with that event/value.
Composite the upscaled image into the existing content viewport, retaining
letterboxing, then draw ImGui. Submit and end output access. Reuse observes
both Dawn-to-Metal and Metal-to-Dawn event dependencies.
6. GPU completion callbacks retain resources after a cache entry is replaced,
disabled, or shut down. At most eight resource sets may exist (four current
plus four retiring); rapid resizing cannot allocate an unbounded queue.
CPU scheduling waits remain, but there are no CPU image transfers or per-frame
GPU-completion waits in the upscaling path. There is one source-size GPU copy
and one full-output GPU blit. Their cost must be measured before promising a
performance gain. The input copy follows the existing perceptual/unorm sampling
path. sRGB texture formats bypass MetalFX to avoid implicit color conversion.
## Standalone GPU regression test
The test builds the actual `lib/webgpu/metalfx.mm` implementation. Point
`Dawn_DIR` at the package used by an existing Aurora build:
```sh
cmake -S aurora-main/tests/metalfx_interop -B build-metalfx-interop \
-DDawn_DIR="/absolute/path/to/dawn_prebuilt-src/lib/cmake/Dawn" \
-DCMAKE_BUILD_TYPE=Release
cmake --build build-metalfx-interop
MTL_DEBUG_LAYER=1 MTL_SHADER_VALIDATION=1 \
ctest --test-dir build-metalfx-interop --output-on-failure -V
```
The GPU test returns 77 (CTest **Skipped**) when no Metal adapter, required
sharing features, or spatial scaler is available. A skip is not evidence of
interoperability. Sandboxed processes may need GPU access. CTest imposes a
60-second timeout. The separate stub test needs no GPU.
Tested on Apple M3, macOS 26.5.1, using Aurora's existing Dawn package
(`v20260603.191052`). Metal API and GPU validation were enabled:
| Formats | Input | Output | Frames per format |
| --- | --- | --- | --- |
| RGBA8Unorm, BGRA8Unorm | 64 × 48 | 128 × 96 | 24 |
| RGBA8Unorm, BGRA8Unorm | 320 × 180 | 480 × 270 | 24 |
| RGBA8Unorm, BGRA8Unorm | 960 × 540 | 1920 × 1080 | 24 |
All 144 frames and 2,304 interior pixel samples passed. Red changes per frame;
green and blue distinguish left/right and top/bottom. All channels are checked
within five 8-bit levels, catching stale images, orientation/channel mistakes,
and missing output. Tests cover 1.5× and 2× scaling, padded readback rows, slot
reuse, dropping wrappers before readback completion, invalid dimensions/sRGB
formats, the eight-set allocation bound, and the unavailable-backend stub.
Readback is only the test oracle and is absent from the game upscaling path.
These samples do not measure reconstruction quality at edges or race performance.
## Windowed presentation test
This optional target exercises Aurora's actual frame submission and presentation
with a synthetic source and an ImGui overlay. It requires no Wii game data and
creates an automatically closing test window. Add the option to an existing
from-source runtime build (the normal dependency/provider options still apply):
```sh
cmake -S runtime -B build-macos -DCMAKE_BUILD_TYPE=Release \
-DAURORA_BUILD_METALFX_PRESENTATION_TEST=ON
cmake --build build-macos --target metalfx_presentation_test
MTL_DEBUG_LAYER=1 MTL_SHADER_VALIDATION=1 \
./build-macos/aurora-build/metalfx_presentation_test
```
On the same M3, all 84 frames passed with Metal API/GPU validation: disabled,
enabled, window resize, 4:3/16:9 aspect changes, native-size bypass, disable, and
re-enable. Assertions check renderer status and errors; this is not a pixel-level
verification of the window image. The core build and macOS 12 deployment-target
availability compilation also passed. Full Mario Kart gameplay, race performance,
visual quality, Intel Macs, other Apple GPUs, older macOS runtime versions, and
non-macOS full builds remain untested.
References: [Apple MetalFX](https://developer.apple.com/documentation/metalfx),
[spatial scaler requirements](https://developer.apple.com/documentation/metalfx/mtlfxspatialscaler),
and the installed Dawn `MetalBackend.h` / `webgpu_cpp.h` APIs.
+269
View File
@@ -0,0 +1,269 @@
#import <Foundation/Foundation.h>
#import <IOSurface/IOSurface.h>
#import <Metal/Metal.h>
#include "webgpu/metalfx.hpp"
#include <dawn/native/MetalBackend.h>
#include <webgpu/webgpu_cpp.h>
#include <array>
#include <atomic>
#include <cmath>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string_view>
#include <vector>
namespace {
constexpr uint64_t kTimeoutNs = 10'000'000'000;
constexpr unsigned kFrames = 24;
std::atomic<unsigned> g_errors{0};
void require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
void wait(const wgpu::Instance& instance, wgpu::Future future) {
require(instance.WaitAny(future, kTimeoutNs) == wgpu::WaitStatus::Success,
"Dawn operation timed out or failed");
}
void runCase(const wgpu::Instance& instance, const wgpu::Device& device,
bool bgra, uint32_t width, uint32_t height,
uint32_t outWidth, uint32_t outHeight) {
const auto format = bgra ? wgpu::TextureFormat::BGRA8Unorm : wgpu::TextureFormat::RGBA8Unorm;
using namespace aurora::webgpu::metalfx;
std::array<std::unique_ptr<SpatialScaler>, 3> slots;
for (auto& slot : slots) {
std::string error;
slot = create(instance, device, {width, height, outWidth, outHeight, format}, error);
if (!slot) {
throw std::runtime_error(error.empty()
? "MetalFX resource pool was still busy retiring earlier slots"
: error);
}
}
// Asymmetric quadrants expose channel swaps, vertical flips, and stale frames.
wgpu::ShaderSourceWGSL source{};
source.code = R"(
@group(0) @binding(0) var<uniform> params: vec4f;
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
let p = array(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
return vec4f(p[i], 0, 1);
}
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
return vec4f(params.x, select(0.2, 0.8, p.x >= params.y / 2),
select(0.3, 0.7, p.y >= params.z / 2), 1);
}
)";
wgpu::ShaderModuleDescriptor shaderDescriptor{};
shaderDescriptor.nextInChain = &source;
auto shader = device.CreateShaderModule(&shaderDescriptor);
wgpu::ColorTargetState target{};
target.format = format;
wgpu::FragmentState fragment{};
fragment.module = shader;
fragment.entryPoint = "fs";
fragment.targetCount = 1;
fragment.targets = &target;
wgpu::RenderPipelineDescriptor pipelineDescriptor{};
pipelineDescriptor.vertex.module = shader;
pipelineDescriptor.vertex.entryPoint = "vs";
pipelineDescriptor.fragment = &fragment;
auto pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
auto dawnQueue = device.GetQueue();
const uint32_t bytesPerRow = (outWidth * 4 + 255) & ~255u;
const uint64_t readbackSize = uint64_t(bytesPerRow) * outHeight;
std::vector<wgpu::Buffer> readbacks;
for (unsigned frame = 0; frame < kFrames; ++frame) {
auto& slot = slots[frame % slots.size()];
require(slot->begin_input(), "Production MetalFX begin_input failed");
const std::array<float, 4> params{0.2f + float(frame % 5) * 0.1f,
float(width), float(height), 0};
wgpu::BufferDescriptor uniformDescriptor{};
uniformDescriptor.size = sizeof(params);
uniformDescriptor.usage = wgpu::BufferUsage::Uniform | wgpu::BufferUsage::CopyDst;
auto uniform = device.CreateBuffer(&uniformDescriptor);
dawnQueue.WriteBuffer(uniform, 0, params.data(), sizeof(params));
wgpu::BindGroupEntry entry{};
entry.binding = 0;
entry.buffer = uniform;
entry.size = sizeof(params);
wgpu::BindGroupDescriptor bindDescriptor{};
bindDescriptor.layout = pipeline.GetBindGroupLayout(0);
bindDescriptor.entryCount = 1;
bindDescriptor.entries = &entry;
auto bindGroup = device.CreateBindGroup(&bindDescriptor);
auto encoder = device.CreateCommandEncoder();
wgpu::RenderPassColorAttachment attachment{};
attachment.view = slot->input_view();
attachment.loadOp = wgpu::LoadOp::Clear;
attachment.storeOp = wgpu::StoreOp::Store;
wgpu::RenderPassDescriptor passDescriptor{};
passDescriptor.colorAttachmentCount = 1;
passDescriptor.colorAttachments = &attachment;
auto pass = encoder.BeginRenderPass(&passDescriptor);
pass.SetPipeline(pipeline);
pass.SetBindGroup(0, bindGroup);
pass.Draw(3);
pass.End();
auto render = encoder.Finish();
dawnQueue.Submit(1, &render);
if (!slot->upscale()) throw std::runtime_error(slot->error());
wgpu::BufferDescriptor readbackDescriptor{};
readbackDescriptor.size = readbackSize;
readbackDescriptor.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead;
auto readback = device.CreateBuffer(&readbackDescriptor);
encoder = device.CreateCommandEncoder();
wgpu::TexelCopyTextureInfo copySource{};
copySource.texture = slot->output_texture();
wgpu::TexelCopyBufferInfo destination{};
destination.buffer = readback;
destination.layout.bytesPerRow = bytesPerRow;
destination.layout.rowsPerImage = outHeight;
const wgpu::Extent3D extent{outWidth, outHeight, 1};
encoder.CopyTextureToBuffer(&copySource, &destination, &extent);
auto copy = encoder.Finish();
dawnQueue.Submit(1, &copy);
if (!slot->end_output()) throw std::runtime_error(slot->error());
readbacks.push_back(std::move(readback));
}
// Model toggle/resize immediately after submission, while either queue may
// still be consuming these textures. Production completion callbacks must
// keep the resources alive after the cache drops its wrappers.
slots = {};
// Readback is only the test oracle. No CPU image transfer or GPU completion
// wait occurs between Dawn rendering, MetalFX, and Dawn consumption above.
for (unsigned frame = 0; frame < kFrames; ++frame) {
bool mapped = false;
auto& readback = readbacks[frame];
wait(instance, readback.MapAsync(wgpu::MapMode::Read, 0, readbackSize,
wgpu::CallbackMode::WaitAnyOnly, [&mapped](wgpu::MapAsyncStatus status, wgpu::StringView) {
mapped = status == wgpu::MapAsyncStatus::Success;
}));
require(mapped, "Output readback mapping failed");
const auto* bytes = static_cast<const uint8_t*>(readback.GetConstMappedRange());
require(bytes != nullptr, "Output readback pointer is null");
for (unsigned y = 0; y < 4; ++y) {
for (unsigned x = 0; x < 4; ++x) {
const unsigned px = (2 * x + 1) * outWidth / 8;
const unsigned py = (2 * y + 1) * outHeight / 8;
const auto* pixel = bytes + py * bytesPerRow + px * 4;
const std::array<float, 4> expected{
0.2f + float(frame % 5) * 0.1f, x >= 2 ? 0.8f : 0.2f,
y >= 2 ? 0.7f : 0.3f, 1};
for (unsigned c = 0; c < 4; ++c) {
const unsigned channel = bgra && c != 1 && c != 3 ? 2 - c : c;
if (std::abs(int(pixel[channel]) - int(std::lround(expected[c] * 255))) > 5) {
std::cerr << "Pixel mismatch: frame=" << frame << " x=" << px << " y=" << py
<< " channel=" << c << " actual=" << int(pixel[channel])
<< " expected=" << std::lround(expected[c] * 255) << '\n';
throw std::runtime_error("MetalFX output failed image validation");
}
}
}
}
readback.Unmap();
}
require(g_errors.load() == 0, "Dawn reported validation errors or device loss");
std::cout << "PASS " << (bgra ? "BGRA8" : "RGBA8") << ' ' << width << 'x' << height
<< " -> " << outWidth << 'x' << outHeight << ": " << kFrames
<< " frames, 3 reused slots, 16 pixel samples/frame\n";
}
int run() {
const wgpu::InstanceFeatureName timedWait = wgpu::InstanceFeatureName::TimedWaitAny;
wgpu::InstanceDescriptor instanceDescriptor{};
instanceDescriptor.requiredFeatureCount = 1;
instanceDescriptor.requiredFeatures = &timedWait;
auto instance = wgpu::CreateInstance(&instanceDescriptor);
require(instance != nullptr, "Dawn instance creation failed");
wgpu::Adapter adapter;
wgpu::RequestAdapterOptions options{};
options.backendType = wgpu::BackendType::Metal;
wait(instance, instance.RequestAdapter(&options, wgpu::CallbackMode::WaitAnyOnly,
[&adapter](wgpu::RequestAdapterStatus status, wgpu::Adapter result, wgpu::StringView message) {
if (status == wgpu::RequestAdapterStatus::Success) adapter = std::move(result);
else std::cerr << "Adapter: " << std::string_view(message) << '\n';
}));
if (!adapter) { std::cout << "SKIP: no Dawn Metal adapter\n"; return 77; }
const std::array features{wgpu::FeatureName::SharedTextureMemoryIOSurface,
wgpu::FeatureName::SharedFenceMTLSharedEvent};
for (auto feature : features) {
if (!adapter.HasFeature(feature)) {
std::cout << "SKIP: Dawn adapter lacks IOSurface/shared-event interoperability\n";
return 77;
}
}
wgpu::DeviceDescriptor descriptor{};
descriptor.requiredFeatureCount = features.size();
descriptor.requiredFeatures = features.data();
descriptor.SetUncapturedErrorCallback(
[](const wgpu::Device&, wgpu::ErrorType, wgpu::StringView message) {
++g_errors;
std::cerr << "Dawn error: " << std::string_view(message) << '\n';
});
descriptor.SetDeviceLostCallback(wgpu::CallbackMode::AllowSpontaneous,
[](const wgpu::Device&, wgpu::DeviceLostReason reason, wgpu::StringView message) {
if (reason != wgpu::DeviceLostReason::Destroyed) {
++g_errors;
std::cerr << "Device lost: " << std::string_view(message) << '\n';
}
});
wgpu::Device device;
wait(instance, adapter.RequestDevice(&descriptor, wgpu::CallbackMode::WaitAnyOnly,
[&device](wgpu::RequestDeviceStatus status, wgpu::Device result, wgpu::StringView message) {
if (status == wgpu::RequestDeviceStatus::Success) device = std::move(result);
else std::cerr << "Device: " << std::string_view(message) << '\n';
}));
require(device != nullptr, "Dawn device creation failed");
id<MTLDevice> native = dawn::native::metal::GetMTLDevice(device.Get());
require(native != nil, "Dawn native Metal device is unavailable");
std::cout << "GPU: " << native.name.UTF8String << '\n';
if (!aurora::webgpu::metalfx::supported(device, wgpu::BackendType::Metal)) {
std::cout << "SKIP: GPU does not support MetalFX spatial scaling\n";
return 77;
}
require(!aurora::webgpu::metalfx::supported(device, wgpu::BackendType::Vulkan),
"MetalFX must reject non-Metal backends");
std::string error;
require(!aurora::webgpu::metalfx::create(instance, device,
{128, 96, 128, 96, wgpu::TextureFormat::RGBA8Unorm}, error) && !error.empty(),
"MetalFX must reject equal-size input/output");
require(!aurora::webgpu::metalfx::create(instance, device,
{128, 96, 256, 192, wgpu::TextureFormat::RGBA8UnormSrgb}, error),
"MetalFX must reject implicit sRGB conversion");
{
using namespace aurora::webgpu::metalfx;
std::array<std::unique_ptr<SpatialScaler>, 8> resources;
for (auto& scaler : resources) {
scaler = create(instance, device, {64, 48, 128, 96, wgpu::TextureFormat::RGBA8Unorm}, error);
require(scaler != nullptr, "Could not fill the MetalFX resource pool");
}
require(!create(instance, device, {64, 48, 128, 96, wgpu::TextureFormat::RGBA8Unorm}, error)
&& error.empty(), "A full retirement pool must defer allocation without a fatal error");
}
for (bool bgra : {false, true}) {
runCase(instance, device, bgra, 64, 48, 128, 96);
runCase(instance, device, bgra, 320, 180, 480, 270);
runCase(instance, device, bgra, 960, 540, 1920, 1080);
}
return 0;
}
} // namespace
int main() {
@autoreleasepool {
try { return run(); }
catch (const std::exception& error) {
std::cerr << "FAIL: " << error.what() << '\n';
return 1;
}
}
}
@@ -0,0 +1,124 @@
// Explicitly opted-in windowed test of Aurora's real frame/presentation path.
// No Wii game data is needed; the source override supplies a synthetic image.
#include <aurora/aurora.h>
#include <imgui.h>
#include <SDL3/SDL_timer.h>
#include "webgpu/gpu.hpp"
#include "window.hpp"
#include <atomic>
#include <chrono>
#include <cstdio>
#include <filesystem>
#include <stdexcept>
#include <string_view>
#include <system_error>
#include <vector>
namespace {
std::atomic<unsigned> g_errors{0};
void log_message(AuroraLogLevel level, const char* module, const char* message, unsigned len) {
if (level >= LOG_ERROR) ++g_errors;
if (level >= LOG_WARNING || std::string_view(message, len).find("MetalFX") != std::string_view::npos)
std::fprintf(stderr, "[%s] %.*s\n", module, static_cast<int>(len), message);
if (level == LOG_FATAL) std::abort();
}
void require(bool value, const char* message) {
if (!value) throw std::runtime_error(message);
}
void draw_frames(uint32_t width, uint32_t height, AuroraMetalFXStatus expected) {
using namespace aurora::webgpu;
auto source = create_render_texture(width, height, false);
auto bindGroup = create_copy_bind_group(source);
std::vector<uint32_t> pixels(size_t(width) * height);
for (uint32_t y = 0; y < height; ++y) {
for (uint32_t x = 0; x < width; ++x) {
pixels[size_t(y) * width + x] = 0xff000000u | ((x / 16 % 2) ? 0x00bb55u : 0xbb5500u);
}
}
wgpu::TexelCopyTextureInfo target{};
target.texture = source.texture;
wgpu::TexelCopyBufferLayout layout{};
layout.bytesPerRow = width * 4;
layout.rowsPerImage = height;
g_queue.WriteTexture(&target, pixels.data(), pixels.size() * sizeof(uint32_t), &layout, &source.size);
unsigned rendered = 0;
unsigned matchingStatus = 0;
for (unsigned attempt = 0; attempt < 300 && rendered < 12; ++attempt) {
aurora_update();
if (!aurora_begin_frame()) { SDL_Delay(5); continue; }
set_present_source_override(bindGroup, source.texture, source.size, source.format);
ImGui::SetNextWindowPos(ImVec2(12, 12), ImGuiCond_Always);
ImGui::Begin("MetalFX presentation test", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
ImGui::TextUnformatted("Output-resolution overlay after game upscaling");
ImGui::Text("Source: %u x %u", width, height);
ImGui::End();
aurora_end_frame();
aurora_wait_for_frame_worker();
const auto status = aurora_get_metalfx_status();
require(status != AURORA_METALFX_ERROR, "MetalFX reported a presentation error");
if (status == expected) ++matchingStatus;
++rendered;
}
require(rendered == 12 && matchingStatus >= 9, "Presentation did not reach the expected MetalFX state");
require(g_errors.load() == 0, "Aurora reported an error");
std::printf("PASS presentation source=%ux%u status=%d frames=%u\n", width, height, expected, rendered);
}
} // namespace
int main(int argc, char** argv) {
const auto cache = std::filesystem::temp_directory_path() /
("aurora-metalfx-presentation-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
std::filesystem::create_directories(cache);
const auto path = cache.string();
AuroraConfig config{};
config.appName = "MetalFX presentation test";
config.userPath = path.c_str();
config.cachePath = path.c_str();
config.resourcesPath = path.c_str();
config.desiredBackend = BACKEND_METAL;
config.windowWidth = 640;
config.windowHeight = 480;
config.msaa = 1;
config.maxTextureAnisotropy = 1;
config.logCallback = log_message;
config.logLevel = LOG_INFO;
aurora_initialize(argc, argv, &config);
int result = 0;
try {
if (!aurora_is_metalfx_spatial_supported()) {
std::puts("SKIP: MetalFX spatial scaling is unavailable");
result = 77;
} else {
aurora::window::lock_present_aspect_ratio(4, 3);
aurora_set_metalfx_spatial(false);
require(!aurora_get_metalfx_spatial(), "Disable request was not retained");
draw_frames(320, 240, AURORA_METALFX_DISABLED);
aurora_set_metalfx_spatial(true);
require(aurora_get_metalfx_spatial(), "Enable request was not retained");
draw_frames(320, 240, AURORA_METALFX_ACTIVE);
aurora::window::set_window_size(800, 500);
draw_frames(320, 240, AURORA_METALFX_ACTIVE);
aurora::window::lock_present_aspect_ratio(16, 9);
draw_frames(320, 240, AURORA_METALFX_ACTIVE);
const auto output = aurora::window::get_window_size();
draw_frames(output.native_fb_width, output.native_fb_height, AURORA_METALFX_NOT_UPSCALING);
aurora_set_metalfx_spatial(false);
draw_frames(320, 240, AURORA_METALFX_DISABLED);
aurora_set_metalfx_spatial(true);
draw_frames(320, 240, AURORA_METALFX_ACTIVE);
}
} catch (const std::exception& error) {
std::fprintf(stderr, "FAIL: %s\n", error.what());
result = 1;
}
aurora_shutdown();
std::error_code cleanupError;
std::filesystem::remove_all(cache, cleanupError);
return result;
}
@@ -0,0 +1,9 @@
#include "webgpu/metalfx.hpp"
int main() {
using namespace aurora::webgpu::metalfx;
if (supported({}, wgpu::BackendType::Vulkan) || supported({}, wgpu::BackendType::Metal)) return 1;
std::string error;
if (create({}, {}, {640, 480, 1280, 960, wgpu::TextureFormat::RGBA8Unorm}, error)) return 1;
return error.empty() ? 1 : 0;
}
+123 -29
View File
@@ -1,27 +1,53 @@
cmake_minimum_required(VERSION 3.25) cmake_minimum_required(VERSION 3.25)
# Dawn's pinned macOS artifacts target 12.0. Set the same floor before project()
# initializes the Apple toolchain so direct developer CMake invocations cannot
# accidentally inherit the running SDK's deployment version. This cache entry
# is harmless on non-Apple platforms and remains overridable by a caller.
if(NOT CMAKE_OSX_DEPLOYMENT_TARGET)
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING
"Minimum macOS version supported by WiiCompiled" FORCE)
endif()
project(mkw_recompiled) project(mkw_recompiled)
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8) if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
message(FATAL_ERROR "WiiCompiled requires a 64-bit Clang toolchain") message(FATAL_ERROR "WiiCompiled requires a 64-bit Clang toolchain")
endif() endif()
if(APPLE AND CMAKE_OSX_ARCHITECTURES)
list(LENGTH CMAKE_OSX_ARCHITECTURES MKW_OSX_ARCHITECTURE_COUNT)
if(MKW_OSX_ARCHITECTURE_COUNT GREATER 1)
message(FATAL_ERROR
"WiiCompiled supports one macOS architecture per build directory; "
"configure separate arm64 and x86_64 build directories")
endif()
list(GET CMAKE_OSX_ARCHITECTURES 0 _mkw_osx_arch)
set(CMAKE_SYSTEM_PROCESSOR "${_mkw_osx_arch}" CACHE STRING "Target processor architecture" FORCE)
set(CMAKE_SYSTEM_PROCESSOR "${_mkw_osx_arch}")
endif()
if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$") if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_PLATFORM_WINDOWS TRUE) set(MKW_PLATFORM_WINDOWS TRUE)
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64)$") elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|arm64|ARM64)$")
# The first native macOS target is Apple Silicon. Intel and universal
# binaries remain future compatibility work; do not silently claim them.
set(MKW_PLATFORM_MACOS TRUE) set(MKW_PLATFORM_MACOS TRUE)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_PLATFORM_MACOS_X86_64 TRUE)
else()
set(MKW_PLATFORM_MACOS_ARM64 TRUE)
endif()
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$") elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$")
set(MKW_PLATFORM_LINUX TRUE) set(MKW_PLATFORM_LINUX TRUE)
else() else()
message(FATAL_ERROR message(FATAL_ERROR
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS arm64") "WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS x86_64/arm64")
endif() endif()
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release") if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
message(FATAL_ERROR "WiiCompiled only supports Release builds") message(FATAL_ERROR "WiiCompiled only supports Release builds")
endif() endif()
option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON) option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON)
option(MKW_BUILD_PSQ_TESTS "Build focused PSQ ISA tests" OFF)
# Preprocessor definitions that belong to this project's own code (the runtime, # Preprocessor definitions that belong to this project's own code (the runtime,
# the translated shards and the product glue) and to nothing else. They are # the translated shards and the product glue) and to nothing else. They are
@@ -59,14 +85,15 @@ target_include_directories(mkw_pugixml PUBLIC third_party/pugixml)
target_compile_features(mkw_pugixml PUBLIC cxx_std_17) target_compile_features(mkw_pugixml PUBLIC cxx_std_17)
set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF) set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF)
# Linux guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric # POSIX x86-64 guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric
# stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers # stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike # directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
# asymmetric resume/yield coroutine libraries, which would need every call site restructured). # asymmetric resume/yield coroutine libraries, which would need every call site restructured).
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is # Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers # separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers;
# and macOS uses the project's x18-safe AArch64 assembly backend, so this target is Linux-only. # Apple Silicon uses the project's x18-safe AArch64 assembly backend, while Intel macOS uses
if(MKW_PLATFORM_LINUX) # libco's existing System V AMD64 backend.
if(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
add_library(mkw_libco STATIC third_party/libco/libco.c) add_library(mkw_libco STATIC third_party/libco/libco.c)
add_library(mkw::libco ALIAS mkw_libco) add_library(mkw::libco ALIAS mkw_libco)
target_include_directories(mkw_libco PUBLIC third_party/libco) target_include_directories(mkw_libco PUBLIC third_party/libco)
@@ -162,6 +189,18 @@ set(MKW_AURORA_DIR "${CMAKE_CURRENT_LIST_DIR}/../aurora-main")
# Fast-math may erase them and change guest-visible integer conversions. # Fast-math may erase them and change guest-visible integer conversions.
set(MKW_TRANSLATED_PPC_FP_OPTIONS -fno-fast-math -ffp-contract=off) set(MKW_TRANSLATED_PPC_FP_OPTIONS -fno-fast-math -ffp-contract=off)
set(MKW_PPC_SEMANTIC_RUNTIME_SOURCES
"${CMAKE_CURRENT_LIST_DIR}/src/ppc_helpers.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/fpu_helpers.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/ppc_quantized.cpp")
set_source_files_properties(${MKW_PPC_SEMANTIC_RUNTIME_SOURCES} PROPERTIES
SKIP_UNITY_BUILD_INCLUSION ON
SKIP_PRECOMPILE_HEADERS ON
COMPILE_OPTIONS "${MKW_TRANSLATED_PPC_FP_OPTIONS}")
# Match the optimization policy of the translated callers as well as their FP policy.
set_property(SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/ppc_quantized.cpp" APPEND PROPERTY
COMPILE_OPTIONS -O2 -fno-slp-vectorize)
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
# Third-party: aurora-main (provides SDL3 + GPU backends) # Third-party: aurora-main (provides SDL3 + GPU backends)
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
@@ -281,12 +320,14 @@ endif()
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp") file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
if(MKW_PLATFORM_MACOS) if(MKW_PLATFORM_MACOS)
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory.cpp") list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory.cpp")
# HostContext's Apple Silicon backend is implemented in a small assembly if(MKW_PLATFORM_MACOS_ARM64)
# companion. It must be part of the product runtime as well as the # HostContext's Apple Silicon backend is implemented in a small assembly
# standalone context test; otherwise the final executable is missing # companion. It must be part of the product runtime as well as the
# mkw_co_init/mkw_co_switch at link time. # standalone context test; otherwise the final executable is missing
enable_language(ASM) # mkw_co_init/mkw_co_switch at link time.
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S") enable_language(ASM)
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
endif()
else() else()
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp") list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
endif() endif()
@@ -310,11 +351,39 @@ set_target_properties(mkw_platform PROPERTIES UNITY_BUILD OFF)
# Keep these independent from Aurora's BUILD_TESTING option: they validate the # Keep these independent from Aurora's BUILD_TESTING option: they validate the
# project's host-platform contracts, not Aurora's third-party test suite. # project's host-platform contracts, not Aurora's third-party test suite.
enable_testing() enable_testing()
if(MKW_BUILD_PSQ_TESTS)
add_executable(mkw_psq_helpers_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/psq_helpers_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/ppc_quantized.cpp")
target_include_directories(mkw_psq_helpers_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/tests/psq_memory"
"${CMAKE_CURRENT_LIST_DIR}/include/isa"
"${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_psq_helpers_tests PRIVATE cxx_std_17)
target_compile_options(mkw_psq_helpers_tests PRIVATE
-O2 ${MKW_TRANSLATED_PPC_FP_OPTIONS} -fno-slp-vectorize)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
target_compile_options(mkw_psq_helpers_tests PRIVATE -march=x86-64-v3)
endif()
set_target_properties(mkw_psq_helpers_tests PROPERTIES UNITY_BUILD OFF)
add_test(NAME mkw_psq_helpers_tests COMMAND mkw_psq_helpers_tests)
add_test(NAME mkw_psq_reserved_tests COMMAND "${CMAKE_COMMAND}"
"-DPSQ_TEST_EXECUTABLE=$<TARGET_FILE:mkw_psq_helpers_tests>"
-P "${CMAKE_CURRENT_LIST_DIR}/tests/psq_reserved_tests.cmake")
endif()
add_executable(mkw_platform_paths_tests "${CMAKE_CURRENT_LIST_DIR}/tests/platform_paths_tests.cpp") 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_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17) target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests) add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
add_executable(mkw_runtime_config_tests "${CMAKE_CURRENT_LIST_DIR}/tests/runtime_config_tests.cpp")
target_include_directories(mkw_runtime_config_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include"
"${CMAKE_CURRENT_LIST_DIR}/third_party/toml11")
target_compile_features(mkw_runtime_config_tests PRIVATE cxx_std_20)
add_test(NAME mkw_runtime_config_tests COMMAND mkw_runtime_config_tests)
add_executable(mkw_nand_save_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_save_tests.cpp") add_executable(mkw_nand_save_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_save_tests.cpp")
target_include_directories(mkw_nand_save_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include") target_include_directories(mkw_nand_save_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17) target_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17)
@@ -357,20 +426,31 @@ if(MKW_PLATFORM_LINUX)
endif() endif()
if(MKW_PLATFORM_MACOS) if(MKW_PLATFORM_MACOS)
# Exercise the Apple Silicon context ABI and the public host-memory # Exercise the public host-memory contracts separately from translated products.
# contracts separately from translated products. if(MKW_PLATFORM_MACOS_ARM64)
enable_language(ASM) # Apple Silicon's context ABI is implemented by the local assembly backend.
add_executable(mkw_macos_context_abi_tests enable_language(ASM)
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp" add_executable(mkw_macos_context_abi_tests
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S") "${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17) "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests) target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
add_executable(mkw_macos_host_context_tests add_executable(mkw_macos_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp" "${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp" "${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S") "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include") target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
else()
# Intel macOS follows the same System V AMD64 libco path as Linux.
add_executable(mkw_macos_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp")
target_include_directories(mkw_macos_host_context_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include"
"${CMAKE_CURRENT_LIST_DIR}/third_party/libco")
target_link_libraries(mkw_macos_host_context_tests PRIVATE mkw::libco)
endif()
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17) target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests) add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
@@ -380,6 +460,13 @@ if(MKW_PLATFORM_MACOS)
target_include_directories(mkw_macos_guest_flat_memory_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include") 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) 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) add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests)
add_executable(mkw_macos_external_audio_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_external_audio_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/external_audio_macos.cpp")
target_include_directories(mkw_macos_external_audio_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_macos_external_audio_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_external_audio_tests COMMAND mkw_macos_external_audio_tests)
endif() endif()
# The translator emits the complete, content-addressed source graph. Consuming # The translator emits the complete, content-addressed source graph. Consuming
@@ -428,7 +515,14 @@ else()
target_compile_definitions(mkw_macos_native_compile PRIVATE SDL_MAIN_HANDLED TARGET_PC) target_compile_definitions(mkw_macos_native_compile PRIVATE SDL_MAIN_HANDLED TARGET_PC)
target_link_libraries(mkw_macos_native_compile PRIVATE target_link_libraries(mkw_macos_native_compile PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
mkw::pugixml mkw::toml11 mkw::cryptopp) mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
if(MKW_PLATFORM_MACOS_X86_64)
target_link_libraries(mkw_macos_native_compile PRIVATE mkw::libco)
# Keep this compile-only audit on the same Haswell-era x86-64-v3
# baseline as the translated product. PPC paired FMA helpers use
# FMA intrinsics and intentionally cannot compile for plain x86-64.
target_compile_options(mkw_macos_native_compile PRIVATE -march=x86-64-v3)
endif()
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF) set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
endif() endif()
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform) add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
+19 -14
View File
@@ -1,4 +1,4 @@
# Public WiiCompiled product graph. # Public WiiCompiled product graph.
# #
# The translator owns the translated build graph. Mario Kart's profile-neutral # The translator owns the translated build graph. Mario Kart's profile-neutral
# functions are compiled once into mkw_base_shared; only callers whose direct # functions are compiled once into mkw_base_shared; only callers whose direct
@@ -28,6 +28,7 @@ list(REMOVE_DUPLICATES SOURCES)
if(MKW_PLATFORM_MACOS) if(MKW_PLATFORM_MACOS)
find_library(MKW_IOKIT_FRAMEWORK IOKit REQUIRED) find_library(MKW_IOKIT_FRAMEWORK IOKit REQUIRED)
find_library(MKW_COREFOUNDATION_FRAMEWORK CoreFoundation REQUIRED) find_library(MKW_COREFOUNDATION_FRAMEWORK CoreFoundation REQUIRED)
find_library(MKW_COREAUDIO_FRAMEWORK CoreAudio REQUIRED)
endif() endif()
function(mkw_apply_common_compile_options target) function(mkw_apply_common_compile_options target)
@@ -86,9 +87,20 @@ if(MKW_PLATFORM_WINDOWS)
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp) target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
elseif(MKW_PLATFORM_LINUX) elseif(MKW_PLATFORM_LINUX)
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS # ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
# media monitoring). Empty string on glibc >= 2.34 where dl* is in libc. # media monitoring).
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS}) target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS})
endif() endif()
if(MKW_PLATFORM_MACOS)
# CoreAudio framework is required for automatic music muting on macOS.
target_link_libraries(mkw_runtime_common PRIVATE "${MKW_COREAUDIO_FRAMEWORK}")
if(MKW_PLATFORM_MACOS_X86_64)
# libco is used by Intel macOS. Apple Silicon uses the local
# x18-safe assembly backend and therefore does not define mkw::libco.
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco)
endif()
endif()
if(MKW_CPPWINRT_INCLUDE_DIR) if(MKW_CPPWINRT_INCLUDE_DIR)
if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h") if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
message(FATAL_ERROR message(FATAL_ERROR
@@ -118,16 +130,7 @@ foreach(source IN LISTS SOURCES)
endif() endif()
set_source_files_properties("${source}" PROPERTIES UNITY_GROUP "${runtime_group}") set_source_files_properties("${source}" PROPERTIES UNITY_GROUP "${runtime_group}")
endforeach() endforeach()
# These translation units implement guest-visible floating-point bit # PPC semantic sources are excluded from unity/PCH and configured in CMakeLists.txt.
# semantics. Keep them out of the fast-math runtime unity groups and apply
# the same contraction/rounding policy as translated PPC shards.
set(MKW_PPC_SEMANTIC_RUNTIME_SOURCES
"${MKW_RUNTIME_SOURCE_DIR}/src/ppc_helpers.cpp"
"${MKW_RUNTIME_SOURCE_DIR}/src/fpu_helpers.cpp")
set_source_files_properties(${MKW_PPC_SEMANTIC_RUNTIME_SOURCES} PROPERTIES
SKIP_UNITY_BUILD_INCLUSION ON
SKIP_PRECOMPILE_HEADERS ON
COMPILE_OPTIONS "${MKW_TRANSLATED_PPC_FP_OPTIONS}")
set_target_properties(mkw_runtime_common PROPERTIES UNITY_BUILD ON UNITY_BUILD_MODE GROUP) set_target_properties(mkw_runtime_common PROPERTIES UNITY_BUILD ON UNITY_BUILD_MODE GROUP)
target_precompile_headers(mkw_runtime_common PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h") target_precompile_headers(mkw_runtime_common PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
mkw_apply_common_compile_options(mkw_runtime_common) mkw_apply_common_compile_options(mkw_runtime_common)
@@ -205,7 +208,9 @@ function(mkw_configure_product target)
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx) aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
if(MKW_PLATFORM_MACOS) if(MKW_PLATFORM_MACOS)
target_link_libraries(${target} PRIVATE target_link_libraries(${target} PRIVATE
"${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}") "${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}" "${MKW_COREAUDIO_FRAMEWORK}")
target_link_options(${target} PRIVATE
"LINKER:-U,_OBJC_CLASS_$_MTLLogStateDescriptor")
endif() endif()
if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake") if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake") include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
@@ -226,7 +231,7 @@ function(mkw_configure_product target)
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp) dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE) set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
elseif(MKW_PLATFORM_LINUX) elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
# mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o # mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o
# files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own # files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own
# target_link_libraries (object libraries don't carry usage requirements to a consumer # target_link_libraries (object libraries don't carry usage requirements to a consumer
+14
View File
@@ -0,0 +1,14 @@
#pragma once
namespace MusicAttenuation {
struct MacOSAudioStatus {
bool available = false;
bool playing = false;
};
// Queries output activity without capturing audio. Requires Core Audio process
// objects (macOS 14.2+); older systems return an unavailable sample.
MacOSAudioStatus QueryMacOSExternalAudio() noexcept;
} // namespace MusicAttenuation
+5 -5
View File
@@ -3,11 +3,11 @@
#include <cstddef> #include <cstddef>
// HostContext is the deliberately small boundary between the guest scheduler // HostContext is the deliberately small boundary between the guest scheduler
// and the host's cooperative-context facility. Windows uses native Fibers and // and the host's cooperative-context facility. Windows uses native Fibers;
// Linux uses libco; macOS AArch64 uses the local assembly backend because it // Linux and Intel macOS use libco's System V x86-64 backend. macOS AArch64 uses
// must preserve Darwin's platform-reserved x18 register, which libco's AArch64 // the local assembly backend because it must preserve Darwin's platform-reserved
// backend does not save. Its handles are only valid on the thread that // x18 register, which libco's AArch64 backend does not save. Its handles are
// initialized the scheduler. // only valid on the thread that initialized the scheduler.
namespace HostContext { namespace HostContext {
using Handle = void*; using Handle = void*;
+64 -94
View File
@@ -1385,37 +1385,13 @@ MKW_PPC_FORCE_INLINE void PPC_PsqStStackInline(uint32_t addr, double value)
// Context-free PSQ entries for translated regions which own GQR state as an // Context-free PSQ entries for translated regions which own GQR state as an
// ordinary native value. All architecturally valid quantization encodings are // ordinary native value. All architecturally valid quantization encodings are
// handled directly; reserved encodings retain the generic helper's abort. // handled directly; reserved encodings retain the generic helper's abort.
template <uint32_t W, uint32_t I, bool Stack> template <uint32_t W, bool Stack>
MKW_PPC_NO_INLINE MKW_PPC_COLD inline double PPC_PsqLStateFallback(uint32_t gqr, uint32_t addr) MKW_PPC_NO_INLINE MKW_PPC_COLD double PPC_PsqLStateFallback(uint32_t gqr, uint32_t addr);
{
static_assert(W <= 1u && I < 8u); extern template double PPC_PsqLStateFallback<0u, false>(uint32_t, uint32_t);
const uint32_t type = (gqr >> 16) & 0x7u; extern template double PPC_PsqLStateFallback<0u, true>(uint32_t, uint32_t);
const uint32_t scale = (gqr >> 24) & 0x3Fu; extern template double PPC_PsqLStateFallback<1u, false>(uint32_t, uint32_t);
if constexpr (W == 0u) extern template double PPC_PsqLStateFallback<1u, true>(uint32_t, uint32_t);
{
switch (type)
{
case 0u: return Stack ? PpcLoadPairPsqFloatStackInline(addr) : PpcLoadPairPsqFloatFastInline(addr);
case 4u: return Stack ? PpcLoadPairPsqIntegerStackInline<uint8_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<uint8_t>(addr, scale);
case 5u: return Stack ? PpcLoadPairPsqIntegerStackInline<uint16_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<uint16_t>(addr, scale);
case 6u: return Stack ? PpcLoadPairPsqIntegerStackInline<int8_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<int8_t>(addr, scale);
case 7u: return Stack ? PpcLoadPairPsqIntegerStackInline<int16_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<int16_t>(addr, scale);
default: std::abort();
}
}
else
{
switch (type)
{
case 0u: return Stack ? PpcLoadSinglePsqFloatStackInline(addr) : PpcLoadSinglePsqFloatFastInline(addr);
case 4u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<uint8_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<uint8_t>(addr, scale);
case 5u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<uint16_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<uint16_t>(addr, scale);
case 6u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<int8_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<int8_t>(addr, scale);
case 7u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<int16_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<int16_t>(addr, scale);
default: std::abort();
}
}
}
// Keep the normal explicit-state path small and directly optimizable. Exact // Keep the normal explicit-state path small and directly optimizable. Exact
// unscaled encodings cover the SDK's common GQR setup; scaled and reserved // unscaled encodings cover the SDK's common GQR setup; scaled and reserved
@@ -1442,41 +1418,17 @@ MKW_PPC_FORCE_INLINE double PPC_PsqLStateInline(uint32_t gqr, uint32_t addr)
if constexpr (W == 0u) return Stack ? PpcLoadPairPsqIntegerStackInline<int16_t>(addr, 0u) : PpcLoadPairPsqIntegerFastInline<int16_t>(addr, 0u); if constexpr (W == 0u) return Stack ? PpcLoadPairPsqIntegerStackInline<int16_t>(addr, 0u) : PpcLoadPairPsqIntegerFastInline<int16_t>(addr, 0u);
else return Stack ? PpcLoadSinglePsqQuantizedStackInline<int16_t>(addr, 0u) : PpcLoadSinglePsqQuantizedFastInline<int16_t>(addr, 0u); else return Stack ? PpcLoadSinglePsqQuantizedStackInline<int16_t>(addr, 0u) : PpcLoadSinglePsqQuantizedFastInline<int16_t>(addr, 0u);
default: default:
return PPC_PsqLStateFallback<W, I, Stack>(gqr, addr); return PPC_PsqLStateFallback<W, Stack>(gqr, addr);
} }
} }
template <uint32_t W, uint32_t I, bool Stack> template <uint32_t W, bool Stack>
MKW_PPC_NO_INLINE MKW_PPC_COLD inline void PPC_PsqStStateFallback(uint32_t gqr, uint32_t addr, double value) MKW_PPC_NO_INLINE MKW_PPC_COLD void PPC_PsqStStateFallback(uint32_t gqr, uint32_t addr, double value);
{
static_assert(W <= 1u && I < 8u); extern template void PPC_PsqStStateFallback<0u, false>(uint32_t, uint32_t, double);
const uint32_t type = gqr & 0x7u; extern template void PPC_PsqStStateFallback<0u, true>(uint32_t, uint32_t, double);
const uint32_t scale = (gqr >> 8) & 0x3Fu; extern template void PPC_PsqStStateFallback<1u, false>(uint32_t, uint32_t, double);
if constexpr (W == 0u) extern template void PPC_PsqStStateFallback<1u, true>(uint32_t, uint32_t, double);
{
switch (type)
{
case 0u: Stack ? PpcStorePairPsqFloatStackInline(addr, value) : PpcStorePairPsqFloatFastInline(addr, value); return;
case 4u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<uint8_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<uint8_t>(addr, value, scale); return;
case 5u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<uint16_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<uint16_t>(addr, value, scale); return;
case 6u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<int8_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<int8_t>(addr, value, scale); return;
case 7u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<int16_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<int16_t>(addr, value, scale); return;
default: std::abort();
}
}
else
{
switch (type)
{
case 0u: if constexpr (Stack) PpcStoreSinglePsqFloatStackInline(addr, value); else PpcStoreSinglePsqFloatFastInline(addr, value); return;
case 4u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<uint8_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<uint8_t>(addr, value, scale); return;
case 5u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<uint16_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<uint16_t>(addr, value, scale); return;
case 6u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<int8_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<int8_t>(addr, value, scale); return;
case 7u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<int16_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<int16_t>(addr, value, scale); return;
default: std::abort();
}
}
}
template <uint32_t W, uint32_t I, bool Stack> template <uint32_t W, uint32_t I, bool Stack>
MKW_PPC_FORCE_INLINE void PPC_PsqStStateInline(uint32_t gqr, uint32_t addr, double value) MKW_PPC_FORCE_INLINE void PPC_PsqStStateInline(uint32_t gqr, uint32_t addr, double value)
@@ -1505,11 +1457,18 @@ MKW_PPC_FORCE_INLINE void PPC_PsqStStateInline(uint32_t gqr, uint32_t addr, doub
else { if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<int16_t>(addr, value, 0u); else PpcStoreSinglePsqQuantizedFastInline<int16_t>(addr, value, 0u); } else { if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<int16_t>(addr, value, 0u); else PpcStoreSinglePsqQuantizedFastInline<int16_t>(addr, value, 0u); }
return; return;
default: default:
PPC_PsqStStateFallback<W, I, Stack>(gqr, addr, value); PPC_PsqStStateFallback<W, Stack>(gqr, addr, value);
return; return;
} }
} }
template <uint32_t W>
MKW_PPC_NO_INLINE MKW_PPC_COLD double PPC_PsqLResolvedStateFallback(
uint32_t gqr, uint8_t* resolvedHost, uint32_t offset, uint32_t addr);
extern template double PPC_PsqLResolvedStateFallback<0u>(uint32_t, uint8_t*, uint32_t, uint32_t);
extern template double PPC_PsqLResolvedStateFallback<1u>(uint32_t, uint8_t*, uint32_t, uint32_t);
template <uint32_t W, uint32_t I> template <uint32_t W, uint32_t I>
MKW_PPC_FORCE_INLINE double PPC_PsqLResolvedStateInline( MKW_PPC_FORCE_INLINE double PPC_PsqLResolvedStateInline(
uint32_t gqr, uint8_t* resolvedHost, uint32_t offset, uint32_t addr) uint32_t gqr, uint8_t* resolvedHost, uint32_t offset, uint32_t addr)
@@ -1517,33 +1476,41 @@ MKW_PPC_FORCE_INLINE double PPC_PsqLResolvedStateInline(
static_assert(W <= 1u && I < 8u); static_assert(W <= 1u && I < 8u);
if (!resolvedHost) [[unlikely]] return PPC_PsqLStateInline<W, I, false>(gqr, addr); if (!resolvedHost) [[unlikely]] return PPC_PsqLStateInline<W, I, false>(gqr, addr);
const uint32_t type = (gqr >> 16) & 0x7u; const uint32_t type = (gqr >> 16) & 0x7u;
const uint32_t scale = (gqr >> 24) & 0x3Fu; if (type == 0u) {
if constexpr (W == 0u) return PpcLoadPairPsqFloatResolvedInline(resolvedHost, offset, addr);
else return PpcLoadSinglePsqFloatResolvedInline(resolvedHost, offset, addr);
}
if constexpr (W == 0u) if constexpr (W == 0u)
{ {
switch (type) switch (gqr & 0x3F070000u)
{ {
case 0u: return PpcLoadPairPsqFloatResolvedInline(resolvedHost, offset, addr); case 0x00040000u: return PpcLoadPairPsqIntegerResolvedInline<uint8_t>(resolvedHost, offset, addr, 0u);
case 4u: return PpcLoadPairPsqIntegerResolvedInline<uint8_t>(resolvedHost, offset, addr, scale); case 0x00050000u: return PpcLoadPairPsqIntegerResolvedInline<uint16_t>(resolvedHost, offset, addr, 0u);
case 5u: return PpcLoadPairPsqIntegerResolvedInline<uint16_t>(resolvedHost, offset, addr, scale); case 0x00060000u: return PpcLoadPairPsqIntegerResolvedInline<int8_t>(resolvedHost, offset, addr, 0u);
case 6u: return PpcLoadPairPsqIntegerResolvedInline<int8_t>(resolvedHost, offset, addr, scale); case 0x00070000u: return PpcLoadPairPsqIntegerResolvedInline<int16_t>(resolvedHost, offset, addr, 0u);
case 7u: return PpcLoadPairPsqIntegerResolvedInline<int16_t>(resolvedHost, offset, addr, scale); default: return PPC_PsqLResolvedStateFallback<W>(gqr, resolvedHost, offset, addr);
default: std::abort();
} }
} }
else else
{ {
switch (type) switch (gqr & 0x3F070000u)
{ {
case 0u: return PpcLoadSinglePsqFloatResolvedInline(resolvedHost, offset, addr); case 0x00040000u: return PpcLoadSinglePsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, 0u);
case 4u: return PpcLoadSinglePsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, scale); case 0x00050000u: return PpcLoadSinglePsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, 0u);
case 5u: return PpcLoadSinglePsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, scale); case 0x00060000u: return PpcLoadSinglePsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, 0u);
case 6u: return PpcLoadSinglePsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, scale); case 0x00070000u: return PpcLoadSinglePsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, 0u);
case 7u: return PpcLoadSinglePsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, scale); default: return PPC_PsqLResolvedStateFallback<W>(gqr, resolvedHost, offset, addr);
default: std::abort();
} }
} }
} }
template <uint32_t W>
MKW_PPC_NO_INLINE MKW_PPC_COLD void PPC_PsqStResolvedStateFallback(
uint32_t gqr, uint8_t* resolvedHost, uint32_t offset, uint32_t addr, double value);
extern template void PPC_PsqStResolvedStateFallback<0u>(uint32_t, uint8_t*, uint32_t, uint32_t, double);
extern template void PPC_PsqStResolvedStateFallback<1u>(uint32_t, uint8_t*, uint32_t, uint32_t, double);
template <uint32_t W, uint32_t I> template <uint32_t W, uint32_t I>
MKW_PPC_FORCE_INLINE void PPC_PsqStResolvedStateInline( MKW_PPC_FORCE_INLINE void PPC_PsqStResolvedStateInline(
uint32_t gqr, uint8_t* resolvedHost, uint32_t offset, uint32_t addr, double value) uint32_t gqr, uint8_t* resolvedHost, uint32_t offset, uint32_t addr, double value)
@@ -1555,29 +1522,32 @@ MKW_PPC_FORCE_INLINE void PPC_PsqStResolvedStateInline(
return; return;
} }
const uint32_t type = gqr & 0x7u; const uint32_t type = gqr & 0x7u;
const uint32_t scale = (gqr >> 8) & 0x3Fu; if (type == 0u) {
if constexpr (W == 0u) PpcStorePairPsqFloatResolvedInline(resolvedHost, offset, addr, value);
else PpcStoreSinglePsqFloatResolvedInline(resolvedHost, offset, addr, value);
return;
}
if constexpr (W == 0u) if constexpr (W == 0u)
{ {
switch (type) switch (gqr & 0x3F07u)
{ {
case 0u: PpcStorePairPsqFloatResolvedInline(resolvedHost, offset, addr, value); return; case 0x3D04u: PpcStorePairPsqU8Scale61ResolvedInline(resolvedHost, offset, addr, value); return;
case 4u: PpcStorePairPsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, value, scale); return; case 4u: PpcStorePairPsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, value, 0u); return;
case 5u: PpcStorePairPsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, value, scale); return; case 5u: PpcStorePairPsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, value, 0u); return;
case 6u: PpcStorePairPsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, value, scale); return; case 6u: PpcStorePairPsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, value, 0u); return;
case 7u: PpcStorePairPsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, value, scale); return; case 7u: PpcStorePairPsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, value, 0u); return;
default: std::abort(); default: PPC_PsqStResolvedStateFallback<W>(gqr, resolvedHost, offset, addr, value); return;
} }
} }
else else
{ {
switch (type) switch (gqr & 0x3F07u)
{ {
case 0u: PpcStoreSinglePsqFloatResolvedInline(resolvedHost, offset, addr, value); return; case 4u: PpcStoreSinglePsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, value, 0u); return;
case 4u: PpcStoreSinglePsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, value, scale); return; case 5u: PpcStoreSinglePsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, value, 0u); return;
case 5u: PpcStoreSinglePsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, value, scale); return; case 6u: PpcStoreSinglePsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, value, 0u); return;
case 6u: PpcStoreSinglePsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, value, scale); return; case 7u: PpcStoreSinglePsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, value, 0u); return;
case 7u: PpcStoreSinglePsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, value, scale); return; default: PPC_PsqStResolvedStateFallback<W>(gqr, resolvedHost, offset, addr, value); return;
default: std::abort();
} }
} }
} }
+2 -1
View File
@@ -5,7 +5,8 @@
namespace MusicAttenuation { namespace MusicAttenuation {
// Enables the optional external-media integration (Windows media sessions, // Enables the optional external-media integration (Windows media sessions,
// or MPRIS over D-Bus on Linux). The monitor is started lazily the first time // Core Audio output activity on macOS, or MPRIS over D-Bus on Linux).
// The monitor is started lazily the first time
// this is enabled. // this is enabled.
void SetEnabled(bool enabled) noexcept; void SetEnabled(bool enabled) noexcept;
void SetMusicVolume(float volume) noexcept; void SetMusicVolume(float volume) noexcept;
+12
View File
@@ -42,6 +42,7 @@ struct RuntimeUserConfig {
std::optional<std::string> graphicsApi; std::optional<std::string> graphicsApi;
std::optional<std::string> displayMode; std::optional<std::string> displayMode;
std::optional<uint32_t> frameInterpolationFps; std::optional<uint32_t> frameInterpolationFps;
std::optional<bool> metalFxSpatialUpscaling;
std::optional<bool> skipUnreadyPipelines; std::optional<bool> skipUnreadyPipelines;
std::optional<bool> disableCopyFilter; std::optional<bool> disableCopyFilter;
std::optional<bool> textureReplacements; std::optional<bool> textureReplacements;
@@ -459,6 +460,8 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.frameInterpolationFps = migrated; config.frameInterpolationFps = migrated;
} }
} }
config.metalFxSpatialUpscaling =
FindConfigValue<bool>(document, "video", "metalfx_spatial_upscaling");
config.skipUnreadyPipelines = FindConfigValue<bool>(document, "video", "skip_unready_pipelines"); config.skipUnreadyPipelines = FindConfigValue<bool>(document, "video", "skip_unready_pipelines");
config.disableCopyFilter = FindConfigValue<bool>(document, "video", "disable_copy_filter"); config.disableCopyFilter = FindConfigValue<bool>(document, "video", "disable_copy_filter");
config.showFps = FindConfigValue<bool>(document, "video", "show_fps"); config.showFps = FindConfigValue<bool>(document, "video", "show_fps");
@@ -660,6 +663,11 @@ inline bool SetFrameInterpolationFps(uint32_t value) {
return WriteSetting("video", "frame_interpolation_fps", std::to_string(value)); return WriteSetting("video", "frame_interpolation_fps", std::to_string(value));
} }
inline bool SetMetalFxSpatialUpscaling(bool value) {
Mutable().metalFxSpatialUpscaling = value;
return WriteSetting("video", "metalfx_spatial_upscaling", value ? "true" : "false");
}
inline bool SetDisplayMode(std::string value) { inline bool SetDisplayMode(std::string value) {
if (!IsSupportedDisplayMode(value)) { if (!IsSupportedDisplayMode(value)) {
return false; return false;
@@ -815,6 +823,10 @@ inline float ResolutionMultiplier(float fallback = 1.0f) {
return std::max(0.0f, Get().resolutionMultiplier.value_or(fallback)); return std::max(0.0f, Get().resolutionMultiplier.value_or(fallback));
} }
inline bool MetalFxSpatialUpscaling(bool fallback = false) {
return Get().metalFxSpatialUpscaling.value_or(fallback);
}
inline float AudioVolume(float fallback = 1.0f) { inline float AudioVolume(float fallback = 1.0f) {
return std::clamp(Get().audioVolume.value_or(fallback), 0.0f, 1.0f); return std::clamp(Get().audioVolume.value_or(fallback), 0.0f, 1.0f);
} }
+87
View File
@@ -0,0 +1,87 @@
#if defined(__APPLE__)
#include "external_audio_macos.h"
#include <CoreAudio/CoreAudio.h>
#include <unistd.h>
#include <vector>
namespace MusicAttenuation {
namespace {
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 140200
constexpr auto kProcessObjectList = kAudioHardwarePropertyProcessObjectList;
constexpr auto kProcessPid = kAudioProcessPropertyPID;
constexpr auto kProcessRunningOutput = kAudioProcessPropertyIsRunningOutput;
#else
// Public Core Audio selector ABI values, for SDKs predating process objects.
// The runtime HasProperty check still determines whether the OS supports them.
constexpr AudioObjectPropertySelector kProcessObjectList = 'prs#';
constexpr AudioObjectPropertySelector kProcessPid = 'ppid';
constexpr AudioObjectPropertySelector kProcessRunningOutput = 'piro';
#endif
template <typename T>
bool ReadAudioProcessProperty(AudioObjectID object, AudioObjectPropertySelector selector,
T& value) noexcept {
const AudioObjectPropertyAddress address{
selector, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain};
UInt32 size = sizeof(value);
return AudioObjectGetPropertyData(object, &address, 0, nullptr, &size, &value) == noErr &&
size == sizeof(value);
}
} // namespace
MacOSAudioStatus QueryMacOSExternalAudio() noexcept {
const AudioObjectPropertyAddress address{
kProcessObjectList,
kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain};
// Probe the property instead of raising the game's minimum macOS version.
if (!AudioObjectHasProperty(kAudioObjectSystemObject, &address)) {
return {};
}
// Processes may start between the size and data queries. Retry a changed
// list a bounded number of times; subsequent monitor polls also retry.
for (int attempt = 0; attempt < 3; ++attempt) {
UInt32 size = 0;
if (AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &address,
0, nullptr, &size) != noErr ||
size % sizeof(AudioObjectID) != 0) {
return {};
}
if (size == 0) {
return {true, false};
}
std::vector<AudioObjectID> processes(size / sizeof(AudioObjectID));
const auto result = AudioObjectGetPropertyData(kAudioObjectSystemObject, &address,
0, nullptr, &size, processes.data());
if (result == kAudioHardwareBadPropertySizeError) {
continue;
}
if (result != noErr || size % sizeof(AudioObjectID) != 0 ||
size / sizeof(AudioObjectID) > processes.size()) {
return {};
}
const pid_t ownPid = getpid();
for (size_t index = 0; index < size / sizeof(AudioObjectID); ++index) {
pid_t pid = 0;
UInt32 runningOutput = 0;
// An exiting process may disappear mid-query. Never count an
// unknown PID or our own SDL output as external playback.
if (ReadAudioProcessProperty(processes[index], kProcessPid, pid) &&
pid > 0 && pid != ownPid &&
ReadAudioProcessProperty(processes[index], kProcessRunningOutput,
runningOutput) && runningOutput != 0) {
return {true, true};
}
}
return {true, false};
}
return {};
}
} // namespace MusicAttenuation
#endif
+129 -32
View File
@@ -2,79 +2,176 @@
#include <mach/mach.h> #include <mach/mach.h>
#include <mach/mach_vm.h> #include <mach/mach_vm.h>
#include <fcntl.h>
#include <sys/mman.h> #include <sys/mman.h>
#include <unistd.h> #include <unistd.h>
#include <algorithm> #include <algorithm>
#include <cstdio>
#include <mutex> #include <mutex>
#include <stdexcept> #include <stdexcept>
#include <string>
#include <vector> #include <vector>
namespace GuestFlat { namespace GuestFlat {
bool g_requiresCheckedAccess = false; bool g_requiresCheckedAccess = false;
namespace { namespace {
struct Mapping { uint32_t base; uint64_t size; uint8_t* host; };
struct Store {
Backing kind = Backing::Owned;
uint32_t owned = 0;
uint64_t size = 0;
uint8_t* host = nullptr;
};
struct Mapping {
uint32_t base = 0;
uint64_t size = 0;
uint8_t* host = nullptr;
};
std::mutex g_mutex; std::mutex g_mutex;
std::vector<Store> g_stores;
std::vector<Mapping> g_mappings; std::vector<Mapping> g_mappings;
std::vector<RegionRequest> g_layout; std::vector<RegionRequest> g_layout;
uint8_t* g_base = nullptr; uint8_t* g_base = nullptr;
bool g_active = false; bool g_active = false;
inline uint64_t RoundUp(uint64_t value, uint64_t align) {
return (value + align - 1) & ~(align - 1);
}
uint64_t Offset(const RegionRequest& r) { uint64_t Offset(const RegionRequest& r) {
if (r.backing == Backing::Mem1) return r.base & 0x1fffffffu; if (r.backing == Backing::Mem1) return r.base & 0x1fffffffu;
if (r.backing == Backing::Mem2) return (r.base & 0x1fffffffu) - 0x10000000u; if (r.backing == Backing::Mem2) return (r.base & 0x1fffffffu) - 0x10000000u;
return 0; return 0;
} }
bool Same(const std::vector<RegionRequest>& a, const std::vector<RegionRequest>& b) { 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(), 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; }); [](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 } // namespace
bool IsActive() { return g_active; } bool IsActive() { return g_active; }
void Initialize(const std::vector<RegionRequest>& regions) { void Initialize(const std::vector<RegionRequest>& regions) {
std::lock_guard lock(g_mutex); std::lock_guard lock(g_mutex);
g_requiresCheckedAccess = static_cast<size_t>(getpagesize()) > kGuestPageSize; const uint64_t pageSize = static_cast<uint64_t>(getpagesize());
if (g_active) { if (!Same(g_layout, regions)) throw std::runtime_error("flat guest layout cannot be remapped"); return; } g_requiresCheckedAccess = pageSize > kGuestPageSize;
if (g_active) {
if (!Same(g_layout, regions))
throw std::runtime_error("flat guest layout cannot be remapped");
return;
}
for (size_t i = 0; i < regions.size(); ++i) {
const auto& r = regions[i];
if (r.size == 0) continue;
if (r.size > kGuestSpaceSize - r.base ||
r.base % pageSize != 0 || Offset(r) % pageSize != 0) {
throw std::runtime_error("invalid flat guest memory region");
}
const uint64_t end = static_cast<uint64_t>(r.base) + RoundUp(r.size, pageSize);
for (size_t j = 0; j < i; ++j) {
const auto& prior = regions[j];
if (prior.size == 0) continue;
const uint64_t priorEnd = static_cast<uint64_t>(prior.base) + RoundUp(prior.size, pageSize);
if (r.base < priorEnd && prior.base < end) {
throw std::runtime_error("overlapping flat guest memory regions");
}
}
}
mach_vm_address_t address = kFixedFlatGuestBase; mach_vm_address_t address = kFixedFlatGuestBase;
if (mach_vm_allocate(mach_task_self(), &address, kGuestSpaceSize, VM_FLAGS_FIXED) != KERN_SUCCESS || 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"); throw std::runtime_error("unable to reserve fixed 4 GiB macOS guest address space");
}
g_base = reinterpret_cast<uint8_t*>(address); g_base = reinterpret_cast<uint8_t*>(address);
struct Store { Backing kind; uint32_t owned; uint64_t size; int fd; };
std::vector<Store> stores; std::vector<Store> stores;
for (const auto& r : regions) { try {
if (!r.size) continue; for (const auto& r : regions) {
const uint32_t owned = r.backing == Backing::Owned ? r.base : 0; if (!r.size) continue;
auto it = std::find_if(stores.begin(), stores.end(), [&](const Store& s) { return s.kind == r.backing && s.owned == owned; }); const uint32_t owned = r.backing == Backing::Owned ? r.base : 0;
const uint64_t need = Offset(r) + r.size; auto it = std::find_if(stores.begin(), stores.end(), [&](const Store& s) {
if (it == stores.end()) stores.push_back({r.backing, owned, need, -1}); else it->size = std::max(it->size, need); return s.kind == r.backing && s.owned == owned;
});
const uint64_t need = RoundUp(Offset(r) + r.size, pageSize);
if (it == stores.end()) {
stores.push_back({r.backing, owned, need, nullptr});
} else {
it->size = std::max(it->size, need);
}
}
for (auto& s : stores) {
void* host = mmap(nullptr, static_cast<size_t>(s.size), PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANON, -1, 0);
if (host == MAP_FAILED) {
throw std::runtime_error("unable to allocate anonymous macOS guest backing store");
}
s.host = static_cast<uint8_t*>(host);
}
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;
});
mach_vm_address_t target = reinterpret_cast<mach_vm_address_t>(g_base + r.base);
vm_prot_t cur = VM_PROT_NONE, max = VM_PROT_NONE;
const kern_return_t kr = mach_vm_remap(
mach_task_self(),
&target,
static_cast<mach_vm_size_t>(RoundUp(r.size, pageSize)),
0,
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,
mach_task_self(),
reinterpret_cast<mach_vm_address_t>(s.host + Offset(r)),
FALSE,
&cur,
&max,
VM_INHERIT_NONE
);
if (kr != KERN_SUCCESS) {
throw std::runtime_error(std::string("mach_vm_remap guest alias failed: ") + mach_error_string(kr));
}
g_mappings.push_back({r.base, r.size, s.host + Offset(r)});
}
g_layout = regions;
g_stores = std::move(stores);
g_active = true;
} catch (...) {
for (auto& s : stores) {
if (s.host != nullptr) {
munmap(s.host, static_cast<size_t>(s.size));
}
}
mach_vm_deallocate(mach_task_self(), address, kGuestSpaceSize);
g_mappings.clear();
g_stores.clear();
g_layout.clear();
g_base = nullptr;
throw;
} }
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; }
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 ProtectDeferredRange(uint32_t, size_t) {}
void UnprotectDeferredRange(uint32_t, size_t) {} void UnprotectDeferredRange(uint32_t, size_t) {}
void RegisterExecutableRange(uint32_t, uint32_t) {} void RegisterExecutableRange(uint32_t, uint32_t) {}
FaultCounters Counters() { return {}; } FaultCounters Counters() { return {}; }
void LogFaultSummary() noexcept {} void LogFaultSummary() noexcept {}
bool HandleAccessViolation(void*, bool) noexcept { return false; } bool HandleAccessViolation(void*, bool) noexcept { return false; }
} // namespace GuestFlat } // namespace GuestFlat
+2 -2
View File
@@ -14,7 +14,7 @@
extern "C" void mkw_co_switch(void** targetSp, void** sourceSp); extern "C" void mkw_co_switch(void** targetSp, void** sourceSp);
extern "C" void* mkw_co_init(void* stackTop, void (*entry)(void*), void* argument); extern "C" void* mkw_co_init(void* stackTop, void (*entry)(void*), void* argument);
#elif defined(__linux__) #elif defined(__linux__) || (defined(__APPLE__) && defined(__x86_64__))
#include <libco.h> #include <libco.h>
#include <cstdlib> #include <cstdlib>
@@ -158,7 +158,7 @@ void Switch(Handle target)
g_current = source; g_current = source;
} }
#elif defined(__linux__) #elif defined(__linux__) || (defined(__APPLE__) && defined(__x86_64__))
namespace { namespace {
struct Context { struct Context {
+11 -2
View File
@@ -44,10 +44,15 @@
#include <signal.h> #include <signal.h>
#if defined(__x86_64__) #if defined(__x86_64__)
// Only the x86 POSIX fault path inspects ucontext_t to recover the page-fault // Only the x86 POSIX fault path inspects ucontext_t to recover the page-fault
// write bit. macOS deprecates ucontext and requires _XOPEN_SOURCE just to // write bit. macOS exposes the signal-handler context through sys/ucontext.h;
// include the header, while the arm64 handler does not use it at all. // avoid ucontext.h itself because its deprecated user-context APIs require
// _XOPEN_SOURCE. The arm64 handler does not inspect a host context at all.
#if defined(__APPLE__)
#include <sys/ucontext.h>
#else
#include <ucontext.h> #include <ucontext.h>
#endif #endif
#endif
#include <unistd.h> #include <unistd.h>
#endif #endif
@@ -1128,7 +1133,11 @@ void PosixMemoryFaultHandler(int sig, siginfo_t* info, void* ucontextVoid) {
// error code x86 pushes on a page fault records whether it was a write. // error code x86 pushes on a page fault records whether it was a write.
if (ucontextVoid != nullptr) { if (ucontextVoid != nullptr) {
auto* uc = static_cast<ucontext_t*>(ucontextVoid); auto* uc = static_cast<ucontext_t*>(ucontextVoid);
#if defined(__APPLE__)
isWrite = uc->uc_mcontext != nullptr && (uc->uc_mcontext->__es.__err & 0x2) != 0;
#else
isWrite = (uc->uc_mcontext.gregs[REG_ERR] & 0x2) != 0; isWrite = (uc->uc_mcontext.gregs[REG_ERR] & 0x2) != 0;
#endif
} }
#endif #endif
+35 -4
View File
@@ -4,9 +4,9 @@
#include <array> #include <array>
#include <atomic> #include <atomic>
#include <bit>
#include <chrono> #include <chrono>
#include <cstdint> #include <cstdint>
#include <cstring>
#include <iostream> #include <iostream>
#include <mutex> #include <mutex>
#include <thread> #include <thread>
@@ -16,6 +16,8 @@
#include <winrt/Windows.Foundation.h> #include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h> #include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Media.Control.h> #include <winrt/Windows.Media.Control.h>
#elif defined(__APPLE__)
#include "external_audio_macos.h"
#elif defined(__linux__) #elif defined(__linux__)
#include <dlfcn.h> #include <dlfcn.h>
@@ -52,6 +54,20 @@ std::array<float, kSoundPlayerCount> g_requestedSoundPlayerVolumes{};
std::array<float, kSoundPlayerCount> g_lastAppliedSoundPlayerVolumes{}; std::array<float, kSoundPlayerCount> g_lastAppliedSoundPlayerVolumes{};
std::array<bool, kSoundPlayerCount> g_haveSoundPlayerVolumes{}; std::array<bool, kSoundPlayerCount> g_haveSoundPlayerVolumes{};
uint32_t FloatBits(float value) noexcept {
static_assert(sizeof(float) == sizeof(uint32_t));
uint32_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
return bits;
}
float BitsFloat(uint32_t bits) noexcept {
static_assert(sizeof(float) == sizeof(uint32_t));
float value = 0.0f;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
float ClampSoundPlayerVolume(float volume) noexcept { float ClampSoundPlayerVolume(float volume) noexcept {
// Match nw4r::snd::SoundPlayer::SetVolume at 0x800A35E0 exactly, // Match nw4r::snd::SoundPlayer::SetVolume at 0x800A35E0 exactly,
// including its NaN behavior (unordered compares select the upper bound). // including its NaN behavior (unordered compares select the upper bound).
@@ -63,7 +79,7 @@ float ClampSoundPlayerVolume(float volume) noexcept {
bool WriteGuestFloat(uint32_t address, float value) noexcept { bool WriteGuestFloat(uint32_t address, float value) noexcept {
try { try {
Memory::Write32(address, std::bit_cast<uint32_t>(value)); Memory::Write32(address, FloatBits(value));
return true; return true;
} catch (const Memory::AccessViolation&) { } catch (const Memory::AccessViolation&) {
return false; return false;
@@ -75,7 +91,7 @@ bool ReadGuestFloat(uint32_t address, float& value) noexcept {
if (!Memory::TryRead32(address, bits)) { if (!Memory::TryRead32(address, bits)) {
return false; return false;
} }
value = std::bit_cast<float>(bits); value = BitsFloat(bits);
return true; return true;
} }
@@ -448,6 +464,19 @@ void MonitorLinuxMprisSessions() noexcept {
} }
#endif #endif
#if defined(__APPLE__)
void MonitorMacOSAudio() noexcept {
using namespace std::chrono_literals;
for (;;) {
const auto status = QueryMacOSExternalAudio();
g_externalMediaPlaying.store(status.playing, std::memory_order_release);
g_mediaControlAvailable.store(status.available, std::memory_order_release);
g_mediaControlInitializationComplete.store(true, std::memory_order_release);
std::this_thread::sleep_for(250ms);
}
}
#endif
void StartMonitor() noexcept { void StartMonitor() noexcept {
#if defined(_WIN32) #if defined(_WIN32)
// The process owns this monitor for its remaining lifetime. Keeping it // The process owns this monitor for its remaining lifetime. Keeping it
@@ -456,6 +485,8 @@ void StartMonitor() noexcept {
#elif defined(__linux__) #elif defined(__linux__)
// Detached so there is no shutdown ordering to manage against static audio state. // Detached so there is no shutdown ordering to manage against static audio state.
std::thread(MonitorLinuxMprisSessions).detach(); std::thread(MonitorLinuxMprisSessions).detach();
#elif defined(__APPLE__)
std::thread(MonitorMacOSAudio).detach();
#else #else
g_mediaControlAvailable.store(false, std::memory_order_release); g_mediaControlAvailable.store(false, std::memory_order_release);
g_mediaControlInitializationComplete.store(true, std::memory_order_release); g_mediaControlInitializationComplete.store(true, std::memory_order_release);
@@ -558,7 +589,7 @@ void SetSoundPlayerVolume(uint32_t soundPlayer, float requestedVolume) {
} }
// Preserve the original function's access semantics. An invalid player is // Preserve the original function's access semantics. An invalid player is
// a guest bug and must not be converted into a silent successful call. // a guest bug and must not be converted into a silent successful call.
Memory::Write32(soundPlayer + kSoundPlayerVolumeOffset, std::bit_cast<uint32_t>(applied)); Memory::Write32(soundPlayer + kSoundPlayerVolumeOffset, FloatBits(applied));
} }
} // namespace MusicAttenuation } // namespace MusicAttenuation
+4 -4
View File
@@ -833,8 +833,8 @@ extern "C" double PPC_PsqL(uint32_t addr, uint32_t w, uint32_t i)
} }
const uint32_t gqr = cpu->gqr[i & 7]; const uint32_t gqr = cpu->gqr[i & 7];
return w == 0 ? PPC_PsqLStateFallback<0u, 0u, false>(gqr, addr) return w == 0 ? PPC_PsqLStateFallback<0u, false>(gqr, addr)
: PPC_PsqLStateFallback<1u, 0u, false>(gqr, addr); : PPC_PsqLStateFallback<1u, false>(gqr, addr);
} }
extern "C" void PPC_PsqSt(uint32_t addr, double value, uint32_t w, uint32_t i) extern "C" void PPC_PsqSt(uint32_t addr, double value, uint32_t w, uint32_t i)
@@ -848,11 +848,11 @@ extern "C" void PPC_PsqSt(uint32_t addr, double value, uint32_t w, uint32_t i)
const uint32_t gqr = cpu->gqr[i & 7]; const uint32_t gqr = cpu->gqr[i & 7];
if (w == 0) if (w == 0)
{ {
PPC_PsqStStateFallback<0u, 0u, false>(gqr, addr, value); PPC_PsqStStateFallback<0u, false>(gqr, addr, value);
} }
else else
{ {
PPC_PsqStStateFallback<1u, 0u, false>(gqr, addr, value); PPC_PsqStStateFallback<1u, false>(gqr, addr, value);
} }
} }
+154
View File
@@ -0,0 +1,154 @@
#include "isa/ppc_isa_quantized.h"
#if defined(__FAST_MATH__) || __FINITE_MATH_ONLY__
#error "PSQ helpers require strict PPC floating-point options"
#endif
template <uint32_t W, bool Stack>
MKW_PPC_NO_INLINE MKW_PPC_COLD double PPC_PsqLStateFallback(uint32_t gqr, uint32_t addr)
{
static_assert(W <= 1u);
const uint32_t type = (gqr >> 16) & 0x7u;
const uint32_t scale = (gqr >> 24) & 0x3Fu;
if constexpr (W == 0u)
{
switch (type)
{
case 0u: return Stack ? PpcLoadPairPsqFloatStackInline(addr) : PpcLoadPairPsqFloatFastInline(addr);
case 4u: return Stack ? PpcLoadPairPsqIntegerStackInline<uint8_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<uint8_t>(addr, scale);
case 5u: return Stack ? PpcLoadPairPsqIntegerStackInline<uint16_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<uint16_t>(addr, scale);
case 6u: return Stack ? PpcLoadPairPsqIntegerStackInline<int8_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<int8_t>(addr, scale);
case 7u: return Stack ? PpcLoadPairPsqIntegerStackInline<int16_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<int16_t>(addr, scale);
default: std::abort();
}
}
else
{
switch (type)
{
case 0u: return Stack ? PpcLoadSinglePsqFloatStackInline(addr) : PpcLoadSinglePsqFloatFastInline(addr);
case 4u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<uint8_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<uint8_t>(addr, scale);
case 5u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<uint16_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<uint16_t>(addr, scale);
case 6u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<int8_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<int8_t>(addr, scale);
case 7u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<int16_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<int16_t>(addr, scale);
default: std::abort();
}
}
}
template <uint32_t W, bool Stack>
MKW_PPC_NO_INLINE MKW_PPC_COLD void PPC_PsqStStateFallback(uint32_t gqr, uint32_t addr, double value)
{
static_assert(W <= 1u);
const uint32_t type = gqr & 0x7u;
const uint32_t scale = (gqr >> 8) & 0x3Fu;
if constexpr (W == 0u)
{
switch (type)
{
case 0u: Stack ? PpcStorePairPsqFloatStackInline(addr, value) : PpcStorePairPsqFloatFastInline(addr, value); return;
case 4u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<uint8_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<uint8_t>(addr, value, scale); return;
case 5u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<uint16_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<uint16_t>(addr, value, scale); return;
case 6u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<int8_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<int8_t>(addr, value, scale); return;
case 7u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<int16_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<int16_t>(addr, value, scale); return;
default: std::abort();
}
}
else
{
switch (type)
{
case 0u: if constexpr (Stack) PpcStoreSinglePsqFloatStackInline(addr, value); else PpcStoreSinglePsqFloatFastInline(addr, value); return;
case 4u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<uint8_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<uint8_t>(addr, value, scale); return;
case 5u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<uint16_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<uint16_t>(addr, value, scale); return;
case 6u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<int8_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<int8_t>(addr, value, scale); return;
case 7u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<int16_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<int16_t>(addr, value, scale); return;
default: std::abort();
}
}
}
template <uint32_t W>
MKW_PPC_NO_INLINE MKW_PPC_COLD double PPC_PsqLResolvedStateFallback(
uint32_t gqr, uint8_t* resolvedHost, uint32_t offset, uint32_t addr)
{
static_assert(W <= 1u);
if (!resolvedHost) [[unlikely]] return PPC_PsqLStateInline<W, 0u, false>(gqr, addr);
const uint32_t type = (gqr >> 16) & 0x7u;
const uint32_t scale = (gqr >> 24) & 0x3Fu;
if constexpr (W == 0u)
{
switch (type)
{
case 0u: return PpcLoadPairPsqFloatResolvedInline(resolvedHost, offset, addr);
case 4u: return PpcLoadPairPsqIntegerResolvedInline<uint8_t>(resolvedHost, offset, addr, scale);
case 5u: return PpcLoadPairPsqIntegerResolvedInline<uint16_t>(resolvedHost, offset, addr, scale);
case 6u: return PpcLoadPairPsqIntegerResolvedInline<int8_t>(resolvedHost, offset, addr, scale);
case 7u: return PpcLoadPairPsqIntegerResolvedInline<int16_t>(resolvedHost, offset, addr, scale);
default: std::abort();
}
}
else
{
switch (type)
{
case 0u: return PpcLoadSinglePsqFloatResolvedInline(resolvedHost, offset, addr);
case 4u: return PpcLoadSinglePsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, scale);
case 5u: return PpcLoadSinglePsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, scale);
case 6u: return PpcLoadSinglePsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, scale);
case 7u: return PpcLoadSinglePsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, scale);
default: std::abort();
}
}
}
template <uint32_t W>
MKW_PPC_NO_INLINE MKW_PPC_COLD void PPC_PsqStResolvedStateFallback(
uint32_t gqr, uint8_t* resolvedHost, uint32_t offset, uint32_t addr, double value)
{
static_assert(W <= 1u);
if (!resolvedHost) [[unlikely]]
{
PPC_PsqStStateInline<W, 0u, false>(gqr, addr, value);
return;
}
const uint32_t type = gqr & 0x7u;
const uint32_t scale = (gqr >> 8) & 0x3Fu;
if constexpr (W == 0u)
{
switch (type)
{
case 0u: PpcStorePairPsqFloatResolvedInline(resolvedHost, offset, addr, value); return;
case 4u: PpcStorePairPsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, value, scale); return;
case 5u: PpcStorePairPsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, value, scale); return;
case 6u: PpcStorePairPsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, value, scale); return;
case 7u: PpcStorePairPsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, value, scale); return;
default: std::abort();
}
}
else
{
switch (type)
{
case 0u: PpcStoreSinglePsqFloatResolvedInline(resolvedHost, offset, addr, value); return;
case 4u: PpcStoreSinglePsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, value, scale); return;
case 5u: PpcStoreSinglePsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, value, scale); return;
case 6u: PpcStoreSinglePsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, value, scale); return;
case 7u: PpcStoreSinglePsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, value, scale); return;
default: std::abort();
}
}
}
template double PPC_PsqLStateFallback<0u, false>(uint32_t, uint32_t);
template void PPC_PsqStStateFallback<0u, false>(uint32_t, uint32_t, double);
template double PPC_PsqLStateFallback<0u, true>(uint32_t, uint32_t);
template void PPC_PsqStStateFallback<0u, true>(uint32_t, uint32_t, double);
template double PPC_PsqLResolvedStateFallback<0u>(uint32_t, uint8_t*, uint32_t, uint32_t);
template void PPC_PsqStResolvedStateFallback<0u>(uint32_t, uint8_t*, uint32_t, uint32_t, double);
template double PPC_PsqLStateFallback<1u, false>(uint32_t, uint32_t);
template void PPC_PsqStStateFallback<1u, false>(uint32_t, uint32_t, double);
template double PPC_PsqLStateFallback<1u, true>(uint32_t, uint32_t);
template void PPC_PsqStStateFallback<1u, true>(uint32_t, uint32_t, double);
template double PPC_PsqLResolvedStateFallback<1u>(uint32_t, uint8_t*, uint32_t, uint32_t);
template void PPC_PsqStResolvedStateFallback<1u>(uint32_t, uint8_t*, uint32_t, uint32_t, double);
+48 -1
View File
@@ -111,6 +111,9 @@ bool g_skipUnreadyPipelines = RuntimeConfigFile::SkipUnreadyPipelines(true);
bool g_disableCopyFilter = RuntimeConfigFile::DisableCopyFilter(true); bool g_disableCopyFilter = RuntimeConfigFile::DisableCopyFilter(true);
bool g_showFps = RuntimeConfigFile::ShowFps(true); bool g_showFps = RuntimeConfigFile::ShowFps(true);
bool g_forceAspect169 = RuntimeConfigFile::ForceAspect169Enabled(); bool g_forceAspect169 = RuntimeConfigFile::ForceAspect169Enabled();
#if defined(__APPLE__)
bool g_metalFxSpatialUpscaling = RuntimeConfigFile::MetalFxSpatialUpscaling(false);
#endif
uint32_t g_disabledPostProcessingPaths = RuntimeConfigFile::DisabledPostProcessingPaths(0); uint32_t g_disabledPostProcessingPaths = RuntimeConfigFile::DisabledPostProcessingPaths(0);
std::array<int32_t, PAD_MAX_CONTROLLERS> g_configuredControllerIndices = [] { std::array<int32_t, PAD_MAX_CONTROLLERS> g_configuredControllerIndices = [] {
std::array<int32_t, PAD_MAX_CONTROLLERS> indices{}; std::array<int32_t, PAD_MAX_CONTROLLERS> indices{};
@@ -998,13 +1001,24 @@ void DrawAudioSettings() {
MusicAttenuation::SetEnabled(g_attenuateMusicWhenMediaPlays); MusicAttenuation::SetEnabled(g_attenuateMusicWhenMediaPlays);
RuntimeConfigFile::SetAttenuateMusicWhenMediaPlays(g_attenuateMusicWhenMediaPlays); RuntimeConfigFile::SetAttenuateMusicWhenMediaPlays(g_attenuateMusicWhenMediaPlays);
} }
#if defined(__APPLE__)
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Detects other apps with active audio output on macOS 14.2 or later. "
"Apps that keep an output stream running silently may keep game music muted.");
}
#endif
if (g_attenuateMusicWhenMediaPlays) { if (g_attenuateMusicWhenMediaPlays) {
if (MusicAttenuation::IsExternalMediaPlaying()) { if (MusicAttenuation::IsExternalMediaPlaying()) {
ImGui::TextDisabled("External media is playing; game music is muted."); ImGui::TextDisabled("External media is playing; game music is muted.");
} else if (!MusicAttenuation::IsMediaControlInitializationComplete()) { } else if (!MusicAttenuation::IsMediaControlInitializationComplete()) {
ImGui::TextDisabled("Waiting for media controls..."); ImGui::TextDisabled("Checking external audio...");
} else if (!MusicAttenuation::IsMediaControlAvailable()) { } else if (!MusicAttenuation::IsMediaControlAvailable()) {
#if defined(__APPLE__)
ImGui::TextDisabled("External audio detection unavailable (requires macOS 14.2 or later).");
#else
ImGui::TextDisabled("Media controls are unavailable."); ImGui::TextDisabled("Media controls are unavailable.");
#endif
} else { } else {
ImGui::TextDisabled("No external media is currently playing."); ImGui::TextDisabled("No external media is currently playing.");
} }
@@ -1093,6 +1107,36 @@ void DrawGraphicsSettings() {
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f); ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::TextDisabled("Frame interpolation is experimental, you might find visual artifacts"); ImGui::TextDisabled("Frame interpolation is experimental, you might find visual artifacts");
ImGui::PopTextWrapPos(); ImGui::PopTextWrapPos();
#if defined(__APPLE__)
const bool metalFxSupported = aurora_is_metalfx_spatial_supported();
ImGui::BeginDisabled(!metalFxSupported);
if (ImGui::Checkbox("MetalFX spatial upscaling", &g_metalFxSpatialUpscaling)) {
aurora_set_metalfx_spatial(g_metalFxSpatialUpscaling);
RuntimeConfigFile::SetMetalFxSpatialUpscaling(g_metalFxSpatialUpscaling);
}
ImGui::EndDisabled();
switch (aurora_get_metalfx_status()) {
case AURORA_METALFX_ACTIVE:
ImGui::TextDisabled("Active: upscaling the game image before the overlay.");
break;
case AURORA_METALFX_NOT_UPSCALING:
ImGui::TextDisabled("Choose a lower internal resolution to use MetalFX.");
break;
case AURORA_METALFX_UNSUPPORTED:
ImGui::TextDisabled("Requires macOS 13+, Metal, and a MetalFX-capable GPU.");
break;
case AURORA_METALFX_ERROR:
ImGui::TextDisabled("Unavailable after a renderer error; toggle off and on to retry.");
break;
case AURORA_METALFX_DISABLED:
if (metalFxSupported) {
ImGui::TextDisabled("Render below output resolution for sharper lower-cost output.");
} else {
ImGui::TextDisabled("MetalFX spatial upscaling is unavailable on this device.");
}
break;
}
#endif
if (ImGui::Checkbox("Disable copy filter", &g_disableCopyFilter)) { if (ImGui::Checkbox("Disable copy filter", &g_disableCopyFilter)) {
aurora_set_disable_copy_filter(g_disableCopyFilter); aurora_set_disable_copy_filter(g_disableCopyFilter);
RuntimeConfigFile::SetDisableCopyFilter(g_disableCopyFilter); RuntimeConfigFile::SetDisableCopyFilter(g_disableCopyFilter);
@@ -1397,6 +1441,9 @@ void InitializeRuntimeSettings() noexcept {
MusicAttenuation::SetVoicesVolume(static_cast<float>(g_voicesVolumePercent) / 100.0f); MusicAttenuation::SetVoicesVolume(static_cast<float>(g_voicesVolumePercent) / 100.0f);
MusicAttenuation::SetEnabled(g_attenuateMusicWhenMediaPlays); MusicAttenuation::SetEnabled(g_attenuateMusicWhenMediaPlays);
RuntimeGameGraphicsOptions::SetDisabledPostProcessingPaths(g_disabledPostProcessingPaths); RuntimeGameGraphicsOptions::SetDisabledPostProcessingPaths(g_disabledPostProcessingPaths);
#if defined(__APPLE__)
aurora_set_metalfx_spatial(g_metalFxSpatialUpscaling);
#endif
const uint32_t targetFps = kFrameInterpolationTargetFps[static_cast<size_t>(g_frameInterpolationMode)]; const uint32_t targetFps = kFrameInterpolationTargetFps[static_cast<size_t>(g_frameInterpolationMode)];
LimitResolutionForFrameRate(); LimitResolutionForFrameRate();
aurora_set_frame_interpolation_fps(targetFps); aurora_set_frame_interpolation_fps(targetFps);
@@ -0,0 +1,109 @@
#include "external_audio_macos.h"
#include <CoreAudio/CoreAudio.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <stdexcept>
#include <vector>
namespace {
struct Process {
pid_t pid;
UInt32 output;
bool disappeared = false;
};
std::vector<Process> processes;
bool supported = true;
bool queryFailed = false;
int sizeRaces = 0;
void Require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
void Expect(bool available, bool playing, const char* message) {
const auto status = MusicAttenuation::QueryMacOSExternalAudio();
Require(status.available == available && status.playing == playing, message);
}
} // namespace
// Fake only the Core Audio boundary so the production enumeration and process
// filtering run unchanged, without depending on other apps on the test machine.
// Use the public selector ABI values so these mocks also build with older SDKs.
extern "C" Boolean AudioObjectHasProperty(AudioObjectID object,
const AudioObjectPropertyAddress* address) {
Require(object == kAudioObjectSystemObject &&
address->mSelector == 'prs#',
"Must query the system process list");
return supported;
}
extern "C" OSStatus AudioObjectGetPropertyDataSize(
AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* size) {
if (queryFailed) return kAudioHardwareUnspecifiedError;
*size = static_cast<UInt32>(processes.size() * sizeof(AudioObjectID));
return noErr;
}
extern "C" OSStatus AudioObjectGetPropertyData(
AudioObjectID object, const AudioObjectPropertyAddress* address, UInt32,
const void*, UInt32* size, void* data) {
Require(address->mScope == kAudioObjectPropertyScopeGlobal, "Must use global scope");
if (object == kAudioObjectSystemObject) {
if (sizeRaces > 0) {
--sizeRaces;
return kAudioHardwareBadPropertySizeError;
}
for (size_t i = 0; i < processes.size(); ++i) {
static_cast<AudioObjectID*>(data)[i] = static_cast<AudioObjectID>(i + 100);
}
*size = static_cast<UInt32>(processes.size() * sizeof(AudioObjectID));
return noErr;
}
const auto& process = processes.at(object - 100);
if (process.disappeared) return kAudioHardwareBadObjectError;
if (address->mSelector == 'ppid') {
std::memcpy(data, &process.pid, sizeof(process.pid));
*size = sizeof(process.pid);
} else {
Require(address->mSelector == 'piro',
"Input-only activity must not count as playback");
Require(process.pid != getpid(), "Must exclude the game's own output");
std::memcpy(data, &process.output, sizeof(process.output));
*size = sizeof(process.output);
}
return noErr;
}
int main() {
supported = false;
Expect(false, false, "Older macOS must report unavailable");
supported = true;
Expect(true, false, "An empty process list is available but inactive");
processes = {{getpid(), 1}};
Expect(true, false, "Game audio alone must not trigger muting");
processes.push_back({getpid() + 1, 0});
Expect(true, false, "An idle or input-only app must not trigger muting");
processes.back().output = 1;
Expect(true, true, "External playback must trigger muting");
processes.back().output = 0;
Expect(true, false, "Stopping playback must restore music");
processes.back().output = 1;
processes.back().disappeared = true;
Expect(true, false, "Exiting apps must not leave music muted");
processes.push_back({getpid() + 2, 1});
Expect(true, true, "A disappearing app must not hide another active player");
sizeRaces = 1;
Expect(true, true, "A growing process list must be retried");
sizeRaces = 3;
Expect(false, false, "List retries must be bounded");
Expect(true, true, "Later polls must recover from list races");
queryFailed = true;
Expect(false, false, "Query failure must clear playing state");
queryFailed = false;
Expect(true, true, "Monitoring must recover from query failure");
processes = {{0, 1}};
Expect(true, false, "Unknown PIDs must not count as external playback");
std::cout << "macOS external audio tests passed\n";
}
+140
View File
@@ -0,0 +1,140 @@
#include "isa/ppc_isa_quantized.h"
#include <csignal>
#include <iostream>
#include <stdexcept>
#include <string>
static uint64_t checksum = 0;
static unsigned checks = 0;
static void Require(bool condition, const char* message) {
++checks;
if (!condition) throw std::runtime_error(message);
}
static void Hash(uint64_t value) { checksum = (checksum ^ value) * 1099511628211ull; }
template <uint32_t W> static double ReferenceLoad(uint32_t type, uint32_t scale, uint32_t addr) {
if constexpr (W == 0u) {
switch (type) {
case 0: return PpcLoadPairPsqFloatFastInline(addr);
case 4: return PpcLoadPairPsqIntegerFastInline<uint8_t>(addr, scale);
case 5: return PpcLoadPairPsqIntegerFastInline<uint16_t>(addr, scale);
case 6: return PpcLoadPairPsqIntegerFastInline<int8_t>(addr, scale);
case 7: return PpcLoadPairPsqIntegerFastInline<int16_t>(addr, scale);
}
} else {
switch (type) {
case 0: return PpcLoadSinglePsqFloatFastInline(addr);
case 4: return PpcLoadSinglePsqQuantizedFastInline<uint8_t>(addr, scale);
case 5: return PpcLoadSinglePsqQuantizedFastInline<uint16_t>(addr, scale);
case 6: return PpcLoadSinglePsqQuantizedFastInline<int8_t>(addr, scale);
case 7: return PpcLoadSinglePsqQuantizedFastInline<int16_t>(addr, scale);
}
}
std::abort();
}
template <uint32_t W> static void ReferenceStore(uint32_t type, uint32_t scale, uint32_t addr, double value) {
if constexpr (W == 0u) {
switch (type) {
case 0: PpcStorePairPsqFloatFastInline(addr, value); return;
case 4: PpcStorePairPsqQuantizedFastInline<uint8_t>(addr, value, scale); return;
case 5: PpcStorePairPsqQuantizedFastInline<uint16_t>(addr, value, scale); return;
case 6: PpcStorePairPsqQuantizedFastInline<int8_t>(addr, value, scale); return;
case 7: PpcStorePairPsqQuantizedFastInline<int16_t>(addr, value, scale); return;
}
} else {
switch (type) {
case 0: PpcStoreSinglePsqFloatFastInline(addr, value); return;
case 4: PpcStoreSinglePsqQuantizedFastInline<uint8_t>(addr, value, scale); return;
case 5: PpcStoreSinglePsqQuantizedFastInline<uint16_t>(addr, value, scale); return;
case 6: PpcStoreSinglePsqQuantizedFastInline<int8_t>(addr, value, scale); return;
case 7: PpcStoreSinglePsqQuantizedFastInline<int16_t>(addr, value, scale); return;
}
}
std::abort();
}
template <uint32_t W, uint32_t I> static void Check() {
constexpr uint32_t edges[] = {
0, 0x80000000u, 1, 0x80000001u, 0x007FFFFFu, 0x00800000u,
0x3F000000u, 0xBF000000u, 0x3F800000u, 0xBF800000u,
0x437F0000u, 0x477FFF00u, 0xC7000000u, 0x7F7FFFFFu,
0x7F800000u, 0xFF800000u, 0x7F800001u, 0x7FC01234u, 0xFFC01234u
};
for (uint32_t type : {0u, 4u, 5u, 6u, 7u}) for (uint32_t scale = 0; scale < 64; ++scale)
for (unsigned n = 0; n < std::size(edges); ++n) {
// Include ignored GQR bits and a different type/scale in the unused half.
const uint32_t half = (scale << 8) | type | 0xC0F8u;
const uint32_t loadGqr = (half << 16) | 0x2105u;
const uint32_t storeGqr = half | 0x21050000u;
const uint64_t raw = (uint64_t(edges[n]) << 32) | edges[(n + 7) % std::size(edges)];
const double value = PpcBitCastToDoubleInline(raw);
for (unsigned mode = 0; mode < 5; ++mode) {
const uint32_t addr = mode == 0 ? 32u : mode == 1 ? 0xCC008000u : mode == 2 ? 0u : 0xFFFFFF00u;
constexpr uint32_t offset = 17;
PsqTestMemory::directStack = mode == 4;
PsqTestMemory::bytes.fill(0xCD);
BigEndian::Write64(PsqTestMemory::Pointer(addr), raw);
const auto expected = PpcBitCastToU64Inline(ReferenceLoad<W>(type, scale, addr));
PsqTestMemory::accesses = 0;
double loaded;
if (mode == 0) loaded = PPC_PsqLResolvedStateInline<W, I>(loadGqr, PsqTestMemory::Pointer(addr) - offset, offset, addr);
else if (mode == 1) loaded = PPC_PsqLResolvedStateInline<W, I>(loadGqr, nullptr, offset, addr);
else if (mode == 2) loaded = PPC_PsqLStateInline<W, I, false>(loadGqr, addr);
else loaded = PPC_PsqLStateInline<W, I, true>(loadGqr, addr);
Require(PpcBitCastToU64Inline(loaded) == expected, "load bits / lane order");
Hash(expected);
const size_t width = (type == 0 ? 4u : (type == 4 || type == 6) ? 1u : 2u) * (2u - W);
Require(PsqTestMemory::accesses == ((mode == 0 || mode == 4) ? 0u : 1u), "load route");
if (PsqTestMemory::accesses) Require(PsqTestMemory::address == addr && PsqTestMemory::width == width, "load slow address / width");
PsqTestMemory::bytes.fill(0xCD);
ReferenceStore<W>(type, scale, addr, value);
const auto expectedBytes = PsqTestMemory::bytes;
PsqTestMemory::bytes.fill(0xCD);
PsqTestMemory::accesses = 0;
if (mode == 0) PPC_PsqStResolvedStateInline<W, I>(storeGqr, PsqTestMemory::Pointer(addr) - offset, offset, addr, value);
else if (mode == 1) PPC_PsqStResolvedStateInline<W, I>(storeGqr, nullptr, offset, addr, value);
else if (mode == 2) PPC_PsqStStateInline<W, I, false>(storeGqr, addr, value);
else PPC_PsqStStateInline<W, I, true>(storeGqr, addr, value);
Require(PsqTestMemory::bytes == expectedBytes, "store bits / untouched lanes");
Hash(PsqTestMemory::ReadHost<uint64_t>(PsqTestMemory::Pointer(addr)));
Require(PsqTestMemory::accesses == ((mode == 0 || mode == 4) ? 0u : 1u), "store route");
if (PsqTestMemory::accesses) Require(PsqTestMemory::address == addr && PsqTestMemory::width == width, "store slow address / width");
}
}
}
int main(int argc, char** argv) {
try {
if (argc > 1) {
std::signal(SIGABRT, [](int) { std::_Exit(86); });
const uint32_t type = static_cast<uint32_t>(std::stoul(argv[2]));
const bool single = std::string(argv[3]) == "1";
uint8_t* host = std::string(argv[4]) == "resolved" ? PsqTestMemory::bytes.data() : nullptr;
if (std::string(argv[1]) == "load") {
if (single) PPC_PsqLResolvedStateInline<1, 7>(type << 16, host, 0, 0);
else PPC_PsqLResolvedStateInline<0, 7>(type << 16, host, 0, 0);
} else {
if (single) PPC_PsqStResolvedStateInline<1, 7>(type, host, 0, 0, 0);
else PPC_PsqStResolvedStateInline<0, 7>(type, host, 0, 0, 0);
}
return 0;
}
const auto saved = MkwGetHostFpControl();
CpuContext ctx{};
for (uint32_t ni : {0u, 4u}) for (uint32_t rounding = 0; rounding < 4; ++rounding) {
ctx.fpscr = ni;
CpuContextScope scope(&ctx);
#if defined(__x86_64__)
MkwSetHostFpControl((MkwGetHostFpControl() & ~(3u << 13)) | (rounding << 13));
#endif
Check<0, 0>(); Check<1, 0>(); Check<0, 7>(); Check<1, 7>();
}
MkwRestoreHostMxcsr(saved);
std::cout << "passed " << checks << " checks, checksum " << std::hex << checksum << '\n';
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
+83
View File
@@ -0,0 +1,83 @@
#pragma once
// Instrumented ISA memory seam: no guest VM reservation or GPU is needed.
#include "big_endian.h"
#include <array>
#include <cstdint>
#include <cstring>
namespace PsqTestMemory {
inline std::array<uint8_t, 512> bytes{};
inline uint32_t address = 0;
inline size_t width = 0;
inline unsigned accesses = 0;
inline bool directStack = false;
inline uint8_t* Pointer(uint32_t addr) { return bytes.data() + (addr & 255u); }
inline void Record(uint32_t addr, size_t size) { address = addr; width = size; ++accesses; }
template <typename T> T ReadHost(const uint8_t* host) {
if constexpr (sizeof(T) == 1) return *host;
else if constexpr (sizeof(T) == 2) return BigEndian::Read16(host);
else if constexpr (sizeof(T) == 4) return BigEndian::Read32(host);
else return (uint64_t(BigEndian::Read32(host)) << 32) | BigEndian::Read32(host + 4);
}
template <typename T> void WriteHost(uint8_t* host, T value) {
if constexpr (sizeof(T) == 1) *host = value;
else if constexpr (sizeof(T) == 2) BigEndian::Write16(host, value);
else if constexpr (sizeof(T) == 4) BigEndian::Write32(host, value);
else BigEndian::Write64(host, value);
}
template <typename T> T Read(uint32_t addr) {
Record(addr, sizeof(T)); return ReadHost<T>(Pointer(addr));
}
template <typename T> void Write(uint32_t addr, T value) {
Record(addr, sizeof(T)); WriteHost(Pointer(addr), value);
}
}
namespace GuestFlat {
inline bool RequiresCheckedAccess() noexcept { return true; }
}
#define MKW_FLAT_GUEST_BASE (PsqTestMemory::bytes.data())
class Memory {
public:
static uint8_t Read8(uint32_t a) { return PsqTestMemory::Read<uint8_t>(a); }
static uint16_t Read16(uint32_t a) { return PsqTestMemory::Read<uint16_t>(a); }
static uint32_t Read32(uint32_t a) { return PsqTestMemory::Read<uint32_t>(a); }
static uint64_t Read64(uint32_t a) { return PsqTestMemory::Read<uint64_t>(a); }
static void Write8(uint32_t a, uint8_t v) { PsqTestMemory::Write(a, v); }
static void Write16(uint32_t a, uint16_t v) { PsqTestMemory::Write(a, v); }
static void Write32(uint32_t a, uint32_t v) { PsqTestMemory::Write(a, v); }
static void Write64(uint32_t a, uint64_t v) { PsqTestMemory::Write(a, v); }
};
namespace MemoryInline {
inline bool FlatWriteNeedsPolicy(uint32_t) { return true; }
inline bool TryGetPointerFast(uint32_t a, size_t, uint8_t*& host) {
host = PsqTestMemory::directStack ? PsqTestMemory::Pointer(a) : nullptr;
return host != nullptr;
}
inline bool TryGetWritablePointerFast(uint32_t a, size_t n, uint8_t*& host) {
return TryGetPointerFast(a, n, host);
}
template <typename T> T ReadResolvedFallback(uint32_t a) { return PsqTestMemory::Read<T>(a); }
template <typename T> void WriteResolvedFallback(uint32_t a, T v) { PsqTestMemory::Write(a, v); }
template <typename T> T ReadResolved(uint8_t* host, uint32_t o, uint32_t a) {
return host ? PsqTestMemory::ReadHost<T>(host + o) : ReadResolvedFallback<T>(a);
}
template <typename T> void WriteResolved(uint8_t* host, uint32_t o, uint32_t a, T v) {
if (host) PsqTestMemory::WriteHost(host + o, v); else WriteResolvedFallback(a, v);
}
#define PSQ_TEST_MEMORY_WIDTH(Bits, Type) \
inline Type ReadStack##Bits(uint32_t a) { \
return PsqTestMemory::directStack ? PsqTestMemory::ReadHost<Type>(PsqTestMemory::Pointer(a)) : PsqTestMemory::Read<Type>(a); } \
inline void WriteStack##Bits(uint32_t a, Type v) { \
if (PsqTestMemory::directStack) PsqTestMemory::WriteHost(PsqTestMemory::Pointer(a), v); else PsqTestMemory::Write(a, v); } \
inline Type ReadResolved##Bits(uint8_t* h, uint32_t o, uint32_t a) { return ReadResolved<Type>(h, o, a); } \
inline void WriteResolved##Bits(uint8_t* h, uint32_t o, uint32_t a, Type v) { WriteResolved(h, o, a, v); }
PSQ_TEST_MEMORY_WIDTH(8, uint8_t)
PSQ_TEST_MEMORY_WIDTH(16, uint16_t)
PSQ_TEST_MEMORY_WIDTH(32, uint32_t)
PSQ_TEST_MEMORY_WIDTH(64, uint64_t)
#undef PSQ_TEST_MEMORY_WIDTH
}
+13
View File
@@ -0,0 +1,13 @@
foreach(operation load store)
foreach(type 1 2 3)
foreach(lanes 0 1)
foreach(route resolved null)
execute_process(COMMAND "${PSQ_TEST_EXECUTABLE}" ${operation} ${type} ${lanes} ${route}
RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error)
if(NOT result EQUAL 86)
message(FATAL_ERROR "Reserved PSQ ${operation}/${type}/${lanes}/${route}: ${result} ${output} ${error}")
endif()
endforeach()
endforeach()
endforeach()
endforeach()
+35
View File
@@ -0,0 +1,35 @@
#include "runtime_config.h"
#include <iostream>
#include <sstream>
namespace {
bool Require(bool value, const char* message) {
if (!value) {
std::cerr << message << '\n';
return false;
}
return true;
}
} // namespace
int main() {
{
std::istringstream input("[video]\nmetalfx_spatial_upscaling = true\n");
const RuntimeUserConfig config = RuntimeConfigFile::ParseConfig(input, "enabled.toml");
if (!Require(config.metalFxSpatialUpscaling == std::optional<bool>{true},
"valid MetalFX setting was not loaded")) return 1;
}
{
std::istringstream input("[video]\nmetalfx_spatial_upscaling = false\n");
const RuntimeUserConfig config = RuntimeConfigFile::ParseConfig(input, "disabled.toml");
if (!Require(config.metalFxSpatialUpscaling == std::optional<bool>{false},
"false MetalFX setting was not loaded")) return 1;
}
{
std::istringstream input("[video]\nmetalfx_spatial_upscaling = \"yes\"\n");
const RuntimeUserConfig config = RuntimeConfigFile::ParseConfig(input, "invalid.toml");
if (!Require(!config.metalFxSpatialUpscaling, "invalid MetalFX setting was accepted")) return 1;
}
return 0;
}
+19 -5
View File
@@ -97,15 +97,29 @@ int main() {
Check(!Pressed(pulse), "pulse expires"); Check(!Pressed(pulse), "pulse expires");
// The timing-window idiom seen in shared Dolphin configs. // The timing-window idiom seen in shared Dolphin configs.
auto window = Compile("!pulse(`W`, 0.05) & pulse(`W`, 0.15)"); auto window = Compile("!pulse(`W`, 0.05) & pulse(`W`, 0.35)");
g_inputs["W"] = 0.0; g_inputs["W"] = 0.0;
window.Evaluate(Source()); window.Evaluate(Source());
g_inputs["W"] = 1.0; g_inputs["W"] = 1.0;
Check(!Pressed(window), "window closed before its start"); Check(!Pressed(window), "window closed before its start");
Sleep(90); bool windowOpened = false;
Check(Pressed(window), "window open between the two pulses"); for (int i = 0; i < 40; ++i) {
Sleep(90); Sleep(10);
Check(!Pressed(window), "window closed after its end"); if (Pressed(window)) {
windowOpened = true;
break;
}
}
Check(windowOpened, "window open between the two pulses");
bool windowClosed = false;
for (int i = 0; i < 50; ++i) {
Sleep(10);
if (!Pressed(window)) {
windowClosed = true;
break;
}
}
Check(windowClosed, "window closed after its end");
// timer ramps 0..1 and wraps, so a threshold turns it into a square wave. // timer ramps 0..1 and wraps, so a threshold turns it into a square wave.
auto timer = Compile("`X` & timer(0.1)"); auto timer = Compile("`X` & timer(0.1)");
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@@ -380,7 +380,7 @@ public sealed partial class CxxLinearCodeGenerator
instructionContinuationLabels.TryGetValue(trace.Address, out var continuationLabel)) instructionContinuationLabels.TryGetValue(trace.Address, out var continuationLabel))
{ {
RecordEmittedLocalLabel(continuationLabel); RecordEmittedLocalLabel(continuationLabel);
body.AppendLine($"{continuationLabel}:"); body.AppendLine($"{continuationLabel}: ;");
} }
var localFallthroughLr = TryGetLocalFallthroughLr(block.Instructions, i, nonReturningCallTargets, lrContinuationCallTargets); var localFallthroughLr = TryGetLocalFallthroughLr(block.Instructions, i, nonReturningCallTargets, lrContinuationCallTargets);
// State-free bodies are cloned after register caching and lose their // State-free bodies are cloned after register caching and lose their
@@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using Translator.Core.Analysis.Representation; using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis.Ssa; using Translator.Core.Analysis.Ssa;
using Translator.Core.CodeGen; using Translator.Core.CodeGen;
@@ -153,4 +153,35 @@ public class EmittedOutputShapeTests
Assert.Contains("f3.d = MemoryInline::FlatReadFloat32((r4 + 16));", code, StringComparison.Ordinal); Assert.Contains("f3.d = MemoryInline::FlatReadFloat32((r4 + 16));", code, StringComparison.Ordinal);
Assert.Contains("f4.d = MemoryInline::FlatReadFloat64((r4 + 24));", code, StringComparison.Ordinal); Assert.Contains("f4.d = MemoryInline::FlatReadFloat64((r4 + 24));", code, StringComparison.Ordinal);
} }
[Fact]
public void ContinuationLabelAtBlockEndEmitsValidCxx17Statement()
{
var function = new IrFunction("continuation_at_block_end", "0x800E7798", new[]
{
new IrBasicBlock("0x800E7798", new IrInstruction[]
{
new IrCall(string.Empty, "0x8179B000", System.Array.Empty<IrValue>()),
new IrTracePpc(0x800E77A0u, "nop", "0x60000000"),
new IrJump("0x800E77A4")
}),
new IrBasicBlock("0x800E77A4", new IrInstruction[]
{
new IrReturn(null)
})
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>());
var code = new CxxLinearCodeGenerator().Emit(
0x800E7798,
new SsaTransformer().Convert(function),
new FunctionAbiClassification(function.Name, ValueRepresentation.Void),
types,
lrContinuationCallTargets: new HashSet<uint> { 0x8179B000u });
// In C++17, a label before a closing brace is invalid without an intervening statement.
Assert.Contains("loc_800E77A0: ;", code, StringComparison.Ordinal);
Assert.DoesNotContain("loc_800E77A0:\n}", code.Replace("\r\n", "\n"), StringComparison.Ordinal);
}
} }
@@ -15,6 +15,7 @@ internal static class TranslatorCppTestHarness
Path.Combine("runtime", "src", "fpu_helpers.cpp"), Path.Combine("runtime", "src", "fpu_helpers.cpp"),
Path.Combine("runtime", "src", "memory.cpp"), Path.Combine("runtime", "src", "memory.cpp"),
Path.Combine("runtime", "src", "ppc_helpers.cpp"), Path.Combine("runtime", "src", "ppc_helpers.cpp"),
Path.Combine("runtime", "src", "ppc_quantized.cpp"),
}; };
public static string BuildCompileArguments( public static string BuildCompileArguments(
@@ -31,6 +32,7 @@ internal static class TranslatorCppTestHarness
args.Append("-std=c++17 "); args.Append("-std=c++17 ");
args.Append("-D_CRT_SECURE_NO_WARNINGS "); args.Append("-D_CRT_SECURE_NO_WARNINGS ");
args.Append("-march=x86-64-v3 "); args.Append("-march=x86-64-v3 ");
args.Append("-fno-fast-math -ffp-contract=off ");
if (!RuntimeHeadersDefineRestrictMacro(repoRoot)) if (!RuntimeHeadersDefineRestrictMacro(repoRoot))
{ {
// Fallback for the window between an emitter change using MKW_RESTRICT and the runtime // Fallback for the window between an emitter change using MKW_RESTRICT and the runtime