Compare commits

...

18 Commits

Author SHA1 Message Date
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
matellush 75886669bc feat: Facilitate reproducible builds on non-Windows systems (#270) 2026-09-30 19:06:18 +02:00
matellush a50bad2970 fix: Use DOWNLOAD_EXTRACT_TIMESTAMP consistently (#276)
* fix: Use `DOWNLOAD_EXTRACT_TIMESTAMP` consistently

* fix: Set all extracted dependency files' timestamps to extraction time
2026-09-30 19:05:47 +02:00
patchzyy 9b7b9913e4 Handle extra Mbed TLS archives (#275) 2026-09-30 09:14:48 +02:00
matellush a88b7b502b feat: Bump xxHash to 0.8.4 and use new CMake subdirectory (#271) 2026-09-29 21:58:57 +02:00
patchzyy d0d58072d1 Bundle mbed TLS in Linux native prebuilt (#272) 2026-09-29 21:50:19 +02:00
patchzyy 6b853ca371 Fix Wii certificates (#265)
confirmed as fixed by forgottonice (in dms)
2026-09-29 20:03:35 +02:00
patchzyy 77a8623416 Fix Mac THP memory path (#267) 2026-09-29 20:01:39 +02:00
patchzyy 10ab54adee Fix native build paths for special characters (#266) 2026-09-29 20:01:03 +02:00
patchzyy a05c89739d Always refresh Retro-WFC payload with fallback (#264) 2026-09-28 21:01:40 +02:00
patchzyy 82991ec337 Version centrilsation 2026-09-27 13:00:12 +02:00
patchzyy e164af9ff4 Shader wait screen 2026-09-27 12:39:10 +02:00
patchzyy e409d9f99b undo black kart fix
it causes stutters
2026-09-27 12:11:03 +02:00
Michael G 85f250155f Fix controller mapping platform, player index sync, and socket errors (#251)
* Fix macOS controller platform, sync player indices, and preserve socket send error

- Preserve captured socket send error across diagnostic logging in network_socket.cpp
- Use SDL_GetPlatform() instead of hardcoded Windows in controller mapping wizard
- Synchronize Aurora and SDL player indices on port assignment and clear in pad.cpp
- Auto-assign Player 1 on macOS when unconfigured in input.cpp

* fix(input): refine macOS port 0 assignment and persistence logic

- Persist port 0 preference for newly connected controller even if already assigned player index 0
- Verify controller has no preference on other ports before assigning to port 0
2026-09-24 22:01:49 +02:00
patchzyy a9c9c0de26 Add force 16:9 viewport mode (#256) 2026-09-24 22:00:57 +02:00
patchzyy c92e45c2e7 Add early Linux linker dependency probe (#255) 2026-09-24 18:53:56 +02:00
patchzyy 583e702547 add bltl (#254) 2026-09-24 18:06:32 +02:00
93 changed files with 3284 additions and 418 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
+130 -10
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
@@ -107,7 +229,7 @@ jobs:
persist-credentials: false persist-credentials: false
# Wheel Wizard runs the downloaded setup with --version and refuses it when the reported # Wheel Wizard runs the downloaded setup with --version and refuses it when the reported
# version differs from the release tag, so a tag that was pushed without bumping every pinned # version differs from the release tag, so a tag that was pushed without bumping the pinned
# version would ship an update nobody can install. Catch that before anything is published. # version would ship an update nobody can install. Catch that before anything is published.
- name: Verify the tag matches the pinned setup version - name: Verify the tag matches the pinned setup version
env: env:
@@ -122,9 +244,7 @@ jobs:
status=1 status=1
fi fi
} }
check Launcher/WiiCompiled.Setup.Windows/Program.cs "public const string Version = \"$version\";" check Launcher/Directory.Build.props "<Version>$version</Version>"
check Launcher/WiiCompiled.Setup.Windows/WiiCompiled.Setup.Windows.csproj "<Version>$version</Version>"
check Launcher/Build-Installer.ps1 "ProductVersion = '$version'"
exit $status exit $status
# The build jobs upload with `archive: false`, which stores each installer as a raw file # The build jobs upload with `archive: false`, which stores each installer as a raw file
@@ -143,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")
+4 -1
View File
@@ -279,9 +279,12 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
if ([string]::IsNullOrWhiteSpace($identities.$required)) { throw "Payload identity output is missing $required." } if ([string]::IsNullOrWhiteSpace($identities.$required)) { throw "Payload identity output is missing $required." }
} }
$productVersion = ((& $setupHost --version) -join '').Trim()
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($productVersion)) { throw 'The setup host did not report its version.' }
$manifest = [ordered]@{ $manifest = [ordered]@{
SchemaVersion = 2 SchemaVersion = 2
ProductVersion = '0.2.32' ProductVersion = $productVersion
ExpectedGameId = $pins.GameId ExpectedGameId = $pins.GameId
ExpectedDolSha256 = $pins.DolSha256 ExpectedDolSha256 = $pins.DolSha256
ExpectedRelSha256 = $pins.RelSha256 ExpectedRelSha256 = $pins.RelSha256
+7
View File
@@ -0,0 +1,7 @@
<Project>
<PropertyGroup>
<!-- The one product version. Release tags must match it. -->
<Version>0.2.33</Version>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
</Project>
+10 -2
View File
@@ -103,7 +103,7 @@ function Write-MkwBuildStep([string]$StepId, [string]$Message) {
function Reset-LocalDirectory([string]$Path) { function Reset-LocalDirectory([string]$Path) {
$full = [IO.Path]::GetFullPath($Path) $full = [IO.Path]::GetFullPath($Path)
$root = [IO.Path]::GetFullPath($Workspace).TrimEnd('\') + '\' $root = [IO.Path]::GetFullPath($Workspace).TrimEnd('\') + '\'
$installRoot = [IO.Path]::GetFullPath((Split-Path -Parent $Workspace)).TrimEnd('\') + '\' $installRoot = [IO.Path]::GetFullPath((Split-Path -Parent $realWorkspace)).TrimEnd('\') + '\'
# The caller-supplied output destinations are legitimate reset targets by # The caller-supplied output destinations are legitimate reset targets by
# definition, wherever the caller placed them: a fresh install's operation # definition, wherever the caller placed them: a fresh install's operation
# scratch lives beside the installation directory rather than inside it. # scratch lives beside the installation directory rather than inside it.
@@ -150,8 +150,16 @@ if ($Profile -eq 'both' -and [string]::IsNullOrWhiteSpace($BaseOutputDirectory))
if ($Profile -ne 'both' -and -not [string]::IsNullOrWhiteSpace($BaseOutputDirectory)) { if ($Profile -ne 'both' -and -not [string]::IsNullOrWhiteSpace($BaseOutputDirectory)) {
throw '-BaseOutputDirectory is valid only with -Profile both.' throw '-BaseOutputDirectory is valid only with -Profile both.'
} }
$realWorkspace = $Workspace.TrimEnd('\')
$Workspace = Get-MkwBuildSafePath $realWorkspace 'workspace' 'runtime\CMakeLists.txt'
# A selected package inside the install may also name the same Code.pul through the real path.
# Give it the workspace spelling before comparing it with the staged copy.
if (-not [string]::IsNullOrWhiteSpace($RetroRewindPackageDirectory) -and
$RetroRewindPackageDirectory.StartsWith($realWorkspace + '\', [StringComparison]::OrdinalIgnoreCase)) {
$RetroRewindPackageDirectory = $Workspace + $RetroRewindPackageDirectory.Substring($realWorkspace.Length)
}
$translator = Join-Path $Toolkit 'Translator\Translator.Cli.exe' $translator = Join-Path $Toolkit 'Translator\Translator.Cli.exe'
$toolchain = Get-MkwShellSafeToolchainRoot $Toolkit $toolchain = Get-MkwBuildSafePath $Toolkit 'toolchain' 'CMake\bin\cmake.exe'
$cmake = Join-Path $toolchain 'CMake\bin\cmake.exe' $cmake = Join-Path $toolchain 'CMake\bin\cmake.exe'
$ninja = Join-Path $toolchain 'Ninja\ninja.exe' $ninja = Join-Path $toolchain 'Ninja\ninja.exe'
$toolchainBin = Join-Path $toolchain 'llvm-mingw\bin' $toolchainBin = Join-Path $toolchain 'llvm-mingw\bin'
+29 -15
View File
@@ -40,41 +40,55 @@ function Get-MkwToolchainPath([string]$ToolchainRoot) {
) -join ';') ) -join ';')
} }
function Get-MkwShellSafeToolchainRoot([string]$ToolchainRoot) { function Get-MkwBuildSafePath([string]$Path, [string]$Kind, [string]$MarkerFile) {
if ([string]::IsNullOrWhiteSpace($ToolchainRoot)) { throw 'A toolchain root is required.' } <#
$full = [IO.Path]::GetFullPath($ToolchainRoot) The Windows native build passes workspace paths through CMake, Ninja response files and
clang, which do not all interpret quotes the same way. Keep those paths plain even when the
user's install directory contains an apostrophe, ampersand or other punctuation.
#>
if ([string]::IsNullOrWhiteSpace($Path)) { throw "A $Kind path is required." }
$full = [IO.Path]::GetFullPath($Path)
# A drive root keeps its separator: "C:" is relative to the current directory on that drive. # A drive root keeps its separator: "C:" is relative to the current directory on that drive.
if ($full -ne [IO.Path]::GetPathRoot($full)) { $full = $full.TrimEnd('\') } if ($full -ne [IO.Path]::GetPathRoot($full)) { $full = $full.TrimEnd('\') }
if ($full -notmatch '[()&^%!]') { return $full } if ($full -cmatch '^[A-Za-z0-9 ._\\:-]+$') { return $full }
Assert-File (Join-Path $full $MarkerFile) "$Kind marker"
$sha = [Security.Cryptography.SHA256]::Create() $sha = [Security.Cryptography.SHA256]::Create()
try { try {
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($full.ToLowerInvariant())) $bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($full.ToLowerInvariant()))
} finally { $sha.Dispose() } } finally { $sha.Dispose() }
$linkName = 'toolchain-' + ((($bytes[0..7]) | ForEach-Object { $_.ToString('x2') }) -join '') $linkName = "$Kind-" + ((($bytes[0..7]) | ForEach-Object { $_.ToString('x2') }) -join '')
$failures = @() $failures = @()
foreach ($base in @($env:ProgramData, $env:PUBLIC)) { foreach ($base in @($env:ProgramData, $env:PUBLIC)) {
if ([string]::IsNullOrWhiteSpace($base) -or $base -match '[()&^%! ]') { continue } if ([string]::IsNullOrWhiteSpace($base) -or $base -cnotmatch '^[A-Za-z0-9._\\:-]+$') { continue }
$link = Join-Path (Join-Path $base 'WiiCompiled') $linkName $link = Join-Path (Join-Path $base 'WiiCompiled') $linkName
try { try {
[IO.Directory]::CreateDirectory((Split-Path -Parent $link)) | Out-Null [IO.Directory]::CreateDirectory((Split-Path -Parent $link)) | Out-Null
# The name already identifies the target, so an existing junction that still resolves is $existing = Get-Item -LiteralPath $link -Force -ErrorAction SilentlyContinue
# this one; only a broken leftover is replaced. Directory.Delete removes the reparse if ($null -ne $existing) {
# point itself, where Remove-Item -Recurse would delete the toolchain it points at. # Never trust a directory just because it has the marker: it could point at a
if (-not (Test-Path -LiteralPath (Join-Path $link 'CMake\bin\cmake.exe') -PathType Leaf)) { # different installation. Nor may we remove a directory we did not create.
if (Test-Path -LiteralPath $link) { [IO.Directory]::Delete($link) } if ($existing.LinkType -ne 'Junction' -or
@($existing.Target).Count -ne 1 -or
-not [string]::Equals([IO.Path]::GetFullPath(@($existing.Target)[0]),
$full, [StringComparison]::OrdinalIgnoreCase)) {
throw "An existing path is not the expected junction: $link"
}
} else {
New-Item -ItemType Junction -Path $link -Target $full -ErrorAction Stop | Out-Null New-Item -ItemType Junction -Path $link -Target $full -ErrorAction Stop | Out-Null
} }
Write-Host "MKWCBUILD: Building through $link, because $full contains characters cmd.exe cannot parse" Assert-File (Join-Path $link $MarkerFile) "$Kind junction marker"
Write-Host "MKWCBUILD: Building the $Kind through $link, because $full contains characters the native build cannot quote reliably"
return $link return $link
} catch { } catch {
$failures += "$link ($($_.Exception.Message))" $failures += "$link ($($_.Exception.Message))"
} }
} }
throw ("The toolchain path $full contains a character (one of ( ) & ^ % !) that the compiler " + throw ("The $Kind path $full cannot be passed safely to the native build, and no junction " +
'cannot be invoked through, and no junction to it could be created: ' + ($failures -join '; ') + 'to it could be created: ' + ($failures -join '; ') + '. Install to a path of plain ' +
'. Install to a path without those characters.') 'letters, digits and spaces, or make a safe junction location available.')
} }
function Get-MkwProjectPins([string]$ProjectFile) { function Get-MkwProjectPins([string]$ProjectFile) {
+3 -3
View File
@@ -82,9 +82,9 @@ $packages = @(
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz' }) Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz' })
}, },
[pscustomobject]@{ [pscustomobject]@{
Name = 'xxhash'; File = 'xxHash-0.8.3.tar.gz' Name = 'xxhash'; File = 'xxHash-0.8.4.tar.gz'
Uris = @('https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.3.tar.gz') Uris = @('https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.4.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.3.tar.gz' }) Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.4.tar.gz' })
}, },
[pscustomobject]@{ [pscustomobject]@{
Name = 'zlib'; File = 'zlib-1.3.2.tar.gz' Name = 'zlib'; File = 'zlib-1.3.2.tar.gz'
+86 -16
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Builds the redistributable precompiled aurora + third-party package for native Linux: aurora # Builds the redistributable precompiled aurora + third-party package for native Linux: aurora
# (~43% of local build CPU time per Prepare-NativePrebuilt.ps1) and vendored Crypto++ are identical # (~43% of local build CPU time per Prepare-NativePrebuilt.ps1), Crypto++ and mbed TLS are identical
# for every user under the pinned toolchain prepare-portable-tools.sh bundles, so this configures # for every user under the pinned toolchain prepare-portable-tools.sh bundles, so this configures
# runtime/ against that toolchain, builds just that closure, and harvests the archives plus a # runtime/ against that toolchain, builds just that closure, and harvests the archives plus a
# generated CMake description into an output package - the Linux counterpart to # generated CMake description into an output package - the Linux counterpart to
@@ -22,11 +22,17 @@ workspace=$(cd "$script_dir/.." && pwd)
arch="" arch=""
output_dir="" output_dir=""
toolchain_dir=""
ninja_bin=""
cmake_bin=""
llvm_dir=""
sysroot=""
stage_dir="$workspace/build/native-prebuilt-stage" stage_dir="$workspace/build/native-prebuilt-stage"
keep_stage=0 keep_stage=0
reuse_stage=0 reuse_stage=0
parallel=0 parallel=0
print_fingerprint_only=0 print_fingerprint_only=0
disconnected=0
usage() { usage() {
cat <<'EOF' cat <<'EOF'
@@ -39,8 +45,7 @@ Usage: Prepare-NativePrebuilt.sh --arch {x86_64|aarch64} [options]
--reuse-stage Reuse an existing staging build directory (maintainer iteration aid: a --reuse-stage Reuse an existing staging build directory (maintainer iteration aid: a
re-harvest does not recompile aurora from scratch) re-harvest does not recompile aurora from scratch)
--parallel N Ninja build parallelism (default: nproc) --parallel N Ninja build parallelism (default: nproc)
--print-fingerprint-only Print the four provenance inputs (compiler_sha256, flag_fingerprint, --print-fingerprint-only Print the provenance inputs as "key=value" lines and
aurora_fingerprint, third_party_fingerprint) as "key=value" lines and
exit, without configuring/building/harvesting anything - lets a caller exit, without configuring/building/harvesting anything - lets a caller
(build-appimage.sh) decide whether an existing package is still current (build-appimage.sh) decide whether an existing package is still current
without paying for a full aurora rebuild just to find out. without paying for a full aurora rebuild just to find out.
@@ -56,6 +61,12 @@ while [[ $# -gt 0 ]]; do
--reuse-stage) reuse_stage=1; shift ;; --reuse-stage) reuse_stage=1; shift ;;
--parallel) parallel=$2; shift 2 ;; --parallel) parallel=$2; shift 2 ;;
--print-fingerprint-only) print_fingerprint_only=1; shift ;; --print-fingerprint-only) print_fingerprint_only=1; shift ;;
--toolchain-dir) toolchain_dir=$2; shift 2 ;;
--ninja-bin) ninja_bin=$2; shift 2 ;;
--cmake-bin) cmake_bin=$2; shift 2 ;;
--llvm-dir) llvm_dir=$2; shift 2 ;;
--sysroot) sysroot=$2; shift 2 ;;
--disconnected) disconnected=1; shift ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
*) echo "Prepare-NativePrebuilt.sh: unknown argument: $1" >&2; exit 1 ;; *) echo "Prepare-NativePrebuilt.sh: unknown argument: $1" >&2; exit 1 ;;
esac esac
@@ -72,14 +83,22 @@ assert_dir() { [[ -d "$1" ]] || fail "$2 is missing: $1"; }
sha256_of() { sha256sum "$1" | awk '{print $1}'; } sha256_of() { sha256sum "$1" | awk '{print $1}'; }
normalize() { readlink -f "$1"; } normalize() { readlink -f "$1"; }
# Environment check: Ensure the `SOURCE_DATE_EPOCH` value is not invalid
# before trying to build anything (fail-fast)
if [[ -n "${SOURCE_DATE_EPOCH:-}" ]] && ! python3 -c \
'import datetime, os; datetime.datetime.fromtimestamp(int(os.environ.get("SOURCE_DATE_EPOCH")), datetime.timezone.utc)'
then
fail "SOURCE_DATE_EPOCH must be a valid UNIX timestamp"
fi
[[ -n "$output_dir" ]] || output_dir="$script_dir/artifacts/native-prebuilt-$arch" [[ -n "$output_dir" ]] || output_dir="$script_dir/artifacts/native-prebuilt-$arch"
[[ "$stage_dir" = /* ]] || stage_dir="$workspace/$stage_dir" [[ "$stage_dir" = /* ]] || stage_dir="$workspace/$stage_dir"
toolchain_dir="$script_dir/artifacts/portable-tools/toolchain-$arch" toolchain_dir="${toolchain_dir:-$script_dir/artifacts/portable-tools/toolchain-$arch}"
cc="$toolchain_dir/bin/clang" cc="${llvm_dir:-$toolchain_dir}/bin/clang"
cxx="$toolchain_dir/bin/clang++" cxx="${llvm_dir:-$toolchain_dir}/bin/clang++"
cmake_bin="$toolchain_dir/bin/cmake" cmake_bin="${cmake_bin:-$toolchain_dir/bin/cmake}"
ninja_bin="$toolchain_dir/bin/ninja" ninja_bin="${ninja_bin:-$toolchain_dir/bin/ninja}"
runtime_source="$workspace/runtime" runtime_source="$workspace/runtime"
aurora_source="$workspace/aurora-main" aurora_source="$workspace/aurora-main"
@@ -88,7 +107,7 @@ assert_file "$ninja_bin" "Portable Ninja"
assert_file "$cc" "Portable C compiler" assert_file "$cc" "Portable C compiler"
assert_file "$cxx" "Portable C++ compiler" assert_file "$cxx" "Portable C++ compiler"
assert_dir "$aurora_source" "aurora-main source tree" assert_dir "$aurora_source" "aurora-main source tree"
clang_binary=$(normalize "$toolchain_dir/bin/clang-22") clang_binary=$(normalize "${llvm_dir:-$toolchain_dir}/bin/clang-22")
assert_file "$clang_binary" "Portable clang driver binary" assert_file "$clang_binary" "Portable clang driver binary"
(( parallel > 0 )) || parallel=$(nproc) (( parallel > 0 )) || parallel=$(nproc)
@@ -131,6 +150,8 @@ fixed_configure_flags=(
-DCMAKE_DISABLE_FIND_PACKAGE_absl=ON -DCMAKE_DISABLE_FIND_PACKAGE_absl=ON
-DCMAKE_DISABLE_FIND_PACKAGE_PNG=ON -DCMAKE_DISABLE_FIND_PACKAGE_PNG=ON
-DCMAKE_DISABLE_FIND_PACKAGE_Freetype=ON -DCMAKE_DISABLE_FIND_PACKAGE_Freetype=ON
-DUSE_STATIC_MBEDTLS_LIBRARY=ON
-DUSE_SHARED_MBEDTLS_LIBRARY=OFF
# Freetype's own vendored CMakeLists.txt separately probes for system BZip2 (optional # Freetype's own vendored CMakeLists.txt separately probes for system BZip2 (optional
# bzip2-compressed-font support aurora-main never asked for) regardless of the Freetype # bzip2-compressed-font support aurora-main never asked for) regardless of the Freetype
# find_package disable above, since that only stops aurora's own outer find_package(Freetype) # find_package disable above, since that only stops aurora's own outer find_package(Freetype)
@@ -141,6 +162,16 @@ fixed_configure_flags=(
-DFT_DISABLE_BZIP2=ON -DFT_DISABLE_BZIP2=ON
-DCMAKE_POLICY_DEFAULT_CMP0168=NEW -DCMAKE_POLICY_DEFAULT_CMP0168=NEW
) )
if [[ "$disconnected" -eq 1 ]]; then
fixed_configure_flags+=(
-DFETCHCONTENT_FULLY_DISCONNECTED=ON
)
fi
if [[ -n "$sysroot" ]]; then
fixed_configure_flags+=(
-DCMAKE_SYSROOT="$sysroot"
)
fi
flag_fingerprint=$(printf '%s\n' "${fixed_configure_flags[@]}" | sha256sum | awk '{print $1}') flag_fingerprint=$(printf '%s\n' "${fixed_configure_flags[@]}" | sha256sum | awk '{print $1}')
# extern/ is excluded because the payload ships that tree separately (aurora-main/extern is bundled # extern/ is excluded because the payload ships that tree separately (aurora-main/extern is bundled
@@ -148,19 +179,24 @@ flag_fingerprint=$(printf '%s\n' "${fixed_configure_flags[@]}" | sha256sum | awk
aurora_fingerprint=$(fingerprint_tree "$aurora_source" extern build) aurora_fingerprint=$(fingerprint_tree "$aurora_source" extern build)
[[ -n "$aurora_fingerprint" ]] || fail "The aurora source tree could not be fingerprinted: $aurora_source" [[ -n "$aurora_fingerprint" ]] || fail "The aurora source tree could not be fingerprinted: $aurora_source"
# The harvested Crypto++ archive is consumed against this tree's headers, so it is fingerprinted # The harvested Crypto++ archive is consumed against this tree's headers, so it is fingerprinted.
# for the same reason as aurora above. No exclusions: unlike aurora's extern/, nothing under
# runtime/third_party is shipped separately.
third_party_fingerprint=$(fingerprint_tree "$runtime_source/third_party") third_party_fingerprint=$(fingerprint_tree "$runtime_source/third_party")
[[ -n "$third_party_fingerprint" ]] || fail "The vendored third-party tree could not be fingerprinted: $runtime_source/third_party" [[ -n "$third_party_fingerprint" ]] || fail "The vendored third-party tree could not be fingerprinted: $runtime_source/third_party"
mbedtls_fingerprint=$(sha256_of "$runtime_source/cmake/MbedTLSPin.cmake")
compiler_sha256=$(sha256_of "$clang_binary") compiler_sha256=$(sha256_of "$cc")
cxx_sha256=$(sha256_of "$cxx")
# Append the CXX SHA256 if it doesn't match the hash of CC
if [[ "${cxx_sha256}" != "${compiler_sha256}" ]]; then
compiler_sha256="${compiler_sha256}:${cxx_sha256}"
fi
if [[ "$print_fingerprint_only" -eq 1 ]]; then if [[ "$print_fingerprint_only" -eq 1 ]]; then
printf 'compiler_sha256=%s\n' "$compiler_sha256" printf 'compiler_sha256=%s\n' "$compiler_sha256"
printf 'flag_fingerprint=%s\n' "$flag_fingerprint" printf 'flag_fingerprint=%s\n' "$flag_fingerprint"
printf 'aurora_fingerprint=%s\n' "$aurora_fingerprint" printf 'aurora_fingerprint=%s\n' "$aurora_fingerprint"
printf 'third_party_fingerprint=%s\n' "$third_party_fingerprint" printf 'third_party_fingerprint=%s\n' "$third_party_fingerprint"
printf 'mbedtls_fingerprint=%s\n' "$mbedtls_fingerprint"
exit 0 exit 0
fi fi
@@ -361,6 +397,21 @@ while IFS='|' read -r name type file linkerfile; do
linker_file_to_reference["$linkerfile"]="@PKG@/$relative_linker" linker_file_to_reference["$linkerfile"]="@PKG@/$relative_linker"
fi fi
done < "$targets_txt" done < "$targets_txt"
mbedtls_refs=()
mbedtls_txt="$export_dir/mbedtls.txt"
assert_file "$mbedtls_txt" "Mbed TLS export targets list"
while IFS='|' read -r name linkerfile; do
[[ -n "$name" ]] || continue
linkerfile=$(normalize "$linkerfile")
ref=${linker_file_to_reference["$linkerfile"]:-}
[[ -n "$ref" ]] || fail "Mbed TLS archive was not harvested: $name ($linkerfile)"
mbedtls_refs+=("$ref")
done < "$mbedtls_txt"
[[ ${#mbedtls_refs[@]} -ge 3 ]] || fail "Expected at least three Mbed TLS archives, got ${#mbedtls_refs[@]}"
mbedtls_source_dir=$(get_meta mbedtls_source_dir)
assert_dir "$mbedtls_source_dir/include/mbedtls" "Mbed TLS headers"
mkdir -p "$output_dir/include/mbedtls"
cp -a "$mbedtls_source_dir/include/." "$output_dir/include/mbedtls/"
# Unlike Windows (SDL/zlib/libpng ship as DLLs by default), everything here was forced static above # Unlike Windows (SDL/zlib/libpng ship as DLLs by default), everything here was forced static above
# and Dawn's own Linux package (verified directly) ships libwebgpu_dawn.a, also static - so zero # and Dawn's own Linux package (verified directly) ships libwebgpu_dawn.a, also static - so zero
# shared imports is the expected, normal outcome, not a failure. # shared imports is the expected, normal outcome, not a failure.
@@ -426,6 +477,9 @@ for item in "${link_items[@]}"; do
if [[ "$item" = /* ]]; then item_abs=$(normalize "$item"); else item_abs=$(normalize "$stage_dir/$item"); fi if [[ "$item" = /* ]]; then item_abs=$(normalize "$item"); else item_abs=$(normalize "$stage_dir/$item"); fi
ref=${linker_file_to_reference["$item_abs"]:-} ref=${linker_file_to_reference["$item_abs"]:-}
if [[ -n "$ref" ]]; then if [[ -n "$ref" ]]; then
for mbedtls_ref in "${mbedtls_refs[@]}"; do
[[ "$ref" == "$mbedtls_ref" ]] && continue 2
done
package_link_items+=("$ref") package_link_items+=("$ref")
continue continue
fi fi
@@ -505,6 +559,9 @@ generated_cmake="$output_dir/native_prebuilt.cmake"
format_cmake_block MKW_NP_COMPILE_DEFINITIONS "${package_definitions[@]}" format_cmake_block MKW_NP_COMPILE_DEFINITIONS "${package_definitions[@]}"
format_cmake_block MKW_NP_COMPILE_OPTIONS "${package_compile_options[@]}" format_cmake_block MKW_NP_COMPILE_OPTIONS "${package_compile_options[@]}"
format_cmake_block MKW_NP_LINK_LIBRARIES "${package_link_items[@]}" format_cmake_block MKW_NP_LINK_LIBRARIES "${package_link_items[@]}"
format_cmake_block MKW_NP_MBEDTLS_LIBRARIES "${mbedtls_refs[@]}"
echo 'set(MKW_NP_MBEDTLS_INCLUDE_DIR "@PKG@/include/mbedtls")'
printf 'set(MKW_NP_MBEDTLS_FINGERPRINT "%s")\n' "$mbedtls_fingerprint"
format_cmake_block MKW_NP_AURORA_TARGETS "aurora::gx" "aurora::pad" "aurora::si" "aurora::vi" "aurora::mtx" format_cmake_block MKW_NP_AURORA_TARGETS "aurora::gx" "aurora::pad" "aurora::si" "aurora::vi" "aurora::mtx"
echo "" echo ""
printf 'set(MKW_NP_DAWN_CONFIG_DIR "%s")\n' "$dawn_config_token" printf 'set(MKW_NP_DAWN_CONFIG_DIR "%s")\n' "$dawn_config_token"
@@ -540,12 +597,12 @@ harvested_count=${#linker_file_to_reference[@]}
python3 - "$output_dir" "$compiler_sha256" "$compiler_version" "$flag_fingerprint" \ python3 - "$output_dir" "$compiler_sha256" "$compiler_version" "$flag_fingerprint" \
"$dawn_version" "$dawn_runtime_sha256" "$aurora_fingerprint" "$third_party_fingerprint" \ "$dawn_version" "$dawn_runtime_sha256" "$aurora_fingerprint" "$third_party_fingerprint" \
"$sdl3_target" "$harvested_count" <<'PY' "$sdl3_target" "$harvested_count" "$mbedtls_fingerprint" <<'PY'
import hashlib, json, os, sys, datetime import hashlib, json, os, sys, datetime
(output_dir, compiler_sha256, compiler_version, flag_fingerprint, dawn_version, (output_dir, compiler_sha256, compiler_version, flag_fingerprint, dawn_version,
dawn_runtime_sha256, aurora_fingerprint, third_party_fingerprint, sdl3_target, dawn_runtime_sha256, aurora_fingerprint, third_party_fingerprint, sdl3_target,
harvested_count) = sys.argv[1:] harvested_count, mbedtls_fingerprint) = sys.argv[1:]
contents = [] contents = []
for root, dirs, files in os.walk(output_dir): for root, dirs, files in os.walk(output_dir):
@@ -560,9 +617,21 @@ for root, dirs, files in os.walk(output_dir):
contents.append({"Path": rel, "Bytes": os.path.getsize(path), "Sha256": digest}) contents.append({"Path": rel, "Bytes": os.path.getsize(path), "Sha256": digest})
contents.sort(key=lambda c: c["Path"]) contents.sort(key=lambda c: c["Path"])
source_date_epoch = os.environ.get("SOURCE_DATE_EPOCH") or None
utc = datetime.timezone.utc
if source_date_epoch is not None:
built_utc = datetime.datetime.fromtimestamp(
int(source_date_epoch),
utc
)
else:
built_utc = datetime.datetime.now(utc)
built_utc = built_utc.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
provenance = { provenance = {
"SchemaVersion": 1, "SchemaVersion": 1,
"BuiltUtc": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", "BuiltUtc": built_utc,
"CompilerSha256": compiler_sha256, "CompilerSha256": compiler_sha256,
"CompilerVersion": compiler_version, "CompilerVersion": compiler_version,
"FlagFingerprint": flag_fingerprint, "FlagFingerprint": flag_fingerprint,
@@ -570,6 +639,7 @@ provenance = {
"DawnRuntimeSha256": dawn_runtime_sha256, "DawnRuntimeSha256": dawn_runtime_sha256,
"AuroraSourceFingerprint": aurora_fingerprint, "AuroraSourceFingerprint": aurora_fingerprint,
"ThirdPartySourceFingerprint": third_party_fingerprint, "ThirdPartySourceFingerprint": third_party_fingerprint,
"MbedTlsFingerprint": mbedtls_fingerprint,
"Sdl3Target": sdl3_target, "Sdl3Target": sdl3_target,
"HarvestedLibraryCount": int(harvested_count), "HarvestedLibraryCount": int(harvested_count),
"Contents": contents, "Contents": contents,
@@ -6,7 +6,6 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace> <RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName> <AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
<Version>0.2.32</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description> <Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description>
@@ -5,7 +5,6 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace> <RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName> <AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
<Version>0.2.32</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description> <Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description>
+5 -1
View File
@@ -1,9 +1,13 @@
using System.Reflection;
namespace WiiCompiled.Setup.Linux; namespace WiiCompiled.Setup.Linux;
internal static class ProductInfo internal static class ProductInfo
{ {
public const string Name = "WiiCompiled"; public const string Name = "WiiCompiled";
public const string Version = "0.2.32"; public static readonly string Version =
typeof(ProductInfo).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!
.InformationalVersion;
} }
/// <summary>One installed product's record inside install-state.json.</summary> /// <summary>One installed product's record inside install-state.json.</summary>
+27 -6
View File
@@ -127,18 +127,39 @@ internal static class Program
string? retroWfcOfflineDir = null; string? retroWfcOfflineDir = null;
if (downloadPayload) if (downloadPayload)
{ {
// Reused if a previous install already downloaded and it's still valid - matches
// Windows's own reuse-if-valid behavior instead of re-downloading on every install.
var cacheDir = Path.Combine(workspace, "generated", "retro-wfc-payload"); var cacheDir = Path.Combine(workspace, "generated", "retro-wfc-payload");
reporter.Progress(InstallStages.Validate, "Preparing the Retro-WFC payload", 1); reporter.Progress(InstallStages.Validate,
"Downloading the current Retro-WFC payload", 1);
try
{
// A valid signature authenticates a payload, but does not prove it is the latest
// signed revision. Always ask the fixed endpoint for the current snapshot; the
// downloader verifies it before atomically replacing the cache.
await RetroWfcPayload.DownloadRetroWfcPayloadAsync(
RetroWfcPayload.CurrentRetroWfcPayloadUri, cacheDir, token);
}
catch (Exception downloadFailure) when (!token.IsCancellationRequested &&
downloadFailure is HttpRequestException or TimeoutException
or IOException)
{
// Offline installs may continue with a previously authenticated snapshot. Do not
// use this path for a newly downloaded payload that failed signature validation:
// that must remain a hard failure instead of hiding possible endpoint tampering.
try try
{ {
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(cacheDir); RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(cacheDir);
} }
catch (InvalidDataException) catch (Exception cacheFailure) when (cacheFailure is IOException or
UnauthorizedAccessException or InvalidDataException)
{ {
await RetroWfcPayload.DownloadRetroWfcPayloadAsync( throw new InvalidOperationException(
RetroWfcPayload.CurrentRetroWfcPayloadUri, cacheDir, token); "The current Retro-WFC payload could not be downloaded and no valid cached " +
$"payload is available ({cacheFailure.Message.TrimEnd('.')}).", downloadFailure);
}
reporter.Diagnostic(
"The current Retro-WFC payload could not be downloaded; using the previously " +
$"verified cached payload instead ({downloadFailure.Message.TrimEnd('.')}).");
} }
retroWfcOfflineDir = cacheDir; retroWfcOfflineDir = cacheDir;
} }
@@ -6,7 +6,6 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyName>WiiCompiled.Setup.Linux</AssemblyName> <AssemblyName>WiiCompiled.Setup.Linux</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Linux</RootNamespace> <RootNamespace>WiiCompiled.Setup.Linux</RootNamespace>
<Version>0.2.32</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled on Linux</Description> <Description>Command-line installer and launcher for WiiCompiled on Linux</Description>
@@ -110,8 +110,7 @@ internal sealed class ProductRepairService
InputValidation.CurrentRetroWfcPayloadUri, payloadScratch, cancellationToken); InputValidation.CurrentRetroWfcPayloadUri, payloadScratch, cancellationToken);
} }
catch (Exception ex) when (!cancellationToken.IsCancellationRequested && catch (Exception ex) when (!cancellationToken.IsCancellationRequested &&
ex is HttpRequestException or IOException or InvalidDataException ex is HttpRequestException or TimeoutException or IOException)
or InvalidOperationException or OperationCanceledException)
{ {
payloadSnapshot = RecoverInstalledRetroWfcPayload(toolkitFingerprint, payloadSnapshot = RecoverInstalledRetroWfcPayload(toolkitFingerprint,
Path.Combine(scratchRoot, "retro-wfc-payload-recovered"), ex, cancellationToken); Path.Combine(scratchRoot, "retro-wfc-payload-recovered"), ex, cancellationToken);
@@ -1,5 +1,6 @@
namespace WiiCompiled.Setup.Windows; namespace WiiCompiled.Setup.Windows;
using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
internal static class Program internal static class Program
@@ -121,7 +122,9 @@ internal static class PlatformChecks
internal static class ProductInfo internal static class ProductInfo
{ {
public const string Name = "WiiCompiled"; public const string Name = "WiiCompiled";
public const string Version = "0.2.32"; public static readonly string Version =
typeof(ProductInfo).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!
.InformationalVersion;
/// <summary> /// <summary>
/// The setup executable is copied into the installation under this name. It is the launcher and /// The setup executable is copied into the installation under this name. It is the launcher and
@@ -7,7 +7,6 @@
<AssemblyName>WiiCompiled.Setup</AssemblyName> <AssemblyName>WiiCompiled.Setup</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace> <RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.2.32</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled</Description> <Description>Command-line installer and launcher for WiiCompiled</Description>
+3 -2
View File
@@ -128,9 +128,9 @@ cp -a "$workspace/Launcher/artifacts/portable-tools/toolchain-$appimagetool_arch
# Precompiled aurora + third-party package (see Prepare-NativePrebuilt.sh) so a user's own # Precompiled aurora + third-party package (see Prepare-NativePrebuilt.sh) so a user's own
# local-build.sh never has to compile aurora itself (~43% of local build CPU time). Re-harvesting # local-build.sh never has to compile aurora itself (~43% of local build CPU time). Re-harvesting
# recompiles the whole aurora/Crypto++ closure with the toolchain above, so this is skipped unless # recompiles the aurora/Crypto++/mbed TLS closure with the toolchain above, so this is skipped unless
# --print-fingerprint-only (a fast, build-free check) says the existing package no longer matches # --print-fingerprint-only (a fast, build-free check) says the existing package no longer matches
# the current compiler/flags/aurora/third_party sources. # the current compiler, flags, source trees or mbed TLS pin.
native_prebuilt_dir="$workspace/Launcher/artifacts/native-prebuilt-$appimagetool_arch" native_prebuilt_dir="$workspace/Launcher/artifacts/native-prebuilt-$appimagetool_arch"
echo "Checking whether the precompiled aurora + third-party package ($appimagetool_arch) is current..." echo "Checking whether the precompiled aurora + third-party package ($appimagetool_arch) is current..."
current_fingerprint=$(bash "$script_dir/Prepare-NativePrebuilt.sh" --arch "$appimagetool_arch" --print-fingerprint-only) current_fingerprint=$(bash "$script_dir/Prepare-NativePrebuilt.sh" --arch "$appimagetool_arch" --print-fingerprint-only)
@@ -148,6 +148,7 @@ fields = {
"flag_fingerprint": "FlagFingerprint", "flag_fingerprint": "FlagFingerprint",
"aurora_fingerprint": "AuroraSourceFingerprint", "aurora_fingerprint": "AuroraSourceFingerprint",
"third_party_fingerprint": "ThirdPartySourceFingerprint", "third_party_fingerprint": "ThirdPartySourceFingerprint",
"mbedtls_fingerprint": "MbedTlsFingerprint",
} }
print(1 if all(provenance.get(v) == current.get(k) for k, v in fields.items()) else 0) print(1 if all(provenance.get(v) == current.get(k) for k, v in fields.items()) else 0)
PY PY
+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"
+24
View File
@@ -190,6 +190,30 @@ assert_file "$project" "Translation project"
assert_file "$assets/main.dol" "Extracted main.dol (see translator/README.md - owning the game is required)" assert_file "$assets/main.dol" "Extracted main.dol (see translator/README.md - owning the game is required)"
assert_file "$assets/StaticR.rel" "Extracted StaticR.rel (see translator/README.md - owning the game is required)" assert_file "$assets/StaticR.rel" "Extracted StaticR.rel (see translator/README.md - owning the game is required)"
# The AppImage bundles Clang, but Linux startup objects and the C/C++ link runtimes
# still come from the host. Check them before the expensive translation so a missing
# development package produces a useful error instead of CMake's generic exit 1.
link_probe_dir=$(mktemp -d)
link_probe_flags=()
[[ -z "$sysroot" ]] || link_probe_flags+=(--sysroot="$sysroot")
[[ -z "$fuse_ld_override" ]] || link_probe_flags+=(-fuse-ld="$fuse_ld_override")
printf 'int main(void) { return 0; }\n' > "$link_probe_dir/probe.c"
cat > "$link_probe_dir/probe.cpp" <<'EOF'
#include <vector>
int main() { std::vector<int> values{1}; return values.front() - 1; }
EOF
if ! "$cc_bin" "${link_probe_flags[@]}" "$link_probe_dir/probe.c" -o "$link_probe_dir/probe-c" > "$link_probe_dir/error" 2>&1; then
cat "$link_probe_dir/error" >&2
rm -rf "$link_probe_dir"
fail "The C compiler cannot link a test program. Linux needs C development files (glibc startup objects and a compiler runtime) in addition to bundled Clang. Install your distribution's development packages, or on SteamOS run WiiCompiled through the Wheel Wizard Flatpak."
fi
if ! "$cxx_bin" "${link_probe_flags[@]}" "$link_probe_dir/probe.cpp" -o "$link_probe_dir/probe-cxx" > "$link_probe_dir/error" 2>&1; then
cat "$link_probe_dir/error" >&2
rm -rf "$link_probe_dir"
fail "The C++ compiler cannot link a test program. Install your distribution's C++ development packages, or on SteamOS run WiiCompiled through the Wheel Wizard Flatpak."
fi
rm -rf "$link_probe_dir"
# Literal line matching against the manifest's fixed shape, not a YAML dependency - the same # Literal line matching against the manifest's fixed shape, not a YAML dependency - the same
# approach NativeBuildFlags.ps1's Get-MkwProjectPins uses on Windows, kept here only for the one # approach NativeBuildFlags.ps1's Get-MkwProjectPins uses on Windows, kept here only for the one
# field this script actually needs from the manifest. # field this script actually needs from the manifest.
+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)
+96 -17
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}")
while IFS= read -r dependency; do queue_rpaths=("${queue_rpaths[@]:1}")
[[ "$dependency" == /opt/homebrew/* || "$dependency" == /usr/local/* ]] || continue current_rpaths=$(expanded_rpaths "$current")
[[ -f "$dependency" ]] || continue if [[ -n "$inherited_rpaths" ]]; then
name=$(basename "$dependency") current_rpaths="${current_rpaths}${current_rpaths:+$'\n'}$inherited_rpaths"
if [[ ! -f "$frameworks/$name" ]]; then
ditto "$dependency" "$frameworks/$name"
install_name_tool -id "@rpath/$name" "$frameworks/$name"
queue+=("$frameworks/$name")
fi fi
done < <(otool -L "$current" | tail -n +2 | awk '{print $1}') 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
[[ -z "$dependency" ]] && continue
[[ -n "$self_id" && "$dependency" == "$self_id" ]] && continue
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
ditto "$dep_path" "$frameworks/$name"
install_name_tool -id "@rpath/$name" "$frameworks/$name"
queue+=("$dep_path")
queue_rpaths+=("$current_rpaths")
fi
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 - {} +
+53 -10
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
@@ -77,26 +106,40 @@ if [[ -n "$retro_dir" ]]; then
# verify its pinned signature before publishing it into the local cache. # verify its pinned signature before publishing it into the local cache.
retro_wfc_dir="$support_root/RetroWfcPayload" retro_wfc_dir="$support_root/RetroWfcPayload"
retro_wfc_payload="$retro_wfc_dir/binary/payload.RMCPD00.bin" retro_wfc_payload="$retro_wfc_dir/binary/payload.RMCPD00.bin"
if [[ -f "$retro_wfc_payload" ]] && ! "$translator" validate-retro-wfc-payload --directory "$retro_wfc_dir"; then cached_payload_valid=0
if [[ -f "$retro_wfc_payload" ]]; then
if "$translator" validate-retro-wfc-payload --directory "$retro_wfc_dir"; then
cached_payload_valid=1
else
printf 'Discarding an invalid cached Retro-WFC payload...\n' >&2 printf 'Discarding an invalid cached Retro-WFC payload...\n' >&2
rm -f "$retro_wfc_payload" rm -f "$retro_wfc_payload"
fi fi
if [[ ! -f "$retro_wfc_payload" ]]; then fi
printf 'Downloading the Retro-WFC payload needed for online play...\n'
# A signed cache may still be an older vulnerable revision, so always attempt to replace it
# with the current signed snapshot. A transport failure may fall back to the verified cache;
# a downloaded snapshot with an invalid signature remains a hard failure.
printf 'Downloading the current Retro-WFC payload needed for online play...\n'
mkdir -p "$retro_wfc_dir" mkdir -p "$retro_wfc_dir"
payload_stage=$(mktemp -d "$retro_wfc_dir/.payload-download.XXXXXX") payload_stage=$(mktemp -d "$retro_wfc_dir/.payload-download.XXXXXX")
temporary_payload="$payload_stage/binary/payload.RMCPD00.bin" temporary_payload="$payload_stage/binary/payload.RMCPD00.bin"
mkdir -p "$(dirname "$temporary_payload")" mkdir -p "$(dirname "$temporary_payload")"
trap 'rm -rf "$payload_stage"' EXIT trap 'rm -rf "$payload_stage"' EXIT
/usr/bin/curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \ if /usr/bin/curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \
--retry 1 --output "$temporary_payload" \ --retry 1 --output "$temporary_payload" \
'https://rwfc.net/api/wfc/payload?g=RMCPD00' || fail 'could not download the Retro-WFC payload needed for online play' 'https://rwfc.net/api/wfc/payload?g=RMCPD00'; then
"$translator" validate-retro-wfc-payload --directory "$payload_stage" || \ "$translator" validate-retro-wfc-payload --directory "$payload_stage" || \
fail 'downloaded Retro-WFC payload failed signature validation' fail 'downloaded Retro-WFC payload failed signature validation'
mkdir -p "$retro_wfc_dir/binary" mkdir -p "$retro_wfc_dir/binary"
mv "$temporary_payload" "$retro_wfc_payload" mv "$temporary_payload" "$retro_wfc_payload"
rmdir "$payload_stage/binary" "$payload_stage" rmdir "$payload_stage/binary" "$payload_stage"
trap - EXIT trap - EXIT
elif (( cached_payload_valid )); then
printf 'Could not download the current Retro-WFC payload; using the previously verified cached payload.\n' >&2
rm -rf "$payload_stage"
trap - EXIT
else
fail 'could not download the current Retro-WFC payload and no valid cached payload is available'
fi fi
build_args+=(--profile both --base-output-dir "$products" --retro-rewind-package-dir "$retro_dir" --retro-wfc-offline-dir "$retro_wfc_dir") build_args+=(--profile both --base-output-dir "$products" --retro-rewind-package-dir "$retro_dir" --retro-wfc-offline-dir "$retro_wfc_dir")
fi fi
+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'
+1 -1
View File
@@ -225,7 +225,7 @@ EOF
# clang++ invocation above would not catch a broken CMAKE_ROOT (Modules/Templates) or a Ninja that # clang++ invocation above would not catch a broken CMAKE_ROOT (Modules/Templates) or a Ninja that
# can't find the compiler. # can't find the compiler.
cat > "$test_dir/CMakeLists.txt" <<'EOF' cat > "$test_dir/CMakeLists.txt" <<'EOF'
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.25)
project(test CXX) project(test CXX)
add_executable(test t.cpp) add_executable(test t.cpp)
EOF EOF
+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
+1 -1
View File
@@ -132,7 +132,7 @@ trees themselves (fetched by `Launcher/Prepare-Dependencies.ps1`) so end-user bu
| Abseil | LTS 20240722.0 | Apache-2.0 | <https://github.com/abseil/abseil-cpp> | | Abseil | LTS 20240722.0 | Apache-2.0 | <https://github.com/abseil/abseil-cpp> |
| Dear ImGui | 1.91.9b-docking | MIT | <https://github.com/ocornut/imgui> | | Dear ImGui | 1.91.9b-docking | MIT | <https://github.com/ocornut/imgui> |
| {fmt} | 11.1.4 | MIT | <https://github.com/fmtlib/fmt> | | {fmt} | 11.1.4 | MIT | <https://github.com/fmtlib/fmt> |
| xxHash | 0.8.3 | BSD-2-Clause | <https://github.com/Cyan4973/xxHash> | | xxHash | 0.8.4 | BSD-2-Clause | <https://github.com/Cyan4973/xxHash> |
| zlib | 1.3.2 | zlib | <https://github.com/madler/zlib> | | zlib | 1.3.2 | zlib | <https://github.com/madler/zlib> |
| libpng | 1.6.58 | PNG Reference Library License v2 | <https://github.com/pnggroup/libpng> | | libpng | 1.6.58 | PNG Reference Library License v2 | <https://github.com/pnggroup/libpng> |
| FreeType | 2.14.3 | **FreeType License (FTL)** - see below | <https://freetype.org/> | | FreeType | 2.14.3 | **FreeType License (FTL)** - see below | <https://freetype.org/> |
+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 ()
+7 -3
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 ()
@@ -101,7 +101,7 @@ if (_aurora_dawn_provider STREQUAL "vendor")
include(FetchContent) include(FetchContent)
FetchContent_Declare(dawn FetchContent_Declare(dawn
URL "https://github.com/google/dawn/archive/refs/tags/${AURORA_DAWN_VERSION}.tar.gz" URL "https://github.com/google/dawn/archive/refs/tags/${AURORA_DAWN_VERSION}.tar.gz"
DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL EXCLUDE_FROM_ALL
) )
FetchContent_MakeAvailable(dawn) FetchContent_MakeAvailable(dawn)
@@ -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")
@@ -194,7 +198,7 @@ elseif (_aurora_dawn_provider STREQUAL "package")
FetchContent_Declare(dawn_prebuilt FetchContent_Declare(dawn_prebuilt
URL "${AURORA_DAWN_PACKAGE_URL}" URL "${AURORA_DAWN_PACKAGE_URL}"
${_dawn_prebuilt_hash_argument} ${_dawn_prebuilt_hash_argument}
DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_EXTRACT_TIMESTAMP FALSE
) )
FetchContent_MakeAvailable(dawn_prebuilt) FetchContent_MakeAvailable(dawn_prebuilt)
+1 -1
View File
@@ -7,7 +7,7 @@
include(FetchContent) include(FetchContent)
FetchContent_Declare(libusb FetchContent_Declare(libusb
URL "https://github.com/libusb/libusb/releases/download/v${AURORA_LIBUSB_VERSION}/libusb-${AURORA_LIBUSB_VERSION}.tar.bz2" URL "https://github.com/libusb/libusb/releases/download/v${AURORA_LIBUSB_VERSION}/libusb-${AURORA_LIBUSB_VERSION}.tar.bz2"
DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_EXTRACT_TIMESTAMP FALSE
) )
# Upstream ships no CMakeLists.txt, so this only populates the source tree. # Upstream ships no CMakeLists.txt, so this only populates the source tree.
FetchContent_MakeAvailable(libusb) FetchContent_MakeAvailable(libusb)
+1 -1
View File
@@ -120,7 +120,7 @@ elseif (_aurora_nod_provider STREQUAL "package")
include(FetchContent) include(FetchContent)
FetchContent_Declare(nod_prebuilt FetchContent_Declare(nod_prebuilt
URL "${AURORA_NOD_PACKAGE_URL}" URL "${AURORA_NOD_PACKAGE_URL}"
DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL EXCLUDE_FROM_ALL
) )
FetchContent_MakeAvailable(nod_prebuilt) FetchContent_MakeAvailable(nod_prebuilt)
+7 -4
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()
@@ -92,7 +95,7 @@ elseif (_aurora_sdl3_provider STREQUAL "package")
include(FetchContent) include(FetchContent)
FetchContent_Declare(sdl3_prebuilt FetchContent_Declare(sdl3_prebuilt
URL "${AURORA_SDL3_PACKAGE_URL}" URL "${AURORA_SDL3_PACKAGE_URL}"
DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_EXTRACT_TIMESTAMP FALSE
) )
FetchContent_MakeAvailable(sdl3_prebuilt) FetchContent_MakeAvailable(sdl3_prebuilt)
@@ -145,7 +148,7 @@ elseif (_aurora_sdl3_provider STREQUAL "vendor")
endif () endif ()
FetchContent_Declare(SDL FetchContent_Declare(SDL
URL "https://github.com/libsdl-org/SDL/releases/download/release-${AURORA_SDL3_VERSION}/SDL3-${AURORA_SDL3_VERSION}.tar.gz" URL "https://github.com/libsdl-org/SDL/releases/download/release-${AURORA_SDL3_VERSION}/SDL3-${AURORA_SDL3_VERSION}.tar.gz"
DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_EXTRACT_TIMESTAMP FALSE
PATCH_COMMAND "${CMAKE_COMMAND}" -DSDL_SOURCE_DIR=<SOURCE_DIR> -P "${_aurora_sdl3_patches}" PATCH_COMMAND "${CMAKE_COMMAND}" -DSDL_SOURCE_DIR=<SOURCE_DIR> -P "${_aurora_sdl3_patches}"
EXCLUDE_FROM_ALL EXCLUDE_FROM_ALL
) )
+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)
+63 -13
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)
@@ -38,7 +88,7 @@ else ()
set(ABSL_PROPAGATE_CXX_STD ON) set(ABSL_PROPAGATE_CXX_STD ON)
FetchContent_Declare(abseil-cpp FetchContent_Declare(abseil-cpp
URL https://github.com/abseil/abseil-cpp/archive/refs/tags/20240722.0.tar.gz URL https://github.com/abseil/abseil-cpp/archive/refs/tags/20240722.0.tar.gz
DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL EXCLUDE_FROM_ALL
) )
FetchContent_MakeAvailable(abseil-cpp) FetchContent_MakeAvailable(abseil-cpp)
@@ -52,10 +102,10 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/AuroraSDL3Provider.cmake)
if (NOT TARGET xxhash) if (NOT TARGET xxhash)
message(STATUS "aurora: Fetching xxhash") message(STATUS "aurora: Fetching xxhash")
FetchContent_Declare(xxhash FetchContent_Declare(xxhash
URL https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.3.tar.gz URL https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.4.tar.gz
URL_HASH SHA256=aae608dfe8213dfd05d909a57718ef82f30722c392344583d3f39050c7f29a80 URL_HASH SHA256=5738270935e7c3d38a79b3adf7c9692566ce7895a25f67de43ad52ab504acd32
SOURCE_SUBDIR cmake_unofficial SOURCE_SUBDIR build/cmake
DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL EXCLUDE_FROM_ALL
) )
set(XXHASH_BUILD_XXHSUM OFF CACHE INTERNAL "Build the xxhsum binary") set(XXHASH_BUILD_XXHSUM OFF CACHE INTERNAL "Build the xxhsum binary")
@@ -78,7 +128,7 @@ if (NOT TARGET fmt)
FetchContent_Declare(fmt FetchContent_Declare(fmt
URL https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz URL https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz
URL_HASH SHA256=ac366b7b4c2e9f0dde63a59b3feb5ee59b67974b14ee5dc9ea8ad78aa2c1ee1e URL_HASH SHA256=ac366b7b4c2e9f0dde63a59b3feb5ee59b67974b14ee5dc9ea8ad78aa2c1ee1e
DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL EXCLUDE_FROM_ALL
) )
FetchContent_MakeAvailable(fmt) FetchContent_MakeAvailable(fmt)
@@ -116,7 +166,7 @@ if (AURORA_ENABLE_GX)
ZLIB ZLIB
URL https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz URL https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz
URL_HASH SHA256=bb329a0a2cd0274d05519d61c667c062e06990d72e125ee2dfa8de64f0119d16 URL_HASH SHA256=bb329a0a2cd0274d05519d61c667c062e06990d72e125ee2dfa8de64f0119d16
DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL EXCLUDE_FROM_ALL
) )
FetchContent_MakeAvailable(ZLIB) FetchContent_MakeAvailable(ZLIB)
@@ -152,7 +202,7 @@ if (AURORA_ENABLE_GX)
PNG PNG
URL https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.58.tar.gz URL https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.58.tar.gz
URL_HASH SHA256=A9D4DF463D36A6E5F9C29BD6F4967312D17E996C1854F3511F833924EB1993CF URL_HASH SHA256=A9D4DF463D36A6E5F9C29BD6F4967312D17E996C1854F3511F833924EB1993CF
DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL EXCLUDE_FROM_ALL
) )
FetchContent_MakeAvailable(PNG) FetchContent_MakeAvailable(PNG)
@@ -176,7 +226,7 @@ if (AURORA_ENABLE_GX)
FetchContent_Declare(Freetype FetchContent_Declare(Freetype
URL https://files.twilitrealm.dev/freetype-2.14.3.tar.gz URL https://files.twilitrealm.dev/freetype-2.14.3.tar.gz
URL_HASH SHA256=e61b31ab26358b946e767ed7eb7f4bb2e507da1cfefeb7a8861ace7fd5c899a1 URL_HASH SHA256=e61b31ab26358b946e767ed7eb7f4bb2e507da1cfefeb7a8861ace7fd5c899a1
DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL EXCLUDE_FROM_ALL
) )
FetchContent_MakeAvailable(Freetype) FetchContent_MakeAvailable(Freetype)
@@ -190,7 +240,7 @@ if (AURORA_ENABLE_GX)
FetchContent_Declare(imgui FetchContent_Declare(imgui
URL https://github.com/ocornut/imgui/archive/refs/tags/v1.91.9b-docking.tar.gz URL https://github.com/ocornut/imgui/archive/refs/tags/v1.91.9b-docking.tar.gz
URL_HASH SHA256=466fdef9b18de15f0bb6e288e3d00ffa3d82200ec458ce5e4f724a161d9528a5 URL_HASH SHA256=466fdef9b18de15f0bb6e288e3d00ffa3d82200ec458ce5e4f724a161d9528a5
DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL EXCLUDE_FROM_ALL
) )
FetchContent_MakeAvailable(imgui) FetchContent_MakeAvailable(imgui)
@@ -235,7 +285,7 @@ if (AURORA_ENABLE_GX)
FetchContent_Declare(sqlite3 FetchContent_Declare(sqlite3
URL https://sqlite.org/2026/sqlite-amalgamation-3510300.zip URL https://sqlite.org/2026/sqlite-amalgamation-3510300.zip
URL_HASH SHA256=acb1e6f5d832484bf6d32b681e858c38add8b2acdfd42ac5df24b8afb46552b4 URL_HASH SHA256=acb1e6f5d832484bf6d32b681e858c38add8b2acdfd42ac5df24b8afb46552b4
DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL EXCLUDE_FROM_ALL
) )
FetchContent_MakeAvailable(sqlite3) FetchContent_MakeAvailable(sqlite3)
@@ -278,7 +328,7 @@ if (AURORA_ENABLE_GX)
FetchContent_Declare(zstd FetchContent_Declare(zstd
URL https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz URL https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz
URL_HASH SHA256=eb33e51f49a15e023950cd7825ca74a4a2b43db8354825ac24fc1b7ee09e6fa3 URL_HASH SHA256=eb33e51f49a15e023950cd7825ca74a4a2b43db8354825ac24fc1b7ee09e6fa3
DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_EXTRACT_TIMESTAMP FALSE
SOURCE_SUBDIR build/cmake SOURCE_SUBDIR build/cmake
EXCLUDE_FROM_ALL EXCLUDE_FROM_ALL
) )
@@ -299,7 +349,7 @@ if (NOT TARGET TracyClient)
tracy tracy
URL https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz URL https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz
URL_HASH SHA256=24d342b5127d7f659dc3cf94f24347b348cc736f25b17a691fd7f69541937658 URL_HASH SHA256=24d342b5127d7f659dc3cf94f24347b348cc736f25b17a691fd7f69541937658
DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL EXCLUDE_FROM_ALL
) )
FetchContent_MakeAvailable(tracy) FetchContent_MakeAvailable(tracy)
+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);
@@ -88,6 +88,7 @@ typedef enum _AuroraViewportPolicy {
AURORA_VIEWPORT_FIT = 0, // Preserve logical aspect in the content framebuffer AURORA_VIEWPORT_FIT = 0, // Preserve logical aspect in the content framebuffer
AURORA_VIEWPORT_STRETCH = 1, // Match content framebuffer aspect to the native surface AURORA_VIEWPORT_STRETCH = 1, // Match content framebuffer aspect to the native surface
AURORA_VIEWPORT_NATIVE = 2, // Use active framebuffer pixels directly AURORA_VIEWPORT_NATIVE = 2, // Use active framebuffer pixels directly
AURORA_VIEWPORT_16_9 = 3, // Fixed 16:9 content with bars on other surfaces
} AuroraViewportPolicy; } AuroraViewportPolicy;
/** /**
+148 -7
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(); }
+1
View File
@@ -46,6 +46,7 @@ void AuroraSetViewportPolicy(AuroraViewportPolicy policy) {
} }
g_gxState.viewportPolicy = policy; g_gxState.viewportPolicy = policy;
aurora::window::set_frame_buffer_aspect_fit(policy == AURORA_VIEWPORT_FIT); aurora::window::set_frame_buffer_aspect_fit(policy == AURORA_VIEWPORT_FIT);
aurora::window::set_force_aspect_16_9(policy == AURORA_VIEWPORT_16_9);
aurora::window::set_present_surface_fill(policy == AURORA_VIEWPORT_STRETCH); aurora::window::set_present_surface_fill(policy == AURORA_VIEWPORT_STRETCH);
if (changed) { if (changed) {
// Reapply the guest viewport and scissor after a resize. // Reapply the guest viewport and scissor after a resize.
+3 -5
View File
@@ -520,11 +520,9 @@ void GXCopyTex(void* dest, GXBool clear) {
clearState.clearAlpha = clear && alphaUpdate; clearState.clearAlpha = clear && alphaUpdate;
} }
const auto copyFilter = combined_copy_filter_coefficients(g_gxState.copyFilterVFilter); const auto copyFilter = combined_copy_filter_coefficients(g_gxState.copyFilterVFilter);
// Every GXCopyTex is observable texture data. Reusing a destination in this // Skip only recurring color copies so one-shot copies are never lost.
// or the previous frame does not guarantee another redraw: menu thumbnail const bool producedConsecutively = handle.revision != 0 && currentFrame - handle.lastProducedFrame <= 1;
// scratch targets can be reused and then retained. Depth copies have the const bool persistentCopy = !aurora::gx::is_depth_format(texCopyFmt) && !producedConsecutively;
// same requirement. Only display presentation may skip unfinished draws.
const bool persistentCopy = true;
aurora::gfx::resolve_pass(handle.handle, rect, clearState.clearColor, clearState.clearAlpha, clearState.clearDepth, aurora::gfx::resolve_pass(handle.handle, rect, clearState.clearColor, clearState.clearAlpha, clearState.clearDepth,
clearState.clearColorValue, aurora::gx::clear_depth_value(), resolveFmt, clearState.clearColorValue, aurora::gx::clear_depth_value(), resolveFmt,
&sourceRect.sampleRect, g_gxState.texCopyHalfScale, &copyFilter, forceOpaqueAlpha, &sourceRect.sampleRect, g_gxState.texCopyHalfScale, &copyFilter, forceOpaqueAlpha,
+31 -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;
}); });
@@ -399,19 +399,20 @@ const char* PADGetNameForControllerIndex(const u32 idx) {
} }
void PADSetPortForIndex(const u32 idx, const u32 port) { void PADSetPortForIndex(const u32 idx, const u32 port) {
if (port >= PAD_MAX_CONTROLLERS) return;
const auto* ctrl = __PADGetControllerForIndex(idx); const auto* ctrl = __PADGetControllerForIndex(idx);
if (ctrl == nullptr) { if (ctrl == nullptr) {
return; return;
} }
const int32_t oldPort = SDL_GetGamepadPlayerIndex(ctrl->m_controller); const int32_t oldPort = aurora::input::player_index(ctrl->m_index);
if (const auto* dest = aurora::input::get_controller_for_player(port); dest != nullptr && dest != ctrl) { if (const auto* dest = aurora::input::get_controller_for_player(port); dest != nullptr && dest != ctrl) {
SDL_SetGamepadPlayerIndex(dest->m_controller, -1); aurora::input::set_player_index(dest->m_index, -1);
} }
if (oldPort >= 0 && oldPort != port) { if (oldPort >= 0 && oldPort != port) {
aurora::input::persist_controller_for_player(oldPort, nullptr); aurora::input::persist_controller_for_player(oldPort, nullptr);
} }
SDL_SetGamepadPlayerIndex(ctrl->m_controller, static_cast<Sint32>(port)); aurora::input::set_player_index(ctrl->m_index, static_cast<Sint32>(port));
aurora::input::persist_controller_for_player(port, ctrl); aurora::input::persist_controller_for_player(port, ctrl);
} }
@@ -437,7 +438,7 @@ void PADClearPort(const u32 port) {
if (ctrl == nullptr) { if (ctrl == nullptr) {
return; return;
} }
SDL_SetGamepadPlayerIndex(ctrl->m_controller, -1); aurora::input::set_player_index(ctrl->m_index, -1);
} }
// Secondary bindings live only in memory; the runtime re-applies them from its // Secondary bindings live only in memory; the runtime re-applies them from its
@@ -646,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;
} }
@@ -737,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)) {
@@ -845,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;
} }
@@ -859,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;
} }
@@ -1189,7 +1190,7 @@ 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;
@@ -1223,7 +1224,7 @@ 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;
@@ -1250,7 +1251,7 @@ 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;
@@ -1425,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;
} }
@@ -1467,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),
@@ -1569,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();
} }
@@ -1583,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();
} }
@@ -1592,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();
} }
+4 -10
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>
@@ -1259,11 +1258,6 @@ void split_staging_batch() {
retained[i].assign(buffers[i]->data(), buffers[i]->data() + g_suspendedEfbBytes[i]); retained[i].assign(buffers[i]->data(), buffers[i]->data() + g_suspendedEfbBytes[i]);
} }
} }
// GXCopyTex can capture the ordinary EFB as well as an explicit offscreen
// target. Its command may arrive in the next batch, after this prefix has
// already been submitted. Preserve every prefix; target kind cannot tell
// us whether the guest will later retain these pixels in a texture.
for (auto& pass : g_renderPasses) pass.requireReadyPipelines = true;
auto encoder = g_device.CreateCommandEncoder(); auto encoder = g_device.CreateCommandEncoder();
end_batch(encoder); end_batch(encoder);
render(encoder); render(encoder);
@@ -1453,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();
} }
@@ -1614,8 +1608,8 @@ bool bind_pipeline(PipelineRef ref, const wgpu::RenderPassEncoder& pass, Pipelin
if (!skip_unready_pipelines()) { if (!skip_unready_pipelines()) {
pipelineReady = wait_pipeline(ref, pipeline); pipelineReady = wait_pipeline(ref, pipeline);
} else if (requireReady) { } else if (requireReady) {
// Texture copies and capacity prefixes must retain complete draw results. // The pass resolves into a persistent texture (a one-shot bake such as MKW's minimap), so a
// A future display frame cannot repair a texture that already captured them. // skipped draw would never be re-issued. These run behind loads, not mid-race.
pipelineReady = wait_pipeline_for_persistent_pass(ref, pipeline); pipelineReady = wait_pipeline_for_persistent_pass(ref, pipeline);
} else { } else {
pipelineReady = try_pipeline(ref, pipeline); pipelineReady = try_pipeline(ref, pipeline);
+2 -1
View File
@@ -214,7 +214,8 @@ Params make_params(wgpu::Extent3D sourceSize, const FrameMapping& mapping) noexc
return params; return params;
} }
const bool stretch = mapping.viewportPolicy == AURORA_VIEWPORT_STRETCH; const bool stretch = mapping.viewportPolicy == AURORA_VIEWPORT_STRETCH ||
mapping.viewportPolicy == AURORA_VIEWPORT_16_9;
const float scaleX = static_cast<float>(sourceSize.width) / static_cast<float>(logicalSize.x); const float scaleX = static_cast<float>(sourceSize.width) / static_cast<float>(logicalSize.x);
const float scaleY = static_cast<float>(sourceSize.height) / static_cast<float>(logicalSize.y); const float scaleY = static_cast<float>(sourceSize.height) / static_cast<float>(logicalSize.y);
const float scale = std::min(scaleX, scaleY); const float scale = std::min(scaleX, scaleY);
+23
View File
@@ -414,6 +414,29 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
g_GameControllers[instance] = controller; g_GameControllers[instance] = controller;
ensure_player_index(g_GameControllers[instance]); ensure_player_index(g_GameControllers[instance]);
apply_port_preferences(); apply_port_preferences();
#if defined(SDL_PLATFORM_MACOS)
// First-use convenience only: never override a saved assignment or None.
if (g_portPreferences[0].state == PortPreferenceState::Unset) {
bool hasOtherPortPreference = false;
for (size_t port = 1; port < g_portPreferences.size(); ++port) {
if (g_portPreferences[port].state == PortPreferenceState::Controller &&
identity_match(g_portPreferences[port].identity, controller_identity(g_GameControllers[instance])) !=
IdentityMatch::None) {
hasOtherPortPreference = true;
break;
}
}
if (!hasOtherPortPreference) {
const auto* p0 = get_controller_for_player(0);
if (p0 == nullptr) {
assign_player_index(g_GameControllers[instance], 0);
persist_controller_for_player(0, &g_GameControllers[instance]);
} else if (p0 == &g_GameControllers[instance]) {
persist_controller_for_player(0, &g_GameControllers[instance]);
}
}
}
#endif
return instance; return instance;
} }
+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
+27 -1
View File
@@ -46,6 +46,7 @@ SDL_Window* g_window;
SDL_Renderer* g_renderer; SDL_Renderer* g_renderer;
float g_frameBufferScale = 0.f; float g_frameBufferScale = 0.f;
bool g_frameBufferAspectFit = true; bool g_frameBufferAspectFit = true;
std::atomic_bool g_forceAspect169{false};
bool g_presentSurfaceFill = false; bool g_presentSurfaceFill = false;
int g_presentAspectWidth = 0; int g_presentAspectWidth = 0;
int g_presentAspectHeight = 0; int g_presentAspectHeight = 0;
@@ -525,7 +526,20 @@ AuroraWindowSize get_window_size() {
int fb_w = native_fb_w; int fb_w = native_fb_w;
int fb_h = native_fb_h; int fb_h = native_fb_h;
const auto [baseW, baseH] = vi::configured_fb_size(); const auto [baseW, baseH] = vi::configured_fb_size();
if (g_frameBufferAspectFit && baseW > 0 && baseH > 0) { if (g_forceAspect169.load(std::memory_order_acquire) && native_fb_w > 0 && native_fb_h > 0) {
if (g_frameBufferScale > 0.f && baseW > 0 && baseH > 0) {
const auto [scaledW, scaledH] =
scale_frame_buffer_to_aspect(static_cast<int>(baseW), static_cast<int>(baseH),
g_frameBufferScale, 16.f / 9.f);
fb_w = scaledW;
fb_h = scaledH;
} else {
fb_w = std::min(native_fb_w,
std::max(1, static_cast<int>(std::lround(native_fb_h * (16.f / 9.f)))));
fb_h = std::min(native_fb_h,
std::max(1, static_cast<int>(std::lround(native_fb_w * (9.f / 16.f)))));
}
} else if (g_frameBufferAspectFit && baseW > 0 && baseH > 0) {
float renderScale = g_frameBufferScale > 0.f ? g_frameBufferScale : 1.f; float renderScale = g_frameBufferScale > 0.f ? g_frameBufferScale : 1.f;
if (g_frameBufferScale <= 0.f) { if (g_frameBufferScale <= 0.f) {
renderScale = std::min(static_cast<float>(native_fb_w) / static_cast<float>(baseW), renderScale = std::min(static_cast<float>(native_fb_w) / static_cast<float>(baseW),
@@ -759,6 +773,14 @@ void set_frame_buffer_aspect_fit(bool fit) {
request_frame_buffer_resize(); request_frame_buffer_resize();
} }
void set_force_aspect_16_9(bool force) {
if (g_forceAspect169.load(std::memory_order_relaxed) == force) {
return;
}
g_forceAspect169.store(force, std::memory_order_release);
request_frame_buffer_resize();
}
void set_present_surface_fill(bool fill) { void set_present_surface_fill(bool fill) {
g_presentSurfaceFill = fill; g_presentSurfaceFill = fill;
} }
@@ -781,6 +803,10 @@ void unlock_present_aspect_ratio() {
} }
bool get_present_aspect_ratio(float& aspect) noexcept { bool get_present_aspect_ratio(float& aspect) noexcept {
if (g_forceAspect169.load(std::memory_order_acquire)) {
aspect = 16.f / 9.f;
return true;
}
if (g_presentSurfaceFill && g_window != nullptr) { if (g_presentSurfaceFill && g_window != nullptr) {
// Queried once per presentation snapshot; use the cached native client size // Queried once per presentation snapshot; use the cached native client size
// instead of re-entering SDL for a value the window procedure already knows. // instead of re-entering SDL for a value the window procedure already knows.
+1
View File
@@ -56,6 +56,7 @@ void sync_frame_buffer_size() noexcept;
void request_frame_buffer_resize(); void request_frame_buffer_resize();
void set_frame_buffer_scale(float scale); void set_frame_buffer_scale(float scale);
void set_frame_buffer_aspect_fit(bool fit); void set_frame_buffer_aspect_fit(bool fit);
void set_force_aspect_16_9(bool force);
void set_present_surface_fill(bool fill); void set_present_surface_fill(bool fill);
void lock_present_aspect_ratio(int width, int height); void lock_present_aspect_ratio(int width, int height);
void unlock_present_aspect_ratio(); void unlock_present_aspect_ratio();
+1 -1
View File
@@ -5,7 +5,7 @@ if (NOT TARGET gtest)
FetchContent_Declare(googletest FetchContent_Declare(googletest
URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.tar.gz URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.tar.gz
URL_HASH SHA256=65fab701d9829d38cb77c14acdc431d2108bfdbf8979e40eb8ae567edf10b27c URL_HASH SHA256=65fab701d9829d38cb77c14acdc431d2108bfdbf8979e40eb8ae567edf10b27c
DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL EXCLUDE_FROM_ALL
) )
FetchContent_MakeAvailable(googletest) FetchContent_MakeAvailable(googletest)
+4 -4
View File
@@ -4267,7 +4267,7 @@ TEST_F(GXFifoTest, CopyTexColorFormatMarksResolvePersistent) {
EXPECT_TRUE(records.front().persistentCopy); EXPECT_TRUE(records.front().persistentCopy);
} }
TEST_F(GXFifoTest, RecurringColorCopyPreservesEveryResolve) { TEST_F(GXFifoTest, RecurringColorCopyKeepsLaterResolveSkippable) {
std::array<u8, 152 * 114 * 4> image{}; std::array<u8, 152 * 114 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24; gxState().pixelFmt = GX_PF_RGBA6_Z24;
@@ -4281,7 +4281,7 @@ TEST_F(GXFifoTest, RecurringColorCopyPreservesEveryResolve) {
const auto& records = aurora::gfx::testing::resolve_pass_records(); const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 2u); ASSERT_EQ(records.size(), 2u);
EXPECT_TRUE(records[0].persistentCopy); EXPECT_TRUE(records[0].persistentCopy);
EXPECT_TRUE(records[1].persistentCopy); EXPECT_FALSE(records[1].persistentCopy);
} }
TEST_F(GXFifoTest, ColorCopyAfterFrameGapRegainsPersistentProtection) { TEST_F(GXFifoTest, ColorCopyAfterFrameGapRegainsPersistentProtection) {
@@ -4301,7 +4301,7 @@ TEST_F(GXFifoTest, ColorCopyAfterFrameGapRegainsPersistentProtection) {
EXPECT_TRUE(records[1].persistentCopy); EXPECT_TRUE(records[1].persistentCopy);
} }
TEST_F(GXFifoTest, CopyTexDepthFormatPreservesResolve) { TEST_F(GXFifoTest, CopyTexDepthFormatKeepsResolveSkippable) {
std::array<u8, 4 * 4 * 4> image{}; std::array<u8, 4 * 4 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24; gxState().pixelFmt = GX_PF_RGBA6_Z24;
@@ -4311,7 +4311,7 @@ TEST_F(GXFifoTest, CopyTexDepthFormatPreservesResolve) {
const auto& records = aurora::gfx::testing::resolve_pass_records(); const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u); ASSERT_EQ(records.size(), 1u);
EXPECT_TRUE(records.front().persistentCopy); EXPECT_FALSE(records.front().persistentCopy);
} }
TEST_F(GXFifoTest, CopyDispResolveIsNotPersistent) { TEST_F(GXFifoTest, CopyDispResolveIsNotPersistent) {
@@ -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;
}
+117 -22
View File
@@ -1,27 +1,53 @@
cmake_minimum_required(VERSION 3.16) 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)
@@ -113,11 +140,10 @@ endif()
# compiles cleanly for Android with nothing beyond a plain C toolchain (no perl/asm build-script # compiles cleanly for Android with nothing beyond a plain C toolchain (no perl/asm build-script
# dependency the way OpenSSL's build has), matching how this project already prefers toolchain- # dependency the way OpenSSL's build has), matching how this project already prefers toolchain-
# simple libraries (see Crypto++ above, similarly stripped of ASM/SIMD for portability). # simple libraries (see Crypto++ above, similarly stripped of ASM/SIMD for portability).
# Fetched at build time from a pinned upstream release tarball with a checked SHA-256, the same way # The from-source build fetches a pinned upstream tarball with a checked SHA-256.
# aurora-main's own dependencies (SDL, zlib, etc.) are pulled in - not committed as a vendored # The Linux native prebuilt package instead ships its compiled archives and headers.
# source tree, so the repository ships the compiled dependency rather than ~280 tracked upstream # Bump both values in cmake/MbedTLSPin.cmake when updating; the hash comes from
# files. Bump MKW_MBEDTLS_VERSION/MKW_MBEDTLS_SHA256 together when updating; the hash comes from # upstream's signed `mbedtls-<version>-sha256sum.txt` release asset.
# upstream's own signed `mbedtls-<version>-sha256sum.txt` release asset.
# #
# The alias exists on every platform so the link lines in cmake/PublicProducts.cmake stay # The alias exists on every platform so the link lines in cmake/PublicProducts.cmake stay
# platform-independent, but it is only populated where network_ssl.cpp actually compiles the mbed # platform-independent, but it is only populated where network_ssl.cpp actually compiles the mbed
@@ -128,14 +154,15 @@ endif()
add_library(mkw_mbedtls INTERFACE) add_library(mkw_mbedtls INTERFACE)
add_library(mkw::mbedtls ALIAS mkw_mbedtls) add_library(mkw::mbedtls ALIAS mkw_mbedtls)
if(NOT MKW_PLATFORM_WINDOWS) if(NOT MKW_PLATFORM_WINDOWS)
include("${CMAKE_CURRENT_LIST_DIR}/cmake/MbedTLSPin.cmake")
if(NOT MKW_NATIVE_PREBUILT_DIR)
include(FetchContent) include(FetchContent)
set(MKW_MBEDTLS_VERSION "3.6.7")
set(MKW_MBEDTLS_SHA256 "a7e8bcbec0e6f761b4af24f25677626b35f762f68eef79c08677a363212d11f6")
FetchContent_Declare(mkw_mbedtls_upstream FetchContent_Declare(mkw_mbedtls_upstream
URL "https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-${MKW_MBEDTLS_VERSION}/mbedtls-${MKW_MBEDTLS_VERSION}.tar.bz2" URL "https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-${MKW_MBEDTLS_VERSION}/mbedtls-${MKW_MBEDTLS_VERSION}.tar.bz2"
URL_HASH SHA256=${MKW_MBEDTLS_SHA256}) URL_HASH SHA256=${MKW_MBEDTLS_SHA256}
# Subproject mode already defaults ENABLE_TESTING off and skips codegen (GEN_FILES), but DOWNLOAD_EXTRACT_TIMESTAMP FALSE
# ENABLE_PROGRAMS defaults on and installation/package-config isn't wanted for a linked-in copy. )
# Subproject mode already disables testing and codegen; it still enables programs.
set(ENABLE_PROGRAMS OFF CACHE BOOL "" FORCE) set(ENABLE_PROGRAMS OFF CACHE BOOL "" FORCE)
set(ENABLE_TESTING OFF CACHE BOOL "" FORCE) set(ENABLE_TESTING OFF CACHE BOOL "" FORCE)
set(MBEDTLS_FATAL_WARNINGS OFF CACHE BOOL "" FORCE) set(MBEDTLS_FATAL_WARNINGS OFF CACHE BOOL "" FORCE)
@@ -143,6 +170,7 @@ if(NOT MKW_PLATFORM_WINDOWS)
FetchContent_MakeAvailable(mkw_mbedtls_upstream) FetchContent_MakeAvailable(mkw_mbedtls_upstream)
target_link_libraries(mkw_mbedtls INTERFACE MbedTLS::mbedtls MbedTLS::mbedx509 MbedTLS::mbedcrypto) target_link_libraries(mkw_mbedtls INTERFACE MbedTLS::mbedtls MbedTLS::mbedx509 MbedTLS::mbedcrypto)
endif() endif()
endif()
set(MKW_TRANSLATED_COMPILE_JOBS 0 CACHE STRING set(MKW_TRANSLATED_COMPILE_JOBS 0 CACHE STRING
"Cap on concurrently compiling translated shard TUs via a Ninja job pool (0 = uncapped). \ "Cap on concurrently compiling translated shard TUs via a Ninja job pool (0 = uncapped). \
@@ -161,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)
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
@@ -280,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")
if(MKW_PLATFORM_MACOS_ARM64)
# HostContext's Apple Silicon backend is implemented in a small assembly # HostContext's Apple Silicon backend is implemented in a small assembly
# companion. It must be part of the product runtime as well as the # companion. It must be part of the product runtime as well as the
# standalone context test; otherwise the final executable is missing # standalone context test; otherwise the final executable is missing
# mkw_co_init/mkw_co_switch at link time. # mkw_co_init/mkw_co_switch at link time.
enable_language(ASM) enable_language(ASM)
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S") 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()
@@ -309,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)
@@ -356,8 +426,9 @@ 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)
# Apple Silicon's context ABI is implemented by the local assembly backend.
enable_language(ASM) enable_language(ASM)
add_executable(mkw_macos_context_abi_tests add_executable(mkw_macos_context_abi_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp" "${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
@@ -370,6 +441,16 @@ if(MKW_PLATFORM_MACOS)
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp" "${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S") "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include") target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
else()
# Intel macOS follows the same System V AMD64 libco path as Linux.
add_executable(mkw_macos_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp")
target_include_directories(mkw_macos_host_context_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include"
"${CMAKE_CURRENT_LIST_DIR}/third_party/libco")
target_link_libraries(mkw_macos_host_context_tests PRIVATE mkw::libco)
endif()
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17) target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests) add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
@@ -379,6 +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
@@ -427,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)
+4
View File
@@ -0,0 +1,4 @@
# Keep the release and verified archive hash together. The Linux native prebuilt
# fingerprint includes this file, so changing either value forces a new harvest.
set(MKW_MBEDTLS_VERSION "3.6.7")
set(MKW_MBEDTLS_SHA256 "a7e8bcbec0e6f761b4af24f25677626b35f762f68eef79c08677a363212d11f6")
+22
View File
@@ -42,6 +42,28 @@ endfunction()
mkw_np_resolve(_mkw_np_includes ${MKW_NP_INCLUDE_DIRECTORIES}) mkw_np_resolve(_mkw_np_includes ${MKW_NP_INCLUDE_DIRECTORIES})
mkw_np_resolve(_mkw_np_links ${MKW_NP_LINK_LIBRARIES}) mkw_np_resolve(_mkw_np_links ${MKW_NP_LINK_LIBRARIES})
mkw_np_resolve(_mkw_np_dawn_config ${MKW_NP_DAWN_CONFIG_DIR}) mkw_np_resolve(_mkw_np_dawn_config ${MKW_NP_DAWN_CONFIG_DIR})
if(NOT MKW_PLATFORM_WINDOWS)
if(NOT MKW_NP_MBEDTLS_LIBRARIES OR NOT MKW_NP_MBEDTLS_INCLUDE_DIR)
message(FATAL_ERROR
"The Linux native prebuilt package has no Mbed TLS archives; regenerate it with Prepare-NativePrebuilt.sh")
endif()
file(SHA256 "${CMAKE_CURRENT_LIST_DIR}/MbedTLSPin.cmake" _mkw_np_current_mbedtls_fingerprint)
if(NOT MKW_NP_MBEDTLS_FINGERPRINT STREQUAL _mkw_np_current_mbedtls_fingerprint)
message(FATAL_ERROR "The native prebuilt Mbed TLS version differs from this workspace; regenerate the package")
endif()
mkw_np_resolve(_mkw_np_mbedtls_links ${MKW_NP_MBEDTLS_LIBRARIES})
mkw_np_resolve(_mkw_np_mbedtls_include ${MKW_NP_MBEDTLS_INCLUDE_DIR})
foreach(_archive IN LISTS _mkw_np_mbedtls_links)
if(NOT EXISTS "${_archive}")
message(FATAL_ERROR "The native prebuilt package is missing Mbed TLS archive: ${_archive}")
endif()
endforeach()
if(NOT IS_DIRECTORY "${_mkw_np_mbedtls_include}")
message(FATAL_ERROR "The native prebuilt package is missing Mbed TLS headers: ${_mkw_np_mbedtls_include}")
endif()
target_include_directories(mkw_mbedtls SYSTEM INTERFACE "${_mkw_np_mbedtls_include}")
target_link_libraries(mkw_mbedtls INTERFACE ${_mkw_np_mbedtls_links})
endif()
foreach(_dir IN LISTS _mkw_np_includes) foreach(_dir IN LISTS _mkw_np_includes)
if(NOT IS_DIRECTORY "${_dir}") if(NOT IS_DIRECTORY "${_dir}")
+23
View File
@@ -77,6 +77,9 @@ target_compile_features(mkw_np_probe PRIVATE cxx_std_20)
target_link_libraries(mkw_np_probe PRIVATE target_link_libraries(mkw_np_probe PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
mkw::cryptopp) mkw::cryptopp)
if(NOT MKW_PLATFORM_WINDOWS)
target_link_libraries(mkw_np_probe PRIVATE mkw::mbedtls)
endif()
get_filename_component(_mkw_np_aurora_dir "${MKW_AURORA_DIR}" ABSOLUTE) get_filename_component(_mkw_np_aurora_dir "${MKW_AURORA_DIR}" ABSOLUTE)
mkw_collect_buildsystem_targets("${_mkw_np_aurora_dir}" _mkw_np_all_targets) mkw_collect_buildsystem_targets("${_mkw_np_aurora_dir}" _mkw_np_all_targets)
@@ -84,6 +87,25 @@ mkw_collect_buildsystem_targets("${_mkw_np_aurora_dir}" _mkw_np_all_targets)
# the scan above cannot see it; it is appended explicitly. The type and closure # the scan above cannot see it; it is appended explicitly. The type and closure
# checks below still apply to it. # checks below still apply to it.
list(APPEND _mkw_np_all_targets "mkw_cryptopp") list(APPEND _mkw_np_all_targets "mkw_cryptopp")
if(NOT MKW_PLATFORM_WINDOWS)
set(_mkw_np_mbedtls_targets mbedtls mbedx509 mbedcrypto)
# mbedcrypto links these third-party archives when upstream defines them.
# Keep dependencies after mbedcrypto for static linking on the consumer side.
foreach(_t everest p256m)
if(TARGET ${_t})
list(APPEND _mkw_np_mbedtls_targets ${_t})
endif()
endforeach()
list(APPEND _mkw_np_all_targets ${_mkw_np_mbedtls_targets})
# Keep the complete TLS archive set separate from aurora's aggregate link interface.
set(_mkw_np_mbedtls_lines "")
foreach(_t IN LISTS _mkw_np_mbedtls_targets)
string(APPEND _mkw_np_mbedtls_lines
"${_t}|$<TARGET_LINKER_FILE:${_t}>\n")
endforeach()
file(GENERATE OUTPUT "${MKW_NATIVE_PREBUILT_EXPORT_DIR}/mbedtls.txt"
CONTENT "${_mkw_np_mbedtls_lines}")
endif()
if(NOT _mkw_np_all_targets) if(NOT _mkw_np_all_targets)
message(FATAL_ERROR "No buildsystem targets were found under ${_mkw_np_aurora_dir}") message(FATAL_ERROR "No buildsystem targets were found under ${_mkw_np_aurora_dir}")
endif() endif()
@@ -136,6 +158,7 @@ string(REPLACE ";" "," _mkw_np_lib_targets_csv "${_mkw_np_lib_targets}")
file(WRITE "${MKW_NATIVE_PREBUILT_EXPORT_DIR}/meta.txt" file(WRITE "${MKW_NATIVE_PREBUILT_EXPORT_DIR}/meta.txt"
"aurora_dir=${_mkw_np_aurora_dir}\n" "aurora_dir=${_mkw_np_aurora_dir}\n"
"runtime_dir=${CMAKE_CURRENT_SOURCE_DIR}\n" "runtime_dir=${CMAKE_CURRENT_SOURCE_DIR}\n"
"mbedtls_source_dir=${mkw_mbedtls_upstream_SOURCE_DIR}\n"
"binary_dir=${CMAKE_BINARY_DIR}\n" "binary_dir=${CMAKE_BINARY_DIR}\n"
"dawn_config_dir=${_mkw_np_dawn_config_dir}\n" "dawn_config_dir=${_mkw_np_dawn_config_dir}\n"
"dawn_prebuilt_source_dir=${dawn_prebuilt_SOURCE_DIR}\n" "dawn_prebuilt_source_dir=${dawn_prebuilt_SOURCE_DIR}\n"
+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
+3 -1
View File
@@ -16,7 +16,9 @@
#endif #endif
extern "C" bool g_dynamicAspectRatioEnabled; extern "C" bool g_dynamicAspectRatioEnabled;
void ConfigureMkwDynamicAspect(bool widescreen, uint32_t surfaceWidth, uint32_t surfaceHeight); void ConfigureMkwDynamicAspect(bool widescreen, bool forceAspect169, uint32_t surfaceWidth, uint32_t surfaceHeight);
void SetMkwForceAspect169(bool enabled);
bool MkwForceAspect169Requested();
void UpdateMkwDynamicAspectSurface(uint32_t surfaceWidth, uint32_t surfaceHeight); void UpdateMkwDynamicAspectSurface(uint32_t surfaceWidth, uint32_t surfaceHeight);
// Arms the "keep EGG::Frustum's projection scale" flag on every screen that // Arms the "keep EGG::Frustum's projection scale" flag on every screen that
// renders to a fixed-size offscreen target. Cheap and idempotent; called from // renders to a fixed-size offscreen target. Cheap and idempotent; called from
+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*;
+66 -96
View File
@@ -294,7 +294,7 @@ inline double PpcLoadPairPsqFloatFastInline(uint32_t addr)
return PpcLoadPairPsqFloatFromHostInline(host); return PpcLoadPairPsqFloatFromHostInline(host);
} }
return PpcBitCastToDoubleInline( return PpcBitCastToDoubleInline(
PpcLoadPairPsqFloatBitsPackedInline(MemoryInline::Read64Slow(addr))); PpcLoadPairPsqFloatBitsPackedInline(Memory::Read64(addr)));
} }
inline double PpcLoadSinglePsqFloatFastInline(uint32_t addr) inline double PpcLoadSinglePsqFloatFastInline(uint32_t addr)
@@ -349,7 +349,7 @@ inline void PpcStorePairPsqFloatFastInline(uint32_t addr, double value)
PpcStorePairPsqFloatToHostInline(host, value); PpcStorePairPsqFloatToHostInline(host, value);
return; return;
} }
MemoryInline::Write64Slow( Memory::Write64(
addr, PpcStorePairPsqFloatBitsPackedInline(PpcBitCastToU64Inline(value))); addr, PpcStorePairPsqFloatBitsPackedInline(PpcBitCastToU64Inline(value)));
} }
@@ -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;
+24
View File
@@ -33,6 +33,7 @@
struct RuntimeUserConfig { struct RuntimeUserConfig {
std::optional<bool> widescreen; std::optional<bool> widescreen;
std::optional<bool> forceAspect169;
std::optional<int32_t> windowPosX; std::optional<int32_t> windowPosX;
std::optional<int32_t> windowPosY; std::optional<int32_t> windowPosY;
std::optional<uint32_t> windowWidth; std::optional<uint32_t> windowWidth;
@@ -41,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;
@@ -298,6 +300,7 @@ inline void EnsureConfigFile() {
"# Set paths.dvd_root to an extracted Mario Kart Wii DATA directory.\n\n" "# Set paths.dvd_root to an extracted Mario Kart Wii DATA directory.\n\n"
"[video]\n" "[video]\n"
"widescreen = true\n" "widescreen = true\n"
"force_16_9 = false\n"
"resolution_multiplier = 1.0\n" "resolution_multiplier = 1.0\n"
"frame_interpolation_fps = 0\n" "frame_interpolation_fps = 0\n"
"display_mode = \"windowed\"\n" "display_mode = \"windowed\"\n"
@@ -426,6 +429,7 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
} }
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen"); config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
config.forceAspect169 = FindConfigValue<bool>(document, "video", "force_16_9");
config.windowPosX = FindConfigInt(document, "video", "window_x"); config.windowPosX = FindConfigInt(document, "video", "window_x");
config.windowPosY = FindConfigInt(document, "video", "window_y"); config.windowPosY = FindConfigInt(document, "video", "window_y");
if (auto value = FindConfigUint(document, "video", "window_width"); value && *value != 0) { if (auto value = FindConfigUint(document, "video", "window_width"); value && *value != 0) {
@@ -456,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");
@@ -657,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;
@@ -782,6 +793,15 @@ inline bool WidescreenEnabled(bool fallback = false) {
return Get().widescreen.value_or(fallback); return Get().widescreen.value_or(fallback);
} }
inline bool ForceAspect169Enabled(bool fallback = false) {
return Get().forceAspect169.value_or(fallback);
}
inline bool SetForceAspect169(bool value) {
Mutable().forceAspect169 = value;
return WriteSetting("video", "force_16_9", value ? "true" : "false");
}
inline bool WindowPosition(int32_t& x, int32_t& y) { inline bool WindowPosition(int32_t& x, int32_t& y) {
if (!Get().windowPosX || !Get().windowPosY) { if (!Get().windowPosX || !Get().windowPosY) {
return false; return false;
@@ -803,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);
} }
+3 -2
View File
@@ -132,8 +132,9 @@ using CryptoEcdsa = CryptoPP::ECDSA<CryptoPP::EC2N, CryptoPP::SHA1>;
inline CryptoEcdsa::PrivateKey MakePrivateKey(const uint8_t* key) { inline CryptoEcdsa::PrivateKey MakePrivateKey(const uint8_t* key) {
CryptoPP::DL_GroupParameters_EC<CryptoPP::EC2N> parameters(CryptoPP::ASN1::sect233r1()); CryptoPP::DL_GroupParameters_EC<CryptoPP::EC2N> parameters(CryptoPP::ASN1::sect233r1());
const CryptoPP::Integer exponent(key, 30); // Wii keys need not be canonical scalars; reduction preserves their public key.
if (exponent <= CryptoPP::Integer::Zero() || exponent >= parameters.GetSubgroupOrder()) { const CryptoPP::Integer exponent = CryptoPP::Integer(key, 30) % parameters.GetSubgroupOrder();
if (exponent == CryptoPP::Integer::Zero()) {
throw std::invalid_argument("Wii ES private key is outside the sect233r1 subgroup"); throw std::invalid_argument("Wii ES private key is outside the sect233r1 subgroup");
} }
+2 -1
View File
@@ -5,6 +5,7 @@
#include <imgui.h> #include <imgui.h>
#include <SDL3/SDL_gamepad.h> #include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_joystick.h> #include <SDL3/SDL_joystick.h>
#include <SDL3/SDL_platform.h>
#include <algorithm> #include <algorithm>
#include <array> #include <array>
@@ -146,7 +147,7 @@ std::string BuildMappingString() {
mapping += std::string(kSteps[i].mappingKey) + ":" + *g_wizard.bindings[i] + ","; mapping += std::string(kSteps[i].mappingKey) + ":" + *g_wizard.bindings[i] + ",";
} }
} }
mapping += "platform:Windows,"; mapping += std::string("platform:") + SDL_GetPlatform() + ",";
return mapping; return mapping;
} }
+45 -7
View File
@@ -2,6 +2,7 @@
#include "memory.h" #include "memory.h"
#include <algorithm> #include <algorithm>
#include <atomic>
#include "aurora_events.h" #include "aurora_events.h"
#include "hle/gx/gx_dynamic_aspect.h" #include "hle/gx/gx_dynamic_aspect.h"
@@ -14,6 +15,10 @@ extern "C" int g_gxFrameCount;
namespace { namespace {
bool g_widescreenConfigured = false; bool g_widescreenConfigured = false;
bool g_widescreenSetting = false;
bool g_forceAspect169 = false;
std::atomic_bool g_requestedForceAspect169{false};
bool g_policyDirty = false;
uint32_t g_lastEggWidth43 = 0; uint32_t g_lastEggWidth43 = 0;
uint32_t g_lastEggWidth169 = 0; uint32_t g_lastEggWidth169 = 0;
@@ -167,21 +172,54 @@ void AssertMkwOffscreenScreenBypass() {
} }
void UpdateMkwDynamicAspectSurface(uint32_t surfaceWidth, uint32_t surfaceHeight) { void UpdateMkwDynamicAspectSurface(uint32_t surfaceWidth, uint32_t surfaceHeight) {
if (!g_widescreenConfigured || surfaceWidth == 0 || surfaceHeight == 0) { const bool requestedForceAspect169 = g_requestedForceAspect169.load(std::memory_order_acquire);
if (g_forceAspect169 != requestedForceAspect169) {
g_forceAspect169 = requestedForceAspect169;
g_widescreenConfigured = g_widescreenSetting || g_forceAspect169;
g_dynamicAspectRatioEnabled = g_widescreenConfigured;
g_policyDirty = true;
}
if (surfaceWidth == 0 || surfaceHeight == 0) {
return; return;
} }
AuroraSetViewportPolicy(AURORA_VIEWPORT_STRETCH); if (!g_widescreenConfigured) {
if (g_policyDirty) {
AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT);
VILockAspectRatio(4, 3);
ApplyEggScreenRecords(surfaceWidth, surfaceHeight); ApplyEggScreenRecords(surfaceWidth, surfaceHeight);
g_policyDirty = false;
}
return;
}
if (g_policyDirty) {
VIUnlockAspectRatio();
g_policyDirty = false;
}
AuroraSetViewportPolicy(g_forceAspect169 ? AURORA_VIEWPORT_16_9 : AURORA_VIEWPORT_STRETCH);
ApplyEggScreenRecords(g_forceAspect169 ? 16u : surfaceWidth,
g_forceAspect169 ? 9u : surfaceHeight);
} }
void ConfigureMkwDynamicAspect(bool widescreen, uint32_t surfaceWidth, uint32_t surfaceHeight) { void SetMkwForceAspect169(bool enabled) {
g_widescreenConfigured = widescreen; g_requestedForceAspect169.store(enabled, std::memory_order_release);
g_dynamicAspectRatioEnabled = widescreen; }
bool MkwForceAspect169Requested() {
return g_requestedForceAspect169.load(std::memory_order_acquire);
}
void ConfigureMkwDynamicAspect(bool widescreen, bool forceAspect169, uint32_t surfaceWidth, uint32_t surfaceHeight) {
g_widescreenSetting = widescreen;
g_widescreenConfigured = widescreen || forceAspect169;
g_forceAspect169 = forceAspect169;
g_requestedForceAspect169.store(forceAspect169, std::memory_order_relaxed);
g_dynamicAspectRatioEnabled = g_widescreenConfigured;
g_lastEggWidth43 = 0; g_lastEggWidth43 = 0;
g_lastEggWidth169 = 0; g_lastEggWidth169 = 0;
if (widescreen) { if (g_widescreenConfigured) {
VIUnlockAspectRatio(); VIUnlockAspectRatio();
ApplyEggScreenRecords(surfaceWidth, surfaceHeight); ApplyEggScreenRecords(forceAspect169 ? 16u : surfaceWidth,
forceAspect169 ? 9u : surfaceHeight);
return; return;
} }
+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
+21 -2
View File
@@ -1,4 +1,5 @@
#include "hle_stubs.h" #include "hle_stubs.h"
#include "input_bindings.h"
#include "memory.h" #include "memory.h"
#include "wii_remote_input.h" #include "wii_remote_input.h"
@@ -203,6 +204,24 @@ void WriteUnifiedStatus(uint32_t addr, const WiiRemoteInput::KpadSample* sample)
} }
} }
// Neutral sticks and no buttons while an overlay owns input; the remote stays connected.
bool ReadSample(uint32_t chan, WiiRemoteInput::KpadSample& sample) {
if (!WiiRemoteInput::ReadKpadSample(chan, sample)) {
return false;
}
if (InputBindings::InputBlocked()) {
sample.hold = 0;
sample.clHold = 0;
sample.stick[0] = sample.stick[1] = 0.0f;
sample.clLStick[0] = sample.clLStick[1] = 0.0f;
sample.clRStick[0] = sample.clRStick[1] = 0.0f;
sample.clLStickRaw[0] = sample.clLStickRaw[1] = 0;
sample.clRStickRaw[0] = sample.clRStickRaw[1] = 0;
sample.clTriggerL = sample.clTriggerR = 0;
}
return true;
}
} // namespace } // namespace
// KPADRead: fills KPADStatus[0] for `chan` from the Bluetooth remote, returns the entry count. // KPADRead: fills KPADStatus[0] for `chan` from the Bluetooth remote, returns the entry count.
@@ -212,7 +231,7 @@ extern "C" int32_t KPAD__Read_HLE(uint32_t chan, uint32_t statusPtr, uint32_t co
return 0; return 0;
} }
WiiRemoteInput::KpadSample sample; WiiRemoteInput::KpadSample sample;
const bool have = WiiRemoteInput::ReadKpadSample(chan, sample); const bool have = ReadSample(chan, sample);
try { try {
return WriteStatus(chan, statusPtr, have ? &sample : nullptr); return WriteStatus(chan, statusPtr, have ? &sample : nullptr);
} catch (const Memory::AccessViolation&) { } catch (const Memory::AccessViolation&) {
@@ -234,7 +253,7 @@ extern "C" int32_t KPAD__GetUnifiedWpadStatus_HLE(uint32_t chan, uint32_t status
return 0; return 0;
} }
WiiRemoteInput::KpadSample sample; WiiRemoteInput::KpadSample sample;
const bool have = WiiRemoteInput::ReadKpadSample(chan, sample); const bool have = ReadSample(chan, sample);
try { try {
const uint32_t entries = std::min(count, kMaxEntries); const uint32_t entries = std::min(count, kMaxEntries);
for (uint32_t i = 0; i < entries; ++i) { for (uint32_t i = 0; i < entries; ++i) {
+2 -1
View File
@@ -467,7 +467,8 @@ int32_t HandleIpTopIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const s
const int ret = sendto(s->native, reinterpret_cast<const char*>(sendData), static_cast<int>(sendSize), const int ret = sendto(s->native, reinterpret_cast<const char*>(sendData), static_cast<int>(sendSize),
static_cast<int>(flags), destPtr, destLen); static_cast<int>(flags), destPtr, destLen);
const int hostError = ret < 0 ? NativeLastError() : 0; const int hostError = ret < 0 ? NativeLastError() : 0;
int32_t result = SocketResult(ret); // Diagnostics may change the native error; use the send result captured above.
int32_t result = ret >= 0 ? SocketResult(ret) : SocketErrorResult(hostError);
if (patchedWrite && ret == static_cast<int>(sendSize)) { if (patchedWrite && ret == static_cast<int>(sendSize)) {
result = static_cast<int32_t>(in[0].size); result = static_cast<int32_t>(in[0].size);
} }
+2 -1
View File
@@ -9,6 +9,7 @@
#include <cstring> #include <cstring>
#include "memory.h" #include "memory.h"
#include "runtime_config.h" #include "runtime_config.h"
#include "aurora_events.h"
#include "runtime_log.h" #include "runtime_log.h"
namespace { namespace {
@@ -52,7 +53,7 @@ PPC_NATIVE_OVERRIDE(801B0220, SCCheckStatus_HLE, uint32_t, (), ());
// Returns: 0 = 4:3, 1 = 16:9 // Returns: 0 = 4:3, 1 = 16:9
extern "C" uint32_t SCGetAspectRatio_HLE() extern "C" uint32_t SCGetAspectRatio_HLE()
{ {
return RuntimeConfigFile::WidescreenEnabled(true) ? 1u : 0u; return (RuntimeConfigFile::WidescreenEnabled(true) || MkwForceAspect169Requested()) ? 1u : 0u;
} }
PPC_NATIVE_OVERRIDE(801B1BE4, SCGetAspectRatio_HLE, uint32_t, (), ()); PPC_NATIVE_OVERRIDE(801B1BE4, SCGetAspectRatio_HLE, uint32_t, (), ());
+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 {
+19 -7
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
@@ -1354,7 +1363,8 @@ int RuntimeMain(int argc, char** argv) {
auroraConfig.logCallback = &RuntimeAuroraLogCallback; auroraConfig.logCallback = &RuntimeAuroraLogCallback;
auroraConfig.logLevel = LOG_DEBUG; auroraConfig.logLevel = LOG_DEBUG;
const bool configWidescreen = RuntimeConfigFile::WidescreenEnabled(true); const bool configWidescreen = RuntimeConfigFile::WidescreenEnabled(true);
auroraConfig.windowWidth = configWidescreen ? 854 : 640; const bool forceAspect169 = RuntimeConfigFile::ForceAspect169Enabled();
auroraConfig.windowWidth = (configWidescreen || forceAspect169) ? 854 : 640;
auroraConfig.windowHeight = 480; auroraConfig.windowHeight = 480;
auroraConfig.windowWidth = RuntimeConfigFile::WindowWidth(auroraConfig.windowWidth); auroraConfig.windowWidth = RuntimeConfigFile::WindowWidth(auroraConfig.windowWidth);
auroraConfig.windowHeight = RuntimeConfigFile::WindowHeight(auroraConfig.windowHeight); auroraConfig.windowHeight = RuntimeConfigFile::WindowHeight(auroraConfig.windowHeight);
@@ -1372,7 +1382,8 @@ int RuntimeMain(int argc, char** argv) {
// No vsync knob: aurora always configures a non-blocking present mode. // No vsync knob: aurora always configures a non-blocking present mode.
auroraConfig.desiredBackend = BACKEND_AUTO; auroraConfig.desiredBackend = BACKEND_AUTO;
const float resolutionMultiplier = RuntimeConfigFile::ResolutionMultiplier(1.0f); const float resolutionMultiplier = RuntimeConfigFile::ResolutionMultiplier(1.0f);
ConfigureMkwDynamicAspect(configWidescreen, auroraConfig.windowWidth, auroraConfig.windowHeight); ConfigureMkwDynamicAspect(configWidescreen, forceAspect169,
auroraConfig.windowWidth, auroraConfig.windowHeight);
VISetFrameBufferScale(resolutionMultiplier); VISetFrameBufferScale(resolutionMultiplier);
// One table for both directions. RuntimeConfigFile::IsSupportedGraphicsApi // One table for both directions. RuntimeConfigFile::IsSupportedGraphicsApi
// whitelists exactly these config names, so an unrecognised value has // whitelists exactly these config names, so an unrecognised value has
@@ -1439,14 +1450,15 @@ int RuntimeMain(int argc, char** argv) {
auroraInfo.windowSize.native_fb_height); auroraInfo.windowSize.native_fb_height);
settings_overlay::InitializeRuntimeSettings(); settings_overlay::InitializeRuntimeSettings();
RT_LOG(RT_TAG_CONFIG) << "video.widescreen=" << (configWidescreen ? "true" : "false") RT_LOG(RT_TAG_CONFIG) << "video.widescreen=" << (configWidescreen ? "true" : "false")
<< " SCGetAspectRatio=" << (configWidescreen ? 1 : 0) << " force_16_9=" << (forceAspect169 ? "true" : "false")
<< " SCGetAspectRatio=" << (configWidescreen || forceAspect169 ? 1 : 0)
<< " resolutionMultiplier=" << resolutionMultiplier << " resolutionMultiplier=" << resolutionMultiplier
<< " window=" << auroraInfo.windowSize.width << "x" << auroraInfo.windowSize.height << " window=" << auroraInfo.windowSize.width << "x" << auroraInfo.windowSize.height
<< " native=" << auroraInfo.windowSize.native_fb_width << "x" << " native=" << auroraInfo.windowSize.native_fb_width << "x"
<< auroraInfo.windowSize.native_fb_height << auroraInfo.windowSize.native_fb_height
<< " viewportPolicy=" << (g_dynamicAspectRatioEnabled ? "stretch" : "fit") << " viewportPolicy=" << (forceAspect169 ? "16:9" : (configWidescreen ? "stretch" : "fit"))
<< " presentAspect=" << " presentAspect="
<< (g_dynamicAspectRatioEnabled ? "surface (dynamic EGG canvas)" : "4:3") << (forceAspect169 ? "16:9" : (configWidescreen ? "surface (dynamic EGG canvas)" : "4:3"))
<< std::endl; << std::endl;
g_auroraInitialized.store(true, std::memory_order_release); g_auroraInitialized.store(true, std::memory_order_release);
+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);
+91 -2
View File
@@ -26,6 +26,7 @@
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <cstdio>
#include <limits> #include <limits>
#include <iostream> #include <iostream>
#include <string> #include <string>
@@ -109,6 +110,10 @@ int g_displayMode = [] {
bool g_skipUnreadyPipelines = RuntimeConfigFile::SkipUnreadyPipelines(true); 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();
#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{};
@@ -161,6 +166,11 @@ uint64_t g_presentedFrame = 0;
std::atomic_bool g_strapInputAccepted = false; std::atomic_bool g_strapInputAccepted = false;
std::atomic_uint64_t g_startupDismissFrame = UINT64_MAX; std::atomic_uint64_t g_startupDismissFrame = UINT64_MAX;
constexpr uint64_t kStrapTransitionCoverFrames = 60; constexpr uint64_t kStrapTransitionCoverFrames = 60;
std::atomic_bool g_bootShadersReady = false;
bool g_bootShaderNotice = false;
Clock::time_point g_bootShaderWaitStart{};
constexpr uint32_t kBootShaderNoticeThreshold = 100;
constexpr auto kBootShaderWaitLimit = std::chrono::minutes(3);
constexpr std::array<ResolutionItem, 8> kResolutions = {{ constexpr std::array<ResolutionItem, 8> kResolutions = {{
{"Auto (window size)", 0.0f}, {"Native (1x)", 1.0f}, {"1.5x", 1.5f}, {"2x", 2.0f}, {"Auto (window size)", 0.0f}, {"Native (1x)", 1.0f}, {"1.5x", 1.5f}, {"2x", 2.0f},
@@ -991,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.");
} }
@@ -1006,6 +1027,12 @@ void DrawAudioSettings() {
void DrawGraphicsSettings() { void DrawGraphicsSettings() {
g_displayMode = static_cast<int>(aurora_get_display_mode()); g_displayMode = static_cast<int>(aurora_get_display_mode());
if (ImGui::Checkbox("Force 16:9", &g_forceAspect169)) {
SetMkwForceAspect169(g_forceAspect169);
RuntimeConfigFile::SetForceAspect169(g_forceAspect169);
}
ImGui::TextDisabled("Keep a 16:9 image with black bars when the window has another shape.");
ImGui::Separator();
struct EffectFlag { struct EffectFlag {
const char* label; const char* label;
uint32_t flag; uint32_t flag;
@@ -1080,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);
@@ -1186,12 +1243,36 @@ void DrawStartupScreen() {
const float startY = std::max(0.0f, (viewport->Size.y - titleSize.y) * 0.5f); const float startY = std::max(0.0f, (viewport->Size.y - titleSize.y) * 0.5f);
ImGui::SetCursorPos(ImVec2(titleX, startY)); ImGui::SetCursorPos(ImVec2(titleX, startY));
ImGui::TextUnformatted(kTitle); ImGui::TextUnformatted(kTitle);
if (g_bootShaderNotice && !g_bootShadersReady.load(std::memory_order_relaxed)) {
ImGui::SetWindowFontScale(0.9f);
char line[96];
std::snprintf(line, sizeof(line), "Compiling shaders, please hold on: %u remaining",
aurora_get_queued_pipeline_count());
const float lineX = std::max(0.0f, (viewport->Size.x - ImGui::CalcTextSize(line).x) * 0.5f);
ImGui::SetCursorPos(ImVec2(lineX, startY + titleSize.y * 1.8f));
ImGui::TextUnformatted(line);
}
} }
ImGui::End(); ImGui::End();
ImGui::PopStyleVar(); ImGui::PopStyleVar();
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }
void UpdateBootShaderState() {
if (g_bootShadersReady.load(std::memory_order_relaxed)) {
return;
}
const auto now = Clock::now();
if (g_bootShaderWaitStart == Clock::time_point{}) {
g_bootShaderWaitStart = now;
}
const uint32_t queued = aurora_get_queued_pipeline_count();
g_bootShaderNotice |= queued > kBootShaderNoticeThreshold;
if (queued == 0 || now - g_bootShaderWaitStart > kBootShaderWaitLimit) {
g_bootShadersReady.store(true, std::memory_order_release);
}
}
void DrawExitPrompt() { void DrawExitPrompt() {
constexpr const char* kTitle = "Exit"; constexpr const char* kTitle = "Exit";
if (g_exitPromptOpen && !ImGui::IsPopupOpen(kTitle)) ImGui::OpenPopup(kTitle); if (g_exitPromptOpen && !ImGui::IsPopupOpen(kTitle)) ImGui::OpenPopup(kTitle);
@@ -1341,7 +1422,7 @@ void PersistDisplayModeIfChanged() {
void ApplyInputBlockState() { void ApplyInputBlockState() {
const bool blocked = controller_mapping_wizard::IsActive() || g_rebind.active || const bool blocked = controller_mapping_wizard::IsActive() || g_rebind.active ||
g_exitPromptOpen || g_topBarVisible; g_exitPromptOpen || g_topBarVisible || StartupScreenVisible();
PADBlockInput(blocked); PADBlockInput(blocked);
InputBindings::SetInputBlocked(blocked); InputBindings::SetInputBlocked(blocked);
} }
@@ -1360,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);
@@ -1369,6 +1453,9 @@ void InitializeRuntimeSettings() noexcept {
aurora_set_skip_unready_pipelines(g_skipUnreadyPipelines); aurora_set_skip_unready_pipelines(g_skipUnreadyPipelines);
g_strapInputAccepted.store(false, std::memory_order_relaxed); g_strapInputAccepted.store(false, std::memory_order_relaxed);
g_startupDismissFrame.store(UINT64_MAX, std::memory_order_relaxed); g_startupDismissFrame.store(UINT64_MAX, std::memory_order_relaxed);
g_bootShadersReady.store(false, std::memory_order_relaxed);
g_bootShaderNotice = false;
g_bootShaderWaitStart = {};
PADBlockInput(false); PADBlockInput(false);
InputBindings::SetInputBlocked(false); InputBindings::SetInputBlocked(false);
} }
@@ -1449,6 +1536,7 @@ void Draw() noexcept {
ApplyConfiguredMappings(); ApplyConfiguredMappings();
PersistDisplayModeIfChanged(); PersistDisplayModeIfChanged();
UpdateCursorAutoHide(); UpdateCursorAutoHide();
UpdateBootShaderState();
if (!StartupScreenVisible()) { if (!StartupScreenVisible()) {
DrawShaderCompilationStatus(); DrawShaderCompilationStatus();
} }
@@ -1462,6 +1550,7 @@ void Draw() noexcept {
bool StartupScreenVisible() noexcept { bool StartupScreenVisible() noexcept {
return !g_strapInputAccepted.load(std::memory_order_acquire) || return !g_strapInputAccepted.load(std::memory_order_acquire) ||
!g_bootShadersReady.load(std::memory_order_acquire) ||
g_presentedFrame < g_startupDismissFrame.load(std::memory_order_relaxed); g_presentedFrame < g_startupDismissFrame.load(std::memory_order_relaxed);
} }
@@ -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 -1
View File
@@ -36,7 +36,7 @@ See `projects/examples/generic-dol.yml` for a minimal manifest driven by `RECOMP
| Tool | Notes | | Tool | Notes |
| --- | --- | | --- | --- |
| .NET 8 SDK | Builds and runs the translator. | | .NET 8 SDK | Builds and runs the translator. |
| CMake ≥ 3.16 and Ninja | Configures and drives the native build. | | CMake ≥ 3.25 and Ninja | Configures and drives the native build. |
| Clang / LLVM | The shipped build uses LLVM-MinGW targeting `x86-64-v3`. MSVC is not the tested path. | | Clang / LLVM | The shipped build uses LLVM-MinGW targeting `x86-64-v3`. MSVC is not the tested path. |
Build the CLI once and invoke the assembly directly: Build the CLI once and invoke the assembly directly:
@@ -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
@@ -1942,6 +1942,22 @@ public sealed partial class PpcLifter
new IrCall(string.Empty, TargetLabel(ins, validAddresses, preferFallthrough: false), blArgs) new IrCall(string.Empty, TargetLabel(ins, validAddresses, preferFallthrough: false), blArgs)
}; };
case "bltl":
{
var crField = "cr0";
if (ops.Count > 0 && ops[0] is PpcConditionRegisterOperand crOp)
{
crField = NormalizeRegister(crOp.Name);
}
return new IrInstruction[]
{
new IrAssign("lr", IrValue.Imm((int)ins.EndAddress)),
new IrBranch("blt", TargetLabel(ins, validAddresses, preferFallthrough: false),
$"0x{ins.EndAddress:X8}", crField)
};
}
case "bcl": case "bcl":
{ {
var rawInstr = ReadRawInstruction(ins); var rawInstr = ReadRawInstruction(ins);
@@ -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);
} }
}
@@ -14,6 +14,23 @@ public class PpcLifterAdditionalCoverageTests
private static PpcInstruction Instruction(uint raw, string mnemonic, params PpcOperand[] operands) private static PpcInstruction Instruction(uint raw, string mnemonic, params PpcOperand[] operands)
=> PpcInstruction.Synthetic(0x80000000, raw, mnemonic, operands); => PpcInstruction.Synthetic(0x80000000, raw, mnemonic, operands);
[Fact]
public void LinkedConditionalBranchSetsLrBeforeEitherPath()
{
var branch = PpcDecoder.Decode(0x80004394, 0x41800029);
var ir = Assert.Single(new PpcLifter().Lift(new[] { branch })).Ir;
Assert.Equal("bltl", branch.Mnemonic);
var lr = Assert.IsType<IrAssign>(ir[0]);
Assert.Equal("lr", lr.Destination);
Assert.Equal(unchecked((int)0x80004398u), lr.Value.Constant);
var decision = Assert.IsType<IrBranch>(ir[1]);
Assert.Equal("blt", decision.Condition);
Assert.Equal("0x800043BC", decision.TrueLabel);
Assert.Equal("0x80004398", decision.FalseLabel);
}
[Fact] [Fact]
public void LiftsAdditionalNonDotArithmeticAndLogicalForms() public void LiftsAdditionalNonDotArithmeticAndLogicalForms()
{ {
@@ -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