mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-10-18 12:59:16 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f89df45039 |
@@ -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
@@ -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")
|
||||
|
||||
@@ -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,5 +1,4 @@
|
||||
/generated/
|
||||
/runtime/native-index.json
|
||||
/Assets/
|
||||
/assets/
|
||||
/PulsarPacks/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 `
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:<id></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,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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
+39
-39
@@ -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:<id></c> lines. The
|
||||
/// <summary>Build step identifiers from the bundled script's <c>MKWCBUILD:STEP:<id></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
|
||||
|
||||
@@ -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
@@ -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 /
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -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) ;;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
@@ -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 - {} +
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
@@ -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
|
||||
|
||||
@@ -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&logo=github"></a>
|
||||
<a href="LICENSE"><img alt="License: GPLv3" src="https://img.shields.io/badge/license-GPLv3-2EA44F?logo=gnu&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
|
||||
|
||||
@@ -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/> |
|
||||
|
||||
@@ -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 ()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Vendored
+13
-63
@@ -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)
|
||||
|
||||
@@ -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
@@ -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(); }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, ©Filter, forceOpaqueAlpha,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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()), ¤tVendor, ¤tProduct, 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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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 = ⌖
|
||||
wgpu::RenderPipelineDescriptor pipelineDescriptor{};
|
||||
pipelineDescriptor.vertex.module = shader;
|
||||
pipelineDescriptor.vertex.entryPoint = "vs";
|
||||
pipelineDescriptor.fragment = &fragment;
|
||||
auto pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
|
||||
auto dawnQueue = device.GetQueue();
|
||||
const uint32_t bytesPerRow = (outWidth * 4 + 255) & ~255u;
|
||||
const uint64_t readbackSize = uint64_t(bytesPerRow) * outHeight;
|
||||
std::vector<wgpu::Buffer> readbacks;
|
||||
|
||||
for (unsigned frame = 0; frame < kFrames; ++frame) {
|
||||
auto& slot = slots[frame % slots.size()];
|
||||
require(slot->begin_input(), "Production MetalFX begin_input failed");
|
||||
const std::array<float, 4> params{0.2f + float(frame % 5) * 0.1f,
|
||||
float(width), float(height), 0};
|
||||
wgpu::BufferDescriptor uniformDescriptor{};
|
||||
uniformDescriptor.size = sizeof(params);
|
||||
uniformDescriptor.usage = wgpu::BufferUsage::Uniform | wgpu::BufferUsage::CopyDst;
|
||||
auto uniform = device.CreateBuffer(&uniformDescriptor);
|
||||
dawnQueue.WriteBuffer(uniform, 0, params.data(), sizeof(params));
|
||||
wgpu::BindGroupEntry entry{};
|
||||
entry.binding = 0;
|
||||
entry.buffer = uniform;
|
||||
entry.size = sizeof(params);
|
||||
wgpu::BindGroupDescriptor bindDescriptor{};
|
||||
bindDescriptor.layout = pipeline.GetBindGroupLayout(0);
|
||||
bindDescriptor.entryCount = 1;
|
||||
bindDescriptor.entries = &entry;
|
||||
auto bindGroup = device.CreateBindGroup(&bindDescriptor);
|
||||
auto encoder = device.CreateCommandEncoder();
|
||||
wgpu::RenderPassColorAttachment attachment{};
|
||||
attachment.view = slot->input_view();
|
||||
attachment.loadOp = wgpu::LoadOp::Clear;
|
||||
attachment.storeOp = wgpu::StoreOp::Store;
|
||||
wgpu::RenderPassDescriptor passDescriptor{};
|
||||
passDescriptor.colorAttachmentCount = 1;
|
||||
passDescriptor.colorAttachments = &attachment;
|
||||
auto pass = encoder.BeginRenderPass(&passDescriptor);
|
||||
pass.SetPipeline(pipeline);
|
||||
pass.SetBindGroup(0, bindGroup);
|
||||
pass.Draw(3);
|
||||
pass.End();
|
||||
auto render = encoder.Finish();
|
||||
dawnQueue.Submit(1, &render);
|
||||
if (!slot->upscale()) throw std::runtime_error(slot->error());
|
||||
|
||||
wgpu::BufferDescriptor readbackDescriptor{};
|
||||
readbackDescriptor.size = readbackSize;
|
||||
readbackDescriptor.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead;
|
||||
auto readback = device.CreateBuffer(&readbackDescriptor);
|
||||
encoder = device.CreateCommandEncoder();
|
||||
wgpu::TexelCopyTextureInfo copySource{};
|
||||
copySource.texture = slot->output_texture();
|
||||
wgpu::TexelCopyBufferInfo destination{};
|
||||
destination.buffer = readback;
|
||||
destination.layout.bytesPerRow = bytesPerRow;
|
||||
destination.layout.rowsPerImage = outHeight;
|
||||
const wgpu::Extent3D extent{outWidth, outHeight, 1};
|
||||
encoder.CopyTextureToBuffer(©Source, &destination, &extent);
|
||||
auto copy = encoder.Finish();
|
||||
dawnQueue.Submit(1, ©);
|
||||
if (!slot->end_output()) throw std::runtime_error(slot->error());
|
||||
readbacks.push_back(std::move(readback));
|
||||
}
|
||||
|
||||
// Model toggle/resize immediately after submission, while either queue may
|
||||
// still be consuming these textures. Production completion callbacks must
|
||||
// keep the resources alive after the cache drops its wrappers.
|
||||
slots = {};
|
||||
|
||||
// Readback is only the test oracle. No CPU image transfer or GPU completion
|
||||
// wait occurs between Dawn rendering, MetalFX, and Dawn consumption above.
|
||||
for (unsigned frame = 0; frame < kFrames; ++frame) {
|
||||
bool mapped = false;
|
||||
auto& readback = readbacks[frame];
|
||||
wait(instance, readback.MapAsync(wgpu::MapMode::Read, 0, readbackSize,
|
||||
wgpu::CallbackMode::WaitAnyOnly, [&mapped](wgpu::MapAsyncStatus status, wgpu::StringView) {
|
||||
mapped = status == wgpu::MapAsyncStatus::Success;
|
||||
}));
|
||||
require(mapped, "Output readback mapping failed");
|
||||
const auto* bytes = static_cast<const uint8_t*>(readback.GetConstMappedRange());
|
||||
require(bytes != nullptr, "Output readback pointer is null");
|
||||
for (unsigned y = 0; y < 4; ++y) {
|
||||
for (unsigned x = 0; x < 4; ++x) {
|
||||
const unsigned px = (2 * x + 1) * outWidth / 8;
|
||||
const unsigned py = (2 * y + 1) * outHeight / 8;
|
||||
const auto* pixel = bytes + py * bytesPerRow + px * 4;
|
||||
const std::array<float, 4> expected{
|
||||
0.2f + float(frame % 5) * 0.1f, x >= 2 ? 0.8f : 0.2f,
|
||||
y >= 2 ? 0.7f : 0.3f, 1};
|
||||
for (unsigned c = 0; c < 4; ++c) {
|
||||
const unsigned channel = bgra && c != 1 && c != 3 ? 2 - c : c;
|
||||
if (std::abs(int(pixel[channel]) - int(std::lround(expected[c] * 255))) > 5) {
|
||||
std::cerr << "Pixel mismatch: frame=" << frame << " x=" << px << " y=" << py
|
||||
<< " channel=" << c << " actual=" << int(pixel[channel])
|
||||
<< " expected=" << std::lround(expected[c] * 255) << '\n';
|
||||
throw std::runtime_error("MetalFX output failed image validation");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
readback.Unmap();
|
||||
}
|
||||
require(g_errors.load() == 0, "Dawn reported validation errors or device loss");
|
||||
std::cout << "PASS " << (bgra ? "BGRA8" : "RGBA8") << ' ' << width << 'x' << height
|
||||
<< " -> " << outWidth << 'x' << outHeight << ": " << kFrames
|
||||
<< " frames, 3 reused slots, 16 pixel samples/frame\n";
|
||||
}
|
||||
|
||||
int run() {
|
||||
const wgpu::InstanceFeatureName timedWait = wgpu::InstanceFeatureName::TimedWaitAny;
|
||||
wgpu::InstanceDescriptor instanceDescriptor{};
|
||||
instanceDescriptor.requiredFeatureCount = 1;
|
||||
instanceDescriptor.requiredFeatures = &timedWait;
|
||||
auto instance = wgpu::CreateInstance(&instanceDescriptor);
|
||||
require(instance != nullptr, "Dawn instance creation failed");
|
||||
wgpu::Adapter adapter;
|
||||
wgpu::RequestAdapterOptions options{};
|
||||
options.backendType = wgpu::BackendType::Metal;
|
||||
wait(instance, instance.RequestAdapter(&options, wgpu::CallbackMode::WaitAnyOnly,
|
||||
[&adapter](wgpu::RequestAdapterStatus status, wgpu::Adapter result, wgpu::StringView message) {
|
||||
if (status == wgpu::RequestAdapterStatus::Success) adapter = std::move(result);
|
||||
else std::cerr << "Adapter: " << std::string_view(message) << '\n';
|
||||
}));
|
||||
if (!adapter) { std::cout << "SKIP: no Dawn Metal adapter\n"; return 77; }
|
||||
const std::array features{wgpu::FeatureName::SharedTextureMemoryIOSurface,
|
||||
wgpu::FeatureName::SharedFenceMTLSharedEvent};
|
||||
for (auto feature : features) {
|
||||
if (!adapter.HasFeature(feature)) {
|
||||
std::cout << "SKIP: Dawn adapter lacks IOSurface/shared-event interoperability\n";
|
||||
return 77;
|
||||
}
|
||||
}
|
||||
wgpu::DeviceDescriptor descriptor{};
|
||||
descriptor.requiredFeatureCount = features.size();
|
||||
descriptor.requiredFeatures = features.data();
|
||||
descriptor.SetUncapturedErrorCallback(
|
||||
[](const wgpu::Device&, wgpu::ErrorType, wgpu::StringView message) {
|
||||
++g_errors;
|
||||
std::cerr << "Dawn error: " << std::string_view(message) << '\n';
|
||||
});
|
||||
descriptor.SetDeviceLostCallback(wgpu::CallbackMode::AllowSpontaneous,
|
||||
[](const wgpu::Device&, wgpu::DeviceLostReason reason, wgpu::StringView message) {
|
||||
if (reason != wgpu::DeviceLostReason::Destroyed) {
|
||||
++g_errors;
|
||||
std::cerr << "Device lost: " << std::string_view(message) << '\n';
|
||||
}
|
||||
});
|
||||
wgpu::Device device;
|
||||
wait(instance, adapter.RequestDevice(&descriptor, wgpu::CallbackMode::WaitAnyOnly,
|
||||
[&device](wgpu::RequestDeviceStatus status, wgpu::Device result, wgpu::StringView message) {
|
||||
if (status == wgpu::RequestDeviceStatus::Success) device = std::move(result);
|
||||
else std::cerr << "Device: " << std::string_view(message) << '\n';
|
||||
}));
|
||||
require(device != nullptr, "Dawn device creation failed");
|
||||
id<MTLDevice> native = dawn::native::metal::GetMTLDevice(device.Get());
|
||||
require(native != nil, "Dawn native Metal device is unavailable");
|
||||
std::cout << "GPU: " << native.name.UTF8String << '\n';
|
||||
if (!aurora::webgpu::metalfx::supported(device, wgpu::BackendType::Metal)) {
|
||||
std::cout << "SKIP: GPU does not support MetalFX spatial scaling\n";
|
||||
return 77;
|
||||
}
|
||||
require(!aurora::webgpu::metalfx::supported(device, wgpu::BackendType::Vulkan),
|
||||
"MetalFX must reject non-Metal backends");
|
||||
std::string error;
|
||||
require(!aurora::webgpu::metalfx::create(instance, device,
|
||||
{128, 96, 128, 96, wgpu::TextureFormat::RGBA8Unorm}, error) && !error.empty(),
|
||||
"MetalFX must reject equal-size input/output");
|
||||
require(!aurora::webgpu::metalfx::create(instance, device,
|
||||
{128, 96, 256, 192, wgpu::TextureFormat::RGBA8UnormSrgb}, error),
|
||||
"MetalFX must reject implicit sRGB conversion");
|
||||
{
|
||||
using namespace aurora::webgpu::metalfx;
|
||||
std::array<std::unique_ptr<SpatialScaler>, 8> resources;
|
||||
for (auto& scaler : resources) {
|
||||
scaler = create(instance, device, {64, 48, 128, 96, wgpu::TextureFormat::RGBA8Unorm}, error);
|
||||
require(scaler != nullptr, "Could not fill the MetalFX resource pool");
|
||||
}
|
||||
require(!create(instance, device, {64, 48, 128, 96, wgpu::TextureFormat::RGBA8Unorm}, error)
|
||||
&& error.empty(), "A full retirement pool must defer allocation without a fatal error");
|
||||
}
|
||||
for (bool bgra : {false, true}) {
|
||||
runCase(instance, device, bgra, 64, 48, 128, 96);
|
||||
runCase(instance, device, bgra, 320, 180, 480, 270);
|
||||
runCase(instance, device, bgra, 960, 540, 1920, 1080);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
@autoreleasepool {
|
||||
try { return run(); }
|
||||
catch (const std::exception& error) {
|
||||
std::cerr << "FAIL: " << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
@@ -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}")
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user