Compare commits

..

1 Commits

Author SHA1 Message Date
patchzyy f89df45039 Add early Linux linker dependency probe 2026-09-24 18:05:48 +02:00
175 changed files with 3283 additions and 8872 deletions
-64
View File
@@ -46,67 +46,3 @@ jobs:
- name: Test
run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal
platform_tests:
name: Host platform (${{ matrix.runner }})
strategy:
fail-fast: false
matrix:
runner: [ubuntu-24.04, macos-14, macos-15-intel]
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Configure without graphics dependencies or game assets
run: >-
cmake -S runtime -B build-platform -G Ninja
-DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
-DMKW_BUILD_PRODUCTS=OFF -DMKW_PLATFORM_TESTS_ONLY=ON
- name: Build and run host-platform tests
run: |
cmake --build build-platform --parallel 3
ctest --test-dir build-platform --output-on-failure
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
+10 -138
View File
@@ -1,8 +1,8 @@
name: Package installers
# Builds the per-platform installer/setup tool (WiiCompiled-Setup.exe /
# WiiCompiled-Setup-x86_64.AppImage / WiiCompiled-Setup.pkg) via the platform packaging scripts -
# the same scripts a maintainer runs by hand today to
# WiiCompiled-Setup-x86_64.AppImage) via Launcher/Build-Installer.ps1 and
# Launcher/build-appimage.sh respectively - the same scripts a maintainer runs by hand today to
# 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),
# which is proprietary and not present in this repository or in CI.
@@ -11,11 +11,6 @@ on:
tags:
- '*'
workflow_dispatch:
inputs:
version:
description: Package version
required: true
type: string
permissions:
contents: read
@@ -95,139 +90,14 @@ jobs:
if-no-files-found: error
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:
PACKAGE_VERSION: ${{ inputs.version }}
TAG_VERSION: ${{ github.ref_name }}
shell: bash
run: |
package_version="$PACKAGE_VERSION"
if [[ -z "$package_version" ]]; then
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
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
# 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 the expected platform assets,
# produced by setup hosts that report that same version.
# (non-prerelease) release whose tag is v<semver>, carrying an asset named exactly
# WiiCompiled-Setup.exe, produced by a setup host that reports that same version.
release:
name: Publish GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
needs: [linux-appimage, windows-installer, macos-setup-package, recompilation]
needs: [linux-appimage, windows-installer, recompilation]
runs-on: ubuntu-latest
permissions:
contents: write
@@ -237,7 +107,7 @@ jobs:
persist-credentials: false
# 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 the pinned
# version differs from the release tag, so a tag that was pushed without bumping every pinned
# version would ship an update nobody can install. Catch that before anything is published.
- name: Verify the tag matches the pinned setup version
env:
@@ -252,7 +122,9 @@ jobs:
status=1
fi
}
check Launcher/Directory.Build.props "<Version>$version</Version>"
check Launcher/WiiCompiled.Setup.Windows/Program.cs "public const string Version = \"$version\";"
check Launcher/WiiCompiled.Setup.Windows/WiiCompiled.Setup.Windows.csproj "<Version>$version</Version>"
check Launcher/Build-Installer.ps1 "ProductVersion = '$version'"
exit $status
# The build jobs upload with `archive: false`, which stores each installer as a raw file
@@ -271,7 +143,7 @@ jobs:
set -euo pipefail
ls -lR artifacts
assets=()
for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage WiiCompiled-Setup.pkg; do
for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage; do
found="$(find artifacts -type f -name "$name" | head -n 1)"
[ -n "$found" ] && [ -s "$found" ] || { echo "::error::missing release asset $name"; exit 1; }
assets+=("$found")
-14
View File
@@ -45,20 +45,6 @@ jobs:
shell: pwsh
run: ./Launcher/Prepare-PortableTools.ps1
- name: Build and run Windows host-platform tests
shell: pwsh
run: |
$tools = Join-Path $PWD 'Launcher/artifacts/portable-tools'
$env:PATH = "$tools/llvm-mingw/bin;$tools/CMake/bin;$tools/Ninja;$env:PATH"
cmake -S runtime -B build-platform -G Ninja `
-DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ `
-DMKW_BUILD_PRODUCTS=OFF -DMKW_PLATFORM_TESTS_ONLY=ON
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
cmake --build build-platform --parallel 3
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
ctest --test-dir build-platform --output-on-failure
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Prepare pinned native dependencies
shell: pwsh
run: ./Launcher/Prepare-Dependencies.ps1
-1
View File
@@ -1,5 +1,4 @@
/generated/
/runtime/native-index.json
/Assets/
/assets/
/PulsarPacks/
+3 -40
View File
@@ -112,20 +112,6 @@ Assert-Directory $dependencySources 'Pinned offline dependency sources'
# back to compiling aurora from source, which is what this package exists to
# avoid - and a mismatched one would be an ABI hazard.
$portableClangHash = Get-MkwFileSha256 (Join-Path $portableTools 'llvm-mingw\bin\clang-22.exe')
# The identity the runtime build compares against the package's runtime archive.
function Get-RuntimeSourceFingerprint([string]$RuntimeDirectory, [string]$DependenciesDirectory) {
$arguments = @("-DMKW_RUNTIME_DIR=$(ConvertTo-MkwCMakePath $RuntimeDirectory)")
$cppWinRt = Join-Path $DependenciesDirectory 'cppwinrt'
if (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType Leaf) {
$arguments += "-DMKW_CPPWINRT_INCLUDE_DIR=$(ConvertTo-MkwCMakePath $cppWinRt)"
}
$arguments += @('-P', (ConvertTo-MkwCMakePath (Join-Path $RuntimeDirectory 'cmake\RuntimeFingerprint.cmake')))
$output = @(& (Join-Path $portableTools 'CMake\bin\cmake.exe') @arguments)
if ($LASTEXITCODE -ne 0) { throw "Runtime fingerprinting failed with exit code $LASTEXITCODE." }
$line = $output | Where-Object { "$_" -match '^runtime_fingerprint=[0-9a-f]{64}$' } | Select-Object -First 1
if (-not $line) { throw "RuntimeFingerprint.cmake printed no fingerprint: $($output -join ' ')" }
return "$line".Substring('runtime_fingerprint='.Length)
}
function Test-NativePrebuiltCurrent([string]$PackageDirectory) {
$provenancePath = Join-Path $PackageDirectory 'provenance.json'
if (-not (Test-Path -LiteralPath $provenancePath -PathType Leaf)) { return $false }
@@ -143,9 +129,6 @@ function Test-NativePrebuiltCurrent([string]$PackageDirectory) {
# release ships runtime/third_party sources that first-party TUs compile
# against, so archives harvested before a vendored change are stale.
$recordedThirdParty = $provenance.PSObject.Properties['ThirdPartySourceFingerprint']
# The precompiled runtime archive must be from these runtime sources; a package without one
# would make every installation compile the runtime itself.
$recordedRuntime = $provenance.PSObject.Properties['RuntimeSourceFingerprint']
return ($provenance.CompilerSha256 -eq $portableClangHash) -and
($provenance.FlagFingerprint -eq (Get-MkwNativeFlagFingerprint)) -and
($null -ne $recordedDawn) -and
@@ -153,13 +136,11 @@ function Test-NativePrebuiltCurrent([string]$PackageDirectory) {
($null -ne $recordedAurora) -and
($recordedAurora.Value -eq (Get-MkwAuroraSourceFingerprint (Join-Path $repoRoot 'aurora-main'))) -and
($null -ne $recordedThirdParty) -and
($recordedThirdParty.Value -eq (Get-MkwThirdPartySourceFingerprint (Join-Path $repoRoot 'runtime\third_party'))) -and
($null -ne $recordedRuntime) -and
($recordedRuntime.Value -eq (Get-RuntimeSourceFingerprint (Join-Path $repoRoot 'runtime') $dependencySources))
($recordedThirdParty.Value -eq (Get-MkwThirdPartySourceFingerprint (Join-Path $repoRoot 'runtime\third_party')))
}
$nativePrebuiltPackage = Join-Path $dependencySources 'native_prebuilt'
if (-not (Test-NativePrebuiltCurrent $nativePrebuiltPackage)) {
Write-Host '[0/6] Building the precompiled aurora, third-party and runtime package...'
Write-Host '[0/6] Building the precompiled aurora and third-party package...'
& (Join-Path $PSScriptRoot 'Prepare-NativePrebuilt.ps1') `
-PortableToolsDirectory $portableTools -DependencySourceDirectory $dependencySources
if ($LASTEXITCODE -ne 0) { throw 'Precompiled aurora package preparation failed.' }
@@ -241,21 +222,6 @@ Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1') -Destina
[IO.Directory]::CreateDirectory((Join-Path $workspace 'Dependencies')) | Out-Null
foreach ($name in $requiredDependencies) { Copy-Directory (Join-Path $dependencySources $name) (Join-Path $workspace "Dependencies\$name") }
# The translation identity covers the runtime's native registrations through this index (see
# ToolkitFingerprint), so it is extracted from exactly the staged runtime sources.
$nativeIndex = Join-Path $workspace 'runtime\native-index.json'
& $translator emit-native-index --project (Join-Path $workspace 'projects\mkwii\recomp.yml') --out $nativeIndex
if ($LASTEXITCODE -ne 0) { throw "Native index extraction failed with exit code $LASTEXITCODE." }
Assert-File $nativeIndex 'Runtime native index'
# A staged runtime that fingerprints differently from the shipped archive would make every
# installation silently compile the runtime from source.
$stagedRuntimeFingerprint = Get-RuntimeSourceFingerprint (Join-Path $workspace 'runtime') (Join-Path $workspace 'Dependencies')
$packagedRuntimeFingerprint = (Get-Content -LiteralPath (Join-Path $workspace 'Dependencies\native_prebuilt\provenance.json') -Raw |
ConvertFrom-Json).RuntimeSourceFingerprint
if ($stagedRuntimeFingerprint -ne $packagedRuntimeFingerprint) {
throw "The staged runtime ($stagedRuntimeFingerprint) does not match the precompiled runtime archive ($packagedRuntimeFingerprint)."
}
[IO.Directory]::CreateDirectory((Join-Path $payloadRoot 'host')) | Out-Null
Copy-Item $setupHost (Join-Path $payloadRoot 'host\WiiCompiled-Setup.exe')
@@ -313,12 +279,9 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
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]@{
SchemaVersion = 2
ProductVersion = $productVersion
ProductVersion = '0.2.32'
ExpectedGameId = $pins.GameId
ExpectedDolSha256 = $pins.DolSha256
ExpectedRelSha256 = $pins.RelSha256
-7
View File
@@ -1,7 +0,0 @@
<Project>
<PropertyGroup>
<!-- The one product version. Release tags must match it. -->
<Version>0.2.34</Version>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
</Project>
+3 -12
View File
@@ -103,7 +103,7 @@ function Write-MkwBuildStep([string]$StepId, [string]$Message) {
function Reset-LocalDirectory([string]$Path) {
$full = [IO.Path]::GetFullPath($Path)
$root = [IO.Path]::GetFullPath($Workspace).TrimEnd('\') + '\'
$installRoot = [IO.Path]::GetFullPath((Split-Path -Parent $realWorkspace)).TrimEnd('\') + '\'
$installRoot = [IO.Path]::GetFullPath((Split-Path -Parent $Workspace)).TrimEnd('\') + '\'
# The caller-supplied output destinations are legitimate reset targets by
# definition, wherever the caller placed them: a fresh install's operation
# scratch lives beside the installation directory rather than inside it.
@@ -150,16 +150,8 @@ if ($Profile -eq 'both' -and [string]::IsNullOrWhiteSpace($BaseOutputDirectory))
if ($Profile -ne 'both' -and -not [string]::IsNullOrWhiteSpace($BaseOutputDirectory)) {
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'
$toolchain = Get-MkwBuildSafePath $Toolkit 'toolchain' 'CMake\bin\cmake.exe'
$toolchain = Get-MkwShellSafeToolchainRoot $Toolkit
$cmake = Join-Path $toolchain 'CMake\bin\cmake.exe'
$ninja = Join-Path $toolchain 'Ninja\ninja.exe'
$toolchainBin = Join-Path $toolchain 'llvm-mingw\bin'
@@ -429,8 +421,7 @@ try {
}
$redist = Join-Path $Toolkit 'Redist'
if (Test-Path -LiteralPath $redist -PathType Container) {
Get-ChildItem -LiteralPath $redist -Filter '*.dll' -File |
Copy-Item -Destination $Destination -Force
Copy-Item -Path (Join-Path $redist '*.dll') -Destination $Destination -Force
}
foreach ($name in @('config','wii_bootstrap')) {
$directory = Join-Path $build $name
+16 -31
View File
@@ -40,55 +40,41 @@ function Get-MkwToolchainPath([string]$ToolchainRoot) {
) -join ';')
}
function Get-MkwBuildSafePath([string]$Path, [string]$Kind, [string]$MarkerFile) {
<#
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)
function Get-MkwShellSafeToolchainRoot([string]$ToolchainRoot) {
if ([string]::IsNullOrWhiteSpace($ToolchainRoot)) { throw 'A toolchain root is required.' }
$full = [IO.Path]::GetFullPath($ToolchainRoot)
# 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 -cmatch '^[A-Za-z0-9 ._\\:-]+$') { return $full }
Assert-File (Join-Path $full $MarkerFile) "$Kind marker"
if ($full -notmatch '[()&^%!]') { return $full }
$sha = [Security.Cryptography.SHA256]::Create()
try {
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($full.ToLowerInvariant()))
} finally { $sha.Dispose() }
$linkName = "$Kind-" + ((($bytes[0..7]) | ForEach-Object { $_.ToString('x2') }) -join '')
$linkName = 'toolchain-' + ((($bytes[0..7]) | ForEach-Object { $_.ToString('x2') }) -join '')
$failures = @()
foreach ($base in @($env:ProgramData, $env:PUBLIC)) {
if ([string]::IsNullOrWhiteSpace($base) -or $base -cnotmatch '^[A-Za-z0-9._\\:-]+$') { continue }
if ([string]::IsNullOrWhiteSpace($base) -or $base -match '[()&^%! ]') { continue }
$link = Join-Path (Join-Path $base 'WiiCompiled') $linkName
try {
[IO.Directory]::CreateDirectory((Split-Path -Parent $link)) | Out-Null
$existing = Get-Item -LiteralPath $link -Force -ErrorAction SilentlyContinue
if ($null -ne $existing) {
# Never trust a directory just because it has the marker: it could point at a
# different installation. Nor may we remove a directory we did not create.
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 {
# The name already identifies the target, so an existing junction that still resolves is
# this one; only a broken leftover is replaced. Directory.Delete removes the reparse
# point itself, where Remove-Item -Recurse would delete the toolchain it points at.
if (-not (Test-Path -LiteralPath (Join-Path $link 'CMake\bin\cmake.exe') -PathType Leaf)) {
if (Test-Path -LiteralPath $link) { [IO.Directory]::Delete($link) }
New-Item -ItemType Junction -Path $link -Target $full -ErrorAction Stop | Out-Null
}
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"
Write-Host "MKWCBUILD: Building through $link, because $full contains characters cmd.exe cannot parse"
return $link
} catch {
$failures += "$link ($($_.Exception.Message))"
}
}
throw ("The $Kind path $full cannot be passed safely to the native build, and no junction " +
'to it could be created: ' + ($failures -join '; ') + '. Install to a path of plain ' +
'letters, digits and spaces, or make a safe junction location available.')
throw ("The toolchain path $full contains a character (one of ( ) & ^ % !) that the compiler " +
'cannot be invoked through, and no junction to it could be created: ' + ($failures -join '; ') +
'. Install to a path without those characters.')
}
function Get-MkwProjectPins([string]$ProjectFile) {
@@ -145,8 +131,7 @@ function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Descri
else { Write-Host "${LogPrefix}: $Description" }
$quotedArguments = @($Arguments | ForEach-Object {
if ($_.Contains('"')) { throw "A native build argument contains an unsupported quote: $_" }
# Backslashes directly before the closing quote would otherwise escape it.
'"' + ($_ -replace '(\\+)$', '$1$1') + '"'
'"' + $_ + '"'
})
if ($WaitForProcessTree) {
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
+3 -3
View File
@@ -82,9 +82,9 @@ $packages = @(
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz' })
},
[pscustomobject]@{
Name = 'xxhash'; File = 'xxHash-0.8.4.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.4.tar.gz' })
Name = 'xxhash'; File = 'xxHash-0.8.3.tar.gz'
Uris = @('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.3.tar.gz' })
},
[pscustomobject]@{
Name = 'zlib'; File = 'zlib-1.3.2.tar.gz'
+11 -34
View File
@@ -1,8 +1,7 @@
# Builds the redistributable precompiled aurora + third-party package: aurora (~43% of local build
# CPU time) and vendored Crypto++ are identical for every user, so this configures runtime/ like
# LocalBuild.ps1 (both dot-source NativeBuildFlags.ps1), builds just that closure plus the
# game-independent runtime archive, and harvests the archives and a generated CMake description
# into launcher/artifacts/dependencies/native_prebuilt.
# LocalBuild.ps1 (both dot-source NativeBuildFlags.ps1), builds just that closure, and harvests the
# archives plus a generated CMake description into launcher/artifacts/dependencies/native_prebuilt.
[CmdletBinding(PositionalBinding = $false)]
param(
[string]$PortableToolsDirectory = 'launcher/artifacts/portable-tools',
@@ -88,29 +87,24 @@ try {
-AdditionalArguments @("-DMKW_NATIVE_PREBUILT_EXPORT_DIR=$(ConvertTo-MkwCMakePath $exportDirectory)")
Invoke-Checked $cmake $configure 'Configuring the aurora/third-party staging build' -LogPrefix 'MKWNP'
$metaPath = Join-Path $exportDirectory 'meta.txt'
Assert-File $metaPath 'Native prebuilt export metadata'
$meta = @{}
foreach ($line in Get-Content -LiteralPath $metaPath) {
if ($line -match '^([a-z0-9_]+)=(.*)$') { $meta[$Matches[1]] = $Matches[2] }
}
# The game-independent runtime ships precompiled too; it is outside the probe closure.
if (-not $meta.ContainsKey('runtime_fingerprint')) {
throw 'The staging configure exported no runtime_fingerprint; the x86_64 package must carry the runtime archive.'
}
# mkw_np_probe links exactly what mkw_runtime_common and the public products
# link, so building it compiles the whole redistributable closure and nothing
# else - and a successful link proves the harvested archives are complete.
Invoke-Checked $cmake @('--build', $stage, '--target', 'mkw_np_probe', '--target', 'mkw_runtime',
'--parallel', "$Parallel") 'Building the aurora/third-party closure and the runtime' -LogPrefix 'MKWNP'
Invoke-Checked $cmake @('--build', $stage, '--target', 'mkw_np_probe', '--parallel', "$Parallel") `
'Building the aurora/third-party closure' -LogPrefix 'MKWNP'
} finally {
$env:PATH = $oldPath
}
# ---------------------------------------------------------------------------
# Read the resolved command lines Ninja holds.
# Read the description CMake wrote, plus the resolved command lines Ninja holds.
# ---------------------------------------------------------------------------
$metaPath = Join-Path $exportDirectory 'meta.txt'
Assert-File $metaPath 'Native prebuilt export metadata'
$meta = @{}
foreach ($line in Get-Content -LiteralPath $metaPath) {
if ($line -match '^([a-z0-9_]+)=(.*)$') { $meta[$Matches[1]] = $Matches[2] }
}
$buildNinja = Join-Path $stage 'build.ninja'
Assert-File $buildNinja 'Generated build.ninja'
@@ -240,12 +234,6 @@ foreach ($name in ($targets.Keys | Sort-Object)) {
}
if ($sharedImports.Count -eq 0) { throw 'No shared libraries were harvested; the SDL/zlib/libpng DLLs would be missing.' }
if ($dawnLinkerFile) { $linkerFileToReference[$dawnLinkerFile] = 'dawn::webgpu_dawn' }
$runtimeTarget = $targets['mkw_runtime']
if ($null -eq $runtimeTarget -or $runtimeTarget.Type -ne 'STATIC_LIBRARY' -or
-not $linkerFileToReference.ContainsKey($runtimeTarget.LinkerFile)) {
throw 'The runtime archive (mkw_runtime) was not built and harvested.'
}
$runtimeArchiveReference = $linkerFileToReference[$runtimeTarget.LinkerFile]
# ---------------------------------------------------------------------------
# Rewrite the compile/link interface into workspace-relative tokens.
@@ -309,9 +297,6 @@ $packageLinkItems = @($linkItems | ForEach-Object {
if (StartsWithPath $absolute $dependencyRoot) { return "`@DEPS`@/$(RelativeTo $absolute $dependencyRoot)" }
throw "Link item is neither a system library nor a harvested archive: $item"
})
if ($packageLinkItems -contains $runtimeArchiveReference) {
throw 'The runtime archive leaked into the aurora link interface.'
}
# fmt's `-include cstdlib` (and anything like it) is recorded from a C++ compile
# line, so re-scope it to C++. The runtime also assembles generated .S blobs
@@ -348,11 +333,6 @@ $generated.Add('')
$generated.Add("set(MKW_NP_DAWN_CONFIG_DIR `"$(ConvertTo-Token $meta['dawn_config_dir'] 'dawn')`")")
$generated.Add("set(MKW_NP_DAWN_VERSION `"$($meta['aurora_dawn_version'])`")")
$generated.Add('')
$generated.Add('# The precompiled game-independent runtime; RuntimeLibrary.cmake decides whether it applies.')
$generated.Add("set(MKW_NP_RUNTIME_ARCHIVE `"$runtimeArchiveReference`")")
$generated.Add("set(MKW_NP_RUNTIME_FINGERPRINT `"$($meta['runtime_fingerprint'])`")")
$generated.Add("set(MKW_NP_RUNTIME_CPU_PROFILE `"$($meta['runtime_cpu_profile'])`")")
$generated.Add('')
$generated.Add('# Shared third-party libraries keep imported SHARED targets so that')
$generated.Add('# $<TARGET_RUNTIME_DLLS> still copies them next to the game executable.')
foreach ($import in $sharedImports) {
@@ -413,9 +393,6 @@ $provenance = [ordered]@{
# release build and the local build can refuse a package aurora has outgrown.
AuroraSourceFingerprint = $auroraSourceFingerprint
ThirdPartySourceFingerprint = $thirdPartySourceFingerprint
# runtime/cmake/RuntimeFingerprint.cmake over the runtime sources the archive was built from.
RuntimeSourceFingerprint = $meta['runtime_fingerprint']
RuntimeCpuProfile = $meta['runtime_cpu_profile']
Sdl3Target = $meta['aurora_sdl3_target']
HarvestedLibraryCount = @($linkerFileToReference.Keys).Count
Contents = $contents
+28 -142
View File
@@ -1,10 +1,9 @@
#!/usr/bin/env bash
# Builds the redistributable precompiled aurora + third-party package for native Linux: aurora
# (~43% of local build CPU time per Prepare-NativePrebuilt.ps1), Crypto++ and mbed TLS are identical
# (~43% of local build CPU time per Prepare-NativePrebuilt.ps1) and vendored Crypto++ are identical
# for every user under the pinned toolchain prepare-portable-tools.sh bundles, so this configures
# runtime/ against that toolchain, builds just that closure (plus, on x86_64, the game-independent
# runtime archive), and harvests the archives plus a generated CMake description into an output
# package - the Linux counterpart to
# 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
# Launcher/Prepare-NativePrebuilt.ps1, consumed by the same platform-agnostic
# runtime/cmake/NativePrebuilt.cmake either script's package works with unmodified.
#
@@ -23,17 +22,11 @@ workspace=$(cd "$script_dir/.." && pwd)
arch=""
output_dir=""
toolchain_dir=""
ninja_bin=""
cmake_bin=""
llvm_dir=""
sysroot=""
stage_dir="$workspace/build/native-prebuilt-stage"
keep_stage=0
reuse_stage=0
parallel=0
print_fingerprint_only=0
disconnected=0
usage() {
cat <<'EOF'
@@ -46,7 +39,8 @@ Usage: Prepare-NativePrebuilt.sh --arch {x86_64|aarch64} [options]
--reuse-stage Reuse an existing staging build directory (maintainer iteration aid: a
re-harvest does not recompile aurora from scratch)
--parallel N Ninja build parallelism (default: nproc)
--print-fingerprint-only Print the provenance inputs as "key=value" lines and
--print-fingerprint-only Print the four provenance inputs (compiler_sha256, flag_fingerprint,
aurora_fingerprint, third_party_fingerprint) as "key=value" lines and
exit, without configuring/building/harvesting anything - lets a caller
(build-appimage.sh) decide whether an existing package is still current
without paying for a full aurora rebuild just to find out.
@@ -62,12 +56,6 @@ while [[ $# -gt 0 ]]; do
--reuse-stage) reuse_stage=1; shift ;;
--parallel) parallel=$2; shift 2 ;;
--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 ;;
*) echo "Prepare-NativePrebuilt.sh: unknown argument: $1" >&2; exit 1 ;;
esac
@@ -84,22 +72,14 @@ assert_dir() { [[ -d "$1" ]] || fail "$2 is missing: $1"; }
sha256_of() { sha256sum "$1" | awk '{print $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"
[[ "$stage_dir" = /* ]] || stage_dir="$workspace/$stage_dir"
toolchain_dir="${toolchain_dir:-$script_dir/artifacts/portable-tools/toolchain-$arch}"
cc="${llvm_dir:-$toolchain_dir}/bin/clang"
cxx="${llvm_dir:-$toolchain_dir}/bin/clang++"
cmake_bin="${cmake_bin:-$toolchain_dir/bin/cmake}"
ninja_bin="${ninja_bin:-$toolchain_dir/bin/ninja}"
toolchain_dir="$script_dir/artifacts/portable-tools/toolchain-$arch"
cc="$toolchain_dir/bin/clang"
cxx="$toolchain_dir/bin/clang++"
cmake_bin="$toolchain_dir/bin/cmake"
ninja_bin="$toolchain_dir/bin/ninja"
runtime_source="$workspace/runtime"
aurora_source="$workspace/aurora-main"
@@ -108,7 +88,7 @@ assert_file "$ninja_bin" "Portable Ninja"
assert_file "$cc" "Portable C compiler"
assert_file "$cxx" "Portable C++ compiler"
assert_dir "$aurora_source" "aurora-main source tree"
clang_binary=$(normalize "${llvm_dir:-$toolchain_dir}/bin/clang-22")
clang_binary=$(normalize "$toolchain_dir/bin/clang-22")
assert_file "$clang_binary" "Portable clang driver binary"
(( parallel > 0 )) || parallel=$(nproc)
@@ -151,8 +131,6 @@ fixed_configure_flags=(
-DCMAKE_DISABLE_FIND_PACKAGE_absl=ON
-DCMAKE_DISABLE_FIND_PACKAGE_PNG=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
# 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)
@@ -163,16 +141,6 @@ fixed_configure_flags=(
-DFT_DISABLE_BZIP2=ON
-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}')
# extern/ is excluded because the payload ships that tree separately (aurora-main/extern is bundled
@@ -180,35 +148,19 @@ flag_fingerprint=$(printf '%s\n' "${fixed_configure_flags[@]}" | sha256sum | awk
aurora_fingerprint=$(fingerprint_tree "$aurora_source" extern build)
[[ -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")
[[ -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 "$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
# The game-independent runtime ships precompiled on x86_64 only; aarch64 compiles it locally.
runtime_fingerprint=""
if [[ "$arch" == x86_64 ]]; then
runtime_fingerprint=$("$cmake_bin" -DMKW_RUNTIME_DIR="$runtime_source" \
-P "$runtime_source/cmake/RuntimeFingerprint.cmake" | sed -n 's/^runtime_fingerprint=\([0-9a-f]\{64\}\)$/\1/p')
[[ -n "$runtime_fingerprint" ]] || fail "RuntimeFingerprint.cmake printed no runtime fingerprint"
fi
compiler_sha256=$(sha256_of "$clang_binary")
if [[ "$print_fingerprint_only" -eq 1 ]]; then
printf 'compiler_sha256=%s\n' "$compiler_sha256"
printf 'flag_fingerprint=%s\n' "$flag_fingerprint"
printf 'aurora_fingerprint=%s\n' "$aurora_fingerprint"
printf 'third_party_fingerprint=%s\n' "$third_party_fingerprint"
printf 'mbedtls_fingerprint=%s\n' "$mbedtls_fingerprint"
if [[ -n "$runtime_fingerprint" ]]; then
printf 'runtime_fingerprint=%s\n' "$runtime_fingerprint"
fi
exit 0
fi
@@ -232,30 +184,20 @@ echo "Prepare-NativePrebuilt.sh: configuring the aurora/third-party staging buil
-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=lld" \
-DMKW_NATIVE_PREBUILT_EXPORT_DIR="$export_dir"
# mkw_np_probe links exactly what mkw_runtime_common and the public products link, so building it
# compiles the whole redistributable closure and nothing else - and a successful link proves the
# harvested archives are complete.
echo "Prepare-NativePrebuilt.sh: building the aurora/third-party closure..."
"$cmake_bin" --build "$stage_dir" --target mkw_np_probe --parallel "$parallel"
# ---------------------------------------------------------------------------
# Read the description CMake wrote, plus the resolved command lines Ninja holds.
# ---------------------------------------------------------------------------
meta_path="$export_dir/meta.txt"
assert_file "$meta_path" "Native prebuilt export metadata"
get_meta() { awk -F= -v k="$1" '$0 ~ "^" k "=" { sub(/^[^=]*=/, ""); print; exit }' "$meta_path"; }
# mkw_np_probe links exactly what mkw_runtime_common and the public products link, so building it
# compiles the whole redistributable closure and nothing else - and a successful link proves the
# harvested archives are complete. The runtime archive is outside that closure and built alongside.
build_targets=(--target mkw_np_probe)
meta_runtime_fingerprint=$(get_meta runtime_fingerprint)
if [[ -n "$meta_runtime_fingerprint" ]]; then
if [[ -n "$runtime_fingerprint" && "$meta_runtime_fingerprint" != "$runtime_fingerprint" ]]; then
fail "The configure's runtime fingerprint ($meta_runtime_fingerprint) differs from RuntimeFingerprint.cmake's ($runtime_fingerprint)"
fi
build_targets+=(--target mkw_runtime)
elif [[ -n "$runtime_fingerprint" ]]; then
fail "The staging configure exported no runtime_fingerprint; the $arch package must carry the runtime archive"
fi
echo "Prepare-NativePrebuilt.sh: building the aurora/third-party closure..."
"$cmake_bin" --build "$stage_dir" "${build_targets[@]}" --parallel "$parallel"
# ---------------------------------------------------------------------------
# Read the resolved command lines Ninja holds.
# ---------------------------------------------------------------------------
build_ninja="$stage_dir/build.ninja"
assert_file "$build_ninja" "Generated build.ninja"
@@ -419,28 +361,6 @@ while IFS='|' read -r name type file linkerfile; do
linker_file_to_reference["$linkerfile"]="@PKG@/$relative_linker"
fi
done < "$targets_txt"
runtime_archive_ref=""
if [[ -n "$meta_runtime_fingerprint" ]]; then
runtime_linker=$(awk -F'|' '$1 == "mkw_runtime" && $2 == "STATIC_LIBRARY" { print $4; exit }' "$targets_txt")
[[ -n "$runtime_linker" && -f "$runtime_linker" ]] || fail "The runtime archive (mkw_runtime) was not built"
runtime_archive_ref=${linker_file_to_reference["$(normalize "$runtime_linker")"]:-}
[[ "$runtime_archive_ref" == "@PKG@/lib/"* ]] || fail "The runtime archive (mkw_runtime) was not harvested"
fi
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
# 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.
@@ -506,9 +426,6 @@ for item in "${link_items[@]}"; do
if [[ "$item" = /* ]]; then item_abs=$(normalize "$item"); else item_abs=$(normalize "$stage_dir/$item"); fi
ref=${linker_file_to_reference["$item_abs"]:-}
if [[ -n "$ref" ]]; then
for mbedtls_ref in "${mbedtls_refs[@]}"; do
[[ "$ref" == "$mbedtls_ref" ]] && continue 2
done
package_link_items+=("$ref")
continue
fi
@@ -527,11 +444,6 @@ for item in "${link_items[@]}"; do
esac
fail "Link item is neither a system library nor a harvested archive: $item"
done
if [[ -n "$runtime_archive_ref" ]]; then
for item in "${package_link_items[@]}"; do
[[ "$item" != "$runtime_archive_ref" ]] || fail "The runtime archive leaked into the aurora link interface"
done
fi
# ---------------------------------------------------------------------------
# Emit the consumer-facing CMake description.
@@ -593,15 +505,6 @@ generated_cmake="$output_dir/native_prebuilt.cmake"
format_cmake_block MKW_NP_COMPILE_DEFINITIONS "${package_definitions[@]}"
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_MBEDTLS_LIBRARIES "${mbedtls_refs[@]}"
echo 'set(MKW_NP_MBEDTLS_INCLUDE_DIR "@PKG@/include/mbedtls")'
printf 'set(MKW_NP_MBEDTLS_FINGERPRINT "%s")\n' "$mbedtls_fingerprint"
if [[ -n "$runtime_archive_ref" ]]; then
# The precompiled game-independent runtime; RuntimeLibrary.cmake decides whether it applies.
printf 'set(MKW_NP_RUNTIME_ARCHIVE "%s")\n' "$runtime_archive_ref"
printf 'set(MKW_NP_RUNTIME_FINGERPRINT "%s")\n' "$meta_runtime_fingerprint"
printf 'set(MKW_NP_RUNTIME_CPU_PROFILE "%s")\n' "$(get_meta runtime_cpu_profile)"
fi
format_cmake_block MKW_NP_AURORA_TARGETS "aurora::gx" "aurora::pad" "aurora::si" "aurora::vi" "aurora::mtx"
echo ""
printf 'set(MKW_NP_DAWN_CONFIG_DIR "%s")\n' "$dawn_config_token"
@@ -637,13 +540,12 @@ harvested_count=${#linker_file_to_reference[@]}
python3 - "$output_dir" "$compiler_sha256" "$compiler_version" "$flag_fingerprint" \
"$dawn_version" "$dawn_runtime_sha256" "$aurora_fingerprint" "$third_party_fingerprint" \
"$sdl3_target" "$harvested_count" "$mbedtls_fingerprint" \
"${runtime_archive_ref:+$meta_runtime_fingerprint}" "${runtime_archive_ref:+$(get_meta runtime_cpu_profile)}" <<'PY'
"$sdl3_target" "$harvested_count" <<'PY'
import hashlib, json, os, sys, datetime
(output_dir, compiler_sha256, compiler_version, flag_fingerprint, dawn_version,
dawn_runtime_sha256, aurora_fingerprint, third_party_fingerprint, sdl3_target,
harvested_count, mbedtls_fingerprint, runtime_fingerprint, runtime_cpu_profile) = sys.argv[1:]
harvested_count) = sys.argv[1:]
contents = []
for root, dirs, files in os.walk(output_dir):
@@ -658,21 +560,9 @@ for root, dirs, files in os.walk(output_dir):
contents.append({"Path": rel, "Bytes": os.path.getsize(path), "Sha256": digest})
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 = {
"SchemaVersion": 1,
"BuiltUtc": built_utc,
"BuiltUtc": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
"CompilerSha256": compiler_sha256,
"CompilerVersion": compiler_version,
"FlagFingerprint": flag_fingerprint,
@@ -680,14 +570,10 @@ provenance = {
"DawnRuntimeSha256": dawn_runtime_sha256,
"AuroraSourceFingerprint": aurora_fingerprint,
"ThirdPartySourceFingerprint": third_party_fingerprint,
"MbedTlsFingerprint": mbedtls_fingerprint,
"Sdl3Target": sdl3_target,
"HarvestedLibraryCount": int(harvested_count),
"Contents": contents,
}
if runtime_fingerprint:
provenance["RuntimeSourceFingerprint"] = runtime_fingerprint
provenance["RuntimeCpuProfile"] = runtime_cpu_profile
with open(os.path.join(output_dir, "provenance.json"), "w", encoding="utf-8") as fh:
json.dump(provenance, fh, indent=2)
fh.write("\n")
+1 -1
View File
@@ -127,7 +127,7 @@ foreach ($mandatory in ([regex]::Matches($parameterBlock,
}
# --- The build steps the installer's progress bar acts on.
$installProgress = Read-SourceFile (Join-Path $common 'InstallProgress.cs') 'InstallProgress.cs'
$installProgress = Read-SourceFile (Join-Path $setup 'InstallProgress.cs') 'InstallProgress.cs'
$knownSteps = Get-QuotedList $installProgress '(?s)class BuildStepIds\s*\{(.*?)\n\}' `
'const string \w+ = "([^"]+)";' 'BuildStepIds'
$emittedSteps = @([regex]::Matches($localBuild, "(?:-StepId|Write-MkwBuildStep)\s+'([^']+)'") |
@@ -6,6 +6,7 @@
<Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
<Version>0.2.32</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description>
@@ -1,7 +1,4 @@
using System.Text;
using Tomlyn;
using Tomlyn.Parsing;
using Tomlyn.Syntax;
namespace WiiCompiled.Setup.Common;
@@ -20,6 +17,12 @@ public static class RuntimeConfiguration
{
public const string ConfigFileName = "Config.toml";
/// <summary>
/// Ordinal, matching the runtime parser/writer (<c>runtime_config.h</c>) exactly. Case-insensitive
/// matching here would let setup rewrite a <c>[Paths]</c>/<c>Dvd_Root</c> spelling the runtime ignores.
/// </summary>
private const StringComparison KeyComparison = StringComparison.Ordinal;
/// <summary>The per-user configuration an ordinary (non-portable) installation shares.</summary>
public static string ApplicationDataConfigPath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
@@ -72,11 +75,9 @@ public static class RuntimeConfiguration
public static void RemoveDvdRootIfOwned(string configPath, string dvdRoot) =>
RemovePathIfOwned(configPath, "dvd_root", dvdRoot);
/// <summary>The stored string of a <c>[paths]</c> key, exactly as the file holds it.</summary>
/// <summary>The raw stored text of a <c>[paths]</c> key, exactly as the file holds it.</summary>
public static string? GetPath(string configPath, string key) =>
TryLoad(configPath) is { } document && Find(document, ["paths"], key)?.Item.Value is StringValueSyntax value
? value.Value!
: null;
TryUnquoteToml(GetRawValue(configPath, "paths", key) ?? "", out var value) ? value : null;
/// <summary>
/// A <c>[paths]</c> value as the runtime will actually use it. The runtime resolves every relative
@@ -98,19 +99,9 @@ public static class RuntimeConfiguration
private static void SetPath(string configPath, string key, string value)
{
var document = Parse(File.Exists(configPath) ? File.ReadAllText(configPath) : DefaultConfig);
var section = new[] { "paths" };
var newValue = new StringValueSyntax(FormatPathValue(configPath, value));
if (Find(document, section, key) is { } existing)
{
LastToken(newValue).TrailingTrivia = LastToken(existing.Item.Value!).TrailingTrivia;
existing.Item.Value = newValue;
}
else
{
Append(document, section, new KeyValueSyntax(key, newValue));
}
Save(configPath, document);
var lines = ReadLinesOrDefault(configPath);
SetSectionValue(lines, "paths", key, QuoteToml(FormatPathValue(configPath, value)));
WriteLines(configPath, lines);
}
/// <summary>
@@ -132,104 +123,176 @@ public static class RuntimeConfiguration
private static void RemovePathIfOwned(string configPath, string key, string expectedValue)
{
if (TryLoad(configPath) is not { } document ||
Find(document, ["paths"], key) is not { } entry ||
entry.Item.Value is not StringValueSyntax configured)
return;
// A portable installation stores this relative, so ownership is decided on the path the
// runtime would actually resolve, not on the stored text.
if (!ResolveAgainstConfig(configPath, configured.Value!)
.Equals(Path.GetFullPath(expectedValue), StringComparison.OrdinalIgnoreCase))
return;
var previous = entry.Previous?.EndOfLineToken ?? entry.Table?.EndOfLineToken;
if (previous is not null && entry.Item.EndOfLineToken?.TrailingTrivia is { } trivia)
(previous.TrailingTrivia ??= []).AddRange(trivia);
entry.Items.RemoveChild(entry.Item);
Save(configPath, document);
if (!File.Exists(configPath)) return;
var lines = File.ReadAllLines(configPath).ToList();
var inPaths = false;
var changed = false;
for (var index = 0; index < lines.Count; index++)
{
var trimmed = RemoveComment(lines[index]).Trim();
if (IsSection(trimmed))
{
inPaths = trimmed.Equals("[paths]", KeyComparison);
continue;
}
if (!inPaths || trimmed.Length == 0) continue;
var equals = trimmed.IndexOf('=');
if (equals < 0 || !trimmed[..equals].Trim().Equals(key, KeyComparison))
continue;
if (!TryUnquoteToml(trimmed[(equals + 1)..].Trim(), out var configured)) return;
// A portable installation stores this relative, so ownership is decided on the path the
// runtime would actually resolve, not on the stored text.
if (!ResolveAgainstConfig(configPath, configured)
.Equals(Path.GetFullPath(expectedValue), StringComparison.OrdinalIgnoreCase))
return;
lines.RemoveAt(index);
changed = true;
break;
}
if (changed) WriteLines(configPath, lines);
}
private const string DefaultConfig = "# WiiCompiled user configuration\n\n";
private sealed record Entry(
KeyValueSyntax Item, SyntaxList<KeyValueSyntax> Items, TableSyntax? Table, KeyValueSyntax? Previous);
private static DocumentSyntax Parse(string text) => SyntaxParser.ParseStrict(text);
private static DocumentSyntax? TryLoad(string configPath)
/// <summary>The raw TOML literal stored for a key, or null when the section or key is absent.</summary>
public static string? GetRawValue(string configPath, string section, string key)
{
if (!File.Exists(configPath)) return null;
try { return Parse(File.ReadAllText(configPath)); }
catch (TomlException) { return null; }
}
/// <summary>Re-parses the edited tree before replacing the file, so an invalid edit never reaches disk.</summary>
private static void Save(string configPath, DocumentSyntax document)
{
var text = document.ToString();
Parse(text);
FileSystemUtilities.WriteAtomic(configPath, new UTF8Encoding(false).GetBytes(text));
}
private static IEnumerable<TableSyntax> Tables(DocumentSyntax document) =>
document.Tables.OfType<TableSyntax>();
private static string[] Parts(KeySyntax key) =>
[Part(key.Key!), .. key.DotKeys.Select(dotted => Part(dotted.Key!))];
private static string Part(BareKeyOrStringValueSyntax part) =>
part is StringValueSyntax quoted ? quoted.Value! : ((BareKeySyntax)part).Key!.Text!;
/// <summary>Matches by TOML meaning, so <c>"dvd_root"</c> and <c>dvd_root</c> are the same key.</summary>
private static Entry? Find(DocumentSyntax document, string[] section, string key)
{
string[] wanted = [.. section, key];
var scopes = Tables(document)
.Select(table => (Table: (TableSyntax?)table, Prefix: Parts(table.Name!), Items: table.Items))
.Prepend((Table: null, Prefix: [], Items: document.KeyValues));
foreach (var (table, prefix, items) in scopes)
var header = $"[{section}]";
var inSection = false;
foreach (var line in File.ReadLines(configPath))
{
KeyValueSyntax? previous = null;
foreach (var item in items)
var trimmed = RemoveComment(line).Trim();
if (IsSection(trimmed))
{
if (prefix.Concat(Parts(item.Key!)).SequenceEqual(wanted))
return new Entry(item, items, table, previous);
previous = item;
inSection = trimmed.Equals(header, KeyComparison);
continue;
}
if (!inSection || trimmed.Length == 0) continue;
var equals = trimmed.IndexOf('=');
if (equals < 0 || !trimmed[..equals].Trim().Equals(key, KeyComparison)) continue;
return trimmed[(equals + 1)..].Trim();
}
return null;
}
private static void Append(DocumentSyntax document, string[] section, KeyValueSyntax item)
/// <summary>
/// Replaces one key in one section, preserving comments, ordering, and every unrelated setting -
/// this file belongs to the user and to the in-game settings bar as much as to setup. A
/// commented-out key is deliberately not matched, matching the runtime's own writer.
/// </summary>
private static void SetSectionValue(List<string> lines, string section, string key, string value)
{
var text = document.ToString();
var newline = text.Contains("\r\n") ? "\r\n" : "\n";
var table = Tables(document).FirstOrDefault(t => Parts(t.Name!).SequenceEqual(section));
if (table is null)
var header = $"[{section}]";
var sectionStart = -1;
var sectionEnd = lines.Count;
for (var index = 0; index < lines.Count; index++)
{
var name = new KeySyntax(section[0]);
foreach (var part in section.Skip(1)) name.DotKeys.Add(new DottedKeyItemSyntax(part));
table = new TableSyntax(name) { EndOfLineToken = NewLine(newline) };
var blankLine = text.EndsWith("\n\n") || text.EndsWith("\n\r\n");
var gap = text.Length == 0 ? 0 : !text.EndsWith('\n') ? 2 : blankLine ? 0 : 1;
for (var i = 0; i < gap; i++) (table.LeadingTrivia ??= []).Add(new SyntaxTrivia(TokenKind.NewLine, newline));
document.Tables.Add(table);
var trimmed = RemoveComment(lines[index]).Trim();
if (!IsSection(trimmed)) continue;
if (sectionStart >= 0)
{
sectionEnd = index;
break;
}
if (trimmed.Equals(header, KeyComparison)) sectionStart = index;
}
if (sectionStart < 0)
{
if (lines.Count > 0 && lines[^1].Length != 0) lines.Add("");
lines.Add(header);
lines.Add($"{key} = {value}");
return;
}
// Blank lines and comments after the last entry stay after the new one.
var last = table.Items.LastOrDefault();
var before = last is null ? table.EndOfLineToken ??= NewLine(newline) : last.EndOfLineToken ??= NewLine(newline);
item.EndOfLineToken = NewLine(newline);
item.EndOfLineToken.TrailingTrivia = before.TrailingTrivia;
before.TrailingTrivia = null;
table.Items.Add(item);
for (var index = sectionStart + 1; index < sectionEnd; index++)
{
var trimmed = RemoveComment(lines[index]).Trim();
if (trimmed.Length == 0) continue;
var equals = trimmed.IndexOf('=');
if (equals >= 0 && trimmed[..equals].Trim().Equals(key, KeyComparison))
{
lines[index] = $"{key} = {value}";
return;
}
}
// Append after the section's last real line, not after the blank line that separates it from
// the next header, so a generated file stays readable for the user who also edits it by hand.
var insertAt = sectionEnd;
while (insertAt > sectionStart + 1 && lines[insertAt - 1].Trim().Length == 0) insertAt--;
lines.Insert(insertAt, $"{key} = {value}");
}
private static SyntaxToken NewLine(string text) => new(TokenKind.NewLine, text);
private static List<string> ReadLinesOrDefault(string configPath) =>
File.Exists(configPath) ? File.ReadAllLines(configPath).ToList() : DefaultConfigLines();
private static SyntaxToken LastToken(SyntaxNode node)
private static void WriteLines(string configPath, List<string> lines) =>
FileSystemUtilities.WriteAtomic(configPath,
Encoding.UTF8.GetBytes(string.Join(Environment.NewLine, lines) + Environment.NewLine));
private static List<string> DefaultConfigLines() =>
[
"# WiiCompiled user configuration",
"",
"[paths]"
];
private static bool IsSection(string value) =>
value.Length >= 2 && value[0] == '[' && value[^1] == ']';
/// <summary>
/// Line-for-line port of <c>RemoveComment</c> in <c>runtime_config.h</c>, including its backslash
/// escaping inside basic strings: without it a Windows path ending in <c>\\"</c> reads as
/// re-opening the quote, and the two implementations disagree about which <c>#</c> starts a comment.
/// </summary>
private static string RemoveComment(string line)
{
while (node is not SyntaxToken)
node = node.GetChild(node.ChildrenCount - 1)!;
return (SyntaxToken)node;
var inSingle = false;
var inDouble = false;
var escaped = false;
for (var index = 0; index < line.Length; index++)
{
var character = line[index];
if (inDouble && character == '\\' && !escaped)
{
escaped = true;
continue;
}
if (character == '\'' && !inDouble) inSingle = !inSingle;
else if (character == '"' && !inSingle && !escaped) inDouble = !inDouble;
else if (character == '#' && !inSingle && !inDouble) return line[..index];
escaped = false;
}
return line;
}
private static string QuoteToml(string value) =>
"\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
public static bool TryUnquoteToml(string value, out string result)
{
result = "";
if (value.Length < 2) return false;
// Literal strings carry no escapes; the runtime's parser treats them the same way.
if (value[0] == '\'' && value[^1] == '\'')
{
result = value[1..^1];
return true;
}
if (value[0] != '"' || value[^1] != '"') return false;
var builder = new StringBuilder(value.Length - 2);
for (var index = 1; index < value.Length - 1; index++)
{
if (value[index] == '\\' && index + 1 < value.Length - 1)
{
var escaped = value[++index];
builder.Append(escaped switch { '\\' => '\\', '"' => '"', _ => escaped });
}
else
{
builder.Append(value[index]);
}
}
result = builder.ToString();
return true;
}
}
@@ -5,13 +5,11 @@
<Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
<Version>0.2.32</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description>
<DebugType>embedded</DebugType>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Tomlyn" Version="2.10.1" />
</ItemGroup>
</Project>
@@ -82,7 +82,6 @@ internal static class BuildRunner
{
startInfo.ArgumentList.Add("--sysroot"); startInfo.ArgumentList.Add(sysroot);
}
startInfo.ArgumentList.Add("--setup-version"); startInfo.ArgumentList.Add(ProductInfo.Version);
using var process = new Process { StartInfo = startInfo };
var window = new BuildProgressWindow(reporter, InstallStages.Build, start: 6, end: 96);
@@ -15,12 +15,6 @@ internal static class DiscTool
string isoPath, ProjectManifest manifest, string assetsDirectory, string workspace,
string? nodToolBin, IInstallReporter reporter, CancellationToken cancellationToken)
{
if (IsAlreadyExtracted(manifest, assetsDirectory))
{
reporter.Progress(InstallStages.ExtractDisc, "Reusing the verified extracted disc", 6);
return;
}
var nodTool = nodToolBin ?? await NodToolProvider.ResolveAsync(workspace, cancellationToken);
// `nodtool info` only decodes the disc/partition headers (milliseconds); `nodtool extract`
@@ -41,8 +35,6 @@ internal static class DiscTool
// time, so it has to survive past this install (see Program.cs, which points dvd_root here).
reporter.Progress(InstallStages.ExtractDisc, "Extracting the disc image", 4);
var dataDir = Path.Combine(assetsDirectory, "DATA");
Directory.CreateDirectory(assetsDirectory);
File.Delete(Path.Combine(assetsDirectory, ExtractionMarker));
if (Directory.Exists(dataDir)) Directory.Delete(dataDir, recursive: true);
await RunExtractAsync(nodTool, isoPath, dataDir, cancellationToken);
@@ -69,7 +61,6 @@ internal static class DiscTool
Directory.CreateDirectory(assetsDirectory);
File.Copy(dolPath, Path.Combine(assetsDirectory, "main.dol"), overwrite: true);
File.Copy(relPath, Path.Combine(assetsDirectory, "StaticR.rel"), overwrite: true);
File.WriteAllText(Path.Combine(assetsDirectory, ExtractionMarker), ExtractionRecord(dolSha, dataDir));
reporter.Progress(InstallStages.ExtractDisc, "Disc validated and extracted", 6);
}
@@ -95,36 +86,6 @@ internal static class DiscTool
return stdout;
}
private const string ExtractionMarker = ".disc-extracted";
// Only one disc revision is accepted, so a completed, verified extraction is identical to a fresh one.
private static bool IsAlreadyExtracted(ProjectManifest manifest, string assetsDirectory)
{
var marker = Path.Combine(assetsDirectory, ExtractionMarker);
var dol = Path.Combine(assetsDirectory, "main.dol");
var rel = Path.Combine(assetsDirectory, "StaticR.rel");
var dataDir = Path.Combine(assetsDirectory, "DATA");
if (!File.Exists(marker) || !File.Exists(dol) || !File.Exists(rel) ||
!Directory.Exists(Path.Combine(dataDir, "files")))
return false;
return string.Equals(File.ReadAllText(marker), ExtractionRecord(manifest.DolSha256, dataDir),
StringComparison.Ordinal) &&
string.Equals(Sha256Of(dol), manifest.DolSha256, StringComparison.Ordinal) &&
string.Equals(Sha256Of(rel), manifest.RelSha256, StringComparison.Ordinal);
}
// The DOL hash plus every extracted file's path and size, so a later deletion or truncation inside
// DATA forces a fresh extraction.
private static string ExtractionRecord(string dolSha, string dataDir)
{
var record = new System.Text.StringBuilder(dolSha).Append('\n');
foreach (var path in Directory.EnumerateFiles(dataDir, "*", SearchOption.AllDirectories)
.Select(path => Path.GetRelativePath(dataDir, path).Replace('\\', '/'))
.Order(StringComparer.Ordinal))
record.Append(new FileInfo(Path.Combine(dataDir, path)).Length).Append(' ').Append(path).Append('\n');
return record.ToString();
}
private static string Sha256Of(string path)
{
using var stream = File.OpenRead(path);
@@ -0,0 +1,209 @@
using System.Text.Json;
namespace WiiCompiled.Setup.Linux;
// Ported near-verbatim from Launcher/WiiCompiled.Setup/InstallProgress.cs: this whole file is
// platform-neutral (System.Text.Json + Console only), so the NDJSON --progress-json wire protocol
// stays byte-for-byte the same shape a future GUI already speaks on Windows.
/// <summary>
/// Stable stage identifiers reported by <c>--progress-json</c>. Kept intentionally small for this
/// lean Linux installer (no toolkit-extraction/publish-transaction stages, since there is no
/// bundled toolkit or staged workspace copy here - see the plan's "operate on a git checkout"
/// scoping decision).
/// </summary>
internal static class InstallStages
{
public const string Validate = "validate";
public const string ExtractDisc = "extract-disc";
public const string Build = "build";
public const string Shortcuts = "shortcuts";
}
/// <summary>
/// Where an installation reports what it is doing. Progress is coarse and monotonic; raw translator
/// and compiler output is a diagnostic, never progress, because it is unbounded and machine-hostile.
/// </summary>
internal interface IInstallReporter
{
void Progress(string stage, string message, int percent);
void Diagnostic(string line);
}
/// <summary>
/// The <c>--progress-json</c> protocol: one JSON object per line on stdout, nothing else on stdout,
/// diagnostics on stderr. The terminal <c>result</c> line is written exactly once.
/// </summary>
internal sealed class NdjsonInstallReporter : IInstallReporter
{
private static readonly JsonSerializerOptions Options = new() { WriteIndented = false };
private readonly object _gate = new();
private int _lastPercent;
private bool _finished;
public void Progress(string stage, string message, int percent)
{
lock (_gate)
{
if (_finished) return;
// Percentages are clamped monotonic: a caller's progress bar must never walk backwards
// because a later stage happened to estimate a lower number.
_lastPercent = Math.Clamp(Math.Max(percent, _lastPercent), 0, 99);
WriteLine(new { type = "progress", stage, message, percent = _lastPercent });
}
}
public void Diagnostic(string line) => Console.Error.WriteLine(line);
public void Success(string installDirectory)
{
lock (_gate)
{
if (_finished) return;
_finished = true;
WriteLine(new { type = "result", success = true, version = ProductInfo.Version, installDir = installDirectory });
}
}
public void Failure(string error)
{
lock (_gate)
{
if (_finished) return;
_finished = true;
WriteLine(new { type = "result", success = false, error });
}
}
/// <summary>
/// The terminal result line is the caller's only completion signal, so no exit path may skip it.
/// Callers invoke this from a finally block; it is a no-op once a result was already written.
/// </summary>
public void EnsureFinished(string errorIfUnfinished) => Failure(errorIfUnfinished);
private static void WriteLine(object value)
{
Console.Out.WriteLine(JsonSerializer.Serialize(value, Options));
Console.Out.Flush();
}
}
/// <summary>Plain-text console reporting for a run without <c>--progress-json</c>.</summary>
internal sealed class ConsoleInstallReporter : IInstallReporter
{
public void Progress(string stage, string message, int percent) =>
Console.Out.WriteLine($"[{percent,3}%] {message}");
public void Diagnostic(string line) => Console.Out.WriteLine(line);
}
/// <summary>
/// Build step identifiers from local-build.sh's <c>MKWCBUILD:STEP:&lt;id&gt;</c> lines - the id is
/// the contract, matched against Launcher/local-build.sh's log_step() call sites.
/// </summary>
internal static class BuildStepIds
{
public const string BuildTranslator = "build-translator";
public const string ReuseBaseTranslation = "reuse-base-translation";
public const string RetranslateBase = "retranslate-base";
public const string TranslateBase = "translate-base";
public const string EmitBaseManifest = "emit-base-manifest";
public const string TranslateMod = "translate-mod";
public const string GenerateDataInit = "generate-data-init";
public const string EmitBuildShards = "emit-build-shards";
public const string ConfigureNative = "configure-native";
public const string Compile = "compile";
}
/// <summary>
/// Maps one local-build.sh run onto a slice of the overall percentage. local-build.sh announces
/// every step it starts with an <c>MKWCBUILD:</c> prefix, so the slice can advance on real events
/// instead of on a timer.
/// </summary>
internal sealed class BuildProgressWindow
{
private const string Marker = "MKWCBUILD:";
private const string StepMarker = "STEP:";
/// <summary>The fraction the compile step reaches; beyond it, compiler output is a heartbeat.</summary>
private const double CompileFraction = 0.58;
private static readonly (string Id, double Fraction, string Message)[] Steps =
[
(BuildStepIds.BuildTranslator, 0.04, "Building the translator"),
(BuildStepIds.ReuseBaseTranslation, 0.30, "Reusing the completed base translation"),
(BuildStepIds.RetranslateBase, 0.08, "The base translation is stale; retranslating it"),
(BuildStepIds.TranslateBase, 0.10, "Translating Mario Kart Wii"),
(BuildStepIds.EmitBaseManifest, 0.34, "Creating the translation manifest"),
(BuildStepIds.TranslateMod, 0.38, "Translating the Retro Rewind Code.pul"),
(BuildStepIds.GenerateDataInit, 0.44, "Generating game data initialization"),
(BuildStepIds.EmitBuildShards, 0.48, "Preparing the native build"),
(BuildStepIds.ConfigureNative, 0.52, "Configuring the compiler"),
(BuildStepIds.Compile, CompileFraction, "Compiling the game. This is the longest step"),
];
private readonly IInstallReporter _reporter;
private readonly string _stage;
private readonly int _start;
private readonly int _end;
private double _fraction;
private string _message = "Preparing the local build";
private int _reportedPercent = -1;
public BuildProgressWindow(IInstallReporter reporter, string stage, int start, int end)
{
_reporter = reporter;
_stage = stage;
_start = start;
_end = end;
}
public void Observe(string line)
{
var index = line.IndexOf(Marker, StringComparison.Ordinal);
if (index >= 0)
{
var text = line[(index + Marker.Length)..].Trim();
if (text.StartsWith(StepMarker, StringComparison.Ordinal))
{
var identifier = text[StepMarker.Length..];
var end = identifier.IndexOf(' ');
if (end >= 0) identifier = identifier[..end];
foreach (var (id, fraction, message) in Steps)
{
if (!id.Equals(identifier, StringComparison.Ordinal)) continue;
if (fraction > _fraction)
{
_fraction = fraction;
_message = message;
Emit();
}
return;
}
}
}
// Anything else - a plain MKWCBUILD note, or raw tool output - stays a diagnostic and only
// feeds the heartbeat below.
_reporter.Diagnostic(line);
// Compilation announces itself once and then emits thousands of compiler lines. Treat that
// output as a heartbeat so the slice keeps creeping forward, but only publish a progress
// line when the rounded percentage actually changes.
if (_fraction >= CompileFraction)
{
_fraction = Math.Min(0.97, _fraction + 0.0015);
Emit();
}
}
private void Emit()
{
var percent = Interpolate(_fraction);
if (percent == _reportedPercent) return;
_reportedPercent = percent;
_reporter.Progress(_stage, _message, percent);
}
private int Interpolate(double fraction) =>
(int)Math.Round(_start + (_end - _start) * Math.Clamp(fraction, 0, 1));
}
@@ -1,10 +0,0 @@
namespace WiiCompiled.Setup.Linux;
/// <summary>Stable stage identifiers reported by <c>--progress-json</c>.</summary>
internal static class InstallStages
{
public const string Validate = "validate";
public const string ExtractDisc = "extract-disc";
public const string Build = "build";
public const string Shortcuts = "shortcuts";
}
+1 -5
View File
@@ -1,13 +1,9 @@
using System.Reflection;
namespace WiiCompiled.Setup.Linux;
internal static class ProductInfo
{
public const string Name = "WiiCompiled";
public static readonly string Version =
typeof(ProductInfo).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!
.InformationalVersion;
public const string Version = "0.2.32";
}
/// <summary>One installed product's record inside install-state.json.</summary>
+8 -29
View File
@@ -31,7 +31,7 @@ internal static class Program
var (command, flags) = ParseArgs(args);
if (command is null) { PrintUsage(); return 1; }
var progressJson = flags.ContainsKey("progress-json");
IInstallReporter reporter = progressJson ? new NdjsonInstallReporter(ProductInfo.Version) : new ConsoleInstallReporter();
IInstallReporter reporter = progressJson ? new NdjsonInstallReporter() : new ConsoleInstallReporter();
try
{
@@ -127,40 +127,19 @@ internal static class Program
string? retroWfcOfflineDir = null;
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");
reporter.Progress(InstallStages.Validate,
"Downloading the current Retro-WFC payload", 1);
reporter.Progress(InstallStages.Validate, "Preparing the 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.
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(cacheDir);
}
catch (InvalidDataException)
{
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
{
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(cacheDir);
}
catch (Exception cacheFailure) when (cacheFailure is IOException or
UnauthorizedAccessException or InvalidDataException)
{
throw new InvalidOperationException(
"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;
}
@@ -6,6 +6,7 @@
<Nullable>enable</Nullable>
<AssemblyName>WiiCompiled.Setup.Linux</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Linux</RootNamespace>
<Version>0.2.32</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled on Linux</Description>
@@ -15,6 +16,5 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
<Using Include="WiiCompiled.Setup.Common" />
</ItemGroup>
</Project>
@@ -57,7 +57,7 @@ internal static class ConsoleCommands
public static int VerifyInputs(CommandLine command)
{
var reporter = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version) : null;
var reporter = command.ProgressJson ? new NdjsonInstallReporter() : null;
var temp = Path.Combine(Path.GetTempPath(), "mkwc-verify-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(temp);
try
@@ -107,7 +107,7 @@ internal static class ConsoleCommands
}
File.WriteAllText(logPath, $"{ProductInfo.Name} setup {ProductInfo.Version}{Environment.NewLine}");
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version, Log) : null;
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(Log) : null;
IInstallReporter reporter = ndjson ?? (IInstallReporter)new ConsoleInstallReporter(Log);
var installDirectory = Path.GetFullPath(command.InstallDirectory!);
try
@@ -145,7 +145,7 @@ internal static class ConsoleCommands
public static int CheckProducts(CommandLine command,
CancellationToken cancellationToken = default)
{
var reporter = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version) : null;
var reporter = command.ProgressJson ? new NdjsonInstallReporter() : null;
var installation = new Installation(string.IsNullOrWhiteSpace(command.InstallDirectory)
? AppContext.BaseDirectory
: Path.GetFullPath(command.InstallDirectory));
@@ -198,7 +198,7 @@ internal static class ConsoleCommands
}
}
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version, Log) : null;
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(Log) : null;
IInstallReporter reporter = ndjson is null ? new ConsoleInstallReporter(Log) : ndjson;
var installDirectory = Path.GetFullPath(command.InstallDirectory!);
var installation = new Installation(installDirectory);
@@ -1,19 +1,35 @@
using System.Text.Json;
namespace WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// Stable stage identifiers reported by <c>--progress-json</c>. These are part of the public
/// WheelWizard contract (docs/WHEELWIZARD_CONTRACT.md): the strings may gain new members, but an
/// existing identifier never changes meaning.
/// </summary>
internal static class InstallStages
{
public const string Validate = "validate";
public const string ExtractToolkit = "extract-toolkit";
public const string ExtractDisc = "extract-disc";
public const string PrepareRetro = "prepare-retro";
public const string BuildBase = "build-base";
public const string BuildRetro = "build-retro";
public const string Publish = "publish";
}
/// <summary>
/// Where an installation reports what it is doing. Progress is coarse and monotonic; raw translator
/// and compiler output is a diagnostic, never progress, because it is unbounded and machine-hostile.
/// </summary>
public interface IInstallReporter
internal interface IInstallReporter
{
void Progress(string stage, string message, int percent);
void Diagnostic(string line);
}
/// <summary>Reports to a caller-supplied sink; used by console and maintenance commands.</summary>
public sealed class DelegatingInstallReporter : IInstallReporter
internal sealed class DelegatingInstallReporter : IInstallReporter
{
private readonly Action<string> _sink;
@@ -27,20 +43,15 @@ public sealed class DelegatingInstallReporter : IInstallReporter
/// The <c>--progress-json</c> protocol: one JSON object per line on stdout, nothing else on stdout,
/// diagnostics on stderr. The terminal <c>result</c> line is written exactly once.
/// </summary>
public sealed class NdjsonInstallReporter : IInstallReporter
internal sealed class NdjsonInstallReporter : IInstallReporter
{
private static readonly JsonSerializerOptions Options = new() { WriteIndented = false };
private readonly string _version;
private readonly Action<string>? _log;
private readonly object _gate = new();
private int _lastPercent;
private bool _finished;
public NdjsonInstallReporter(string version, Action<string>? log = null)
{
_version = version;
_log = log;
}
public NdjsonInstallReporter(Action<string>? log = null) => _log = log;
public void Progress(string stage, string message, int percent)
{
@@ -67,7 +78,7 @@ public sealed class NdjsonInstallReporter : IInstallReporter
{
if (_finished) return;
_finished = true;
WriteLine(new { type = "result", success = true, version = _version, installDir = installDirectory });
WriteLine(new { type = "result", success = true, version = ProductInfo.Version, installDir = installDirectory });
}
}
@@ -95,8 +106,8 @@ public sealed class NdjsonInstallReporter : IInstallReporter
}
}
/// <summary>Plain-text console reporting for a run without <c>--progress-json</c>.</summary>
public sealed class ConsoleInstallReporter : IInstallReporter
/// <summary>Plain-text console reporting for a silent install without <c>--progress-json</c>.</summary>
internal sealed class ConsoleInstallReporter : IInstallReporter
{
private readonly Action<string>? _log;
@@ -115,10 +126,10 @@ public sealed class ConsoleInstallReporter : IInstallReporter
}
}
/// <summary>Build step identifiers from the build scripts' <c>MKWCBUILD:STEP:&lt;id&gt;</c> lines. The
/// <summary>Build step identifiers from the bundled script's <c>MKWCBUILD:STEP:&lt;id&gt;</c> lines. The
/// id is the contract; LocalBuild.ps1 emits these exactly and Test-PinnedFacts.ps1 fails the release if
/// the two sets disagree, so progress never depends on matching reworded English prose.</summary>
public static class BuildStepIds
internal static class BuildStepIds
{
public const string ReuseBaseTranslation = "reuse-base-translation";
public const string RetranslateBase = "retranslate-base";
@@ -132,16 +143,16 @@ public static class BuildStepIds
}
/// <summary>
/// Maps one local translate-and-compile run onto a slice of the overall percentage. The build
/// scripts announce every step they start with an <c>MKWCBUILD:</c> prefix, so the slice can advance
/// on real events instead of on a timer.
/// Maps one local translate-and-compile run onto a slice of the overall percentage. The bundled
/// build script announces every step it starts with an <c>MKWCBUILD:</c> prefix, so the slice can
/// advance on real events instead of on a timer.
/// </summary>
public sealed class BuildProgressWindow
internal sealed class BuildProgressWindow
{
private const string Marker = "MKWCBUILD:";
private const string StepMarker = "STEP:";
/// <summary>The fraction the compile step reaches; beyond it, Ninja's counter drives progress.</summary>
/// <summary>The fraction the compile step reaches; beyond it, compiler output is a heartbeat.</summary>
private const double CompileFraction = 0.58;
private static readonly (string Id, double Fraction, string Message)[] Steps =
@@ -153,7 +164,7 @@ public sealed class BuildProgressWindow
(BuildStepIds.TranslateMod, 0.38, "Translating the Retro Rewind Code.pul"),
(BuildStepIds.GenerateDataInit, 0.44, "Generating game data initialization"),
(BuildStepIds.EmitBuildShards, 0.48, "Preparing the native build"),
(BuildStepIds.ConfigureNative, 0.52, "Configuring the compiler"),
(BuildStepIds.ConfigureNative, 0.52, "Configuring the bundled compiler"),
(BuildStepIds.Compile, CompileFraction, "Compiling the game. This is the longest step"),
];
@@ -198,30 +209,19 @@ public sealed class BuildProgressWindow
}
}
// Anything else - an unknown step identifier, a plain MKWCBUILD note, or raw tool output -
// stays a diagnostic.
// Anything else - an unknown step identifier from a newer script, a plain MKWCBUILD note, or
// raw tool output - stays a diagnostic and only feeds the heartbeat below.
_reporter.Diagnostic(line);
// Ninja prefixes every edge with [done/total]; how many edges there are depends on how much
// of the build is already up to date, so follow the counter rather than counting lines.
if (_fraction >= CompileFraction && TryParseNinjaProgress(line, out var done, out var total))
// Compilation announces itself once and then emits thousands of compiler lines. Treat that
// output as a heartbeat so the slice keeps creeping forward, but only publish a progress
// line when the rounded percentage actually changes.
if (_fraction >= CompileFraction)
{
_fraction = Math.Max(_fraction, CompileFraction + (0.97 - CompileFraction) * done / total);
_fraction = Math.Min(0.97, _fraction + 0.0015);
Emit();
}
}
private static bool TryParseNinjaProgress(string line, out int done, out int total)
{
done = total = 0;
if (!line.StartsWith('[')) return false;
var close = line.IndexOf(']');
var slash = line.IndexOf('/');
return close > 0 && slash > 0 && slash < close
&& int.TryParse(line.AsSpan(1, slash - 1), out done)
&& int.TryParse(line.AsSpan(slash + 1, close - slash - 1), out total)
&& total > 0 && done <= total;
}
private void Emit()
{
var percent = Interpolate(_fraction);
@@ -1,17 +0,0 @@
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// Stable stage identifiers reported by <c>--progress-json</c>. These are part of the public
/// WheelWizard contract (docs/WHEELWIZARD_CONTRACT.md): the strings may gain new members, but an
/// existing identifier never changes meaning.
/// </summary>
internal static class InstallStages
{
public const string Validate = "validate";
public const string ExtractToolkit = "extract-toolkit";
public const string ExtractDisc = "extract-disc";
public const string PrepareRetro = "prepare-retro";
public const string BuildBase = "build-base";
public const string BuildRetro = "build-retro";
public const string Publish = "publish";
}
@@ -501,31 +501,17 @@ internal sealed class InstallerEngine
var dol = Path.Combine(dataRoot, "sys", "main.dol");
var rel = Path.Combine(dataRoot, "files", "rel", "StaticR.rel");
var fst = Path.Combine(dataRoot, "sys", "fst.bin");
var missing = new[] { dol, rel, fst }.Where(path => !File.Exists(path))
.Select(path => Path.GetRelativePath(dataRoot, path)).ToList();
if (missing.Count > 0)
throw new InvalidDataException(
"The disc extraction is missing required Mario Kart Wii files: " +
string.Join(", ", missing) + ".");
var mismatches = new List<string>();
foreach (var (name, path, expected) in new[]
{
("main.dol", dol, manifest.ExpectedDolSha256),
("StaticR.rel", rel, manifest.ExpectedRelSha256)
})
if (!File.Exists(dol) || !File.Exists(rel) || !File.Exists(fst))
throw new InvalidDataException("The disc extraction is missing required Mario Kart Wii files.");
var dolHash = InputValidation.Sha256File(dol);
var relHash = InputValidation.Sha256File(rel);
if (!dolHash.Equals(manifest.ExpectedDolSha256, StringComparison.OrdinalIgnoreCase) ||
!relHash.Equals(manifest.ExpectedRelSha256, StringComparison.OrdinalIgnoreCase))
{
var actual = InputValidation.Sha256File(path);
if (!actual.Equals(expected, StringComparison.OrdinalIgnoreCase))
mismatches.Add($"{name} is {new FileInfo(path).Length:N0} bytes with SHA-256 {actual} " +
$"(expected {expected})");
}
if (mismatches.Count > 0)
throw new InvalidDataException(
"The disc is RMCP01 but its game code does not match the supported clean PAL revision: " +
string.Join("; ", mismatches) + ". " +
"The disc is RMCP01 but does not match the supported clean PAL revision. " +
"Patched or otherwise modified game code cannot be installed safely.");
}
}
private static void ValidateInstallDirectory(string path) =>
@@ -110,7 +110,8 @@ internal sealed class ProductRepairService
InputValidation.CurrentRetroWfcPayloadUri, payloadScratch, cancellationToken);
}
catch (Exception ex) when (!cancellationToken.IsCancellationRequested &&
ex is HttpRequestException or TimeoutException or IOException)
ex is HttpRequestException or IOException or InvalidDataException
or InvalidOperationException or OperationCanceledException)
{
payloadSnapshot = RecoverInstalledRetroWfcPayload(toolkitFingerprint,
Path.Combine(scratchRoot, "retro-wfc-payload-recovered"), ex, cancellationToken);
@@ -1,6 +1,5 @@
namespace WiiCompiled.Setup.Windows;
using System.Reflection;
using System.Runtime.InteropServices;
internal static class Program
@@ -31,7 +30,7 @@ internal static class Program
{
// A command line was supplied, so a caller is driving this process. Rejecting the
// command with a modal dialog would hang that caller forever.
if (progressJson) new NdjsonInstallReporter(ProductInfo.Version).Failure(ex.Message);
if (progressJson) new NdjsonInstallReporter().Failure(ex.Message);
else Console.Error.WriteLine(ex.Message);
return 1;
}
@@ -91,7 +90,7 @@ internal static class Program
// errors raised outside it.
if (progressJson)
{
new NdjsonInstallReporter(ProductInfo.Version).Failure(ex.Message);
new NdjsonInstallReporter().Failure(ex.Message);
Console.Error.WriteLine(ex);
}
else
@@ -122,9 +121,7 @@ internal static class PlatformChecks
internal static class ProductInfo
{
public const string Name = "WiiCompiled";
public static readonly string Version =
typeof(ProductInfo).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!
.InformationalVersion;
public const string Version = "0.2.32";
/// <summary>
/// The setup executable is copied into the installation under this name. It is the launcher and
+10 -117
View File
@@ -1,6 +1,5 @@
using System.Buffers.Binary;
using System.Security.Cryptography;
using Tomlyn;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
@@ -52,7 +51,6 @@ internal static class SelfTests
RuntimeConfiguration.SetRetroRewindRoot, RuntimeConfiguration.RemoveRetroRewindRootIfOwned),
failures);
Test("Runtime configuration key matching", TestRuntimeConfigurationKeyMatching, failures);
Test("Runtime configuration quoted keys", TestRuntimeConfigurationQuotedKeys, failures);
Test("Install location validation", TestInstallLocationValidation, failures);
Test("Command line contract", TestCommandLineContract, failures);
Test("Portable root discovery", TestPortableRootDiscovery, failures);
@@ -231,70 +229,6 @@ internal static class SelfTests
}
}
/// <summary>
/// Quoted and bare spellings of a key or table are the same TOML key, so rewriting replaces the
/// entry in place, keeps everything else byte-identical, and never touches an unparseable file.
/// </summary>
private static void TestRuntimeConfigurationQuotedKeys()
{
var root = Path.Combine(Path.GetTempPath(), "mkwc-tomlquoted-" + Guid.NewGuid().ToString("N"));
var config = Path.Combine(root, RuntimeConfiguration.ConfigFileName);
var owned = Path.Combine(root, "Owned Location");
var fullOwned = Path.GetFullPath(owned);
try
{
Directory.CreateDirectory(owned);
foreach (var header in new[] { "[\"paths\"]", "[ 'paths' ]", "[paths]" })
foreach (var key in new[] { "\"dvd_root\"", "'dvd_root'", "\"dvd\\u005Froot\"", "dvd_root" })
{
File.WriteAllText(config,
$"# top\r\n[video]\r\nwidescreen = true # keep\r\n\r\n{header}\r\n {key}=\"D:\\\\Old\" # note\r\nother = 1\r\n");
RuntimeConfiguration.SetDvdRoot(config, owned);
var text = File.ReadAllText(config);
var expected = $"# top\r\n[video]\r\nwidescreen = true # keep\r\n\r\n{header}\r\n {key}=" +
$"\"{fullOwned.Replace("\\", "\\\\")}\" # note\r\nother = 1\r\n";
if (text != expected)
throw new Exception($"{header} {key} was not replaced in place:\r\n{text}");
if (RuntimeConfiguration.GetResolvedPath(config, "dvd_root") != fullOwned)
throw new Exception($"The replaced {key} could not be read back.");
}
File.WriteAllText(config, "[paths]\r\nother = 1\r\n\r\n# next\r\n[video]\r\nx = 2");
RuntimeConfiguration.SetDvdRoot(config, owned);
var appended = File.ReadAllText(config);
if (!appended.StartsWith("[paths]\r\nother = 1\r\ndvd_root = ", StringComparison.Ordinal) ||
!appended.EndsWith("\r\n\r\n# next\r\n[video]\r\nx = 2", StringComparison.Ordinal))
throw new Exception("A missing key was not appended to its section:\r\n" + appended);
File.WriteAllText(config, "dvd_root = \"D:\\\\Root\"\r\n[video]\r\nx = 2\r\n");
RuntimeConfiguration.SetDvdRoot(config, owned);
var created = File.ReadAllText(config);
if (!created.StartsWith("dvd_root = \"D:\\\\Root\"\r\n[video]\r\nx = 2\r\n\r\n[paths]\r\n", StringComparison.Ordinal) ||
RuntimeConfiguration.GetResolvedPath(config, "dvd_root") != fullOwned)
throw new Exception("A missing section was not created without touching a same-named root key:\r\n" + created);
File.Delete(config);
RuntimeConfiguration.SetDvdRoot(config, owned);
if (RuntimeConfiguration.GetResolvedPath(config, "dvd_root") != fullOwned)
throw new Exception("A missing config file was not created.");
const string broken = "[paths]\r\ndvd_root = 1\r\ndvd_root = 2\r\n";
File.WriteAllText(config, broken);
try
{
RuntimeConfiguration.SetDvdRoot(config, owned);
throw new Exception("An unparseable config was rewritten.");
}
catch (TomlException) { }
if (File.ReadAllText(config) != broken)
throw new Exception("An unparseable config was modified.");
}
finally
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}
/// <summary>
/// One location rule set covers both an installation directory and a portable root, so the
/// rejections a portable install performs are the ones an ordinary install performs too.
@@ -1139,10 +1073,10 @@ internal static class SelfTests
if (topologyChangedRuntimeAssets == changedRuntimeAssets)
throw new Exception("An empty copied runtime-asset directory was omitted from its identity.");
// Cache-reuse subsets: translator or native registration changes invalidate translation
// but not toolchain, since CMake/Ninja rebuild exactly the affected objects; a compiler
// change invalidates only toolchain; a native_prebuilt provenance rewrite invalidates
// neither. Without a current native index, runtime/src itself decides translation.
// Cache-reuse subsets: translator or runtime/src changes (the translator regex-scans it
// for native overrides and effect contracts) invalidate translation but not toolchain,
// since CMake/Ninja rebuild exactly the affected objects; a compiler change invalidates
// only toolchain; a native_prebuilt provenance rewrite invalidates neither.
var components = ToolkitFingerprint.ComputeComponents(root);
if (components.Compile != afterPatch)
throw new Exception("ComputeComponents disagrees with the compile identity.");
@@ -1153,36 +1087,15 @@ internal static class SelfTests
throw new Exception("A new translator did not change the translation identity.");
if (afterNewTranslator.NativeToolchain != components.NativeToolchain)
throw new Exception("A new translator needlessly changed the native toolchain identity.");
var runtimeMain = Path.Combine(root, "BuildWorkspace", "runtime", "src", "main.cpp");
File.WriteAllText(runtimeMain, "int main3();");
var afterUnindexedRuntime = ToolkitFingerprint.ComputeComponents(root);
if (afterUnindexedRuntime.Translation == afterNewTranslator.Translation)
throw new Exception("A runtime source change without a native index did not change the translation identity.");
File.WriteAllText(runtimeMain, "int main();");
if (WriteFakeNativeIndex(root, "\"a\"") !=
"50b9779591f4b205751361c8899accd362f96ea5dfd32febed1bedca534c523a")
throw new Exception("The native index source digest no longer matches the translator's.");
var indexed = ToolkitFingerprint.ComputeComponents(root);
File.WriteAllText(runtimeMain, "int main3();");
WriteFakeNativeIndex(root, "\"a\"");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "src", "main.cpp"),
"int main3();");
var afterNewRuntime = ToolkitFingerprint.ComputeComponents(root);
if (afterNewRuntime.Translation != indexed.Translation)
throw new Exception("A runtime source change with unchanged registrations changed the translation identity.");
if (afterNewRuntime.NativeToolchain != indexed.NativeToolchain)
if (afterNewRuntime.Translation == afterNewTranslator.Translation)
throw new Exception("A runtime source change did not change the translation identity.");
if (afterNewRuntime.NativeToolchain != afterNewTranslator.NativeToolchain)
throw new Exception("A runtime source change needlessly changed the native toolchain identity.");
if (afterNewRuntime.Compile == indexed.Compile)
if (afterNewRuntime.Compile == afterNewTranslator.Compile)
throw new Exception("A runtime source change did not change the compile identity.");
File.WriteAllText(runtimeMain, "int main4();");
if (ToolkitFingerprint.ComputeComponents(root).Translation == afterNewRuntime.Translation)
throw new Exception("A stale native index was trusted for the translation identity.");
File.WriteAllText(runtimeMain, "int main3();");
WriteFakeNativeIndex(root, "\"a\", \"b\"");
afterNewRuntime = ToolkitFingerprint.ComputeComponents(root);
if (afterNewRuntime.Translation == indexed.Translation)
throw new Exception("A native registration change did not change the translation identity.");
if (afterNewRuntime.NativeToolchain != indexed.NativeToolchain)
throw new Exception("A native registration change needlessly changed the native toolchain identity.");
File.WriteAllText(Path.Combine(root, "Toolkit", "llvm-mingw", "bin", "clang-22.exe"),
"clang-v2");
var afterNewCompiler = ToolkitFingerprint.ComputeComponents(root);
@@ -1235,26 +1148,6 @@ internal static class SelfTests
}
}
/// <summary>Writes a native index current for the fake runtime/src; returns its source digest.</summary>
private static string WriteFakeNativeIndex(string root, string registrations)
{
var source = Path.Combine(root, "BuildWorkspace", "runtime", "src");
var manifest = new System.Text.StringBuilder();
foreach (var (relative, path) in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories)
.Where(path => path.EndsWith(".cpp", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".h", StringComparison.OrdinalIgnoreCase) ||
path.EndsWith(".hpp", StringComparison.OrdinalIgnoreCase))
.Select(path => (Path.GetRelativePath(source, path).Replace('\\', '/'), path))
.OrderBy(item => item.Item1, StringComparer.Ordinal))
manifest.Append(InputValidation.Sha256File(path)).Append(" ").Append(relative).Append('\n');
var digest = Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(manifest.ToString())))
.ToLowerInvariant();
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "native-index.json"),
"{\n \"format\": \"wiicompiled-runtime-native-index\",\n \"sourceDigest\": \"" + digest +
"\",\n \"registrations\": [" + registrations + "]\n}\n");
return digest;
}
private static void CreateFakeToolkit(string root)
{
Directory.CreateDirectory(Path.Combine(root, "Toolkit", "Translator"));
@@ -1,7 +1,5 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
@@ -14,10 +12,9 @@ namespace WiiCompiled.Setup.Windows;
internal static class ToolkitFingerprint
{
// v6/native-toolchain v2 forces one clean rebuild past a Ninja stale-object bug.
// translation v2 added runtime/src, which the translator regex-scans for native overrides;
// v3 replaced it with the native index extracted from it, whenever that index is current.
// translation v2 added runtime/src, which the translator regex-scans for native overrides.
private const string Version = "mkwc-toolkit-compile-v6";
private const string TranslationVersion = "mkwc-toolkit-translation-v3";
private const string TranslationVersion = "mkwc-toolkit-translation-v2";
private const string NativeToolchainVersion = "mkwc-toolkit-native-toolchain-v2";
private const string PackageVersion = "mkwc-toolkit-package-v1";
private const string RuntimeAssetsVersion = "mkwc-product-runtime-assets-v3";
@@ -28,25 +25,20 @@ internal static class ToolkitFingerprint
".cmake", ".txt", ".yml", ".yaml", ".toml", ".json", ".ps1", ".in", ".def", ".patch"];
/// <summary>
/// Inputs that decide translator output: translator, translation project, the native override
/// registrations/effect contracts it extracts from runtime/src (see
/// <see cref="AddNativeRegistrations"/>), and the build script's translator command lines.
/// Unchanged means the completed base translation can be reused instead of re-run.
/// Inputs that decide translator output: translator, translation project, runtime sources it
/// scans for native override registrations/effect contracts, and the build script's translator
/// command lines. Unchanged means the completed base translation can be reused instead of re-run.
/// </summary>
private static readonly string[] TranslationPrefixes =
[
InstalledLayout.ToolkitEntryPrefix + "Translator/",
InstalledLayout.WorkspaceEntryPrefix + "projects/"
InstalledLayout.WorkspaceEntryPrefix + "projects/",
InstalledLayout.WorkspaceEntryPrefix + "runtime/src/"
];
private static readonly string[] TranslationFiles =
[InstalledLayout.WorkspaceEntryPrefix + "LocalBuild.ps1"];
private const string NativeSourcePrefix = InstalledLayout.WorkspaceEntryPrefix + "runtime/src/";
private const string NativeIndexEntry = InstalledLayout.WorkspaceEntryPrefix + "runtime/native-index.json";
// The file set the translator's native index source digest covers.
private static readonly string[] NativeIndexSourceExtensions = [".cpp", ".h", ".hpp"];
/// <summary>
/// Inputs that make compiled objects unsafe to reuse: the compiler/CMake/Ninja toolchain, the
/// shipped runtime DLLs, and the scripts owning configure flags. Source/dependency changes are
@@ -116,16 +108,14 @@ internal static class ToolkitFingerprint
if (entries.Count == 0)
throw new InvalidDataException($"No toolkit files were found under {root}; the installation is incomplete.");
var translation = SelectSubset(entries, TranslationPrefixes, TranslationFiles);
AddNativeRegistrations(translation, entries, root);
return new ToolkitFingerprintComponents(
BuildIdentity(Version, entries),
BuildIdentity(TranslationVersion, translation),
BuildIdentity(NativeToolchainVersion,
SelectSubset(entries, NativeToolchainPrefixes, NativeToolchainFiles)));
BuildSubsetIdentity(TranslationVersion, entries, TranslationPrefixes, TranslationFiles),
BuildSubsetIdentity(NativeToolchainVersion, entries, NativeToolchainPrefixes,
NativeToolchainFiles));
}
private static SortedDictionary<string, string> SelectSubset(SortedDictionary<string, string> entries,
private static string BuildSubsetIdentity(string version, SortedDictionary<string, string> entries,
string[] prefixes, string[] files)
{
var subset = new SortedDictionary<string, string>(StringComparer.Ordinal);
@@ -135,61 +125,9 @@ internal static class ToolkitFingerprint
files.Any(file => relative.Equals(file, StringComparison.Ordinal)))
subset[relative] = hash;
}
return subset;
return BuildIdentity(version, subset);
}
/// <summary>
/// A native-index.json extracted from exactly the shipped runtime/src stands in for those
/// sources, minus its source digest, so a runtime change that leaves the registrations alone
/// keeps the base translation. A missing, unreadable or stale index keeps the sources instead.
/// </summary>
private static void AddNativeRegistrations(SortedDictionary<string, string> subset,
SortedDictionary<string, string> entries, string root)
{
var sources = entries.Where(entry => entry.Key.StartsWith(NativeSourcePrefix, StringComparison.Ordinal))
.ToList();
var registrations = TryHashCurrentNativeIndex(
Path.Combine(InstalledLayout.Workspace(root), "runtime", "native-index.json"), sources);
if (registrations is not null)
{
subset[NativeIndexEntry] = registrations;
return;
}
foreach (var (relative, hash) in sources) subset[relative] = hash;
}
private static string? TryHashCurrentNativeIndex(string path, List<KeyValuePair<string, string>> sources)
{
if (!File.Exists(path)) return null;
JsonObject? document;
try { document = JsonNode.Parse(File.ReadAllText(path)) as JsonObject; }
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or JsonException)
{
return null;
}
if (document?["sourceDigest"] is not JsonValue recorded || !recorded.TryGetValue<string>(out var digest))
return null;
// Mirrors the translator's RuntimeNativeIndexFile.ComputeSourceDigest.
var manifest = new StringBuilder();
foreach (var (relative, hash) in sources)
{
if (!NativeIndexSourceExtensions.Any(extension =>
relative.EndsWith(extension, StringComparison.OrdinalIgnoreCase)))
continue;
manifest.Append(hash).Append(" ").Append(relative, NativeSourcePrefix.Length,
relative.Length - NativeSourcePrefix.Length).Append('\n');
}
if (!digest.Equals(Sha256Hex(manifest.ToString()), StringComparison.OrdinalIgnoreCase))
return null;
document.Remove("sourceDigest");
return Sha256Hex(document.ToJsonString());
}
private static string Sha256Hex(string text) =>
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(text))).ToLowerInvariant();
/// <summary>
/// Content identity of the source-owned files copied verbatim beside every product. This is
/// intentionally independent of <see cref="Compute"/>: an asset-only release is published
@@ -335,7 +273,8 @@ internal static class ToolkitFingerprint
var builder = new StringBuilder(version).Append('\n');
foreach (var (relative, hash) in entries)
builder.Append(relative).Append('|').Append(hash).Append('\n');
return Sha256Hex(builder.ToString());
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(builder.ToString())))
.ToLowerInvariant();
}
}
@@ -7,6 +7,7 @@
<AssemblyName>WiiCompiled.Setup</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.2.32</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled</Description>
@@ -15,6 +16,5 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
<Using Include="WiiCompiled.Setup.Common" />
</ItemGroup>
</Project>
+11 -33
View File
@@ -127,10 +127,10 @@ mkdir -p "$appdir/usr/toolchain"
cp -a "$workspace/Launcher/artifacts/portable-tools/toolchain-$appimagetool_arch"/. "$appdir/usr/toolchain/"
# 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), nor on x86_64
# the game-independent runtime. Re-harvesting recompiles that 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 the current compiler, flags, source trees or mbed TLS pin.
# 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
# --print-fingerprint-only (a fast, build-free check) says the existing package no longer matches
# the current compiler/flags/aurora/third_party sources.
native_prebuilt_dir="$workspace/Launcher/artifacts/native-prebuilt-$appimagetool_arch"
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)
@@ -148,9 +148,6 @@ fields = {
"flag_fingerprint": "FlagFingerprint",
"aurora_fingerprint": "AuroraSourceFingerprint",
"third_party_fingerprint": "ThirdPartySourceFingerprint",
"mbedtls_fingerprint": "MbedTlsFingerprint",
# Only printed on x86_64, so a package without the runtime archive is stale there.
"runtime_fingerprint": "RuntimeSourceFingerprint",
}
print(1 if all(provenance.get(v) == current.get(k) for k, v in fields.items()) else 0)
PY
@@ -176,29 +173,7 @@ done
# is a plain developer build directory.
find "$appdir/workspace/aurora-main/extern" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} +
rm -rf "$appdir/workspace/runtime/build"
cp "$workspace/Launcher/local-build.sh" "$workspace/Launcher/build-cache.sh" "$appdir/workspace/Launcher/"
# The translation identity (build-cache.sh) covers the runtime's native registrations through this
# index, so it is extracted from exactly the bundled runtime sources.
echo "Extracting the runtime native index..."
"$appdir/usr/bin/translator-cli" emit-native-index \
--project "$appdir/workspace/projects/mkwii/recomp.yml" \
--out "$appdir/workspace/runtime/native-index.json"
[[ -f "$appdir/workspace/runtime/native-index.json" ]] || { echo "build-appimage.sh: the native index was not written" >&2; exit 1; }
# A bundled runtime that fingerprints differently from the shipped archive would make every
# installation silently compile the runtime from source.
if [[ "$appimagetool_arch" == x86_64 ]]; then
staged_runtime_fingerprint=$("$workspace/Launcher/artifacts/portable-tools/toolchain-$appimagetool_arch/bin/cmake" \
-DMKW_RUNTIME_DIR="$appdir/workspace/runtime" -P "$appdir/workspace/runtime/cmake/RuntimeFingerprint.cmake" |
sed -n 's/^runtime_fingerprint=//p')
packaged_runtime_fingerprint=$(python3 -c 'import json, sys; print(json.load(open(sys.argv[1], encoding="utf-8")).get("RuntimeSourceFingerprint", ""))' \
"$appdir/native-prebuilt/provenance.json")
if [[ -z "$staged_runtime_fingerprint" || "$staged_runtime_fingerprint" != "$packaged_runtime_fingerprint" ]]; then
echo "build-appimage.sh: the bundled runtime ($staged_runtime_fingerprint) does not match the precompiled runtime archive ($packaged_runtime_fingerprint)" >&2
exit 1
fi
fi
cp "$workspace/Launcher/local-build.sh" "$appdir/workspace/Launcher/local-build.sh"
# AppRun re-syncs runtime/aurora-main/projects/local-build.sh into the writable cache only when
# this changes, so it must change whenever any of those bundled paths actually did - a bare commit
@@ -228,9 +203,12 @@ CACHE="${XDG_DATA_HOME:-$HOME/.local/share}/WiiCompiled/workspace"
mkdir -p "$CACHE"
if [ ! -f "$CACHE/.bundle-version" ] || \
[ "$(cat "$HERE/workspace/.bundle-version")" != "$(cat "$CACHE/.bundle-version")" ]; then
# Unchanged files keep their timestamps so the incremental build survives the update.
source "$HERE/workspace/Launcher/build-cache.sh"
mkw_sync_tree "$HERE/workspace" "$CACHE" runtime aurora-main projects Launcher >&2
mkdir -p "$CACHE/Launcher"
for dir in runtime aurora-main projects; do
rm -rf "$CACHE/$dir"
cp -r "$HERE/workspace/$dir" "$CACHE/$dir"
done
cp "$HERE/workspace/Launcher/local-build.sh" "$CACHE/Launcher/local-build.sh"
cp "$HERE/workspace/.bundle-version" "$CACHE/.bundle-version"
fi
# toolchain/ and native-prebuilt/ are NOT copied into the cache (they're large - ~500 MiB /
-148
View File
@@ -1,148 +0,0 @@
# Shared incremental-build logic for the Unix setups (Linux AppImage and macOS), sourced by
# local-build.sh, local-build-macos.command, the AppImage's AppRun and macos/setup.command.
# Mirrors the Windows installer's translation/toolchain identities and timestamp preservation.
# Must stay compatible with macOS's bash 3.2.
if command -v sha256sum >/dev/null 2>&1; then
mkw_sha256_cmd=(sha256sum)
else
mkw_sha256_cmd=(shasum -a 256)
fi
mkw_sha256() {
"${mkw_sha256_cmd[@]}" "$@" | awk '{print $1}'
}
# Prints "<sha> <relative path>" for every file under each directory, relative to $1.
mkw_digest_tree() {
local root=$1; shift
(
cd "$root" || exit 1
local dir
for dir in "$@"; do
if [[ -d "$dir" ]]; then
printf 'tree %s\n' "$dir"
find "$dir" -type f -print0 | LC_ALL=C sort -z | xargs -0 "${mkw_sha256_cmd[@]}" /dev/null
else
printf 'absent %s\n' "$dir"
fi
done
)
}
# Content-only digest of a file or directory, independent of where it lives (the AppImage mount
# path changes every launch).
mkw_digest_path() {
if [[ -d "$1" ]]; then
mkw_digest_tree "$1" . | mkw_sha256
elif [[ -f "$1" ]]; then
mkw_sha256 < "$1"
else
printf 'absent\n'
fi
}
# Same digest as the translator's RuntimeNativeIndexFile.ComputeSourceDigest. $1 = runtime/src.
mkw_native_source_digest() {
(
cd "$1" || exit 1
find . -type f \( -iname '*.cpp' -o -iname '*.h' -o -iname '*.hpp' \) -print0 | LC_ALL=C sort -z |
xargs -0 "${mkw_sha256_cmd[@]}" /dev/null
) | grep -v ' /dev/null$' | sed 's| \./| |' | mkw_sha256
}
# The native registrations the translator reads from runtime/src. A current native-index.json
# stands in for the sources (minus its source digest), so runtime edits that leave the
# registrations alone keep the translation; a missing or stale index falls back to the sources.
# $1 = workspace.
mkw_native_index_identity() {
local index="$1/runtime/native-index.json" recorded
recorded=$(mkw_read_identity "$index" sourceDigest)
if [[ -n "$recorded" && -d "$1/runtime/src" && "$recorded" == "$(mkw_native_source_digest "$1/runtime/src")" ]]; then
printf 'native-index %s\n' "$(grep -v '^[[:space:]]*"sourceDigest"[[:space:]]*:' "$index" | mkw_sha256)"
else
mkw_digest_tree "$1" runtime/src
fi
}
# Everything the base translation depends on: game inputs, project, native registrations, the
# translator itself and the build script that drives it.
# $1 = workspace, $2 = translator file or directory, $3 = build script.
mkw_translation_identity() {
local workspace=$1 translator=$2 script=$3
{
printf 'mkw-translation-v2\n'
printf 'dol %s\n' "$(mkw_digest_path "$workspace/Assets/main.dol")"
printf 'rel %s\n' "$(mkw_digest_path "$workspace/Assets/StaticR.rel")"
printf 'translator %s\n' "$(mkw_digest_path "$translator")"
printf 'script %s\n' "$(mkw_digest_path "$script")"
mkw_digest_tree "$workspace" projects
mkw_native_index_identity "$workspace"
} | mkw_sha256
}
# Inputs that make compiled objects unsafe to reuse: compiler/CMake/Ninja versions and the
# from-source dependency pins. Source changes are excluded; Ninja rebuilds exactly what they touch.
# $1 = workspace, remaining = tools (C compiler, C++ compiler, cmake, ninja).
mkw_toolchain_identity() {
local workspace=$1; shift
local tool
{
printf 'mkw-toolchain-v2\n'
for tool in "$@"; do
# First line only: clang appends its InstalledDir, which moves with the AppImage mount.
printf '%s\n' "$("$tool" --version 2>/dev/null | head -n 1)"
done
mkw_digest_tree "$workspace" aurora-main/extern aurora-main/cmake
printf 'aurora %s\n' "$(mkw_digest_path "$workspace/aurora-main/CMakeLists.txt")"
printf 'mbedtls %s\n' "$(mkw_digest_path "$workspace/runtime/cmake/MbedTLSPin.cmake")"
} | mkw_sha256
}
# $1 = JSON file, $2 = key. Prints the recorded string value, or nothing.
mkw_read_identity() {
[[ -f "$1" ]] || return 0
grep -o "\"$2\" *: *\"[^\"]*\"" "$1" 2>/dev/null | sed 's/.*"\([^"]*\)"$/\1/' || true
}
mkw_write_identity() {
printf '{"SchemaVersion":1,"%s":"%s"}\n' "$2" "$3" > "$1"
}
# Mirrors each named directory from $1 into $2. Unchanged files are left untouched so their
# timestamps (which clang's PCH validation and Ninja compare) survive; changed and new files are
# copied fresh and files no longer shipped are removed.
mkw_sync_tree() {
local source=$1 destination=$2; shift 2
local dir relative changed=0 unchanged=0 removed=0
for dir in "$@"; do
if [[ ! -d "$source/$dir" ]]; then
rm -rf "$destination/$dir"
continue
fi
if [[ -e "$destination/$dir" && ! -d "$destination/$dir" ]]; then
rm -f "$destination/$dir"
fi
mkdir -p "$destination/$dir"
while IFS= read -r -d '' relative; do
relative=${relative#./}
if [[ -d "$source/$dir/$relative" ]]; then
[[ -d "$destination/$dir/$relative" ]] || { rm -f "$destination/$dir/$relative"; mkdir -p "$destination/$dir/$relative"; }
elif [[ -f "$destination/$dir/$relative" ]] && cmp -s "$source/$dir/$relative" "$destination/$dir/$relative"; then
unchanged=$((unchanged + 1))
else
rm -rf "$destination/$dir/$relative"
cp "$source/$dir/$relative" "$destination/$dir/$relative"
changed=$((changed + 1))
fi
done < <(cd "$source/$dir" && find . -mindepth 1 -print0)
while IFS= read -r -d '' relative; do
relative=${relative#./}
if [[ ! -e "$source/$dir/$relative" && ( -e "$destination/$dir/$relative" || -L "$destination/$dir/$relative" ) ]]; then
[[ -d "$destination/$dir/$relative" ]] || removed=$((removed + 1))
rm -rf "$destination/$dir/$relative"
fi
done < <(cd "$destination/$dir" && find . -mindepth 1 -depth -print0)
done
printf 'Workspace refreshed: %d changed, %d unchanged, %d removed file(s).\n' "$changed" "$unchanged" "$removed"
}
+12 -52
View File
@@ -4,12 +4,11 @@ set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
default_workspace=$(cd "$script_dir/.." && pwd)
# shellcheck source=build-cache.sh
source "$script_dir/build-cache.sh"
fail() { printf 'local-build-macos.command: error: %s\n' "$*" >&2; exit 1; }
step() { printf 'MKWCBUILD:STEP:%s %s\n' "$1" "$2"; }
assert_file() { [[ -f "$1" ]] || fail "$2 is missing: $1"; }
sha256() { shasum -a 256 "$1" | awk '{ print $1 }'; }
usage() {
cat <<'EOF'
@@ -23,20 +22,17 @@ Usage: local-build-macos.command --output-dir DIR [options]
--retro-rewind-package-dir DIR RetroRewind6 directory (required for Retro Rewind)
--retro-wfc-offline-dir DIR Directory containing binary/payload.RMCPD00.bin
--skip-retro-wfc-payload Build Retro Rewind without the shared Retro-WFC payload
--force-clean-build Delete local generated and current-architecture native build caches
--force-clean-build Delete local generated and native-build-macos caches
--parallel N Pin translation and build parallelism
--cmake PATH --ninja PATH Override build tools
--dotnet PATH Override dotnet
--translator-bin PATH Use a self-contained Translator.Cli executable
--setup-version VERSION Version recorded in the runtime log (default: <Version> in
Launcher/Directory.Build.props when present)
EOF
}
workspace="$default_workspace"; profile=base; output_dir=""; base_output_dir=""
game=""; nodtool=""; retro_root=""; retro_wfc=""; skip_retro_wfc=0; force_clean=0
parallel=0; cmake_bin=cmake; ninja_bin=ninja; dotnet_bin=dotnet; translator_bin=""
setup_version=""
while (($#)); do
case "$1" in
--workspace) workspace=${2:-}; shift 2 ;;
@@ -54,21 +50,14 @@ while (($#)); do
--ninja) ninja_bin=${2:-}; shift 2 ;;
--dotnet) dotnet_bin=${2:-}; shift 2 ;;
--translator-bin) translator_bin=${2:-}; shift 2 ;;
--setup-version) setup_version=${2:-}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown option: $1" ;;
esac
done
[[ $(uname -s) == Darwin ]] || fail 'this build script is for macOS only'
macos_arch=$(uname -m)
case "$macos_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $macos_arch" ;; esac
macos_deployment_target=12.0
[[ $(uname -m) == arm64 ]] || fail 'the current macOS product target is Apple Silicon only'
workspace=$(cd "$workspace" && pwd)
if [[ -z "$setup_version" && -f "$workspace/Launcher/Directory.Build.props" ]]; then
setup_version=$(sed -nE 's/.*<Version>([^<]+)<\/Version>.*/\1/p' "$workspace/Launcher/Directory.Build.props" | head -n 1)
fi
[[ "$setup_version" =~ ^[0-9A-Za-z.+-]*$ ]] || fail '--setup-version contains unsupported characters'
[[ -n "$output_dir" ]] || fail '--output-dir is required'
case "$profile" in base|retro-rewind|both) ;; *) fail '--profile must be base, retro-rewind, or both' ;; esac
builds_retro=0; [[ "$profile" != base ]] && builds_retro=1
@@ -84,8 +73,7 @@ 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"
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-$macos_arch"
manifest="$manifest_dir/mkwii_base_manifest.json"; shards="$generated/build_shards"; native_build="$workspace/native-build-macos"
assert_file "$project" 'translation project'
if [[ -n "$game" ]]; then "$script_dir/macos/extract-disc.command" --game "$game" --assets-dir "$assets" --nodtool "$nodtool"; fi
assert_file "$assets/main.dol" 'extracted main.dol'; assert_file "$assets/StaticR.rel" 'extracted StaticR.rel'
@@ -127,30 +115,11 @@ if (( builds_retro )); then
fi
fi
translation_provenance="$generated/translation-provenance.json"
translation_fingerprint=$(mkw_translation_identity "$workspace" "${translator_bin:-$(dirname "${translator_dll:-}")}" "$script_dir/local-build-macos.command")
reuse_base=0
if [[ "$(mkw_read_identity "$translation_provenance" TranslationFingerprint)" == "$translation_fingerprint" && -f "$metadata" && -f "$manifest" ]]; then
reuse_base=1
fi
if (( reuse_base && builds_retro )); then
# A base tree that never saw this Code.pul would bake vanilla code into the modded product.
pul_sha=$(mkw_sha256 "$retro_root/Binaries/Code.pul")
if ! grep -q "\"codePulSha256\":\"$pul_sha\"" "$metadata" && \
! translator check-base-mod-awareness --project "$project" --profile retro-rewind --translation-output-metadata "$metadata" --code-pul "$retro_root/Binaries/Code.pul"; then
reuse_base=0
fi
fi
if (( reuse_base )); then
step reuse-base-translation 'Reusing the completed base translation'
else
rm -f "$translation_provenance"; mkdir -p "$functions" "$manifest_dir"
step translate-base 'Translating the user-owned base game'
translator translate-recursive "$entry_point" --project "$project" --outdir "$functions" --output-metadata "$metadata" --production-source-bundle "$generated/base_translation_sources.bin" --no-function-files --prune-stale --threads "$translator_threads"
step emit-base-manifest 'Creating the local base translation manifest'
translator emit-base-manifest --project "$project" --out "$manifest_dir" --functions-dir "$functions" --translation-output-metadata "$metadata" --region P
mkw_write_identity "$translation_provenance" TranslationFingerprint "$translation_fingerprint"
fi
step translate-base 'Translating the user-owned base game'
rm -rf "$functions" "$metadata" "$manifest_dir"; mkdir -p "$functions" "$manifest_dir"
translator translate-recursive "$entry_point" --project "$project" --outdir "$functions" --output-metadata "$metadata" --production-source-bundle "$generated/base_translation_sources.bin" --no-function-files --prune-stale --threads "$translator_threads"
step emit-base-manifest 'Creating the local base translation manifest'
translator emit-base-manifest --project "$project" --out "$manifest_dir" --functions-dir "$functions" --translation-output-metadata "$metadata" --region P
if (( builds_retro )); then
mod_out="$workspace/build/mods/retro_rewind_full_cpp"; args=(translate-mod --project "$project" --profile retro-rewind --base-manifest "$manifest" --base-translation-output-metadata "$metadata" --code-pul "$retro_root/Binaries/Code.pul" --mod-root "$retro_root" --mod-name 'Retro Rewind' --region P --out "$mod_out" --prefer-cached-inputs --emit-cpp --threads "$translator_threads")
if (( skip_retro_wfc )); then args+=(--skip-retro-wfc); else offline_payload="$retro_wfc/binary/payload.RMCPD00.bin"; assert_file "$offline_payload" 'Offline Retro-WFC payload'; args+=(--retro-wfc-payload "$offline_payload"); fi
@@ -161,19 +130,10 @@ args=(emit-build-shards --project "$project" --base-metadata "$metadata" --base-
if (( builds_retro )); then args+=(--resolved-profile "$mod_out/resolved_dispatch_profile.json" --retro-cpp-dir "$mod_out/cpp"); fi
step emit-build-shards 'Preparing native build shards'; translator "${args[@]}"
toolchain_provenance="$native_build/toolchain-provenance.json"
toolchain_fingerprint=$(mkw_toolchain_identity "$workspace" clang clang++ "$cmake_bin" "$ninja_bin")
if [[ -d "$native_build" && "$(mkw_read_identity "$toolchain_provenance" ToolchainFingerprint)" != "$toolchain_fingerprint" ]]; then
printf 'MKWCBUILD: The toolchain or a pinned dependency changed; rebuilding the native build from scratch\n'
rm -rf "$native_build"
fi
step configure-native 'Configuring the native toolchain'
# 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 -DMKW_SETUP_VERSION="$setup_version"
mkw_write_identity "$toolchain_provenance" ToolchainFingerprint "$toolchain_fingerprint"
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs"
targets=(); [[ "$profile" != retro-rewind ]] && targets+=(WiiCompiled); [[ "$profile" != base ]] && targets+=(RetroRewind)
step compile "Compiling ${targets[*]} locally"; "$cmake_bin" --build "$native_build" --target "${targets[@]}" --parallel "$global_jobs"
if [[ "$profile" != retro-rewind ]]; then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product WiiCompiled --output-dir "${base_output_dir:-$output_dir}" --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" --architecture "$macos_arch" --minimum-system-version "$macos_deployment_target"; 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}"; fi
if (( builds_retro )); then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product RetroRewind --output-dir "$output_dir"; fi
printf 'MKWCBUILD:OUTPUT=%s\n' "$output_dir"
+15 -34
View File
@@ -39,7 +39,7 @@ require_command() {
}
sha256_of() {
mkw_sha256 "$1"
sha256sum "$1" | awk '{print $1}'
}
# ---------------------------------------------------------------------------
@@ -47,8 +47,6 @@ sha256_of() {
# ---------------------------------------------------------------------------
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=build-cache.sh
source "$script_dir/build-cache.sh"
workspace=$(cd "$script_dir/.." && pwd)
profile=base
output_dir=""
@@ -68,7 +66,6 @@ translator_bin_override=""
fuse_ld_override=""
native_prebuilt_dir=""
sysroot=""
setup_version=""
usage() {
cat <<'EOF'
@@ -93,8 +90,6 @@ Usage: local-build.sh --output-dir DIR [options]
skips compiling aurora-main from source entirely
--sysroot PATH Passed to CMake as -DCMAKE_SYSROOT: where the compiler resolves
standard headers/startup files
--setup-version VERSION Version recorded in the runtime log (default: <Version> in
Launcher/Directory.Build.props when present)
EOF
}
@@ -119,17 +114,11 @@ while [[ $# -gt 0 ]]; do
--translator-bin) translator_bin_override=$2; shift 2 ;;
--native-prebuilt-dir) native_prebuilt_dir=$2; shift 2 ;;
--sysroot) sysroot=$2; shift 2 ;;
--setup-version) setup_version=$2; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown argument: $1" ;;
esac
done
if [[ -z "$setup_version" && -f "$workspace/Launcher/Directory.Build.props" ]]; then
setup_version=$(sed -nE 's/.*<Version>([^<]+)<\/Version>.*/\1/p' "$workspace/Launcher/Directory.Build.props" | head -n 1)
fi
[[ "$setup_version" =~ ^[0-9A-Za-z.+-]*$ ]] || fail "--setup-version contains unsupported characters"
[[ -n "$output_dir" ]] || { usage; fail "--output-dir is required"; }
case "$profile" in
base|retro-rewind|both) ;;
@@ -277,9 +266,10 @@ else
fi
# ---------------------------------------------------------------------------
# Translation cache: this script is the only owner of the reuse decision. The identity (see
# build-cache.sh) covers the game inputs, project, native registrations, translator and this script,
# matching the Windows installer's translation identity.
# Translation cache: this script is the only owner of the reuse decision (unlike LocalBuild.ps1,
# which is handed caller-computed fingerprints by the Windows installer - there is no Linux
# installer yet to supply anything). Hash the game inputs the translation actually depends on;
# a match plus every expected output file present means the previous translation is still good.
# ---------------------------------------------------------------------------
if (( force_clean_build )); then
@@ -287,12 +277,13 @@ if (( force_clean_build )); then
rm -rf "$generated" "$base_manifest_dir" "$build"
fi
translator_identity_input=${translator_bin:-$(dirname "$translator_dll")}
translation_fingerprint=$(mkw_translation_identity "$workspace" "$translator_identity_input" "$script_dir/local-build.sh")
translation_fingerprint=$(cat "$assets/main.dol" "$assets/StaticR.rel" "$project" | sha256sum | awk '{print $1}')
reuse_base=0
if [[ "$(mkw_read_identity "$translation_provenance" TranslationFingerprint)" == "$translation_fingerprint" \
&& -f "$base_metadata" && -f "$base_manifest" ]]; then
reuse_base=1
if [[ -f "$translation_provenance" ]]; then
recorded=$(grep -o '"TranslationFingerprint" *: *"[^"]*"' "$translation_provenance" 2>/dev/null | sed 's/.*"\([0-9a-f]*\)"$/\1/' || true)
if [[ "$recorded" == "$translation_fingerprint" && -f "$base_metadata" && -f "$base_manifest" ]]; then
reuse_base=1
fi
fi
if (( builds_retro )); then
@@ -340,7 +331,8 @@ else
translator emit-base-manifest --project "$project" --out "$base_manifest_dir" \
--functions-dir "$functions" --translation-output-metadata "$base_metadata" --region P
mkw_write_identity "$translation_provenance" TranslationFingerprint "$translation_fingerprint"
printf '{"SchemaVersion":1,"TranslationFingerprint":"%s"}' "$translation_fingerprint" \
> "$translation_provenance"
fi
if (( builds_retro )); then
@@ -415,13 +407,6 @@ if [[ -f "$build/CMakeCache.txt" ]]; then
done < <(grep -o ':FILEPATH=.*' "$build/CMakeCache.txt" | sed 's/^:FILEPATH=//')
fi
fi
# The AppImage keeps tool paths stable across updates, so a changed compiler is only visible here.
toolchain_fingerprint=$(mkw_toolchain_identity "$workspace" "$cc_bin" "$cxx_bin" "$cmake_bin" "$ninja_bin")
if [[ "$keep_native_build" -eq 1 && \
"$(mkw_read_identity "$toolchain_provenance" ToolchainFingerprint)" != "$toolchain_fingerprint" ]]; then
keep_native_build=0
stale_reason=" (the toolchain or a pinned dependency changed)"
fi
if [[ -d "$build" && "$keep_native_build" -eq 0 ]]; then
echo "MKWCBUILD: The native build cache does not belong to this workspace path or toolchain; rebuilding from scratch$stale_reason"
rm -rf "$build"
@@ -433,8 +418,7 @@ configure_args=(-S "$workspace/runtime" -B "$build" -G Ninja
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_C_COMPILER="$cc_bin" -DCMAKE_CXX_COMPILER="$cxx_bin"
-DCMAKE_MAKE_PROGRAM="$ninja_bin"
-DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs"
-DMKW_SETUP_VERSION="$setup_version")
-DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs")
# CMAKE_C_COMPILER/CXX_COMPILER stay stable across AppImage runs on their own (they're exactly
# what's passed via -D above, and AppRun points --cc/--cxx at a symlink it re-targets at the
# current mount every launch - see build-appimage.sh). CMAKE_AR/RANLIB/LINKER/ASM_COMPILER do NOT
@@ -453,9 +437,7 @@ if [[ "$cc_bin" == */* ]]; then
configure_args+=(-DCMAKE_ASM_COMPILER="$cc_bin")
fi
if [[ -n "$fuse_ld_override" ]]; then
# CMAKE_EXE_LINKER_FLAGS is one string that CMake pastes into the shell link command.
printf -v fuse_ld_quoted '%q' "$fuse_ld_override"
configure_args+=(-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=$fuse_ld_quoted")
configure_args+=(-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=$fuse_ld_override")
fi
if [[ -n "$native_prebuilt_dir" ]]; then
configure_args+=(-DMKW_NATIVE_PREBUILT_DIR="$native_prebuilt_dir")
@@ -471,7 +453,6 @@ fi
log_step configure-native "Configuring the native toolchain"
"$cmake_bin" "${configure_args[@]}"
mkw_write_identity "$toolchain_provenance" ToolchainFingerprint "$toolchain_fingerprint"
case "$profile" in
base) targets=(WiiCompiled) ;;
+20 -86
View File
@@ -9,97 +9,45 @@ fail() { printf 'build-setup-pkg.command: error: %s\n' "$*" >&2; exit 1; }
copy_clean() { DITTONORSRC=1 ditto --norsrc --noqtn "$@"; }
usage() {
cat <<'EOF'
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]
Usage: build-setup-pkg.command --nodtool PATH --translator PATH --cmake-root DIR --ninja PATH --output PKG [options]
Creates a game-code-free WiiCompiled Setup.pkg. The supplied tools must be
maintainer-verified, redistributable macOS artifacts for both arm64 and
x86_64. The setup package selects native tools for its host while the game is
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.
maintainer-verified, redistributable macOS arm64 artifacts. The resulting pkg
is unsigned unless --installer-identity is supplied; releases should sign and
notarize it with a Developer ID Installer certificate.
--workspace DIR Repository root (default: script's grandparent)
--version VERSION Bundle/package version (default: project version, or 0.1.0)
--version VERSION Bundle/package version (default: 0.1.0)
--installer-identity NAME Developer ID Installer identity for productbuild
EOF
}
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace=$(cd "$script_dir/../.." && pwd); nodtool_arm64=""; nodtool_x86_64=""; translator_arm64=""; translator_x86_64=""; cmake_root=""; ninja_arm64=""; ninja_x86_64=""; output=""; version=0.1.0; identity=""
version_supplied=false
workspace=$(cd "$script_dir/../.." && pwd); nodtool=""; translator=""; cmake_root=""; ninja=""; output=""; version=0.1.0; identity=""
while (($#)); do
case "$1" in
--workspace) workspace=${2:-}; shift 2 ;;
--nodtool-arm64) nodtool_arm64=${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 ;;
--nodtool) nodtool=${2:-}; shift 2 ;;
--translator) translator=${2:-}; shift 2 ;;
--cmake-root) cmake_root=${2:-}; shift 2 ;;
--ninja-arm64) ninja_arm64=${2:-}; shift 2 ;;
--ninja-x86_64) ninja_x86_64=${2:-}; shift 2 ;;
--ninja) ninja=${2:-}; shift 2 ;;
--output) output=${2:-}; shift 2 ;;
--version) version=${2:-}; version_supplied=true; shift 2 ;;
--version) version=${2:-}; shift 2 ;;
--installer-identity) identity=${2:-}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown option: $1" ;;
esac
done
version=${version#v}
if [[ "$version_supplied" == false ]]; 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'
IFS=. read -r version_major version_minor version_patch <<< "$version"
short_version="$version_major.${version_minor:-0}.${version_patch:-0}"
for tool in pkgbuild productbuild ditto codesign lipo; do command -v "$tool" >/dev/null || fail "required macOS tool unavailable: $tool"; done
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
for tool in pkgbuild productbuild ditto codesign; do command -v "$tool" >/dev/null || fail "required macOS tool unavailable: $tool"; done
[[ -x "$nodtool" ]] || fail '--nodtool must name an executable'
[[ -x "$translator" ]] || fail '--translator must name an executable'
[[ -x "$cmake_root/bin/cmake" ]] || fail '--cmake-root must contain bin/cmake'
require_arch() {
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
[[ -x "$ninja" ]] || fail '--ninja must name an executable'
"$nodtool" --version >/dev/null || fail '--nodtool did not run successfully'
workspace=$(cd "$workspace" && pwd); output=$(cd "$(dirname "$output")" && pwd)/$(basename "$output")
stage=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-pkg.XXXXXX")
trap 'rm -rf "$stage"' EXIT
@@ -116,7 +64,7 @@ cat > "$app/Contents/Info.plist" <<EOF
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>$short_version</string>
<key>CFBundleVersion</key><string>$version</string>
<key>LSMinimumSystemVersion</key><string>12.0</string>
<key>LSMinimumSystemVersion</key><string>14.0</string>
</dict></plist>
EOF
cat > "$app/Contents/MacOS/WiiCompiledSetup" <<'EOF'
@@ -150,30 +98,16 @@ for source in aurora-main projects runtime translator; do
[[ -d "$workspace/$source" ]] || fail "required workspace directory is missing: $source"
copy_clean "$workspace/$source" "$resources/workspace/$source"
done
# The translation identity (build-cache.sh) covers the runtime's native registrations through this
# index, so it is extracted from exactly the packaged runtime sources.
if [[ "$host_arch" == arm64 ]]; then host_translator=$translator_arm64; else host_translator=$translator_x86_64; fi
"$host_translator" emit-native-index --project "$resources/workspace/projects/mkwii/recomp.yml" \
--out "$resources/workspace/runtime/native-index.json" || fail 'the runtime native index could not be extracted'
[[ -f "$resources/workspace/runtime/native-index.json" ]] || fail 'the runtime native index was not written'
mkdir -p "$resources/workspace/Launcher/macos"
copy_clean "$workspace/Launcher/Directory.Build.props" "$resources/workspace/Launcher/Directory.Build.props"
copy_clean "$workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/local-build-macos.command"
copy_clean "$workspace/Launcher/build-cache.sh" "$resources/workspace/Launcher/build-cache.sh"
copy_clean "$workspace/Launcher/macos/extract-disc.command" "$resources/workspace/Launcher/macos/extract-disc.command"
copy_clean "$workspace/Launcher/macos/publish-app.command" "$resources/workspace/Launcher/macos/publish-app.command"
chmod +x "$resources/workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/macos/"*.command
# setup.command uses this marker to update source inputs in an existing user
# workspace without replacing extracted game assets or Retro Rewind files.
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"
mkdir -p "$resources/tools/cmake"
copy_clean "$nodtool" "$resources/tools/nodtool"; chmod +x "$resources/tools/nodtool"
copy_clean "$translator" "$resources/tools/Translator.Cli"; chmod +x "$resources/tools/Translator.Cli"
copy_clean "$cmake_root" "$resources/tools/cmake"
copy_clean "$ninja_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 "$ninja" "$resources/tools/ninja"; chmod +x "$resources/tools/ninja"
copy_clean "$workspace/LICENSE" "$resources/LICENSE"
copy_clean "$workspace/THIRD-PARTY-NOTICES.md" "$resources/THIRD-PARTY-NOTICES.md"
codesign --force --deep --sign - "$app"
+4 -18
View File
@@ -38,16 +38,6 @@ done
[[ -x "$nodtool" ]] || fail "nodtool is not executable: $nodtool"
assets_dir=$(mkdir -p "$assets_dir" && cd "$assets_dir" && pwd)
# Sorted "<size> <path>" of every extracted file; recorded at extraction, compared on reuse.
data_manifest() { (cd "$1" && find . -type f -print0 | LC_ALL=C sort -z | xargs -0 stat -f '%z %N'); }
if [[ -d "$assets_dir/DATA/files" && -f "$assets_dir/.data-manifest" && -f "$assets_dir/main.dol" && -f "$assets_dir/StaticR.rel" ]] && \
[[ $(sha256 "$assets_dir/main.dol") == "$EXPECTED_DOL_SHA256" && $(sha256 "$assets_dir/StaticR.rel") == "$EXPECTED_REL_SHA256" ]] && \
[[ "$(data_manifest "$assets_dir/DATA")" == "$(cat "$assets_dir/.data-manifest")" ]]; then
printf 'MKWCBUILD:STEP:disc-ready Reusing the verified extracted game assets\n'
exit 0
fi
scratch=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-disc.XXXXXX")
cleanup() { rm -rf "$scratch"; }
trap cleanup EXIT
@@ -61,10 +51,8 @@ dol=$(find "$scratch/extracted" -type f -path '*/sys/main.dol' -print -quit)
rel=$(find "$scratch/extracted" -type f -path '*/files/rel/StaticR.rel' -print -quit)
[[ -n "$dol" ]] || fail 'nodtool extraction did not contain sys/main.dol'
[[ -n "$rel" ]] || fail 'nodtool extraction did not contain files/rel/StaticR.rel'
dol_sha=$(sha256 "$dol")
rel_sha=$(sha256 "$rel")
[[ $dol_sha == "$EXPECTED_DOL_SHA256" ]] || fail "main.dol does not match the clean PAL RMCP01 image (expected $EXPECTED_DOL_SHA256, got $dol_sha)"
[[ $rel_sha == "$EXPECTED_REL_SHA256" ]] || fail "StaticR.rel does not match the clean PAL RMCP01 image (expected $EXPECTED_REL_SHA256, got $rel_sha)"
[[ $(sha256 "$dol") == "$EXPECTED_DOL_SHA256" ]] || fail 'disc is not the supported clean PAL RMCP01 Mario Kart Wii image'
[[ $(sha256 "$rel") == "$EXPECTED_REL_SHA256" ]] || fail 'disc has an unexpected StaticR.rel; use a clean PAL RMCP01 image'
data_root=$(dirname "$(dirname "$dol")")
[[ -d "$data_root/files" ]] || fail 'nodtool extraction did not contain the Wii files directory'
@@ -76,17 +64,15 @@ mkdir -p "$stage"
ditto "$data_root" "$stage/DATA"
ditto "$dol" "$stage/main.dol"
ditto "$rel" "$stage/StaticR.rel"
data_manifest "$stage/DATA" > "$stage/.data-manifest"
backup="$assets_dir/.previous-extraction-$(date +%Y%m%d-%H%M%S)"
if [[ -e "$assets_dir/DATA" || -e "$assets_dir/.data-manifest" || -e "$assets_dir/main.dol" || -e "$assets_dir/StaticR.rel" ]]; then
if [[ -e "$assets_dir/DATA" || -e "$assets_dir/main.dol" || -e "$assets_dir/StaticR.rel" ]]; then
mkdir -p "$backup"
for item in DATA .data-manifest main.dol StaticR.rel; do
for item in DATA main.dol StaticR.rel; do
[[ -e "$assets_dir/$item" ]] && mv "$assets_dir/$item" "$backup/$item"
done
fi
mv "$stage/DATA" "$assets_dir/DATA"
mv "$stage/.data-manifest" "$assets_dir/.data-manifest"
mv "$stage/main.dol" "$assets_dir/main.dol"
mv "$stage/StaticR.rel" "$assets_dir/StaticR.rel"
rmdir "$stage"
@@ -1,11 +0,0 @@
# 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)
+14 -93
View File
@@ -5,36 +5,28 @@ set -euo pipefail
fail() { printf 'publish-app.command: error: %s\n' "$*" >&2; exit 1; }
usage() {
cat <<'EOF'
Usage: publish-app.command --build-dir DIR --product {WiiCompiled|RetroRewind} --output-dir DIR [options]
Usage: publish-app.command --build-dir DIR --product {WiiCompiled|RetroRewind} --output-dir DIR
Copies a locally built product and its runtime assets into OUTPUT-DIR/<product>.app.
It bundles non-system dylibs, rewrites their install names, and ad-hoc signs the
result. This is suitable for local use; a release must replace ad-hoc signing
with the project's Developer ID signing and notarization process.
--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
}
build_dir=""; product=""; output_dir=""; architecture=$(uname -m); minimum_system_version=12.0
build_dir=""; product=""; output_dir=""
while (($#)); do
case "$1" in
--build-dir) build_dir=${2:-}; shift 2 ;;
--product) product=${2:-}; shift 2 ;;
--output-dir) output_dir=${2:-}; shift 2 ;;
--architecture) architecture=${2:-}; shift 2 ;;
--minimum-system-version) minimum_system_version=${2:-}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown option: $1" ;;
esac
done
[[ "$product" == WiiCompiled || "$product" == RetroRewind ]] || fail '--product must be WiiCompiled or RetroRewind'
[[ "$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
for tool in codesign ditto install_name_tool otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
[[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product"
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
app="$output_dir/$product.app"
@@ -55,7 +47,7 @@ cat > "$app/Contents/Info.plist" <<EOF
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>0.1.0</string>
<key>CFBundleVersion</key><string>1</string>
<key>LSMinimumSystemVersion</key><string>$minimum_system_version</string>
<key>LSMinimumSystemVersion</key><string>14.0</string>
<key>NSHighResolutionCapable</key><true/>
</dict></plist>
EOF
@@ -65,96 +57,25 @@ for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do
ln -s "../Resources/$asset" "$macos/$asset"
done
# Expand each image's rpaths before inheriting them, so @loader_path stays
# 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=("")
# Build a closure of Homebrew dylibs. System libraries remain system references.
queue=("$macos/$product")
while ((${#queue[@]})); do
current=${queue[0]}
inherited_rpaths=${queue_rpaths[0]}
queue=("${queue[@]:1}")
queue_rpaths=("${queue_rpaths[@]:1}")
current_rpaths=$(expanded_rpaths "$current")
if [[ -n "$inherited_rpaths" ]]; then
current_rpaths="${current_rpaths}${current_rpaths:+$'\n'}$inherited_rpaths"
fi
self_id=$(otool -D "$current" 2>/dev/null | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]*$/\1/p' || true)
while IFS= read -r dependency; do
[[ -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")
[[ "$dependency" == /opt/homebrew/* || "$dependency" == /usr/local/* ]] || continue
[[ -f "$dependency" ]] || continue
name=$(basename "$dependency")
if [[ ! -f "$frameworks/$name" ]]; then
ditto "$dep_path" "$frameworks/$name"
ditto "$dependency" "$frameworks/$name"
install_name_tool -id "@rpath/$name" "$frameworks/$name"
queue+=("$dep_path")
queue_rpaths+=("$current_rpaths")
queue+=("$frameworks/$name")
fi
done < <(otool -L "$current" | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]+\(compatibility version .*/\1/p')
done < <(otool -L "$current" | tail -n +2 | awk '{print $1}')
done
while IFS= read -r binary; do
while IFS= read -r old; do
[[ "$old" == /opt/homebrew/* || "$old" == /usr/local/* ]] || continue
name=$(basename "$old")
[[ -f "$frameworks/$name" ]] || continue
if [[ "$binary" == "$macos/$product" ]]; then
@@ -162,7 +83,7 @@ while IFS= read -r binary; do
else
install_name_tool -change "$old" "@loader_path/$name" "$binary"
fi
done < <(otool -L "$binary" | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]+\(compatibility version .*/\1/p')
done < <(otool -L "$binary" | tail -n +2 | awk '{print $1}')
done < <(find "$frameworks" -type f -print; printf '%s\n' "$macos/$product")
find "$frameworks" -type f -exec codesign --force --sign - {} +
+23 -74
View File
@@ -5,19 +5,16 @@ set -euo pipefail
resources=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace_source="$resources/workspace"
nodtool="$resources/tools/nodtool"
translator="$resources/tools/Translator.Cli"
cmake_bin="$resources/tools/cmake/bin/cmake"
ninja_bin="$resources/tools/ninja"
support_root="$HOME/Library/Application Support/WiiCompiled"
workspace="$support_root/BuildWorkspace"
products="$support_root/Products"
fail() { printf 'WiiCompiled Setup: %s\n' "$*" >&2; exit 1; }
notice() {
/usr/bin/osascript - "$1" >/dev/null <<'APPLESCRIPT'
on run argv
display dialog (item 1 of argv) buttons {"OK"} default button "OK" with icon caution
end run
APPLESCRIPT
}
notice() { /usr/bin/osascript -e "display dialog \"${1//\"/\\\"}\" buttons {\"OK\"} default button \"OK\" with icon caution" >/dev/null; }
usage() {
cat <<'EOF'
Usage: setup.command --game IMAGE [--retro-dir DIR] [--install-location {user|applications}]
@@ -39,13 +36,6 @@ while (($#)); do
done
[[ "$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
game=$(/usr/bin/osascript <<'APPLESCRIPT'
set selectedFile to choose file with prompt "Choose your clean Mario Kart Wii PAL (RMCP01) disc image"
@@ -70,34 +60,12 @@ if ! /usr/bin/xcode-select -p >/dev/null 2>&1; then
/usr/bin/xcode-select --install || true
exit 1
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"
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
if [[ ! -d "$workspace/.git" && ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then
printf 'Preparing the local build workspace...\n'
if [[ ! -d "$workspace" ]]; then
/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; unchanged files
# keep their timestamps so the incremental native build survives.
# shellcheck source=../build-cache.sh
source "$workspace_source/Launcher/build-cache.sh"
mkw_sync_tree "$workspace_source" "$workspace" aurora-main projects runtime translator Launcher
/usr/bin/ditto "$source_bundle_version" "$workspace_bundle_version"
fi
rm -rf "$workspace"
/usr/bin/ditto "$workspace_source" "$workspace"
fi
profile=base
@@ -109,47 +77,33 @@ if [[ -n "$retro_dir" ]]; then
# verify its pinned signature before publishing it into the local cache.
retro_wfc_dir="$support_root/RetroWfcPayload"
retro_wfc_payload="$retro_wfc_dir/binary/payload.RMCPD00.bin"
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
rm -f "$retro_wfc_payload"
fi
if [[ -f "$retro_wfc_payload" ]] && ! "$translator" validate-retro-wfc-payload --directory "$retro_wfc_dir"; then
printf 'Discarding an invalid cached Retro-WFC payload...\n' >&2
rm -f "$retro_wfc_payload"
fi
# 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"
payload_stage=$(mktemp -d "$retro_wfc_dir/.payload-download.XXXXXX")
temporary_payload="$payload_stage/binary/payload.RMCPD00.bin"
mkdir -p "$(dirname "$temporary_payload")"
trap 'rm -rf "$payload_stage"' EXIT
if /usr/bin/curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \
--retry 1 --output "$temporary_payload" \
'https://rwfc.net/api/wfc/payload?g=RMCPD00'; then
if [[ ! -f "$retro_wfc_payload" ]]; then
printf 'Downloading the Retro-WFC payload needed for online play...\n'
mkdir -p "$retro_wfc_dir"
payload_stage=$(mktemp -d "$retro_wfc_dir/.payload-download.XXXXXX")
temporary_payload="$payload_stage/binary/payload.RMCPD00.bin"
mkdir -p "$(dirname "$temporary_payload")"
trap 'rm -rf "$payload_stage"' EXIT
/usr/bin/curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \
--retry 1 --output "$temporary_payload" \
'https://rwfc.net/api/wfc/payload?g=RMCPD00' || fail 'could not download the Retro-WFC payload needed for online play'
"$translator" validate-retro-wfc-payload --directory "$payload_stage" || \
fail 'downloaded Retro-WFC payload failed signature validation'
mkdir -p "$retro_wfc_dir/binary"
mv "$temporary_payload" "$retro_wfc_payload"
rmdir "$payload_stage/binary" "$payload_stage"
trap - EXIT
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
build_args+=(--profile both --base-output-dir "$products" --retro-rewind-package-dir "$retro_dir" --retro-wfc-offline-dir "$retro_wfc_dir")
fi
"$workspace/Launcher/local-build-macos.command" "${build_args[@]}" --profile "$profile" --cmake "$cmake_bin" --ninja "$ninja_bin" --translator-bin "$translator"
config="$support_root/Config.toml"
toml_string() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'; }
toml_string() { printf '%s' "$1" | sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g'; }
set_path() {
local key=$1 value=$2 encoded line temporary
encoded=$(toml_string "$value"); line="$key = \"$encoded\""; temporary="$config.tmp"
@@ -175,13 +129,8 @@ install_app() {
local app=$1
[[ -d "$products/$app" ]] || return 0
if [[ "$destination" == /Applications ]]; then
/usr/bin/osascript - "$products/$app" "/Applications/$app" <<'APPLESCRIPT'
on run argv
set sourcePath to quoted form of (item 1 of argv)
set installedPath to quoted form of (item 2 of argv)
do shell script "mkdir -p /Applications && rm -rf " & installedPath & " && /usr/bin/ditto " & sourcePath & " " & installedPath with administrator privileges
end run
APPLESCRIPT
command="mkdir -p /Applications && rm -rf '/Applications/$app' && ditto '$products/$app' '/Applications/$app'"
/usr/bin/osascript -e "do shell script \"$command\" with administrator privileges"
else
mkdir -p "$destination"; rm -rf "$destination/$app"; /usr/bin/ditto "$products/$app" "$destination/$app"
fi
-61
View File
@@ -1,61 +0,0 @@
#!/usr/bin/env bash
# Exercise a real dyld dependency chain before and after app packaging.
set -euo pipefail
[[ $(uname -s) == Darwin ]] || { printf 'This test requires macOS.\n' >&2; exit 1; }
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
temp_root=$(cd "${TMPDIR:-/tmp}" && pwd)
test_root=$(mktemp -d "$temp_root/wiicompiled-publish-test.XXXXXX")
[[ "$test_root" == "$temp_root"/wiicompiled-publish-test.* ]] || exit 1
trap 'rm -rf "$test_root"' EXIT
build_dir="$test_root/build with spaces"
output_dir="$test_root/output with spaces"
mkdir -p "$build_dir/A/b" "$build_dir/A/c" "$build_dir/wii_bootstrap" "$output_dir"
cat > "$test_root/c.c" <<'EOF'
int value_c(void) { return 7; }
EOF
cat > "$test_root/b.c" <<'EOF'
extern int value_c(void);
int value_b(void) { return 2 * value_c(); }
EOF
cat > "$test_root/a.c" <<'EOF'
extern int value_b(void);
int value_a(void) { return 1 + value_b(); }
EOF
cat > "$test_root/main.c" <<'EOF'
#include <stdio.h>
extern int value_a(void);
int main(void) { printf("%d\n", value_a()); return 0; }
EOF
clang -dynamiclib "$test_root/c.c" -o "$build_dir/A/c/libC.dylib" \
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libC.dylib
clang -dynamiclib "$test_root/b.c" -o "$build_dir/A/b/libB.dylib" \
-L "$build_dir/A/c" -lC \
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libB.dylib
clang -dynamiclib "$test_root/a.c" -o "$build_dir/A/libA.dylib" \
-L "$build_dir/A/b" -lB \
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libA.dylib \
-Wl,-rpath,@loader_path/b -Wl,-rpath,@loader_path/c
clang "$test_root/main.c" -o "$build_dir/WiiCompiled" \
-L "$build_dir/A" -lA \
-Wl,-headerpad_max_install_names -Wl,-rpath,@executable_path/A
# B has no rpaths: its C dependency must inherit A's loader-relative path.
[[ $("$build_dir/WiiCompiled") == 15 ]]
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem; do
: > "$build_dir/$asset"
done
bash "$script_dir/publish-app.command" --build-dir "$build_dir" \
--product WiiCompiled --output-dir "$output_dir" --architecture "$(uname -m)"
for name in libA.dylib libB.dylib libC.dylib; do
[[ -f "$output_dir/WiiCompiled.app/Contents/Frameworks/$name" ]]
done
# Remove access to the original paths and relocate the app before executing.
mv "$build_dir" "$test_root/hidden build"
mkdir "$test_root/relocated app"
mv "$output_dir/WiiCompiled.app" "$test_root/relocated app/WiiCompiled.app"
[[ $("$test_root/relocated app/WiiCompiled.app/Contents/MacOS/WiiCompiled") == 15 ]]
printf 'publish-app dependency-chain test passed\n'
+1 -1
View File
@@ -225,7 +225,7 @@ EOF
# clang++ invocation above would not catch a broken CMAKE_ROOT (Modules/Templates) or a Ninja that
# can't find the compiler.
cat > "$test_dir/CMakeLists.txt" <<'EOF'
cmake_minimum_required(VERSION 3.25)
cmake_minimum_required(VERSION 3.16)
project(test CXX)
add_executable(test t.cpp)
EOF
+3 -23
View File
@@ -11,7 +11,6 @@
<a href="#building-from-source"><img alt="PowerPC static recompilation" src="https://img.shields.io/badge/PowerPC-static%20recompilation-FF9F0A"></a>
<a href="#retro-rewind"><img alt="Retro Rewind supported" src="https://img.shields.io/badge/Retro%20Rewind-supported-FF375F"></a>
<a href="https://github.com/TeamWheelWizard/WheelWizard/releases"><img alt="Install with Wheel Wizard" src="https://img.shields.io/badge/install%20with-Wheel%20Wizard-8B5CF6"></a>
<a href="https://github.com/patchzyy/Wiicompiled/releases"><img alt="Total downloads" src="https://img.shields.io/github/downloads/patchzyy/Wiicompiled/total?color=00B8D9&amp;logo=github"></a>
<a href="LICENSE"><img alt="License: GPLv3" src="https://img.shields.io/badge/license-GPLv3-2EA44F?logo=gnu&amp;logoColor=white"></a>
</p>
@@ -53,16 +52,12 @@ Play at several times the console's resolution.
**Music ducking.**
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
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.
rather it didn't. All audio that shows in your display media controls on your windows pc fall under this.
**An in-game settings bar.**
Press **F10** while the game window has focus:
- Internal resolution
- FPS counter
- MetalFX spatial upscaling on supported macOS GPUs
- Controller assignment for all four ports
- Full per-controller button mapping, including the bumpers
- Dolphin-syntax input expressions and GCPadNew.ini import
@@ -101,8 +96,8 @@ Known limitations of the Wii Remote path:
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
- About 20 GB of free disk space during installation (Final game size ~5 GB)
- macOS 12 (Monterey) or later on Apple Silicon (`arm64`) or Intel (`x86_64-v3`); pre-Haswell Intel CPUs are unsupported
- On macOS, a Metal-capable GPU and Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
- macOS 14 (Sonoma) or later on Apple Silicon
- On macOS, Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
GCZ, CISO, WBFS, WIA and RVZ are accepted.
@@ -123,21 +118,6 @@ image under Settings, turn on **WiiCompiled (beta)**, and hit install from the H
Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
### 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]
> Only take builds from this repository's
+1 -1
View File
@@ -132,7 +132,7 @@ trees themselves (fetched by `Launcher/Prepare-Dependencies.ps1`) so end-user bu
| Abseil | LTS 20240722.0 | Apache-2.0 | <https://github.com/abseil/abseil-cpp> |
| Dear ImGui | 1.91.9b-docking | MIT | <https://github.com/ocornut/imgui> |
| {fmt} | 11.1.4 | MIT | <https://github.com/fmtlib/fmt> |
| xxHash | 0.8.4 | BSD-2-Clause | <https://github.com/Cyan4973/xxHash> |
| xxHash | 0.8.3 | BSD-2-Clause | <https://github.com/Cyan4973/xxHash> |
| zlib | 1.3.2 | zlib | <https://github.com/madler/zlib> |
| 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/> |
-8
View File
@@ -2,7 +2,6 @@ cmake_minimum_required(VERSION 3.25)
project(aurora LANGUAGES C CXX)
if (APPLE)
enable_language(OBJC)
enable_language(OBJCXX)
endif()
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 20)
@@ -84,10 +83,3 @@ if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND NOT CMAKE_CROSSCOMPIL
enable_testing()
add_subdirectory(tests)
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 ()
@@ -15,7 +15,6 @@ function(aurora_copy_runtime_dlls target)
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_RUNTIME_DLLS:${_t}> $<TARGET_FILE_DIR:${target}>
COMMAND_EXPAND_LISTS
VERBATIM
)
endforeach ()
@@ -31,7 +30,6 @@ function(aurora_copy_runtime_dlls target)
if (EXISTS "${_dawn_bin_dir}/${_dll}")
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_dawn_bin_dir}/${_dll}" $<TARGET_FILE_DIR:${target}>
VERBATIM
)
endif ()
endforeach ()
+3 -7
View File
@@ -48,7 +48,7 @@ if (_aurora_dawn_provider STREQUAL "auto")
set(_has_package TRUE)
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|aarch64)$")
set(_has_package TRUE)
elseif (APPLE AND (CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|x86_64)$" OR CMAKE_OSX_ARCHITECTURES MATCHES "^(arm64|x86_64)$"))
elseif (APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|x86_64)$")
set(_has_package TRUE)
endif ()
@@ -101,7 +101,7 @@ if (_aurora_dawn_provider STREQUAL "vendor")
include(FetchContent)
FetchContent_Declare(dawn
URL "https://github.com/google/dawn/archive/refs/tags/${AURORA_DAWN_VERSION}.tar.gz"
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(dawn)
@@ -143,10 +143,6 @@ elseif (_aurora_dawn_provider STREQUAL "package")
if (NOT AURORA_DAWN_PACKAGE_URL)
string(TOLOWER "${CMAKE_SYSTEM_NAME}" _dawn_system)
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_arch STREQUAL "x86_64")
set(_dawn_arch "amd64")
@@ -198,7 +194,7 @@ elseif (_aurora_dawn_provider STREQUAL "package")
FetchContent_Declare(dawn_prebuilt
URL "${AURORA_DAWN_PACKAGE_URL}"
${_dawn_prebuilt_hash_argument}
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
FetchContent_MakeAvailable(dawn_prebuilt)
+1 -1
View File
@@ -7,7 +7,7 @@
include(FetchContent)
FetchContent_Declare(libusb
URL "https://github.com/libusb/libusb/releases/download/v${AURORA_LIBUSB_VERSION}/libusb-${AURORA_LIBUSB_VERSION}.tar.bz2"
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
# Upstream ships no CMakeLists.txt, so this only populates the source tree.
FetchContent_MakeAvailable(libusb)
+1 -1
View File
@@ -120,7 +120,7 @@ elseif (_aurora_nod_provider STREQUAL "package")
include(FetchContent)
FetchContent_Declare(nod_prebuilt
URL "${AURORA_NOD_PACKAGE_URL}"
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(nod_prebuilt)
-259
View File
@@ -32,52 +32,6 @@ function(aurora_sdl3_apply_patches sdl_source_dir)
if (NOT EXISTS "${_wii}")
message(FATAL_ERROR "aurora: SDL3 source tree at ${sdl_source_dir} has no SDL_hidapi_wii.c")
endif ()
set(_rumble "${sdl_source_dir}/src/joystick/hidapi/SDL_hidapi_rumble.c")
if (NOT EXISTS "${_rumble}")
message(FATAL_ERROR "aurora: SDL3 source tree at ${sdl_source_dir} has no SDL_hidapi_rumble.c")
endif ()
set(_hidapi "${sdl_source_dir}/src/joystick/hidapi/SDL_hidapijoystick.c")
if (NOT EXISTS "${_hidapi}")
message(FATAL_ERROR "aurora: SDL3 source tree at ${sdl_source_dir} has no SDL_hidapijoystick.c")
endif ()
# Backport of SDL 71f4af732 (drop once SDL includes it): rumble writes must not hold dev_lock.
_aurora_sdl3_replace("${_rumble}" "HIDAPI rumble writes no longer hold the device lock"
[==[ if (request) {
SDL_LockMutex(request->device->dev_lock);
if (request->device->dev) {
#ifdef DEBUG_RUMBLE
HIDAPI_DumpPacket("Rumble packet: size = %d", request->data, request->size);
#endif
SDL_hid_write(request->device->dev, request->data, request->size);
}
SDL_UnlockMutex(request->device->dev_lock);
]==]
[==[ if (request) {
if (request->device->dev) {
#ifdef DEBUG_RUMBLE
HIDAPI_DumpPacket("Rumble packet: size = %d", request->data, request->size);
#endif
SDL_hid_write(request->device->dev, request->data, request->size);
}
]==])
# Without the lock, closing the handle must wait for in-flight rumble writes.
_aurora_sdl3_replace("${_hidapi}" "HIDAPI device close waits for pending rumble"
[==[ device->driver->FreeDevice(device);
device->driver = NULL;
SDL_LockMutex(device->dev_lock);
]==]
[==[ device->driver->FreeDevice(device);
device->driver = NULL;
while (SDL_GetAtomicInt(&device->rumble_pending) > 0) {
SDL_Delay(10);
}
SDL_LockMutex(device->dev_lock);
]==])
# Wii Remote: rebuild the joystick in place after an extension change.
#
@@ -164,219 +118,6 @@ static void UpdateDeviceIdentity(SDL_HIDAPI_Device *device)
now = SDL_GetTicks();
]==])
# Classic Controller sticks: the stock path stretches each half-range by learned min/max;
# this posts WPAD's linear value (raw - first-report centre) * unit, times 64.
_aurora_sdl3_replace("${_wii}" "Wii Classic Controller linear sticks (helper)"
[==[static void HandleGamepadControllerButtonData(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick, const WiiButtonData *data)
{
]==]
[==[/* WiiCompiled: axis = (raw - centre) * unit * 64, y inverted; see AuroraSDL3Patches.cmake */
static void PostClassicStickLinear(Uint64 timestamp, SDL_Joystick *joystick, StickCalibrationData *calibration, Uint8 axis, Uint16 data, int unit)
{
int value;
if (!calibration->center) {
// Center on first read
calibration->center = data;
return;
}
value = ((int)data - (int)calibration->center) * unit * 64;
if (axis == SDL_GAMEPAD_AXIS_LEFTY || axis == SDL_GAMEPAD_AXIS_RIGHTY) {
value = -value;
}
SDL_SendJoystickAxis(timestamp, joystick, axis, (Sint16)SDL_clamp(value, SDL_JOYSTICK_AXIS_MIN, SDL_JOYSTICK_AXIS_MAX));
}
static void HandleGamepadControllerButtonData(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick, const WiiButtonData *data)
{
]==])
_aurora_sdl3_replace("${_wii}" "Wii Classic Controller linear sticks (axes)"
[==[ PostStickCalibrated(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[0], SDL_GAMEPAD_AXIS_LEFTX, lx);
PostStickCalibrated(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[1], SDL_GAMEPAD_AXIS_LEFTY, ly);
PostStickCalibrated(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[2], SDL_GAMEPAD_AXIS_RIGHTX, rx);
PostStickCalibrated(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[3], SDL_GAMEPAD_AXIS_RIGHTY, ry);
]==]
[==[ PostClassicStickLinear(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[0], SDL_GAMEPAD_AXIS_LEFTX, lx, 16);
PostClassicStickLinear(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[1], SDL_GAMEPAD_AXIS_LEFTY, ly, 16);
PostClassicStickLinear(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[2], SDL_GAMEPAD_AXIS_RIGHTX, rx, 32);
PostClassicStickLinear(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[3], SDL_GAMEPAD_AXIS_RIGHTY, ry, 32);
]==])
# Wii Remote: decode the accelerometer with the factory calibration from the EEPROM.
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (context fields)"
[==[ StickCalibrationData m_StickCalibrationData[6];
} SDL_DriverWii_Context;
]==]
[==[ StickCalibrationData m_StickCalibrationData[6];
float m_fAccelZeroG[3];
float m_fAccelPerG[3];
} SDL_DriverWii_Context;
]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (memory space parameter)"
[==[static bool ReadRegister(SDL_DriverWii_Context *ctx, Uint32 address, int size, bool sync)
{
Uint8 readRequest[7];
readRequest[0] = k_eWiiOutputReportIDs_ReadMemory;
readRequest[1] = (Uint8)(0x04 | (Uint8)ctx->m_bRumbleActive);
]==]
[==[static bool ReadMemory(SDL_DriverWii_Context *ctx, Uint8 addressSpace, Uint32 address, int size, bool sync);
static bool ReadRegister(SDL_DriverWii_Context *ctx, Uint32 address, int size, bool sync)
{
return ReadMemory(ctx, 0x04, address, size, sync);
}
static bool ReadMemory(SDL_DriverWii_Context *ctx, Uint8 addressSpace, Uint32 address, int size, bool sync)
{
Uint8 readRequest[7];
readRequest[0] = k_eWiiOutputReportIDs_ReadMemory;
readRequest[1] = (Uint8)(addressSpace | (Uint8)ctx->m_bRumbleActive);
]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (EEPROM read)"
[==[static bool HIDAPI_DriverWii_InitDevice(SDL_HIDAPI_Device *device)
{
]==]
[==[// Nominal decode: zero-g at 0x200, 100 units per g
#define WII_ACCEL_NOMINAL_ZERO_G 512.0f
#define WII_ACCEL_NOMINAL_PER_G 100.0f
// EEPROM calibration at 0x16, copy at 0x20: zero-g XYZ, 1 g XYZ, LSBs of both, checksum
#define WII_ACCEL_CALIBRATION_SIZE 10
#define WII_ADDRESS_SPACE_EEPROM 0x00
static bool ReadAccelCalibrationBlock(SDL_DriverWii_Context *ctx, Uint8 address, Uint8 *block)
{
if (!ReadMemory(ctx, WII_ADDRESS_SPACE_EEPROM, address, WII_ACCEL_CALIBRATION_SIZE, true)) {
return false;
}
if (ctx->m_rgucReadBuffer[5] != address ||
ctx->m_rgucReadBuffer[3] != ((WII_ACCEL_CALIBRATION_SIZE - 1) << 4)) {
SDL_SetError("Unexpected EEPROM read response");
return false;
}
SDL_memcpy(block, ctx->m_rgucReadBuffer + 6, WII_ACCEL_CALIBRATION_SIZE);
return true;
}
static bool ParseAccelCalibration(SDL_DriverWii_Context *ctx, const Uint8 *block)
{
int zero[3];
int perG[3];
Uint8 checksum = 0x55;
int i;
for (i = 0; i < WII_ACCEL_CALIBRATION_SIZE - 1; ++i) {
checksum = (Uint8)(checksum + block[i]);
}
if (checksum != block[WII_ACCEL_CALIBRATION_SIZE - 1]) {
SDL_SetError("Bad checksum");
return false;
}
for (i = 0; i < 3; ++i) {
int shift = 4 - 2 * i;
int oneG = (block[4 + i] << 2) | ((block[7] >> shift) & 0x03);
zero[i] = (block[i] << 2) | ((block[3] >> shift) & 0x03);
perG[i] = oneG - zero[i];
// Sanity limits around the nominal values
if (zero[i] < 384 || zero[i] > 640 || perG[i] < 60 || perG[i] > 160) {
SDL_SetError("Implausible values");
return false;
}
}
for (i = 0; i < 3; ++i) {
ctx->m_fAccelZeroG[i] = (float)zero[i];
ctx->m_fAccelPerG[i] = (float)perG[i];
}
return true;
}
static void ReadAccelCalibration(SDL_DriverWii_Context *ctx)
{
static const Uint8 addresses[] = { 0x16, 0x20 };
Uint8 block[WII_ACCEL_CALIBRATION_SIZE];
int i;
for (i = 0; i < SDL_arraysize(addresses); ++i) {
if (ReadAccelCalibrationBlock(ctx, addresses[i], block) &&
ParseAccelCalibration(ctx, block)) {
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Accelerometer calibration from EEPROM 0x%02X: zero %d/%d/%d, %d/%d/%d units per g",
addresses[i],
(int)ctx->m_fAccelZeroG[0], (int)ctx->m_fAccelZeroG[1], (int)ctx->m_fAccelZeroG[2],
(int)ctx->m_fAccelPerG[0], (int)ctx->m_fAccelPerG[1], (int)ctx->m_fAccelPerG[2]);
return;
}
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Accelerometer calibration at EEPROM 0x%02X unusable: %s", addresses[i], SDL_GetError());
}
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Using nominal accelerometer calibration");
}
static bool HIDAPI_DriverWii_InitDevice(SDL_HIDAPI_Device *device)
{
]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (nominal defaults)"
[==[ ctx->device = device;
device->context = ctx;
]==]
[==[ ctx->device = device;
device->context = ctx;
for (int i = 0; i < 3; ++i) {
ctx->m_fAccelZeroG[i] = WII_ACCEL_NOMINAL_ZERO_G;
ctx->m_fAccelPerG[i] = WII_ACCEL_NOMINAL_PER_G;
}
]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (read at init)"
[==[ ctx->m_eExtensionControllerType = ReadExtensionControllerType(device);
]==]
[==[ ctx->m_eExtensionControllerType = ReadExtensionControllerType(device);
ReadAccelCalibration(ctx);
]==])
# Reports without accelerometer bytes (status, memory) decoded as garbage.
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer guard"
[==[ }
x = ((data->rgucAccelerometer[0] << 2) | ((data->rgucBaseButtons[0] >> 5) & 0x03))]==]
[==[ }
if (!data->hasAccelerometer) {
return;
}
x = ((data->rgucAccelerometer[0] << 2) | ((data->rgucBaseButtons[0] >> 5) & 0x03))]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (nominal constant)"
[==[const WiiButtonData *data)
{
const float ACCEL_RES_PER_G = 100.0f;
Sint16 x, y, z;
]==]
[==[const WiiButtonData *data)
{
Sint16 x, y, z;
]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (decode)"
[==[ x = ((data->rgucAccelerometer[0] << 2) | ((data->rgucBaseButtons[0] >> 5) & 0x03)) - 0x200;
y = ((data->rgucAccelerometer[1] << 2) | ((data->rgucBaseButtons[1] >> 4) & 0x02)) - 0x200;
z = ((data->rgucAccelerometer[2] << 2) | ((data->rgucBaseButtons[1] >> 5) & 0x02)) - 0x200;
values[0] = -((float)x / ACCEL_RES_PER_G) * SDL_STANDARD_GRAVITY;
values[1] = ((float)z / ACCEL_RES_PER_G) * SDL_STANDARD_GRAVITY;
values[2] = ((float)y / ACCEL_RES_PER_G) * SDL_STANDARD_GRAVITY;
]==]
[==[ x = ((data->rgucAccelerometer[0] << 2) | ((data->rgucBaseButtons[0] >> 5) & 0x03));
y = ((data->rgucAccelerometer[1] << 2) | ((data->rgucBaseButtons[1] >> 4) & 0x02));
z = ((data->rgucAccelerometer[2] << 2) | ((data->rgucBaseButtons[1] >> 5) & 0x02));
values[0] = -(((float)x - ctx->m_fAccelZeroG[0]) / ctx->m_fAccelPerG[0]) * SDL_STANDARD_GRAVITY;
values[1] = (((float)z - ctx->m_fAccelZeroG[2]) / ctx->m_fAccelPerG[2]) * SDL_STANDARD_GRAVITY;
values[2] = (((float)y - ctx->m_fAccelZeroG[1]) / ctx->m_fAccelPerG[1]) * SDL_STANDARD_GRAVITY;
]==])
endfunction()
# Script mode (FetchContent PATCH_COMMAND).
+4 -7
View File
@@ -41,10 +41,7 @@ if (_aurora_sdl3_provider STREQUAL "auto")
set(_aurora_sdl3_provider "package")
else ()
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
# 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)
find_package(SDL3 QUIET)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
if (SDL3_FOUND)
set(_aurora_sdl3_provider "system")
@@ -61,7 +58,7 @@ if (_aurora_sdl3_provider STREQUAL "system")
message(STATUS "aurora: Using system SDL3 (provider=system)")
if (NOT SDL3_FOUND)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
find_package(SDL3 ${AURORA_SDL3_VERSION} REQUIRED)
find_package(SDL3 REQUIRED)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
endif ()
_aurora_sdl3_select_target()
@@ -95,7 +92,7 @@ elseif (_aurora_sdl3_provider STREQUAL "package")
include(FetchContent)
FetchContent_Declare(sdl3_prebuilt
URL "${AURORA_SDL3_PACKAGE_URL}"
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
FetchContent_MakeAvailable(sdl3_prebuilt)
@@ -148,7 +145,7 @@ elseif (_aurora_sdl3_provider STREQUAL "vendor")
endif ()
FetchContent_Declare(SDL
URL "https://github.com/libsdl-org/SDL/releases/download/release-${AURORA_SDL3_VERSION}/SDL3-${AURORA_SDL3_VERSION}.tar.gz"
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
PATCH_COMMAND "${CMAKE_COMMAND}" -DSDL_SOURCE_DIR=<SOURCE_DIR> -P "${_aurora_sdl3_patches}"
EXCLUDE_FROM_ALL
)
+1 -14
View File
@@ -34,17 +34,6 @@ if (AURORA_ENABLE_GX)
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_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)
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_VULKAN)
endif ()
@@ -52,9 +41,7 @@ if (AURORA_ENABLE_GX)
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_METAL)
target_sources(aurora_core PRIVATE lib/dawn/MetalBinding.mm)
set_source_files_properties(lib/dawn/MetalBinding.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
target_link_options(aurora_core PUBLIC
"LINKER:-weak_framework,Metal"
"LINKER:-U,_OBJC_CLASS_$_MTLLogStateDescriptor")
target_link_options(aurora_core PUBLIC "LINKER:-weak_framework,Metal")
endif ()
if (DAWN_ENABLE_D3D11)
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_D3D11)
+13 -63
View File
@@ -16,60 +16,10 @@ function(aurora_find_package_global)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ${_PREV_FIND_PACKAGE_TARGETS_GLOBAL})
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)
include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/AuroraDawnProvider.cmake)
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.
# 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)
@@ -88,7 +38,7 @@ else ()
set(ABSL_PROPAGATE_CXX_STD ON)
FetchContent_Declare(abseil-cpp
URL https://github.com/abseil/abseil-cpp/archive/refs/tags/20240722.0.tar.gz
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(abseil-cpp)
@@ -102,10 +52,10 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/AuroraSDL3Provider.cmake)
if (NOT TARGET xxhash)
message(STATUS "aurora: Fetching xxhash")
FetchContent_Declare(xxhash
URL https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.4.tar.gz
URL_HASH SHA256=5738270935e7c3d38a79b3adf7c9692566ce7895a25f67de43ad52ab504acd32
SOURCE_SUBDIR build/cmake
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
URL https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.3.tar.gz
URL_HASH SHA256=aae608dfe8213dfd05d909a57718ef82f30722c392344583d3f39050c7f29a80
SOURCE_SUBDIR cmake_unofficial
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL
)
set(XXHASH_BUILD_XXHSUM OFF CACHE INTERNAL "Build the xxhsum binary")
@@ -128,7 +78,7 @@ if (NOT TARGET fmt)
FetchContent_Declare(fmt
URL https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz
URL_HASH SHA256=ac366b7b4c2e9f0dde63a59b3feb5ee59b67974b14ee5dc9ea8ad78aa2c1ee1e
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(fmt)
@@ -166,7 +116,7 @@ if (AURORA_ENABLE_GX)
ZLIB
URL https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz
URL_HASH SHA256=bb329a0a2cd0274d05519d61c667c062e06990d72e125ee2dfa8de64f0119d16
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(ZLIB)
@@ -202,7 +152,7 @@ if (AURORA_ENABLE_GX)
PNG
URL https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.58.tar.gz
URL_HASH SHA256=A9D4DF463D36A6E5F9C29BD6F4967312D17E996C1854F3511F833924EB1993CF
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(PNG)
@@ -226,7 +176,7 @@ if (AURORA_ENABLE_GX)
FetchContent_Declare(Freetype
URL https://files.twilitrealm.dev/freetype-2.14.3.tar.gz
URL_HASH SHA256=e61b31ab26358b946e767ed7eb7f4bb2e507da1cfefeb7a8861ace7fd5c899a1
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(Freetype)
@@ -240,7 +190,7 @@ if (AURORA_ENABLE_GX)
FetchContent_Declare(imgui
URL https://github.com/ocornut/imgui/archive/refs/tags/v1.91.9b-docking.tar.gz
URL_HASH SHA256=466fdef9b18de15f0bb6e288e3d00ffa3d82200ec458ce5e4f724a161d9528a5
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(imgui)
@@ -285,7 +235,7 @@ if (AURORA_ENABLE_GX)
FetchContent_Declare(sqlite3
URL https://sqlite.org/2026/sqlite-amalgamation-3510300.zip
URL_HASH SHA256=acb1e6f5d832484bf6d32b681e858c38add8b2acdfd42ac5df24b8afb46552b4
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(sqlite3)
@@ -328,7 +278,7 @@ if (AURORA_ENABLE_GX)
FetchContent_Declare(zstd
URL https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz
URL_HASH SHA256=eb33e51f49a15e023950cd7825ca74a4a2b43db8354825ac24fc1b7ee09e6fa3
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
SOURCE_SUBDIR build/cmake
EXCLUDE_FROM_ALL
)
@@ -349,7 +299,7 @@ if (NOT TARGET TracyClient)
tracy
URL https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz
URL_HASH SHA256=24d342b5127d7f659dc3cf94f24347b348cc736f25b17a691fd7f69541937658
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(tracy)
-15
View File
@@ -170,21 +170,6 @@ void aurora_set_background_input(bool value);
void aurora_set_display_mode(AuroraDisplayMode 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();
const AuroraBackend* aurora_get_available_backends(size_t* count);
@@ -88,7 +88,6 @@ typedef enum _AuroraViewportPolicy {
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_NATIVE = 2, // Use active framebuffer pixels directly
AURORA_VIEWPORT_16_9 = 3, // Fixed 16:9 content with bars on other surfaces
} AuroraViewportPolicy;
/**
+9 -150
View File
@@ -7,7 +7,6 @@
#include "gx/shader_info.hpp"
#include "imgui.hpp"
#include "webgpu/gpu.hpp"
#include "webgpu/metalfx.hpp"
#include <webgpu/webgpu_cpp.h>
#endif
@@ -61,9 +60,6 @@ std::atomic<AuroraFrameWorkerWaitCallback> g_frameWorkerWaitCallback{nullptr};
// 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_presentScheduleIntervalNanos{0};
std::atomic<bool> g_metalfxRequested{false};
std::atomic<bool> g_metalfxSupported{false};
std::atomic<AuroraMetalFXStatus> g_metalfxStatus{AURORA_METALFX_DISABLED};
namespace {
Module Log("aurora");
@@ -769,9 +765,6 @@ AuroraInfo initialize(int argc, char* argv[], const AuroraConfig& config) noexce
#ifdef AURORA_ENABLE_GX
gfx::initialize();
g_metalfxSupported.store(webgpu::metalfx::supported(g_device, webgpu::g_backendType));
g_metalfxStatus.store(AURORA_METALFX_DISABLED);
imgui::create_context();
#endif
const auto size = window::get_window_size();
@@ -1194,114 +1187,12 @@ void stop_presenter() noexcept {
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
// gfx::begin_frame() may already have cleared the display-copy override.
wgpu::BindGroup encode_presentation_snapshot(wgpu::CommandEncoder& encoder,
const webgpu::PresentSource& presentSource,
const PresentationImage& image,
bool includeImGui, bool metalfxEnabled,
const wgpu::BindGroup* cachedMetalFXOutput = nullptr) {
void encode_presentation_snapshot(const wgpu::CommandEncoder& encoder,
const webgpu::PresentSource& presentSource,
const PresentationImage& image,
bool includeImGui) {
ZoneScoped;
auto viewport = webgpu::calculate_present_viewport(
image.texture.size.width, image.texture.size.height, presentSource.size.width,
@@ -1312,13 +1203,6 @@ wgpu::BindGroup encode_presentation_snapshot(wgpu::CommandEncoder& encoder,
image.texture.size.width, image.texture.size.height, presentAspect);
}
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{
wgpu::RenderPassColorAttachment{
@@ -1359,7 +1243,6 @@ wgpu::BindGroup encode_presentation_snapshot(wgpu::CommandEncoder& encoder,
imgui::render(pass);
pass.End();
}
return newMetalFXOutput;
}
#endif
@@ -1368,13 +1251,6 @@ void shutdown() noexcept {
#ifdef AURORA_ENABLE_GX
stop_presenter();
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();
gfx::shutdown();
webgpu::shutdown();
@@ -1487,7 +1363,6 @@ struct SealedFrameContext {
uint32_t logicalFrame = 0;
bool interpolationActive = false;
bool replayInterpolatedFrames = false;
bool metalfxEnabled = false;
};
// Phase 1: everything that touches producer-shared renderer state. Needs g_rendererGpuMutex and
@@ -1515,7 +1390,6 @@ void seal_frame_locked(gfx::SealedFrame& sealedFrame, SealedFrameContext& ctx) {
ctx.snapshotWidth = (std::max)(windowSize.native_fb_width, 1u);
ctx.snapshotHeight = (std::max)(windowSize.native_fb_height, 1u);
ctx.logicalFrame = gfx::current_frame();
ctx.metalfxEnabled = g_metalfxRequested.load();
// Latched before webgpu::clear_present_source_override() in the producer's
// next gfx::begin_frame().
ctx.presentSource = webgpu::current_present_source();
@@ -1563,14 +1437,10 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
const wgpu::CommandBufferDescriptor cmdBufDescriptor{
.label = "Presentation slot command buffer",
};
const auto submitEncodedSlot = [&](wgpu::CommandEncoder& target, bool releaseMetalFXOutput = true) {
const auto submitEncodedSlot = [&](wgpu::CommandEncoder& target) {
const auto buffer = target.Finish(&cmdBufDescriptor);
std::lock_guard submitLock(g_queueSubmitMutex);
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) {
@@ -1579,7 +1449,7 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
gfx::render(sealedFrame, encoder, static_cast<int32_t>(interpolatedFrame), false);
auto image =
acquire_presentation_image(interpolatedFrame, ctx.snapshotWidth, ctx.snapshotHeight);
encode_presentation_snapshot(encoder, ctx.presentSource, *image, true, ctx.metalfxEnabled);
encode_presentation_snapshot(encoder, ctx.presentSource, *image, true);
presentationJobs.push_back({
.image = std::move(image),
.logicalFrame = ctx.logicalFrame,
@@ -1597,18 +1467,12 @@ 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;
// completion is harvested in gfx::after_submit, never waited on here.
gfx::efb_ram::encode_async_downloads(encoder);
wgpu::BindGroup duplicatedMetalFXOutput;
if (!ctx.replayInterpolatedFrames) {
for (uint32_t interpolatedFrame = 0; interpolatedFrame < ctx.interpolatedFrameCount;
++interpolatedFrame) {
auto image =
acquire_presentation_image(interpolatedFrame, ctx.snapshotWidth, ctx.snapshotHeight);
const auto newMetalFXOutput = encode_presentation_snapshot(
encoder, ctx.presentSource, *image, true, ctx.metalfxEnabled,
duplicatedMetalFXOutput ? &duplicatedMetalFXOutput : nullptr);
if (!duplicatedMetalFXOutput && newMetalFXOutput) {
duplicatedMetalFXOutput = newMetalFXOutput;
}
encode_presentation_snapshot(encoder, ctx.presentSource, *image, true);
presentationJobs.push_back({
.image = std::move(image),
.logicalFrame = ctx.logicalFrame,
@@ -1616,14 +1480,13 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
.interpolated = true,
.duplicated = true,
});
submitEncodedSlot(encoder, false);
submitEncodedSlot(encoder);
encoder = g_device.CreateCommandEncoder(&encoderDescriptor);
}
}
auto finalImage =
acquire_presentation_image(ctx.interpolatedFrameCount, ctx.snapshotWidth, ctx.snapshotHeight);
encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true, ctx.metalfxEnabled,
duplicatedMetalFXOutput ? &duplicatedMetalFXOutput : nullptr);
encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true);
auto pendingFrameCapture = encode_frame_capture(encoder, ctx.presentSource);
presentationJobs.push_back({
.image = std::move(finalImage),
@@ -2099,7 +1962,3 @@ void aurora_set_background_input(bool value) {
}
void aurora_set_display_mode(AuroraDisplayMode mode) { aurora::window::set_display_mode(mode); }
AuroraDisplayMode aurora_get_display_mode() { return aurora::window::get_display_mode(); }
void aurora_set_metalfx_spatial(bool enabled) { aurora::g_metalfxRequested.store(enabled); }
bool aurora_get_metalfx_spatial() { return aurora::g_metalfxRequested.load(); }
bool aurora_is_metalfx_spatial_supported() { return aurora::g_metalfxSupported.load(); }
AuroraMetalFXStatus aurora_get_metalfx_status() { return aurora::g_metalfxStatus.load(); }
-1
View File
@@ -46,7 +46,6 @@ void AuroraSetViewportPolicy(AuroraViewportPolicy policy) {
}
g_gxState.viewportPolicy = policy;
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);
if (changed) {
// Reapply the guest viewport and scissor after a resize.
+2 -2
View File
@@ -66,7 +66,7 @@ void GXCallDisplayList(const void* data, u32 nbytes) {
// Decode the display list immediately while its borrowed resources are valid.
aurora::gx::fifo::drain();
aurora::gx::fifo::process_all(static_cast<const u8*>(data), nbytes, true);
aurora::gx::fifo::process(static_cast<const u8*>(data), nbytes, true);
}
void GXCallDisplayListLE(const void* data, u32 nbytes) {
@@ -84,6 +84,6 @@ void GXCallDisplayListLE(const void* data, u32 nbytes) {
aurora::gx::fifo::drain();
// Process the display list through the command processor (little-endian)
aurora::gx::fifo::process_all(static_cast<const u8*>(data), nbytes, false);
aurora::gx::fifo::process(static_cast<const u8*>(data), nbytes, false);
}
}
+5 -3
View File
@@ -520,9 +520,11 @@ void GXCopyTex(void* dest, GXBool clear) {
clearState.clearAlpha = clear && alphaUpdate;
}
const auto copyFilter = combined_copy_filter_coefficients(g_gxState.copyFilterVFilter);
// Skip only recurring color copies so one-shot copies are never lost.
const bool producedConsecutively = handle.revision != 0 && currentFrame - handle.lastProducedFrame <= 1;
const bool persistentCopy = !aurora::gx::is_depth_format(texCopyFmt) && !producedConsecutively;
// Every GXCopyTex is observable texture data. Reusing a destination in this
// or the previous frame does not guarantee another redraw: menu thumbnail
// scratch targets can be reused and then retained. Depth copies have the
// 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,
clearState.clearColorValue, aurora::gx::clear_depth_value(), resolveFmt,
&sourceRect.sampleRect, g_gxState.texCopyHalfScale, &copyFilter, forceOpaqueAlpha,
+31 -41
View File
@@ -6,10 +6,10 @@
#include <SDL3/SDL_mouse.h>
#include <SDL3/SDL_joystick.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <sys/stat.h>
#include <ranges>
namespace {
constexpr int32_t k_mappingsFileVersion = 3;
@@ -195,7 +195,7 @@ std::array<PADButtonMapping, PAD_BUTTON_COUNT> g_defaultButtonsJoyPair{{
}};
// Wii U Pro Controllers through SDL's HIDAPI Wii driver. No SDL_GamepadType
// singles them out, so they are picked by name or USB id (see
// singles them out, so they are picked by the name the driver gives them (see
// __PADSetDefaultMapping). Wii Remotes, with or without a Nunchuk or Classic
// Controller, are read by the game through KPAD instead and never get a
// GameCube mapping (the runtime hides those ports from PADRead).
@@ -354,7 +354,7 @@ BOOL PADInit() {
}
g_initialized = true;
std::for_each(g_keyboardBindings.begin(), g_keyboardBindings.end(), [](auto& state) {
std::ranges::for_each(g_keyboardBindings, [](auto& state) {
state.m_buttonMapping = g_defaultKeys;
state.m_axisMapping = g_defaultKeyAxis;
});
@@ -399,20 +399,19 @@ const char* PADGetNameForControllerIndex(const u32 idx) {
}
void PADSetPortForIndex(const u32 idx, const u32 port) {
if (port >= PAD_MAX_CONTROLLERS) return;
const auto* ctrl = __PADGetControllerForIndex(idx);
if (ctrl == nullptr) {
return;
}
const int32_t oldPort = aurora::input::player_index(ctrl->m_index);
const int32_t oldPort = SDL_GetGamepadPlayerIndex(ctrl->m_controller);
if (const auto* dest = aurora::input::get_controller_for_player(port); dest != nullptr && dest != ctrl) {
aurora::input::set_player_index(dest->m_index, -1);
SDL_SetGamepadPlayerIndex(dest->m_controller, -1);
}
if (oldPort >= 0 && oldPort != port) {
aurora::input::persist_controller_for_player(oldPort, nullptr);
}
aurora::input::set_player_index(ctrl->m_index, static_cast<Sint32>(port));
SDL_SetGamepadPlayerIndex(ctrl->m_controller, static_cast<Sint32>(port));
aurora::input::persist_controller_for_player(port, ctrl);
}
@@ -438,7 +437,7 @@ void PADClearPort(const u32 port) {
if (ctrl == nullptr) {
return;
}
aurora::input::set_player_index(ctrl->m_index, -1);
SDL_SetGamepadPlayerIndex(ctrl->m_controller, -1);
}
// Secondary bindings live only in memory; the runtime re-applies them from its
@@ -451,14 +450,11 @@ static void reset_alt_button_mapping(aurora::input::GameController* controller)
// SDL's hidapi Wii driver names the pad "Nintendo Wii U Pro Controller"; the
// other names it produces are Wii Remotes, which the game reads through KPAD
// and which therefore never take a GameCube mapping. The Linux kernel driver's
// "Nintendo Wii Remote Pro Controller" is only recognised by its USB id.
// and which therefore never take a GameCube mapping.
static bool wii_default_mapping(const aurora::input::GameController* controller,
std::array<PADButtonMapping, PAD_BUTTON_COUNT>& out) {
const char* name = SDL_GetGamepadName(controller->m_controller);
const bool wiiUProId = SDL_GetGamepadVendor(controller->m_controller) == 0x057e &&
SDL_GetGamepadProduct(controller->m_controller) == 0x0330;
if (!wiiUProId && (name == nullptr || SDL_strstr(name, "Wii U Pro Controller") == nullptr)) {
if (name == nullptr || SDL_strstr(name, "Wii U Pro Controller") == nullptr) {
return false;
}
out = g_defaultButtonsWiiUPro;
@@ -650,8 +646,8 @@ static void EnsureMappingLoaded(aurora::input::GameController* controller) {
static Sint16 _get_axis_value(const aurora::input::GameController* controller, // NOLINT(*-reserved-identifier)
PADAxis axis) {
const auto iter = std::find_if(controller->m_axisMapping.begin(), controller->m_axisMapping.end(),
[axis](const auto& pair) { return pair.padAxis == axis; });
const auto iter =
std::ranges::find_if(controller->m_axisMapping, [axis](const auto& pair) { return pair.padAxis == axis; });
if (iter == controller->m_axisMapping.end()) {
return 0;
}
@@ -741,8 +737,8 @@ u32 PADRead(PADStatus* status) {
status[i].err = PAD_ERR_NONE;
if (g_keyboardBindings[i].m_mappingsSet && SDL_GetKeyboardFocus() != nullptr) {
std::for_each(g_keyboardBindings[i].m_buttonMapping.begin(), g_keyboardBindings[i].m_buttonMapping.end(),
[&kbState, &numKeys, &i, &status](const PADKeyButtonBinding& mapping) {
std::ranges::for_each(
g_keyboardBindings[i].m_buttonMapping, [&kbState, &numKeys, &i, &status](const PADKeyButtonBinding& mapping) {
if (mapping.scancode > PAD_KEY_INVALID && mapping.scancode < numKeys && kbState[mapping.scancode]) {
status[i].button |= mapping.padButton;
} else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) {
@@ -849,8 +845,8 @@ u32 PADRead(PADStatus* status) {
bool leftTriggerSet = false;
bool rightTriggerSet = false;
std::for_each(controller->m_buttonMapping.begin(), controller->m_buttonMapping.end(),
[&controller, &i, &status, &leftTriggerSet, &rightTriggerSet](const auto& mapping) {
std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet,
&rightTriggerSet](const auto& mapping) {
if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) {
status[i].button |= mapping.padButton;
}
@@ -863,8 +859,8 @@ u32 PADRead(PADStatus* status) {
}
});
std::for_each(controller->m_altButtonMapping.begin(), controller->m_altButtonMapping.end(),
[&controller, &i, &status, &leftTriggerSet, &rightTriggerSet](const auto& mapping) {
std::ranges::for_each(controller->m_altButtonMapping, [&controller, &i, &status, &leftTriggerSet,
&rightTriggerSet](const auto& mapping) {
if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) {
return;
}
@@ -1193,8 +1189,8 @@ void PADSetButtonMapping(const u32 port, const PADButtonMapping mapping) {
return;
}
const auto iter = std::find_if(controller->m_buttonMapping.begin(), controller->m_buttonMapping.end(),
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
const auto iter = std::ranges::find_if(controller->m_buttonMapping,
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
if (iter == controller->m_buttonMapping.end()) {
return;
}
@@ -1227,8 +1223,8 @@ void PADSetAltButtonMapping(const u32 port, const PADButtonMapping mapping) {
return;
}
const auto iter = std::find_if(controller->m_altButtonMapping.begin(), controller->m_altButtonMapping.end(),
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
const auto iter = std::ranges::find_if(controller->m_altButtonMapping,
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
if (iter == controller->m_altButtonMapping.end()) {
return;
}
@@ -1254,8 +1250,8 @@ void PADSetAxisMapping(const u32 port, const PADAxisMapping mapping) {
return;
}
const auto iter = std::find_if(controller->m_axisMapping.begin(), controller->m_axisMapping.end(),
[mapping](const auto& pair) { return mapping.padAxis == pair.padAxis; });
const auto iter = std::ranges::find_if(controller->m_axisMapping,
[mapping](const auto& pair) { return mapping.padAxis == pair.padAxis; });
if (iter == controller->m_axisMapping.end()) {
return;
}
@@ -1429,10 +1425,9 @@ static void load_keyboard_bindings() {
if (mappingsSet) {
const bool anyBound =
std::any_of(buttonMapping.begin(), buttonMapping.end(),
[](const PADKeyButtonBinding& b) { return b.scancode != PAD_KEY_INVALID; }) ||
std::any_of(axisMapping.begin(), axisMapping.end(),
[](const PADKeyAxisBinding& b) { return b.scancode != PAD_KEY_INVALID; });
std::ranges::any_of(buttonMapping,
[](const PADKeyButtonBinding& b) { return b.scancode != PAD_KEY_INVALID; }) ||
std::ranges::any_of(axisMapping, [](const PADKeyAxisBinding& b) { return b.scancode != PAD_KEY_INVALID; });
if (!anyBound) {
mappingsSet = false;
}
@@ -1472,10 +1467,7 @@ void __PADWriteDeadZones(SDL_IOStream* file, // NOLINT(*-reserved-identifier)
void PADSerializeMappings() {
const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath);
// 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;
for (auto& controller : aurora::input::g_GameControllers | std::views::values) {
EnsureMappingLoaded(&controller);
const auto filePath =
basePath / fmt::format("{}_{:04X}_{:04X}.controller", aurora::input::controller_name(controller.m_index),
@@ -1577,8 +1569,8 @@ static constexpr std::array<std::pair<PADButton, std::string_view>, PAD_AXIS_COU
const char* PADGetButtonName(const PADButton button) {
if (const auto iter = std::find_if(skButtonNames.begin(), skButtonNames.end(),
[&button](const auto& pair) { return button == pair.first; });
if (const auto iter =
std::ranges::find_if(skButtonNames, [&button](const auto& pair) { return button == pair.first; });
iter != skButtonNames.end()) {
return iter->second.data();
}
@@ -1591,8 +1583,7 @@ const char* PADGetNativeButtonName(u32 button) {
}
const char* PADGetAxisName(const PADAxis axis) {
if (const auto it = std::find_if(skAxisNames.begin(), skAxisNames.end(),
[&axis](const auto& pair) { return axis == pair.first; });
if (const auto it = std::ranges::find_if(skAxisNames, [&axis](const auto& pair) { return axis == pair.first; });
it != skAxisNames.end()) {
return it->second.data();
}
@@ -1601,8 +1592,7 @@ const char* PADGetAxisName(const PADAxis axis) {
}
const char* PADGetAxisDirectionLabel(const PADAxis axis) {
if (const auto it = std::find_if(skAxisDirLabels.begin(), skAxisDirLabels.end(),
[&axis](const auto& pair) { return axis == pair.first; });
if (const auto it = std::ranges::find_if(skAxisDirLabels, [&axis](const auto& pair) { return axis == pair.first; });
it != skAxisDirLabels.end()) {
return it->second.data();
}
+10 -4
View File
@@ -23,6 +23,7 @@
#include <memory>
#include <mutex>
#include <optional>
#include <ranges>
#include <absl/container/flat_hash_map.h>
#include <magic_enum.hpp>
@@ -1258,6 +1259,11 @@ void split_staging_batch() {
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();
end_batch(encoder);
render(encoder);
@@ -1447,8 +1453,8 @@ static void render_impl(std::vector<RenderPass>& renderPasses, wgpu::CommandEnco
#if defined(AURORA_GFX_DEBUG_GROUPS)
if (finalize && !debugFrame.groups.empty()) {
for (auto it = debugFrame.groups.rbegin(); it != debugFrame.groups.rend(); ++it) {
Log.warn("Debug group was not popped at end of frame: {}", *it);
for (auto& it : std::ranges::reverse_view(debugFrame.groups)) {
Log.warn("Debug group was not popped at end of frame: {}", it);
}
debugFrame.groups.clear();
}
@@ -1608,8 +1614,8 @@ bool bind_pipeline(PipelineRef ref, const wgpu::RenderPassEncoder& pass, Pipelin
if (!skip_unready_pipelines()) {
pipelineReady = wait_pipeline(ref, pipeline);
} else if (requireReady) {
// The pass resolves into a persistent texture (a one-shot bake such as MKW's minimap), so a
// skipped draw would never be re-issued. These run behind loads, not mid-race.
// Texture copies and capacity prefixes must retain complete draw results.
// A future display frame cannot repair a texture that already captured them.
pipelineReady = wait_pipeline_for_persistent_pass(ref, pipeline);
} else {
pipelineReady = try_pipeline(ref, pipeline);
+1 -2
View File
@@ -214,8 +214,7 @@ Params make_params(wgpu::Extent3D sourceSize, const FrameMapping& mapping) noexc
return params;
}
const bool stretch = mapping.viewportPolicy == AURORA_VIEWPORT_STRETCH ||
mapping.viewportPolicy == AURORA_VIEWPORT_16_9;
const bool stretch = mapping.viewportPolicy == AURORA_VIEWPORT_STRETCH;
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 scale = std::min(scaleX, scaleY);
+1 -1
View File
@@ -553,7 +553,7 @@ void build_index() noexcept {
continue;
}
s_replacementIndex.try_emplace(*parsed, ReplacementIndexEntry{path});
s_replacementIndex.try_emplace(*parsed, path);
}
Log.info("Indexed {} texture replacements", s_replacementIndex.size());
+4 -8
View File
@@ -82,23 +82,19 @@ void drain() {
if (detail::sBufferSize == 0) {
return;
}
process_all(detail::sBufferData, detail::sBufferSize, true);
detail::sBufferSize = 0;
}
void process_all(const uint8_t* data, uint32_t size, bool bigEndian) {
uint32_t consumed = 0;
bool retried = false;
while (consumed < size) {
const auto count = process(data + consumed, size - consumed, bigEndian);
while (consumed < detail::sBufferSize) {
const auto count = process(detail::sBufferData + consumed, detail::sBufferSize - consumed, true);
if (count == 0 && retried)
throw gfx::StagingCapacityError("FIFO draw does not fit after capacity submission");
consumed += count;
if (consumed == size) break;
if (consumed == detail::sBufferSize) break;
// process returned with its renderer lock released. No recursive drain.
gfx::split_staging_batch();
retried = true;
}
detail::sBufferSize = 0;
}
const uint8_t* get_buffer_data() { return detail::sBufferData; }
-4
View File
@@ -81,10 +81,6 @@ bool in_display_list();
// Drain the internal FIFO buffer through the command processor
void drain();
// Decode a whole command stream, submitting the staging batch whenever a draw does not fit.
// process() alone stops at that draw, so a caller that ignores its count drops the rest.
void process_all(const uint8_t* data, uint32_t size, bool bigEndian);
// Internal buffer inspection
const uint8_t* get_buffer_data();
uint32_t get_buffer_size();
-16
View File
@@ -174,12 +174,6 @@ static std::string color_arg_reg(GXTevColorArg arg, size_t stageIdx, const Shade
case GX_CC_KONST: {
switch (stage.kcSel) {
DEFAULT_FATAL("invalid kcSel {}", underlying(stage.kcSel));
// Unsourced encodings evaluate to zero in hardware.
case static_cast<GXTevKColorSel>(0x08):
case static_cast<GXTevKColorSel>(0x09):
case static_cast<GXTevKColorSel>(0x0A):
case static_cast<GXTevKColorSel>(0x0B):
return "vec3f(0.0)";
case GX_TEV_KCSEL_8_8:
return "vec3f(1.0)";
case GX_TEV_KCSEL_7_8:
@@ -281,16 +275,6 @@ static std::string alpha_arg_reg(GXTevAlphaArg arg, size_t stageIdx, const Shade
case GX_CA_KONST: {
switch (stage.kaSel) {
DEFAULT_FATAL("invalid kaSel {}", underlying(stage.kaSel));
// Unsourced encodings evaluate to zero in hardware.
case static_cast<GXTevKAlphaSel>(0x08):
case static_cast<GXTevKAlphaSel>(0x09):
case static_cast<GXTevKAlphaSel>(0x0A):
case static_cast<GXTevKAlphaSel>(0x0B):
case static_cast<GXTevKAlphaSel>(0x0C):
case static_cast<GXTevKAlphaSel>(0x0D):
case static_cast<GXTevKAlphaSel>(0x0E):
case static_cast<GXTevKAlphaSel>(0x0F):
return "0.0";
case GX_TEV_KASEL_8_8:
return "1.0";
case GX_TEV_KASEL_7_8:
+56 -129
View File
@@ -14,7 +14,6 @@
#include <array>
#include <string>
#include <utility>
#include <vector>
using namespace std::string_view_literals;
@@ -24,7 +23,7 @@ absl::flat_hash_map<Uint32, GameController> g_GameControllers;
namespace {
constexpr uint32_t kPortPreferencesMagic = SBIG('CPRT');
constexpr uint32_t kPortPreferencesVersion = 3;
constexpr uint32_t kPortPreferencesVersion = 2;
constexpr uint32_t kMaxPersistedStringLength = 256;
enum class PortPreferenceState : uint8_t {
@@ -36,8 +35,6 @@ enum class PortPreferenceState : uint8_t {
struct ControllerIdentity {
std::string guid;
std::string serial;
std::string path;
int32_t slot = -1;
};
struct PortPreference {
@@ -47,9 +44,6 @@ struct PortPreference {
std::array<PortPreference, PAD_MAX_CONTROLLERS> g_portPreferences;
bool g_portPreferencesLoaded = false;
// Startup controllers are added one by one via events; capture their SDL player index at init,
// before earlier port reassignments can move it.
absl::flat_hash_map<SDL_JoystickID, int32_t> g_connectSlots;
std::string port_preferences_path() {
if (g_config.userPath == nullptr) {
@@ -89,8 +83,6 @@ ControllerIdentity controller_identity(const GameController& controller) {
SDL_GUIDToString(SDL_GetGamepadGUIDForID(SDL_GetGamepadID(controller.m_controller)), guid, sizeof(guid));
identity.guid = guid;
identity.serial = normalize_serial(SDL_GetGamepadSerial(controller.m_controller));
identity.path = controller.m_path;
identity.slot = controller.m_slot;
return identity;
}
@@ -145,16 +137,12 @@ bool write_string(SDL_IOStream* file, const std::string& value) {
return write_value(file, size) && (size == 0 || write_exact(file, value.data(), size));
}
bool read_identity(SDL_IOStream* file, uint32_t version, ControllerIdentity& identity) {
if (!read_string(file, identity.guid) || !read_string(file, identity.serial)) {
return false;
}
return version < 3 || (read_string(file, identity.path) && read_value(file, identity.slot));
bool read_identity(SDL_IOStream* file, ControllerIdentity& identity) {
return read_string(file, identity.guid) && read_string(file, identity.serial);
}
bool write_identity(SDL_IOStream* file, const ControllerIdentity& identity) {
return write_string(file, identity.guid) && write_string(file, identity.serial) &&
write_string(file, identity.path) && write_value(file, identity.slot);
return write_string(file, identity.guid) && write_string(file, identity.serial);
}
bool read_port_preferences_file(std::array<PortPreference, PAD_MAX_CONTROLLERS>& preferences) {
@@ -171,13 +159,13 @@ bool read_port_preferences_file(std::array<PortPreference, PAD_MAX_CONTROLLERS>&
uint32_t magic = 0;
uint32_t version = 0;
bool ok = read_value(file, magic) && read_value(file, version) && magic == kPortPreferencesMagic &&
version >= 2 && version <= kPortPreferencesVersion;
version == kPortPreferencesVersion;
for (auto& preference : preferences) {
uint8_t state = 0;
ControllerIdentity identity;
ok = ok && read_value(file, state) && state <= static_cast<uint8_t>(PortPreferenceState::Controller) &&
read_identity(file, version, identity);
read_identity(file, identity);
if (ok) {
preference.state = static_cast<PortPreferenceState>(state);
preference.identity = std::move(identity);
@@ -237,31 +225,22 @@ void save_port_preferences() {
}
}
// Zero means no match. Higher scores claim ports first: path+slot > GUID > VID/PID.
constexpr int kScoreVidPid = 1;
constexpr int kScoreGuid = 2;
constexpr int kScoreSlot = 1;
constexpr int kScorePath = 2;
enum class IdentityMatch {
None,
Fallback,
Exact,
};
int identity_score(const ControllerIdentity& saved, const ControllerIdentity& current) {
IdentityMatch identity_match(const ControllerIdentity& saved, const ControllerIdentity& current) {
if (saved.guid.empty()) {
return 0;
return IdentityMatch::None;
}
if (saved.guid == current.guid) {
if (!saved.serial.empty() && !serial_matches(saved.serial, current.serial)) {
return 0;
}
int score = kScoreGuid;
if (!saved.path.empty() && saved.path == current.path) {
score += kScorePath;
}
if (saved.slot >= 0 && saved.slot == current.slot) {
score += kScoreSlot;
}
return score;
return saved.serial.empty() || serial_matches(saved.serial, current.serial) ? IdentityMatch::Exact
: IdentityMatch::None;
}
if (!serial_matches(saved.serial, current.serial)) {
return 0;
return IdentityMatch::None;
}
// Attempt to match against VID/PID as a fallback if the GUID changes
@@ -271,9 +250,9 @@ int identity_score(const ControllerIdentity& saved, const ControllerIdentity& cu
uint16_t currentProduct = 0;
SDL_GetJoystickGUIDInfo(SDL_StringToGUID(saved.guid.c_str()), &savedVendor, &savedProduct, nullptr, nullptr);
SDL_GetJoystickGUIDInfo(SDL_StringToGUID(current.guid.c_str()), &currentVendor, &currentProduct, nullptr, nullptr);
const bool sameDevice =
savedVendor != 0 && savedVendor == currentVendor && savedProduct != 0 && savedProduct == currentProduct;
return sameDevice ? kScoreVidPid : 0;
return savedVendor != 0 && savedVendor == currentVendor && savedProduct != 0 && savedProduct == currentProduct
? IdentityMatch::Fallback
: IdentityMatch::None;
}
void assign_player_index(GameController& controller, int32_t port) {
@@ -288,6 +267,12 @@ int32_t effective_player_index(const GameController& controller) {
return player >= 0 ? player : controller.m_playerIndex;
}
bool is_instance_claimed(const std::array<Uint32, PAD_MAX_CONTROLLERS>& claimedControllers, size_t claimedCount,
Uint32 instance) {
return std::find(claimedControllers.begin(), claimedControllers.begin() + claimedCount, instance) !=
claimedControllers.begin() + claimedCount;
}
void apply_port_preferences() noexcept {
ensure_port_preferences_loaded();
if (!std::any_of(g_portPreferences.begin(), g_portPreferences.end(),
@@ -303,62 +288,44 @@ void apply_port_preferences() noexcept {
}
}
struct Candidate {
int score;
uint32_t port;
Uint32 instance;
};
std::vector<Candidate> candidates;
for (const auto& [instance, controller] : g_GameControllers) {
const auto identity = controller_identity(controller);
for (uint32_t port = 0; port < g_portPreferences.size(); ++port) {
if (g_portPreferences[port].state != PortPreferenceState::Controller) {
continue;
}
if (const int score = identity_score(g_portPreferences[port].identity, identity); score > 0) {
candidates.push_back({score, port, instance});
}
}
}
// Best matches claim first so identical controllers each end up on their own port
std::sort(candidates.begin(), candidates.end(), [](const Candidate& a, const Candidate& b) {
if (a.score != b.score) {
return a.score > b.score;
}
return a.port != b.port ? a.port < b.port : a.instance < b.instance;
});
std::array<bool, PAD_MAX_CONTROLLERS> portClaimed{};
std::array<Uint32, PAD_MAX_CONTROLLERS> claimedInstances{};
std::array<Uint32, PAD_MAX_CONTROLLERS> claimedControllers{};
size_t claimedCount = 0;
bool upgraded = false;
for (const auto& candidate : candidates) {
if (portClaimed[candidate.port] ||
std::find(claimedInstances.begin(), claimedInstances.begin() + claimedCount, candidate.instance) !=
claimedInstances.begin() + claimedCount) {
for (uint32_t port = 0; port < g_portPreferences.size(); ++port) {
const auto& preference = g_portPreferences[port];
if (preference.state != PortPreferenceState::Controller) {
continue;
}
portClaimed[candidate.port] = true;
claimedInstances[claimedCount++] = candidate.instance;
auto& controller = g_GameControllers[candidate.instance];
assign_player_index(controller, static_cast<int32_t>(candidate.port));
// Preferences saved before path and slot existed gain them on their first GUID match
auto& saved = g_portPreferences[candidate.port].identity;
if (candidate.score >= kScoreGuid && (saved.path.empty() || saved.slot < 0)) {
const auto current = controller_identity(controller);
if (saved.path.empty() && !current.path.empty()) {
saved.path = current.path;
upgraded = true;
Uint32 fallbackInstance = 0;
GameController* fallbackController = nullptr;
for (auto& [instance, controller] : g_GameControllers) {
if (is_instance_claimed(claimedControllers, claimedCount, instance)) {
continue;
}
if (saved.slot < 0 && current.slot >= 0) {
saved.slot = current.slot;
upgraded = true;
switch (identity_match(preference.identity, controller_identity(controller))) {
case IdentityMatch::Exact:
assign_player_index(controller, static_cast<int32_t>(port));
claimedControllers[claimedCount++] = instance;
fallbackController = nullptr;
break;
case IdentityMatch::Fallback:
// Prefer any later exact match before claiming a fallback candidate
if (fallbackController == nullptr) {
fallbackInstance = instance;
fallbackController = &controller;
}
continue;
case IdentityMatch::None:
continue;
}
break;
}
if (fallbackController != nullptr) {
assign_player_index(*fallbackController, static_cast<int32_t>(port));
claimedControllers[claimedCount++] = fallbackInstance;
}
}
if (upgraded) {
save_port_preferences();
}
}
@@ -434,15 +401,6 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
return -1;
}
controller.m_isGameCube = controller.m_vid == 0x057E && controller.m_pid == 0x0337;
if (const char* path = SDL_GetGamepadPath(ctrl); path != nullptr) {
controller.m_path = path;
}
if (const auto slot = g_connectSlots.find(which); slot != g_connectSlots.end()) {
controller.m_slot = slot->second;
g_connectSlots.erase(slot);
} else {
controller.m_slot = SDL_GetGamepadPlayerIndex(ctrl);
}
const char* serial = SDL_GetGamepadSerial(ctrl);
controller.m_gameCubeUseOrdinaryStop = controller.m_isGameCube && serial && "GCP+"sv == serial;
if (controller.m_isGameCube ||
@@ -456,28 +414,6 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
g_GameControllers[instance] = controller;
ensure_player_index(g_GameControllers[instance]);
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_score(g_portPreferences[port].identity, controller_identity(g_GameControllers[instance])) > 0) {
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;
}
@@ -498,7 +434,6 @@ bool refresh_controller(SDL_JoystickID instance) noexcept {
}
void remove_controller(Uint32 instance) noexcept {
g_connectSlots.erase(instance);
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
SDL_CloseGamepad(it->second.m_controller);
g_GameControllers.erase(it);
@@ -582,14 +517,6 @@ void initialize() noexcept {
* as expected */
ASSERT(SDL_Init(SDL_INIT_HAPTIC | SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD), "Failed to initialize SDL subsystems: {}",
SDL_GetError());
int count = 0;
if (SDL_JoystickID* ids = SDL_GetGamepads(&count)) {
for (int i = 0; i < count; ++i) {
g_connectSlots[ids[i]] = SDL_GetGamepadPlayerIndexForID(ids[i]);
}
SDL_free(ids);
}
}
struct MouseScrollStatus {
-3
View File
@@ -20,9 +20,6 @@ struct GameController {
bool m_gameCubeUseOrdinaryStop = false;
Sint32 m_index = -1;
Sint32 m_playerIndex = -1;
// Device path and the SDL player index at connect (the adapter port for GameCube adapters).
std::string m_path;
Sint32 m_slot = -1;
bool m_hasRumble = false;
PADDeadZones m_deadZones{
.emulateTriggers = true,
+1 -1
View File
@@ -235,7 +235,7 @@ std::string GetOSVersion() {
constexpr auto name = "iOS";
#elif TARGET_OS_TV
constexpr auto name = "tvOS";
#else
#elif
constexpr auto name = Unknown;
#endif
+1 -14
View File
@@ -642,14 +642,6 @@ bool initialize(AuroraBackend auroraBackend) {
requiredLimits.maxDynamicStorageBuffersPerPipelineLayout, requiredLimits.maxStorageBuffersPerShaderStage,
requiredLimits.minUniformBufferOffsetAlignment, requiredLimits.minStorageBufferOffsetAlignment);
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;
wgpu::SupportedFeatures supportedFeatures;
g_adapter.GetFeatures(&supportedFeatures);
@@ -893,14 +885,9 @@ void release_surface() noexcept {
}
g_surface = {};
if (hadSurface && g_instance && g_device && g_queue) {
// Retired swapchain is freed after a later submission completes and the device ticks.
submit_staging_commands(g_device.CreateCommandEncoder().Finish());
const auto future = g_queue.OnSubmittedWorkDone(
wgpu::CallbackMode::WaitAnyOnly, [](wgpu::QueueWorkDoneStatus, wgpu::StringView) {});
if (const auto status = g_instance.WaitAny(future, 1000000000); status != wgpu::WaitStatus::Success) {
Log.warn("Timed out waiting for queue idle on surface release: {}", magic_enum::enum_name(status));
}
g_device.Tick();
g_instance.WaitAny(future, 1000000000);
}
}
-41
View File
@@ -1,41 +0,0 @@
#pragma once
#include <webgpu/webgpu_cpp.h>
#include <memory>
#include <string>
namespace aurora::webgpu::metalfx {
struct Size {
uint32_t inputWidth;
uint32_t inputHeight;
uint32_t outputWidth;
uint32_t outputHeight;
wgpu::TextureFormat format;
bool operator==(const Size&) const = default;
};
// All methods except supported() belong to the serialized frame encoder.
// GPU ownership is explicit: begin_input -> submit input -> upscale ->
// submit output consumption -> end_output. Neither texture may be used by
// Dawn outside its access interval. Destruction retires in-flight resources.
class SpatialScaler {
public:
virtual ~SpatialScaler() = default;
virtual const wgpu::TextureView& input_view() const = 0;
virtual const wgpu::TextureView& output_view() const = 0;
virtual const wgpu::Texture& output_texture() const = 0;
virtual bool begin_input() = 0;
virtual bool upscale() = 0;
virtual bool end_output() = 0;
virtual const std::string& error() const = 0;
};
bool supported(const wgpu::Device& device, wgpu::BackendType backend);
// A null result with no error means the bounded retirement pool is busy;
// skip upscaling for this frame and retry at a later frame boundary.
std::unique_ptr<SpatialScaler> create(const wgpu::Instance& instance,
const wgpu::Device& device, const Size& size,
std::string& error);
} // namespace aurora::webgpu::metalfx
-326
View File
@@ -1,326 +0,0 @@
#include "metalfx.hpp"
#import <Foundation/Foundation.h>
#import <IOSurface/IOSurface.h>
#import <Metal/Metal.h>
#import <MetalFX/MetalFX.h>
#include <dawn/native/MetalBackend.h>
#include <atomic>
#include <string_view>
namespace aurora::webgpu::metalfx {
namespace {
constexpr uint64_t kScheduleTimeoutNs = 1'000'000'000;
// Four current slots plus at most four retiring slots during resize. A busy
// GPU must not allow resize events to allocate unbounded full-resolution images.
constexpr unsigned kMaxLiveResources = 8;
std::atomic<unsigned> g_liveResources{0};
struct SharedImage {
IOSurfaceRef surface = nullptr;
id<MTLTexture> metal;
wgpu::SharedTextureMemory memory;
wgpu::Texture texture;
wgpu::TextureView view;
~SharedImage() { if (surface) CFRelease(surface); }
bool create(const wgpu::Device& device, id<MTLDevice> native, uint32_t width,
uint32_t height, wgpu::TextureFormat format, MTLTextureUsage nativeUsage,
wgpu::TextureUsage usage) {
const bool bgra = format == wgpu::TextureFormat::BGRA8Unorm;
const size_t rowBytes = IOSurfaceAlignProperty(kIOSurfaceBytesPerRow, size_t(width) * 4);
NSDictionary* properties = @{
(id)kIOSurfaceWidth: @(width), (id)kIOSurfaceHeight: @(height),
(id)kIOSurfaceBytesPerElement: @4, (id)kIOSurfaceBytesPerRow: @(rowBytes),
(id)kIOSurfaceAllocSize: @(rowBytes * height),
(id)kIOSurfacePixelFormat: @(bgra ? 0x42475241u : 0x52474241u)
};
surface = IOSurfaceCreate((__bridge CFDictionaryRef)properties);
if (!surface) return false;
auto descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:
bgra ? MTLPixelFormatBGRA8Unorm : MTLPixelFormatRGBA8Unorm
width:width height:height mipmapped:NO];
descriptor.storageMode = MTLStorageModeShared;
descriptor.usage = nativeUsage;
metal = [native newTextureWithDescriptor:descriptor iosurface:surface plane:0];
if (!metal) return false;
wgpu::SharedTextureMemoryIOSurfaceDescriptor io{};
io.ioSurface = surface;
io.allowStorageBinding = false;
wgpu::SharedTextureMemoryDescriptor importDescriptor{};
importDescriptor.nextInChain = &io;
memory = device.ImportSharedTextureMemory(&importDescriptor);
wgpu::SharedTextureMemoryProperties actual{};
if (!memory || memory.GetProperties(&actual) != wgpu::Status::Success ||
actual.format != format || actual.size.width != width || actual.size.height != height ||
(actual.usage & usage) != usage) return false;
wgpu::TextureDescriptor textureDescriptor{};
textureDescriptor.label = "MetalFX shared texture";
textureDescriptor.size = {width, height, 1};
textureDescriptor.format = format;
textureDescriptor.usage = usage;
texture = memory.CreateTexture(&textureDescriptor);
if (!texture) return false;
view = texture.CreateView();
return view != nullptr;
}
};
struct API_AVAILABLE(macos(13.0)) Resources {
SharedImage input, output;
id<MTLFXSpatialScaler> scaler;
id<MTLTexture> privateOutput;
id<MTLCommandQueue> nativeQueue;
id<MTLSharedEvent> event;
wgpu::SharedFence fence;
std::atomic<bool> failed{false};
Resources() { ++g_liveResources; }
~Resources() { --g_liveResources; }
};
class API_AVAILABLE(macos(13.0)) MetalSpatialScaler final : public SpatialScaler {
wgpu::Instance m_instance;
wgpu::Queue m_queue;
std::shared_ptr<Resources> m_resources;
wgpu::SharedTextureMemoryEndAccessState m_outputReleased{};
wgpu::Future m_outputScheduled{};
uint64_t m_value = 0;
std::string m_error;
bool fail(const char* reason) {
m_error = reason;
m_resources->failed = true;
return false;
}
bool wait_scheduled(wgpu::Future future) {
return m_instance.WaitAny(future, kScheduleTimeoutNs) == wgpu::WaitStatus::Success ||
fail("Timed out scheduling MetalFX GPU work");
}
bool end_access(SharedImage& image, wgpu::SharedTextureMemoryEndAccessState& state,
wgpu::Future& scheduled) {
wgpu::SharedTextureMemoryMetalEndAccessState metal{};
state.nextInChain = &metal;
const auto status = image.memory.EndAccess(image.texture, &state);
state.nextInChain = nullptr;
scheduled = metal.commandsScheduledFuture;
return status == wgpu::Status::Success || fail("MetalFX Dawn EndAccess failed");
}
bool begin_access(SharedImage& image, bool initialized, uint64_t value) {
wgpu::SharedTextureMemoryBeginAccessDescriptor access{};
access.initialized = initialized;
if (value) {
access.fenceCount = 1;
access.fences = &m_resources->fence;
access.signaledValueCount = 1;
access.signaledValues = &value;
}
return image.memory.BeginAccess(image.texture, &access) == wgpu::Status::Success ||
fail("MetalFX Dawn BeginAccess failed");
}
bool encode_waits(id<MTLCommandBuffer> commands,
const wgpu::SharedTextureMemoryEndAccessState& state) {
for (size_t i = 0; i < state.fenceCount; ++i) {
wgpu::SharedFenceMTLSharedEventExportInfo metal{};
wgpu::SharedFenceExportInfo info{};
info.nextInChain = &metal;
state.fences[i].ExportInfo(&info);
if (info.type != wgpu::SharedFenceType::MTLSharedEvent || !metal.sharedEvent)
return fail("Dawn did not export a MetalFX shared-event dependency");
[commands encodeWaitForEvent:(__bridge id<MTLSharedEvent>)metal.sharedEvent
value:state.signaledValues[i]];
}
return true;
}
void retain_until_dawn_done() {
// A resize/toggle can destroy this wrapper immediately. The last submitted
// Dawn consumer keeps the IOSurfaces/scaler alive independently of the cache.
m_queue.OnSubmittedWorkDone(wgpu::CallbackMode::AllowSpontaneous,
[resources = m_resources](wgpu::QueueWorkDoneStatus status, wgpu::StringView) {
if (status != wgpu::QueueWorkDoneStatus::Success) resources->failed = true;
});
}
public:
MetalSpatialScaler(const wgpu::Instance& instance, const wgpu::Device& device,
std::shared_ptr<Resources> resources)
: m_instance(instance), m_queue(device.GetQueue()), m_resources(std::move(resources)) {}
const wgpu::TextureView& input_view() const override { return m_resources->input.view; }
const wgpu::TextureView& output_view() const override { return m_resources->output.view; }
const wgpu::Texture& output_texture() const override { return m_resources->output.texture; }
const std::string& error() const override { return m_error; }
bool begin_input() override {
if (m_resources->failed) return fail("Previous MetalFX GPU work failed");
return begin_access(m_resources->input, m_value != 0, m_value);
}
bool upscale() override {
@autoreleasepool {
// Input has already been submitted. Retain it even if an export or native
// allocation fails and the caller immediately falls back to normal copy.
retain_until_dawn_done();
wgpu::SharedTextureMemoryEndAccessState inputReleased{};
wgpu::Future inputScheduled{};
if (!end_access(m_resources->input, inputReleased, inputScheduled) ||
!wait_scheduled(inputScheduled)) return false;
if (m_value && !wait_scheduled(m_outputScheduled)) return false;
id<MTLCommandBuffer> commands = [m_resources->nativeQueue commandBuffer];
if (!commands) return fail("Could not allocate a MetalFX command buffer");
commands.label = @"MetalFX spatial upscale and return to Dawn";
if (!encode_waits(commands, inputReleased) || !encode_waits(commands, m_outputReleased))
return false;
[m_resources->scaler encodeToCommandBuffer:commands];
id<MTLBlitCommandEncoder> blit = [commands blitCommandEncoder];
if (!blit) return fail("Could not allocate the MetalFX output blit");
[blit copyFromTexture:m_resources->privateOutput sourceSlice:0 sourceLevel:0
sourceOrigin:MTLOriginMake(0, 0, 0)
sourceSize:MTLSizeMake(m_resources->privateOutput.width, m_resources->privateOutput.height, 1)
toTexture:m_resources->output.metal destinationSlice:0 destinationLevel:0
destinationOrigin:MTLOriginMake(0, 0, 0)];
[blit endEncoding];
++m_value;
[commands encodeSignalEvent:m_resources->event value:m_value];
const auto resources = m_resources;
[commands addCompletedHandler:^(id<MTLCommandBuffer> completed) {
if (completed.status == MTLCommandBufferStatusError) resources->failed = true;
}];
[commands commit];
// Scheduling is required to order independent Metal queues. Completion
// remains asynchronous; resource reuse is guarded by shared GPU events.
[commands waitUntilScheduled];
if (commands.status == MTLCommandBufferStatusError)
return fail("MetalFX command buffer failed");
return begin_access(m_resources->output, true, m_value);
}
}
bool end_output() override {
retain_until_dawn_done();
m_outputReleased = {};
return end_access(m_resources->output, m_outputReleased, m_outputScheduled);
}
};
// Allocation failures must be caught here rather than reaching Aurora's fatal
// uncaptured-error callback. Scope callbacks own their strings even on timeout.
bool pop_scope(const wgpu::Instance& instance, const wgpu::Device& device, std::string& error) {
auto message = std::make_shared<std::string>();
auto future = device.PopErrorScope(wgpu::CallbackMode::WaitAnyOnly,
[message](wgpu::PopErrorScopeStatus status, wgpu::ErrorType type, wgpu::StringView text) {
if (status != wgpu::PopErrorScopeStatus::Success || type != wgpu::ErrorType::NoError) {
const std::string_view detail{text};
*message = detail.empty() ? "MetalFX texture allocation failed" : std::string(detail);
}
});
if (instance.WaitAny(future, kScheduleTimeoutNs) != wgpu::WaitStatus::Success) {
error = "Timed out checking MetalFX texture allocation";
return false;
}
if (!message->empty()) { error = *message; return false; }
return true;
}
} // namespace
bool supported(const wgpu::Device& device, wgpu::BackendType backend) {
if (@available(macOS 13.0, *)) {
if (!device || backend != wgpu::BackendType::Metal ||
!device.HasFeature(wgpu::FeatureName::SharedTextureMemoryIOSurface) ||
!device.HasFeature(wgpu::FeatureName::SharedFenceMTLSharedEvent)) return false;
auto native = dawn::native::metal::GetMTLDevice(device.Get());
return native && [MTLFXSpatialScalerDescriptor supportsDevice:native];
}
return false;
}
std::unique_ptr<SpatialScaler> create(const wgpu::Instance& instance,
const wgpu::Device& device, const Size& size,
std::string& error) {
error.clear();
if (@available(macOS 13.0, *)) {
@autoreleasepool {
if (!supported(device, wgpu::BackendType::Metal)) {
error = "MetalFX spatial scaling is unsupported";
return {};
}
wgpu::Limits limits{};
device.GetLimits(&limits);
if (!size.inputWidth || !size.inputHeight || size.inputWidth >= size.outputWidth ||
size.inputHeight >= size.outputHeight || size.outputWidth > limits.maxTextureDimension2D ||
size.outputHeight > limits.maxTextureDimension2D ||
(size.format != wgpu::TextureFormat::RGBA8Unorm && size.format != wgpu::TextureFormat::BGRA8Unorm)) {
error = "MetalFX requires smaller input dimensions and an RGBA8/BGRA8 unorm target";
return {};
}
if (g_liveResources.load() >= kMaxLiveResources) {
return {};
}
auto native = dawn::native::metal::GetMTLDevice(device.Get());
auto resources = std::make_shared<Resources>();
auto descriptor = [MTLFXSpatialScalerDescriptor new];
descriptor.inputWidth = size.inputWidth;
descriptor.inputHeight = size.inputHeight;
descriptor.outputWidth = size.outputWidth;
descriptor.outputHeight = size.outputHeight;
descriptor.colorTextureFormat = size.format == wgpu::TextureFormat::BGRA8Unorm
? MTLPixelFormatBGRA8Unorm : MTLPixelFormatRGBA8Unorm;
descriptor.outputTextureFormat = descriptor.colorTextureFormat;
descriptor.colorProcessingMode = MTLFXSpatialScalerColorProcessingModePerceptual;
resources->scaler = [descriptor newSpatialScalerWithDevice:native];
resources->nativeQueue = [native newCommandQueue];
resources->event = [native newSharedEvent];
if (!resources->scaler || !resources->nativeQueue || !resources->event) {
error = "Could not create MetalFX spatial resources";
return {};
}
auto outputDescriptor = [MTLTextureDescriptor
texture2DDescriptorWithPixelFormat:descriptor.outputTextureFormat
width:size.outputWidth height:size.outputHeight mipmapped:NO];
outputDescriptor.storageMode = MTLStorageModePrivate;
outputDescriptor.usage = resources->scaler.outputTextureUsage;
resources->privateOutput = [native newTextureWithDescriptor:outputDescriptor];
if (!resources->privateOutput) { error = "Could not allocate MetalFX private output"; return {}; }
device.PushErrorScope(wgpu::ErrorFilter::Validation);
device.PushErrorScope(wgpu::ErrorFilter::OutOfMemory);
device.PushErrorScope(wgpu::ErrorFilter::Internal);
bool allocated = resources->input.create(device, native, size.inputWidth, size.inputHeight,
size.format, resources->scaler.colorTextureUsage, wgpu::TextureUsage::RenderAttachment);
allocated = allocated && resources->output.create(device, native, size.outputWidth, size.outputHeight,
size.format, MTLTextureUsageShaderRead, wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc);
if (allocated) {
wgpu::SharedFenceMTLSharedEventDescriptor event{};
event.sharedEvent = (__bridge void*)resources->event;
wgpu::SharedFenceDescriptor fence{};
fence.nextInChain = &event;
resources->fence = device.ImportSharedFence(&fence);
allocated = resources->fence != nullptr;
}
for (int i = 0; i < 3; ++i) {
if (!pop_scope(instance, device, error)) allocated = false;
}
if (!allocated) {
if (error.empty()) error = "Could not import MetalFX IOSurface textures into Dawn";
return {};
}
resources->scaler.colorTexture = resources->input.metal;
resources->scaler.outputTexture = resources->privateOutput;
resources->scaler.inputContentWidth = size.inputWidth;
resources->scaler.inputContentHeight = size.inputHeight;
return std::make_unique<MetalSpatialScaler>(instance, device, std::move(resources));
}
}
error = "MetalFX requires macOS 13 or newer";
return {};
}
} // namespace aurora::webgpu::metalfx
-11
View File
@@ -1,11 +0,0 @@
#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
+1 -27
View File
@@ -46,7 +46,6 @@ SDL_Window* g_window;
SDL_Renderer* g_renderer;
float g_frameBufferScale = 0.f;
bool g_frameBufferAspectFit = true;
std::atomic_bool g_forceAspect169{false};
bool g_presentSurfaceFill = false;
int g_presentAspectWidth = 0;
int g_presentAspectHeight = 0;
@@ -526,20 +525,7 @@ AuroraWindowSize get_window_size() {
int fb_w = native_fb_w;
int fb_h = native_fb_h;
const auto [baseW, baseH] = vi::configured_fb_size();
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) {
if (g_frameBufferAspectFit && baseW > 0 && baseH > 0) {
float renderScale = g_frameBufferScale > 0.f ? g_frameBufferScale : 1.f;
if (g_frameBufferScale <= 0.f) {
renderScale = std::min(static_cast<float>(native_fb_w) / static_cast<float>(baseW),
@@ -773,14 +759,6 @@ void set_frame_buffer_aspect_fit(bool fit) {
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) {
g_presentSurfaceFill = fill;
}
@@ -803,10 +781,6 @@ void unlock_present_aspect_ratio() {
}
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) {
// Queried once per presentation snapshot; use the cached native client size
// instead of re-entering SDL for a value the window procedure already knows.
-1
View File
@@ -56,7 +56,6 @@ void sync_frame_buffer_size() noexcept;
void request_frame_buffer_resize();
void set_frame_buffer_scale(float scale);
void set_frame_buffer_aspect_fit(bool fit);
void set_force_aspect_16_9(bool force);
void set_present_surface_fill(bool fill);
void lock_present_aspect_ratio(int width, int height);
void unlock_present_aspect_ratio();
+1 -1
View File
@@ -5,7 +5,7 @@ if (NOT TARGET gtest)
FetchContent_Declare(googletest
URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.tar.gz
URL_HASH SHA256=65fab701d9829d38cb77c14acdc431d2108bfdbf8979e40eb8ae567edf10b27c
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(googletest)
+4 -4
View File
@@ -4267,7 +4267,7 @@ TEST_F(GXFifoTest, CopyTexColorFormatMarksResolvePersistent) {
EXPECT_TRUE(records.front().persistentCopy);
}
TEST_F(GXFifoTest, RecurringColorCopyKeepsLaterResolveSkippable) {
TEST_F(GXFifoTest, RecurringColorCopyPreservesEveryResolve) {
std::array<u8, 152 * 114 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24;
@@ -4281,7 +4281,7 @@ TEST_F(GXFifoTest, RecurringColorCopyKeepsLaterResolveSkippable) {
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 2u);
EXPECT_TRUE(records[0].persistentCopy);
EXPECT_FALSE(records[1].persistentCopy);
EXPECT_TRUE(records[1].persistentCopy);
}
TEST_F(GXFifoTest, ColorCopyAfterFrameGapRegainsPersistentProtection) {
@@ -4301,7 +4301,7 @@ TEST_F(GXFifoTest, ColorCopyAfterFrameGapRegainsPersistentProtection) {
EXPECT_TRUE(records[1].persistentCopy);
}
TEST_F(GXFifoTest, CopyTexDepthFormatKeepsResolveSkippable) {
TEST_F(GXFifoTest, CopyTexDepthFormatPreservesResolve) {
std::array<u8, 4 * 4 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24;
@@ -4311,7 +4311,7 @@ TEST_F(GXFifoTest, CopyTexDepthFormatKeepsResolveSkippable) {
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u);
EXPECT_FALSE(records.front().persistentCopy);
EXPECT_TRUE(records.front().persistentCopy);
}
TEST_F(GXFifoTest, CopyDispResolveIsNotPersistent) {
-4
View File
@@ -50,9 +50,6 @@ void reset_uniform_allocations() noexcept;
const std::vector<uint8_t>& uniform_allocation(size_t index) noexcept;
void use_draw_command_tracking(bool enabled) noexcept;
void use_real_vertex_format_helpers(bool enabled) noexcept;
// Admit this many draws, then refuse one as if the staging batch were full.
void refuse_staging_admission_after(uint32_t admissions) noexcept;
void reset_staging_capacity() noexcept;
} // namespace aurora::gfx::testing
class GXFifoTest : public ::testing::Test {
@@ -68,7 +65,6 @@ protected:
aurora::gfx::testing::reset_resolve_pass_records();
aurora::gfx::testing::reset_vertex_push_record();
aurora::gfx::testing::use_real_vertex_format_helpers(false);
aurora::gfx::testing::reset_staging_capacity();
}
// Copy the internal FIFO buffer contents and clear it
+2 -30
View File
@@ -34,9 +34,6 @@ std::optional<aurora::gx::DrawData> s_lastGxDraw;
bool s_trackDrawCommands = false;
bool s_useRealVertexFormatHelpers = false;
std::deque<std::vector<uint8_t>> s_uniformAllocations;
std::optional<uint32_t> s_stagingAdmissionsBeforeRefusal;
bool s_stagingRefused = false;
uint64_t s_stagingSplitCount = 0;
} // namespace
// --- aurora::g_config ---
@@ -304,23 +301,8 @@ std::pair<ByteBuffer, Range> copy_uniform(Range source) {
uint32_t align_uniform(uint32_t value) { return (value + 255u) & ~255u; }
uint64_t staging_uniform_bytes(uint64_t value) { return staging_padded(value, 256); }
uint64_t staging_storage_bytes(uint64_t value) { return staging_padded(value, 256); }
bool staging_has_space(const StagingSizes&) {
if (!s_stagingAdmissionsBeforeRefusal) return true;
if (*s_stagingAdmissionsBeforeRefusal > 0) {
--*s_stagingAdmissionsBeforeRefusal;
return true;
}
// Refuse once, as a full batch would; the split that follows makes room again.
s_stagingAdmissionsBeforeRefusal.reset();
s_stagingRefused = true;
return false;
}
void split_staging_batch() {
if (!s_stagingRefused) throw StagingCapacityError("Unexpected split in FIFO unit test");
s_stagingRefused = false;
++s_stagingSplitCount;
}
uint64_t staging_split_count() noexcept { return s_stagingSplitCount; }
bool staging_has_space(const StagingSizes&) { return true; }
void split_staging_batch() { throw StagingCapacityError("Unexpected split in FIFO unit test"); }
Vec2<uint32_t> get_render_target_size() noexcept { return s_renderTargetSize; }
Vec2<uint32_t> get_frame_buffer_size() noexcept { return s_renderTargetSize; }
@@ -363,15 +345,6 @@ void use_draw_command_tracking(bool enabled) noexcept {
void use_real_vertex_format_helpers(bool enabled) noexcept {
s_useRealVertexFormatHelpers = enabled;
}
void refuse_staging_admission_after(uint32_t admissions) noexcept {
s_stagingAdmissionsBeforeRefusal = admissions;
s_stagingRefused = false;
}
void reset_staging_capacity() noexcept {
s_stagingAdmissionsBeforeRefusal.reset();
s_stagingRefused = false;
s_stagingSplitCount = 0;
}
} // namespace aurora::gfx::testing
// --- Pipeline/draw command stubs ---
@@ -552,7 +525,6 @@ std::optional<TextureHandle> find_replacement(const GXTexObj_&) noexcept { retur
namespace aurora::window {
AuroraWindowSize get_window_size() { return {640, 480, 640, 480, 640, 480, 1.0f}; }
void set_frame_buffer_aspect_fit(bool) {}
void set_force_aspect_16_9(bool) {}
} // namespace aurora::window
// --- WebGPU C API stubs (prevent linker errors from wgpu:: destructors) ---
@@ -1,28 +0,0 @@
cmake_minimum_required(VERSION 3.25)
project(metalfx_interop LANGUAGES CXX)
if(NOT APPLE)
message(FATAL_ERROR "The MetalFX interoperability probe requires macOS")
endif()
enable_language(OBJCXX)
set(CMAKE_OBJCXX_STANDARD 20)
set(CMAKE_OBJCXX_STANDARD_REQUIRED ON)
set(CMAKE_OSX_DEPLOYMENT_TARGET 13.0)
# Use the same Dawn package as Aurora; no separate download or renderer build.
find_package(Threads REQUIRED)
find_package(Dawn CONFIG REQUIRED)
add_executable(metalfx_interop main.mm ../../lib/webgpu/metalfx.mm)
target_include_directories(metalfx_interop PRIVATE ../../lib)
target_compile_options(metalfx_interop PRIVATE -fobjc-arc -Wall -Wextra)
target_link_libraries(metalfx_interop PRIVATE dawn::webgpu_dawn
"-framework MetalFX" "-framework Metal" "-framework IOSurface" "-framework Foundation")
enable_testing()
add_test(NAME metalfx_interop COMMAND metalfx_interop)
set_tests_properties(metalfx_interop PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
add_executable(metalfx_stub_test stub_test.cpp ../../lib/webgpu/metalfx_stub.cpp)
target_include_directories(metalfx_stub_test PRIVATE ../../lib)
target_compile_features(metalfx_stub_test PRIVATE cxx_std_20)
target_link_libraries(metalfx_stub_test PRIVATE dawn::webgpu_dawn)
add_test(NAME metalfx_stub COMMAND metalfx_stub_test)
-124
View File
@@ -1,124 +0,0 @@
# MetalFX spatial upscaling: renderer integration and tests
Aurora can now upscale the completed game image with MetalFX before aspect-fit
presentation and ImGui composition. It is opt-in and requires macOS 13+, a
supported Metal device, and Dawn IOSurface/shared-event support. Other backends
and builds without the MetalFX SDK use a stub and the existing presentation path.
The MetalFX framework is weak-linked; the game's deployment target is unchanged.
## Trying the renderer integration
Use F10 → Graphics → MetalFX spatial upscaling. Then use the existing
Resolution control to render below the output viewport's size.
Both source dimensions must be smaller than the output dimensions. Equal-size
rendering, supersampling, and unsupported source formats bypass MetalFX. In
particular, Auto (window size) generally offers no upscaling opportunity.
The F10 toggle is saved in `Config.toml` as
`video.metalfx_spatial_upscaling`. It uses these thread-safe Aurora entry points:
- `aurora_set_metalfx_spatial(bool)` requests a change at the next sealed frame.
- `aurora_get_metalfx_spatial()` returns the requested setting.
- `aurora_is_metalfx_spatial_supported()` reports device/build support.
- `aurora_get_metalfx_status()` distinguishes Disabled, Unsupported,
Not Upscaling, Active, and Error. A busy resize-retirement pool temporarily
bypasses upscaling and retries on a later frame. Other upscaler errors log a
reason and use normal presentation until a disabled frame resets the error.
The game's HUD is part of the source image and is upscaled. ImGui/F10/FPS overlays
are composed afterward at output resolution. Existing source-frame captures
still capture the original source image. The interpolation snapshot call sites
all use the same upscaling hook; game-specific interpolation remains untested.
## GPU path and ownership
1. Request Dawn's `SharedTextureMemoryIOSurface` and `SharedFenceMTLSharedEvent`
features when the Metal adapter supports both. Use that Dawn device's native
`MTLDevice`, not a separately selected default device.
2. Cache MaxInterpolatedFrames + 1 upscaling slots with IOSurface-backed input and output textures,
a spatial scaler, a private MetalFX output, and shared-event dependencies.
Check texture formats, dimensions, usages, and device size limits on creation.
3. Begin Dawn input access, copy the completed game image at its source size,
and submit the scene plus copy. End input access and wait for Dawn's
`commandsScheduledFuture` before submitting dependent native Metal work.
4. On the native queue, wait for input rendering and any prior Dawn consumption
of the shared output. Encode MetalFX into its required **private** output
texture, then GPU-blit the result into the output IOSurface and signal an event.
5. After native scheduling, begin Dawn output access with that event/value.
Composite the upscaled image into the existing content viewport, retaining
letterboxing, then draw ImGui. Submit and end output access. Reuse observes
both Dawn-to-Metal and Metal-to-Dawn event dependencies.
6. GPU completion callbacks retain resources after a cache entry is replaced,
disabled, or shut down. At most eight resource sets may exist (four current
plus four retiring); rapid resizing cannot allocate an unbounded queue.
CPU scheduling waits remain, but there are no CPU image transfers or per-frame
GPU-completion waits in the upscaling path. There is one source-size GPU copy
and one full-output GPU blit. Their cost must be measured before promising a
performance gain. The input copy follows the existing perceptual/unorm sampling
path. sRGB texture formats bypass MetalFX to avoid implicit color conversion.
## Standalone GPU regression test
The test builds the actual `lib/webgpu/metalfx.mm` implementation. Point
`Dawn_DIR` at the package used by an existing Aurora build:
```sh
cmake -S aurora-main/tests/metalfx_interop -B build-metalfx-interop \
-DDawn_DIR="/absolute/path/to/dawn_prebuilt-src/lib/cmake/Dawn" \
-DCMAKE_BUILD_TYPE=Release
cmake --build build-metalfx-interop
MTL_DEBUG_LAYER=1 MTL_SHADER_VALIDATION=1 \
ctest --test-dir build-metalfx-interop --output-on-failure -V
```
The GPU test returns 77 (CTest **Skipped**) when no Metal adapter, required
sharing features, or spatial scaler is available. A skip is not evidence of
interoperability. Sandboxed processes may need GPU access. CTest imposes a
60-second timeout. The separate stub test needs no GPU.
Tested on Apple M3, macOS 26.5.1, using Aurora's existing Dawn package
(`v20260603.191052`). Metal API and GPU validation were enabled:
| Formats | Input | Output | Frames per format |
| --- | --- | --- | --- |
| RGBA8Unorm, BGRA8Unorm | 64 × 48 | 128 × 96 | 24 |
| RGBA8Unorm, BGRA8Unorm | 320 × 180 | 480 × 270 | 24 |
| RGBA8Unorm, BGRA8Unorm | 960 × 540 | 1920 × 1080 | 24 |
All 144 frames and 2,304 interior pixel samples passed. Red changes per frame;
green and blue distinguish left/right and top/bottom. All channels are checked
within five 8-bit levels, catching stale images, orientation/channel mistakes,
and missing output. Tests cover 1.5× and 2× scaling, padded readback rows, slot
reuse, dropping wrappers before readback completion, invalid dimensions/sRGB
formats, the eight-set allocation bound, and the unavailable-backend stub.
Readback is only the test oracle and is absent from the game upscaling path.
These samples do not measure reconstruction quality at edges or race performance.
## Windowed presentation test
This optional target exercises Aurora's actual frame submission and presentation
with a synthetic source and an ImGui overlay. It requires no Wii game data and
creates an automatically closing test window. Add the option to an existing
from-source runtime build (the normal dependency/provider options still apply):
```sh
cmake -S runtime -B build-macos -DCMAKE_BUILD_TYPE=Release \
-DAURORA_BUILD_METALFX_PRESENTATION_TEST=ON
cmake --build build-macos --target metalfx_presentation_test
MTL_DEBUG_LAYER=1 MTL_SHADER_VALIDATION=1 \
./build-macos/aurora-build/metalfx_presentation_test
```
On the same M3, all 84 frames passed with Metal API/GPU validation: disabled,
enabled, window resize, 4:3/16:9 aspect changes, native-size bypass, disable, and
re-enable. Assertions check renderer status and errors; this is not a pixel-level
verification of the window image. The core build and macOS 12 deployment-target
availability compilation also passed. Full Mario Kart gameplay, race performance,
visual quality, Intel Macs, other Apple GPUs, older macOS runtime versions, and
non-macOS full builds remain untested.
References: [Apple MetalFX](https://developer.apple.com/documentation/metalfx),
[spatial scaler requirements](https://developer.apple.com/documentation/metalfx/mtlfxspatialscaler),
and the installed Dawn `MetalBackend.h` / `webgpu_cpp.h` APIs.
-269
View File
@@ -1,269 +0,0 @@
#import <Foundation/Foundation.h>
#import <IOSurface/IOSurface.h>
#import <Metal/Metal.h>
#include "webgpu/metalfx.hpp"
#include <dawn/native/MetalBackend.h>
#include <webgpu/webgpu_cpp.h>
#include <array>
#include <atomic>
#include <cmath>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string_view>
#include <vector>
namespace {
constexpr uint64_t kTimeoutNs = 10'000'000'000;
constexpr unsigned kFrames = 24;
std::atomic<unsigned> g_errors{0};
void require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
void wait(const wgpu::Instance& instance, wgpu::Future future) {
require(instance.WaitAny(future, kTimeoutNs) == wgpu::WaitStatus::Success,
"Dawn operation timed out or failed");
}
void runCase(const wgpu::Instance& instance, const wgpu::Device& device,
bool bgra, uint32_t width, uint32_t height,
uint32_t outWidth, uint32_t outHeight) {
const auto format = bgra ? wgpu::TextureFormat::BGRA8Unorm : wgpu::TextureFormat::RGBA8Unorm;
using namespace aurora::webgpu::metalfx;
std::array<std::unique_ptr<SpatialScaler>, 3> slots;
for (auto& slot : slots) {
std::string error;
slot = create(instance, device, {width, height, outWidth, outHeight, format}, error);
if (!slot) {
throw std::runtime_error(error.empty()
? "MetalFX resource pool was still busy retiring earlier slots"
: error);
}
}
// Asymmetric quadrants expose channel swaps, vertical flips, and stale frames.
wgpu::ShaderSourceWGSL source{};
source.code = R"(
@group(0) @binding(0) var<uniform> params: vec4f;
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
let p = array(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
return vec4f(p[i], 0, 1);
}
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
return vec4f(params.x, select(0.2, 0.8, p.x >= params.y / 2),
select(0.3, 0.7, p.y >= params.z / 2), 1);
}
)";
wgpu::ShaderModuleDescriptor shaderDescriptor{};
shaderDescriptor.nextInChain = &source;
auto shader = device.CreateShaderModule(&shaderDescriptor);
wgpu::ColorTargetState target{};
target.format = format;
wgpu::FragmentState fragment{};
fragment.module = shader;
fragment.entryPoint = "fs";
fragment.targetCount = 1;
fragment.targets = &target;
wgpu::RenderPipelineDescriptor pipelineDescriptor{};
pipelineDescriptor.vertex.module = shader;
pipelineDescriptor.vertex.entryPoint = "vs";
pipelineDescriptor.fragment = &fragment;
auto pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
auto dawnQueue = device.GetQueue();
const uint32_t bytesPerRow = (outWidth * 4 + 255) & ~255u;
const uint64_t readbackSize = uint64_t(bytesPerRow) * outHeight;
std::vector<wgpu::Buffer> readbacks;
for (unsigned frame = 0; frame < kFrames; ++frame) {
auto& slot = slots[frame % slots.size()];
require(slot->begin_input(), "Production MetalFX begin_input failed");
const std::array<float, 4> params{0.2f + float(frame % 5) * 0.1f,
float(width), float(height), 0};
wgpu::BufferDescriptor uniformDescriptor{};
uniformDescriptor.size = sizeof(params);
uniformDescriptor.usage = wgpu::BufferUsage::Uniform | wgpu::BufferUsage::CopyDst;
auto uniform = device.CreateBuffer(&uniformDescriptor);
dawnQueue.WriteBuffer(uniform, 0, params.data(), sizeof(params));
wgpu::BindGroupEntry entry{};
entry.binding = 0;
entry.buffer = uniform;
entry.size = sizeof(params);
wgpu::BindGroupDescriptor bindDescriptor{};
bindDescriptor.layout = pipeline.GetBindGroupLayout(0);
bindDescriptor.entryCount = 1;
bindDescriptor.entries = &entry;
auto bindGroup = device.CreateBindGroup(&bindDescriptor);
auto encoder = device.CreateCommandEncoder();
wgpu::RenderPassColorAttachment attachment{};
attachment.view = slot->input_view();
attachment.loadOp = wgpu::LoadOp::Clear;
attachment.storeOp = wgpu::StoreOp::Store;
wgpu::RenderPassDescriptor passDescriptor{};
passDescriptor.colorAttachmentCount = 1;
passDescriptor.colorAttachments = &attachment;
auto pass = encoder.BeginRenderPass(&passDescriptor);
pass.SetPipeline(pipeline);
pass.SetBindGroup(0, bindGroup);
pass.Draw(3);
pass.End();
auto render = encoder.Finish();
dawnQueue.Submit(1, &render);
if (!slot->upscale()) throw std::runtime_error(slot->error());
wgpu::BufferDescriptor readbackDescriptor{};
readbackDescriptor.size = readbackSize;
readbackDescriptor.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead;
auto readback = device.CreateBuffer(&readbackDescriptor);
encoder = device.CreateCommandEncoder();
wgpu::TexelCopyTextureInfo copySource{};
copySource.texture = slot->output_texture();
wgpu::TexelCopyBufferInfo destination{};
destination.buffer = readback;
destination.layout.bytesPerRow = bytesPerRow;
destination.layout.rowsPerImage = outHeight;
const wgpu::Extent3D extent{outWidth, outHeight, 1};
encoder.CopyTextureToBuffer(&copySource, &destination, &extent);
auto copy = encoder.Finish();
dawnQueue.Submit(1, &copy);
if (!slot->end_output()) throw std::runtime_error(slot->error());
readbacks.push_back(std::move(readback));
}
// Model toggle/resize immediately after submission, while either queue may
// still be consuming these textures. Production completion callbacks must
// keep the resources alive after the cache drops its wrappers.
slots = {};
// Readback is only the test oracle. No CPU image transfer or GPU completion
// wait occurs between Dawn rendering, MetalFX, and Dawn consumption above.
for (unsigned frame = 0; frame < kFrames; ++frame) {
bool mapped = false;
auto& readback = readbacks[frame];
wait(instance, readback.MapAsync(wgpu::MapMode::Read, 0, readbackSize,
wgpu::CallbackMode::WaitAnyOnly, [&mapped](wgpu::MapAsyncStatus status, wgpu::StringView) {
mapped = status == wgpu::MapAsyncStatus::Success;
}));
require(mapped, "Output readback mapping failed");
const auto* bytes = static_cast<const uint8_t*>(readback.GetConstMappedRange());
require(bytes != nullptr, "Output readback pointer is null");
for (unsigned y = 0; y < 4; ++y) {
for (unsigned x = 0; x < 4; ++x) {
const unsigned px = (2 * x + 1) * outWidth / 8;
const unsigned py = (2 * y + 1) * outHeight / 8;
const auto* pixel = bytes + py * bytesPerRow + px * 4;
const std::array<float, 4> expected{
0.2f + float(frame % 5) * 0.1f, x >= 2 ? 0.8f : 0.2f,
y >= 2 ? 0.7f : 0.3f, 1};
for (unsigned c = 0; c < 4; ++c) {
const unsigned channel = bgra && c != 1 && c != 3 ? 2 - c : c;
if (std::abs(int(pixel[channel]) - int(std::lround(expected[c] * 255))) > 5) {
std::cerr << "Pixel mismatch: frame=" << frame << " x=" << px << " y=" << py
<< " channel=" << c << " actual=" << int(pixel[channel])
<< " expected=" << std::lround(expected[c] * 255) << '\n';
throw std::runtime_error("MetalFX output failed image validation");
}
}
}
}
readback.Unmap();
}
require(g_errors.load() == 0, "Dawn reported validation errors or device loss");
std::cout << "PASS " << (bgra ? "BGRA8" : "RGBA8") << ' ' << width << 'x' << height
<< " -> " << outWidth << 'x' << outHeight << ": " << kFrames
<< " frames, 3 reused slots, 16 pixel samples/frame\n";
}
int run() {
const wgpu::InstanceFeatureName timedWait = wgpu::InstanceFeatureName::TimedWaitAny;
wgpu::InstanceDescriptor instanceDescriptor{};
instanceDescriptor.requiredFeatureCount = 1;
instanceDescriptor.requiredFeatures = &timedWait;
auto instance = wgpu::CreateInstance(&instanceDescriptor);
require(instance != nullptr, "Dawn instance creation failed");
wgpu::Adapter adapter;
wgpu::RequestAdapterOptions options{};
options.backendType = wgpu::BackendType::Metal;
wait(instance, instance.RequestAdapter(&options, wgpu::CallbackMode::WaitAnyOnly,
[&adapter](wgpu::RequestAdapterStatus status, wgpu::Adapter result, wgpu::StringView message) {
if (status == wgpu::RequestAdapterStatus::Success) adapter = std::move(result);
else std::cerr << "Adapter: " << std::string_view(message) << '\n';
}));
if (!adapter) { std::cout << "SKIP: no Dawn Metal adapter\n"; return 77; }
const std::array features{wgpu::FeatureName::SharedTextureMemoryIOSurface,
wgpu::FeatureName::SharedFenceMTLSharedEvent};
for (auto feature : features) {
if (!adapter.HasFeature(feature)) {
std::cout << "SKIP: Dawn adapter lacks IOSurface/shared-event interoperability\n";
return 77;
}
}
wgpu::DeviceDescriptor descriptor{};
descriptor.requiredFeatureCount = features.size();
descriptor.requiredFeatures = features.data();
descriptor.SetUncapturedErrorCallback(
[](const wgpu::Device&, wgpu::ErrorType, wgpu::StringView message) {
++g_errors;
std::cerr << "Dawn error: " << std::string_view(message) << '\n';
});
descriptor.SetDeviceLostCallback(wgpu::CallbackMode::AllowSpontaneous,
[](const wgpu::Device&, wgpu::DeviceLostReason reason, wgpu::StringView message) {
if (reason != wgpu::DeviceLostReason::Destroyed) {
++g_errors;
std::cerr << "Device lost: " << std::string_view(message) << '\n';
}
});
wgpu::Device device;
wait(instance, adapter.RequestDevice(&descriptor, wgpu::CallbackMode::WaitAnyOnly,
[&device](wgpu::RequestDeviceStatus status, wgpu::Device result, wgpu::StringView message) {
if (status == wgpu::RequestDeviceStatus::Success) device = std::move(result);
else std::cerr << "Device: " << std::string_view(message) << '\n';
}));
require(device != nullptr, "Dawn device creation failed");
id<MTLDevice> native = dawn::native::metal::GetMTLDevice(device.Get());
require(native != nil, "Dawn native Metal device is unavailable");
std::cout << "GPU: " << native.name.UTF8String << '\n';
if (!aurora::webgpu::metalfx::supported(device, wgpu::BackendType::Metal)) {
std::cout << "SKIP: GPU does not support MetalFX spatial scaling\n";
return 77;
}
require(!aurora::webgpu::metalfx::supported(device, wgpu::BackendType::Vulkan),
"MetalFX must reject non-Metal backends");
std::string error;
require(!aurora::webgpu::metalfx::create(instance, device,
{128, 96, 128, 96, wgpu::TextureFormat::RGBA8Unorm}, error) && !error.empty(),
"MetalFX must reject equal-size input/output");
require(!aurora::webgpu::metalfx::create(instance, device,
{128, 96, 256, 192, wgpu::TextureFormat::RGBA8UnormSrgb}, error),
"MetalFX must reject implicit sRGB conversion");
{
using namespace aurora::webgpu::metalfx;
std::array<std::unique_ptr<SpatialScaler>, 8> resources;
for (auto& scaler : resources) {
scaler = create(instance, device, {64, 48, 128, 96, wgpu::TextureFormat::RGBA8Unorm}, error);
require(scaler != nullptr, "Could not fill the MetalFX resource pool");
}
require(!create(instance, device, {64, 48, 128, 96, wgpu::TextureFormat::RGBA8Unorm}, error)
&& error.empty(), "A full retirement pool must defer allocation without a fatal error");
}
for (bool bgra : {false, true}) {
runCase(instance, device, bgra, 64, 48, 128, 96);
runCase(instance, device, bgra, 320, 180, 480, 270);
runCase(instance, device, bgra, 960, 540, 1920, 1080);
}
return 0;
}
} // namespace
int main() {
@autoreleasepool {
try { return run(); }
catch (const std::exception& error) {
std::cerr << "FAIL: " << error.what() << '\n';
return 1;
}
}
}
@@ -1,124 +0,0 @@
// 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;
}
@@ -1,9 +0,0 @@
#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;
}
@@ -88,31 +88,6 @@ TEST_F(GXFifoTest, SingleExpandedPrimitiveCannotMergeWithTriangles) {
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{0, 1, 2}));
}
TEST_F(GXFifoTest, DisplayListSubmitsStagingWhenADrawDoesNotFit) {
__GXSetDirtyState();
aurora::gx::fifo::clear_buffer();
g_gxState.lastVtxFmt = GX_VTXFMT0;
g_gxState.lastVtxSize = 1;
// admitted=1 refuses the middle draw; admitted=2 refuses the last, so only a replay of the refused draw pushes 0x33.
for (const u32 admitted : {1u, 2u}) for (const bool bigEndian : {true, false}) {
std::vector<u8> list;
for (const u8 fill : {0x11, 0x22, 0x33}) {
auto bytes = draw(GX_TRIANGLES, 3);
if (!bigEndian) std::swap(bytes[1], bytes[2]);
std::fill(bytes.begin() + 3, bytes.end(), fill);
list.insert(list.end(), bytes.begin(), bytes.end());
}
const auto splitsBefore = aurora::gfx::staging_split_count();
aurora::gfx::testing::refuse_staging_admission_after(admitted);
if (bigEndian) GXCallDisplayList(list.data(), static_cast<u32>(list.size()));
else GXCallDisplayListLE(list.data(), static_cast<u32>(list.size()));
EXPECT_EQ(aurora::gfx::staging_split_count() - splitsBefore, 1u)
<< "admitted=" << admitted << " bigEndian=" << bigEndian;
EXPECT_EQ(aurora::gfx::testing::last_pushed_vertices(), (std::vector<u8>{0x33, 0x33, 0x33}))
<< "admitted=" << admitted << " bigEndian=" << bigEndian;
}
}
TEST(StagingMapping, RetiredCallbacksCannotPublishAnotherBuffersReadiness) {
using namespace aurora::gfx;
StagingMapState state;
+143 -129
View File
@@ -1,79 +1,27 @@
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()
cmake_minimum_required(VERSION 3.16)
project(mkw_recompiled)
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
message(FATAL_ERROR "WiiCompiled requires a 64-bit Clang toolchain")
endif()
if(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)$")
set(MKW_PLATFORM_WINDOWS TRUE)
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|arm64|ARM64)$")
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64)$")
# The first native macOS target is Apple Silicon. Intel and universal
# binaries remain future compatibility work; do not silently claim them.
set(MKW_PLATFORM_MACOS TRUE)
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)$")
set(MKW_PLATFORM_LINUX TRUE)
else()
message(FATAL_ERROR
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS x86_64/arm64")
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS arm64")
endif()
if(MKW_PLATFORM_WINDOWS)
# CMake and Ninja write response files with Windows quoting, but clang tokenizes them with
# POSIX rules by default, where a bare apostrophe in a path opens a quoted span.
add_link_options(--rsp-quoting=windows)
endif()
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
message(FATAL_ERROR "WiiCompiled only supports Release builds")
endif()
option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON)
option(MKW_BUILD_PSQ_TESTS "Build focused PSQ ISA tests" OFF)
set(MKW_X86_CPU_PROFILE "v3" CACHE STRING
"Minimum x86-64 CPU profile for translated products (v2 or v3)")
set_property(CACHE MKW_X86_CPU_PROFILE PROPERTY STRINGS v2 v3)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$" AND
NOT MKW_X86_CPU_PROFILE MATCHES "^(v2|v3)$")
message(FATAL_ERROR "MKW_X86_CPU_PROFILE must be v2 or v3 (got '${MKW_X86_CPU_PROFILE}')")
endif()
set(MKW_SETUP_VERSION "" CACHE STRING "WiiCompiled version recorded in the runtime log")
option(MKW_PLATFORM_TESTS_ONLY "Build host-platform tests without graphics dependencies or game assets" OFF)
include("${CMAKE_CURRENT_LIST_DIR}/cmake/HostLibraries.cmake")
if(MKW_PLATFORM_TESTS_ONLY)
if(MKW_BUILD_PRODUCTS)
message(FATAL_ERROR "MKW_PLATFORM_TESTS_ONLY requires MKW_BUILD_PRODUCTS=OFF")
endif()
add_compile_definitions(NOMINMAX)
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PlatformTests.cmake")
return()
endif()
# 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
@@ -94,8 +42,6 @@ set(MKW_NATIVE_PREBUILT_DIR "" CACHE PATH
# package can be harvested from it.
set(MKW_NATIVE_PREBUILT_EXPORT_DIR "" CACHE PATH
"Maintainer-only: write a description of the configured aurora build here")
option(MKW_USE_PREBUILT_RUNTIME
"Link the native prebuilt package's runtime archive when it matches this workspace" ON)
# The two Crypto++ configuration macros are load-bearing for ABI: they shape the
# structs and inline code the headers generate, so a first-party TU including
@@ -113,6 +59,20 @@ target_include_directories(mkw_pugixml PUBLIC third_party/pugixml)
target_compile_features(mkw_pugixml PUBLIC cxx_std_17)
set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF)
# Linux guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric
# stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
# asymmetric resume/yield coroutine libraries, which would need every call site restructured).
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers
# and macOS uses the project's x18-safe AArch64 assembly backend, so this target is Linux-only.
if(MKW_PLATFORM_LINUX)
add_library(mkw_libco STATIC third_party/libco/libco.c)
add_library(mkw::libco ALIAS mkw_libco)
target_include_directories(mkw_libco PUBLIC third_party/libco)
set_target_properties(mkw_libco PROPERTIES UNITY_BUILD OFF)
endif()
# Runtime configuration is real TOML, parsed by toml11 rather than a project-
# specific line parser. Keep it header-only and vendored so disconnected release
# builds have exactly the same parser as developer builds.
@@ -153,10 +113,11 @@ endif()
# 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-
# simple libraries (see Crypto++ above, similarly stripped of ASM/SIMD for portability).
# The from-source build fetches a pinned upstream tarball with a checked SHA-256.
# The Linux native prebuilt package instead ships its compiled archives and headers.
# Bump both values in cmake/MbedTLSPin.cmake when updating; the hash comes from
# upstream's signed `mbedtls-<version>-sha256sum.txt` release asset.
# Fetched at build time from a pinned upstream release tarball with a checked SHA-256, the same way
# aurora-main's own dependencies (SDL, zlib, etc.) are pulled in - not committed as a vendored
# source tree, so the repository ships the compiled dependency rather than ~280 tracked upstream
# files. Bump MKW_MBEDTLS_VERSION/MKW_MBEDTLS_SHA256 together when updating; the hash comes from
# 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
# platform-independent, but it is only populated where network_ssl.cpp actually compiles the mbed
@@ -167,22 +128,20 @@ endif()
add_library(mkw_mbedtls INTERFACE)
add_library(mkw::mbedtls ALIAS mkw_mbedtls)
if(NOT MKW_PLATFORM_WINDOWS)
include("${CMAKE_CURRENT_LIST_DIR}/cmake/MbedTLSPin.cmake")
if(NOT MKW_NATIVE_PREBUILT_DIR)
include(FetchContent)
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_HASH SHA256=${MKW_MBEDTLS_SHA256}
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
)
# Subproject mode already disables testing and codegen; it still enables programs.
set(ENABLE_PROGRAMS OFF CACHE BOOL "" FORCE)
set(ENABLE_TESTING OFF CACHE BOOL "" FORCE)
set(MBEDTLS_FATAL_WARNINGS OFF CACHE BOOL "" FORCE)
set(DISABLE_PACKAGE_CONFIG_AND_INSTALL ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(mkw_mbedtls_upstream)
target_link_libraries(mkw_mbedtls INTERFACE MbedTLS::mbedtls MbedTLS::mbedx509 MbedTLS::mbedcrypto)
endif()
include(FetchContent)
set(MKW_MBEDTLS_VERSION "3.6.7")
set(MKW_MBEDTLS_SHA256 "a7e8bcbec0e6f761b4af24f25677626b35f762f68eef79c08677a363212d11f6")
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_HASH SHA256=${MKW_MBEDTLS_SHA256})
# Subproject mode already defaults ENABLE_TESTING off and skips codegen (GEN_FILES), but
# ENABLE_PROGRAMS defaults on and installation/package-config isn't wanted for a linked-in copy.
set(ENABLE_PROGRAMS OFF CACHE BOOL "" FORCE)
set(ENABLE_TESTING OFF CACHE BOOL "" FORCE)
set(MBEDTLS_FATAL_WARNINGS OFF CACHE BOOL "" FORCE)
set(DISABLE_PACKAGE_CONFIG_AND_INSTALL ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(mkw_mbedtls_upstream)
target_link_libraries(mkw_mbedtls INTERFACE MbedTLS::mbedtls MbedTLS::mbedx509 MbedTLS::mbedcrypto)
endif()
set(MKW_TRANSLATED_COMPILE_JOBS 0 CACHE STRING
@@ -202,18 +161,6 @@ set(MKW_AURORA_DIR "${CMAKE_CURRENT_LIST_DIR}/../aurora-main")
# 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_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)
# ----------------------------------------------------------------------
@@ -307,16 +254,20 @@ else()
TARGET_DIRECTORY aurora_gx
PROPERTIES COMPILE_OPTIONS "-fno-finite-math-only")
endif()
if(MKW_NATIVE_PREBUILT_EXPORT_DIR)
include("${CMAKE_CURRENT_LIST_DIR}/cmake/NativePrebuiltExport.cmake")
endif()
endif()
# Everything from here down is this project's own code, so this is where the
# project definitions enter the directory scope. A directory-scoped definition
# seeds the COMPILE_DEFINITIONS of targets as they are created, which means:
# * aurora-main and its third-party tree were added above -> unaffected;
# * the runtime library, the translated shard libraries and the products in
# cmake/RuntimeLibrary.cmake and cmake/PublicProducts.cmake are all created
# below (those files are include()d, not add_subdirectory()d, so they share
# this directory scope) -> they pick the definitions up exactly as before.
# * mkw_runtime_common, the translated shard libraries and the products in
# cmake/PublicProducts.cmake are all created below (that file is include()d,
# not add_subdirectory()d, so it shares this directory scope) -> they pick
# the definitions up exactly as before.
if(MKW_PROJECT_COMPILE_DEFINITIONS)
add_compile_definitions(${MKW_PROJECT_COMPILE_DEFINITIONS})
endif()
@@ -329,14 +280,12 @@ endif()
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
if(MKW_PLATFORM_MACOS)
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory.cpp")
if(MKW_PLATFORM_MACOS_ARM64)
# HostContext's Apple Silicon backend is implemented in a small assembly
# companion. It must be part of the product runtime as well as the
# standalone context test; otherwise the final executable is missing
# mkw_co_init/mkw_co_switch at link time.
enable_language(ASM)
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
endif()
# HostContext's Apple Silicon backend is implemented in a small assembly
# companion. It must be part of the product runtime as well as the
# standalone context test; otherwise the final executable is missing
# mkw_co_init/mkw_co_switch at link time.
enable_language(ASM)
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
else()
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
endif()
@@ -345,25 +294,91 @@ set(MKW_BASE_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/base_product.
set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro_rewind_product.cpp")
# The host ISA guard is the one translation unit that must not receive the
# x86-64-v3 flags, so it gets its own object library instead of riding along in
# the runtime library. See cmake/PublicProducts.cmake and the file's own header.
# mkw_runtime_common. See cmake/PublicProducts.cmake and the file's own header.
set(MKW_CPU_BASELINE_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/host_cpu_baseline.cpp")
list(REMOVE_ITEM SOURCES ${MKW_BASE_PRODUCT_SOURCE} ${MKW_RETRO_REWIND_PRODUCT_SOURCE}
${MKW_CPU_BASELINE_SOURCE} ${MKW_PLATFORM_SOURCE})
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PlatformTests.cmake")
# This deliberately small library contains host services that are safe to
# validate before guest memory and fiber work makes a full runtime build viable.
add_library(mkw_platform STATIC "${MKW_PLATFORM_SOURCE}")
target_include_directories(mkw_platform PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_platform PUBLIC cxx_std_17)
set_target_properties(mkw_platform PROPERTIES UNITY_BUILD OFF)
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
include("${CMAKE_CURRENT_LIST_DIR}/cmake/RuntimeLibrary.cmake")
# Keep these independent from Aurora's BUILD_TESTING option: they validate the
# project's host-platform contracts, not Aurora's third-party test suite.
enable_testing()
add_executable(mkw_platform_paths_tests "${CMAKE_CURRENT_LIST_DIR}/tests/platform_paths_tests.cpp")
target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
# The prebuilt export also carries the game-independent runtime as a static
# archive (x86_64 only; AArch64 compiles it locally with -mcpu=native). It is
# defined here, after the project definitions, so its flags match the products'.
if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT MKW_NATIVE_PREBUILT_DIR)
if(MKW_HOST_IS_X86_64)
mkw_add_runtime_library(mkw_runtime STATIC ${SOURCES})
set_target_properties(mkw_runtime PROPERTIES EXCLUDE_FROM_ALL ON)
endif()
include("${CMAKE_CURRENT_LIST_DIR}/cmake/NativePrebuiltExport.cmake")
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_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_save_tests COMMAND mkw_nand_save_tests)
add_executable(mkw_nand_settings_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_settings_tests.cpp")
find_package(Threads REQUIRED)
target_link_libraries(mkw_nand_settings_tests PRIVATE Threads::Threads)
target_include_directories(mkw_nand_settings_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
add_executable(mkw_sc_serial_tests "${CMAKE_CURRENT_LIST_DIR}/tests/sc_serial_tests.cpp")
target_include_directories(mkw_sc_serial_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_sc_serial_tests PRIVATE cxx_std_17)
add_test(NAME mkw_sc_serial_tests COMMAND mkw_sc_serial_tests)
# The input expression engine is self-contained, so it can be exercised without
# linking the runtime or SDL.
add_executable(mkw_input_expr_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/test_expr.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/input_expr.cpp")
target_include_directories(mkw_input_expr_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
# HostContext deliberately keeps the platform-specific context primitive out
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
# refactors cannot silently remove its headers, implementation, or link edge.
if(MKW_PLATFORM_LINUX)
add_executable(mkw_linux_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp")
target_include_directories(mkw_linux_host_context_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include"
"${CMAKE_CURRENT_LIST_DIR}/third_party/libco")
target_compile_features(mkw_linux_host_context_tests PRIVATE cxx_std_17)
target_link_libraries(mkw_linux_host_context_tests PRIVATE mkw::libco)
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
endif()
if(MKW_PLATFORM_MACOS)
# Exercise the Apple Silicon context ABI and the public host-memory
# contracts separately from translated products.
enable_language(ASM)
add_executable(mkw_macos_context_abi_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
add_executable(mkw_macos_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
add_executable(mkw_macos_guest_flat_memory_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_guest_flat_memory_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
target_include_directories(mkw_macos_guest_flat_memory_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_macos_guest_flat_memory_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests)
endif()
# The translator emits the complete, content-addressed source graph. Consuming
@@ -373,8 +388,8 @@ if(MKW_BUILD_PRODUCTS)
set(MKW_TRANSLATED_SHARD_MANIFEST
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
CACHE FILEPATH "Translator-owned aggregate shard manifest")
# The prebuilt export only needs the aurora/third-party closure and the runtime
# configured above, so a packaging machine without a translation stops here.
# The prebuilt export only needs the aurora/third-party closure configured above, so a
# packaging machine without a translation stops here instead of failing.
if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
message(STATUS "No translator shard manifest; configuring the native prebuilt export only")
return()
@@ -391,13 +406,20 @@ if(MKW_BUILD_PRODUCTS)
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; "
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions")
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake")
else()
if(MKW_PLATFORM_MACOS)
# Compile-only audit of native runtime sources. It deliberately avoids
# translated products until their host dependencies are portable.
# runtime/src is game-independent; per-game glue lives in runtime/game.
add_library(mkw_macos_native_compile OBJECT ${SOURCES})
# These sources depend on generated/RuntimeConfig.h, which is emitted for
# a particular game by the translator and is intentionally unavailable
# in this platform-only configuration.
set(MKW_MACOS_NATIVE_AUDIT_SOURCES ${SOURCES})
list(REMOVE_ITEM MKW_MACOS_NATIVE_AUDIT_SOURCES
"${CMAKE_CURRENT_LIST_DIR}/src/abi_bridge.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/hle/os/os_alarm.cpp")
add_library(mkw_macos_native_compile OBJECT ${MKW_MACOS_NATIVE_AUDIT_SOURCES})
target_include_directories(mkw_macos_native_compile PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include" "${CMAKE_CURRENT_LIST_DIR}/src"
"${CMAKE_CURRENT_LIST_DIR}/.." "${CMAKE_CURRENT_LIST_DIR}/../aurora-main/include")
@@ -405,15 +427,7 @@ else()
target_compile_definitions(mkw_macos_native_compile PRIVATE SDL_MAIN_HANDLED TARGET_PC)
target_link_libraries(mkw_macos_native_compile PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
if(MKW_PLATFORM_MACOS_X86_64)
target_link_libraries(mkw_macos_native_compile PRIVATE mkw::libco)
target_compile_options(mkw_macos_native_compile PRIVATE
-march=x86-64-${MKW_X86_CPU_PROFILE})
if(MKW_X86_CPU_PROFILE STREQUAL "v2")
target_compile_definitions(mkw_macos_native_compile PRIVATE MKW_X86_CPU_PROFILE_V2=1)
endif()
endif()
mkw::pugixml mkw::toml11 mkw::cryptopp)
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
endif()
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
-29
View File
@@ -1,29 +0,0 @@
# Shared by the product build and the dependency-free platform test configuration.
get_filename_component(MKW_HOST_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
# POSIX guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric
# stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
# asymmetric resume/yield coroutine libraries, which would need every call site restructured).
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers;
# Apple Silicon uses the project's x18-safe AArch64 assembly backend, while Intel macOS uses
# libco's existing System V AMD64 backend.
if(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
add_library(mkw_libco STATIC "${MKW_HOST_RUNTIME_DIR}/third_party/libco/libco.c")
add_library(mkw::libco ALIAS mkw_libco)
target_include_directories(mkw_libco PUBLIC "${MKW_HOST_RUNTIME_DIR}/third_party/libco")
set_target_properties(mkw_libco PROPERTIES UNITY_BUILD OFF)
endif()
# This deliberately small library contains host services that are safe to
# validate before guest memory and fiber work makes a full runtime build viable.
add_library(mkw_platform STATIC "${MKW_HOST_RUNTIME_DIR}/src/platform/host_platform.cpp")
target_include_directories(mkw_platform PUBLIC "${MKW_HOST_RUNTIME_DIR}/include")
target_compile_features(mkw_platform PUBLIC cxx_std_17)
set_target_properties(mkw_platform PROPERTIES UNITY_BUILD OFF)
if(MKW_PLATFORM_WINDOWS)
target_compile_definitions(mkw_platform PRIVATE NOMINMAX)
target_link_libraries(mkw_platform PUBLIC shell32 ole32 uuid)
endif()
-4
View File
@@ -1,4 +0,0 @@
# 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")
+4 -30
View File
@@ -3,10 +3,10 @@
#
# Produced by launcher/Prepare-NativePrebuilt.ps1 with the same toolchain and the
# same canonical flag set the consuming build uses, which is what makes a single
# package valid for every user. The translated shards and the products still
# compile locally and still link targets named aurora::gx / aurora::pad /
# aurora::si / aurora::vi / aurora::mtx. The package may also carry the native
# runtime archive; cmake/RuntimeLibrary.cmake decides whether it can be used.
# package valid for every user. Nothing about this project's own code changes:
# the runtime, the translated shards and the products still compile locally and
# still link targets named aurora::gx / aurora::pad / aurora::si / aurora::vi /
# aurora::mtx, so cmake/PublicProducts.cmake needs no prebuilt-specific branch.
#
# The aurora and dependency source trees are still present in the workspace - the
# runtime includes aurora headers directly - so the package records include
@@ -42,32 +42,6 @@ endfunction()
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_dawn_config ${MKW_NP_DAWN_CONFIG_DIR})
set(MKW_NP_RUNTIME_ARCHIVE_PATH "")
if(MKW_NP_RUNTIME_ARCHIVE)
mkw_np_resolve(MKW_NP_RUNTIME_ARCHIVE_PATH "${MKW_NP_RUNTIME_ARCHIVE}")
endif()
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)
if(NOT IS_DIRECTORY "${_dir}")
-35
View File
@@ -77,9 +77,6 @@ target_compile_features(mkw_np_probe PRIVATE cxx_std_20)
target_link_libraries(mkw_np_probe PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
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)
mkw_collect_buildsystem_targets("${_mkw_np_aurora_dir}" _mkw_np_all_targets)
@@ -87,30 +84,6 @@ 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
# checks below still apply to it.
list(APPEND _mkw_np_all_targets "mkw_cryptopp")
# The game-independent runtime archive (RuntimeLibrary.cmake). It is outside the
# probe closure, so it ships only when the harvester builds it explicitly.
if(TARGET mkw_runtime)
list(APPEND _mkw_np_all_targets "mkw_runtime")
endif()
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)
message(FATAL_ERROR "No buildsystem targets were found under ${_mkw_np_aurora_dir}")
endif()
@@ -163,7 +136,6 @@ string(REPLACE ";" "," _mkw_np_lib_targets_csv "${_mkw_np_lib_targets}")
file(WRITE "${MKW_NATIVE_PREBUILT_EXPORT_DIR}/meta.txt"
"aurora_dir=${_mkw_np_aurora_dir}\n"
"runtime_dir=${CMAKE_CURRENT_SOURCE_DIR}\n"
"mbedtls_source_dir=${mkw_mbedtls_upstream_SOURCE_DIR}\n"
"binary_dir=${CMAKE_BINARY_DIR}\n"
"dawn_config_dir=${_mkw_np_dawn_config_dir}\n"
"dawn_prebuilt_source_dir=${dawn_prebuilt_SOURCE_DIR}\n"
@@ -173,12 +145,5 @@ file(WRITE "${MKW_NATIVE_PREBUILT_EXPORT_DIR}/meta.txt"
"cxx_compiler=${CMAKE_CXX_COMPILER}\n"
"cxx_compiler_version=${CMAKE_CXX_COMPILER_VERSION}\n"
"library_targets=${_mkw_np_lib_targets_csv}\n")
if(TARGET mkw_runtime)
mkw_runtime_fingerprint(_mkw_np_runtime_fingerprint "${CMAKE_CURRENT_SOURCE_DIR}"
"${MKW_CPPWINRT_INCLUDE_DIR}" "${MKW_X86_CPU_PROFILE}")
file(APPEND "${MKW_NATIVE_PREBUILT_EXPORT_DIR}/meta.txt"
"runtime_fingerprint=${_mkw_np_runtime_fingerprint}\n"
"runtime_cpu_profile=${MKW_X86_CPU_PROFILE}\n")
endif()
message(STATUS "Native prebuilt export: ${MKW_NATIVE_PREBUILT_EXPORT_DIR}")
-188
View File
@@ -1,188 +0,0 @@
get_filename_component(MKW_TEST_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
# Keep these independent from Aurora's BUILD_TESTING option: they validate the
# project's host-platform contracts, not Aurora's third-party test suite.
enable_testing()
if(MKW_BUILD_PSQ_TESTS)
add_executable(mkw_psq_helpers_tests
"${MKW_TEST_RUNTIME_DIR}/tests/psq_helpers_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/ppc_quantized.cpp")
target_include_directories(mkw_psq_helpers_tests PRIVATE
"${MKW_TEST_RUNTIME_DIR}/tests/psq_memory"
"${MKW_TEST_RUNTIME_DIR}/include/isa"
"${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_psq_helpers_tests PRIVATE cxx_std_17)
target_compile_options(mkw_psq_helpers_tests PRIVATE
-O2 -fno-fast-math -ffp-contract=off -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-${MKW_X86_CPU_PROFILE})
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 "${MKW_TEST_RUNTIME_DIR}/tests/psq_reserved_tests.cmake")
endif()
add_executable(mkw_platform_paths_tests "${MKW_TEST_RUNTIME_DIR}/tests/platform_paths_tests.cpp")
target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
add_executable(mkw_runtime_config_tests "${MKW_TEST_RUNTIME_DIR}/tests/runtime_config_tests.cpp")
target_include_directories(mkw_runtime_config_tests PRIVATE
"${MKW_TEST_RUNTIME_DIR}/include"
"${MKW_TEST_RUNTIME_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 "${MKW_TEST_RUNTIME_DIR}/tests/nand_save_tests.cpp")
target_include_directories(mkw_nand_save_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_save_tests COMMAND mkw_nand_save_tests)
add_executable(mkw_nand_settings_tests "${MKW_TEST_RUNTIME_DIR}/tests/nand_settings_tests.cpp")
find_package(Threads REQUIRED)
target_link_libraries(mkw_nand_settings_tests PRIVATE Threads::Threads)
target_include_directories(mkw_nand_settings_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
add_executable(mkw_sc_serial_tests "${MKW_TEST_RUNTIME_DIR}/tests/sc_serial_tests.cpp")
target_include_directories(mkw_sc_serial_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_sc_serial_tests PRIVATE cxx_std_17)
add_test(NAME mkw_sc_serial_tests COMMAND mkw_sc_serial_tests)
# The input expression engine is self-contained, so it can be exercised without
# linking the runtime or SDL.
add_executable(mkw_input_expr_tests
"${MKW_TEST_RUNTIME_DIR}/tests/test_expr.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/input_expr.cpp")
target_include_directories(mkw_input_expr_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
# HostContext deliberately keeps the platform-specific context primitive out
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
# refactors cannot silently remove its headers, implementation, or link edge.
if(MKW_PLATFORM_LINUX)
add_executable(mkw_linux_host_context_tests
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp")
target_include_directories(mkw_linux_host_context_tests PRIVATE
"${MKW_TEST_RUNTIME_DIR}/include"
"${MKW_TEST_RUNTIME_DIR}/third_party/libco")
target_compile_features(mkw_linux_host_context_tests PRIVATE cxx_std_17)
target_link_libraries(mkw_linux_host_context_tests PRIVATE mkw::libco)
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
endif()
if(MKW_PLATFORM_WINDOWS)
add_executable(mkw_windows_host_context_tests
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp")
target_include_directories(mkw_windows_host_context_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_windows_host_context_tests PRIVATE cxx_std_17)
add_test(NAME mkw_windows_host_context_tests COMMAND mkw_windows_host_context_tests)
endif()
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
# Probe at the base ISA, including OS support for AVX register state. Cross
# builds execute target code only through a configured emulator.
if(NOT CMAKE_CROSSCOMPILING OR CMAKE_CROSSCOMPILING_EMULATOR)
include(CheckCXXSourceRuns)
include(CMakePushCheckState)
cmake_push_check_state(RESET)
set(CMAKE_REQUIRED_FLAGS "-march=x86-64")
check_cxx_source_runs([=[
#include <cpuid.h>
int main() {
unsigned a, b, c, d;
if (__get_cpuid_max(0, nullptr) < 7 ||
__get_cpuid_max(0x80000000u, nullptr) < 0x80000001u)
return 1;
__cpuid_count(1, 0, a, b, c, d);
// v2 plus FMA, MOVBE, XSAVE, OSXSAVE, AVX and F16C.
const unsigned leaf1 = (1u << 0) | (1u << 9) | (1u << 12) |
(1u << 13) | (1u << 19) | (1u << 20) | (1u << 22) |
(1u << 23) | (1u << 26) | (1u << 27) | (1u << 28) | (1u << 29);
if ((c & leaf1) != leaf1) return 1;
__cpuid_count(7, 0, a, b, c, d);
const unsigned leaf7 = (1u << 3) | (1u << 5) | (1u << 8);
if ((b & leaf7) != leaf7) return 1;
__cpuid_count(0x80000001u, 0, a, b, c, d);
if ((c & 0x21u) != 0x21u) return 1; // LAHF-SAHF and LZCNT.
__asm__ __volatile__("xgetbv" : "=a"(a), "=d"(d) : "c"(0));
return (a & 0x6u) == 0x6u ? 0 : 1;
}
]=] MKW_HOST_SUPPORTS_X86_V3)
cmake_pop_check_state()
endif()
# Compile the same oracle cases for both paths. The v2 binary proves the
# scalar fallback stays free of FMA; the v3 binary checks the existing
# vector intrinsic path against the same strict result.
foreach(profile IN ITEMS v2 v3)
add_executable(mkw_ppc_pair_fma_${profile}_tests
"${MKW_TEST_RUNTIME_DIR}/tests/ppc_pair_fma_tests.cpp")
target_include_directories(mkw_ppc_pair_fma_${profile}_tests PRIVATE
"${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_ppc_pair_fma_${profile}_tests PRIVATE cxx_std_17)
target_compile_options(mkw_ppc_pair_fma_${profile}_tests PRIVATE
-march=x86-64-${profile} -fno-fast-math -ffp-contract=off)
if((NOT CMAKE_CROSSCOMPILING OR CMAKE_CROSSCOMPILING_EMULATOR) AND
(profile STREQUAL "v2" OR MKW_HOST_SUPPORTS_X86_V3))
add_test(NAME mkw_ppc_pair_fma_${profile}_tests COMMAND mkw_ppc_pair_fma_${profile}_tests)
endif()
endforeach()
add_library(mkw_cpu_baseline_v2_compile OBJECT
"${MKW_TEST_RUNTIME_DIR}/src/host_cpu_baseline.cpp")
target_compile_features(mkw_cpu_baseline_v2_compile PRIVATE cxx_std_17)
target_compile_definitions(mkw_cpu_baseline_v2_compile PRIVATE MKW_X86_CPU_PROFILE_V2=1)
target_compile_options(mkw_cpu_baseline_v2_compile PRIVATE -march=x86-64-v2 -w)
endif()
if(MKW_PLATFORM_MACOS)
# Exercise the public host-memory 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)
add_executable(mkw_macos_context_abi_tests
"${MKW_TEST_RUNTIME_DIR}/tests/macos_context_abi_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/platform/macos/co_switch.S")
target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
add_executable(mkw_macos_host_context_tests
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/platform/macos/co_switch.S")
target_include_directories(mkw_macos_host_context_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
else()
# Intel macOS follows the same System V AMD64 libco path as Linux.
add_executable(mkw_macos_host_context_tests
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp")
target_include_directories(mkw_macos_host_context_tests PRIVATE
"${MKW_TEST_RUNTIME_DIR}/include"
"${MKW_TEST_RUNTIME_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)
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
add_executable(mkw_macos_guest_flat_memory_tests
"${MKW_TEST_RUNTIME_DIR}/tests/macos_guest_flat_memory_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/guest_flat_memory_macos.cpp")
target_include_directories(mkw_macos_guest_flat_memory_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_macos_guest_flat_memory_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests)
add_executable(mkw_macos_external_audio_tests
"${MKW_TEST_RUNTIME_DIR}/tests/macos_external_audio_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/external_audio_macos.cpp")
target_include_directories(mkw_macos_external_audio_tests PRIVATE "${MKW_TEST_RUNTIME_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()
+115 -89
View File
@@ -1,45 +1,57 @@
# Public WiiCompiled product graph.
# Public WiiCompiled product graph.
#
# The translator owns the translated build graph. Mario Kart's profile-neutral
# functions are compiled once into mkw_base_shared; only callers whose direct
# ABI differs between profiles receive small base/RR variants.
# Per-game data the runtime reads but does not own. Kept out of the runtime
# library so that library stays game-independent (and can be precompiled).
set(MKW_GAME_DATA_SOURCES "")
set(DATA_INIT_FILE "${MKW_RUNTIME_SOURCE_DIR}/../generated/data_sections_init.cpp")
set(DATA_INIT_BLOB_ASM "${MKW_RUNTIME_SOURCE_DIR}/../generated/data_sections_init_blobs.S")
if(EXISTS "${DATA_INIT_FILE}")
list(APPEND MKW_GAME_DATA_SOURCES "${DATA_INIT_FILE}")
list(APPEND SOURCES "${DATA_INIT_FILE}")
endif()
# Crash-report symbolization table emitted by generate-data-init. The stub
# (deliberately outside the globbed src/ tree so it is never picked up twice)
# keeps link succeeding when the generated table has not been produced yet.
set(GUEST_SYMBOL_TABLE_FILE "${MKW_RUNTIME_SOURCE_DIR}/../generated/guest_symbol_table.cpp")
if(EXISTS "${GUEST_SYMBOL_TABLE_FILE}")
list(APPEND MKW_GAME_DATA_SOURCES "${GUEST_SYMBOL_TABLE_FILE}")
list(APPEND SOURCES "${GUEST_SYMBOL_TABLE_FILE}")
else()
list(APPEND MKW_GAME_DATA_SOURCES "${MKW_RUNTIME_SOURCE_DIR}/cmake/guest_symbol_table_stub.cpp")
list(APPEND SOURCES "${MKW_RUNTIME_SOURCE_DIR}/cmake/guest_symbol_table_stub.cpp")
endif()
if(EXISTS "${DATA_INIT_BLOB_ASM}")
enable_language(ASM)
set_source_files_properties("${DATA_INIT_BLOB_ASM}" PROPERTIES LANGUAGE ASM SKIP_UNITY_BUILD_INCLUSION ON)
list(APPEND MKW_GAME_DATA_SOURCES "${DATA_INIT_BLOB_ASM}")
list(APPEND SOURCES "${DATA_INIT_BLOB_ASM}")
endif()
# Runtime glue that depends on generated/ headers (e.g. RuntimeConfig.h).
file(GLOB MKW_GAME_GLUE_SOURCES CONFIGURE_DEPENDS "${MKW_RUNTIME_SOURCE_DIR}/game/*.cpp")
list(APPEND MKW_GAME_DATA_SOURCES ${MKW_GAME_GLUE_SOURCES})
list(REMOVE_DUPLICATES SOURCES)
if(MKW_PLATFORM_MACOS)
find_library(MKW_IOKIT_FRAMEWORK IOKit REQUIRED)
find_library(MKW_COREFOUNDATION_FRAMEWORK CoreFoundation REQUIRED)
endif()
function(mkw_apply_common_compile_options target)
target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe)
endfunction()
function(mkw_apply_translated_compile_options target)
target_compile_options(${target} PRIVATE
-O2 ${MKW_TRANSLATED_PPC_FP_OPTIONS} -fno-slp-vectorize -w -pipe)
endfunction()
function(mkw_configure_object_target target)
target_include_directories(${target} PRIVATE
"${MKW_RUNTIME_SOURCE_DIR}/include"
"${MKW_RUNTIME_SOURCE_DIR}/src"
# Workspace root, so translator output is spelled "generated/<x>.h"
# instead of a ../ chain whose depth depends on the includer.
"${MKW_RUNTIME_SOURCE_DIR}/.."
"${MKW_RUNTIME_SOURCE_DIR}/../aurora-main/include")
target_compile_definitions(${target} PRIVATE
TARGET_PC)
set_target_properties(${target} PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON)
endfunction()
# Translated shard TUs are the memory-hungry compiles; everything else in the build is
# comparatively small. A dedicated Ninja job pool caps how many of them run at once so the
# global parallelism can use every core for the cheap TUs without the memory ceiling being a
@@ -61,64 +73,83 @@ function(mkw_configure_translated_target target)
mkw_bound_translated_compiles(${target})
endfunction()
mkw_select_runtime_provider()
if(MKW_RUNTIME_USE_PREBUILT)
mkw_import_prebuilt_runtime(mkw_runtime_prebuilt "${MKW_NP_RUNTIME_ARCHIVE_PATH}")
# A replacement archive can carry an older mtime than the last link, so relink on fingerprint change.
set(MKW_RUNTIME_PREBUILT_STAMP "${CMAKE_BINARY_DIR}/mkw_runtime_prebuilt.stamp")
file(CONFIGURE OUTPUT "${MKW_RUNTIME_PREBUILT_STAMP}" CONTENT "${MKW_NP_RUNTIME_FINGERPRINT}\n")
else()
mkw_add_runtime_library(mkw_runtime_common OBJECT ${SOURCES})
endif()
add_library(mkw_game_data OBJECT ${MKW_GAME_DATA_SOURCES})
mkw_configure_object_target(mkw_game_data)
target_compile_features(mkw_game_data PRIVATE cxx_std_20)
target_compile_definitions(mkw_game_data PRIVATE
add_library(mkw_runtime_common OBJECT ${SOURCES})
mkw_configure_object_target(mkw_runtime_common)
target_compile_features(mkw_runtime_common PRIVATE cxx_std_20)
target_compile_definitions(mkw_runtime_common PRIVATE
SDL_MAIN_HANDLED
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
if(MKW_SETUP_VERSION)
set_property(SOURCE "${MKW_RUNTIME_SOURCE_DIR}/game/setup_version.cpp" APPEND PROPERTY
COMPILE_DEFINITIONS "MKW_SETUP_VERSION=\"${MKW_SETUP_VERSION}\"")
target_link_libraries(mkw_runtime_common PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
if(MKW_PLATFORM_WINDOWS)
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
elseif(MKW_PLATFORM_LINUX)
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
# media monitoring). Empty string on glibc >= 2.34 where dl* is in libc.
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS})
endif()
if(MKW_CPPWINRT_INCLUDE_DIR)
if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
message(FATAL_ERROR
"MKW_CPPWINRT_INCLUDE_DIR does not contain winrt/base.h: ${MKW_CPPWINRT_INCLUDE_DIR}")
endif()
target_include_directories(mkw_runtime_common PRIVATE "${MKW_CPPWINRT_INCLUDE_DIR}")
endif()
mkw_runtime_link_dependencies(mkw_game_data PRIVATE)
set_target_properties(mkw_game_data PROPERTIES UNITY_BUILD OFF)
mkw_apply_common_compile_options(mkw_game_data)
# Host ISA guard deliberately keeps the plain baseline ISA and checks the
# selected x86 profile before optional instructions can execute. AArch64 has
# no equivalent optional ISA floor to probe: NEON/FMA are architectural
# requirements.
if(MKW_HOST_IS_X86_64)
# Keep runtime unity units small and semantically related. The old generated-TU
# batch size put all 57 native runtime sources into one memory-heavy compiler job.
foreach(source IN LISTS SOURCES)
get_filename_component(source_name "${source}" NAME_WE)
string(REPLACE "\\" "/" source_normalized "${source}")
if(source_normalized MATCHES "/hle/gx/")
set(runtime_group "gx_bridge")
elseif(source_name MATCHES "network|socket|dns|dwc|ios")
set(runtime_group "network_ios")
elseif(source_name MATCHES "os_|system|memory|fiber|scheduler")
set(runtime_group "guest_system")
elseif(source_name MATCHES "debug|trace|prof")
set(runtime_group "diagnostics")
else()
string(SHA256 source_hash "${source_name}")
string(SUBSTRING "${source_hash}" 0 4 source_hash_prefix)
math(EXPR runtime_bucket "0x${source_hash_prefix} % 8")
set(runtime_group "runtime_${runtime_bucket}")
endif()
set_source_files_properties("${source}" PROPERTIES UNITY_GROUP "${runtime_group}")
endforeach()
# These translation units implement guest-visible floating-point bit
# 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)
target_precompile_headers(mkw_runtime_common PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
mkw_apply_common_compile_options(mkw_runtime_common)
# Host ISA guard. Windows and Linux x86_64 product targets use x86-64-v3, so
# this object deliberately keeps the plain baseline ISA and checks the CPU
# before any AVX2/FMA code can execute. AArch64 has no equivalent optional ISA
# floor to probe: NEON/FMA are architectural requirements.
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}")
target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17)
set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF)
target_compile_options(mkw_cpu_baseline PRIVATE -w)
if(MKW_X86_CPU_PROFILE STREQUAL "v2")
target_compile_definitions(mkw_cpu_baseline PRIVATE MKW_X86_CPU_PROFILE_V2=1)
endif()
endif()
if(NOT MKW_BASE_COMMON_SHARDS)
message(FATAL_ERROR "Translator build graph contains no shared base shards")
endif()
# Translated code compiles against the guest ABI surface only, so runtime
# implementation edits do not invalidate its PCH.
set(MKW_TRANSLATED_PCH "${MKW_RUNTIME_SOURCE_DIR}/include/guest_abi/guest_abi_pch.h")
if(NOT EXISTS "${MKW_TRANSLATED_PCH}")
set(MKW_TRANSLATED_PCH "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
endif()
add_library(mkw_base_shared STATIC ${MKW_BASE_COMMON_SHARDS})
mkw_configure_translated_target(mkw_base_shared)
target_precompile_headers(mkw_base_shared PRIVATE "${MKW_TRANSLATED_PCH}")
# Registration and indirect-dispatch tables are translator output too; they
# compile with the translated configuration rather than inside the products.
add_library(mkw_base_registration OBJECT ${MKW_BASE_REGISTRATION_SOURCES})
mkw_configure_translated_target(mkw_base_registration)
target_precompile_headers(mkw_base_registration REUSE_FROM mkw_base_shared)
target_precompile_headers(mkw_base_shared PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
if(MKW_BASE_PORTABLE_SENSITIVE_SHARDS)
add_library(mkw_base_sensitive OBJECT ${MKW_BASE_PORTABLE_SENSITIVE_SHARDS})
@@ -141,31 +172,16 @@ if(MKW_HAVE_RETRO_REWIND)
set_source_files_properties("${source}" PROPERTIES LANGUAGE ASM SKIP_PRECOMPILE_HEADERS ON)
endif()
endforeach()
# mod_data_patches (MKW_RETRO_EXTRA_SOURCES) calls runtime APIs directly; it
# keeps the full runtime include path this target already has.
add_library(mkw_retro_rewind_functions OBJECT ${MKW_RETRO_TRANSLATED_SOURCES})
mkw_configure_translated_target(mkw_retro_rewind_functions)
target_precompile_headers(mkw_retro_rewind_functions REUSE_FROM mkw_base_shared)
add_library(mkw_retro_registration OBJECT ${MKW_RETRO_REGISTRATION_SOURCES})
mkw_configure_translated_target(mkw_retro_registration)
target_precompile_headers(mkw_retro_registration REUSE_FROM mkw_base_shared)
endif()
function(mkw_configure_product target)
if(TARGET mkw_runtime_common)
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_runtime_common>)
else()
# Natives self-register from static constructors, so every archive
# member must be kept. Listed before mkw_base_shared so single-pass
# linkers resolve the runtime's references to translated functions.
target_link_libraries(${target} PRIVATE "$<LINK_LIBRARY:WHOLE_ARCHIVE,mkw_runtime_prebuilt>")
set_property(TARGET ${target} APPEND PROPERTY LINK_DEPENDS "${MKW_RUNTIME_PREBUILT_STAMP}")
endif()
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_game_data>)
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_runtime_common>)
# Startup CPU check. Must stay a separate object library so it keeps the
# plain baseline ISA while everything around it is built for x86-64-v3.
if(MKW_HOST_IS_X86_64)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_cpu_baseline>)
endif()
target_include_directories(${target} PRIVATE
@@ -179,6 +195,9 @@ function(mkw_configure_product target)
SDL_MAIN_HANDLED _DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION TARGET_PC)
target_compile_features(${target} PRIVATE cxx_std_20)
mkw_apply_common_compile_options(${target})
# The dispatch-table and registration shards compile inside the product target itself and
# include the same fat translated headers; bound them by the same pool.
mkw_bound_translated_compiles(${target})
target_link_libraries(${target} PRIVATE
mkw_platform mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
@@ -186,9 +205,7 @@ function(mkw_configure_product target)
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
if(MKW_PLATFORM_MACOS)
target_link_libraries(${target} PRIVATE
"${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}" "${MKW_COREAUDIO_FRAMEWORK}")
target_link_options(${target} PRIVATE
"LINKER:-U,_OBJC_CLASS_$_MTLLogStateDescriptor")
"${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}")
endif()
if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
@@ -201,7 +218,7 @@ function(mkw_configure_product target)
(MKW_SQLITE_TARGET_TYPE STREQUAL "SHARED_LIBRARY" OR
MKW_SQLITE_TARGET_TYPE STREQUAL "MODULE_LIBRARY"))
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}> VERBATIM)
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
endif()
if(MKW_PLATFORM_WINDOWS)
@@ -209,7 +226,7 @@ function(mkw_configure_product target)
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
elseif(MKW_PLATFORM_LINUX)
# mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o
# files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own
# target_link_libraries (object libraries don't carry usage requirements to a consumer
@@ -235,7 +252,7 @@ function(mkw_configure_product target)
endif()
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${runtime_dll_path}" $<TARGET_FILE_DIR:${target}> VERBATIM)
"${runtime_dll_path}" $<TARGET_FILE_DIR:${target}>)
endforeach()
endif()
@@ -244,7 +261,7 @@ function(mkw_configure_product target)
message(FATAL_ERROR "Missing Wii first-run bootstrap payload: ${MKW_WII_BOOTSTRAP_SOURCE_DIR}")
endif()
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory
"${MKW_WII_BOOTSTRAP_SOURCE_DIR}" "$<TARGET_FILE_DIR:${target}>/wii_bootstrap" VERBATIM)
"${MKW_WII_BOOTSTRAP_SOURCE_DIR}" "$<TARGET_FILE_DIR:${target}>/wii_bootstrap")
set(MKW_DSP_COEFFICIENT_ROM "${MKW_RUNTIME_SOURCE_DIR}/assets/dsp/dsp_coef.bin")
if(NOT EXISTS "${MKW_DSP_COEFFICIENT_ROM}")
@@ -256,7 +273,7 @@ function(mkw_configure_product target)
message(FATAL_ERROR "Wii DSP coefficient ROM hash mismatch: ${MKW_DSP_COEFFICIENT_ROM_SHA256}")
endif()
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${MKW_DSP_COEFFICIENT_ROM}" "$<TARGET_FILE_DIR:${target}>/dsp_coef.bin" VERBATIM)
"${MKW_DSP_COEFFICIENT_ROM}" "$<TARGET_FILE_DIR:${target}>/dsp_coef.bin")
# Aurora imports this portable recipe database into each user's writable
# pipeline cache. Keep the upstream filename so its default resourcesPath
@@ -268,7 +285,7 @@ function(mkw_configure_product target)
endif()
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${MKW_INITIAL_PIPELINE_CACHE}"
"$<TARGET_FILE_DIR:${target}>/initial_pipeline_cache.db" VERBATIM)
"$<TARGET_FILE_DIR:${target}>/initial_pipeline_cache.db")
# Non-Windows TLS (runtime/src/hle/net/network_ssl.cpp's mbed TLS path) needs a trusted root
# CA bundle to verify server certificates against - Windows gets this for free from the OS via
@@ -282,13 +299,12 @@ function(mkw_configure_product target)
message(FATAL_ERROR "Missing TLS root CA bundle: ${MKW_CA_CERTIFICATE_BUNDLE}")
endif()
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${MKW_CA_CERTIFICATE_BUNDLE}" "$<TARGET_FILE_DIR:${target}>/cacert.pem" VERBATIM)
"${MKW_CA_CERTIFICATE_BUNDLE}" "$<TARGET_FILE_DIR:${target}>/cacert.pem")
endif()
endfunction()
add_executable(WiiCompiled "${MKW_BASE_PRODUCT_SOURCE}")
add_executable(WiiCompiled "${MKW_BASE_PRODUCT_SOURCE}" ${MKW_BASE_REGISTRATION_SOURCES})
mkw_configure_product(WiiCompiled)
target_sources(WiiCompiled PRIVATE $<TARGET_OBJECTS:mkw_base_registration>)
target_precompile_headers(WiiCompiled PRIVATE
"${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
if(TARGET mkw_base_sensitive)
@@ -296,10 +312,9 @@ if(TARGET mkw_base_sensitive)
endif()
if(MKW_HAVE_RETRO_REWIND)
add_executable(RetroRewind "${MKW_RETRO_REWIND_PRODUCT_SOURCE}")
add_executable(RetroRewind "${MKW_RETRO_REWIND_PRODUCT_SOURCE}" ${MKW_RETRO_REGISTRATION_SOURCES})
mkw_configure_product(RetroRewind)
target_precompile_headers(RetroRewind REUSE_FROM WiiCompiled)
target_sources(RetroRewind PRIVATE $<TARGET_OBJECTS:mkw_retro_registration>)
if(TARGET mkw_retro_sensitive)
target_sources(RetroRewind PRIVATE $<TARGET_OBJECTS:mkw_retro_sensitive>)
endif()
@@ -313,12 +328,23 @@ else()
message(STATUS "RetroRewind target disabled (run translate-mod and emit-build-shards)")
endif()
# mkw_runtime_common gets the same flags from mkw_add_runtime_library.
# Windows and Linux x86_64 share the x86-64-v3 floor that the CPU baseline
# object above checks. AArch64 builds are compiled locally for the host that
# will run them, so both Linux and Apple Silicon use the compiler's native CPU
# tuning rather than leaving target-specific performance on the table.
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
else()
set(MKW_BASELINE_ARCH_FLAG "")
endif()
set(MKW_ALL_BUILD_TARGETS
mkw_game_data mkw_base_shared mkw_base_registration mkw_base_sensitive mkw_retro_sensitive
mkw_retro_rewind_functions mkw_retro_registration WiiCompiled RetroRewind)
mkw_runtime_common mkw_base_shared mkw_base_sensitive mkw_retro_sensitive
mkw_retro_rewind_functions WiiCompiled RetroRewind)
foreach(target IN LISTS MKW_ALL_BUILD_TARGETS)
if(TARGET ${target})
mkw_apply_baseline_arch(${target})
if(TARGET ${target} AND MKW_BASELINE_ARCH_FLAG)
target_compile_options(${target} PRIVATE ${MKW_BASELINE_ARCH_FLAG})
endif()
endforeach()
-98
View File
@@ -1,98 +0,0 @@
# Content identity of the game-independent runtime and the rule deciding whether
# a precompiled runtime archive may replace compiling it. Script mode prints
# exactly one stdout line, runtime_fingerprint=<sha256 hex>:
# cmake -DMKW_RUNTIME_DIR=<abs runtime dir> [-DMKW_CPPWINRT_INCLUDE_DIR=<dir>]
# [-DMKW_X86_CPU_PROFILE=v2|v3] [-DCMAKE_OSX_DEPLOYMENT_TARGET=<ver>]
# -P runtime/cmake/RuntimeFingerprint.cmake
#
# Inputs: src/, include/, cmake/ and CMakeLists.txt (sorted relative paths and
# content hashes), winrt/base.h, the CPU profile and the macOS deployment target.
# third_party/ has its own package fingerprint; game/ is per-user. Dot-files
# differ between machines.
function(mkw_runtime_fingerprint_inputs out_var runtime_dir)
get_filename_component(_root "${runtime_dir}" ABSOLUTE)
file(GLOB_RECURSE _files LIST_DIRECTORIES false RELATIVE "${_root}"
"${_root}/src/*" "${_root}/include/*" "${_root}/cmake/*")
list(FILTER _files EXCLUDE REGEX "(^|/)\\.")
list(APPEND _files "CMakeLists.txt")
list(SORT _files COMPARE STRING CASE SENSITIVE)
set(${out_var} "${_files}" PARENT_SCOPE)
endfunction()
function(mkw_runtime_fingerprint out_var runtime_dir cppwinrt_dir cpu_profile)
get_filename_component(_root "${runtime_dir}" ABSOLUTE)
mkw_runtime_fingerprint_inputs(_files "${_root}")
set(_manifest "")
foreach(_file IN LISTS _files)
if(NOT EXISTS "${_root}/${_file}")
message(FATAL_ERROR "Runtime fingerprint input is missing: ${_root}/${_file}")
endif()
file(SHA256 "${_root}/${_file}" _hash)
string(APPEND _manifest "${_file} ${_hash}\n")
endforeach()
if(cppwinrt_dir)
if(NOT EXISTS "${cppwinrt_dir}/winrt/base.h")
message(FATAL_ERROR "MKW_CPPWINRT_INCLUDE_DIR does not contain winrt/base.h: ${cppwinrt_dir}")
endif()
file(SHA256 "${cppwinrt_dir}/winrt/base.h" _hash)
string(APPEND _manifest "cppwinrt:winrt/base.h ${_hash}\n")
endif()
string(APPEND _manifest "x86_cpu_profile ${cpu_profile}\n")
string(APPEND _manifest "osx_deployment_target ${CMAKE_OSX_DEPLOYMENT_TARGET}\n")
string(SHA256 _fingerprint "${_manifest}")
set(${out_var} "${_fingerprint}" PARENT_SCOPE)
endfunction()
# Sets out_use to TRUE/FALSE and out_reason to a one-line explanation. Pure, so
# it can be exercised in script mode without a configured project.
cmake_policy(PUSH)
if(POLICY CMP0174)
cmake_policy(SET CMP0174 NEW)
endif()
function(mkw_runtime_prebuilt_decision out_use out_reason)
cmake_parse_arguments(PARSE_ARGV 2 _d ""
"ARCHIVE;ENABLED;X86_64;ARCHIVE_EXISTS;PACKAGE_PROFILE;PROFILE;PACKAGE_FINGERPRINT;FINGERPRINT" "")
set(_use FALSE)
if(NOT _d_ARCHIVE)
set(_reason "the native prebuilt package has no runtime archive")
elseif(NOT _d_ENABLED)
set(_reason "MKW_USE_PREBUILT_RUNTIME is OFF")
elseif(NOT _d_X86_64)
set(_reason "the precompiled runtime is x86_64-only")
elseif(NOT _d_ARCHIVE_EXISTS)
set(_reason "the runtime archive is missing: ${_d_ARCHIVE}")
elseif(NOT _d_PACKAGE_PROFILE STREQUAL _d_PROFILE)
set(_reason "the package was built for x86-64-${_d_PACKAGE_PROFILE}, this build targets x86-64-${_d_PROFILE}")
elseif(NOT _d_PACKAGE_FINGERPRINT)
set(_reason "the package records no runtime fingerprint")
elseif(NOT _d_FINGERPRINT OR NOT _d_PACKAGE_FINGERPRINT STREQUAL _d_FINGERPRINT)
set(_reason "runtime sources differ from the package (package ${_d_PACKAGE_FINGERPRINT}, workspace ${_d_FINGERPRINT})")
else()
set(_use TRUE)
set(_reason "fingerprint ${_d_FINGERPRINT} matches")
endif()
set(${out_use} ${_use} PARENT_SCOPE)
set(${out_reason} "${_reason}" PARENT_SCOPE)
endfunction()
cmake_policy(POP)
if(CMAKE_SCRIPT_MODE_FILE)
get_filename_component(_mkw_script "${CMAKE_SCRIPT_MODE_FILE}" REALPATH)
get_filename_component(_mkw_self "${CMAKE_CURRENT_LIST_FILE}" REALPATH)
endif()
if(CMAKE_SCRIPT_MODE_FILE AND _mkw_script STREQUAL _mkw_self)
if(NOT MKW_RUNTIME_DIR OR NOT IS_DIRECTORY "${MKW_RUNTIME_DIR}")
message(FATAL_ERROR "Pass -DMKW_RUNTIME_DIR=<runtime directory>")
endif()
if(NOT MKW_X86_CPU_PROFILE)
set(MKW_X86_CPU_PROFILE "v3")
endif()
# Same default runtime/CMakeLists.txt forces on every platform before project().
if(NOT CMAKE_OSX_DEPLOYMENT_TARGET)
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0")
endif()
mkw_runtime_fingerprint(_mkw_fingerprint "${MKW_RUNTIME_DIR}" "${MKW_CPPWINRT_INCLUDE_DIR}" "${MKW_X86_CPU_PROFILE}")
# message() writes to stderr; the echo child inherits stdout.
execute_process(COMMAND "${CMAKE_COMMAND}" -E echo "runtime_fingerprint=${_mkw_fingerprint}")
endif()
-180
View File
@@ -1,180 +0,0 @@
# The game-independent native runtime (runtime/src). One definition serves
# three uses: the OBJECT library the products link from source, the STATIC
# archive the native prebuilt export ships (mkw_runtime), and that archive
# imported in place of compiling it (mkw_runtime_prebuilt).
include("${CMAKE_CURRENT_LIST_DIR}/RuntimeFingerprint.cmake")
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_HOST_IS_X86_64 TRUE)
else()
set(MKW_HOST_IS_X86_64 FALSE)
endif()
# Windows, Linux and Intel macOS x86_64 use the selected profile. AArch64
# builds are compiled locally for the host that will run them, so both Linux
# and Apple Silicon use the compiler's native CPU tuning rather than leaving
# target-specific performance on the table.
if(MKW_HOST_IS_X86_64)
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-${MKW_X86_CPU_PROFILE})
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
else()
set(MKW_BASELINE_ARCH_FLAG "")
endif()
if(MKW_PLATFORM_MACOS)
find_library(MKW_COREAUDIO_FRAMEWORK CoreAudio REQUIRED)
endif()
# CMake decides WHOLE_ARCHIVE's form by probing ld.lld in ELF mode, which
# accepts --push-state; the MinGW lld driver rejects it at link time.
if(MKW_PLATFORM_WINDOWS)
set(CMAKE_CXX_LINK_LIBRARY_USING_WHOLE_ARCHIVE
"LINKER:--whole-archive" "<LINK_ITEM>" "LINKER:--no-whole-archive")
endif()
function(mkw_apply_common_compile_options target)
target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe)
endfunction()
function(mkw_apply_baseline_arch target)
if(MKW_BASELINE_ARCH_FLAG)
target_compile_options(${target} PRIVATE ${MKW_BASELINE_ARCH_FLAG})
if(MKW_X86_CPU_PROFILE STREQUAL "v2")
target_compile_definitions(${target} PRIVATE MKW_X86_CPU_PROFILE_V2=1)
endif()
endif()
endfunction()
function(mkw_configure_object_target target)
target_include_directories(${target} PRIVATE
"${MKW_RUNTIME_SOURCE_DIR}/include"
"${MKW_RUNTIME_SOURCE_DIR}/src"
# Workspace root, so translator output is spelled "generated/<x>.h"
# instead of a ../ chain whose depth depends on the includer.
"${MKW_RUNTIME_SOURCE_DIR}/.."
"${MKW_RUNTIME_SOURCE_DIR}/../aurora-main/include")
target_compile_definitions(${target} PRIVATE
TARGET_PC)
set_target_properties(${target} PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON)
endfunction()
# scope is PRIVATE for a compiled target and INTERFACE for the imported archive.
function(mkw_runtime_link_dependencies target scope)
target_link_libraries(${target} ${scope}
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
target_link_libraries(${target} ${scope} mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
if(MKW_PLATFORM_WINDOWS)
target_link_libraries(${target} ${scope} shell32 windowsapp)
elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
# media monitoring). Empty on platforms where dl* is already in libc/libSystem.
target_link_libraries(${target} ${scope} mkw::libco ${CMAKE_DL_LIBS})
endif()
if(MKW_PLATFORM_MACOS)
# CoreAudio framework is required for automatic music muting on macOS.
target_link_libraries(${target} ${scope} "${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(${target} ${scope} mkw::libco)
endif()
endif()
endfunction()
# Compiles the runtime sources as an OBJECT or STATIC library.
function(mkw_add_runtime_library target type)
set(_sources ${ARGN})
add_library(${target} ${type} ${_sources})
mkw_configure_object_target(${target})
target_compile_features(${target} PRIVATE cxx_std_20)
target_compile_definitions(${target} PRIVATE
SDL_MAIN_HANDLED
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
mkw_runtime_link_dependencies(${target} PRIVATE)
if(MKW_CPPWINRT_INCLUDE_DIR)
if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
message(FATAL_ERROR
"MKW_CPPWINRT_INCLUDE_DIR does not contain winrt/base.h: ${MKW_CPPWINRT_INCLUDE_DIR}")
endif()
target_include_directories(${target} PRIVATE "${MKW_CPPWINRT_INCLUDE_DIR}")
endif()
# Keep runtime unity units small and semantically related. The old generated-TU
# batch size put all 57 native runtime sources into one memory-heavy compiler job.
foreach(source IN LISTS _sources)
get_filename_component(source_name "${source}" NAME_WE)
string(REPLACE "\\" "/" source_normalized "${source}")
if(source_normalized MATCHES "/hle/gx/")
set(runtime_group "gx_bridge")
elseif(source_name MATCHES "network|socket|dns|dwc|ios")
set(runtime_group "network_ios")
elseif(source_name MATCHES "os_|system|memory|fiber|scheduler")
set(runtime_group "guest_system")
elseif(source_name MATCHES "debug|trace|prof")
set(runtime_group "diagnostics")
else()
string(SHA256 source_hash "${source_name}")
string(SUBSTRING "${source_hash}" 0 4 source_hash_prefix)
math(EXPR runtime_bucket "0x${source_hash_prefix} % 8")
set(runtime_group "runtime_${runtime_bucket}")
endif()
set_source_files_properties("${source}" PROPERTIES UNITY_GROUP "${runtime_group}")
endforeach()
# PPC semantic sources are excluded from unity/PCH and configured in CMakeLists.txt.
set_target_properties(${target} PROPERTIES UNITY_BUILD ON UNITY_BUILD_MODE GROUP)
target_precompile_headers(${target} PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
mkw_apply_common_compile_options(${target})
mkw_apply_baseline_arch(${target})
endfunction()
function(mkw_import_prebuilt_runtime target archive)
add_library(${target} STATIC IMPORTED GLOBAL)
set_target_properties(${target} PROPERTIES IMPORTED_LOCATION "${archive}")
mkw_runtime_link_dependencies(${target} INTERFACE)
endfunction()
# Sets MKW_RUNTIME_USE_PREBUILT in the caller. The archive replaces the
# runtime's objects only when it was built from exactly this workspace's
# runtime inputs for the same x86-64 profile.
function(mkw_select_runtime_provider)
set(_archive "${MKW_NP_RUNTIME_ARCHIVE_PATH}")
set(_exists FALSE)
if(_archive AND EXISTS "${_archive}")
set(_exists TRUE)
endif()
set(_fingerprint "")
if(_exists AND MKW_USE_PREBUILT_RUNTIME AND MKW_HOST_IS_X86_64 AND
MKW_NP_RUNTIME_CPU_PROFILE STREQUAL MKW_X86_CPU_PROFILE)
mkw_runtime_fingerprint(_fingerprint "${MKW_RUNTIME_SOURCE_DIR}"
"${MKW_CPPWINRT_INCLUDE_DIR}" "${MKW_X86_CPU_PROFILE}")
endif()
mkw_runtime_prebuilt_decision(_use _reason
ARCHIVE "${_archive}"
ENABLED "${MKW_USE_PREBUILT_RUNTIME}"
X86_64 "${MKW_HOST_IS_X86_64}"
ARCHIVE_EXISTS "${_exists}"
PACKAGE_PROFILE "${MKW_NP_RUNTIME_CPU_PROFILE}"
PROFILE "${MKW_X86_CPU_PROFILE}"
PACKAGE_FINGERPRINT "${MKW_NP_RUNTIME_FINGERPRINT}"
FINGERPRINT "${_fingerprint}")
if(NOT MKW_NATIVE_PREBUILT_DIR)
set(_reason "no native prebuilt package is configured")
endif()
if(_use)
message(STATUS "Native runtime: precompiled archive ${_archive} (${_reason})")
# Any runtime edit must re-run configure so the mismatch is noticed.
mkw_runtime_fingerprint_inputs(_inputs "${MKW_RUNTIME_SOURCE_DIR}")
list(TRANSFORM _inputs PREPEND "${MKW_RUNTIME_SOURCE_DIR}/")
if(MKW_CPPWINRT_INCLUDE_DIR)
list(APPEND _inputs "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
endif()
set_property(DIRECTORY "${CMAKE_SOURCE_DIR}" APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${_inputs})
else()
message(STATUS "Native runtime: compiled from source (${_reason})")
endif()
set(MKW_RUNTIME_USE_PREBUILT ${_use} PARENT_SCOPE)
endfunction()
-8
View File
@@ -1,8 +0,0 @@
#include "guest_small_data.h"
#include "generated/RuntimeConfig.h"
namespace GuestSmallData {
extern const uint32_t kSda1Base = RuntimeConfig::SDA1_BASE;
extern const uint32_t kSda2Base = RuntimeConfig::SDA2_BASE;
} // namespace GuestSmallData
-7
View File
@@ -1,7 +0,0 @@
// Compiled per user rather than into the shared runtime archive.
extern const char* const kMkwSetupVersion =
#if defined(MKW_SETUP_VERSION)
MKW_SETUP_VERSION;
#else
"";
#endif
+629 -5
View File
@@ -18,14 +18,638 @@
#include <tuple>
#include <type_traits>
#include "guest_abi/dispatch.h"
inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx);
// Some game-facing runtime options alter arguments at well-defined ABI
// boundaries. Keep this independent of the dispatch mechanism: generated
// static calls deliberately bypass InvokeDirectCpu for performance.
// (used for the path mask filtering in ScnRenderer::createPath: depth of
// field is always removed, bloom when the user disabled it)
inline void ApplyRuntimeCallOptions(uint32_t target, CpuContext* ctx) {
if (target == 0x8023BD38u) {
// ScnRenderer::createPath receives the post-processing path mask in r4.
ctx->gpr[4] = RuntimeGameGraphicsOptions::FilterScnRendererPathMask(ctx->gpr[4]);
}
}
// Persistent per-thread CPU context used across translated function calls.
CpuContext& GetPersistentCpuContext();
void InitializePersistentCpuContext();
std::optional<TranslatedFunctionInfo> FindNearestTranslatedFunction(uint32_t address);
// Look up a registered function by host (native) address.
// This enables stack trace symbolication for translated functions.
std::optional<TranslatedFunctionInfo> FindTranslatedFunctionByHostAddress(uintptr_t hostAddr);
enum class FunctionKind : uint8_t {
BaseTranslated = 0,
ModTranslated = 1,
Native = 2,
};
inline constexpr uint32_t kPpcAllNonvolatileFprMask = 0xffffc000u;
inline constexpr uint32_t kBaseTranslatedFunctionPriority = 0;
inline constexpr uint32_t kModTranslatedFunctionPriorityBase = 100;
inline constexpr uint32_t kNativeFunctionPriority = 10000;
#include "isa/ppc_isa_cr.h"
struct TranslatedFunctionInfo {
uint32_t address = 0;
const char* name = "";
uint64_t moduleId = 0;
uint32_t priority = 0;
uint32_t nonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
void* entryPoint = nullptr; // Raw typed function pointer for indirect-call resolution
void (*rawCpuInvoker)(CpuContext*) = nullptr;
bool mustRemainDynamicallyDispatchable = true;
FunctionKind kind = FunctionKind::BaseTranslated;
};
struct RawDispatchRecord {
uint32_t address = 0;
void (*entry)(CpuContext*) = nullptr;
uint32_t nonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
bool preserveNonvolatileGprs = false;
};
// Translator-owned indirect dispatch is split by the high address byte and
// then by 4 KiB guest pages. Each populated segment stores only the contiguous
// page range that contains verified function entry points. The final search is
// bounded to at most the 1024 aligned instruction addresses in one guest page.
struct StaticIndirectDispatchPage {
uint32_t firstEntry = 0;
uint16_t entryCount = 0;
// Explicit tail padding to an 8-byte stride. The emitter writes it as a
// literal third initializer, so it is part of the generated-data contract.
uint16_t reserved = 0;
};
struct StaticIndirectDispatchSegment {
const StaticIndirectDispatchPage* pages = nullptr;
uint16_t firstPage = 0;
uint16_t pageCount = 0;
};
struct StaticIndirectDispatchTable {
const char* profileName = nullptr;
const StaticIndirectDispatchSegment* segments = nullptr; // Exactly 256 high-byte segments.
const RawDispatchRecord* entries = nullptr;
size_t entryCount = 0;
};
inline const RawDispatchRecord* FindStaticIndirectDispatchEntry(
const StaticIndirectDispatchTable* table,
uint32_t address) noexcept {
if (!table || !table->segments || !table->entries || table->entryCount == 0) {
return nullptr;
}
const auto& segment = table->segments[address >> 24];
const uint32_t pageNumber = (address >> 12) & 0x0FFFu;
if (!segment.pages || pageNumber < segment.firstPage) {
return nullptr;
}
const uint32_t relativePage = pageNumber - segment.firstPage;
if (relativePage >= segment.pageCount) {
return nullptr;
}
const auto& page = segment.pages[relativePage];
const size_t first = page.firstEntry;
const size_t count = page.entryCount;
if (first > table->entryCount || count > table->entryCount - first) {
return nullptr;
}
size_t lower = first;
size_t upper = first + count;
while (lower < upper) {
const size_t middle = lower + (upper - lower) / 2;
const uint32_t candidate = table->entries[middle].address;
if (candidate < address) {
lower = middle + 1;
} else {
upper = middle;
}
}
return lower < first + count && table->entries[lower].address == address
? &table->entries[lower]
: nullptr;
}
void RegisterStaticIndirectDispatchTable(const StaticIndirectDispatchTable* table);
inline std::atomic<const StaticIndirectDispatchTable*> g_publishedStaticIndirectDispatchTable{nullptr};
class StaticIndirectDispatchTableRegistrar {
public:
explicit StaticIndirectDispatchTableRegistrar(const StaticIndirectDispatchTable* table) {
RegisterStaticIndirectDispatchTable(table);
}
};
inline unsigned PpcLowestSetBitIndex(uint32_t value) noexcept {
return static_cast<unsigned>(__builtin_ctz(value));
}
class PpcNonvolatileFprGuard {
public:
explicit PpcNonvolatileFprGuard(CpuContext* cpu, uint32_t mask = kPpcAllNonvolatileFprMask) noexcept
: cpu_(cpu), mask_(mask & kPpcAllNonvolatileFprMask) {
if (!cpu_ || mask_ == 0) {
return;
}
// Real masks are a single contiguous register run - f14..f31 for the
// conservative default and f23..f31 or similar for a measured writer -
// so resolve the run once and copy it as one straight-line block instead
// of testing 18 mask bits. Any other mask keeps the per-bit loop, which
// saves and restores exactly the same registers.
const uint32_t lowest = mask_ & (0u - mask_);
const uint32_t aboveRun = mask_ + lowest;
if ((aboveRun & (aboveRun - 1u)) == 0u) {
const unsigned first = PpcLowestSetBitIndex(mask_);
const unsigned end = aboveRun == 0u ? 32u : PpcLowestSetBitIndex(aboveRun);
firstSlot_ = static_cast<uint8_t>(first - 14u);
slotCount_ = static_cast<uint8_t>(end - first);
const size_t count = slotCount_;
for (size_t i = 0; i < count; ++i) {
saved_[firstSlot_ + i] = cpu_->fpr[first + i];
}
return;
}
for (size_t i = 0; i < saved_.size(); ++i) {
if ((mask_ & (1u << (14 + i))) == 0) {
continue;
}
saved_[i] = cpu_->fpr[14 + i];
}
}
~PpcNonvolatileFprGuard() noexcept {
if (!cpu_ || mask_ == 0) {
return;
}
if (slotCount_ != 0) {
const unsigned first = 14u + firstSlot_;
const size_t count = slotCount_;
for (size_t i = 0; i < count; ++i) {
cpu_->fpr[first + i] = saved_[firstSlot_ + i];
}
return;
}
for (size_t i = 0; i < saved_.size(); ++i) {
if ((mask_ & (1u << (14 + i))) == 0) {
continue;
}
cpu_->fpr[14 + i] = saved_[i];
}
}
PpcNonvolatileFprGuard(const PpcNonvolatileFprGuard&) = delete;
PpcNonvolatileFprGuard& operator=(const PpcNonvolatileFprGuard&) = delete;
private:
CpuContext* cpu_ = nullptr;
uint32_t mask_ = 0;
// Contiguous-run description of mask_. slotCount_ == 0 means the mask is not
// a single run, so the destructor mirrors the constructor's per-bit loop.
uint8_t firstSlot_ = 0;
uint8_t slotCount_ = 0;
// Every element selected by mask_ is written before it is read. Leaving the
// remaining slots uninitialized avoids zero-filling all 18 values around a
// call that may preserve only one or two architectural FPRs.
std::array<PPC_FPR, 18> saved_;
};
class PpcNonvolatileGprGuard {
public:
explicit PpcNonvolatileGprGuard(CpuContext* cpu, bool enabled = true) noexcept
: cpu_(enabled ? cpu : nullptr) {
if (!cpu_) {
return;
}
for (size_t i = 0; i < saved_.size(); ++i) {
saved_[i] = cpu_->gpr[14 + i];
}
}
~PpcNonvolatileGprGuard() noexcept {
if (!cpu_) {
return;
}
for (size_t i = 0; i < saved_.size(); ++i) {
cpu_->gpr[14 + i] = saved_[i];
}
}
PpcNonvolatileGprGuard(const PpcNonvolatileGprGuard&) = delete;
PpcNonvolatileGprGuard& operator=(const PpcNonvolatileGprGuard&) = delete;
private:
CpuContext* cpu_ = nullptr;
// Left uninitialized: enabled writes all 18 slots before reading them, disabled reads none,
// so skipping the zero-fill saves 72 bytes of stack work on the common disabled hot path.
std::array<uint32_t, 18> saved_;
};
// Guest address of OSLoadContext, which models rfi-style context restoration
// and intentionally replaces the full guest register file instead of returning
// like a normal ABI call. It can never be guarded.
inline constexpr uint32_t kOSLoadContextAddress = 0x801A1F58u;
inline bool ShouldPreserveNonvolatileGprsForRawCpuCall(const TranslatedFunctionInfo* info) noexcept {
return info->kind == FunctionKind::Native && info->address != kOSLoadContextAddress;
}
inline uint32_t NonvolatileFprGuardMaskFor(const TranslatedFunctionInfo* info) noexcept {
if (!info || info->nonvolatileFprWriteMask == 0) {
return 0;
}
return info->nonvolatileFprWriteMask & kPpcAllNonvolatileFprMask;
}
template <uint32_t Target>
struct KnownTranslatedCpuCall {
static constexpr bool kAvailable = false;
static constexpr uint32_t kNonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
static constexpr bool kMustRemainDynamicallyDispatchable = true;
static constexpr void (*Entry)(CpuContext*) = nullptr;
};
// Translator-emitted trait specializations, one macro shared by every generated shard.
// `addr` is the 8-digit hex entry point, `winner` the resolved symbol (mods/Retro Rewind can
// publish their own name). A specialization only exists for a single statically bound winner,
// so overridable/dynamically-dispatchable are hardcoded here rather than passed in.
#define MKW_TRANSLATED_TRAIT(addr, winner, nonvolatile_fpr_write_mask) \
extern "C" void winner(CpuContext* ctx); \
template <> \
struct KnownTranslatedCpuCall<0x##addr##u> { \
static constexpr bool kAvailable = true; \
static constexpr uint32_t kNonvolatileFprWriteMask = nonvolatile_fpr_write_mask; \
static constexpr bool kMustRemainDynamicallyDispatchable = false; \
static constexpr void (*Entry)(CpuContext*) = &winner; \
}
template <uint32_t Target>
struct KnownNativeCpuCall {
static constexpr bool kAvailable = false;
static constexpr uint32_t kNonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;
static constexpr void (*Entry)(CpuContext*) = nullptr;
};
template <uint32_t Target>
struct KnownTypedNativeCpuCall {
static constexpr bool kAvailable = false;
};
#include "native_cpu_calls.inc"
// Direct-mapped thread-local memo in front of the sorted registry lookup every bctrl performs;
// a miss just re-runs the sorted lookup. 512 entries (12 KiB) covers the per-frame indirect
// working set while staying L1/L2 resident, unlike 4096 which would thrash L2. Must stay a
// power of two, the index masks with (size - 1).
inline constexpr size_t kIndirectDispatchCacheEntries = 512;
// Namespace-scope `inline thread_local`, not function-local `static thread_local`, to avoid a
// thread-static init epoch check on every bctrl (same as g_currentCpuContext in ppc_runtime.h).
struct IndirectResolvedDispatchMemoEntry {
bool valid;
uint32_t address;
const TranslatedFunctionInfo* info;
};
inline thread_local IndirectResolvedDispatchMemoEntry
g_indirectResolvedDispatchMemo[kIndirectDispatchCacheEntries]{};
struct IndirectRawDispatchMemoEntry {
bool valid;
uint32_t address;
const RawDispatchRecord* record;
};
inline thread_local IndirectRawDispatchMemoEntry
g_indirectRawDispatchMemo[kIndirectDispatchCacheEntries]{};
class TranslatedFunctionRegistry {
public:
static void Register(TranslatedFunctionInfo info);
static void Finalize();
static inline bool IsLookupPublished() noexcept {
return lookupPublished_.load(std::memory_order_acquire);
}
static std::optional<TranslatedFunctionInfo> FindNearestByAddress(uint32_t address);
static inline const TranslatedFunctionInfo* FindByAddressPtr(uint32_t address) {
if (!lookupPublished_.load(std::memory_order_acquire)) {
return FindByAddressPtrSlow(address);
}
auto& cached = g_indirectResolvedDispatchMemo[
((address >> 2) * 2654435761u) & (kIndirectDispatchCacheEntries - 1)];
if (cached.valid && cached.address == address) {
return cached.info;
}
const auto* info = FindByAddressPtrSlow(address);
cached = {true, address, info};
return info;
}
// Hot wrapper: only the direct-mapped memo probe stays inline. Inlining the
// generated-table binary search alongside it pushed the whole function past
// the inliner's budget and out-of-lined the probe itself, which is the part
// every bctrl executes.
static MKW_PPC_FORCE_INLINE const RawDispatchRecord* FindRawByAddressPtr(uint32_t address) {
if (!lookupPublished_.load(std::memory_order_acquire)) {
return nullptr;
}
auto& cached = g_indirectRawDispatchMemo[
((address >> 2) * 2654435761u) & (kIndirectDispatchCacheEntries - 1)];
if (cached.valid && cached.address == address) {
return cached.record;
}
return FindRawByAddressPtrMiss(address, cached);
}
// Look up a registered function by host (native) address.
// This enables stack trace symbolication for translated functions.
static std::optional<TranslatedFunctionInfo> FindByHostAddress(uintptr_t hostAddr);
private:
// Memo miss: resolve against the header-inline generated table first; only dynamically
// registered records or a not-yet-published table fall through to the slow path.
static MKW_PPC_NO_INLINE MKW_PPC_COLD const RawDispatchRecord* FindRawByAddressPtrMiss(
uint32_t address, IndirectRawDispatchMemoEntry& cached) {
const RawDispatchRecord* record = nullptr;
if (const auto* table =
g_publishedStaticIndirectDispatchTable.load(std::memory_order_acquire)) {
record = FindStaticIndirectDispatchEntry(table, address);
}
if (record == nullptr) {
record = FindRawByAddressPtrSlow(address);
}
cached = {true, address, record};
return record;
}
static const TranslatedFunctionInfo* FindByAddressPtrSlow(uint32_t address);
static const RawDispatchRecord* FindRawByAddressPtrSlow(uint32_t address);
// Finalize publishes immutable registry, generated-table, and dynamic raw
// dispatch storage. Header-inlined cache hits are enabled only after that
// release publication, so a pre-finalization miss can never poison them.
inline static std::atomic_bool lookupPublished_{false};
};
// Cold, profile-owned registration data is emitted in compact table shards.
// Keeping it out of generated function bodies avoids one static constructor
// per translated function.
struct BulkTranslatedFunctionRecord {
uint32_t address;
const char* name;
void (*entry)(CpuContext*);
FunctionKind kind;
bool preservesNonvolatileFprs;
uint32_t nonvolatileFprWriteMask;
uint32_t priority;
uint64_t moduleId;
bool mustRemainDynamicallyDispatchable;
};
void RegisterBulkTranslatedFunctions(const BulkTranslatedFunctionRecord* records, size_t count);
class BulkTranslatedFunctionRegistrar {
public:
BulkTranslatedFunctionRegistrar(const BulkTranslatedFunctionRecord* records, size_t count) {
RegisterBulkTranslatedFunctions(records, count);
}
};
MKW_PPC_FORCE_INLINE bool TryDispatchResolvedCpuTarget(const TranslatedFunctionInfo* info, CpuContext* cpu) {
if (!info || !info->rawCpuInvoker) {
return false;
}
RecompMod::ScopedTranslatedExecutionAddress translatedExecution(info->address);
PpcNonvolatileFprGuard fprGuard(cpu, NonvolatileFprGuardMaskFor(info));
PpcNonvolatileGprGuard gprGuard(cpu, ShouldPreserveNonvolatileGprsForRawCpuCall(info));
if (TryGetCpuContext() != cpu) {
CpuContextScope scope(cpu);
info->rawCpuInvoker(cpu);
return true;
}
info->rawCpuInvoker(cpu);
return true;
}
inline bool TryDispatchRawCpuTarget(const RawDispatchRecord* record, CpuContext* cpu) {
if (!record || !record->entry) {
return false;
}
RecompMod::ScopedTranslatedExecutionAddress translatedExecution(record->address);
PpcNonvolatileGprGuard gprGuard(cpu, record->preserveNonvolatileGprs);
PpcNonvolatileFprGuard fprGuard(cpu, record->nonvolatileFprWriteMask);
if (TryGetCpuContext() != cpu) {
CpuContextScope scope(cpu);
record->entry(cpu);
} else {
record->entry(cpu);
}
return true;
}
template <uint32_t Target>
inline void DispatchKnownTranslatedCpuTargetStatic(CpuContext* cpu) {
const auto invokeKnownTranslated = [&]() {
KnownTranslatedCpuCall<Target>::Entry(cpu);
};
// A statically resolved, non-overridable translated call already running
// in this CpuContext needs no registry, context, or diagnostic target
// transition. This is the normal generated-to-generated hot path.
if (TryGetCpuContext() == cpu) {
if constexpr (KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask == 0) {
invokeKnownTranslated();
return;
} else {
PpcNonvolatileFprGuard fprGuard(
cpu, KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask);
invokeKnownTranslated();
return;
}
}
CpuContextScope contextScope(cpu);
RecompMod::ScopedTranslatedExecutionAddress translatedExecution(Target);
if constexpr (KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask == 0) {
invokeKnownTranslated();
return;
} else {
PpcNonvolatileFprGuard fprGuard(cpu, KnownTranslatedCpuCall<Target>::kNonvolatileFprWriteMask);
invokeKnownTranslated();
}
}
[[noreturn]] inline void ReportMissingCpuTarget(uint32_t target, CpuContext* cpu) {
// Centralized crash path: stderr diagnostics, heuristics, crash artifacts
// in the run's log folder, popup, exit.
RuntimeCrash::FatalMissingGuestTarget(target, cpu);
}
template <uint32_t Target>
inline const TranslatedFunctionInfo* ResolveDirectCpuTargetInfo() {
return TranslatedFunctionRegistry::FindByAddressPtr(Target);
}
template <uint32_t Target>
inline const TranslatedFunctionInfo* ResolveDirectCpuTargetInfoCached() {
// Only latch a result once the registry is published (immutable for the process lifetime);
// a pre-finalization miss or provisional winner must never be cached. 0 = not yet resolved,
// 1 = negative-cache sentinel, avoiding a second ready flag on the hot path.
static std::atomic<uintptr_t> cachedValue{0};
constexpr uintptr_t kNegativeCache = 1;
const uintptr_t cached = cachedValue.load(std::memory_order_acquire);
if (cached != 0) {
return cached == kNegativeCache
? nullptr
: reinterpret_cast<const TranslatedFunctionInfo*>(cached);
}
if (!TranslatedFunctionRegistry::IsLookupPublished()) {
return ResolveDirectCpuTargetInfo<Target>();
}
const auto* info = ResolveDirectCpuTargetInfo<Target>();
cachedValue.store(info != nullptr
? reinterpret_cast<uintptr_t>(info)
: kNegativeCache,
std::memory_order_release);
return info;
}
// Caches the "is the published winner still base translation?" verdict for state-free fast
// paths guarding tens of thousands of call sites. Same latching rule as above: 0 = unresolved,
// 1 = negative, 2 = positive, and a verdict computed pre-publication is never stored.
template <uint32_t Target>
inline bool IsBaseTranslatedCpuTargetActive() {
static std::atomic<uint8_t> cachedVerdict{0};
constexpr uint8_t kNegativeVerdict = 1;
constexpr uint8_t kPositiveVerdict = 2;
const uint8_t cached = cachedVerdict.load(std::memory_order_acquire);
if (cached != 0) {
return cached == kPositiveVerdict;
}
const bool publishable = TranslatedFunctionRegistry::IsLookupPublished();
const auto* info = ResolveDirectCpuTargetInfo<Target>();
const bool active = info != nullptr && info->kind == FunctionKind::BaseTranslated;
if (publishable) {
cachedVerdict.store(active ? kPositiveVerdict : kNegativeVerdict,
std::memory_order_release);
}
return active;
}
template <uint32_t Target>
inline void InvokeDirectCpu(CpuContext* ctx) {
static_assert(Target != 0, "InvokeDirectCpu cannot target address 0");
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
ApplyRuntimeCallOptions(Target, cpu);
if constexpr (KnownNativeCpuCall<Target>::kAvailable) {
const auto invokeKnownNative = [&]() {
PpcNonvolatileGprGuard gprGuard(cpu);
if (TryGetCpuContext() != cpu) {
CpuContextScope scope(cpu);
KnownNativeCpuCall<Target>::Entry(cpu);
return;
}
KnownNativeCpuCall<Target>::Entry(cpu);
};
if constexpr (KnownNativeCpuCall<Target>::kNonvolatileFprWriteMask == 0) {
invokeKnownNative();
} else {
PpcNonvolatileFprGuard fprGuard(cpu, KnownNativeCpuCall<Target>::kNonvolatileFprWriteMask);
invokeKnownNative();
}
return;
}
if constexpr (KnownTypedNativeCpuCall<Target>::kAvailable) {
PpcNonvolatileGprGuard gprGuard(cpu);
if (TryGetCpuContext() != cpu) {
CpuContextScope scope(cpu);
KnownTypedNativeCpuCall<Target>::Invoke(cpu);
} else {
KnownTypedNativeCpuCall<Target>::Invoke(cpu);
}
return;
}
if constexpr (KnownTranslatedCpuCall<Target>::kAvailable) {
DispatchKnownTranslatedCpuTargetStatic<Target>(cpu);
return;
}
const auto* registeredInfo = ResolveDirectCpuTargetInfoCached<Target>();
if (TryDispatchResolvedCpuTarget(registeredInfo, cpu)) {
return;
}
ReportMissingCpuTarget(Target, cpu);
}
// Indirect jump (bctr without link) - used for switch tables.
inline void InvokeIndirectJump(uint32_t target, CpuContext* ctx) {
// Use the caller's context, not the persistent global context!
// This is essential for tail calls via bctr where the 'this' pointer (r3)
// must be passed correctly to the target function.
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
if (TryDispatchRawCpuTarget(TranslatedFunctionRegistry::FindRawByAddressPtr(target), cpu)) {
return;
}
// Cold fallback for dynamically registered or signature-only targets.
const auto* info = TranslatedFunctionRegistry::FindByAddressPtr(target);
if (info) {
if (TryDispatchResolvedCpuTarget(info, cpu)) {
return;
}
}
// If not registered, this is likely a switch table jump to an intra-function label.
// The translator should have recognized this pattern and generated proper switch code.
RT_LOG(RT_TAG_RUNTIME) << "InvokeIndirectJump: target 0x"
<< std::hex << target << std::dec
<< " is not a registered function.\n"
<< "This is likely a switch statement jump table that the translator\n"
<< "failed to recognize. The function containing this bctr needs\n"
<< "proper switch pattern recognition." << std::endl;
RT_LOG(RT_TAG_RUNTIME) << "Indirect jump context: pc=0x" << std::hex << cpu->pc
<< " lr=0x" << cpu->lr << " ctr=0x" << cpu->ctr
<< " r1=0x" << cpu->gpr[1] << " r3=0x" << cpu->gpr[3]
<< " r29=0x" << cpu->gpr[29] << " r30=0x" << cpu->gpr[30]
<< " r31=0x" << cpu->gpr[31] << std::dec << std::endl;
DumpHostStackTraceForRuntimeHelper();
std::fflush(stderr);
std::ostringstream message;
message << "The game stopped because an indirect jump targeted guest address 0x"
<< std::hex << target
<< ", but that address is not a registered translated function.\n\n"
<< "The translator may have missed a switch/jump-table target.";
// Same artifact set as RuntimeCrash::FatalMissingGuestTarget, which is this
// path's sibling for indirect *calls*: MarkFatalErrorReported below stops the
// atexit reporter, so the crash log has to be written here.
RuntimeCrash::WriteCrashArtifacts("missing_jump_target", message.str(), &target);
SetRuntimeExitCode(EXIT_FAILURE);
ShowRuntimeFatalPopup("Missing indirect jump target", message.str());
MarkFatalErrorReported();
std::exit(EXIT_FAILURE);
}
inline void InvokeIndirectCpu(uint32_t target, CpuContext* ctx) {
CpuContext* cpu = ctx ? ctx : &GetPersistentCpuContext();
if (target == 0) {
ReportMissingCpuTarget(target, cpu);
}
ApplyRuntimeCallOptions(target, cpu);
if (TryDispatchRawCpuTarget(TranslatedFunctionRegistry::FindRawByAddressPtr(target), cpu)) {
return;
}
if (TryDispatchResolvedCpuTarget(TranslatedFunctionRegistry::FindByAddressPtr(target), cpu)) {
return;
}
ReportMissingCpuTarget(target, cpu);
}
template <typename Signature>
class AbiTrampoline;
+1 -3
View File
@@ -16,9 +16,7 @@
#endif
extern "C" bool g_dynamicAspectRatioEnabled;
void ConfigureMkwDynamicAspect(bool widescreen, bool forceAspect169, uint32_t surfaceWidth, uint32_t surfaceHeight);
void SetMkwForceAspect169(bool enabled);
bool MkwForceAspect169Requested();
void ConfigureMkwDynamicAspect(bool widescreen, 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
// renders to a fixed-size offscreen target. Cheap and idempotent; called from
-14
View File
@@ -1,14 +0,0 @@
#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
-4
View File
@@ -1,4 +0,0 @@
#pragma once
// Bump whenever guest_abi/, isa/ or ppc_isa_memory.h changes what translated code compiles against.
#define MKW_GUEST_ABI_VERSION 1

Some files were not shown because too many files have changed in this diff Show More