mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-10-04 16:37:30 -04:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 279ce8328f | |||
| 9d182f8316 | |||
| 75886669bc | |||
| a50bad2970 | |||
| 9b7b9913e4 | |||
| a88b7b502b | |||
| d0d58072d1 | |||
| 6b853ca371 | |||
| 77a8623416 | |||
| 10ab54adee | |||
| a05c89739d | |||
| 82991ec337 | |||
| e164af9ff4 | |||
| e409d9f99b | |||
| 85f250155f | |||
| a9c9c0de26 | |||
| c92e45c2e7 | |||
| 583e702547 |
@@ -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
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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'
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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 - {} +
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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/> |
|
||||||
|
|||||||
@@ -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 ()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Vendored
+63
-13
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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(); }
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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, ©Filter, forceOpaqueAlpha,
|
&sourceRect.sampleRect, g_gxState.texCopyHalfScale, ©Filter, forceOpaqueAlpha,
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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.
|
||||||
@@ -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 = ⌖
|
||||||
|
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(©Source, &destination, &extent);
|
||||||
|
auto copy = encoder.Finish();
|
||||||
|
dawnQueue.Submit(1, ©);
|
||||||
|
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
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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*;
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, (), ());
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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)");
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user