mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-10-18 12:59:16 -04:00
Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3186e5f511 | |||
| efedf42035 | |||
| 3ff14738bd | |||
| 8676d1ab14 | |||
| c255df5136 | |||
| 2ee48fc880 | |||
| d7b64361e7 | |||
| 5ee158918f | |||
| bd12087cc3 | |||
| d5296779ec | |||
| a93eaf582f | |||
| ed6fea0efb | |||
| 95a49f3c89 | |||
| 60197f2e75 | |||
| b87186c96b | |||
| 0b12fc864a | |||
| 90589131b5 | |||
| bd219e38a3 | |||
| 196b48528e | |||
| 54c608ef9c | |||
| f3df63b529 | |||
| dd6f596b45 | |||
| 6fa24737d3 | |||
| 279ce8328f | |||
| 9d182f8316 | |||
| 75886669bc | |||
| a50bad2970 | |||
| 9b7b9913e4 | |||
| a88b7b502b | |||
| d0d58072d1 | |||
| 6b853ca371 | |||
| 77a8623416 | |||
| 10ab54adee | |||
| a05c89739d | |||
| 82991ec337 | |||
| e164af9ff4 | |||
| e409d9f99b | |||
| 85f250155f | |||
| a9c9c0de26 | |||
| c92e45c2e7 | |||
| 583e702547 | |||
| b59e035b87 | |||
| 6f14bde26a | |||
| 83463764b8 | |||
| 8008d885ad | |||
| 7e6604c415 | |||
| 6fb593749e | |||
| 8ec3a1b752 | |||
| 8705e957c7 | |||
| 8e0cc96898 | |||
| 6458ec6abe | |||
| 209405dfb7 | |||
| 4bdaff01fc | |||
| b555ede2d3 | |||
| 53d8f71c68 | |||
| 149cfef608 | |||
| 25c69ae28e | |||
| 0bb15f0a44 | |||
| 466d06d7db | |||
| 8769cf6dea | |||
| 452b478bb3 | |||
| 407f8a7190 | |||
| c2289e4ba4 | |||
| a135beb201 | |||
| 88b990b060 | |||
| e0e362bd99 | |||
| 5654d8f21b | |||
| 730e3122d5 | |||
| f424536d3b | |||
| 2d9dc4e0f2 | |||
| 8e57cc162f | |||
| 1c0a3edee9 | |||
| d1d80613cc | |||
| 5d67b229f6 | |||
| 56db6ba641 | |||
| a67069afd3 | |||
| 009697fb97 | |||
| 3f7fed48c9 | |||
| 6eba523d70 | |||
| 8c6c177857 | |||
| be153e0fa0 | |||
| e34f055b3a | |||
| 602348f905 | |||
| e6f9b2197e | |||
| 65047bc7b7 | |||
| 989d5e00da | |||
| efc44b0482 | |||
| d3d0de62a6 | |||
| c6ef17378e |
@@ -3,3 +3,4 @@
|
||||
|
||||
# Patch files must stay LF: git apply matches context bytes against LF upstream sources
|
||||
*.patch -text
|
||||
translator/tests/Translator.Tests/TestAssets/**/*.bin binary
|
||||
|
||||
@@ -14,6 +14,10 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
recompilation:
|
||||
name: Recompilation test
|
||||
uses: ./.github/workflows/recomp-test.yml
|
||||
|
||||
translator:
|
||||
name: Translator (build + test)
|
||||
runs-on: windows-latest
|
||||
@@ -26,6 +30,14 @@ jobs:
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('translator/Translator.sln', '**/*.csproj', '**/*.props', '**/*.targets', '**/packages.lock.json', 'global.json', 'NuGet.config', 'nuget.config') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore translator/Translator.sln
|
||||
|
||||
@@ -34,3 +46,67 @@ 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
|
||||
|
||||
+142
-10
@@ -1,8 +1,8 @@
|
||||
name: Package installers
|
||||
|
||||
# Builds the per-platform installer/setup tool (WiiCompiled-Setup.exe /
|
||||
# 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
|
||||
# WiiCompiled-Setup-x86_64.AppImage / WiiCompiled-Setup.pkg) via the platform packaging scripts -
|
||||
# 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,6 +11,11 @@ on:
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Package version
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -20,6 +25,10 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
recompilation:
|
||||
name: Recompilation test
|
||||
uses: ./.github/workflows/recomp-test.yml
|
||||
|
||||
linux-appimage:
|
||||
name: Linux (AppImage, ${{ matrix.arch }})
|
||||
strategy:
|
||||
@@ -86,14 +95,139 @@ 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 an asset named exactly
|
||||
# WiiCompiled-Setup.exe, produced by a setup host that reports that same version.
|
||||
# (non-prerelease) release whose tag is v<semver>, carrying the expected platform assets,
|
||||
# produced by setup hosts that report that same version.
|
||||
release:
|
||||
name: Publish GitHub Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [linux-appimage, windows-installer]
|
||||
needs: [linux-appimage, windows-installer, macos-setup-package, recompilation]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -103,7 +237,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 every pinned
|
||||
# version differs from the release tag, so a tag that was pushed without bumping the pinned
|
||||
# version would ship an update nobody can install. Catch that before anything is published.
|
||||
- name: Verify the tag matches the pinned setup version
|
||||
env:
|
||||
@@ -118,9 +252,7 @@ jobs:
|
||||
status=1
|
||||
fi
|
||||
}
|
||||
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'"
|
||||
check Launcher/Directory.Build.props "<Version>$version</Version>"
|
||||
exit $status
|
||||
|
||||
# The build jobs upload with `archive: false`, which stores each installer as a raw file
|
||||
@@ -139,7 +271,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
ls -lR artifacts
|
||||
assets=()
|
||||
for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage; do
|
||||
for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage WiiCompiled-Setup.pkg; do
|
||||
found="$(find artifacts -type f -name "$name" | head -n 1)"
|
||||
[ -n "$found" ] && [ -s "$found" ] || { echo "::error::missing release asset $name"; exit 1; }
|
||||
assets+=("$found")
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
name: Synthetic recompilation
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
windows:
|
||||
name: Windows runtime (synthetic DOL)
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
# Cache downloads only. Preparation still validates pins, and every run
|
||||
# compiles current Aurora, runtime, and generated sources from scratch.
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: Launcher/artifacts/downloads
|
||||
key: windows-recomp-downloads-${{ hashFiles('Launcher/Prepare-PortableTools.ps1', 'Launcher/Prepare-Dependencies.ps1') }}
|
||||
|
||||
# Install sccache.
|
||||
- name: Run sccache-action
|
||||
uses: mozilla/sccache-action@v0.0.11
|
||||
|
||||
# Tell CMake to use sccache and use GitHub's API.
|
||||
- name: Configure sccache environment
|
||||
shell: pwsh
|
||||
run: |
|
||||
"SCCACHE_GHA_ENABLED=true" | Add-Content -Path $env:GITHUB_ENV
|
||||
"ACTIONS_CACHE_SERVICE_V2=on" | Add-Content -Path $env:GITHUB_ENV
|
||||
"CMAKE_C_COMPILER_LAUNCHER=$env:SCCACHE_PATH" | Add-Content -Path $env:GITHUB_ENV
|
||||
"CMAKE_CXX_COMPILER_LAUNCHER=$env:SCCACHE_PATH" | Add-Content -Path $env:GITHUB_ENV
|
||||
|
||||
- name: Prepare the shipped Windows toolchain
|
||||
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
|
||||
|
||||
# Expose cache token context to the build script.
|
||||
- name: Translate, compile the full runtime, and link
|
||||
shell: pwsh
|
||||
run: ./Launcher/Test-Recompilation.ps1 -Parallel 4
|
||||
|
||||
# Print cache results (even if the build fails)
|
||||
- name: Show sccache stats
|
||||
if: always()
|
||||
shell: pwsh
|
||||
run: sccache --show-stats
|
||||
@@ -1,4 +1,5 @@
|
||||
/generated/
|
||||
/runtime/native-index.json
|
||||
/Assets/
|
||||
/assets/
|
||||
/PulsarPacks/
|
||||
@@ -26,6 +27,8 @@ Code.pul
|
||||
/build/
|
||||
/build-*/
|
||||
/native-build/
|
||||
/native-build-macos/
|
||||
/local-products/
|
||||
/dist/
|
||||
/out/
|
||||
[Bb]in/
|
||||
@@ -70,3 +73,5 @@ project.lock.json
|
||||
*.log
|
||||
output.txt
|
||||
|
||||
# Operating System
|
||||
.DS_Store
|
||||
|
||||
@@ -112,6 +112,20 @@ 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 }
|
||||
@@ -129,6 +143,9 @@ 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
|
||||
@@ -136,11 +153,13 @@ 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')))
|
||||
($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))
|
||||
}
|
||||
$nativePrebuiltPackage = Join-Path $dependencySources 'native_prebuilt'
|
||||
if (-not (Test-NativePrebuiltCurrent $nativePrebuiltPackage)) {
|
||||
Write-Host '[0/6] Building the precompiled aurora and third-party package...'
|
||||
Write-Host '[0/6] Building the precompiled aurora, third-party and runtime package...'
|
||||
& (Join-Path $PSScriptRoot 'Prepare-NativePrebuilt.ps1') `
|
||||
-PortableToolsDirectory $portableTools -DependencySourceDirectory $dependencySources
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Precompiled aurora package preparation failed.' }
|
||||
@@ -222,6 +241,22 @@ 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'
|
||||
# The payload has no game inputs, so the project file (which validates them) cannot be loaded here.
|
||||
& $translator emit-native-index --native-source-dir (Join-Path $workspace 'runtime\src') --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')
|
||||
|
||||
@@ -279,9 +314,12 @@ 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 = '0.2.26'
|
||||
ProductVersion = $productVersion
|
||||
ExpectedGameId = $pins.GameId
|
||||
ExpectedDolSha256 = $pins.DolSha256
|
||||
ExpectedRelSha256 = $pins.RelSha256
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- The one product version. Release tags must match it. -->
|
||||
<Version>0.2.34</Version>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
+12
-3
@@ -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 $Workspace)).TrimEnd('\') + '\'
|
||||
$installRoot = [IO.Path]::GetFullPath((Split-Path -Parent $realWorkspace)).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,8 +150,16 @@ 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-MkwShellSafeToolchainRoot $Toolkit
|
||||
$toolchain = Get-MkwBuildSafePath $Toolkit 'toolchain' 'CMake\bin\cmake.exe'
|
||||
$cmake = Join-Path $toolchain 'CMake\bin\cmake.exe'
|
||||
$ninja = Join-Path $toolchain 'Ninja\ninja.exe'
|
||||
$toolchainBin = Join-Path $toolchain 'llvm-mingw\bin'
|
||||
@@ -421,7 +429,8 @@ try {
|
||||
}
|
||||
$redist = Join-Path $Toolkit 'Redist'
|
||||
if (Test-Path -LiteralPath $redist -PathType Container) {
|
||||
Copy-Item -Path (Join-Path $redist '*.dll') -Destination $Destination -Force
|
||||
Get-ChildItem -LiteralPath $redist -Filter '*.dll' -File |
|
||||
Copy-Item -Destination $Destination -Force
|
||||
}
|
||||
foreach ($name in @('config','wii_bootstrap')) {
|
||||
$directory = Join-Path $build $name
|
||||
|
||||
@@ -40,41 +40,55 @@ function Get-MkwToolchainPath([string]$ToolchainRoot) {
|
||||
) -join ';')
|
||||
}
|
||||
|
||||
function Get-MkwShellSafeToolchainRoot([string]$ToolchainRoot) {
|
||||
if ([string]::IsNullOrWhiteSpace($ToolchainRoot)) { throw 'A toolchain root is required.' }
|
||||
$full = [IO.Path]::GetFullPath($ToolchainRoot)
|
||||
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)
|
||||
# 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 -notmatch '[()&^%!]') { return $full }
|
||||
if ($full -cmatch '^[A-Za-z0-9 ._\\:-]+$') { return $full }
|
||||
|
||||
Assert-File (Join-Path $full $MarkerFile) "$Kind marker"
|
||||
|
||||
$sha = [Security.Cryptography.SHA256]::Create()
|
||||
try {
|
||||
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($full.ToLowerInvariant()))
|
||||
} finally { $sha.Dispose() }
|
||||
$linkName = 'toolchain-' + ((($bytes[0..7]) | ForEach-Object { $_.ToString('x2') }) -join '')
|
||||
$linkName = "$Kind-" + ((($bytes[0..7]) | ForEach-Object { $_.ToString('x2') }) -join '')
|
||||
|
||||
$failures = @()
|
||||
foreach ($base in @($env:ProgramData, $env:PUBLIC)) {
|
||||
if ([string]::IsNullOrWhiteSpace($base) -or $base -match '[()&^%! ]') { continue }
|
||||
if ([string]::IsNullOrWhiteSpace($base) -or $base -cnotmatch '^[A-Za-z0-9._\\:-]+$') { continue }
|
||||
$link = Join-Path (Join-Path $base 'WiiCompiled') $linkName
|
||||
try {
|
||||
[IO.Directory]::CreateDirectory((Split-Path -Parent $link)) | Out-Null
|
||||
# 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) }
|
||||
$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 {
|
||||
New-Item -ItemType Junction -Path $link -Target $full -ErrorAction Stop | Out-Null
|
||||
}
|
||||
Write-Host "MKWCBUILD: Building through $link, because $full contains characters cmd.exe cannot parse"
|
||||
Assert-File (Join-Path $link $MarkerFile) "$Kind junction marker"
|
||||
Write-Host "MKWCBUILD: Building the $Kind through $link, because $full contains characters the native build cannot quote reliably"
|
||||
return $link
|
||||
} catch {
|
||||
$failures += "$link ($($_.Exception.Message))"
|
||||
}
|
||||
}
|
||||
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.')
|
||||
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.')
|
||||
}
|
||||
|
||||
function Get-MkwProjectPins([string]$ProjectFile) {
|
||||
@@ -117,12 +131,13 @@ function Get-MkwProjectPins([string]$ProjectFile) {
|
||||
}
|
||||
|
||||
function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Description,
|
||||
[string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '') {
|
||||
[string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '', [bool]$WaitForProcessTree = $true) {
|
||||
<#
|
||||
Runs a build tool and turns a non-zero exit code into a described failure. Start-Process -Wait
|
||||
is deliberate: it waits for the whole process tree, since a .NET single-file bundle host may
|
||||
hand off to an extracted child that PowerShell's call operator would not wait for. Start-Process
|
||||
doesn't publish $LASTEXITCODE, so this sets it manually for callers that check it.
|
||||
Runs a build tool and turns a non-zero exit code into a described failure. By default,
|
||||
Start-Process -Wait waits for the whole process tree, since a .NET single-file bundle host may
|
||||
hand off to an extracted child that PowerShell's call operator would not wait for. Callers that
|
||||
need to avoid waiting on unrelated descendants can opt into the call-operator path.
|
||||
Start-Process doesn't publish $LASTEXITCODE, so this sets it manually for callers that check it.
|
||||
-StepId emits the machine-readable form the installer's progress bar consumes (BuildStepIds in
|
||||
WiiCompiled.Setup/InstallProgress.cs); the human sentence stays on the same log line.
|
||||
#>
|
||||
@@ -130,11 +145,17 @@ 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') + '"'
|
||||
})
|
||||
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
|
||||
-NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
if ($WaitForProcessTree) {
|
||||
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
|
||||
-NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
} else {
|
||||
& $FilePath @Arguments
|
||||
$exitCode = $LASTEXITCODE
|
||||
}
|
||||
$global:LASTEXITCODE = $exitCode
|
||||
if ($exitCode -ne 0) { throw "$Description failed with exit code $exitCode." }
|
||||
}
|
||||
@@ -187,13 +208,14 @@ function Get-MkwThirdPartySourceFingerprint([string]$ThirdPartyDirectory) {
|
||||
SHA-256 over runtime/third_party, the vendored sources behind the prebuilt package. The
|
||||
precompiled Crypto++ archive is linked while first-party TUs compile against the same headers,
|
||||
so a workspace whose vendored tree moved on must not consume archives harvested from an older
|
||||
one (same reasoning as the aurora fingerprint below). Unlike that scan this hashes hidden files
|
||||
too, since Build-Installer stages this tree with Copy-Item -Force. Returns '' when absent.
|
||||
one (same reasoning as the aurora fingerprint below), skipping dot-paths by name the same way.
|
||||
Returns '' when absent.
|
||||
#>
|
||||
if ([string]::IsNullOrWhiteSpace($ThirdPartyDirectory)) { return '' }
|
||||
if (-not (Test-Path -LiteralPath $ThirdPartyDirectory -PathType Container)) { return '' }
|
||||
$root = [IO.Path]::GetFullPath($ThirdPartyDirectory).TrimEnd('\')
|
||||
$files = Get-ChildItem -LiteralPath $root -Recurse -File |
|
||||
$files = Get-ChildItem -LiteralPath $root -Recurse -File -Force |
|
||||
Where-Object { -not ($_.FullName.Substring($root.Length + 1) -split '\\' | Where-Object { $_.StartsWith('.') }) } |
|
||||
Sort-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') }
|
||||
$sha = [Security.Cryptography.SHA256]::Create()
|
||||
try {
|
||||
@@ -224,13 +246,13 @@ function Get-MkwAuroraSourceFingerprint([string]$AuroraDirectory) {
|
||||
(Join-Path $root 'extern') + '\',
|
||||
(Join-Path $root 'build') + '\'
|
||||
)
|
||||
# Hidden files are skipped deliberately: Build-Installer.ps1 stages this tree
|
||||
# with Copy-Item -Recurse (no -Force), so they never reach the payload and
|
||||
# hashing them would make the two sides disagree by construction.
|
||||
$files = Get-ChildItem -LiteralPath $root -Recurse -File |
|
||||
# Dot-paths (the submodule's .git pointer) are skipped by name, not by the hidden attribute:
|
||||
# the payload zip drops that attribute, so the installed side would otherwise hash them.
|
||||
$files = Get-ChildItem -LiteralPath $root -Recurse -File -Force |
|
||||
Where-Object {
|
||||
$full = $_.FullName
|
||||
-not ($excludedPrefixes | Where-Object { $full.StartsWith($_, [StringComparison]::OrdinalIgnoreCase) })
|
||||
-not ($excludedPrefixes | Where-Object { $full.StartsWith($_, [StringComparison]::OrdinalIgnoreCase) }) -and
|
||||
-not ($full.Substring($root.Length + 1) -split '\\' | Where-Object { $_.StartsWith('.') })
|
||||
} |
|
||||
Sort-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') }
|
||||
$sha = [Security.Cryptography.SHA256]::Create()
|
||||
|
||||
@@ -39,9 +39,9 @@ $packages = @(
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'dawn_prebuilt'; File = 'dawn-v20260603.191052-windows-amd64.tar.gz'
|
||||
Uris = @('https://github.com/encounter/dawn-build/releases/download/v20260603.191052/dawn-windows-amd64.tar.gz')
|
||||
Uris = @('https://github.com/theofficialgman/dawn-build/releases/download/v20260603.191052/dawn-windows-amd64.tar.gz')
|
||||
Pins = @(@{ File = $auroraCMake; Text = 'set(AURORA_DAWN_VERSION "v20260603.191052"' },
|
||||
@{ File = $auroraDawn; Text = 'SHA256=7785373d569b3b0237918ec9c523239f7d0667857c5ea8242e3cdfde95e6aeab' })
|
||||
@{ File = $auroraDawn; Text = 'SHA256=13be9cff8b9b179c42dcd16aeabb6effcc8f0dfdcc14463eda2a5caeda225142' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'fmt'; File = 'fmt-11.1.4.tar.gz'
|
||||
@@ -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.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' })
|
||||
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' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'zlib'; File = 'zlib-1.3.2.tar.gz'
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# 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, and harvests the
|
||||
# archives plus a generated CMake description into launcher/artifacts/dependencies/native_prebuilt.
|
||||
# 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.
|
||||
[CmdletBinding(PositionalBinding = $false)]
|
||||
param(
|
||||
[string]$PortableToolsDirectory = 'launcher/artifacts/portable-tools',
|
||||
@@ -87,24 +88,29 @@ 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', '--parallel', "$Parallel") `
|
||||
'Building the aurora/third-party closure' -LogPrefix 'MKWNP'
|
||||
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'
|
||||
} finally {
|
||||
$env:PATH = $oldPath
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Read the description CMake wrote, plus the resolved command lines Ninja holds.
|
||||
# Read 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'
|
||||
@@ -234,6 +240,12 @@ 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.
|
||||
@@ -297,6 +309,9 @@ $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
|
||||
@@ -333,6 +348,11 @@ $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) {
|
||||
@@ -393,6 +413,9 @@ $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,9 +1,10 @@
|
||||
#!/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) and vendored Crypto++ are identical
|
||||
# (~43% of local build CPU time per Prepare-NativePrebuilt.ps1), Crypto++ and mbed TLS are identical
|
||||
# for every user under the pinned toolchain prepare-portable-tools.sh bundles, so this configures
|
||||
# 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
|
||||
# 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
|
||||
# Launcher/Prepare-NativePrebuilt.ps1, consumed by the same platform-agnostic
|
||||
# runtime/cmake/NativePrebuilt.cmake either script's package works with unmodified.
|
||||
#
|
||||
@@ -22,11 +23,17 @@ 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'
|
||||
@@ -39,8 +46,7 @@ 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 four provenance inputs (compiler_sha256, flag_fingerprint,
|
||||
aurora_fingerprint, third_party_fingerprint) as "key=value" lines and
|
||||
--print-fingerprint-only Print the provenance inputs 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.
|
||||
@@ -56,6 +62,12 @@ 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
|
||||
@@ -72,14 +84,22 @@ 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="$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"
|
||||
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}"
|
||||
runtime_source="$workspace/runtime"
|
||||
aurora_source="$workspace/aurora-main"
|
||||
|
||||
@@ -88,7 +108,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 "$toolchain_dir/bin/clang-22")
|
||||
clang_binary=$(normalize "${llvm_dir:-$toolchain_dir}/bin/clang-22")
|
||||
assert_file "$clang_binary" "Portable clang driver binary"
|
||||
|
||||
(( parallel > 0 )) || parallel=$(nproc)
|
||||
@@ -131,6 +151,8 @@ 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)
|
||||
@@ -141,6 +163,16 @@ 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
|
||||
@@ -148,19 +180,35 @@ 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
|
||||
# for the same reason as aurora above. No exclusions: unlike aurora's extern/, nothing under
|
||||
# runtime/third_party is shipped separately.
|
||||
# The harvested Crypto++ archive is consumed against this tree's headers, so it is fingerprinted.
|
||||
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 "$clang_binary")
|
||||
compiler_sha256=$(sha256_of "$cc")
|
||||
cxx_sha256=$(sha256_of "$cxx")
|
||||
# Append the CXX SHA256 if it doesn't match the hash of CC
|
||||
if [[ "${cxx_sha256}" != "${compiler_sha256}" ]]; then
|
||||
compiler_sha256="${compiler_sha256}:${cxx_sha256}"
|
||||
fi
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
@@ -184,20 +232,30 @@ 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"
|
||||
|
||||
@@ -361,6 +419,28 @@ 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.
|
||||
@@ -426,6 +506,9 @@ 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
|
||||
@@ -444,6 +527,11 @@ 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.
|
||||
@@ -505,6 +593,15 @@ 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"
|
||||
@@ -540,12 +637,13 @@ 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" <<'PY'
|
||||
"$sdl3_target" "$harvested_count" "$mbedtls_fingerprint" \
|
||||
"${runtime_archive_ref:+$meta_runtime_fingerprint}" "${runtime_archive_ref:+$(get_meta runtime_cpu_profile)}" <<'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) = sys.argv[1:]
|
||||
harvested_count, mbedtls_fingerprint, runtime_fingerprint, runtime_cpu_profile) = sys.argv[1:]
|
||||
|
||||
contents = []
|
||||
for root, dirs, files in os.walk(output_dir):
|
||||
@@ -560,9 +658,21 @@ 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": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
||||
"BuiltUtc": built_utc,
|
||||
"CompilerSha256": compiler_sha256,
|
||||
"CompilerVersion": compiler_version,
|
||||
"FlagFingerprint": flag_fingerprint,
|
||||
@@ -570,10 +680,14 @@ provenance = {
|
||||
"DawnRuntimeSha256": dawn_runtime_sha256,
|
||||
"AuroraSourceFingerprint": aurora_fingerprint,
|
||||
"ThirdPartySourceFingerprint": third_party_fingerprint,
|
||||
"MbedTlsFingerprint": mbedtls_fingerprint,
|
||||
"Sdl3Target": sdl3_target,
|
||||
"HarvestedLibraryCount": int(harvested_count),
|
||||
"Contents": contents,
|
||||
}
|
||||
if runtime_fingerprint:
|
||||
provenance["RuntimeSourceFingerprint"] = runtime_fingerprint
|
||||
provenance["RuntimeCpuProfile"] = runtime_cpu_profile
|
||||
with open(os.path.join(output_dir, "provenance.json"), "w", encoding="utf-8") as fh:
|
||||
json.dump(provenance, fh, indent=2)
|
||||
fh.write("\n")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Fails the release build when a fact duplicated across the repo stops agreeing with the copy
|
||||
# that owns it (recomp.yml). Scripts read pinned facts through Get-MkwProjectPins, but three
|
||||
# consumers can't read YAML (the C++ runtime header, the C# constants, hand-written lists on
|
||||
# consumers can't read YAML (the C++ runtime header, the C# constants, shell scripts, and hand-written lists on
|
||||
# both sides of the C#/PowerShell boundary), so those are checked here instead.
|
||||
[CmdletBinding()]
|
||||
param([string]$RepositoryRoot)
|
||||
@@ -58,6 +58,12 @@ $hostUri = Get-CapturedValue $retroWfcPayload 'CurrentRetroWfcPayloadUri\s*=\s*"
|
||||
if ($hostUri -cne $pins.RetroWfcPayloadUri) {
|
||||
Add-Failure "InputValidation.CurrentRetroWfcPayloadUri is '$hostUri' but recomp.yml pins '$($pins.RetroWfcPayloadUri)'."
|
||||
}
|
||||
$macosSetup = Read-SourceFile (Join-Path $launcher 'macos\setup.command') 'macOS setup.command'
|
||||
$macosUri = Get-CapturedValue $macosSetup "'([^']*/api/wfc/payload\?g=RMCPD00)'" `
|
||||
'The macOS Retro-WFC endpoint'
|
||||
if ($macosUri -cne $pins.RetroWfcPayloadUri) {
|
||||
Add-Failure "macOS setup.command downloads '$macosUri' but recomp.yml pins '$($pins.RetroWfcPayloadUri)'."
|
||||
}
|
||||
|
||||
# --- The game identity: the manifest carries it, but the host also compiles a fallback for a
|
||||
# --- manifest that predates the field, and that fallback decides which disc is accepted.
|
||||
@@ -121,7 +127,7 @@ foreach ($mandatory in ([regex]::Matches($parameterBlock,
|
||||
}
|
||||
|
||||
# --- The build steps the installer's progress bar acts on.
|
||||
$installProgress = Read-SourceFile (Join-Path $setup 'InstallProgress.cs') 'InstallProgress.cs'
|
||||
$installProgress = Read-SourceFile (Join-Path $common '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+'([^']+)'") |
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# Build the real Windows runtime with translated, entirely synthetic PowerPC code.
|
||||
# No game dump, game symbol map, REL, mod download, or existing generated/ output is used.
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$PortableToolsDirectory = 'Launcher/artifacts/portable-tools',
|
||||
[string]$DependencySourceDirectory = 'Launcher/artifacts/dependencies',
|
||||
[string]$StageDirectory = 'build/recomp-test',
|
||||
[ValidateRange(1, 64)] [int]$Parallel = 3
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 3.0
|
||||
. (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1')
|
||||
|
||||
$repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
||||
function Full([string]$Path) {
|
||||
if ([IO.Path]::IsPathRooted($Path)) { return [IO.Path]::GetFullPath($Path) }
|
||||
return [IO.Path]::GetFullPath((Join-Path $repoRoot $Path))
|
||||
}
|
||||
$portableTools = Full $PortableToolsDirectory
|
||||
$dependencies = Full $DependencySourceDirectory
|
||||
$stage = Full $StageDirectory
|
||||
$dotnet = (Get-Command dotnet -CommandType Application).Source
|
||||
$cmake = Join-Path $portableTools 'CMake/bin/cmake.exe'
|
||||
$ninja = Join-Path $portableTools 'Ninja/ninja.exe'
|
||||
$compilerBin = Join-Path $portableTools 'llvm-mingw/bin'
|
||||
Assert-File $cmake 'Pinned CMake (run Prepare-PortableTools.ps1 first)'
|
||||
Assert-File $ninja 'Pinned Ninja'
|
||||
Assert-File (Join-Path $dependencies 'cppwinrt/winrt/base.h') 'Pinned dependencies (run Prepare-Dependencies.ps1 first)'
|
||||
|
||||
# Refuse reuse so a developer's game translation or an earlier build cannot make
|
||||
# the test pass. Keep the staging tree after the run for diagnostics.
|
||||
if (Test-Path -LiteralPath $stage) { throw "Test stage already exists; choose a fresh -StageDirectory: $stage" }
|
||||
[IO.Directory]::CreateDirectory($stage) | Out-Null
|
||||
Write-Host "Synthetic recompilation workspace: $stage"
|
||||
|
||||
# Copy current sources, including uncommitted edits, but no ignored build output.
|
||||
# An isolated workspace preserves the developer's real generated/ directory.
|
||||
$sourceFiles = & git -C $repoRoot -c core.quotepath=false ls-files --cached --others --exclude-standard -- runtime aurora-main
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not enumerate runtime and Aurora sources.' }
|
||||
foreach ($relative in $sourceFiles | Sort-Object -Unique) {
|
||||
$destination = Join-Path $stage $relative
|
||||
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination)) | Out-Null
|
||||
Copy-Item -LiteralPath (Join-Path $repoRoot $relative) -Destination $destination
|
||||
}
|
||||
|
||||
# One synthetic text section: li r3,40; addi r3,r3,2; nop; blr.
|
||||
# Native HLE wrappers also call these eight guest symbols directly. Give each
|
||||
# its own generated blr function so the real product can link without game code.
|
||||
# Keep this list explicit: a new unresolved guest dependency must fail the test.
|
||||
[uint32]$entry = 0x80001000L
|
||||
[uint32[]]$guestCallbacks = @(
|
||||
0x8012B830L, 0x801A0620L, 0x801A1ED8L, 0x801A961CL,
|
||||
0x801AADE0L, 0x801D8D30L, 0x801D9E94L, 0x8055531CL
|
||||
)
|
||||
$textSize = [int]($guestCallbacks[-1] - $entry + 4)
|
||||
$dataOffset = 0x100 + $textSize
|
||||
$dol = [byte[]]::new($dataOffset + 4)
|
||||
function Write-BigEndian32([int]$Offset, [uint32]$Value) {
|
||||
$dol[$Offset] = [byte](($Value -shr 24) -band 255)
|
||||
$dol[$Offset + 1] = [byte](($Value -shr 16) -band 255)
|
||||
$dol[$Offset + 2] = [byte](($Value -shr 8) -band 255)
|
||||
$dol[$Offset + 3] = [byte]($Value -band 255)
|
||||
}
|
||||
Write-BigEndian32 0x00 0x100 # text[0] file offset
|
||||
Write-BigEndian32 0x48 $entry # text[0] guest address
|
||||
Write-BigEndian32 0x90 $textSize # text[0] length (unreachable gaps are zero)
|
||||
Write-BigEndian32 0x1C $dataOffset # data[0] file offset
|
||||
Write-BigEndian32 0x64 0x80600000L # data[0] guest address
|
||||
Write-BigEndian32 0xAC 4 # data[0] length
|
||||
Write-BigEndian32 0xD8 0x80601000L # BSS address
|
||||
Write-BigEndian32 0xDC 32 # BSS length
|
||||
Write-BigEndian32 0xE0 $entry # entry point
|
||||
Write-BigEndian32 0x100 0x38600028 # li r3,40
|
||||
Write-BigEndian32 0x104 0x38630002 # addi r3,r3,2
|
||||
Write-BigEndian32 0x108 0x60000000 # nop
|
||||
Write-BigEndian32 0x10C 0x4E800020 # blr
|
||||
foreach ($address in $guestCallbacks) {
|
||||
Write-BigEndian32 ([int](0x100 + $address - $entry)) 0x4E800020
|
||||
}
|
||||
Write-BigEndian32 $dataOffset 0x12345678
|
||||
[IO.File]::WriteAllBytes((Join-Path $stage 'synthetic.dol'), $dol)
|
||||
$entryPoints = (@($entry) + $guestCallbacks | ForEach-Object { '0x{0:X8}' -f $_ }) -join ', '
|
||||
$functionMap = (@($entry) + $guestCallbacks | ForEach-Object { '{0:X8} func_{0:X8}' -f $_ }) -join "`n"
|
||||
[IO.File]::WriteAllText((Join-Path $stage 'synthetic-functions.txt'), $functionMap)
|
||||
$manifest = Join-Path $stage 'recomp.yml'
|
||||
[IO.File]::WriteAllText($manifest, @"
|
||||
schema_version: 1
|
||||
workspace_root: .
|
||||
project:
|
||||
id: ci-synthetic-dol
|
||||
display_name: CI Synthetic DOL
|
||||
memory:
|
||||
base: 0x80000000
|
||||
size: 0x01800000
|
||||
sda_base: 0x80600000
|
||||
sda2_base: 0x80600000
|
||||
inputs:
|
||||
dol:
|
||||
path: synthetic.dol
|
||||
translation:
|
||||
entry_points: [$entryPoints]
|
||||
function_map:
|
||||
path: synthetic-functions.txt
|
||||
allow_unsupported_instructions: false
|
||||
runtime:
|
||||
native_abi_directories: []
|
||||
native_registration_root: runtime/src
|
||||
output:
|
||||
root: generated
|
||||
"@)
|
||||
|
||||
$translatorProject = Join-Path $repoRoot 'translator/src/Translator.Cli/Translator.Cli.csproj'
|
||||
Invoke-Checked $dotnet @('build', $translatorProject, '-c', 'Release', '--disable-build-servers') 'Building the translator'
|
||||
$translator = Join-Path $repoRoot 'translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll'
|
||||
$metadata = Join-Path $stage 'generated/base_translation_output.json'
|
||||
Invoke-Checked $dotnet @($translator, 'translate-recursive', '0x80001000', '--project', $manifest,
|
||||
'--output-metadata', $metadata, '--threads', "$Parallel") `
|
||||
'Translating the synthetic DOL'
|
||||
# Function-map seeds can be skipped by discovery; do not accept a partial fixture.
|
||||
$translated = Get-Content -LiteralPath $metadata -Raw | ConvertFrom-Json
|
||||
foreach ($address in @($entry) + $guestCallbacks) {
|
||||
if ($address -notin $translated.functions.entryPoint) {
|
||||
throw ('Synthetic function 0x{0:X8} was not translated.' -f $address)
|
||||
}
|
||||
}
|
||||
Invoke-Checked $dotnet @($translator, 'generate-data-init', '--project', $manifest) 'Generating synthetic data and runtime configuration'
|
||||
Invoke-Checked $dotnet @($translator, 'emit-build-shards', '--project', $manifest) 'Emitting the production build graph'
|
||||
|
||||
$nativeBuild = Join-Path $stage 'native-build'
|
||||
$oldPath = $env:PATH
|
||||
try {
|
||||
$env:PATH = Get-MkwToolchainPath $portableTools
|
||||
$configure = Get-MkwNativeConfigureArguments -SourceDirectory (Join-Path $stage 'runtime') -BuildDirectory $nativeBuild `
|
||||
-Ninja $ninja -CCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang.exe') `
|
||||
-CxxCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang++.exe') `
|
||||
-ResourceCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-windres.exe') `
|
||||
-DependenciesDirectory $dependencies -AdditionalArguments @('-DMKW_BUILD_PRODUCTS=ON')
|
||||
Invoke-Checked $cmake $configure 'Configuring the production Windows runtime' `
|
||||
-WaitForProcessTree $false
|
||||
Invoke-Checked $cmake @('--build', $nativeBuild, '--target', 'WiiCompiled', '--parallel', "$Parallel") `
|
||||
'Compiling and linking the synthetic product with the full runtime' `
|
||||
-WaitForProcessTree $false
|
||||
Assert-File (Join-Path $nativeBuild 'WiiCompiled.exe') 'Linked synthetic product'
|
||||
} finally {
|
||||
$env:PATH = $oldPath
|
||||
}
|
||||
Write-Host 'Synthetic recompilation passed (translation, data generation, runtime compilation, and product link).'
|
||||
@@ -6,7 +6,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace>
|
||||
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
|
||||
<Version>0.2.22</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>
|
||||
|
||||
+39
-39
@@ -1,35 +1,19 @@
|
||||
using System.Text.Json;
|
||||
|
||||
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";
|
||||
}
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
/// <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
|
||||
public 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>
|
||||
internal sealed class DelegatingInstallReporter : IInstallReporter
|
||||
public sealed class DelegatingInstallReporter : IInstallReporter
|
||||
{
|
||||
private readonly Action<string> _sink;
|
||||
|
||||
@@ -43,15 +27,20 @@ internal 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>
|
||||
internal sealed class NdjsonInstallReporter : IInstallReporter
|
||||
public 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(Action<string>? log = null) => _log = log;
|
||||
public NdjsonInstallReporter(string version, Action<string>? log = null)
|
||||
{
|
||||
_version = version;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
public void Progress(string stage, string message, int percent)
|
||||
{
|
||||
@@ -78,7 +67,7 @@ internal sealed class NdjsonInstallReporter : IInstallReporter
|
||||
{
|
||||
if (_finished) return;
|
||||
_finished = true;
|
||||
WriteLine(new { type = "result", success = true, version = ProductInfo.Version, installDir = installDirectory });
|
||||
WriteLine(new { type = "result", success = true, version = _version, installDir = installDirectory });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,8 +95,8 @@ internal sealed class NdjsonInstallReporter : IInstallReporter
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Plain-text console reporting for a silent install without <c>--progress-json</c>.</summary>
|
||||
internal sealed class ConsoleInstallReporter : IInstallReporter
|
||||
/// <summary>Plain-text console reporting for a run without <c>--progress-json</c>.</summary>
|
||||
public sealed class ConsoleInstallReporter : IInstallReporter
|
||||
{
|
||||
private readonly Action<string>? _log;
|
||||
|
||||
@@ -126,10 +115,10 @@ internal sealed class ConsoleInstallReporter : IInstallReporter
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Build step identifiers from the bundled script's <c>MKWCBUILD:STEP:<id></c> lines. The
|
||||
/// <summary>Build step identifiers from the build scripts' <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>
|
||||
internal static class BuildStepIds
|
||||
public static class BuildStepIds
|
||||
{
|
||||
public const string ReuseBaseTranslation = "reuse-base-translation";
|
||||
public const string RetranslateBase = "retranslate-base";
|
||||
@@ -143,16 +132,16 @@ internal static class BuildStepIds
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal sealed class BuildProgressWindow
|
||||
public 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>
|
||||
/// <summary>The fraction the compile step reaches; beyond it, Ninja's counter drives progress.</summary>
|
||||
private const double CompileFraction = 0.58;
|
||||
|
||||
private static readonly (string Id, double Fraction, string Message)[] Steps =
|
||||
@@ -164,7 +153,7 @@ internal 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 bundled compiler"),
|
||||
(BuildStepIds.ConfigureNative, 0.52, "Configuring the compiler"),
|
||||
(BuildStepIds.Compile, CompileFraction, "Compiling the game. This is the longest step"),
|
||||
];
|
||||
|
||||
@@ -209,19 +198,30 @@ internal sealed class BuildProgressWindow
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Anything else - an unknown step identifier, a plain MKWCBUILD note, or raw tool output -
|
||||
// stays a diagnostic.
|
||||
_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)
|
||||
// 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))
|
||||
{
|
||||
_fraction = Math.Min(0.97, _fraction + 0.0015);
|
||||
_fraction = Math.Max(_fraction, CompileFraction + (0.97 - CompileFraction) * done / total);
|
||||
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);
|
||||
@@ -24,7 +24,7 @@ public static class RetroWfcPayload
|
||||
private static readonly TimeSpan RetroWfcDownloadTimeout = TimeSpan.FromSeconds(30);
|
||||
private static readonly TimeSpan RetroWfcRetryDelay = TimeSpan.FromSeconds(1);
|
||||
|
||||
public const string CurrentRetroWfcPayloadUri = "http://nas.play.rwfc.net/payload?g=RMCPD00";
|
||||
public const string CurrentRetroWfcPayloadUri = "https://rwfc.net/api/wfc/payload?g=RMCPD00";
|
||||
private static readonly string RetroWfcOfflinePayloadFile =
|
||||
Path.Combine("binary", "payload.RMCPD00.bin");
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using System.Text;
|
||||
using Tomlyn;
|
||||
using Tomlyn.Parsing;
|
||||
using Tomlyn.Syntax;
|
||||
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
@@ -17,12 +20,6 @@ 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),
|
||||
@@ -75,9 +72,11 @@ public static class RuntimeConfiguration
|
||||
public static void RemoveDvdRootIfOwned(string configPath, string dvdRoot) =>
|
||||
RemovePathIfOwned(configPath, "dvd_root", dvdRoot);
|
||||
|
||||
/// <summary>The raw stored text of a <c>[paths]</c> key, exactly as the file holds it.</summary>
|
||||
/// <summary>The stored string of a <c>[paths]</c> key, exactly as the file holds it.</summary>
|
||||
public static string? GetPath(string configPath, string key) =>
|
||||
TryUnquoteToml(GetRawValue(configPath, "paths", key) ?? "", out var value) ? value : null;
|
||||
TryLoad(configPath) is { } document && Find(document, ["paths"], key)?.Item.Value is StringValueSyntax value
|
||||
? value.Value!
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
/// A <c>[paths]</c> value as the runtime will actually use it. The runtime resolves every relative
|
||||
@@ -99,9 +98,19 @@ public static class RuntimeConfiguration
|
||||
|
||||
private static void SetPath(string configPath, string key, string value)
|
||||
{
|
||||
var lines = ReadLinesOrDefault(configPath);
|
||||
SetSectionValue(lines, "paths", key, QuoteToml(FormatPathValue(configPath, value)));
|
||||
WriteLines(configPath, lines);
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -123,176 +132,104 @@ public static class RuntimeConfiguration
|
||||
|
||||
private static void RemovePathIfOwned(string configPath, string key, string expectedValue)
|
||||
{
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
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)
|
||||
{
|
||||
if (!File.Exists(configPath)) return null;
|
||||
var header = $"[{section}]";
|
||||
var inSection = false;
|
||||
foreach (var line in File.ReadLines(configPath))
|
||||
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 trimmed = RemoveComment(line).Trim();
|
||||
if (IsSection(trimmed))
|
||||
KeyValueSyntax? previous = null;
|
||||
foreach (var item in items)
|
||||
{
|
||||
inSection = trimmed.Equals(header, KeyComparison);
|
||||
continue;
|
||||
if (prefix.Concat(Parts(item.Key!)).SequenceEqual(wanted))
|
||||
return new Entry(item, items, table, previous);
|
||||
previous = item;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
private static void Append(DocumentSyntax document, string[] section, KeyValueSyntax item)
|
||||
{
|
||||
var header = $"[{section}]";
|
||||
var sectionStart = -1;
|
||||
var sectionEnd = lines.Count;
|
||||
for (var index = 0; index < lines.Count; index++)
|
||||
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 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;
|
||||
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);
|
||||
}
|
||||
|
||||
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}");
|
||||
// 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);
|
||||
}
|
||||
|
||||
private static List<string> ReadLinesOrDefault(string configPath) =>
|
||||
File.Exists(configPath) ? File.ReadAllLines(configPath).ToList() : DefaultConfigLines();
|
||||
private static SyntaxToken NewLine(string text) => new(TokenKind.NewLine, text);
|
||||
|
||||
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)
|
||||
private static SyntaxToken LastToken(SyntaxNode 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;
|
||||
while (node is not SyntaxToken)
|
||||
node = node.GetChild(node.ChildrenCount - 1)!;
|
||||
return (SyntaxToken)node;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
|
||||
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
|
||||
<Version>0.2.22</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>
|
||||
|
||||
@@ -13,7 +13,8 @@ internal static class BuildRunner
|
||||
string workspace, string profile, string outputDir, string? baseOutputDir,
|
||||
string? retroDir, string? retroWfcOfflineDir, bool skipRetroWfcPayload,
|
||||
bool forceCleanBuild, string? translatorBin, string? ccBin, string? cxxBin, string? fuseLd,
|
||||
string? cmakeBin, string? ninjaBin, string? nativePrebuiltDir, IInstallReporter reporter,
|
||||
string? cmakeBin, string? ninjaBin, string? nativePrebuiltDir, string? sysroot,
|
||||
IInstallReporter reporter,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var script = Path.Combine(workspace, "Launcher", "local-build.sh");
|
||||
@@ -77,6 +78,11 @@ internal static class BuildRunner
|
||||
{
|
||||
startInfo.ArgumentList.Add("--native-prebuilt-dir"); startInfo.ArgumentList.Add(nativePrebuiltDir);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(sysroot))
|
||||
{
|
||||
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,6 +15,12 @@ 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`
|
||||
@@ -35,6 +41,8 @@ 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);
|
||||
|
||||
@@ -61,6 +69,7 @@ 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);
|
||||
}
|
||||
|
||||
@@ -86,6 +95,36 @@ 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);
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
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));
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
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,9 +1,13 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
internal static class ProductInfo
|
||||
{
|
||||
public const string Name = "WiiCompiled";
|
||||
public const string Version = "0.2.22";
|
||||
public static readonly string Version =
|
||||
typeof(ProductInfo).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!
|
||||
.InformationalVersion;
|
||||
}
|
||||
|
||||
/// <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() : new ConsoleInstallReporter();
|
||||
IInstallReporter reporter = progressJson ? new NdjsonInstallReporter(ProductInfo.Version) : new ConsoleInstallReporter();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -127,22 +127,52 @@ 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, "Preparing the Retro-WFC payload", 1);
|
||||
reporter.Progress(InstallStages.Validate,
|
||||
"Downloading the current Retro-WFC payload", 1);
|
||||
try
|
||||
{
|
||||
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(cacheDir);
|
||||
}
|
||||
catch (InvalidDataException)
|
||||
{
|
||||
// A valid signature authenticates a payload, but does not prove it is the latest
|
||||
// signed revision. Always ask the fixed endpoint for the current snapshot; the
|
||||
// downloader verifies it before atomically replacing the cache.
|
||||
await RetroWfcPayload.DownloadRetroWfcPayloadAsync(
|
||||
RetroWfcPayload.CurrentRetroWfcPayloadUri, cacheDir, token);
|
||||
}
|
||||
catch (Exception downloadFailure) when (!token.IsCancellationRequested &&
|
||||
downloadFailure is HttpRequestException or TimeoutException
|
||||
or IOException)
|
||||
{
|
||||
// Offline installs may continue with a previously authenticated snapshot. Do not
|
||||
// use this path for a newly downloaded payload that failed signature validation:
|
||||
// that must remain a hard failure instead of hiding possible endpoint tampering.
|
||||
try
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
var sysroot = flags.GetValueOrDefault("sysroot");
|
||||
// --sysroot explicitly provided (even as bare flag at end of argv, which ParseArgs
|
||||
// stores as null) must carry a path; omitting --sysroot entirely is fine (local-build.sh
|
||||
// adds -UCMAKE_SYSROOT to clear any stale cached value from a prior configure).
|
||||
if (flags.ContainsKey("sysroot") && string.IsNullOrWhiteSpace(sysroot))
|
||||
{
|
||||
throw new ArgumentException("--sysroot requires a non-empty directory path.");
|
||||
}
|
||||
|
||||
await BuildRunner.RunAsync(
|
||||
workspace, profile, installDir, baseInstallDir,
|
||||
retroDir,
|
||||
@@ -156,6 +186,7 @@ internal static class Program
|
||||
flags.GetValueOrDefault("cmake"),
|
||||
flags.GetValueOrDefault("ninja"),
|
||||
flags.GetValueOrDefault("native-prebuilt-dir"),
|
||||
sysroot,
|
||||
reporter, token);
|
||||
|
||||
reporter.Progress(InstallStages.Shortcuts, "Creating shortcuts", 98);
|
||||
@@ -324,7 +355,7 @@ internal static class Program
|
||||
{--download-retro-wfc-payload | --skip-retro-wfc-payload}]
|
||||
[--force-clean-build] [--translator-bin PATH] [--disc-tool-bin PATH]
|
||||
[--cc PATH] [--cxx PATH] [--fuse-ld NAME_OR_PATH] [--cmake PATH] [--ninja PATH]
|
||||
[--native-prebuilt-dir DIR] [--progress-json] [--workspace DIR]
|
||||
[--native-prebuilt-dir DIR] [--sysroot PATH] [--progress-json] [--workspace DIR]
|
||||
uninstall
|
||||
launch-base
|
||||
launch-retro
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>WiiCompiled.Setup.Linux</AssemblyName>
|
||||
<RootNamespace>WiiCompiled.Setup.Linux</RootNamespace>
|
||||
<Version>0.2.22</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Command-line installer and launcher for WiiCompiled on Linux</Description>
|
||||
@@ -16,5 +15,6 @@
|
||||
</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() : null;
|
||||
var reporter = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version) : 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(Log) : null;
|
||||
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version, 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() : null;
|
||||
var reporter = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version) : 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(Log) : null;
|
||||
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(ProductInfo.Version, Log) : null;
|
||||
IInstallReporter reporter = ndjson is null ? new ConsoleInstallReporter(Log) : ndjson;
|
||||
var installDirectory = Path.GetFullPath(command.InstallDirectory!);
|
||||
var installation = new Installation(installDirectory);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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";
|
||||
}
|
||||
@@ -22,6 +22,9 @@ internal sealed class InstallerEngine
|
||||
var existing = new Installation(installDirectory);
|
||||
|
||||
PortableInstallHealing.HealMovedInstall(existing, _reporter);
|
||||
// Capture this before publishing anything. An existing installation may be repaired or
|
||||
// updated by this invocation, but those operations must not recreate the user's shortcuts.
|
||||
var firstInstall = !existing.IsPresent;
|
||||
var previousState = existing.ReadInstallState();
|
||||
using var scratch = Directory.Exists(installDirectory)
|
||||
? InstallScratchSpace.CreateInsideInstall(installDirectory, _reporter)
|
||||
@@ -139,7 +142,7 @@ internal sealed class InstallerEngine
|
||||
updatedState.RetroRewindInstalled, candidateRuntimeAssetsFingerprint,
|
||||
remainingCancellation);
|
||||
Publish(staging, installDirectory, canonicalRetroRoot, updatedState,
|
||||
releaseEntries, remainingCancellation);
|
||||
releaseEntries, remainingCancellation, createShortcuts: firstInstall);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,7 +164,8 @@ internal sealed class InstallerEngine
|
||||
|
||||
await PublishToolkitAndReconcileProductsAsync(existing, staging, workspace, manifest,
|
||||
previousState, options, canonicalRetroRoot, retroCompileInputs,
|
||||
publishGameAssets: reusableGameAssets is null, cancellationToken);
|
||||
publishGameAssets: reusableGameAssets is null, createShortcuts: firstInstall,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -186,7 +190,7 @@ internal sealed class InstallerEngine
|
||||
string stagedWorkspace, PayloadManifest manifest, InstallState? previousState,
|
||||
InstallOptions options, string? canonicalRetroRoot,
|
||||
RetroRewindCompileInputs? retroCompileInputs,
|
||||
bool publishGameAssets, CancellationToken cancellationToken)
|
||||
bool publishGameAssets, bool createShortcuts, CancellationToken cancellationToken)
|
||||
{
|
||||
var installDirectory = existing.Root;
|
||||
|
||||
@@ -214,7 +218,8 @@ internal sealed class InstallerEngine
|
||||
if (publishGameAssets) AddComponent(entries, staging, installDirectory, "GameAssets");
|
||||
|
||||
Publish(staging, installDirectory, canonicalRetroRoot, state,
|
||||
entries, cancellationToken, progressPercent: 8, completionPercent: 10);
|
||||
entries, cancellationToken, progressPercent: 8, completionPercent: 10,
|
||||
createShortcuts: createShortcuts);
|
||||
|
||||
_reporter.Progress(InstallStages.BuildBase,
|
||||
"Producing the installed products with the published toolkit...", 11);
|
||||
@@ -277,7 +282,7 @@ internal sealed class InstallerEngine
|
||||
private void Publish(string staging, string installDirectory,
|
||||
string? canonicalRetroRoot, InstallState state,
|
||||
List<InstallTransactionEntry> entries, CancellationToken cancellationToken,
|
||||
int progressPercent = 95, int completionPercent = 99)
|
||||
int progressPercent = 95, int completionPercent = 99, bool createShortcuts = false)
|
||||
{
|
||||
entries.Add(InstallTransactionEntry.Directory(Path.Combine(staging, "licenses"),
|
||||
Path.Combine(installDirectory, "licenses")));
|
||||
@@ -322,7 +327,8 @@ internal sealed class InstallerEngine
|
||||
{
|
||||
ShellIntegration.RegisterUninstaller(installDirectory, state.RetroRewindInstalled);
|
||||
}
|
||||
ShellIntegration.CreateShortcuts(installDirectory);
|
||||
if (createShortcuts)
|
||||
ShellIntegration.CreateShortcuts(installDirectory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -495,17 +501,31 @@ 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");
|
||||
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 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 is RMCP01 but does not match the supported clean PAL revision. " +
|
||||
"Patched or otherwise modified game code cannot be installed safely.");
|
||||
"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)
|
||||
})
|
||||
{
|
||||
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) + ". " +
|
||||
"Patched or otherwise modified game code cannot be installed safely.");
|
||||
}
|
||||
|
||||
private static void ValidateInstallDirectory(string path) =>
|
||||
|
||||
@@ -110,8 +110,7 @@ internal sealed class ProductRepairService
|
||||
InputValidation.CurrentRetroWfcPayloadUri, payloadScratch, cancellationToken);
|
||||
}
|
||||
catch (Exception ex) when (!cancellationToken.IsCancellationRequested &&
|
||||
ex is HttpRequestException or IOException or InvalidDataException
|
||||
or InvalidOperationException or OperationCanceledException)
|
||||
ex is HttpRequestException or TimeoutException or IOException)
|
||||
{
|
||||
payloadSnapshot = RecoverInstalledRetroWfcPayload(toolkitFingerprint,
|
||||
Path.Combine(scratchRoot, "retro-wfc-payload-recovered"), ex, cancellationToken);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
internal static class Program
|
||||
@@ -30,7 +31,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().Failure(ex.Message);
|
||||
if (progressJson) new NdjsonInstallReporter(ProductInfo.Version).Failure(ex.Message);
|
||||
else Console.Error.WriteLine(ex.Message);
|
||||
return 1;
|
||||
}
|
||||
@@ -90,7 +91,7 @@ internal static class Program
|
||||
// errors raised outside it.
|
||||
if (progressJson)
|
||||
{
|
||||
new NdjsonInstallReporter().Failure(ex.Message);
|
||||
new NdjsonInstallReporter(ProductInfo.Version).Failure(ex.Message);
|
||||
Console.Error.WriteLine(ex);
|
||||
}
|
||||
else
|
||||
@@ -121,7 +122,9 @@ internal static class PlatformChecks
|
||||
internal static class ProductInfo
|
||||
{
|
||||
public const string Name = "WiiCompiled";
|
||||
public const string Version = "0.2.26";
|
||||
public static readonly string Version =
|
||||
typeof(ProductInfo).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!
|
||||
.InformationalVersion;
|
||||
|
||||
/// <summary>
|
||||
/// The setup executable is copied into the installation under this name. It is the launcher and
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Security.Cryptography;
|
||||
using Tomlyn;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
@@ -51,6 +52,7 @@ 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);
|
||||
@@ -229,6 +231,70 @@ 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.
|
||||
@@ -1073,10 +1139,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 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.
|
||||
// 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.
|
||||
var components = ToolkitFingerprint.ComputeComponents(root);
|
||||
if (components.Compile != afterPatch)
|
||||
throw new Exception("ComputeComponents disagrees with the compile identity.");
|
||||
@@ -1087,15 +1153,36 @@ 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.");
|
||||
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "runtime", "src", "main.cpp"),
|
||||
"int main3();");
|
||||
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\"");
|
||||
var afterNewRuntime = ToolkitFingerprint.ComputeComponents(root);
|
||||
if (afterNewRuntime.Translation == afterNewTranslator.Translation)
|
||||
throw new Exception("A runtime source change did not change the translation identity.");
|
||||
if (afterNewRuntime.NativeToolchain != afterNewTranslator.NativeToolchain)
|
||||
if (afterNewRuntime.Translation != indexed.Translation)
|
||||
throw new Exception("A runtime source change with unchanged registrations changed the translation identity.");
|
||||
if (afterNewRuntime.NativeToolchain != indexed.NativeToolchain)
|
||||
throw new Exception("A runtime source change needlessly changed the native toolchain identity.");
|
||||
if (afterNewRuntime.Compile == afterNewTranslator.Compile)
|
||||
if (afterNewRuntime.Compile == indexed.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);
|
||||
@@ -1148,6 +1235,26 @@ 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,5 +1,7 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
@@ -12,9 +14,10 @@ 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.
|
||||
// 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.
|
||||
private const string Version = "mkwc-toolkit-compile-v6";
|
||||
private const string TranslationVersion = "mkwc-toolkit-translation-v2";
|
||||
private const string TranslationVersion = "mkwc-toolkit-translation-v3";
|
||||
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";
|
||||
@@ -25,20 +28,25 @@ internal static class ToolkitFingerprint
|
||||
".cmake", ".txt", ".yml", ".yaml", ".toml", ".json", ".ps1", ".in", ".def", ".patch"];
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private static readonly string[] TranslationPrefixes =
|
||||
[
|
||||
InstalledLayout.ToolkitEntryPrefix + "Translator/",
|
||||
InstalledLayout.WorkspaceEntryPrefix + "projects/",
|
||||
InstalledLayout.WorkspaceEntryPrefix + "runtime/src/"
|
||||
InstalledLayout.WorkspaceEntryPrefix + "projects/"
|
||||
];
|
||||
|
||||
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
|
||||
@@ -108,14 +116,16 @@ 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),
|
||||
BuildSubsetIdentity(TranslationVersion, entries, TranslationPrefixes, TranslationFiles),
|
||||
BuildSubsetIdentity(NativeToolchainVersion, entries, NativeToolchainPrefixes,
|
||||
NativeToolchainFiles));
|
||||
BuildIdentity(TranslationVersion, translation),
|
||||
BuildIdentity(NativeToolchainVersion,
|
||||
SelectSubset(entries, NativeToolchainPrefixes, NativeToolchainFiles)));
|
||||
}
|
||||
|
||||
private static string BuildSubsetIdentity(string version, SortedDictionary<string, string> entries,
|
||||
private static SortedDictionary<string, string> SelectSubset(SortedDictionary<string, string> entries,
|
||||
string[] prefixes, string[] files)
|
||||
{
|
||||
var subset = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||
@@ -125,9 +135,61 @@ internal static class ToolkitFingerprint
|
||||
files.Any(file => relative.Equals(file, StringComparison.Ordinal)))
|
||||
subset[relative] = hash;
|
||||
}
|
||||
return BuildIdentity(version, subset);
|
||||
return 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
|
||||
@@ -273,8 +335,7 @@ 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 Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(builder.ToString())))
|
||||
.ToLowerInvariant();
|
||||
return Sha256Hex(builder.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<AssemblyName>WiiCompiled.Setup</AssemblyName>
|
||||
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Version>0.2.26</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Command-line installer and launcher for WiiCompiled</Description>
|
||||
@@ -16,5 +15,6 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
|
||||
<Using Include="WiiCompiled.Setup.Common" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
+33
-11
@@ -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). 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.
|
||||
# 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.
|
||||
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,6 +148,9 @@ 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
|
||||
@@ -173,7 +176,29 @@ 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" "$appdir/workspace/Launcher/local-build.sh"
|
||||
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 \
|
||||
--native-source-dir "$appdir/workspace/runtime/src" \
|
||||
--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
|
||||
|
||||
# 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
|
||||
@@ -203,12 +228,9 @@ 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
|
||||
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"
|
||||
# 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
|
||||
cp "$HERE/workspace/.bundle-version" "$CACHE/.bundle-version"
|
||||
fi
|
||||
# toolchain/ and native-prebuilt/ are NOT copied into the cache (they're large - ~500 MiB /
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# 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,11 +4,12 @@ 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'
|
||||
@@ -22,17 +23,20 @@ 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 native-build-macos caches
|
||||
--force-clean-build Delete local generated and current-architecture native build 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 ;;
|
||||
@@ -50,14 +54,21 @@ 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'
|
||||
[[ $(uname -m) == arm64 ]] || fail 'the current macOS product target is Apple Silicon only'
|
||||
macos_arch=$(uname -m)
|
||||
case "$macos_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $macos_arch" ;; esac
|
||||
macos_deployment_target=12.0
|
||||
workspace=$(cd "$workspace" && pwd)
|
||||
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
|
||||
@@ -73,7 +84,8 @@ for tool in "$cmake_bin" "$ninja_bin" clang clang++ shasum; do command -v "$tool
|
||||
|
||||
project="$workspace/projects/mkwii/recomp.yml"; assets="$workspace/Assets"; generated="$workspace/generated"
|
||||
functions="$generated/functions"; metadata="$generated/base_translation_output.json"; manifest_dir="$workspace/build/base"
|
||||
manifest="$manifest_dir/mkwii_base_manifest.json"; shards="$generated/build_shards"; native_build="$workspace/native-build-macos"
|
||||
manifest="$manifest_dir/mkwii_base_manifest.json"; shards="$generated/build_shards"
|
||||
native_build="$workspace/native-build-macos-$macos_arch"
|
||||
assert_file "$project" 'translation project'
|
||||
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'
|
||||
@@ -115,11 +127,30 @@ if (( builds_retro )); then
|
||||
fi
|
||||
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
|
||||
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
|
||||
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
|
||||
@@ -130,10 +161,19 @@ 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'
|
||||
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs"
|
||||
# Use Aurora's pinned SDL3 source on macOS. A system SDL3 can be older than
|
||||
# Aurora's required API even when find_package() succeeds.
|
||||
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DCMAKE_OSX_ARCHITECTURES="$macos_arch" -DCMAKE_OSX_DEPLOYMENT_TARGET="$macos_deployment_target" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs" -DAURORA_SDL3_PROVIDER=vendor -DMKW_SETUP_VERSION="$setup_version"
|
||||
mkw_write_identity "$toolchain_provenance" ToolchainFingerprint "$toolchain_fingerprint"
|
||||
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}"; fi
|
||||
if (( builds_retro )); then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product RetroRewind --output-dir "$output_dir"; fi
|
||||
if [[ "$profile" != retro-rewind ]]; then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product WiiCompiled --output-dir "${base_output_dir:-$output_dir}" --architecture "$macos_arch" --minimum-system-version "$macos_deployment_target"; fi
|
||||
if (( builds_retro )); then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product RetroRewind --output-dir "$output_dir" --architecture "$macos_arch" --minimum-system-version "$macos_deployment_target"; fi
|
||||
printf 'MKWCBUILD:OUTPUT=%s\n' "$output_dir"
|
||||
|
||||
+73
-16
@@ -39,7 +39,7 @@ require_command() {
|
||||
}
|
||||
|
||||
sha256_of() {
|
||||
sha256sum "$1" | awk '{print $1}'
|
||||
mkw_sha256 "$1"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -47,6 +47,8 @@ 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=""
|
||||
@@ -65,6 +67,8 @@ translator_dll_override=""
|
||||
translator_bin_override=""
|
||||
fuse_ld_override=""
|
||||
native_prebuilt_dir=""
|
||||
sysroot=""
|
||||
setup_version=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
@@ -87,6 +91,10 @@ Usage: local-build.sh --output-dir DIR [options]
|
||||
--translator-bin PATH Self-contained Translator.Cli executable (skips building AND needs no dotnet at all)
|
||||
--native-prebuilt-dir DIR Precompiled aurora/third-party package (see Prepare-NativePrebuilt.sh);
|
||||
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
|
||||
}
|
||||
|
||||
@@ -110,11 +118,18 @@ while [[ $# -gt 0 ]]; do
|
||||
--translator-dll) translator_dll_override=$2; shift 2 ;;
|
||||
--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) ;;
|
||||
@@ -186,6 +201,30 @@ assert_file "$project" "Translation project"
|
||||
assert_file "$assets/main.dol" "Extracted main.dol (see translator/README.md - owning the game is required)"
|
||||
assert_file "$assets/StaticR.rel" "Extracted StaticR.rel (see translator/README.md - owning the game is required)"
|
||||
|
||||
# The AppImage bundles Clang, but Linux startup objects and the C/C++ link runtimes
|
||||
# still come from the host. Check them before the expensive translation so a missing
|
||||
# development package produces a useful error instead of CMake's generic exit 1.
|
||||
link_probe_dir=$(mktemp -d)
|
||||
link_probe_flags=()
|
||||
[[ -z "$sysroot" ]] || link_probe_flags+=(--sysroot="$sysroot")
|
||||
[[ -z "$fuse_ld_override" ]] || link_probe_flags+=(-fuse-ld="$fuse_ld_override")
|
||||
printf 'int main(void) { return 0; }\n' > "$link_probe_dir/probe.c"
|
||||
cat > "$link_probe_dir/probe.cpp" <<'EOF'
|
||||
#include <vector>
|
||||
int main() { std::vector<int> values{1}; return values.front() - 1; }
|
||||
EOF
|
||||
if ! "$cc_bin" "${link_probe_flags[@]}" "$link_probe_dir/probe.c" -o "$link_probe_dir/probe-c" > "$link_probe_dir/error" 2>&1; then
|
||||
cat "$link_probe_dir/error" >&2
|
||||
rm -rf "$link_probe_dir"
|
||||
fail "The C compiler cannot link a test program. Linux needs C development files (glibc startup objects and a compiler runtime) in addition to bundled Clang. Install your distribution's development packages, or on SteamOS run WiiCompiled through the Wheel Wizard Flatpak."
|
||||
fi
|
||||
if ! "$cxx_bin" "${link_probe_flags[@]}" "$link_probe_dir/probe.cpp" -o "$link_probe_dir/probe-cxx" > "$link_probe_dir/error" 2>&1; then
|
||||
cat "$link_probe_dir/error" >&2
|
||||
rm -rf "$link_probe_dir"
|
||||
fail "The C++ compiler cannot link a test program. Install your distribution's C++ development packages, or on SteamOS run WiiCompiled through the Wheel Wizard Flatpak."
|
||||
fi
|
||||
rm -rf "$link_probe_dir"
|
||||
|
||||
# Literal line matching against the manifest's fixed shape, not a YAML dependency - the same
|
||||
# approach NativeBuildFlags.ps1's Get-MkwProjectPins uses on Windows, kept here only for the one
|
||||
# field this script actually needs from the manifest.
|
||||
@@ -238,10 +277,9 @@ else
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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.
|
||||
# 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.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if (( force_clean_build )); then
|
||||
@@ -249,13 +287,12 @@ if (( force_clean_build )); then
|
||||
rm -rf "$generated" "$base_manifest_dir" "$build"
|
||||
fi
|
||||
|
||||
translation_fingerprint=$(cat "$assets/main.dol" "$assets/StaticR.rel" "$project" | sha256sum | awk '{print $1}')
|
||||
translator_identity_input=${translator_bin:-$(dirname "$translator_dll")}
|
||||
translation_fingerprint=$(mkw_translation_identity "$workspace" "$translator_identity_input" "$script_dir/local-build.sh")
|
||||
reuse_base=0
|
||||
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
|
||||
if [[ "$(mkw_read_identity "$translation_provenance" TranslationFingerprint)" == "$translation_fingerprint" \
|
||||
&& -f "$base_metadata" && -f "$base_manifest" ]]; then
|
||||
reuse_base=1
|
||||
fi
|
||||
|
||||
if (( builds_retro )); then
|
||||
@@ -303,8 +340,7 @@ else
|
||||
translator emit-base-manifest --project "$project" --out "$base_manifest_dir" \
|
||||
--functions-dir "$functions" --translation-output-metadata "$base_metadata" --region P
|
||||
|
||||
printf '{"SchemaVersion":1,"TranslationFingerprint":"%s"}' "$translation_fingerprint" \
|
||||
> "$translation_provenance"
|
||||
mkw_write_identity "$translation_provenance" TranslationFingerprint "$translation_fingerprint"
|
||||
fi
|
||||
|
||||
if (( builds_retro )); then
|
||||
@@ -379,6 +415,13 @@ 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"
|
||||
@@ -390,7 +433,8 @@ 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_TRANSLATED_COMPILE_JOBS="$translated_jobs"
|
||||
-DMKW_SETUP_VERSION="$setup_version")
|
||||
# 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
|
||||
@@ -409,14 +453,25 @@ if [[ "$cc_bin" == */* ]]; then
|
||||
configure_args+=(-DCMAKE_ASM_COMPILER="$cc_bin")
|
||||
fi
|
||||
if [[ -n "$fuse_ld_override" ]]; then
|
||||
configure_args+=(-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=$fuse_ld_override")
|
||||
# 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")
|
||||
fi
|
||||
if [[ -n "$native_prebuilt_dir" ]]; then
|
||||
configure_args+=(-DMKW_NATIVE_PREBUILT_DIR="$native_prebuilt_dir")
|
||||
fi
|
||||
if [[ -n "$sysroot" ]]; then
|
||||
configure_args+=(-DCMAKE_SYSROOT="$sysroot")
|
||||
else
|
||||
# Explicitly clear any cached CMAKE_SYSROOT from a prior configure so an
|
||||
# incremental build that transitions from one sysroot to none does not
|
||||
# silently keep the stale cached path.
|
||||
configure_args+=(-UCMAKE_SYSROOT)
|
||||
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) ;;
|
||||
@@ -445,7 +500,9 @@ publish_built_product() {
|
||||
local exe=$build/$target
|
||||
assert_file "$exe" "Locally compiled game executable"
|
||||
cp -f "$exe" "$destination/$target"
|
||||
for name in dsp_coef.bin initial_pipeline_cache.db; do
|
||||
# cacert.pem is the TLS root bundle the mbed TLS path looks up beside the executable
|
||||
# (runtime/src/hle/net/network_ssl.cpp); without it HTTPS fails at runtime.
|
||||
for name in dsp_coef.bin initial_pipeline_cache.db cacert.pem; do
|
||||
[[ -f "$build/$name" ]] && cp -f "$build/$name" "$destination/"
|
||||
done
|
||||
[[ -d "$build/wii_bootstrap" ]] && cp -rf "$build/wii_bootstrap" "$destination/"
|
||||
|
||||
@@ -9,45 +9,97 @@ 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 PATH --translator PATH --cmake-root DIR --ninja PATH --output PKG [options]
|
||||
Usage: build-setup-pkg.command --nodtool-arm64 PATH --nodtool-x86_64 PATH --translator-arm64 PATH --translator-x86_64 PATH --cmake-root DIR --ninja-arm64 PATH --ninja-x86_64 PATH --output PKG [options]
|
||||
|
||||
Creates a game-code-free WiiCompiled Setup.pkg. The supplied tools must be
|
||||
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.
|
||||
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.
|
||||
|
||||
--workspace DIR Repository root (default: script's grandparent)
|
||||
--version VERSION Bundle/package version (default: 0.1.0)
|
||||
--version VERSION Bundle/package version (default: project version, or 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=""; translator=""; cmake_root=""; ninja=""; output=""; version=0.1.0; identity=""
|
||||
workspace=$(cd "$script_dir/../.." && pwd); nodtool_arm64=""; nodtool_x86_64=""; translator_arm64=""; translator_x86_64=""; cmake_root=""; ninja_arm64=""; ninja_x86_64=""; output=""; version=0.1.0; identity=""
|
||||
version_supplied=false
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--workspace) workspace=${2:-}; shift 2 ;;
|
||||
--nodtool) nodtool=${2:-}; shift 2 ;;
|
||||
--translator) translator=${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 ;;
|
||||
--cmake-root) cmake_root=${2:-}; shift 2 ;;
|
||||
--ninja) ninja=${2:-}; shift 2 ;;
|
||||
--ninja-arm64) ninja_arm64=${2:-}; shift 2 ;;
|
||||
--ninja-x86_64) ninja_x86_64=${2:-}; shift 2 ;;
|
||||
--output) output=${2:-}; shift 2 ;;
|
||||
--version) version=${2:-}; shift 2 ;;
|
||||
--version) version=${2:-}; version_supplied=true; 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; 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'
|
||||
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
|
||||
[[ -x "$cmake_root/bin/cmake" ]] || fail '--cmake-root must contain bin/cmake'
|
||||
[[ -x "$ninja" ]] || fail '--ninja must name an executable'
|
||||
"$nodtool" --version >/dev/null || fail '--nodtool did not run successfully'
|
||||
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
|
||||
workspace=$(cd "$workspace" && pwd); output=$(cd "$(dirname "$output")" && pwd)/$(basename "$output")
|
||||
stage=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-pkg.XXXXXX")
|
||||
trap 'rm -rf "$stage"' EXIT
|
||||
@@ -64,7 +116,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>14.0</string>
|
||||
<key>LSMinimumSystemVersion</key><string>12.0</string>
|
||||
</dict></plist>
|
||||
EOF
|
||||
cat > "$app/Contents/MacOS/WiiCompiledSetup" <<'EOF'
|
||||
@@ -98,16 +150,30 @@ 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 --native-source-dir "$resources/workspace/runtime/src" \
|
||||
--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
|
||||
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"
|
||||
# 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"
|
||||
copy_clean "$cmake_root" "$resources/tools/cmake"
|
||||
copy_clean "$ninja" "$resources/tools/ninja"; chmod +x "$resources/tools/ninja"
|
||||
copy_clean "$ninja_arm64" "$resources/tools/arm64/ninja"; chmod +x "$resources/tools/arm64/ninja"
|
||||
copy_clean "$ninja_x86_64" "$resources/tools/x86_64/ninja"; chmod +x "$resources/tools/x86_64/ninja"
|
||||
copy_clean "$workspace/LICENSE" "$resources/LICENSE"
|
||||
copy_clean "$workspace/THIRD-PARTY-NOTICES.md" "$resources/THIRD-PARTY-NOTICES.md"
|
||||
codesign --force --deep --sign - "$app"
|
||||
|
||||
@@ -38,6 +38,16 @@ 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
|
||||
@@ -51,8 +61,10 @@ 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'
|
||||
[[ $(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'
|
||||
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)"
|
||||
|
||||
data_root=$(dirname "$(dirname "$dol")")
|
||||
[[ -d "$data_root/files" ]] || fail 'nodtool extraction did not contain the Wii files directory'
|
||||
@@ -64,15 +76,17 @@ 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/main.dol" || -e "$assets_dir/StaticR.rel" ]]; then
|
||||
if [[ -e "$assets_dir/DATA" || -e "$assets_dir/.data-manifest" || -e "$assets_dir/main.dol" || -e "$assets_dir/StaticR.rel" ]]; then
|
||||
mkdir -p "$backup"
|
||||
for item in DATA main.dol StaticR.rel; do
|
||||
for item in DATA .data-manifest 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"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Cross-compile a thin x86_64 macOS build from an Apple Silicon Mac.
|
||||
# Pass this file on the first configure with:
|
||||
# -DCMAKE_TOOLCHAIN_FILE=/absolute/path/to/macos-x86_64-toolchain.cmake
|
||||
#
|
||||
# CMAKE_SYSTEM_PROCESSOR is deliberately declared here rather than inferred
|
||||
# from CMAKE_OSX_ARCHITECTURES, so target-aware CMake dependencies select their
|
||||
# x86_64 artifacts.
|
||||
set(CMAKE_SYSTEM_NAME Darwin)
|
||||
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||
set(CMAKE_OSX_ARCHITECTURES x86_64 CACHE STRING
|
||||
"Target macOS architectures" FORCE)
|
||||
@@ -5,29 +5,37 @@ 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
|
||||
Usage: publish-app.command --build-dir DIR --product {WiiCompiled|RetroRewind} --output-dir DIR [options]
|
||||
|
||||
Copies a locally built product and its runtime assets into OUTPUT-DIR/<product>.app.
|
||||
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=""
|
||||
build_dir=""; product=""; output_dir=""; architecture=$(uname -m); minimum_system_version=12.0
|
||||
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'
|
||||
for tool in codesign ditto install_name_tool otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
|
||||
[[ "$architecture" == arm64 || "$architecture" == x86_64 ]] || fail '--architecture must be arm64 or x86_64'
|
||||
[[ "$minimum_system_version" =~ ^[0-9]+(\.[0-9]+){1,2}$ ]] || fail '--minimum-system-version must contain two or three period-separated integers'
|
||||
for tool in codesign ditto install_name_tool lipo otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
|
||||
[[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product"
|
||||
for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done
|
||||
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"
|
||||
macos="$app/Contents/MacOS"
|
||||
@@ -47,35 +55,106 @@ 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>14.0</string>
|
||||
<key>LSMinimumSystemVersion</key><string>$minimum_system_version</string>
|
||||
<key>NSHighResolutionCapable</key><true/>
|
||||
</dict></plist>
|
||||
EOF
|
||||
ditto "$build_dir/$product" "$macos/$product"
|
||||
for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do
|
||||
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do
|
||||
ditto "$build_dir/$asset" "$resources/$asset"
|
||||
ln -s "../Resources/$asset" "$macos/$asset"
|
||||
done
|
||||
|
||||
# Build a closure of Homebrew dylibs. System libraries remain system references.
|
||||
queue=("$macos/$product")
|
||||
# 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=("")
|
||||
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
|
||||
[[ "$dependency" == /opt/homebrew/* || "$dependency" == /usr/local/* ]] || continue
|
||||
[[ -f "$dependency" ]] || continue
|
||||
name=$(basename "$dependency")
|
||||
[[ -z "$dependency" ]] && continue
|
||||
[[ -n "$self_id" && "$dependency" == "$self_id" ]] && continue
|
||||
case "$dependency" in
|
||||
/System/Library/*|/usr/lib/*) continue ;;
|
||||
esac
|
||||
dep_path=$(dependency_path "$current" "$dependency" "$current_rpaths") || fail "unresolved non-system dependency: '$dependency' needed by '$current'"
|
||||
name=$(basename "$dep_path")
|
||||
if [[ ! -f "$frameworks/$name" ]]; then
|
||||
ditto "$dependency" "$frameworks/$name"
|
||||
ditto "$dep_path" "$frameworks/$name"
|
||||
install_name_tool -id "@rpath/$name" "$frameworks/$name"
|
||||
queue+=("$frameworks/$name")
|
||||
queue+=("$dep_path")
|
||||
queue_rpaths+=("$current_rpaths")
|
||||
fi
|
||||
done < <(otool -L "$current" | tail -n +2 | awk '{print $1}')
|
||||
done < <(otool -L "$current" | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]+\(compatibility version .*/\1/p')
|
||||
done
|
||||
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
|
||||
@@ -83,7 +162,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 | awk '{print $1}')
|
||||
done < <(otool -L "$binary" | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]+\(compatibility version .*/\1/p')
|
||||
done < <(find "$frameworks" -type f -print; printf '%s\n' "$macos/$product")
|
||||
|
||||
find "$frameworks" -type f -exec codesign --force --sign - {} +
|
||||
|
||||
@@ -5,16 +5,19 @@ 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 -e "display dialog \"${1//\"/\\\"}\" buttons {\"OK\"} default button \"OK\" with icon caution" >/dev/null; }
|
||||
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
|
||||
}
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: setup.command --game IMAGE [--retro-dir DIR] [--install-location {user|applications}]
|
||||
@@ -36,6 +39,13 @@ 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"
|
||||
@@ -60,12 +70,34 @@ 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"
|
||||
if [[ ! -d "$workspace/.git" && ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then
|
||||
source_bundle_version="$workspace_source/.bundle-version"
|
||||
workspace_bundle_version="$workspace/.bundle-version"
|
||||
needs_workspace_refresh=0
|
||||
if [[ ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then
|
||||
needs_workspace_refresh=1
|
||||
elif [[ -f "$source_bundle_version" ]] && [[ ! -f "$workspace_bundle_version" || "$(<"$source_bundle_version")" != "$(<"$workspace_bundle_version")" ]]; then
|
||||
needs_workspace_refresh=1
|
||||
fi
|
||||
|
||||
if (( needs_workspace_refresh )); then
|
||||
printf 'Preparing the local build workspace...\n'
|
||||
rm -rf "$workspace"
|
||||
/usr/bin/ditto "$workspace_source" "$workspace"
|
||||
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
|
||||
fi
|
||||
|
||||
profile=base
|
||||
@@ -77,33 +109,47 @@ 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"
|
||||
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"
|
||||
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
|
||||
fi
|
||||
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" \
|
||||
'http://nas.play.rwfc.net/payload?g=RMCPD00' || fail 'could not download the Retro-WFC payload needed for online play'
|
||||
|
||||
# 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
|
||||
"$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"
|
||||
@@ -129,8 +175,13 @@ install_app() {
|
||||
local app=$1
|
||||
[[ -d "$products/$app" ]] || return 0
|
||||
if [[ "$destination" == /Applications ]]; then
|
||||
command="mkdir -p /Applications && rm -rf '/Applications/$app' && ditto '$products/$app' '/Applications/$app'"
|
||||
/usr/bin/osascript -e "do shell script \"$command\" with administrator privileges"
|
||||
/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
|
||||
else
|
||||
mkdir -p "$destination"; rm -rf "$destination/$app"; /usr/bin/ditto "$products/$app" "$destination/$app"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# Exercise a real dyld dependency chain before and after app packaging.
|
||||
set -euo pipefail
|
||||
|
||||
[[ $(uname -s) == Darwin ]] || { printf 'This test requires macOS.\n' >&2; exit 1; }
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
temp_root=$(cd "${TMPDIR:-/tmp}" && pwd)
|
||||
test_root=$(mktemp -d "$temp_root/wiicompiled-publish-test.XXXXXX")
|
||||
[[ "$test_root" == "$temp_root"/wiicompiled-publish-test.* ]] || exit 1
|
||||
trap 'rm -rf "$test_root"' EXIT
|
||||
|
||||
build_dir="$test_root/build with spaces"
|
||||
output_dir="$test_root/output with spaces"
|
||||
mkdir -p "$build_dir/A/b" "$build_dir/A/c" "$build_dir/wii_bootstrap" "$output_dir"
|
||||
cat > "$test_root/c.c" <<'EOF'
|
||||
int value_c(void) { return 7; }
|
||||
EOF
|
||||
cat > "$test_root/b.c" <<'EOF'
|
||||
extern int value_c(void);
|
||||
int value_b(void) { return 2 * value_c(); }
|
||||
EOF
|
||||
cat > "$test_root/a.c" <<'EOF'
|
||||
extern int value_b(void);
|
||||
int value_a(void) { return 1 + value_b(); }
|
||||
EOF
|
||||
cat > "$test_root/main.c" <<'EOF'
|
||||
#include <stdio.h>
|
||||
extern int value_a(void);
|
||||
int main(void) { printf("%d\n", value_a()); return 0; }
|
||||
EOF
|
||||
|
||||
clang -dynamiclib "$test_root/c.c" -o "$build_dir/A/c/libC.dylib" \
|
||||
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libC.dylib
|
||||
clang -dynamiclib "$test_root/b.c" -o "$build_dir/A/b/libB.dylib" \
|
||||
-L "$build_dir/A/c" -lC \
|
||||
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libB.dylib
|
||||
clang -dynamiclib "$test_root/a.c" -o "$build_dir/A/libA.dylib" \
|
||||
-L "$build_dir/A/b" -lB \
|
||||
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libA.dylib \
|
||||
-Wl,-rpath,@loader_path/b -Wl,-rpath,@loader_path/c
|
||||
clang "$test_root/main.c" -o "$build_dir/WiiCompiled" \
|
||||
-L "$build_dir/A" -lA \
|
||||
-Wl,-headerpad_max_install_names -Wl,-rpath,@executable_path/A
|
||||
|
||||
# B has no rpaths: its C dependency must inherit A's loader-relative path.
|
||||
[[ $("$build_dir/WiiCompiled") == 15 ]]
|
||||
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem; do
|
||||
: > "$build_dir/$asset"
|
||||
done
|
||||
bash "$script_dir/publish-app.command" --build-dir "$build_dir" \
|
||||
--product WiiCompiled --output-dir "$output_dir" --architecture "$(uname -m)"
|
||||
for name in libA.dylib libB.dylib libC.dylib; do
|
||||
[[ -f "$output_dir/WiiCompiled.app/Contents/Frameworks/$name" ]]
|
||||
done
|
||||
|
||||
# Remove access to the original paths and relocate the app before executing.
|
||||
mv "$build_dir" "$test_root/hidden build"
|
||||
mkdir "$test_root/relocated app"
|
||||
mv "$output_dir/WiiCompiled.app" "$test_root/relocated app/WiiCompiled.app"
|
||||
[[ $("$test_root/relocated app/WiiCompiled.app/Contents/MacOS/WiiCompiled") == 15 ]]
|
||||
printf 'publish-app dependency-chain test passed\n'
|
||||
@@ -52,13 +52,13 @@ done
|
||||
|
||||
case "$arch" in
|
||||
x86_64) llvm_release_arch=X64; target_triple=x86_64-unknown-linux-gnu
|
||||
llvm_release_sha256=df0e1ecf16caf3489a272a5eea4eec9b0d82878f6477fa309504f918a0006384
|
||||
llvm_release_sha256=fccecb1906e7ddf5ec040aec5b646b650e2daaafa4423b41341c4717db5bdec0
|
||||
cmake_release_arch=x86_64
|
||||
cmake_sha256=927b2368a946c37269c3a66225ab00544e756459cdd0b5d0da438694fb9ff802
|
||||
ninja_asset=ninja-linux.zip
|
||||
ninja_sha256=5749cbc4e668273514150a80e387a957f933c6ed3f5f11e03fb30955e2bbead6 ;;
|
||||
aarch64) llvm_release_arch=ARM64; target_triple=aarch64-unknown-linux-gnu
|
||||
llvm_release_sha256=805efad2bb91cb4967fa569e0881d10c0f69c04461cf671cccbae19f547acc34
|
||||
llvm_release_sha256=d431eff9f064c86ee7c4c94af570a8f74fcccd1f74c6f0da3af32ce34a1e1b05
|
||||
cmake_release_arch=aarch64
|
||||
cmake_sha256=9ea38356dbd3e32e51029a3e09a0f2f8e117ef4fbcaad7a21ffb36409bbd5cb4
|
||||
ninja_asset=ninja-linux-aarch64.zip
|
||||
@@ -101,11 +101,14 @@ rm -rf "$work"
|
||||
mkdir -p "$work/bin" "$work/lib/$target_triple" "$work/include/$target_triple/c++/v1"
|
||||
|
||||
# --- clang/lld/llvm-ar, pruned from the official LLVM release ---
|
||||
|
||||
llvm_archive_name="LLVM-$llvm_version-Linux-$llvm_release_arch.tar.xz"
|
||||
# built from PR https://github.com/llvm/llvm-project/pull/222821 on official LLVM Github Actions Runner
|
||||
# only switch to an official stable LLVM release again once:
|
||||
# - this PR has merged https://github.com/llvm/llvm-project/pull/221365 and been backported to LLVM stable branch
|
||||
# - this bug has been fixed with a workaround in the Wiicompiled translator https://github.com/patchzyy/Wiicompiled/issues/208 or in LLVM and been backported to LLVM stable branch
|
||||
llvm_archive_name="LLVM-PR222821-5ae1c7c43a11b4cdc5ce4dd483c28357bab7dae2-Linux-$llvm_release_arch.tar.xz"
|
||||
llvm_archive="$downloads/$llvm_archive_name"
|
||||
download_verified "$llvm_archive" \
|
||||
"https://github.com/llvm/llvm-project/releases/download/llvmorg-$llvm_version/$llvm_archive_name" \
|
||||
"https://github.com/theofficialgman/llvm-project/releases/download/llvmorg-22.1.8-patched/$llvm_archive_name" \
|
||||
"$llvm_release_sha256"
|
||||
|
||||
extract_root="$script_dir/artifacts/.extract-clang-$arch"
|
||||
@@ -113,7 +116,7 @@ rm -rf "$extract_root"
|
||||
mkdir -p "$extract_root"
|
||||
echo "prepare-portable-tools.sh: extracting $llvm_archive_name (this is the full ~1.9 GiB release; only a fraction is kept)..."
|
||||
tar -xf "$llvm_archive" -C "$extract_root"
|
||||
src="$extract_root/LLVM-$llvm_version-Linux-$llvm_release_arch"
|
||||
src="$extract_root/${llvm_archive_name%.tar.xz}"
|
||||
[[ -d "$src" ]] || { echo "prepare-portable-tools.sh: unexpected archive layout, expected $src" >&2; exit 1; }
|
||||
|
||||
echo "prepare-portable-tools.sh: pruning to the minimal compile+link toolchain..."
|
||||
@@ -165,7 +168,8 @@ rm -rf "$cmake_extract_root"
|
||||
mkdir -p "$cmake_extract_root"
|
||||
echo "prepare-portable-tools.sh: extracting $cmake_archive_name..."
|
||||
tar -xzf "$cmake_archive" -C "$cmake_extract_root"
|
||||
cmake_src="$cmake_extract_root/cmake-$cmake_version-linux-$cmake_release_arch"
|
||||
|
||||
cmake_src="$cmake_extract_root/${cmake_archive_name%.tar.gz}"
|
||||
[[ -d "$cmake_src" ]] || { echo "prepare-portable-tools.sh: unexpected archive layout, expected $cmake_src" >&2; exit 1; }
|
||||
|
||||
mkdir -p "$work/share/cmake-$cmake_share_version"
|
||||
@@ -201,10 +205,10 @@ Ninja $ninja_version
|
||||
Apache License 2.0
|
||||
EOF
|
||||
|
||||
echo "prepare-portable-tools.sh: smoke-testing the toolchain..."
|
||||
smoke_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$smoke_dir"' EXIT
|
||||
cat > "$smoke_dir/t.cpp" <<'EOF'
|
||||
echo "prepare-portable-tools.sh: testing the toolchain..."
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
cat > "$test_dir/t.cpp" <<'EOF'
|
||||
#include <vector>
|
||||
#include <cstdio>
|
||||
int main() {
|
||||
@@ -214,24 +218,25 @@ int main() {
|
||||
return sum == 6 ? 0 : 1;
|
||||
}
|
||||
EOF
|
||||
"$work/bin/clang++" -std=c++20 -fuse-ld=lld "$smoke_dir/t.cpp" -o "$smoke_dir/t"
|
||||
"$smoke_dir/t"
|
||||
"$work/bin/clang++" -std=c++20 -fuse-ld=lld "$test_dir/t.cpp" -o "$test_dir/t"
|
||||
"$test_dir/t"
|
||||
|
||||
# Also exercised together through CMake+Ninja, exactly how local-build.sh drives them - a plain
|
||||
# clang++ invocation above would not catch a broken CMAKE_ROOT (Modules/Templates) or a Ninja that
|
||||
# can't find the compiler.
|
||||
cat > "$smoke_dir/CMakeLists.txt" <<'EOF'
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(smoke CXX)
|
||||
add_executable(smoke t.cpp)
|
||||
cat > "$test_dir/CMakeLists.txt" <<'EOF'
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
project(test CXX)
|
||||
add_executable(test t.cpp)
|
||||
EOF
|
||||
"$work/bin/cmake" -S "$smoke_dir" -B "$smoke_dir/build" -G Ninja \
|
||||
"$work/bin/cmake" -S "$test_dir" -B "$test_dir/build" -G Ninja \
|
||||
-DCMAKE_MAKE_PROGRAM="$work/bin/ninja" -DCMAKE_CXX_COMPILER="$work/bin/clang++" >/dev/null
|
||||
"$work/bin/cmake" --build "$smoke_dir/build" >/dev/null
|
||||
"$smoke_dir/build/smoke"
|
||||
"$work/bin/cmake" --build "$test_dir/build" >/dev/null
|
||||
"$test_dir/build/test"
|
||||
|
||||
rm -rf "$smoke_dir"
|
||||
rm -rf "$test_dir"
|
||||
trap - EXIT
|
||||
|
||||
rm -rf "$toolchain_dir"
|
||||
mv "$work" "$toolchain_dir"
|
||||
echo "prepare-portable-tools.sh: toolchain ready at $toolchain_dir ($(du -sh "$toolchain_dir" | cut -f1))"
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
<img width="4190" height="1232" alt="wiicomplogofinalfinalfinalev2MADEBY_INKWRECK_plzcredit" src="https://github.com/user-attachments/assets/df7a3f2e-5336-479a-b4c0-968dd578726d" />
|
||||
|
||||
# WiiCompiled
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/patchzyy/Wiicompiled/releases"><img alt="Windows 10 / 11, x64" src="https://img.shields.io/badge/Windows-10%20%2F%2011%20%C2%B7%20x64-0078D4"></a>
|
||||
<a href="https://github.com/patchzyy/Wiicompiled/releases"><img alt="Linux, x64 / ARM64" src="https://img.shields.io/badge/Linux-x64%20%2F%20ARM64-FCC624?logo=linux&logoColor=white"></a>
|
||||
<a href="https://github.com/patchzyy/Wiicompiled/releases"><img alt="macOS 14+, Apple Silicon" src="https://img.shields.io/badge/macOS-14%2B%20%C2%B7%20Apple%20Silicon-0A84FF?logo=apple&logoColor=white"></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<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>
|
||||
|
||||
A native PC port of Mario Kart Wii, made with static recompilation.
|
||||
|
||||
There's no emulator in the loop, no interpreter, no JIT, no PowerPC
|
||||
@@ -39,42 +53,37 @@ 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. All audio that shows in your display media controls on your windows pc fall under this.
|
||||
rather it didn't. Windows uses system media controls and Linux uses MPRIS players.
|
||||
On macOS 14.2 or later, this detects other apps with active audio output and excludes
|
||||
the game's own audio. Apps that keep an output stream running silently can keep
|
||||
game music muted even when nothing is audible.
|
||||
|
||||
**An in-game settings bar.**
|
||||
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
|
||||
- Full per-controller button mapping, including the bumpers
|
||||
- Dolphin-syntax input expressions and GCPadNew.ini import
|
||||
- Controller vibration on/off
|
||||
- Volume, instant mute, and the music ducking toggle
|
||||
|
||||
Everything you change is saved to `Config.toml` on the spot and restored next launch.
|
||||
|
||||
**Real controller support.**
|
||||
Controllers are fed to the game as a GameCube controller.
|
||||
Mappings are positional (`south`, `east`, `west`, `north`) rather than Xbox-labelled, so the
|
||||
same config makes sense on Xbox, PlayStation, Nintendo and generic SDL pads alike, and extra
|
||||
inputs like paddles, touchpads and share buttons show up when the hardware reports them.
|
||||
**Dolphin-compatible input expressions.**
|
||||
Each GameCube control can carry an expression in Dolphin's input syntax, with the same operators
|
||||
and the same functions.
|
||||
A Dolphin `GCPadNew.ini` can be imported directly from the F10 bar.
|
||||
|
||||
**Vibration toggle.**
|
||||
Force feedback can be turned off for every port at once.
|
||||
The official Wii U / Switch GameCube adapter (WUP-028) works too; as with Dolphin, on Windows the
|
||||
adapter must be switched to the WinUSB driver once (Zadig).
|
||||
|
||||
**Real Wii Remotes over Bluetooth.**
|
||||
Pair a Wii Remote with Windows (Settings > Bluetooth > Add device, press 1+2 or SYNC, leave the
|
||||
PIN empty) and the game reads it as an actual Wii Remote through KPAD: Wii Remote icons and
|
||||
prompts, Wii Wheel tilt steering, wheelies and tricks all come from the game's own motion code.
|
||||
Nunchuk and Classic Controller are real Wii extensions too: the game gets the Nunchuk's stick,
|
||||
C/Z and accelerometer, and the Classic Controller through `KPADGetUnifiedWpadStatus` with its own
|
||||
layout and icons, so its buttons do what the game says they do and no mapping is involved. Plug an
|
||||
extension in or pull it out mid-game and the game switches control scheme like on the console
|
||||
(the runtime patches SDL's Wii driver, which otherwise loses the remote for good on an extension
|
||||
change). Only the Wii U Pro Controller, which has no Wii-era equivalent, is fed to the game as a
|
||||
GameCube pad with Nintendo's layout. If a remote drops out or was switched on after launch, the
|
||||
runtime keeps rescanning Bluetooth until it comes back (F10 > Controller settings > Wii Remotes). SDL's read of
|
||||
the remote's factory accelerometer calibration often times out over Bluetooth (`console.log`
|
||||
then says "Using fallback accelerometer calibration") and it falls back to a nominal zero point,
|
||||
so the same menu has a one-button calibration (remote flat, buttons up) that removes the small
|
||||
tilt offset some remotes show.
|
||||
PIN empty)
|
||||
|
||||
Known limitations of the Wii Remote path:
|
||||
- No IR pointer yet: menus are navigated with the D-pad and A (the game treats the remote as
|
||||
@@ -92,8 +101,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 14 (Sonoma) or later on Apple Silicon
|
||||
- On macOS, Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
|
||||
- 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)
|
||||
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
|
||||
GCZ, CISO, WBFS, WIA and RVZ are accepted.
|
||||
|
||||
@@ -114,6 +123,21 @@ image under Settings, turn on **WiiCompiled (beta)**, and hit install from the H
|
||||
Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and
|
||||
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
|
||||
@@ -153,7 +177,9 @@ The default test suite needs no binaries and no host C++ compiler, so you can ha
|
||||
translator without any game data around.
|
||||
|
||||
For everything beyond that, feeding in your own `main.dol`/`StaticR.rel`, running the
|
||||
translation, generating the manifest and build graph, and compiling. see [`translator/README.md`](translator/README.md).
|
||||
translation, generating the manifest and build graph, and compiling, see [`translator/README.md`](translator/README.md).
|
||||
|
||||
For a step-by-step guide on compiling both WiiCompiled and Retro Rewind from source on macOS (Apple Silicon), see the [macOS Build Guide](docs/building-macos.md).
|
||||
|
||||
## FAQ
|
||||
|
||||
@@ -202,7 +228,7 @@ AI coding tools were used during development of this project.
|
||||
All translated output is verified against real hardware behavior and most importantly, physics accuracy is proven synced across Wii, Dolphin, and WiiCompiled (see FAQ).
|
||||
|
||||
## Credits
|
||||
|
||||
- **inkwreck** - making the logo
|
||||
- **[aurora](https://github.com/encounter/aurora)** - the GX rendering/windowing backend this
|
||||
project's whole graphics layer sits on. MIT licensed.
|
||||
- **[Dawn](https://dawn.googlesource.com/dawn)** - Google's WebGPU implementation, powering
|
||||
|
||||
@@ -114,14 +114,16 @@ Source: <https://github.com/higan-emu/libco>. Full license text:
|
||||
|
||||
## Fetched at build time and redistributed in release builds
|
||||
|
||||
These are pinned in `aurora-main/extern/CMakeLists.txt`, `aurora-main/CMakeLists.txt` and
|
||||
`aurora-main/cmake/AuroraDawnProvider.cmake`. They are not stored in this repository; the build
|
||||
downloads them, and release installers carry the resulting binaries. Their license texts are
|
||||
These are pinned in `aurora-main/extern/CMakeLists.txt`, `aurora-main/CMakeLists.txt`,
|
||||
`aurora-main/cmake/AuroraDawnProvider.cmake`, and (for Mbed TLS) `runtime/CMakeLists.txt`. They are
|
||||
not stored in this repository; the build downloads them - each fetch is pinned to an exact version
|
||||
with a checked SHA-256 - and links or redistributes the resulting binaries. Their license texts are
|
||||
included in the installer's `licenses/` folder. The Windows installer bundles the pinned source
|
||||
trees themselves (fetched by `Launcher/Prepare-Dependencies.ps1`) so end-user builds run offline.
|
||||
|
||||
| Component | Version | License | Upstream |
|
||||
| --- | --- | --- | --- |
|
||||
| Mbed TLS | 3.6.7 | Apache-2.0 / GPL-2.0-or-later | <https://github.com/Mbed-TLS/mbedtls> |
|
||||
| Dawn (WebGPU) | `v20260603.191052` prebuilt | BSD-3-Clause | <https://dawn.googlesource.com/dawn> |
|
||||
| Tint (part of Dawn) | with Dawn | BSD-3-Clause | <https://dawn.googlesource.com/dawn> |
|
||||
| DirectXShaderCompiler (`dxcompiler.dll`) | with Dawn | NCSA / University of Illinois Open Source | <https://github.com/microsoft/DirectXShaderCompiler> |
|
||||
@@ -130,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.3 | BSD-2-Clause | <https://github.com/Cyan4973/xxHash> |
|
||||
| xxHash | 0.8.4 | BSD-2-Clause | <https://github.com/Cyan4973/xxHash> |
|
||||
| zlib | 1.3.2 | zlib | <https://github.com/madler/zlib> |
|
||||
| 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,6 +2,7 @@ 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)
|
||||
@@ -83,3 +84,10 @@ 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,6 +15,7 @@ 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 ()
|
||||
|
||||
@@ -30,6 +31,7 @@ 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)$")
|
||||
elseif (APPLE AND (CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|x86_64)$" OR CMAKE_OSX_ARCHITECTURES 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 TRUE
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(dawn)
|
||||
@@ -143,6 +143,10 @@ 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")
|
||||
@@ -151,15 +155,36 @@ elseif (_aurora_dawn_provider STREQUAL "package")
|
||||
endif ()
|
||||
endif ()
|
||||
set(AURORA_DAWN_PACKAGE_URL
|
||||
"https://github.com/encounter/dawn-build/releases/download/${AURORA_DAWN_VERSION}/dawn-${_dawn_system}-${_dawn_arch}.tar.gz")
|
||||
"https://github.com/theofficialgman/dawn-build/releases/download/${AURORA_DAWN_VERSION}/dawn-${_dawn_system}-${_dawn_arch}.tar.gz")
|
||||
|
||||
# A release asset is mutable: the same tag has already served two different windows-amd64 archives,
|
||||
# and a cached extraction is never re-verified. Pin the digest for the combinations we ship.
|
||||
if (NOT AURORA_DAWN_PACKAGE_URL_HASH
|
||||
AND AURORA_DAWN_VERSION STREQUAL "v20260603.191052"
|
||||
AND _dawn_system STREQUAL "windows" AND _dawn_arch STREQUAL "amd64")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=7785373d569b3b0237918ec9c523239f7d0667857c5ea8242e3cdfde95e6aeab")
|
||||
if (NOT AURORA_DAWN_PACKAGE_URL_HASH AND AURORA_DAWN_VERSION STREQUAL "v20260603.191052")
|
||||
if (_dawn_system STREQUAL "windows" AND _dawn_arch STREQUAL "amd64")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=13be9cff8b9b179c42dcd16aeabb6effcc8f0dfdcc14463eda2a5caeda225142")
|
||||
elseif (_dawn_system STREQUAL "windows" AND _dawn_arch STREQUAL "arm64")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=bf2d921110f14a1d6553f673c5597988e66c02af5587e4a1fee167937d247734")
|
||||
elseif (_dawn_system STREQUAL "linux" AND _dawn_arch STREQUAL "x86_64")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=7adcf241bb2a24ec0c576609f2d67203e0e65db9c5a286ca2bbb6281fa644b35")
|
||||
elseif (_dawn_system STREQUAL "linux" AND _dawn_arch STREQUAL "aarch64")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=2415e253d46f91b2d72fc73bf6055fb31b98b67c773dc546e1991b1cf019732f")
|
||||
elseif (_dawn_system STREQUAL "darwin" AND _dawn_arch STREQUAL "arm64")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=0a8ea8eb0159fc0ba1083c52155d9376fb173cffe690b400464a6ad8881bb461")
|
||||
elseif (_dawn_system STREQUAL "darwin" AND _dawn_arch STREQUAL "x86_64")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=5fe2c7a2a8b4cb82acee4af16779a83ae333c7657b9dc1a5008f5fd1f5ad5f80")
|
||||
elseif (_dawn_system STREQUAL "ios" AND _dawn_arch STREQUAL "arm64")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=f97701d26fd1f25bbcc260b4c31736ede134c730c12556029e2470fde967f424")
|
||||
elseif (_dawn_system STREQUAL "android" AND _dawn_arch STREQUAL "aarch64")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=0e63e8cbf53551f703f582d1306f4257c0380353f66b53369d96952ce6d9f934")
|
||||
endif ()
|
||||
endif ()
|
||||
endif ()
|
||||
message(STATUS "aurora: Fetching prebuilt Dawn package from ${AURORA_DAWN_PACKAGE_URL}")
|
||||
@@ -173,7 +198,7 @@ elseif (_aurora_dawn_provider STREQUAL "package")
|
||||
FetchContent_Declare(dawn_prebuilt
|
||||
URL "${AURORA_DAWN_PACKAGE_URL}"
|
||||
${_dawn_prebuilt_hash_argument}
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
)
|
||||
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 TRUE
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
)
|
||||
# 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 TRUE
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(nod_prebuilt)
|
||||
|
||||
@@ -32,6 +32,52 @@ 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.
|
||||
#
|
||||
@@ -118,6 +164,219 @@ 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,7 +41,10 @@ if (_aurora_sdl3_provider STREQUAL "auto")
|
||||
set(_aurora_sdl3_provider "package")
|
||||
else ()
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
|
||||
find_package(SDL3 QUIET)
|
||||
# Aurora uses APIs from the SDL version pinned by AURORA_SDL3_VERSION.
|
||||
# Do not silently select an older system package and fail later while
|
||||
# compiling its headers.
|
||||
find_package(SDL3 ${AURORA_SDL3_VERSION} QUIET)
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
|
||||
if (SDL3_FOUND)
|
||||
set(_aurora_sdl3_provider "system")
|
||||
@@ -58,7 +61,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 REQUIRED)
|
||||
find_package(SDL3 ${AURORA_SDL3_VERSION} REQUIRED)
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
|
||||
endif ()
|
||||
_aurora_sdl3_select_target()
|
||||
@@ -92,7 +95,7 @@ elseif (_aurora_sdl3_provider STREQUAL "package")
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(sdl3_prebuilt
|
||||
URL "${AURORA_SDL3_PACKAGE_URL}"
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
)
|
||||
FetchContent_MakeAvailable(sdl3_prebuilt)
|
||||
|
||||
@@ -145,7 +148,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 TRUE
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
PATCH_COMMAND "${CMAKE_COMMAND}" -DSDL_SOURCE_DIR=<SOURCE_DIR> -P "${_aurora_sdl3_patches}"
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
|
||||
@@ -34,6 +34,17 @@ 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 ()
|
||||
@@ -41,7 +52,9 @@ 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")
|
||||
target_link_options(aurora_core PUBLIC
|
||||
"LINKER:-weak_framework,Metal"
|
||||
"LINKER:-U,_OBJC_CLASS_$_MTLLogStateDescriptor")
|
||||
endif ()
|
||||
if (DAWN_ENABLE_D3D11)
|
||||
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_D3D11)
|
||||
|
||||
Vendored
+63
-13
@@ -16,10 +16,60 @@ function(aurora_find_package_global)
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ${_PREV_FIND_PACKAGE_TARGETS_GLOBAL})
|
||||
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)
|
||||
@@ -38,7 +88,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 TRUE
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(abseil-cpp)
|
||||
@@ -52,10 +102,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.3.tar.gz
|
||||
URL_HASH SHA256=aae608dfe8213dfd05d909a57718ef82f30722c392344583d3f39050c7f29a80
|
||||
SOURCE_SUBDIR cmake_unofficial
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
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
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
set(XXHASH_BUILD_XXHSUM OFF CACHE INTERNAL "Build the xxhsum binary")
|
||||
@@ -78,7 +128,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 TRUE
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(fmt)
|
||||
@@ -116,7 +166,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 TRUE
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(ZLIB)
|
||||
@@ -152,7 +202,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 TRUE
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(PNG)
|
||||
@@ -176,7 +226,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 TRUE
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(Freetype)
|
||||
@@ -190,7 +240,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 TRUE
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(imgui)
|
||||
@@ -235,7 +285,7 @@ if (AURORA_ENABLE_GX)
|
||||
FetchContent_Declare(sqlite3
|
||||
URL https://sqlite.org/2026/sqlite-amalgamation-3510300.zip
|
||||
URL_HASH SHA256=acb1e6f5d832484bf6d32b681e858c38add8b2acdfd42ac5df24b8afb46552b4
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(sqlite3)
|
||||
@@ -278,7 +328,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 TRUE
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
SOURCE_SUBDIR build/cmake
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
@@ -299,7 +349,7 @@ if (NOT TARGET TracyClient)
|
||||
tracy
|
||||
URL https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz
|
||||
URL_HASH SHA256=24d342b5127d7f659dc3cf94f24347b348cc736f25b17a691fd7f69541937658
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(tracy)
|
||||
|
||||
@@ -127,12 +127,20 @@ typedef struct {
|
||||
const char* pipelineCachePath;
|
||||
} AuroraConfig;
|
||||
|
||||
typedef enum {
|
||||
AURORA_INITIALIZATION_SUCCESS = 0,
|
||||
AURORA_INITIALIZATION_GRAPHICS_UNAVAILABLE = 1,
|
||||
} AuroraInitializationStatus;
|
||||
|
||||
typedef struct {
|
||||
AuroraBackend backend;
|
||||
const char* userPath;
|
||||
const char* cachePath;
|
||||
SDL_Window* window;
|
||||
AuroraWindowSize windowSize;
|
||||
AuroraInitializationStatus initializationStatus;
|
||||
// On failure, owned by SDL on the calling thread. Copy before another SDL call.
|
||||
const char* initializationError;
|
||||
} AuroraInfo;
|
||||
|
||||
AuroraInfo aurora_initialize(int argc, char* argv[], const AuroraConfig* config);
|
||||
@@ -162,6 +170,21 @@ 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,6 +88,7 @@ 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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -171,6 +171,22 @@ typedef struct PADButtonMapping {
|
||||
PADButton padButton;
|
||||
} PADButtonMapping;
|
||||
|
||||
// Explicitly disabled, unlike INVALID which permits default L/R trigger input.
|
||||
#define PAD_NATIVE_BUTTON_DISABLED 0xfffffffeu
|
||||
|
||||
// Axis-to-button bindings share the persisted nativeButton field without
|
||||
// changing the binary layout of existing controller mapping files.
|
||||
constexpr u32 PADEncodeAxisButton(u32 axis, bool negative, u32 threshold = 50) {
|
||||
return 0x10000u | axis | (negative ? 0x80u : 0u) | (threshold << 8);
|
||||
}
|
||||
constexpr bool PADIsAxisButton(u32 binding) { return (binding & 0xffff0000u) == 0x10000u; }
|
||||
constexpr u32 PADAxisButtonThreshold(u32 binding) { return (binding >> 8) & 0xffu; }
|
||||
constexpr u32 PADAxisButtonAxis(u32 binding) { return binding & 0x7fu; }
|
||||
constexpr bool PADAxisButtonNegative(u32 binding) { return (binding & 0x80u) != 0; }
|
||||
constexpr u32 PADAxisButtonIdentity(u32 binding) {
|
||||
return PADIsAxisButton(binding) ? (binding & ~0xff00u) : binding;
|
||||
}
|
||||
|
||||
typedef struct PADAxisMapping {
|
||||
PADSignedNativeAxis nativeAxis;
|
||||
s32 nativeButton;
|
||||
|
||||
+181
-26
@@ -7,6 +7,7 @@
|
||||
#include "gx/shader_info.hpp"
|
||||
#include "imgui.hpp"
|
||||
#include "webgpu/gpu.hpp"
|
||||
#include "webgpu/metalfx.hpp"
|
||||
#include <webgpu/webgpu_cpp.h>
|
||||
#endif
|
||||
|
||||
@@ -60,6 +61,9 @@ std::atomic<AuroraFrameWorkerWaitCallback> g_frameWorkerWaitCallback{nullptr};
|
||||
// deadlines derived from it, so the presenter cannot drift. Zero means present when ready.
|
||||
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");
|
||||
@@ -225,7 +229,7 @@ enum class ImGuiFramePolicy {
|
||||
bool begin_frame_impl(bool pumpEvents, ImGuiFramePolicy imguiPolicy = ImGuiFramePolicy::Immediate,
|
||||
bool* imguiNewFrameOwed = nullptr) noexcept;
|
||||
bool begin_frame_render_state_impl(ImGuiFramePolicy imguiPolicy, bool* imguiNewFrameOwed) noexcept;
|
||||
void end_frame_impl(bool pumpEvents, bool drainFifo) noexcept;
|
||||
void end_frame_impl(bool pumpEvents, bool drainFifo);
|
||||
|
||||
// The two publication points of a frame-worker cycle, cleared together under `mutex`. Sealed:
|
||||
// producer-shared renderer state is free again. Done: slots encoded, presented, ImGui restarted.
|
||||
@@ -689,15 +693,23 @@ AuroraInfo initialize(int argc, char* argv[], const AuroraConfig& config) noexce
|
||||
const AuroraBackend requestedBackend = config.desiredBackend;
|
||||
AuroraBackend selectedBackend = requestedBackend;
|
||||
bool windowCreated = false;
|
||||
std::string firstGraphicsError;
|
||||
const auto rememberGraphicsError = [&] {
|
||||
if (firstGraphicsError.empty() && SDL_GetError()[0] != '\0') {
|
||||
firstGraphicsError = SDL_GetError();
|
||||
}
|
||||
};
|
||||
if (selectedBackend != BACKEND_AUTO) {
|
||||
Log.info("Requested graphics backend: {}", backend_name(selectedBackend));
|
||||
if (window::create_window(selectedBackend)) {
|
||||
if (webgpu::initialize(selectedBackend)) {
|
||||
windowCreated = true;
|
||||
} else {
|
||||
rememberGraphicsError();
|
||||
window::destroy_window();
|
||||
}
|
||||
} else {
|
||||
rememberGraphicsError();
|
||||
Log.error("Failed to create a window for backend {}: {}", backend_name(selectedBackend),
|
||||
SDL_GetError());
|
||||
}
|
||||
@@ -714,18 +726,28 @@ AuroraInfo initialize(int argc, char* argv[], const AuroraConfig& config) noexce
|
||||
for (const auto backendType : PreferredBackendOrder) {
|
||||
selectedBackend = backendType;
|
||||
if (!window::create_window(selectedBackend)) {
|
||||
rememberGraphicsError();
|
||||
continue;
|
||||
}
|
||||
if (webgpu::initialize(selectedBackend)) {
|
||||
windowCreated = true;
|
||||
break;
|
||||
} else {
|
||||
rememberGraphicsError();
|
||||
window::destroy_window();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT(windowCreated, "Error creating window: {}", SDL_GetError());
|
||||
if (!windowCreated) {
|
||||
if (firstGraphicsError.empty()) firstGraphicsError = "No supported graphics backend is available";
|
||||
SDL_SetError("%s", firstGraphicsError.c_str());
|
||||
Log.error("Graphics initialization failed: {}", firstGraphicsError);
|
||||
return {
|
||||
.initializationStatus = AURORA_INITIALIZATION_GRAPHICS_UNAVAILABLE,
|
||||
.initializationError = SDL_GetError(),
|
||||
};
|
||||
}
|
||||
if (requestedBackend != BACKEND_AUTO && selectedBackend != requestedBackend) {
|
||||
Log.error("Graphics backend fallback in effect: video.graphics_api requested {}, "
|
||||
"running on {}",
|
||||
@@ -747,6 +769,9 @@ 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();
|
||||
@@ -1169,12 +1194,114 @@ 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.
|
||||
void encode_presentation_snapshot(const wgpu::CommandEncoder& encoder,
|
||||
const webgpu::PresentSource& presentSource,
|
||||
const PresentationImage& image,
|
||||
bool includeImGui) {
|
||||
wgpu::BindGroup encode_presentation_snapshot(wgpu::CommandEncoder& encoder,
|
||||
const webgpu::PresentSource& presentSource,
|
||||
const PresentationImage& image,
|
||||
bool includeImGui, bool metalfxEnabled,
|
||||
const wgpu::BindGroup* cachedMetalFXOutput = nullptr) {
|
||||
ZoneScoped;
|
||||
auto viewport = webgpu::calculate_present_viewport(
|
||||
image.texture.size.width, image.texture.size.height, presentSource.size.width,
|
||||
@@ -1185,6 +1312,13 @@ void encode_presentation_snapshot(const 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{
|
||||
@@ -1225,6 +1359,7 @@ void encode_presentation_snapshot(const wgpu::CommandEncoder& encoder,
|
||||
imgui::render(pass);
|
||||
pass.End();
|
||||
}
|
||||
return newMetalFXOutput;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1233,6 +1368,13 @@ 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();
|
||||
@@ -1345,6 +1487,7 @@ 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
|
||||
@@ -1372,6 +1515,7 @@ 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();
|
||||
@@ -1419,10 +1563,14 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
|
||||
const wgpu::CommandBufferDescriptor cmdBufDescriptor{
|
||||
.label = "Presentation slot command buffer",
|
||||
};
|
||||
const auto submitEncodedSlot = [&](wgpu::CommandEncoder& target) {
|
||||
const auto submitEncodedSlot = [&](wgpu::CommandEncoder& target, bool releaseMetalFXOutput = true) {
|
||||
const auto buffer = target.Finish(&cmdBufDescriptor);
|
||||
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) {
|
||||
@@ -1431,7 +1579,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);
|
||||
encode_presentation_snapshot(encoder, ctx.presentSource, *image, true, ctx.metalfxEnabled);
|
||||
presentationJobs.push_back({
|
||||
.image = std::move(image),
|
||||
.logicalFrame = ctx.logicalFrame,
|
||||
@@ -1449,12 +1597,18 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
|
||||
// The copy targets now hold this frame's resolves, so queue their readbacks on the same encoder;
|
||||
// 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);
|
||||
encode_presentation_snapshot(encoder, ctx.presentSource, *image, true);
|
||||
const auto newMetalFXOutput = encode_presentation_snapshot(
|
||||
encoder, ctx.presentSource, *image, true, ctx.metalfxEnabled,
|
||||
duplicatedMetalFXOutput ? &duplicatedMetalFXOutput : nullptr);
|
||||
if (!duplicatedMetalFXOutput && newMetalFXOutput) {
|
||||
duplicatedMetalFXOutput = newMetalFXOutput;
|
||||
}
|
||||
presentationJobs.push_back({
|
||||
.image = std::move(image),
|
||||
.logicalFrame = ctx.logicalFrame,
|
||||
@@ -1462,13 +1616,14 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
|
||||
.interpolated = true,
|
||||
.duplicated = true,
|
||||
});
|
||||
submitEncodedSlot(encoder);
|
||||
submitEncodedSlot(encoder, false);
|
||||
encoder = g_device.CreateCommandEncoder(&encoderDescriptor);
|
||||
}
|
||||
}
|
||||
auto finalImage =
|
||||
acquire_presentation_image(ctx.interpolatedFrameCount, ctx.snapshotWidth, ctx.snapshotHeight);
|
||||
encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true);
|
||||
encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true, ctx.metalfxEnabled,
|
||||
duplicatedMetalFXOutput ? &duplicatedMetalFXOutput : nullptr);
|
||||
auto pendingFrameCapture = encode_frame_capture(encoder, ctx.presentSource);
|
||||
presentationJobs.push_back({
|
||||
.image = std::move(finalImage),
|
||||
@@ -1661,7 +1816,7 @@ bool run_frame_worker_cycle(gfx::SealedFrame& sealedFrame) noexcept {
|
||||
|
||||
// Synchronous frame submission: seal, encode and present inline on the calling thread. Used when
|
||||
// the frame worker is disabled (RenderDoc captures) and on the boot path.
|
||||
void end_frame_impl(bool pumpEvents, bool drainFifo) noexcept {
|
||||
void end_frame_impl(bool pumpEvents, bool drainFifo) {
|
||||
ZoneScoped;
|
||||
#ifdef AURORA_ENABLE_GX
|
||||
webgpu::fail_if_device_lost();
|
||||
@@ -1671,11 +1826,9 @@ void end_frame_impl(bool pumpEvents, bool drainFifo) noexcept {
|
||||
gfx::SealedFrame sealedFrame;
|
||||
SealedFrameContext ctx;
|
||||
std::vector<PresentationJob> presentationJobs;
|
||||
if (drainFifo) gx::fifo::drain();
|
||||
{
|
||||
std::lock_guard gpuLock(g_rendererGpuMutex);
|
||||
if (drainFifo) {
|
||||
gx::fifo::drain();
|
||||
}
|
||||
seal_frame_locked(sealedFrame, ctx);
|
||||
presentationJobs = encode_sealed_frame(sealedFrame, ctx);
|
||||
}
|
||||
@@ -1752,7 +1905,7 @@ bool begin_frame() noexcept {
|
||||
return prepared;
|
||||
}
|
||||
|
||||
void end_frame() noexcept {
|
||||
void end_frame() {
|
||||
#ifdef AURORA_ENABLE_GX
|
||||
webgpu::fail_if_device_lost();
|
||||
#endif
|
||||
@@ -1768,10 +1921,7 @@ void end_frame() noexcept {
|
||||
|
||||
// Seal all current GX work on the CPU while the renderer is known ready.
|
||||
// Later FIFO writes belong exclusively to the next frame.
|
||||
{
|
||||
std::lock_guard gpuLock(g_rendererGpuMutex);
|
||||
gx::fifo::drain();
|
||||
}
|
||||
gx::fifo::drain();
|
||||
{
|
||||
std::lock_guard lock(g_frameWorker.mutex);
|
||||
g_frameWorker.framePrepared = false;
|
||||
@@ -1797,6 +1947,10 @@ bool wait_for_frame_worker_for(std::chrono::microseconds timeout) noexcept {
|
||||
return wait_for_frame_worker_private_for(FrameWorkerPhase::Done, timeout);
|
||||
}
|
||||
std::recursive_mutex& renderer_gpu_mutex() noexcept { return g_rendererGpuMutex; }
|
||||
void submit_staging_commands(const wgpu::CommandBuffer& commands) {
|
||||
std::lock_guard submitLock(g_queueSubmitMutex);
|
||||
webgpu::g_queue.Submit(1, &commands);
|
||||
}
|
||||
} // namespace aurora
|
||||
|
||||
// C API bindings
|
||||
@@ -1859,10 +2013,6 @@ bool aurora_flush_efb_copies_to_ram() {
|
||||
if (!aurora::gfx::efb_ram::has_pending()) {
|
||||
return true;
|
||||
}
|
||||
if (!aurora::gfx::efb_ram::prepare_downloads()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This finalizes the frame still being recorded, on the producer thread, so join the whole cycle
|
||||
// first: the encode phase owns the previous passes, EFB targets and image pool.
|
||||
aurora::wait_for_frame_worker();
|
||||
@@ -1870,6 +2020,7 @@ bool aurora_flush_efb_copies_to_ram() {
|
||||
// suffix cannot safely be replayed against the same mutable EFB resources.
|
||||
aurora::gx::mark_frame_interpolation_replay_unsafe();
|
||||
aurora::gx::fifo::drain();
|
||||
if (!aurora::gfx::efb_ram::prepare_downloads()) return false;
|
||||
const wgpu::CommandEncoderDescriptor encoderDescriptor{
|
||||
.label = "GX CPU-visible EFB copy encoder",
|
||||
};
|
||||
@@ -1895,8 +2046,7 @@ bool aurora_flush_efb_copies_to_ram() {
|
||||
}
|
||||
bool aurora_flush_efb_copy_to_ram(void* dest) {
|
||||
#ifdef AURORA_ENABLE_GX
|
||||
if (dest == nullptr || !aurora::gfx::efb_ram::has_pending(dest) ||
|
||||
!aurora::gfx::efb_ram::prepare_downloads(dest)) {
|
||||
if (dest == nullptr || !aurora::gfx::efb_ram::has_pending(dest)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1907,6 +2057,7 @@ bool aurora_flush_efb_copy_to_ram(void* dest) {
|
||||
// image instead of replaying this split frame.
|
||||
aurora::gx::mark_frame_interpolation_replay_unsafe();
|
||||
aurora::gx::fifo::drain();
|
||||
if (!aurora::gfx::efb_ram::prepare_downloads(dest)) return false;
|
||||
const wgpu::CommandEncoderDescriptor encoderDescriptor{
|
||||
.label = "GX demanded EFB copy encoder",
|
||||
};
|
||||
@@ -1948,3 +2099,7 @@ 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(); }
|
||||
|
||||
@@ -2,12 +2,43 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <SDL3/SDL_metal.h>
|
||||
#include <SDL3/SDL_properties.h>
|
||||
#include <SDL3/SDL_video.h>
|
||||
|
||||
namespace aurora::webgpu::utils {
|
||||
namespace {
|
||||
constexpr const char* MetalViewProperty = "aurora.window.metal_view";
|
||||
|
||||
void SDLCALL DestroyMetalView(void*, void* value) {
|
||||
SDL_Metal_DestroyView(value);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::shared_ptr<wgpu::ChainedStruct> SetupWindowAndGetSurfaceDescriptorCocoa(SDL_Window* window) {
|
||||
SDL_MetalView view = SDL_Metal_CreateView(window);
|
||||
std::shared_ptr<wgpu::SurfaceSourceMetalLayer> desc = std::make_shared<wgpu::SurfaceSourceMetalLayer>();
|
||||
const auto properties = SDL_GetWindowProperties(window);
|
||||
if (!properties) {
|
||||
return nullptr;
|
||||
}
|
||||
auto view = SDL_GetPointerProperty(properties, MetalViewProperty, nullptr);
|
||||
if (!view) {
|
||||
view = SDL_Metal_CreateView(window);
|
||||
if (!view) {
|
||||
return nullptr;
|
||||
}
|
||||
// Own one view per window, not per WebGPU surface. Surface recovery must
|
||||
// preserve the UIKit root and its controls (and the Cocoa Metal subview).
|
||||
// SDL cleans window properties before destroying its native window.
|
||||
// The cleanup callback also runs if setting the property fails.
|
||||
if (!SDL_SetPointerPropertyWithCleanup(properties, MetalViewProperty, view, DestroyMetalView, nullptr)) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
auto desc = std::make_shared<wgpu::SurfaceSourceMetalLayer>();
|
||||
desc->layer = SDL_Metal_GetLayer(view);
|
||||
return std::move(desc);
|
||||
if (!desc->layer) {
|
||||
SDL_ClearProperty(properties, MetalViewProperty);
|
||||
return nullptr;
|
||||
}
|
||||
return desc;
|
||||
}
|
||||
} // namespace aurora::webgpu::utils
|
||||
|
||||
@@ -46,6 +46,7 @@ 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(static_cast<const u8*>(data), nbytes, true);
|
||||
aurora::gx::fifo::process_all(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(static_cast<const u8*>(data), nbytes, false);
|
||||
aurora::gx::fifo::process_all(static_cast<const u8*>(data), nbytes, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 the name the driver gives them (see
|
||||
// singles them out, so they are picked by name or USB id (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).
|
||||
@@ -319,6 +319,18 @@ std::array<bool, PAD_CHANMAX> g_suppressLeftTrigger{};
|
||||
std::array<bool, PAD_CHANMAX> g_suppressRightTrigger{};
|
||||
|
||||
bool is_mouse_scancode(const s32 scancode) { return scancode < PAD_KEY_INVALID; }
|
||||
bool is_native_binding_pressed(SDL_Gamepad* gamepad, u32 binding) {
|
||||
if (PADIsAxisButton(binding)) {
|
||||
const u32 axis = PADAxisButtonAxis(binding);
|
||||
const u32 threshold = PADAxisButtonThreshold(binding);
|
||||
if (axis >= SDL_GAMEPAD_AXIS_COUNT || threshold < 1 || threshold > 100) return false;
|
||||
int value = SDL_GetGamepadAxis(gamepad, static_cast<SDL_GamepadAxis>(axis));
|
||||
if (PADAxisButtonNegative(binding)) value = -value;
|
||||
return value > 0 && value * 100 >= static_cast<int>(threshold) * 32767;
|
||||
}
|
||||
return binding < SDL_GAMEPAD_BUTTON_COUNT &&
|
||||
SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(binding));
|
||||
}
|
||||
bool is_mouse_button_pressed(const s32 scancode) {
|
||||
const int32_t buttonNum = -(scancode + 1);
|
||||
if (buttonNum < 1 || buttonNum > 5) {
|
||||
@@ -342,7 +354,7 @@ BOOL PADInit() {
|
||||
}
|
||||
g_initialized = true;
|
||||
|
||||
std::ranges::for_each(g_keyboardBindings, [](auto& state) {
|
||||
std::for_each(g_keyboardBindings.begin(), g_keyboardBindings.end(), [](auto& state) {
|
||||
state.m_buttonMapping = g_defaultKeys;
|
||||
state.m_axisMapping = g_defaultKeyAxis;
|
||||
});
|
||||
@@ -387,19 +399,20 @@ 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 = SDL_GetGamepadPlayerIndex(ctrl->m_controller);
|
||||
const int32_t oldPort = aurora::input::player_index(ctrl->m_index);
|
||||
if (const auto* dest = aurora::input::get_controller_for_player(port); dest != nullptr && dest != ctrl) {
|
||||
SDL_SetGamepadPlayerIndex(dest->m_controller, -1);
|
||||
aurora::input::set_player_index(dest->m_index, -1);
|
||||
}
|
||||
if (oldPort >= 0 && oldPort != port) {
|
||||
aurora::input::persist_controller_for_player(oldPort, nullptr);
|
||||
}
|
||||
SDL_SetGamepadPlayerIndex(ctrl->m_controller, static_cast<Sint32>(port));
|
||||
aurora::input::set_player_index(ctrl->m_index, static_cast<Sint32>(port));
|
||||
aurora::input::persist_controller_for_player(port, ctrl);
|
||||
}
|
||||
|
||||
@@ -425,7 +438,7 @@ void PADClearPort(const u32 port) {
|
||||
if (ctrl == nullptr) {
|
||||
return;
|
||||
}
|
||||
SDL_SetGamepadPlayerIndex(ctrl->m_controller, -1);
|
||||
aurora::input::set_player_index(ctrl->m_index, -1);
|
||||
}
|
||||
|
||||
// Secondary bindings live only in memory; the runtime re-applies them from its
|
||||
@@ -438,11 +451,14 @@ 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.
|
||||
// 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.
|
||||
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);
|
||||
if (name == nullptr || SDL_strstr(name, "Wii U Pro Controller") == nullptr) {
|
||||
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)) {
|
||||
return false;
|
||||
}
|
||||
out = g_defaultButtonsWiiUPro;
|
||||
@@ -634,8 +650,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::ranges::find_if(controller->m_axisMapping, [axis](const auto& pair) { return pair.padAxis == axis; });
|
||||
const auto iter = std::find_if(controller->m_axisMapping.begin(), controller->m_axisMapping.end(),
|
||||
[axis](const auto& pair) { return pair.padAxis == axis; });
|
||||
if (iter == controller->m_axisMapping.end()) {
|
||||
return 0;
|
||||
}
|
||||
@@ -724,10 +740,10 @@ u32 PADRead(PADStatus* status) {
|
||||
}
|
||||
|
||||
status[i].err = PAD_ERR_NONE;
|
||||
if (g_keyboardBindings[i].m_mappingsSet) {
|
||||
std::ranges::for_each(
|
||||
g_keyboardBindings[i].m_buttonMapping, [&kbState, &i, &status](const PADKeyButtonBinding& mapping) {
|
||||
if (mapping.scancode > PAD_KEY_INVALID && kbState[mapping.scancode]) {
|
||||
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) {
|
||||
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)) {
|
||||
status[i].button |= mapping.padButton;
|
||||
@@ -788,7 +804,7 @@ u32 PADRead(PADStatus* status) {
|
||||
status[i].triggerRight = static_cast<u8>(std::min(static_cast<int>(status[i].triggerRight) + tr, 255));
|
||||
}
|
||||
|
||||
if (controller) {
|
||||
if (controller && !g_keyboardBindings[i].m_mappingsSet) {
|
||||
EnsureMappingLoaded(controller);
|
||||
|
||||
// Wii U Pro Controller raw D-pad fallback. SDL's HIDAPI Wii driver posts
|
||||
@@ -833,9 +849,9 @@ u32 PADRead(PADStatus* status) {
|
||||
|
||||
bool leftTriggerSet = false;
|
||||
bool rightTriggerSet = false;
|
||||
std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet,
|
||||
&rightTriggerSet](const auto& mapping) {
|
||||
if (SDL_GetGamepadButton(controller->m_controller, static_cast<SDL_GamepadButton>(mapping.nativeButton))) {
|
||||
std::for_each(controller->m_buttonMapping.begin(), controller->m_buttonMapping.end(),
|
||||
[&controller, &i, &status, &leftTriggerSet, &rightTriggerSet](const auto& mapping) {
|
||||
if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) {
|
||||
status[i].button |= mapping.padButton;
|
||||
}
|
||||
|
||||
@@ -847,12 +863,12 @@ u32 PADRead(PADStatus* status) {
|
||||
}
|
||||
});
|
||||
|
||||
std::ranges::for_each(controller->m_altButtonMapping, [&controller, &i, &status, &leftTriggerSet,
|
||||
&rightTriggerSet](const auto& mapping) {
|
||||
std::for_each(controller->m_altButtonMapping.begin(), controller->m_altButtonMapping.end(),
|
||||
[&controller, &i, &status, &leftTriggerSet, &rightTriggerSet](const auto& mapping) {
|
||||
if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) {
|
||||
return;
|
||||
}
|
||||
if (SDL_GetGamepadButton(controller->m_controller, static_cast<SDL_GamepadButton>(mapping.nativeButton))) {
|
||||
if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) {
|
||||
status[i].button |= mapping.padButton;
|
||||
}
|
||||
|
||||
@@ -946,6 +962,17 @@ u32 PADRead(PADStatus* status) {
|
||||
Sint16 tl = std::max(static_cast<Sint16>(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_L));
|
||||
Sint16 tr = std::max(static_cast<Sint16>(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_R));
|
||||
|
||||
// Games can read either the digital L/R bits or their analog pressure.
|
||||
// An explicit button binding must drive both, otherwise the original
|
||||
// L2/R2 axis still activates L/R even when it was rebound to L1/R1.
|
||||
// Real GC pads retain independent analog travel and end-stop clicks.
|
||||
if (!(controller->m_isGameCube ||
|
||||
(SDL_GetGamepadType(controller->m_controller) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO &&
|
||||
controller->m_pid == 0x2073))) {
|
||||
if (leftTriggerSet) tl = (status[i].button & PAD_TRIGGER_L) != 0 ? 32767 : 0;
|
||||
if (rightTriggerSet) tr = (status[i].button & PAD_TRIGGER_R) != 0 ? 32767 : 0;
|
||||
}
|
||||
|
||||
if (controller->m_deadZones.emulateTriggers) {
|
||||
if (!leftTriggerSet && tl > controller->m_deadZones.leftTriggerActivationZone) {
|
||||
status[i].button |= PAD_TRIGGER_L;
|
||||
@@ -990,12 +1017,13 @@ void PADControlMotor(const u32 chan, const u32 cmd) {
|
||||
}
|
||||
|
||||
if (controller->m_isGameCube) {
|
||||
if (cmd == PAD_MOTOR_STOP) {
|
||||
aurora::input::controller_rumble(instance, 0, 1, 0);
|
||||
if (cmd == PAD_MOTOR_STOP || cmd == PAD_MOTOR_STOP_HARD) {
|
||||
// Use an unambiguous motor-off request. The (0, 1) coast encoding
|
||||
// requires SDL's GameCube brake mode; other backends or an overridden
|
||||
// hint interpret it as rumble and can leave the controller vibrating.
|
||||
aurora::input::controller_rumble(instance, 0, 0, 0);
|
||||
} else if (cmd == PAD_MOTOR_RUMBLE) {
|
||||
aurora::input::controller_rumble(instance, 1, 1, 0);
|
||||
} else if (cmd == PAD_MOTOR_STOP_HARD) {
|
||||
aurora::input::controller_rumble(instance, 0, 0, 0);
|
||||
}
|
||||
} else {
|
||||
if (cmd == PAD_MOTOR_STOP) {
|
||||
@@ -1165,8 +1193,8 @@ void PADSetButtonMapping(const u32 port, const PADButtonMapping mapping) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto iter = std::ranges::find_if(controller->m_buttonMapping,
|
||||
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
|
||||
const auto iter = std::find_if(controller->m_buttonMapping.begin(), controller->m_buttonMapping.end(),
|
||||
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
|
||||
if (iter == controller->m_buttonMapping.end()) {
|
||||
return;
|
||||
}
|
||||
@@ -1199,8 +1227,8 @@ void PADSetAltButtonMapping(const u32 port, const PADButtonMapping mapping) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto iter = std::ranges::find_if(controller->m_altButtonMapping,
|
||||
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
|
||||
const auto iter = std::find_if(controller->m_altButtonMapping.begin(), controller->m_altButtonMapping.end(),
|
||||
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
|
||||
if (iter == controller->m_altButtonMapping.end()) {
|
||||
return;
|
||||
}
|
||||
@@ -1226,8 +1254,8 @@ void PADSetAxisMapping(const u32 port, const PADAxisMapping mapping) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto iter = std::ranges::find_if(controller->m_axisMapping,
|
||||
[mapping](const auto& pair) { return mapping.padAxis == pair.padAxis; });
|
||||
const auto iter = std::find_if(controller->m_axisMapping.begin(), controller->m_axisMapping.end(),
|
||||
[mapping](const auto& pair) { return mapping.padAxis == pair.padAxis; });
|
||||
if (iter == controller->m_axisMapping.end()) {
|
||||
return;
|
||||
}
|
||||
@@ -1278,6 +1306,11 @@ BOOL PADSetKeyButtonBindings(const u32 port, PADKeyButtonBinding bindings[PAD_BU
|
||||
}
|
||||
|
||||
PADKeyButtonBinding* PADGetKeyButtonBindings(const u32 port, u32* buttonCount) {
|
||||
PADInit();
|
||||
if (!g_keyboardBindingsLoaded) {
|
||||
g_keyboardBindingsLoaded = true;
|
||||
load_keyboard_bindings();
|
||||
}
|
||||
if (port >= PAD_MAX_CONTROLLERS || !g_keyboardBindings[port].m_mappingsSet) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1396,9 +1429,10 @@ static void load_keyboard_bindings() {
|
||||
|
||||
if (mappingsSet) {
|
||||
const bool anyBound =
|
||||
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; });
|
||||
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; });
|
||||
if (!anyBound) {
|
||||
mappingsSet = false;
|
||||
}
|
||||
@@ -1438,7 +1472,10 @@ void __PADWriteDeadZones(SDL_IOStream* file, // NOLINT(*-reserved-identifier)
|
||||
void PADSerializeMappings() {
|
||||
const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath);
|
||||
|
||||
for (auto& controller : aurora::input::g_GameControllers | std::views::values) {
|
||||
// Avoid std::views::values here: older Apple libc++ releases implement the
|
||||
// C++20 ranges algorithms we use but not this adaptor.
|
||||
for (auto& entry : aurora::input::g_GameControllers) {
|
||||
auto& controller = entry.second;
|
||||
EnsureMappingLoaded(&controller);
|
||||
const auto filePath =
|
||||
basePath / fmt::format("{}_{:04X}_{:04X}.controller", aurora::input::controller_name(controller.m_index),
|
||||
@@ -1540,8 +1577,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::ranges::find_if(skButtonNames, [&button](const auto& pair) { return button == pair.first; });
|
||||
if (const auto iter = std::find_if(skButtonNames.begin(), skButtonNames.end(),
|
||||
[&button](const auto& pair) { return button == pair.first; });
|
||||
iter != skButtonNames.end()) {
|
||||
return iter->second.data();
|
||||
}
|
||||
@@ -1554,7 +1591,8 @@ const char* PADGetNativeButtonName(u32 button) {
|
||||
}
|
||||
|
||||
const char* PADGetAxisName(const PADAxis axis) {
|
||||
if (const auto it = std::ranges::find_if(skAxisNames, [&axis](const auto& pair) { return axis == pair.first; });
|
||||
if (const auto it = std::find_if(skAxisNames.begin(), skAxisNames.end(),
|
||||
[&axis](const auto& pair) { return axis == pair.first; });
|
||||
it != skAxisNames.end()) {
|
||||
return it->second.data();
|
||||
}
|
||||
@@ -1563,7 +1601,8 @@ const char* PADGetAxisName(const PADAxis axis) {
|
||||
}
|
||||
|
||||
const char* PADGetAxisDirectionLabel(const PADAxis axis) {
|
||||
if (const auto it = std::ranges::find_if(skAxisDirLabels, [&axis](const auto& pair) { return axis == pair.first; });
|
||||
if (const auto it = std::find_if(skAxisDirLabels.begin(), skAxisDirLabels.end(),
|
||||
[&axis](const auto& pair) { return axis == pair.first; });
|
||||
it != skAxisDirLabels.end()) {
|
||||
return it->second.data();
|
||||
}
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <optional>
|
||||
#include <mutex>
|
||||
|
||||
namespace aurora::vi {
|
||||
std::optional<GXRenderModeObj> g_renderMode;
|
||||
namespace {
|
||||
std::atomic<float> g_presentAspectCorrection{1.f};
|
||||
std::mutex g_renderModeMutex;
|
||||
|
||||
float calculate_present_aspect_correction(const GXRenderModeObj& rm) noexcept {
|
||||
if (rm.viWidth == 0 || rm.viHeight == 0) {
|
||||
@@ -29,9 +31,8 @@ float calculate_present_aspect_correction(const GXRenderModeObj& rm) noexcept {
|
||||
const float verticalFill = static_cast<float>(rm.viHeight) / nominalActiveHeight;
|
||||
return horizontalFill / verticalFill;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Vec2<uint32_t> render_mode_size() noexcept {
|
||||
Vec2<uint32_t> render_mode_size_locked() noexcept {
|
||||
if (!g_renderMode) {
|
||||
return {640, 528};
|
||||
}
|
||||
@@ -40,18 +41,31 @@ Vec2<uint32_t> render_mode_size() noexcept {
|
||||
return {std::max<uint32_t>(g_renderMode->fbWidth, 640), std::max<uint32_t>(g_renderMode->efbHeight, 528)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Vec2<uint32_t> render_mode_size() noexcept {
|
||||
std::lock_guard lock(g_renderModeMutex);
|
||||
return render_mode_size_locked();
|
||||
}
|
||||
|
||||
void configure(const GXRenderModeObj* rm) noexcept {
|
||||
const auto oldSize = render_mode_size();
|
||||
if (rm == nullptr) {
|
||||
g_renderMode.reset();
|
||||
} else {
|
||||
g_renderMode = *rm;
|
||||
g_presentAspectCorrection.store(calculate_present_aspect_correction(*rm), std::memory_order_release);
|
||||
bool sizeChanged = false;
|
||||
{
|
||||
std::lock_guard lock(g_renderModeMutex);
|
||||
const auto oldSize = render_mode_size_locked();
|
||||
if (rm == nullptr) {
|
||||
g_renderMode.reset();
|
||||
} else {
|
||||
g_renderMode = *rm;
|
||||
g_presentAspectCorrection.store(calculate_present_aspect_correction(*rm), std::memory_order_release);
|
||||
}
|
||||
if (rm == nullptr) {
|
||||
g_presentAspectCorrection.store(1.f, std::memory_order_release);
|
||||
}
|
||||
sizeChanged = render_mode_size_locked() != oldSize;
|
||||
}
|
||||
if (rm == nullptr) {
|
||||
g_presentAspectCorrection.store(1.f, std::memory_order_release);
|
||||
}
|
||||
if (render_mode_size() != oldSize) {
|
||||
// Never hold the mode lock across a resize request or a renderer callback.
|
||||
if (sizeChanged) {
|
||||
window::request_frame_buffer_resize();
|
||||
}
|
||||
}
|
||||
@@ -61,6 +75,7 @@ Vec2<uint32_t> configured_fb_size() noexcept {
|
||||
}
|
||||
|
||||
Vec2<uint32_t> visible_fb_size() noexcept {
|
||||
std::lock_guard lock(g_renderModeMutex);
|
||||
if (!g_renderMode) {
|
||||
return {640, 528};
|
||||
}
|
||||
|
||||
+191
-53
@@ -1,4 +1,5 @@
|
||||
#include "common.hpp"
|
||||
#include "staging_map.hpp"
|
||||
#include "../gx/shader_info.hpp"
|
||||
|
||||
#include "clear.hpp"
|
||||
@@ -22,7 +23,6 @@
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
|
||||
#include <absl/container/flat_hash_map.h>
|
||||
#include <magic_enum.hpp>
|
||||
@@ -36,10 +36,13 @@ using webgpu::g_device;
|
||||
using webgpu::g_instance;
|
||||
using webgpu::g_queue;
|
||||
|
||||
struct DebugFrameData {
|
||||
#ifdef AURORA_GFX_DEBUG_GROUPS
|
||||
std::vector<std::string> g_debugGroupStack;
|
||||
std::vector<std::string> g_debugMarkers;
|
||||
std::vector<std::string> groups;
|
||||
std::vector<std::string> markers;
|
||||
#endif
|
||||
};
|
||||
DebugFrameData g_debugFrame;
|
||||
|
||||
constexpr uint64_t StagingBufferSize = UniformBufferSize + VertexBufferSize + IndexBufferSize + StorageBufferSize +
|
||||
(UseTextureBuffer ? TextureUploadSize : 0);
|
||||
@@ -128,12 +131,7 @@ wgpu::Buffer g_storageBuffer;
|
||||
constexpr size_t FrameSlotCount = 3;
|
||||
static std::array<wgpu::Buffer, FrameSlotCount> g_stagingBuffers;
|
||||
static size_t currentStagingBuffer = 0;
|
||||
enum class BufferMapState {
|
||||
Unmapped,
|
||||
Mapping,
|
||||
Mapped,
|
||||
};
|
||||
static std::atomic s_mappingState{BufferMapState::Unmapped};
|
||||
static StagingMapState s_mappingState;
|
||||
static wgpu::Limits g_cachedLimits;
|
||||
// Advanced once per logical frame in the seal prologue, under the renderer GPU mutex and with the
|
||||
// producer blocked, so every later reader sees a value that no longer moves.
|
||||
@@ -168,7 +166,12 @@ struct RenderPass {
|
||||
Range resolveUniformRange;
|
||||
std::array<u32, 3> resolveCopyFilterCoefficients{0, 64, 0};
|
||||
Vec4<float> clearColorValue{0.f, 0.f, 0.f, 0.f};
|
||||
float clearDepthValue = 1.f;
|
||||
// 1.f is the forward-Z "farthest" clear value; under UseReversedZ farthest is 0.f instead (see
|
||||
// gx::clear_depth_value(), which the main render pass explicitly overrides this default with -
|
||||
// any OTHER pass that keeps this default, e.g. an offscreen render-to-texture pass composited
|
||||
// later, needs the same reversed-Z-aware value or its depth buffer starts "already nearest",
|
||||
// failing every subsequent depth test and making whatever's drawn into it vanish).
|
||||
float clearDepthValue = gx::UseReversedZ ? 0.f : 1.f;
|
||||
CommandList commands;
|
||||
bool clearColor = true;
|
||||
bool clearDepth = true;
|
||||
@@ -229,6 +232,8 @@ static void recycle_render_passes(std::vector<RenderPass>& passes) noexcept {
|
||||
}
|
||||
|
||||
struct SealedFrameData {
|
||||
depth_peek::FrameMapping depthMapping;
|
||||
DebugFrameData debug;
|
||||
std::vector<RenderPass> passes;
|
||||
};
|
||||
|
||||
@@ -250,6 +255,51 @@ static std::atomic_bool g_inOffscreen{false};
|
||||
static std::optional<RenderPass> g_suspendedEfbPass;
|
||||
static Viewport g_suspendedEfbViewport;
|
||||
static ClipRect g_suspendedEfbScissor;
|
||||
// Prefix referenced by a suspended EFB pass. Preserve its offsets across an
|
||||
// offscreen split, without rendering it before the bake it may sample finishes.
|
||||
static StagingSizes g_suspendedEfbBytes{};
|
||||
static constexpr StagingSizes PhysicalStagingCapacity{
|
||||
VertexBufferSize, UniformBufferSize, IndexBufferSize, StorageBufferSize};
|
||||
static StagingSizes g_stagingCapacity = PhysicalStagingCapacity;
|
||||
static uint64_t g_stagingEpoch = 0;
|
||||
static uint64_t g_stagingSplitCount = 0;
|
||||
static StagingSizes g_stagingHighWater{};
|
||||
|
||||
StagingSizes staging_usage() noexcept {
|
||||
return {g_verts.size(), g_uniforms.size(), g_indices.size(), g_storage.size()};
|
||||
}
|
||||
StagingSizes staging_high_water() noexcept { return g_stagingHighWater; }
|
||||
uint64_t staging_epoch() noexcept { return g_stagingEpoch; }
|
||||
uint64_t staging_split_count() noexcept { return g_stagingSplitCount; }
|
||||
uint64_t staging_uniform_bytes(uint64_t bytes) {
|
||||
return staging_padded(bytes, g_cachedLimits.minUniformBufferOffsetAlignment);
|
||||
}
|
||||
uint64_t staging_storage_bytes(uint64_t bytes) {
|
||||
return staging_padded(bytes, g_cachedLimits.minStorageBufferOffsetAlignment);
|
||||
}
|
||||
void set_staging_capacity_limits_for_testing(const StagingSizes& limits) {
|
||||
for (unsigned i = 0; i < limits.size(); ++i) {
|
||||
if (limits[i] > PhysicalStagingCapacity[i])
|
||||
throw StagingCapacityError("Test staging capacity exceeds physical buffer");
|
||||
}
|
||||
g_stagingCapacity = limits;
|
||||
g_stagingHighWater = {};
|
||||
}
|
||||
bool staging_has_space(const StagingSizes& demand) {
|
||||
// Async readback preparation runs in the worker's noexcept seal prologue.
|
||||
// Reserve all 32 slots plus the uniform binding's 3840-byte trailing window.
|
||||
const StagingSizes tail{0, gx::MaxUniformSize + efb_ram::MaxAsyncReadbackSlots * staging_uniform_bytes(48), 0, 0};
|
||||
const StagingSizes retained = g_suspendedEfbPass ? g_suspendedEfbBytes : StagingSizes{};
|
||||
if (!staging_fits(retained, demand, tail, g_stagingCapacity))
|
||||
throw StagingCapacityError("GPU operation exceeds staging capacity including retained EFB data");
|
||||
return staging_fits(staging_usage(), demand, tail, g_stagingCapacity);
|
||||
}
|
||||
void ensure_staging_space(const StagingSizes& demand) {
|
||||
if (staging_has_space(demand)) return;
|
||||
split_staging_batch();
|
||||
if (!staging_has_space(demand))
|
||||
throw StagingCapacityError("GPU operation still exceeds staging capacity after submission");
|
||||
}
|
||||
|
||||
static void discard_suspended_efb_pass() noexcept {
|
||||
if (g_suspendedEfbPass) {
|
||||
@@ -274,7 +324,8 @@ static size_t g_recordingSnapshotSlot = 0;
|
||||
static TextureHandle new_resolve_source_snapshot(wgpu::Extent3D size, wgpu::TextureFormat format) noexcept {
|
||||
const wgpu::TextureDescriptor textureDescriptor{
|
||||
.label = "GX Copy Source Snapshot",
|
||||
.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst,
|
||||
.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc |
|
||||
wgpu::TextureUsage::CopyDst,
|
||||
.dimension = wgpu::TextureDimension::e2D,
|
||||
.size = size,
|
||||
.format = format,
|
||||
@@ -420,7 +471,7 @@ static inline void push_command(CommandType type, const Command::Data& data) {
|
||||
g_renderPasses[g_currentRenderPass].commands.push_back({
|
||||
.type = type,
|
||||
#ifdef AURORA_GFX_DEBUG_GROUPS
|
||||
.debugGroupStack = g_debugGroupStack,
|
||||
.debugGroupStack = g_debugFrame.groups,
|
||||
#endif
|
||||
.data = data,
|
||||
});
|
||||
@@ -480,6 +531,7 @@ void set_scissor(const ClipRect& cmd) noexcept {
|
||||
template <>
|
||||
void push_draw_command(clear::DrawData data) {
|
||||
if (data.uniformRange.size == 0) {
|
||||
ensure_staging_space({0, staging_uniform_bytes(16), 0, 0});
|
||||
const std::array clearUniform{
|
||||
std::clamp(data.depth, 0.f, 1.f),
|
||||
0.f,
|
||||
@@ -506,6 +558,7 @@ void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool cl
|
||||
Log.warn("Dropping resolve pass without an active render pass");
|
||||
return;
|
||||
}
|
||||
ensure_staging_space({0, 2 * staging_uniform_bytes(48), 0, 0});
|
||||
auto& prevPass = g_renderPasses[g_currentRenderPass];
|
||||
const auto targetWidth = static_cast<int32_t>(prevPass.targetSize.width);
|
||||
const auto targetHeight = static_cast<int32_t>(prevPass.targetSize.height);
|
||||
@@ -538,7 +591,7 @@ void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool cl
|
||||
sourceRect = {srcLeft, srcTop, std::max(srcRight - srcLeft, 1.0f), std::max(srcBottom - srcTop, 1.0f)};
|
||||
}
|
||||
prevPass.resolveTarget = std::move(texture);
|
||||
prevPass.requireReadyPipelines = persistentCopy;
|
||||
prevPass.requireReadyPipelines |= persistentCopy;
|
||||
prevPass.resolveRect = rect;
|
||||
prevPass.resolveSourceRect = sourceRect;
|
||||
prevPass.resolveFormat = resolveFormat;
|
||||
@@ -734,6 +787,7 @@ void begin_offscreen(uint32_t width, uint32_t height) {
|
||||
if (!g_inOffscreen) {
|
||||
auto& currentPass = g_renderPasses[g_currentRenderPass];
|
||||
if (!currentPass.resolveTarget) {
|
||||
g_suspendedEfbBytes = staging_usage();
|
||||
g_suspendedEfbPass = std::move(currentPass);
|
||||
g_renderPasses.pop_back();
|
||||
--g_currentRenderPass;
|
||||
@@ -757,7 +811,9 @@ void begin_offscreen(uint32_t width, uint32_t height) {
|
||||
.targetSize = {width, height, 1},
|
||||
.msaaSamples = 1,
|
||||
.clearColorValue = {0.f, 0.f, 0.f, 0.f},
|
||||
.clearDepthValue = 1.f,
|
||||
// See the RenderPass::clearDepthValue default's comment: this offscreen pass gets its own
|
||||
// depth buffer, and the farthest clear value is 0.f, not 1.f, under UseReversedZ.
|
||||
.clearDepthValue = gx::UseReversedZ ? 0.f : 1.f,
|
||||
.clearColor = true,
|
||||
.clearDepth = true,
|
||||
};
|
||||
@@ -844,7 +900,7 @@ void initialize() {
|
||||
label.c_str());
|
||||
}
|
||||
currentStagingBuffer = 0;
|
||||
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
|
||||
s_mappingState.reset();
|
||||
map_staging_buffer();
|
||||
|
||||
{
|
||||
@@ -950,6 +1006,8 @@ void shutdown() {
|
||||
g_uniformBuffer = {};
|
||||
g_indexBuffer = {};
|
||||
g_storageBuffer = {};
|
||||
// Invalidate outstanding callbacks before releasing their buffers.
|
||||
s_mappingState.reset();
|
||||
g_stagingBuffers.fill({});
|
||||
for (auto& pool : g_resolveSourceSnapshotPools) {
|
||||
pool.entry.reset();
|
||||
@@ -968,37 +1026,36 @@ void shutdown() {
|
||||
g_inOffscreen = false;
|
||||
g_frameIndex = UINT32_MAX;
|
||||
currentStagingBuffer = 0;
|
||||
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
|
||||
}
|
||||
|
||||
void map_staging_buffer() {
|
||||
auto expected = BufferMapState::Unmapped;
|
||||
if (!s_mappingState.compare_exchange_strong(expected, BufferMapState::Mapping, std::memory_order_acq_rel,
|
||||
std::memory_order_acquire)) {
|
||||
const auto generation = s_mappingState.request();
|
||||
if (generation == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
g_stagingBuffers[currentStagingBuffer].MapAsync(
|
||||
wgpu::MapMode::Write, 0, StagingBufferSize, wgpu::CallbackMode::AllowSpontaneous,
|
||||
[](wgpu::MapAsyncStatus status, wgpu::StringView message) {
|
||||
[generation](wgpu::MapAsyncStatus status, wgpu::StringView message) {
|
||||
const auto result = status == wgpu::MapAsyncStatus::Success
|
||||
? BufferMapState::Mapped : BufferMapState::Unmapped;
|
||||
if (!s_mappingState.complete(generation, result)) return;
|
||||
if (status == wgpu::MapAsyncStatus::CallbackCancelled || status == wgpu::MapAsyncStatus::Aborted) {
|
||||
Log.warn("Buffer mapping {}: {}", magic_enum::enum_name(status), message);
|
||||
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
ASSERT(status == wgpu::MapAsyncStatus::Success, "Buffer mapping failed: {} {}", magic_enum::enum_name(status),
|
||||
message);
|
||||
s_mappingState.store(BufferMapState::Mapped, std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
static bool begin_frame_impl(bool clearEfb) {
|
||||
static bool begin_frame_impl(bool clearEfb, bool capacityResume = false) {
|
||||
ZoneScoped;
|
||||
{
|
||||
ZoneScopedN("Wait for buffer map");
|
||||
map_staging_buffer();
|
||||
while (true) {
|
||||
const auto mappingState = s_mappingState.load(std::memory_order_acquire);
|
||||
const auto mappingState = s_mappingState.state();
|
||||
if (mappingState == BufferMapState::Mapped) {
|
||||
break;
|
||||
}
|
||||
@@ -1014,8 +1071,11 @@ static bool begin_frame_impl(bool clearEfb) {
|
||||
return false;
|
||||
}
|
||||
g_instance.ProcessEvents();
|
||||
webgpu::fail_if_device_lost();
|
||||
s_mappingState.wait_for_progress();
|
||||
}
|
||||
}
|
||||
++g_stagingEpoch;
|
||||
g_recordingSnapshotSlot = currentStagingBuffer;
|
||||
size_t bufferOffset = 0;
|
||||
const auto& stagingBuf = g_stagingBuffers[currentStagingBuffer];
|
||||
@@ -1040,7 +1100,7 @@ static bool begin_frame_impl(bool clearEfb) {
|
||||
gx::begin_frame_interpolation();
|
||||
}
|
||||
discard_suspended_efb_pass();
|
||||
webgpu::clear_present_source_override();
|
||||
if (!capacityResume) webgpu::clear_present_source_override();
|
||||
|
||||
push_render_pass(RenderPass{});
|
||||
set_efb_targets(g_renderPasses[0]);
|
||||
@@ -1079,12 +1139,12 @@ void abort_frame() noexcept {
|
||||
g_textureUploads.clear();
|
||||
g_textureUpload.release();
|
||||
}
|
||||
if (s_mappingState.load(std::memory_order_acquire) == BufferMapState::Mapped) {
|
||||
if (s_mappingState.state() == BufferMapState::Mapped) {
|
||||
// Pending interpolation tasks hold raw pointers into the mapped staging
|
||||
// range; they must be dropped before the buffer is unmapped and rotated.
|
||||
gx::drop_pending_frame_interpolation_uniforms();
|
||||
g_stagingBuffers[currentStagingBuffer].Unmap();
|
||||
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
|
||||
s_mappingState.reset();
|
||||
currentStagingBuffer = (currentStagingBuffer + 1) % g_stagingBuffers.size();
|
||||
map_staging_buffer();
|
||||
}
|
||||
@@ -1101,7 +1161,7 @@ void abort_frame() noexcept {
|
||||
|
||||
static void end_batch_impl(const wgpu::CommandEncoder& cmd, bool advanceFrame) {
|
||||
ZoneScoped;
|
||||
ASSERT(!g_inOffscreen, "end_frame called while offscreen rendering is active");
|
||||
ASSERT(!advanceFrame || !g_inOffscreen, "end_frame called while offscreen rendering is active");
|
||||
if (advanceFrame) {
|
||||
gx::finalize_frame_interpolation();
|
||||
} else {
|
||||
@@ -1110,6 +1170,8 @@ static void end_batch_impl(const wgpu::CommandEncoder& cmd, bool advanceFrame) {
|
||||
gx::drop_pending_frame_interpolation_uniforms();
|
||||
}
|
||||
g_uniforms.append_zeroes(gx::MaxUniformSize); // Pad the end of the buffer
|
||||
const auto used = staging_usage();
|
||||
for (unsigned i = 0; i < used.size(); ++i) g_stagingHighWater[i] = std::max(g_stagingHighWater[i], used[i]);
|
||||
uint64_t bufferOffset = 0;
|
||||
const auto writeBuffer = [&](ByteBuffer& buf, wgpu::Buffer& out, uint64_t size, std::string_view label) {
|
||||
const auto writeSize = buf.size(); // Only need to copy this many bytes
|
||||
@@ -1121,7 +1183,7 @@ static void end_batch_impl(const wgpu::CommandEncoder& cmd, bool advanceFrame) {
|
||||
return writeSize;
|
||||
};
|
||||
g_stagingBuffers[currentStagingBuffer].Unmap();
|
||||
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
|
||||
s_mappingState.reset();
|
||||
g_stats.drawCallCount = g_drawCallCount;
|
||||
g_stats.mergedDrawCallCount = g_mergedDrawCallCount;
|
||||
g_stats.lastVertSize = writeBuffer(g_verts, g_vertexBuffer, VertexBufferSize, "Vertex");
|
||||
@@ -1164,6 +1226,63 @@ void end_frame(const wgpu::CommandEncoder& cmd) { end_batch_impl(cmd, true); }
|
||||
|
||||
void end_batch(const wgpu::CommandEncoder& cmd) { end_batch_impl(cmd, false); }
|
||||
|
||||
void split_staging_batch() {
|
||||
// Never called under the decoder's renderer lock: the worker needs that lock
|
||||
// to reach DONE. FIFO admission yields its unconsumed command first.
|
||||
aurora::wait_for_frame_worker();
|
||||
std::lock_guard gpuLock(aurora::renderer_gpu_mutex());
|
||||
if (!has_current_render_pass())
|
||||
throw StagingCapacityError("Cannot split staging outside an active render pass");
|
||||
gx::mark_frame_interpolation_replay_unsafe();
|
||||
const bool offscreen = g_inOffscreen;
|
||||
const auto viewport = g_cachedViewport;
|
||||
const auto scissor = g_cachedScissor;
|
||||
const auto renderViewport = gx::g_gxState.renderViewport;
|
||||
const auto renderScissor = gx::g_gxState.renderScissor;
|
||||
const auto& active = g_renderPasses[g_currentRenderPass];
|
||||
RenderPass continuation{
|
||||
.colorView = active.colorView, .resolveView = active.resolveView,
|
||||
.depthView = active.depthView, .copySourceTexture = active.copySourceTexture,
|
||||
.copySourceView = active.copySourceView, .copySourceDepthView = active.copySourceDepthView,
|
||||
.targetSize = active.targetSize, .msaaSamples = active.msaaSamples,
|
||||
.clearColor = false, .clearDepth = false,
|
||||
.requireReadyPipelines = active.requireReadyPipelines || offscreen,
|
||||
};
|
||||
auto suspended = std::move(g_suspendedEfbPass);
|
||||
g_suspendedEfbPass.reset();
|
||||
std::array<std::vector<uint8_t>, 4> retained;
|
||||
std::array<ByteBuffer*, 4> buffers{&g_verts, &g_uniforms, &g_indices, &g_storage};
|
||||
if (suspended) {
|
||||
for (unsigned i = 0; i < buffers.size(); ++i) {
|
||||
if (g_suspendedEfbBytes[i])
|
||||
retained[i].assign(buffers[i]->data(), buffers[i]->data() + g_suspendedEfbBytes[i]);
|
||||
}
|
||||
}
|
||||
auto encoder = g_device.CreateCommandEncoder();
|
||||
end_batch(encoder);
|
||||
render(encoder);
|
||||
aurora::submit_staging_commands(encoder.Finish());
|
||||
after_submit();
|
||||
if (!begin_frame_impl(false, true))
|
||||
throw StagingCapacityError("Staging remap failed after capacity submission");
|
||||
recycle_render_passes(g_renderPasses);
|
||||
push_render_pass(std::move(continuation));
|
||||
g_currentRenderPass = 0;
|
||||
g_suspendedEfbPass = std::move(suspended);
|
||||
for (unsigned i = 0; i < buffers.size(); ++i) {
|
||||
if (!retained[i].empty()) buffers[i]->append(retained[i].data(), retained[i].size());
|
||||
}
|
||||
g_inOffscreen = offscreen;
|
||||
g_cachedViewport = viewport;
|
||||
g_cachedScissor = scissor;
|
||||
gx::g_gxState.renderViewport = renderViewport;
|
||||
gx::g_gxState.renderScissor = renderScissor;
|
||||
gx::g_gxState.stateDirty = true;
|
||||
push_command(CommandType::SetViewport, Command::Data{.setViewport = viewport});
|
||||
push_command(CommandType::SetScissor, Command::Data{.setScissor = scissor});
|
||||
++g_stagingSplitCount;
|
||||
}
|
||||
|
||||
uint32_t current_frame() noexcept { return g_frameIndex; }
|
||||
|
||||
// The only place that erases from g_cachedBindGroups, whose handles the frame being encoded still
|
||||
@@ -1196,10 +1315,10 @@ static const char* render_pass_label(u32 index) noexcept {
|
||||
}
|
||||
|
||||
static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vector<RenderPass>& passes, u32 idx,
|
||||
int32_t interpolatedFrame);
|
||||
int32_t interpolatedFrame, DebugFrameData& debugFrame);
|
||||
|
||||
static void render_impl(std::vector<RenderPass>& renderPasses, wgpu::CommandEncoder& cmd, int32_t interpolatedFrame,
|
||||
bool finalize) {
|
||||
bool finalize, DebugFrameData& debugFrame, const depth_peek::FrameMapping& depthMapping) {
|
||||
ZoneScoped;
|
||||
// Palette conversions, MSAA resolves and EFB copies depend on sealed frame state, not on the
|
||||
// interpolation weight, so encode them on the native render and let replay slots sample them.
|
||||
@@ -1249,11 +1368,11 @@ static void render_impl(std::vector<RenderPass>& renderPasses, wgpu::CommandEnco
|
||||
};
|
||||
|
||||
auto pass = cmd.BeginRenderPass(&renderPassDescriptor);
|
||||
render_pass_impl(pass, renderPasses, i, interpolatedFrame);
|
||||
render_pass_impl(pass, renderPasses, i, interpolatedFrame, debugFrame);
|
||||
pass.End();
|
||||
|
||||
if (finalize && i == renderPasses.size() - 1) {
|
||||
depth_peek::encode_frame_snapshot(cmd, passInfo.copySourceDepthView, passInfo.targetSize, passInfo.msaaSamples);
|
||||
depth_peek::encode_frame_snapshot(cmd, passInfo.copySourceDepthView, passInfo.targetSize, passInfo.msaaSamples, depthMapping);
|
||||
}
|
||||
|
||||
if (passInfo.resolveTarget) {
|
||||
@@ -1327,20 +1446,21 @@ static void render_impl(std::vector<RenderPass>& renderPasses, wgpu::CommandEnco
|
||||
}
|
||||
|
||||
#if defined(AURORA_GFX_DEBUG_GROUPS)
|
||||
if (finalize && !g_debugGroupStack.empty()) {
|
||||
for (auto& it : std::ranges::reverse_view(g_debugGroupStack)) {
|
||||
Log.warn("Debug group was not popped at end of frame: {}", it);
|
||||
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);
|
||||
}
|
||||
g_debugGroupStack.clear();
|
||||
debugFrame.groups.clear();
|
||||
}
|
||||
|
||||
if (finalize && g_debugMarkers.size() > 0) {
|
||||
g_debugMarkers.clear();
|
||||
if (finalize && debugFrame.markers.size() > 0) {
|
||||
debugFrame.markers.clear();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void seal_frame(SealedFrame& out) noexcept {
|
||||
out.data().depthMapping = depth_peek::capture_frame_mapping();
|
||||
ZoneScoped;
|
||||
// The encode that could still have been holding these has completed: the
|
||||
// producer joins the worker's DONE phase before it seals another frame.
|
||||
@@ -1350,15 +1470,24 @@ void seal_frame(SealedFrame& out) noexcept {
|
||||
// capacity included, back to the producer.
|
||||
recycle_render_passes(passes);
|
||||
passes.swap(g_renderPasses);
|
||||
#ifdef AURORA_GFX_DEBUG_GROUPS
|
||||
// Marker indices and unmatched-group warnings belong to these detached passes.
|
||||
// The next producer frame must not modify strings still read by this encoder.
|
||||
auto& debug = out.data().debug;
|
||||
debug.groups.clear();
|
||||
debug.markers.clear();
|
||||
debug.groups.swap(g_debugFrame.groups);
|
||||
debug.markers.swap(g_debugFrame.markers);
|
||||
#endif
|
||||
g_currentRenderPass = UINT32_MAX;
|
||||
}
|
||||
|
||||
void render(SealedFrame& frame, wgpu::CommandEncoder& cmd, int32_t interpolatedFrame, bool finalize) {
|
||||
render_impl(frame.data().passes, cmd, interpolatedFrame, finalize);
|
||||
render_impl(frame.data().passes, cmd, interpolatedFrame, finalize, frame.data().debug, frame.data().depthMapping);
|
||||
}
|
||||
|
||||
void render(wgpu::CommandEncoder& cmd, int32_t interpolatedFrame, bool finalize) {
|
||||
render_impl(g_renderPasses, cmd, interpolatedFrame, finalize);
|
||||
render_impl(g_renderPasses, cmd, interpolatedFrame, finalize, g_debugFrame, depth_peek::capture_frame_mapping());
|
||||
if (finalize) {
|
||||
g_currentRenderPass = UINT32_MAX;
|
||||
expire_bind_group_cache();
|
||||
@@ -1376,7 +1505,7 @@ void after_submit() noexcept {
|
||||
}
|
||||
|
||||
static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vector<RenderPass>& renderPasses, u32 idx,
|
||||
int32_t interpolatedFrame) {
|
||||
int32_t interpolatedFrame, DebugFrameData& debugFrame) {
|
||||
// Per-invocation, not per-process: two encoders can be recording at once.
|
||||
gx::DrawEncodeState encodeState{};
|
||||
encodeState.boundTextureBindGroup = gx::g_emptyTextureBindGroup.Get();
|
||||
@@ -1410,10 +1539,19 @@ static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vec
|
||||
switch (cmd.type) {
|
||||
case CommandType::SetViewport: {
|
||||
const auto& vp = cmd.data.setViewport;
|
||||
// WebGPU requires 0 <= minDepth <= maxDepth <= 1, and the guest's (near, far) order is already
|
||||
// reproduced in clip space. Passing the raw swapped pair diverged per backend in release builds.
|
||||
const float minDepth = std::clamp(std::min(vp.znear, vp.zfar), 0.0f, 1.0f);
|
||||
const float maxDepth = std::clamp(std::max(vp.znear, vp.zfar), 0.0f, 1.0f);
|
||||
// WebGPU requires 0 <= minDepth <= maxDepth <= 1. vp.znear/vp.zfar are in GX's own distance
|
||||
// terms (0 = near); under UseReversedZ the host depth-buffer storage direction is flipped
|
||||
// (near = 1, far = 0), so this range has to be remapped through 1-x the same way the
|
||||
// projection matrix, depth compare function, and clear value all are - a plain min/max clamp
|
||||
// (the previous code here) maps a *restricted* range (e.g. a viewport deliberately narrowed
|
||||
// to force something to draw "in front of everything") to the wrong end of the buffer: what
|
||||
// should land near the near-storage-extreme (1.0) instead lands near the far-storage-extreme
|
||||
// (0.0), so anything else drawn afterward at its true depth wins the compare test and the
|
||||
// "in front" geometry silently vanishes. A full [0,1] viewport is unaffected either way,
|
||||
// which is why this only broke specific elements, not the whole scene. Matches upstream
|
||||
// aurora's apply_viewport (lib/gfx/encoding.cpp) exactly.
|
||||
const float minDepth = gx::UseReversedZ ? 1.0f - vp.zfar : vp.znear;
|
||||
const float maxDepth = gx::UseReversedZ ? 1.0f - vp.znear : vp.zfar;
|
||||
pass.SetViewport(vp.left, vp.top, vp.width, vp.height, minDepth, maxDepth);
|
||||
} break;
|
||||
case CommandType::SetScissor: {
|
||||
@@ -1447,7 +1585,7 @@ static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vec
|
||||
} break;
|
||||
case CommandType::DebugMarker: {
|
||||
#if defined(AURORA_GFX_DEBUG_GROUPS)
|
||||
pass.InsertDebugMarker(wgpu::StringView(g_debugMarkers[cmd.data.debugMarkerIndex]));
|
||||
pass.InsertDebugMarker(wgpu::StringView(debugFrame.markers[cmd.data.debugMarkerIndex]));
|
||||
#endif
|
||||
} break;
|
||||
}
|
||||
@@ -1600,8 +1738,8 @@ uint32_t align_uniform(uint32_t value) { return AURORA_ALIGN(value, g_cachedLimi
|
||||
|
||||
void insert_debug_marker(std::string label) {
|
||||
#if defined(AURORA_GFX_DEBUG_GROUPS)
|
||||
auto idx = g_debugMarkers.size();
|
||||
g_debugMarkers.emplace_back(std::move(label));
|
||||
auto idx = g_debugFrame.markers.size();
|
||||
g_debugFrame.markers.emplace_back(std::move(label));
|
||||
push_command(CommandType::DebugMarker, {.debugMarkerIndex = idx});
|
||||
#endif
|
||||
}
|
||||
@@ -1610,22 +1748,22 @@ void insert_debug_marker(std::string label) {
|
||||
|
||||
void aurora::gfx::push_debug_group(std::string label) {
|
||||
#if defined(AURORA_GFX_DEBUG_GROUPS)
|
||||
g_debugGroupStack.push_back(std::move(label));
|
||||
g_debugFrame.groups.push_back(std::move(label));
|
||||
#endif
|
||||
}
|
||||
void aurora_push_debug_group(const char* label) {
|
||||
#ifdef AURORA_GFX_DEBUG_GROUPS
|
||||
aurora::gfx::g_debugGroupStack.emplace_back(label);
|
||||
aurora::gfx::g_debugFrame.groups.emplace_back(label);
|
||||
#endif
|
||||
}
|
||||
void aurora_pop_debug_group() {
|
||||
#ifdef AURORA_GFX_DEBUG_GROUPS
|
||||
if (aurora::gfx::g_debugGroupStack.empty()) {
|
||||
if (aurora::gfx::g_debugFrame.groups.empty()) {
|
||||
aurora::gfx::Log.error("Debug group stack underflowed!");
|
||||
return;
|
||||
}
|
||||
|
||||
aurora::gfx::g_debugGroupStack.pop_back();
|
||||
aurora::gfx::g_debugFrame.groups.pop_back();
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include "staging_capacity.hpp"
|
||||
|
||||
#include "../internal.hpp"
|
||||
#include "../webgpu/gpu.hpp"
|
||||
@@ -394,6 +395,20 @@ wgpu::BindGroup& find_bind_group(BindGroupRef id);
|
||||
wgpu::Sampler& sampler_ref(const wgpu::SamplerDescriptor& descriptor);
|
||||
|
||||
uint32_t align_uniform(uint32_t value);
|
||||
uint64_t staging_uniform_bytes(uint64_t bytes);
|
||||
uint64_t staging_storage_bytes(uint64_t bytes);
|
||||
// Admission does not allocate. A false result requires a producer-side split.
|
||||
// Oversized operations fail before mutating the current draw/pass.
|
||||
bool staging_has_space(const StagingSizes& demand);
|
||||
void ensure_staging_space(const StagingSizes& demand);
|
||||
void split_staging_batch();
|
||||
uint64_t staging_epoch() noexcept;
|
||||
StagingSizes staging_usage() noexcept;
|
||||
StagingSizes staging_high_water() noexcept;
|
||||
uint64_t staging_split_count() noexcept;
|
||||
// Internal integration-test seam: never increases the physical allocation.
|
||||
void set_staging_capacity_limits_for_testing(const StagingSizes& limits);
|
||||
|
||||
|
||||
Vec2<uint32_t> get_render_target_size() noexcept;
|
||||
// Same value as get_render_target_size() outside a render pass, but never
|
||||
|
||||
@@ -92,7 +92,7 @@ struct Params {
|
||||
|
||||
constexpr std::string_view ReversedZBody = R"(
|
||||
fn gx_z24(depth: f32) -> u32 {
|
||||
return min(u32(clamp(depth, 0.0, 1.0) * 16777216.0), 0x00ffffffu);
|
||||
return min(u32(clamp(1.0 - depth, 0.0, 1.0) * 16777215.0 + 0.5), 0x00ffffffu);
|
||||
}
|
||||
)"sv;
|
||||
|
||||
@@ -196,7 +196,8 @@ wgpu::BindGroupLayout create_bind_group_layout(const char* label) {
|
||||
return g_device.CreateBindGroupLayout(&descriptor);
|
||||
}
|
||||
|
||||
Params make_params(wgpu::Extent3D sourceSize, Vec2<uint32_t> dstSize) noexcept {
|
||||
Params make_params(wgpu::Extent3D sourceSize, const FrameMapping& mapping) noexcept {
|
||||
const auto dstSize = mapping.logicalSize;
|
||||
Params params{
|
||||
.dstWidth = dstSize.x,
|
||||
.dstHeight = dstSize.y,
|
||||
@@ -204,16 +205,17 @@ Params make_params(wgpu::Extent3D sourceSize, Vec2<uint32_t> dstSize) noexcept {
|
||||
.srcHeight = sourceSize.height,
|
||||
};
|
||||
|
||||
if (gx::g_gxState.viewportPolicy == AURORA_VIEWPORT_NATIVE) {
|
||||
if (mapping.viewportPolicy == AURORA_VIEWPORT_NATIVE) {
|
||||
return params;
|
||||
}
|
||||
|
||||
const auto logicalSize = vi::configured_fb_size();
|
||||
const auto logicalSize = mapping.logicalSize;
|
||||
if (logicalSize.x == 0 || logicalSize.y == 0 || sourceSize.width == 0 || sourceSize.height == 0) {
|
||||
return params;
|
||||
}
|
||||
|
||||
const bool stretch = gx::g_gxState.viewportPolicy == AURORA_VIEWPORT_STRETCH;
|
||||
const bool stretch = mapping.viewportPolicy == AURORA_VIEWPORT_STRETCH ||
|
||||
mapping.viewportPolicy == AURORA_VIEWPORT_16_9;
|
||||
const float scaleX = static_cast<float>(sourceSize.width) / static_cast<float>(logicalSize.x);
|
||||
const float scaleY = static_cast<float>(sourceSize.height) / static_cast<float>(logicalSize.y);
|
||||
const float scale = std::min(scaleX, scaleY);
|
||||
@@ -336,8 +338,12 @@ void poll() noexcept {
|
||||
}
|
||||
}
|
||||
|
||||
FrameMapping capture_frame_mapping() noexcept {
|
||||
return {vi::configured_fb_size(), gx::g_gxState.viewportPolicy};
|
||||
}
|
||||
|
||||
void encode_frame_snapshot(const wgpu::CommandEncoder& cmd, const wgpu::TextureView& depthView,
|
||||
wgpu::Extent3D sourceSize, uint32_t msaaSamples) noexcept {
|
||||
wgpu::Extent3D sourceSize, uint32_t msaaSamples, const FrameMapping& mapping) noexcept {
|
||||
ZoneScoped;
|
||||
const auto now = Clock::now();
|
||||
{
|
||||
@@ -349,7 +355,7 @@ void encode_frame_snapshot(const wgpu::CommandEncoder& cmd, const wgpu::TextureV
|
||||
g_nextSnapshotTime = now + SnapshotInterval;
|
||||
}
|
||||
|
||||
const auto dstSize = vi::configured_fb_size();
|
||||
const auto dstSize = mapping.logicalSize;
|
||||
if (!depthView || dstSize.x == 0 || dstSize.y == 0 || sourceSize.width == 0 || sourceSize.height == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -357,7 +363,7 @@ void encode_frame_snapshot(const wgpu::CommandEncoder& cmd, const wgpu::TextureV
|
||||
Log.fatal("Depth Peek from multisampled EFB targets is not supported");
|
||||
}
|
||||
|
||||
const Params params = make_params(sourceSize, dstSize);
|
||||
const Params params = make_params(sourceSize, mapping);
|
||||
wgpu::Buffer storageBuffer;
|
||||
wgpu::Buffer readbackBuffer;
|
||||
wgpu::Buffer paramsBuffer;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.hpp"
|
||||
#include <dolphin/gx/GXAurora.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
@@ -13,8 +14,15 @@ void request_snapshot() noexcept;
|
||||
bool read_latest(uint16_t x, uint16_t y, uint32_t& z) noexcept;
|
||||
void poll() noexcept;
|
||||
|
||||
// Captured before SEALED; the producer may configure the next frame during encode.
|
||||
struct FrameMapping {
|
||||
Vec2<uint32_t> logicalSize{};
|
||||
AuroraViewportPolicy viewportPolicy = AURORA_VIEWPORT_FIT;
|
||||
};
|
||||
FrameMapping capture_frame_mapping() noexcept;
|
||||
|
||||
void encode_frame_snapshot(const wgpu::CommandEncoder& cmd, const wgpu::TextureView& depthView,
|
||||
wgpu::Extent3D sourceSize, uint32_t msaaSamples) noexcept;
|
||||
wgpu::Extent3D sourceSize, uint32_t msaaSamples, const FrameMapping& mapping) noexcept;
|
||||
void after_submit() noexcept;
|
||||
|
||||
namespace testing {
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
@@ -27,7 +29,7 @@ using webgpu::g_instance;
|
||||
constexpr size_t kAsyncReadbackMaxBytes = 256;
|
||||
// Each destination keeps its readback buffer forever. Only a handful are expected, and the cap
|
||||
// stops an unexpected pattern of one-shot destinations from leaking GPU buffers.
|
||||
constexpr size_t kMaxAsyncSlots = 32;
|
||||
constexpr size_t kMaxAsyncSlots = MaxAsyncReadbackSlots;
|
||||
|
||||
struct PendingCopy {
|
||||
void* dest = nullptr;
|
||||
@@ -37,6 +39,7 @@ struct PendingCopy {
|
||||
TextureHandle texture;
|
||||
TextureHandle nativeTexture;
|
||||
Range nativeBlitUniform;
|
||||
uint64_t nativeUniformEpoch = 0;
|
||||
};
|
||||
|
||||
struct Download {
|
||||
@@ -81,6 +84,7 @@ std::vector<PendingCopy> g_asyncSealed;
|
||||
std::mutex g_asyncMutex;
|
||||
std::unordered_map<void*, AsyncSlot> g_asyncSlots;
|
||||
uint32_t g_asyncMapsInFlight = 0;
|
||||
uint64_t g_asyncGeneration = 1;
|
||||
|
||||
uint32_t align_to(uint32_t value, uint32_t alignment) noexcept { return (value + alignment - 1) & ~(alignment - 1); }
|
||||
|
||||
@@ -90,10 +94,10 @@ void ensure_native_texture(PendingCopy& pending, TextureHandle* cache = nullptr)
|
||||
if (pending.texture->size.width == pending.width && pending.texture->size.height == pending.height) {
|
||||
return;
|
||||
}
|
||||
if (pending.nativeTexture && pending.nativeUniformEpoch == staging_epoch()) return;
|
||||
if (pending.nativeTexture) {
|
||||
return;
|
||||
}
|
||||
if (cache != nullptr && *cache && (*cache)->size.width == pending.width &&
|
||||
// Keep the texture; its old staging range belongs to a submitted batch.
|
||||
} else if (cache != nullptr && *cache && (*cache)->size.width == pending.width &&
|
||||
(*cache)->size.height == pending.height) {
|
||||
pending.nativeTexture = *cache;
|
||||
} else {
|
||||
@@ -102,10 +106,12 @@ void ensure_native_texture(PendingCopy& pending, TextureHandle* cache = nullptr)
|
||||
*cache = pending.nativeTexture;
|
||||
}
|
||||
}
|
||||
// The shared blit shader clamps Y to flags.z/w; preserve the full source.
|
||||
const std::array nativeBlitUniform{
|
||||
0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 64.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
|
||||
0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 64.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,
|
||||
};
|
||||
pending.nativeBlitUniform = push_uniform(nativeBlitUniform);
|
||||
pending.nativeUniformEpoch = staging_epoch();
|
||||
}
|
||||
|
||||
void encode_native_blit(const wgpu::CommandEncoder& encoder, const PendingCopy& pending) noexcept {
|
||||
@@ -125,16 +131,17 @@ HostPixelOrder texture_pixel_order(const TextureHandle& texture) noexcept {
|
||||
return texture->format == wgpu::TextureFormat::BGRA8Unorm ? HostPixelOrder::BGRA : HostPixelOrder::RGBA;
|
||||
}
|
||||
|
||||
void complete_async_slot(void* dest, wgpu::MapAsyncStatus status, wgpu::StringView message) noexcept {
|
||||
void complete_async_slot(void* dest, uint64_t generation, wgpu::MapAsyncStatus status,
|
||||
wgpu::StringView message) noexcept {
|
||||
std::lock_guard lock{g_asyncMutex};
|
||||
if (g_asyncMapsInFlight > 0) {
|
||||
--g_asyncMapsInFlight;
|
||||
}
|
||||
if (generation != g_asyncGeneration) return;
|
||||
const auto it = g_asyncSlots.find(dest);
|
||||
if (it == g_asyncSlots.end()) {
|
||||
return;
|
||||
}
|
||||
auto& slot = it->second;
|
||||
if (slot.state != AsyncState::MapPending) return;
|
||||
if (g_asyncMapsInFlight > 0) --g_asyncMapsInFlight;
|
||||
if (status == wgpu::MapAsyncStatus::Success) {
|
||||
const auto* pixels = static_cast<const uint8_t*>(slot.buffer.GetConstMappedRange(0, slot.bufferSize));
|
||||
if (pixels != nullptr) {
|
||||
@@ -227,7 +234,14 @@ bool has_pending(void* dest) noexcept {
|
||||
[dest](const Download& download) { return download.copy.dest == dest; });
|
||||
}
|
||||
|
||||
bool prepare_downloads(void* dest) noexcept {
|
||||
bool prepare_downloads(void* dest) {
|
||||
uint64_t copies = 0;
|
||||
for (const auto& pending : g_pending) {
|
||||
if (dest != nullptr && pending.dest != dest) continue;
|
||||
if (pending.texture->size.width != pending.width || pending.texture->size.height != pending.height) ++copies;
|
||||
}
|
||||
// Reserve all copies, even already-prepared ones: a split retires their ranges.
|
||||
ensure_staging_space({0, copies * staging_uniform_bytes(48), 0, 0});
|
||||
bool found = false;
|
||||
for (auto& pending : g_pending) {
|
||||
if (dest != nullptr && pending.dest != dest) continue;
|
||||
@@ -290,15 +304,35 @@ void encode_downloads(const wgpu::CommandEncoder& encoder, void* dest) noexcept
|
||||
bool complete_downloads() noexcept {
|
||||
bool success = true;
|
||||
for (auto& download : g_downloads) {
|
||||
wgpu::MapAsyncStatus mapStatus = wgpu::MapAsyncStatus::CallbackCancelled;
|
||||
wgpu::StringView mapMessage{};
|
||||
// WaitAny may time out before Dawn delivers cancellation. The callback must
|
||||
// own its result rather than retaining references to this stack frame.
|
||||
struct MapResult {
|
||||
std::mutex mutex;
|
||||
wgpu::MapAsyncStatus status = wgpu::MapAsyncStatus::CallbackCancelled;
|
||||
std::string message;
|
||||
};
|
||||
const auto result = std::make_shared<MapResult>();
|
||||
const auto future =
|
||||
download.buffer.MapAsync(wgpu::MapMode::Read, 0, download.bufferSize, wgpu::CallbackMode::WaitAnyOnly,
|
||||
[&mapStatus, &mapMessage](wgpu::MapAsyncStatus status, wgpu::StringView message) {
|
||||
mapStatus = status;
|
||||
mapMessage = message;
|
||||
[result](wgpu::MapAsyncStatus status, wgpu::StringView message) {
|
||||
std::lock_guard lock{result->mutex};
|
||||
result->status = status;
|
||||
if (message.data != nullptr) {
|
||||
size_t length = 0;
|
||||
while (length < 512 && length < message.length && message.data[length] != '\0') {
|
||||
++length;
|
||||
}
|
||||
result->message.assign(message.data, length);
|
||||
}
|
||||
});
|
||||
const auto waitStatus = g_instance.WaitAny(future, 5000000000);
|
||||
wgpu::MapAsyncStatus mapStatus;
|
||||
std::string mapMessage;
|
||||
{
|
||||
std::lock_guard lock{result->mutex};
|
||||
mapStatus = result->status;
|
||||
mapMessage = result->message;
|
||||
}
|
||||
if (waitStatus != wgpu::WaitStatus::Success || mapStatus != wgpu::MapAsyncStatus::Success) {
|
||||
Log.error("EFB RAM readback failed wait={} map={} message={}", magic_enum::enum_name(waitStatus),
|
||||
magic_enum::enum_name(mapStatus), mapMessage);
|
||||
@@ -412,6 +446,7 @@ void after_submit() noexcept {
|
||||
void* dest;
|
||||
wgpu::Buffer buffer;
|
||||
uint64_t bufferSize;
|
||||
uint64_t generation;
|
||||
};
|
||||
std::vector<PendingMap> pendingMaps;
|
||||
{
|
||||
@@ -422,14 +457,15 @@ void after_submit() noexcept {
|
||||
}
|
||||
slot.state = AsyncState::MapPending;
|
||||
++g_asyncMapsInFlight;
|
||||
pendingMaps.push_back({dest, slot.buffer, slot.bufferSize});
|
||||
pendingMaps.push_back({dest, slot.buffer, slot.bufferSize, g_asyncGeneration});
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& pending : pendingMaps) {
|
||||
pending.buffer.MapAsync(wgpu::MapMode::Read, 0, pending.bufferSize, wgpu::CallbackMode::AllowSpontaneous,
|
||||
[dest = pending.dest](wgpu::MapAsyncStatus status, wgpu::StringView message) {
|
||||
complete_async_slot(dest, status, message);
|
||||
[dest = pending.dest, generation = pending.generation](wgpu::MapAsyncStatus status,
|
||||
wgpu::StringView message) {
|
||||
complete_async_slot(dest, generation, status, message);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -443,9 +479,15 @@ void abort_async() noexcept { g_asyncSealed.clear(); }
|
||||
void shutdown() noexcept {
|
||||
cancel();
|
||||
g_asyncSealed.clear();
|
||||
std::lock_guard lock{g_asyncMutex};
|
||||
g_asyncSlots.clear();
|
||||
g_asyncMapsInFlight = 0;
|
||||
// Retire callbacks before releasing buffers, and release outside their mutex:
|
||||
// destruction may itself deliver an AllowSpontaneous cancellation callback.
|
||||
decltype(g_asyncSlots) retiredSlots;
|
||||
{
|
||||
std::lock_guard lock{g_asyncMutex};
|
||||
++g_asyncGeneration;
|
||||
retiredSlots.swap(g_asyncSlots);
|
||||
g_asyncMapsInFlight = 0;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace aurora::gfx::efb_ram
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
|
||||
namespace aurora::gfx::efb_ram {
|
||||
|
||||
inline constexpr size_t MaxAsyncReadbackSlots = 32;
|
||||
|
||||
void schedule(void* dest, uint32_t width, uint32_t height, GXTexFmt format, TextureHandle texture) noexcept;
|
||||
bool has_pending(void* dest = nullptr) noexcept;
|
||||
bool prepare_downloads(void* dest = nullptr) noexcept;
|
||||
bool prepare_downloads(void* dest = nullptr);
|
||||
void encode_downloads(const wgpu::CommandEncoder& encoder, void* dest = nullptr) noexcept;
|
||||
bool complete_downloads() noexcept;
|
||||
void cancel() noexcept;
|
||||
|
||||
@@ -396,6 +396,7 @@ static PendingPipeline* touch_pending_pipeline(PipelineRef hash, bool prioritize
|
||||
|
||||
g_priorityPipelines.emplace_back(std::move(*backgroundIt));
|
||||
g_backgroundPipelines.erase(backgroundIt);
|
||||
g_pipelineCv.notify_all();
|
||||
return &g_priorityPipelines.back();
|
||||
}
|
||||
|
||||
@@ -530,7 +531,8 @@ static PipelineRef find_pipeline_impl(ShaderType type, const PipelineConfig& con
|
||||
}
|
||||
|
||||
if (notifyWorker) {
|
||||
g_pipelineCv.notify_one();
|
||||
// Compiler workers and renderer waiters share this condition variable.
|
||||
g_pipelineCv.notify_all();
|
||||
}
|
||||
if (notifyWaiters) {
|
||||
g_pipelineCv.notify_all();
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace aurora::gfx {
|
||||
// Byte counts after each allocation's own trailing alignment, in V/U/I/S order.
|
||||
using StagingSizes = std::array<uint64_t, 4>;
|
||||
class StagingCapacityError : public std::runtime_error {
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
struct StagingBatchFull {};
|
||||
inline uint64_t staging_padded(uint64_t bytes, uint64_t alignment) {
|
||||
if (!bytes) return alignment;
|
||||
const auto remainder = alignment ? bytes % alignment : 0;
|
||||
const auto padding = remainder ? alignment - remainder : 0;
|
||||
if (bytes > UINT64_MAX - padding) throw StagingCapacityError("Staging allocation size overflow");
|
||||
return bytes + padding;
|
||||
}
|
||||
inline bool staging_fits(const StagingSizes& used, const StagingSizes& demand,
|
||||
const StagingSizes& tail, const StagingSizes& capacity) noexcept {
|
||||
for (unsigned i = 0; i < used.size(); ++i) {
|
||||
const auto limit = capacity[i] < UINT32_MAX ? capacity[i] : UINT32_MAX;
|
||||
// The final GPU copy rounds to four bytes. Subtractions avoid wraparound.
|
||||
const auto alignedLimit = limit & ~uint64_t(3);
|
||||
if (tail[i] > alignedLimit || used[i] > alignedLimit - tail[i] ||
|
||||
demand[i] > alignedLimit - tail[i] - used[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace aurora::gfx
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
|
||||
namespace aurora::gfx {
|
||||
|
||||
enum class BufferMapState { Unmapped, Mapping, Mapped };
|
||||
|
||||
// The renderer owns request/reset; Dawn may complete a request on another thread.
|
||||
// An old callback must never publish readiness for a different staging slot.
|
||||
class StagingMapState {
|
||||
mutable std::mutex mutex_;
|
||||
std::condition_variable changed_;
|
||||
uint64_t generation_ = 0;
|
||||
BufferMapState state_ = BufferMapState::Unmapped;
|
||||
|
||||
public:
|
||||
uint64_t request() {
|
||||
std::lock_guard lock(mutex_);
|
||||
if (state_ != BufferMapState::Unmapped) return 0;
|
||||
state_ = BufferMapState::Mapping;
|
||||
return ++generation_;
|
||||
}
|
||||
|
||||
bool complete(uint64_t generation, BufferMapState state) {
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
if (generation != generation_ || state_ != BufferMapState::Mapping) return false;
|
||||
state_ = state;
|
||||
}
|
||||
changed_.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
++generation_;
|
||||
state_ = BufferMapState::Unmapped;
|
||||
}
|
||||
changed_.notify_all();
|
||||
}
|
||||
|
||||
BufferMapState state() const {
|
||||
std::lock_guard lock(mutex_);
|
||||
return state_;
|
||||
}
|
||||
|
||||
void wait_for_progress() {
|
||||
std::unique_lock lock(mutex_);
|
||||
// ProcessEvents is still serviced between waits for implementations that
|
||||
// need it. A spontaneous completion wakes immediately, without polling.
|
||||
changed_.wait_for(lock, std::chrono::milliseconds(1),
|
||||
[&] { return state_ != BufferMapState::Mapping; });
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace aurora::gfx
|
||||
@@ -137,7 +137,7 @@ fn gx_z24_at_coord(unclamped_coord: vec2i) -> u32 {
|
||||
let tex_size = vec2i(textureDimensions(src));
|
||||
let coord = clamp(unclamped_coord, vec2i(0), tex_size - vec2i(1));
|
||||
let depth = textureLoad(src, coord, 0);
|
||||
return min(u32(clamp(depth, 0.0, 1.0) * 16777216.0), 0x00ffffffu);
|
||||
return min(u32(clamp(1.0 - depth, 0.0, 1.0) * 16777215.0 + 0.5), 0x00ffffffu);
|
||||
}
|
||||
)"s
|
||||
: R"(
|
||||
@@ -373,7 +373,7 @@ static wgpu::BindGroupLayout g_depthBindGroupLayout;
|
||||
static wgpu::Sampler g_nearestSampler;
|
||||
static wgpu::Sampler g_linearSampler;
|
||||
static absl::flat_hash_map<GXTexFmt, wgpu::RenderPipeline> g_pipelines;
|
||||
static wgpu::RenderPipeline g_blitPipeline;
|
||||
static absl::flat_hash_map<wgpu::TextureFormat, wgpu::RenderPipeline> g_blitPipelines;
|
||||
|
||||
static wgpu::RenderPipeline create_pipeline(const ConvPipeline& conv, const std::string_view shaderPreamble,
|
||||
const wgpu::BindGroupLayout& bindGroupLayout) {
|
||||
@@ -487,9 +487,12 @@ void initialize() {
|
||||
};
|
||||
g_depthBindGroupLayout = g_device.CreateBindGroupLayout(&depthBindGroupLayoutDescriptor);
|
||||
|
||||
g_blitPipeline = create_pipeline(
|
||||
{GX_TF_RGBA8, FragPassthrough, webgpu::g_graphicsConfig.surfaceConfiguration.format, "TexCopyConv Blit"},
|
||||
ShaderPreamble, g_bindGroupLayout);
|
||||
// Native RAM readback uses RGBA even when the EFB/surface uses BGRA.
|
||||
// Build both variants here; frame workers only read the completed map.
|
||||
for (const auto format : {wgpu::TextureFormat::RGBA8Unorm, wgpu::TextureFormat::BGRA8Unorm}) {
|
||||
g_blitPipelines[format] = create_pipeline(
|
||||
{GX_TF_RGBA8, FragPassthrough, format, "TexCopyConv Blit"}, ShaderPreamble, g_bindGroupLayout);
|
||||
}
|
||||
for (const auto& conv : ConvPipelines) {
|
||||
g_pipelines[conv.fmt] = create_pipeline(conv, ShaderPreamble, g_bindGroupLayout);
|
||||
if (conv.outputFormat != to_wgpu(conv.fmt)) {
|
||||
@@ -520,7 +523,7 @@ void initialize() {
|
||||
|
||||
void shutdown() {
|
||||
g_pipelines.clear();
|
||||
g_blitPipeline = {};
|
||||
g_blitPipelines.clear();
|
||||
g_bindGroupLayout = {};
|
||||
g_depthBindGroupLayout = {};
|
||||
g_nearestSampler = {};
|
||||
@@ -602,6 +605,12 @@ void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req) {
|
||||
execute(cmd, req, it->second);
|
||||
}
|
||||
|
||||
void blit(const wgpu::CommandEncoder& cmd, const ConvRequest& req) { execute(cmd, req, g_blitPipeline); }
|
||||
void blit(const wgpu::CommandEncoder& cmd, const ConvRequest& req) {
|
||||
const auto it = g_blitPipelines.find(req.dst->format);
|
||||
if (it == g_blitPipelines.end()) {
|
||||
Log.fatal("Unsupported blit destination format {}", static_cast<int>(req.dst->format));
|
||||
}
|
||||
execute(cmd, req, it->second);
|
||||
}
|
||||
|
||||
} // namespace aurora::gfx::tex_copy_conv
|
||||
|
||||
@@ -553,7 +553,7 @@ void build_index() noexcept {
|
||||
continue;
|
||||
}
|
||||
|
||||
s_replacementIndex.try_emplace(*parsed, path);
|
||||
s_replacementIndex.try_emplace(*parsed, ReplacementIndexEntry{path});
|
||||
}
|
||||
|
||||
Log.info("Indexed {} texture replacements", s_replacementIndex.size());
|
||||
|
||||
@@ -30,10 +30,11 @@ using IndexBuffer = std::vector<u16>;
|
||||
static u32 prepare_idx_template(IndexBuffer& buf, GXPrimitive prim, u16 vtxCount) {
|
||||
size_t writePos = 0;
|
||||
if (prim == GX_QUADS) {
|
||||
// Retain the existing incomplete-quad behavior: every started group emits a complete six-index quad.
|
||||
buf.resize(((static_cast<u32>(vtxCount) + 3u) / 4u) * 6u);
|
||||
// GX renders a three-vertex remainder as a triangle. One/two are ignored.
|
||||
const u32 completeVertices = static_cast<u32>(vtxCount) & ~3u;
|
||||
buf.resize((completeVertices / 4u) * 6u + (vtxCount % 4u == 3u ? 3u : 0u));
|
||||
|
||||
for (u16 v = 0; v < vtxCount; v += 4) {
|
||||
for (u32 v = 0; v < completeVertices; v += 4) {
|
||||
const u16 idx0 = v;
|
||||
const u16 idx1 = static_cast<u16>(v + 1);
|
||||
const u16 idx2 = static_cast<u16>(v + 2);
|
||||
@@ -45,15 +46,21 @@ static u32 prepare_idx_template(IndexBuffer& buf, GXPrimitive prim, u16 vtxCount
|
||||
buf[writePos++] = idx3;
|
||||
buf[writePos++] = idx0;
|
||||
}
|
||||
if (vtxCount % 4u == 3u) {
|
||||
buf[writePos++] = static_cast<u16>(completeVertices);
|
||||
buf[writePos++] = static_cast<u16>(completeVertices + 1u);
|
||||
buf[writePos++] = static_cast<u16>(completeVertices + 2u);
|
||||
}
|
||||
} else if (prim == GX_TRIANGLES) {
|
||||
buf.resize(vtxCount);
|
||||
for (u16 v = 0; v < vtxCount; ++v) {
|
||||
const u32 completeVertices = (static_cast<u32>(vtxCount) / 3u) * 3u;
|
||||
buf.resize(completeVertices);
|
||||
for (u32 v = 0; v < completeVertices; ++v) {
|
||||
buf[writePos++] = v;
|
||||
}
|
||||
} else if (prim == GX_TRIANGLEFAN) {
|
||||
const u32 indexCount = vtxCount <= 3 ? vtxCount : 3u + (static_cast<u32>(vtxCount) - 3u) * 3u;
|
||||
const u32 indexCount = vtxCount < 3 ? 0u : (static_cast<u32>(vtxCount) - 2u) * 3u;
|
||||
buf.resize(indexCount);
|
||||
for (u16 v = 0; v < vtxCount; ++v) {
|
||||
for (u32 v = 0; indexCount != 0 && v < vtxCount; ++v) {
|
||||
if (v < 3) {
|
||||
buf[writePos++] = v;
|
||||
continue;
|
||||
@@ -63,9 +70,9 @@ static u32 prepare_idx_template(IndexBuffer& buf, GXPrimitive prim, u16 vtxCount
|
||||
buf[writePos++] = v;
|
||||
}
|
||||
} else if (prim == GX_TRIANGLESTRIP) {
|
||||
const u32 indexCount = vtxCount <= 3 ? vtxCount : 3u + (static_cast<u32>(vtxCount) - 3u) * 3u;
|
||||
const u32 indexCount = vtxCount < 3 ? 0u : (static_cast<u32>(vtxCount) - 2u) * 3u;
|
||||
buf.resize(indexCount);
|
||||
for (u16 v = 0; v < vtxCount; ++v) {
|
||||
for (u32 v = 0; indexCount != 0 && v < vtxCount; ++v) {
|
||||
if (v < 3) {
|
||||
buf[writePos++] = v;
|
||||
continue;
|
||||
@@ -88,6 +95,13 @@ static u32 prepare_idx_template(IndexBuffer& buf, GXPrimitive prim, u16 vtxCount
|
||||
return static_cast<u32>(writePos);
|
||||
}
|
||||
|
||||
// Empty/incomplete draws consume FIFO bytes but cannot produce a primitive.
|
||||
static bool has_complete_primitive(GXPrimitive prim, u16 count) {
|
||||
if (prim == GX_POINTS) return count >= 1;
|
||||
if (prim == GX_LINES || prim == GX_LINESTRIP) return count >= 2;
|
||||
return count >= 3;
|
||||
}
|
||||
|
||||
// GX FIFO opcodes - use CP_ prefix to avoid clashing with GXCommandList.h macros
|
||||
static constexpr u8 CP_CMD_NOP = GX_NOP;
|
||||
static constexpr u8 CP_CMD_LOAD_CP_REG = GX_LOAD_CP_REG;
|
||||
@@ -466,13 +480,14 @@ static void handle_xf(const u8* data, u32& pos, u32 size, bool bigEndian);
|
||||
static bool handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndian);
|
||||
static bool handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian);
|
||||
|
||||
void process(const u8* data, u32 size, bool bigEndian) {
|
||||
uint32_t process(const u8* data, u32 size, bool bigEndian) {
|
||||
ZoneScoped;
|
||||
// Everything decoded here mutates renderer state (GX state, the recorded command lists and the mapped staging buffers), so take the renderer GPU mutex once for the whole drain rather than once per draw command.
|
||||
std::lock_guard gpuLock(aurora::renderer_gpu_mutex());
|
||||
u32 pos = 0;
|
||||
|
||||
while (pos < size) {
|
||||
const u32 commandStart = pos;
|
||||
u8 cmd = data[pos++];
|
||||
u8 opcode = cmd & CP_OPCODE_MASK;
|
||||
// Log.warn("Processing opcode {:02x} at pos {} (size {})", opcode, pos - 1, size);
|
||||
@@ -551,12 +566,16 @@ void process(const u8* data, u32 size, bool bigEndian) {
|
||||
for (int i = GX_VA_POS; i <= GX_VA_TEX7; ++i) {
|
||||
g_gxState.arrays[i].cachedRange = {};
|
||||
}
|
||||
// A merged draw retains its previous array uploads. Force a new draw so
|
||||
// handle_draw_unmerged observes the invalidation and uploads fresh data.
|
||||
// Pipeline configuration itself did not change.
|
||||
g_gxState.stateDirty = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case GX_LOAD_AURORA: {
|
||||
if (!handle_aurora(data, pos, size, bigEndian)) {
|
||||
return;
|
||||
return size;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -564,8 +583,10 @@ void process(const u8* data, u32 size, bool bigEndian) {
|
||||
default:
|
||||
// Draw commands occupy the full 0x80-0xBF range.
|
||||
if (is_draw_cmd(cmd)) {
|
||||
if (!handle_draw(cmd, data, pos, size, bigEndian)) {
|
||||
return;
|
||||
try {
|
||||
if (!handle_draw(cmd, data, pos, size, bigEndian)) return size;
|
||||
} catch (const gfx::StagingBatchFull&) {
|
||||
return commandStart;
|
||||
}
|
||||
} else {
|
||||
static u32 unknownLogCount = 0;
|
||||
@@ -588,6 +609,7 @@ void process(const u8* data, u32 size, bool bigEndian) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
// Helper to extract bit fields from a 32-bit register
|
||||
@@ -1848,6 +1870,10 @@ static u32 calculate_last_vtx_size(GXVtxFmt fmt) {
|
||||
|
||||
g_gxState.lastVtxFmt = fmt;
|
||||
g_gxState.lastVtxSize = vtxSize;
|
||||
// The format is selected by the draw opcode, without a register write.
|
||||
// Even equal-stride formats may decode bytes differently, so do not merge
|
||||
// into a draw using the previous format's shader and uniform layout.
|
||||
g_gxState.stateDirty = true;
|
||||
|
||||
return vtxSize;
|
||||
}
|
||||
@@ -2080,6 +2106,22 @@ static const CachedPipelineState& resolve_pipeline_state(GXPrimitive prim, GXVtx
|
||||
return state;
|
||||
}
|
||||
|
||||
static bool admit_draw(GXPrimitive prim, GXVtxFmt fmt, u16 count, uint32_t vertexBytes, bool merged = false) {
|
||||
const auto& indexTemplate = cached_index_template(prim, count);
|
||||
gfx::StagingSizes demand{vertexBytes, 0, indexTemplate.indices.size() * sizeof(u16), 0};
|
||||
if (merged) return gfx::staging_has_space(demand);
|
||||
const auto& info = resolve_pipeline_state(prim, fmt).shaderInfo;
|
||||
demand[1] = gfx::staging_uniform_bytes(info.uniformSize);
|
||||
if (frame_interpolation_identity_needed() && frame_interpolation_replay_safe())
|
||||
demand[1] *= 1 + MaxInterpolatedFrames;
|
||||
for (int i = GX_VA_POS; i <= GX_VA_TEX7; ++i) {
|
||||
if ((g_gxState.vtxDesc[i] == GX_INDEX8 || g_gxState.vtxDesc[i] == GX_INDEX16) &&
|
||||
g_gxState.arrays[i].cachedRange.size == 0)
|
||||
demand[3] += gfx::staging_storage_bytes(g_gxState.arrays[i].size);
|
||||
}
|
||||
return gfx::staging_has_space(demand);
|
||||
}
|
||||
|
||||
bool submit_raw_draw(GXPrimitive prim, GXVtxFmt fmt, const uint8_t* vertices, uint16_t vtxCount,
|
||||
uint32_t vertexBytes) {
|
||||
ZoneScoped;
|
||||
@@ -2112,8 +2154,17 @@ bool submit_raw_draw(GXPrimitive prim, GXVtxFmt fmt, const uint8_t* vertices, ui
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!has_complete_primitive(prim, vtxCount)) return true;
|
||||
|
||||
// This entry point bypasses process(), so it owns the renderer lock itself.
|
||||
std::lock_guard gpuLock(aurora::renderer_gpu_mutex());
|
||||
std::unique_lock gpuLock(aurora::renderer_gpu_mutex());
|
||||
if (!admit_draw(prim, fmt, vtxCount, vertexBytes)) {
|
||||
gpuLock.unlock();
|
||||
gfx::split_staging_batch();
|
||||
gpuLock.lock();
|
||||
if (!admit_draw(prim, fmt, vtxCount, vertexBytes))
|
||||
throw gfx::StagingCapacityError("Raw draw does not fit after capacity submission");
|
||||
}
|
||||
const gfx::Range vertRange = gfx::push_verts(vertices, vertexBytes);
|
||||
const bool interpolationIdentityActive = frame_interpolation_identity_needed();
|
||||
const PnMtxUsage matrixUsage = interpolationIdentityActive
|
||||
@@ -2151,17 +2202,32 @@ static bool handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndi
|
||||
}
|
||||
|
||||
|
||||
// Push raw vertex data to buffer
|
||||
const uint8_t* vertices = data + pos;
|
||||
gfx::Range vertRange = gfx::push_verts(vertices, totalVtxBytes);
|
||||
pos += totalVtxBytes;
|
||||
if (!has_complete_primitive(prim, vtxCount)) {
|
||||
pos += totalVtxBytes;
|
||||
return true;
|
||||
}
|
||||
|
||||
DrawData* mergeTarget = nullptr;
|
||||
// Decide admission before allocating anything. The merged path needs only
|
||||
// vertices and indices; it must not resolve pipelines or upload arrays.
|
||||
// Try to merge with previous draw call
|
||||
if (!g_gxState.stateDirty) LIKELY {
|
||||
auto* lastDraw = gfx::get_last_draw_command<DrawData>();
|
||||
// Only if the previous draw call was a single instance draw (no lines/points handling)
|
||||
// Expanded lines/points have different vertex interpretation even with one instance.
|
||||
// Triangle-list output has no restart index; index 65535 is usable.
|
||||
// Overflow would address earlier vertices instead of the appended geometry.
|
||||
if (lastDraw != nullptr && prim != GX_LINES && prim != GX_LINESTRIP && prim != GX_POINTS &&
|
||||
lastDraw->instanceCount == 1) LIKELY {
|
||||
!lastDraw->expandedPrimitive && lastDraw->instanceCount == 1 &&
|
||||
uint64_t(lastDraw->vtxCount) +
|
||||
vtxCount <= 65536u) LIKELY {
|
||||
mergeTarget = lastDraw;
|
||||
}
|
||||
}
|
||||
if (!admit_draw(prim, fmt, vtxCount, totalVtxBytes, mergeTarget != nullptr)) throw gfx::StagingBatchFull{};
|
||||
const uint8_t* vertices = data + pos;
|
||||
gfx::Range vertRange = gfx::push_verts(vertices, totalVtxBytes);
|
||||
pos += totalVtxBytes;
|
||||
if (auto* lastDraw = mergeTarget) {
|
||||
const auto& indexTemplate = cached_index_template(prim, vtxCount);
|
||||
const auto indices = offset_index_template(indexTemplate, lastDraw->vtxCount);
|
||||
const u32 numIndices = indexTemplate.indexCount;
|
||||
@@ -2182,7 +2248,6 @@ static bool handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndi
|
||||
extend_interpolation_draw(pn_mtx_mask(vertices, vtxCount, vtxSize));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const bool interpolationIdentityActive = frame_interpolation_identity_needed();
|
||||
@@ -2278,6 +2343,7 @@ static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount,
|
||||
.vtxCount = vtxCount,
|
||||
.indexCount = numIndices,
|
||||
.instanceCount = instanceCount,
|
||||
.expandedPrimitive = prim == GX_LINES || prim == GX_LINESTRIP || prim == GX_POINTS,
|
||||
.bindGroups = bindGroups,
|
||||
.dstAlpha = pipelineState.dstAlpha,
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace aurora::gx::fifo {
|
||||
void reset_cp_register_cache();
|
||||
|
||||
// Process a buffer of GX FIFO commands
|
||||
void process(const uint8_t* data, uint32_t size, bool bigEndian);
|
||||
uint32_t process(const uint8_t* data, uint32_t size, bool bigEndian);
|
||||
|
||||
// Submit already-packed direct vertex bytes against the current GX state.
|
||||
bool submit_raw_draw(GXPrimitive prim, GXVtxFmt fmt, const uint8_t* vertices, uint16_t vtxCount,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "fifo.hpp"
|
||||
#include "command_processor.hpp"
|
||||
#include "../gfx/common.hpp"
|
||||
#include "../internal.hpp"
|
||||
|
||||
#include <chrono>
|
||||
@@ -81,10 +82,25 @@ void drain() {
|
||||
if (detail::sBufferSize == 0) {
|
||||
return;
|
||||
}
|
||||
process(detail::sBufferData, detail::sBufferSize, true);
|
||||
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);
|
||||
if (count == 0 && retried)
|
||||
throw gfx::StagingCapacityError("FIFO draw does not fit after capacity submission");
|
||||
consumed += count;
|
||||
if (consumed == size) break;
|
||||
// process returned with its renderer lock released. No recursive drain.
|
||||
gfx::split_staging_batch();
|
||||
retried = true;
|
||||
}
|
||||
}
|
||||
|
||||
const uint8_t* get_buffer_data() { return detail::sBufferData; }
|
||||
uint32_t get_buffer_size() { return detail::sBufferSize; }
|
||||
void clear_buffer() {
|
||||
|
||||
@@ -81,6 +81,10 @@ 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();
|
||||
|
||||
@@ -143,6 +143,7 @@ private:
|
||||
};
|
||||
struct FrameTransformSnapshot {
|
||||
Mat4x4<float> projection{};
|
||||
HashType viewportIdentity = 0;
|
||||
Mat3x4<float> position{};
|
||||
Mat3x4<float> normal{};
|
||||
uint16_t usedMatrixMask = 1;
|
||||
@@ -1185,7 +1186,8 @@ void finalize_frame_interpolation() noexcept {
|
||||
if ((transform.usedMatrixMask & (1u << slot)) == 0) {
|
||||
continue;
|
||||
}
|
||||
paletteSlotKeys.push_back({transform.indexedMatrices->slotHash[slot], palette, slot});
|
||||
paletteSlotKeys.push_back({combine_identity(transform.indexedMatrices->slotHash[slot],
|
||||
transform.viewportIdentity), palette, slot});
|
||||
}
|
||||
}
|
||||
std::sort(paletteSlotKeys.begin(), paletteSlotKeys.end(),
|
||||
@@ -1446,10 +1448,21 @@ void extend_interpolation_draw(uint16_t usedPnMtxMask) noexcept {
|
||||
}
|
||||
|
||||
std::array<gfx::Range, MaxInterpolatedFrames> record_interpolation_draw(
|
||||
const FrameInterpolationDrawIdentity& identity, const Mat4x4<float>& projection,
|
||||
const FrameInterpolationDrawIdentity& drawIdentity, const Mat4x4<float>& projection,
|
||||
uint16_t usedPnMtxMask, const InterpolatedUniformLayout& uniformLayout) noexcept {
|
||||
// Split-screen cameras can draw identical meshes in unrelated view spaces.
|
||||
// Scope exact, material-only and sibling-palette history to the guest viewport.
|
||||
// Logical coordinates keep render-scale changes out of the camera identity.
|
||||
const auto& viewport = g_gxState.logicalViewport;
|
||||
const std::array viewportValues{viewport.left, viewport.top, viewport.width,
|
||||
viewport.height, viewport.znear, viewport.zfar};
|
||||
const HashType viewportIdentity = xxh3_hash_s(viewportValues.data(), sizeof(viewportValues));
|
||||
auto identity = drawIdentity;
|
||||
identity.combined = combine_identity(identity.combined, viewportIdentity);
|
||||
identity.pipeline = combine_identity(identity.pipeline, viewportIdentity);
|
||||
FrameTransformSnapshot snapshot{
|
||||
.projection = projection,
|
||||
.viewportIdentity = viewportIdentity,
|
||||
.usedMatrixMask = usedPnMtxMask,
|
||||
};
|
||||
if (uniformLayout.indexedMatrices) {
|
||||
|
||||
@@ -1416,23 +1416,32 @@ static inline GXBlendFactor remove_dst_alpha_usage(GXBlendFactor fac) {
|
||||
}
|
||||
}
|
||||
|
||||
// GX_LEQUAL etc. describe "pass if this pixel is closer than/equal to what's stored" in GX's own
|
||||
// distance terms, independent of how that distance is encoded as a host depth value. Under
|
||||
// UseReversedZ the encoding is flipped (near=1, far=0), so "closer" now corresponds to a *larger*
|
||||
// stored value, not a smaller one - the ordered compare functions (LESS/LEQUAL/GREATER/GEQUAL)
|
||||
// must invert to match, or the depth test silently runs backwards (verified directly: this was
|
||||
// the actual cause of a bug report after the projection/shader half of the reverse-Z fix
|
||||
// eliminated the double-negation that used to accidentally keep the unreversed comparisons
|
||||
// correct - LEQUAL now needs GreaterEqual, not LessEqual, once the encoding it's testing against
|
||||
// is genuinely reversed). Matches upstream aurora's to_compare_function exactly.
|
||||
static inline wgpu::CompareFunction to_compare_function(GXCompare func) {
|
||||
switch (func) {
|
||||
DEFAULT_FATAL("invalid depth fn {}", underlying(func));
|
||||
case GX_NEVER:
|
||||
return wgpu::CompareFunction::Never;
|
||||
case GX_LESS:
|
||||
return wgpu::CompareFunction::Less;
|
||||
return UseReversedZ ? wgpu::CompareFunction::Greater : wgpu::CompareFunction::Less;
|
||||
case GX_EQUAL:
|
||||
return wgpu::CompareFunction::Equal;
|
||||
case GX_LEQUAL:
|
||||
return wgpu::CompareFunction::LessEqual;
|
||||
return UseReversedZ ? wgpu::CompareFunction::GreaterEqual : wgpu::CompareFunction::LessEqual;
|
||||
case GX_GREATER:
|
||||
return wgpu::CompareFunction::Greater;
|
||||
return UseReversedZ ? wgpu::CompareFunction::Less : wgpu::CompareFunction::Greater;
|
||||
case GX_NEQUAL:
|
||||
return wgpu::CompareFunction::NotEqual;
|
||||
case GX_GEQUAL:
|
||||
return wgpu::CompareFunction::GreaterEqual;
|
||||
return UseReversedZ ? wgpu::CompareFunction::LessEqual : wgpu::CompareFunction::GreaterEqual;
|
||||
case GX_ALWAYS:
|
||||
return wgpu::CompareFunction::Always;
|
||||
}
|
||||
|
||||
@@ -436,6 +436,8 @@ struct GXState {
|
||||
u32 pipelineStateGeneration = next_gx_state_epoch();
|
||||
std::array<u32, 0x100> bpRegCache = [] {
|
||||
std::array<u32, 0x100> regs{};
|
||||
// Force the first GEN_MODE decode without changing its masked reset value.
|
||||
regs[0x00] = 0xFF000000;
|
||||
regs[0xFE] = 0x00FFFFFF;
|
||||
return regs;
|
||||
}();
|
||||
@@ -485,7 +487,14 @@ const gfx::TextureBind& get_texture(GXTexMapID id) noexcept;
|
||||
void resolve_sampled_textures(const ShaderInfo& info) noexcept;
|
||||
|
||||
inline float clear_depth_value() {
|
||||
return std::min(static_cast<float>(g_gxState.clearDepth) / 16777216.f, 16777215.f / 16777216.f);
|
||||
// g_gxState.clearDepth is in GX's own distance terms (0 = near, larger = farther), independent of
|
||||
// how UseReversedZ encodes that as a host depth value - it must be re-mapped the same way the
|
||||
// projection matrix and depth compare function are, or the buffer clears to the wrong extreme
|
||||
// (verified directly: matches upstream aurora's clear_depth_value, which does this same inversion
|
||||
// and was the second missing piece alongside to_compare_function's compare-op inversion).
|
||||
const float normalizedDepth =
|
||||
std::min(static_cast<float>(g_gxState.clearDepth) / 16777216.f, 16777215.f / 16777216.f);
|
||||
return UseReversedZ ? (1.f - normalizedDepth) : normalizedDepth;
|
||||
}
|
||||
|
||||
inline bool render_target_has_alpha(GXPixelFmt pixelFmt) noexcept { return pixelFmt == GX_PF_RGBA6_Z24; }
|
||||
|
||||
@@ -13,6 +13,7 @@ struct DrawData {
|
||||
uint32_t vtxCount;
|
||||
uint32_t indexCount;
|
||||
uint32_t instanceCount;
|
||||
bool expandedPrimitive;
|
||||
GXBindGroups bindGroups;
|
||||
uint32_t dstAlpha;
|
||||
};
|
||||
|
||||
@@ -174,6 +174,12 @@ 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:
|
||||
@@ -275,6 +281,16 @@ 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:
|
||||
@@ -993,11 +1009,13 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
|
||||
"\n let clip_base = select(clip_a, clip_b, use_b);"
|
||||
"\n out.pos = vec4f(clip_base.xy + offset_ndc * clip_base.w, clip_base.zw);";
|
||||
}
|
||||
if constexpr (UseReversedZ) {
|
||||
vtxXfrAttrsPre += "\n out.pos.z = -out.pos.z;";
|
||||
} else {
|
||||
vtxXfrAttrsPre += "\n out.pos.z += out.pos.w;";
|
||||
}
|
||||
// The near/far depth correction used to be applied here per-vertex (out.pos.z = -out.pos.z for
|
||||
// reversed, or += out.pos.w for forward), redundantly on top of the same correction already
|
||||
// folded into ubuf.proj by effective_projection() (shader_info.cpp) - applying it twice canceled
|
||||
// out for the common case (any draw where effective_projection() decides to flip), silently
|
||||
// making "reversed" Z behave identically to forward Z. It is now applied exactly once, in the
|
||||
// projection matrix alone (matching upstream aurora commit 1dde08fa: "Move depth correction to
|
||||
// projection matrix"), so nothing needs to happen to out.pos.z here.
|
||||
// GX rasterizes at a 7/12 pixel center when antialiasing is disabled, while WebGPU rasterizes at 1/2.
|
||||
vtxXfrAttrsPre +=
|
||||
"\n let gx_pixel_center_correction = "
|
||||
@@ -1465,7 +1483,14 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
|
||||
textureDependency.texMapId, uvIn);
|
||||
}
|
||||
|
||||
std::string fogDepthExpr = UseReversedZ ? "in.pos.z" : "(1.0 - in.pos.z)";
|
||||
// in.pos.z is the host NDC z (forward: 0=near/1=far; reversed: 1=near/0=far post-fix), but this
|
||||
// expression needs to produce GX's own native distance term (always 0=near/1=far, matching how
|
||||
// g_gxState.clearDepth/clear_depth_value() are interpreted before their own UseReversedZ
|
||||
// inversion) - forward already matches directly; reversed needs the same 1-x flip everything
|
||||
// else reversed-Z-aware uses. This was backwards (verified directly against upstream aurora's
|
||||
// identical expression in build_shader_source), which fed both fog density and the GX_ZT_ADD
|
||||
// z-texture path the wrong distance value.
|
||||
std::string fogDepthExpr = UseReversedZ ? "(1.0 - in.pos.z)" : "in.pos.z";
|
||||
std::string fogZCoordExpr =
|
||||
fmt::format("u32(round(clamp({}, 0.0, 1.0) * 16777216.0))", fogDepthExpr);
|
||||
if (usesZTextureDepth) {
|
||||
@@ -1498,7 +1523,7 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
|
||||
fragmentFn += fmt::format(
|
||||
"\n let oldZ = u32(round(clamp({0}, 0.0, 1.0) * 16777216.0));"
|
||||
"\n ztexCoord = (ztexCoord + oldZ) & 0x00ffffffu;",
|
||||
UseReversedZ ? "in.pos.z" : "(1.0 - in.pos.z)");
|
||||
UseReversedZ ? "(1.0 - in.pos.z)" : "in.pos.z");
|
||||
}
|
||||
fragmentFn += "\n let ztexDepth = f32(ztexCoord) / 16777216.0;";
|
||||
fogZCoordExpr = "ztexCoord";
|
||||
@@ -1639,7 +1664,13 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
|
||||
" @builtin(frag_depth) depth: f32,\n"
|
||||
"};";
|
||||
|
||||
fragmentFn += fmt::format("\n let fragDepth = {}ztexDepth;", UseReversedZ ? "" : "1.0 - ");
|
||||
// ztexDepth is in GX's native distance terms (0=near/1=far, see fogDepthExpr's comment above),
|
||||
// but frag_depth must be written in the same host NDC-z convention in.pos.z itself uses -
|
||||
// forward matches directly (no change), reversed needs the same 1-x flip. This was backwards
|
||||
// the same way fogDepthExpr was (verified by the same derivation, since aurora upstream has no
|
||||
// directly equivalent line here to cross-check against - this z-texture-depth-output path
|
||||
// appears to be specific to this fork).
|
||||
fragmentFn += fmt::format("\n let fragDepth = {}ztexDepth;", UseReversedZ ? "1.0 - " : "");
|
||||
fragmentReturnType = "FragmentOutput";
|
||||
fragmentReturn =
|
||||
" var out: FragmentOutput;\n"
|
||||
@@ -1693,8 +1724,22 @@ fn load_u16(p: ptr<storage, array<u32>>, byte_off: u32, le: bool) -> u32 {{
|
||||
return bswap16(raw, le);
|
||||
}}
|
||||
|
||||
fn load_u24_raw(p: ptr<storage, array<u32>>, byte_off: u32) -> u32 {{
|
||||
let word_idx = byte_off >> 2u;
|
||||
let sub = byte_off & 3u;
|
||||
let word = p[word_idx];
|
||||
// Three bytes at offsets zero or one fit entirely in this word. Do not
|
||||
// access the next word: this attribute may end at the binding boundary.
|
||||
if (sub <= 1u) {{
|
||||
return (word >> (sub * 8u)) & 0x00FFFFFFu;
|
||||
}}
|
||||
let next = p[word_idx + 1u];
|
||||
let shift = sub * 8u;
|
||||
return ((word >> shift) | (next << (32u - shift))) & 0x00FFFFFFu;
|
||||
}}
|
||||
|
||||
fn load_u24(p: ptr<storage, array<u32>>, byte_off: u32, le: bool) -> u32 {{
|
||||
let raw = load_u32_raw(p, byte_off) & 0x00FFFFFFu;
|
||||
let raw = load_u24_raw(p, byte_off);
|
||||
if (le) {{
|
||||
return raw;
|
||||
}}
|
||||
@@ -1734,7 +1779,7 @@ fn raw_fetch_u8_2(p: ptr<storage, array<u32>>, byte_off: u32) -> vec2u {{
|
||||
}}
|
||||
|
||||
fn raw_fetch_u8_3(p: ptr<storage, array<u32>>, byte_off: u32) -> vec3u {{
|
||||
let raw = load_u32_raw(p, byte_off);
|
||||
let raw = load_u24_raw(p, byte_off);
|
||||
return vec3u(
|
||||
extractBits(raw, 0u, 8u),
|
||||
extractBits(raw, 8u, 8u),
|
||||
|
||||
@@ -548,14 +548,22 @@ constexpr size_t kStagedUniformBytes =
|
||||
96 + sizeof(Mat4x4<float>) + sizeof(Mat3x4<float>) * (MaxPostexMtx + MaxPnMtx);
|
||||
|
||||
// The host viewport always receives the normalized GX depth window (render_pass_impl clamps to minDepth <= maxDepth).
|
||||
//
|
||||
// Folds the near/far depth correction the vertex shader used to apply per-vertex directly into the
|
||||
// projection matrix instead (matching upstream aurora commit 1dde08fa, "Move depth correction to
|
||||
// projection matrix") - valid because the correction is a linear combination of the z/w rows, so
|
||||
// applying it once here to the row is equivalent to applying it once per-vertex to the dot product,
|
||||
// and it must be applied exactly once: doing it here AND in the shader (the previous bug) canceled
|
||||
// the negation out for `flip`, silently making "reversed" Z behave identically to forward Z.
|
||||
// `flip` decides which of the two single-application forms this draw needs: true bakes in the
|
||||
// reversed-Z inversion (z' = -z), false bakes in the forward-Z near/far combination (z' = z + w) -
|
||||
// exactly one always applies, never both, and never neither.
|
||||
static Mat4x4<float> effective_projection() noexcept {
|
||||
const auto& vp = g_gxState.renderViewport;
|
||||
const bool flip = (vp.znear <= vp.zfar) == UseReversedZ;
|
||||
Mat4x4<float> proj = g_gxState.proj;
|
||||
if (flip) {
|
||||
for (size_t i = 0; i < 4; ++i) {
|
||||
proj.m2.m[i] = -(proj.m2.m[i] + proj.m3.m[i]);
|
||||
}
|
||||
for (size_t i = 0; i < 4; ++i) {
|
||||
proj.m2.m[i] = flip ? -proj.m2.m[i] : (proj.m2.m[i] + proj.m3.m[i]);
|
||||
}
|
||||
return proj;
|
||||
}
|
||||
|
||||
@@ -75,18 +75,30 @@ void initialize() noexcept {
|
||||
|
||||
void shutdown() noexcept {
|
||||
ZoneScoped;
|
||||
if (g_useSdlRenderer) {
|
||||
ImGui_ImplSDLRenderer3_Shutdown();
|
||||
} else {
|
||||
ImGui_ImplWGPU_Shutdown();
|
||||
// Startup can fail before either backend initializes. A context alone does
|
||||
// not mean its renderer/platform backend owns resources to release.
|
||||
if (ImGui::GetCurrentContext() != nullptr) {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
if (io.BackendRendererUserData != nullptr) {
|
||||
if (g_useSdlRenderer) {
|
||||
ImGui_ImplSDLRenderer3_Shutdown();
|
||||
} else {
|
||||
ImGui_ImplWGPU_Shutdown();
|
||||
}
|
||||
}
|
||||
if (io.BackendPlatformUserData != nullptr) {
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
}
|
||||
ImGui::DestroyContext();
|
||||
}
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
for (const auto& texture : g_sdlTextures) {
|
||||
SDL_DestroyTexture(texture);
|
||||
}
|
||||
g_sdlTextures.clear();
|
||||
g_wgpuTextures.clear();
|
||||
g_useSdlRenderer = false;
|
||||
g_scale = 0.f;
|
||||
g_frameDataBuilt = false;
|
||||
}
|
||||
|
||||
void process_event(const SDL_Event& event) noexcept {
|
||||
|
||||
+138
-56
@@ -14,6 +14,7 @@
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace std::string_view_literals;
|
||||
|
||||
@@ -23,7 +24,7 @@ absl::flat_hash_map<Uint32, GameController> g_GameControllers;
|
||||
|
||||
namespace {
|
||||
constexpr uint32_t kPortPreferencesMagic = SBIG('CPRT');
|
||||
constexpr uint32_t kPortPreferencesVersion = 2;
|
||||
constexpr uint32_t kPortPreferencesVersion = 3;
|
||||
constexpr uint32_t kMaxPersistedStringLength = 256;
|
||||
|
||||
enum class PortPreferenceState : uint8_t {
|
||||
@@ -35,6 +36,8 @@ enum class PortPreferenceState : uint8_t {
|
||||
struct ControllerIdentity {
|
||||
std::string guid;
|
||||
std::string serial;
|
||||
std::string path;
|
||||
int32_t slot = -1;
|
||||
};
|
||||
|
||||
struct PortPreference {
|
||||
@@ -44,6 +47,9 @@ 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) {
|
||||
@@ -83,6 +89,8 @@ 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;
|
||||
}
|
||||
|
||||
@@ -137,12 +145,16 @@ 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, ControllerIdentity& identity) {
|
||||
return read_string(file, identity.guid) && read_string(file, identity.serial);
|
||||
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 write_identity(SDL_IOStream* file, const ControllerIdentity& identity) {
|
||||
return write_string(file, identity.guid) && write_string(file, identity.serial);
|
||||
return write_string(file, identity.guid) && write_string(file, identity.serial) &&
|
||||
write_string(file, identity.path) && write_value(file, identity.slot);
|
||||
}
|
||||
|
||||
bool read_port_preferences_file(std::array<PortPreference, PAD_MAX_CONTROLLERS>& preferences) {
|
||||
@@ -159,13 +171,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 == kPortPreferencesVersion;
|
||||
version >= 2 && 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, identity);
|
||||
read_identity(file, version, identity);
|
||||
if (ok) {
|
||||
preference.state = static_cast<PortPreferenceState>(state);
|
||||
preference.identity = std::move(identity);
|
||||
@@ -225,22 +237,31 @@ void save_port_preferences() {
|
||||
}
|
||||
}
|
||||
|
||||
enum class IdentityMatch {
|
||||
None,
|
||||
Fallback,
|
||||
Exact,
|
||||
};
|
||||
// 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;
|
||||
|
||||
IdentityMatch identity_match(const ControllerIdentity& saved, const ControllerIdentity& current) {
|
||||
int identity_score(const ControllerIdentity& saved, const ControllerIdentity& current) {
|
||||
if (saved.guid.empty()) {
|
||||
return IdentityMatch::None;
|
||||
return 0;
|
||||
}
|
||||
if (saved.guid == current.guid) {
|
||||
return saved.serial.empty() || serial_matches(saved.serial, current.serial) ? IdentityMatch::Exact
|
||||
: IdentityMatch::None;
|
||||
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;
|
||||
}
|
||||
if (!serial_matches(saved.serial, current.serial)) {
|
||||
return IdentityMatch::None;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Attempt to match against VID/PID as a fallback if the GUID changes
|
||||
@@ -250,9 +271,9 @@ IdentityMatch identity_match(const ControllerIdentity& saved, const ControllerId
|
||||
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);
|
||||
return savedVendor != 0 && savedVendor == currentVendor && savedProduct != 0 && savedProduct == currentProduct
|
||||
? IdentityMatch::Fallback
|
||||
: IdentityMatch::None;
|
||||
const bool sameDevice =
|
||||
savedVendor != 0 && savedVendor == currentVendor && savedProduct != 0 && savedProduct == currentProduct;
|
||||
return sameDevice ? kScoreVidPid : 0;
|
||||
}
|
||||
|
||||
void assign_player_index(GameController& controller, int32_t port) {
|
||||
@@ -267,12 +288,6 @@ 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(),
|
||||
@@ -288,45 +303,63 @@ void apply_port_preferences() noexcept {
|
||||
}
|
||||
}
|
||||
|
||||
std::array<Uint32, PAD_MAX_CONTROLLERS> claimedControllers{};
|
||||
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{};
|
||||
size_t claimedCount = 0;
|
||||
for (uint32_t port = 0; port < g_portPreferences.size(); ++port) {
|
||||
const auto& preference = g_portPreferences[port];
|
||||
if (preference.state != PortPreferenceState::Controller) {
|
||||
bool upgraded = false;
|
||||
for (const auto& candidate : candidates) {
|
||||
if (portClaimed[candidate.port] ||
|
||||
std::find(claimedInstances.begin(), claimedInstances.begin() + claimedCount, candidate.instance) !=
|
||||
claimedInstances.begin() + claimedCount) {
|
||||
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));
|
||||
|
||||
Uint32 fallbackInstance = 0;
|
||||
GameController* fallbackController = nullptr;
|
||||
for (auto& [instance, controller] : g_GameControllers) {
|
||||
if (is_instance_claimed(claimedControllers, claimedCount, instance)) {
|
||||
continue;
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
if (saved.slot < 0 && current.slot >= 0) {
|
||||
saved.slot = current.slot;
|
||||
upgraded = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (fallbackController != nullptr) {
|
||||
assign_player_index(*fallbackController, static_cast<int32_t>(port));
|
||||
claimedControllers[claimedCount++] = fallbackInstance;
|
||||
}
|
||||
}
|
||||
if (upgraded) {
|
||||
save_port_preferences();
|
||||
}
|
||||
}
|
||||
|
||||
// SDL only hands out a player index when the device already had a gamepad mapping
|
||||
@@ -401,6 +434,17 @@ 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 ||
|
||||
(SDL_GetGamepadType(ctrl) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO && controller.m_pid == 0x2073)) {
|
||||
controller.m_deadZones.emulateTriggers = false;
|
||||
@@ -412,6 +456,28 @@ 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;
|
||||
}
|
||||
|
||||
@@ -432,6 +498,7 @@ 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);
|
||||
@@ -481,6 +548,13 @@ bool controller_has_rumble(Uint32 instance) noexcept {
|
||||
void controller_rumble(uint32_t instance, uint16_t low_freq_intensity, uint16_t high_freq_intensity,
|
||||
uint16_t duration_ms) noexcept {
|
||||
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
|
||||
// GC Pocket+ has been observed continuing to vibrate after a hard stop;
|
||||
// an ordinary stop cleared it. With GAMECUBE_RUMBLE_BRAKE enabled, SDL
|
||||
// encodes (0, 1) as adapter command 0, whereas (0, 0) sends command 2.
|
||||
// Apply the workaround here so shutdown uses the same stop as PAD calls.
|
||||
if (it->second.m_gameCubeUseOrdinaryStop && low_freq_intensity == 0 && high_freq_intensity == 0) {
|
||||
high_freq_intensity = 1;
|
||||
}
|
||||
SDL_RumbleGamepad(it->second.m_controller, low_freq_intensity, high_freq_intensity, duration_ms);
|
||||
}
|
||||
}
|
||||
@@ -508,6 +582,14 @@ 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 {
|
||||
|
||||
@@ -17,8 +17,12 @@ extern Module Log;
|
||||
struct GameController {
|
||||
SDL_Gamepad* m_controller = nullptr;
|
||||
bool m_isGameCube = false;
|
||||
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,
|
||||
|
||||
@@ -122,7 +122,10 @@ auto underlying(T value) -> std::underlying_type_t<T> {
|
||||
|
||||
#define UNIMPLEMENTED() FATAL("UNIMPLEMENTED: {}", __FUNCTION__)
|
||||
|
||||
namespace wgpu { class CommandBuffer; }
|
||||
|
||||
namespace aurora {
|
||||
void submit_staging_commands(const wgpu::CommandBuffer& commands);
|
||||
extern AuroraConfig g_config;
|
||||
extern uint32_t g_sdlCustomEventsStart;
|
||||
extern char g_gameName[4];
|
||||
|
||||
@@ -235,7 +235,7 @@ std::string GetOSVersion() {
|
||||
constexpr auto name = "iOS";
|
||||
#elif TARGET_OS_TV
|
||||
constexpr auto name = "tvOS";
|
||||
#elif
|
||||
#else
|
||||
constexpr auto name = Unknown;
|
||||
#endif
|
||||
|
||||
|
||||
@@ -570,12 +570,16 @@ bool initialize(AuroraBackend auroraBackend) {
|
||||
g_adapter = std::move(adapter);
|
||||
} else {
|
||||
Log.warn("Adapter request failed: {}", message);
|
||||
const std::string_view reason{message};
|
||||
SDL_SetError("Graphics adapter unavailable: %.*s",
|
||||
static_cast<int>(std::min<size_t>(reason.size(), 512)), reason.data());
|
||||
}
|
||||
});
|
||||
const auto status = g_instance.WaitAny(future, 5000000000);
|
||||
if (status != wgpu::WaitStatus::Success) {
|
||||
Log.error("Failed to create {} adapter: {}", magic_enum::enum_name(backend),
|
||||
magic_enum::enum_name(status));
|
||||
SDL_SetError("Graphics adapter request did not complete within its startup deadline");
|
||||
return false;
|
||||
}
|
||||
if (!g_adapter) {
|
||||
@@ -638,6 +642,14 @@ 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);
|
||||
@@ -738,11 +750,15 @@ bool initialize(AuroraBackend auroraBackend) {
|
||||
g_device = std::move(device);
|
||||
} else {
|
||||
Log.warn("Device request failed: {}", message);
|
||||
const std::string_view reason{message};
|
||||
SDL_SetError("Graphics device unavailable: %.*s",
|
||||
static_cast<int>(std::min<size_t>(reason.size(), 512)), reason.data());
|
||||
}
|
||||
});
|
||||
const auto status = g_instance.WaitAny(future, 5000000000);
|
||||
if (status != wgpu::WaitStatus::Success) {
|
||||
Log.error("Failed to create device: {}", magic_enum::enum_name(status));
|
||||
SDL_SetError("Graphics device request did not complete within its startup deadline");
|
||||
return false;
|
||||
}
|
||||
if (!g_device) {
|
||||
@@ -877,9 +893,14 @@ 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) {});
|
||||
g_instance.WaitAny(future, 1000000000);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <mutex>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
@@ -286,8 +287,33 @@ size_t load_from_cache(void const* key, size_t keySize, void* value, size_t valu
|
||||
if (ret == SQLITE_ROW) {
|
||||
// Hit
|
||||
const auto foundPtr = sqlite3_column_blob(load_stmt, 0);
|
||||
foundSize = sqlite3_column_int64(load_stmt, 1);
|
||||
const bool compressed = sqlite3_column_int(load_stmt, 2) != 0;
|
||||
const auto declaredSize = sqlite3_column_int64(load_stmt, 1);
|
||||
const auto storedSize = sqlite3_column_bytes(load_stmt, 0);
|
||||
const auto compression = sqlite3_column_int(load_stmt, 2);
|
||||
const bool compressed = compression == 1;
|
||||
// Dawn asks for the size before allocating its destination. Validate here,
|
||||
// not only during the copy: corrupt metadata must become a cache miss.
|
||||
bool valid = declaredSize > 0 &&
|
||||
static_cast<uint64_t>(declaredSize) <= std::numeric_limits<size_t>::max() &&
|
||||
foundPtr != nullptr && storedSize > 0 && (compression == 0 || compression == 1);
|
||||
if (valid && compressed) {
|
||||
#if defined(AURORA_CACHE_USE_ZSTD)
|
||||
// Our writer uses ZSTD_compress, which records the original content size.
|
||||
const auto frameSize = ZSTD_getFrameContentSize(foundPtr, static_cast<size_t>(storedSize));
|
||||
valid = frameSize != ZSTD_CONTENTSIZE_ERROR && frameSize != ZSTD_CONTENTSIZE_UNKNOWN &&
|
||||
frameSize == static_cast<uint64_t>(declaredSize);
|
||||
#else
|
||||
valid = false;
|
||||
#endif
|
||||
} else if (valid) {
|
||||
valid = declaredSize == storedSize;
|
||||
}
|
||||
if (!valid) {
|
||||
Log.error("Ignoring cache entry with inconsistent size or compression metadata");
|
||||
check(sqlite3_reset(load_stmt));
|
||||
return 0;
|
||||
}
|
||||
foundSize = static_cast<size_t>(declaredSize);
|
||||
if (value == nullptr) {
|
||||
g_hits.fetch_add(1, std::memory_order_relaxed);
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <webgpu/webgpu_cpp.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace aurora::webgpu::metalfx {
|
||||
struct Size {
|
||||
uint32_t inputWidth;
|
||||
uint32_t inputHeight;
|
||||
uint32_t outputWidth;
|
||||
uint32_t outputHeight;
|
||||
wgpu::TextureFormat format;
|
||||
|
||||
bool operator==(const Size&) const = default;
|
||||
};
|
||||
|
||||
// All methods except supported() belong to the serialized frame encoder.
|
||||
// GPU ownership is explicit: begin_input -> submit input -> upscale ->
|
||||
// submit output consumption -> end_output. Neither texture may be used by
|
||||
// Dawn outside its access interval. Destruction retires in-flight resources.
|
||||
class SpatialScaler {
|
||||
public:
|
||||
virtual ~SpatialScaler() = default;
|
||||
virtual const wgpu::TextureView& input_view() const = 0;
|
||||
virtual const wgpu::TextureView& output_view() const = 0;
|
||||
virtual const wgpu::Texture& output_texture() const = 0;
|
||||
virtual bool begin_input() = 0;
|
||||
virtual bool upscale() = 0;
|
||||
virtual bool end_output() = 0;
|
||||
virtual const std::string& error() const = 0;
|
||||
};
|
||||
|
||||
bool supported(const wgpu::Device& device, wgpu::BackendType backend);
|
||||
// A null result with no error means the bounded retirement pool is busy;
|
||||
// skip upscaling for this frame and retry at a later frame boundary.
|
||||
std::unique_ptr<SpatialScaler> create(const wgpu::Instance& instance,
|
||||
const wgpu::Device& device, const Size& size,
|
||||
std::string& error);
|
||||
} // namespace aurora::webgpu::metalfx
|
||||
@@ -0,0 +1,326 @@
|
||||
#include "metalfx.hpp"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <IOSurface/IOSurface.h>
|
||||
#import <Metal/Metal.h>
|
||||
#import <MetalFX/MetalFX.h>
|
||||
|
||||
#include <dawn/native/MetalBackend.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <string_view>
|
||||
|
||||
namespace aurora::webgpu::metalfx {
|
||||
namespace {
|
||||
constexpr uint64_t kScheduleTimeoutNs = 1'000'000'000;
|
||||
// Four current slots plus at most four retiring slots during resize. A busy
|
||||
// GPU must not allow resize events to allocate unbounded full-resolution images.
|
||||
constexpr unsigned kMaxLiveResources = 8;
|
||||
std::atomic<unsigned> g_liveResources{0};
|
||||
|
||||
struct SharedImage {
|
||||
IOSurfaceRef surface = nullptr;
|
||||
id<MTLTexture> metal;
|
||||
wgpu::SharedTextureMemory memory;
|
||||
wgpu::Texture texture;
|
||||
wgpu::TextureView view;
|
||||
|
||||
~SharedImage() { if (surface) CFRelease(surface); }
|
||||
|
||||
bool create(const wgpu::Device& device, id<MTLDevice> native, uint32_t width,
|
||||
uint32_t height, wgpu::TextureFormat format, MTLTextureUsage nativeUsage,
|
||||
wgpu::TextureUsage usage) {
|
||||
const bool bgra = format == wgpu::TextureFormat::BGRA8Unorm;
|
||||
const size_t rowBytes = IOSurfaceAlignProperty(kIOSurfaceBytesPerRow, size_t(width) * 4);
|
||||
NSDictionary* properties = @{
|
||||
(id)kIOSurfaceWidth: @(width), (id)kIOSurfaceHeight: @(height),
|
||||
(id)kIOSurfaceBytesPerElement: @4, (id)kIOSurfaceBytesPerRow: @(rowBytes),
|
||||
(id)kIOSurfaceAllocSize: @(rowBytes * height),
|
||||
(id)kIOSurfacePixelFormat: @(bgra ? 0x42475241u : 0x52474241u)
|
||||
};
|
||||
surface = IOSurfaceCreate((__bridge CFDictionaryRef)properties);
|
||||
if (!surface) return false;
|
||||
auto descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:
|
||||
bgra ? MTLPixelFormatBGRA8Unorm : MTLPixelFormatRGBA8Unorm
|
||||
width:width height:height mipmapped:NO];
|
||||
descriptor.storageMode = MTLStorageModeShared;
|
||||
descriptor.usage = nativeUsage;
|
||||
metal = [native newTextureWithDescriptor:descriptor iosurface:surface plane:0];
|
||||
if (!metal) return false;
|
||||
|
||||
wgpu::SharedTextureMemoryIOSurfaceDescriptor io{};
|
||||
io.ioSurface = surface;
|
||||
io.allowStorageBinding = false;
|
||||
wgpu::SharedTextureMemoryDescriptor importDescriptor{};
|
||||
importDescriptor.nextInChain = &io;
|
||||
memory = device.ImportSharedTextureMemory(&importDescriptor);
|
||||
wgpu::SharedTextureMemoryProperties actual{};
|
||||
if (!memory || memory.GetProperties(&actual) != wgpu::Status::Success ||
|
||||
actual.format != format || actual.size.width != width || actual.size.height != height ||
|
||||
(actual.usage & usage) != usage) return false;
|
||||
wgpu::TextureDescriptor textureDescriptor{};
|
||||
textureDescriptor.label = "MetalFX shared texture";
|
||||
textureDescriptor.size = {width, height, 1};
|
||||
textureDescriptor.format = format;
|
||||
textureDescriptor.usage = usage;
|
||||
texture = memory.CreateTexture(&textureDescriptor);
|
||||
if (!texture) return false;
|
||||
view = texture.CreateView();
|
||||
return view != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
struct API_AVAILABLE(macos(13.0)) Resources {
|
||||
SharedImage input, output;
|
||||
id<MTLFXSpatialScaler> scaler;
|
||||
id<MTLTexture> privateOutput;
|
||||
id<MTLCommandQueue> nativeQueue;
|
||||
id<MTLSharedEvent> event;
|
||||
wgpu::SharedFence fence;
|
||||
std::atomic<bool> failed{false};
|
||||
|
||||
Resources() { ++g_liveResources; }
|
||||
~Resources() { --g_liveResources; }
|
||||
};
|
||||
|
||||
class API_AVAILABLE(macos(13.0)) MetalSpatialScaler final : public SpatialScaler {
|
||||
wgpu::Instance m_instance;
|
||||
wgpu::Queue m_queue;
|
||||
std::shared_ptr<Resources> m_resources;
|
||||
wgpu::SharedTextureMemoryEndAccessState m_outputReleased{};
|
||||
wgpu::Future m_outputScheduled{};
|
||||
uint64_t m_value = 0;
|
||||
std::string m_error;
|
||||
|
||||
bool fail(const char* reason) {
|
||||
m_error = reason;
|
||||
m_resources->failed = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool wait_scheduled(wgpu::Future future) {
|
||||
return m_instance.WaitAny(future, kScheduleTimeoutNs) == wgpu::WaitStatus::Success ||
|
||||
fail("Timed out scheduling MetalFX GPU work");
|
||||
}
|
||||
|
||||
bool end_access(SharedImage& image, wgpu::SharedTextureMemoryEndAccessState& state,
|
||||
wgpu::Future& scheduled) {
|
||||
wgpu::SharedTextureMemoryMetalEndAccessState metal{};
|
||||
state.nextInChain = &metal;
|
||||
const auto status = image.memory.EndAccess(image.texture, &state);
|
||||
state.nextInChain = nullptr;
|
||||
scheduled = metal.commandsScheduledFuture;
|
||||
return status == wgpu::Status::Success || fail("MetalFX Dawn EndAccess failed");
|
||||
}
|
||||
|
||||
bool begin_access(SharedImage& image, bool initialized, uint64_t value) {
|
||||
wgpu::SharedTextureMemoryBeginAccessDescriptor access{};
|
||||
access.initialized = initialized;
|
||||
if (value) {
|
||||
access.fenceCount = 1;
|
||||
access.fences = &m_resources->fence;
|
||||
access.signaledValueCount = 1;
|
||||
access.signaledValues = &value;
|
||||
}
|
||||
return image.memory.BeginAccess(image.texture, &access) == wgpu::Status::Success ||
|
||||
fail("MetalFX Dawn BeginAccess failed");
|
||||
}
|
||||
|
||||
bool encode_waits(id<MTLCommandBuffer> commands,
|
||||
const wgpu::SharedTextureMemoryEndAccessState& state) {
|
||||
for (size_t i = 0; i < state.fenceCount; ++i) {
|
||||
wgpu::SharedFenceMTLSharedEventExportInfo metal{};
|
||||
wgpu::SharedFenceExportInfo info{};
|
||||
info.nextInChain = &metal;
|
||||
state.fences[i].ExportInfo(&info);
|
||||
if (info.type != wgpu::SharedFenceType::MTLSharedEvent || !metal.sharedEvent)
|
||||
return fail("Dawn did not export a MetalFX shared-event dependency");
|
||||
[commands encodeWaitForEvent:(__bridge id<MTLSharedEvent>)metal.sharedEvent
|
||||
value:state.signaledValues[i]];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void retain_until_dawn_done() {
|
||||
// A resize/toggle can destroy this wrapper immediately. The last submitted
|
||||
// Dawn consumer keeps the IOSurfaces/scaler alive independently of the cache.
|
||||
m_queue.OnSubmittedWorkDone(wgpu::CallbackMode::AllowSpontaneous,
|
||||
[resources = m_resources](wgpu::QueueWorkDoneStatus status, wgpu::StringView) {
|
||||
if (status != wgpu::QueueWorkDoneStatus::Success) resources->failed = true;
|
||||
});
|
||||
}
|
||||
|
||||
public:
|
||||
MetalSpatialScaler(const wgpu::Instance& instance, const wgpu::Device& device,
|
||||
std::shared_ptr<Resources> resources)
|
||||
: m_instance(instance), m_queue(device.GetQueue()), m_resources(std::move(resources)) {}
|
||||
|
||||
const wgpu::TextureView& input_view() const override { return m_resources->input.view; }
|
||||
const wgpu::TextureView& output_view() const override { return m_resources->output.view; }
|
||||
const wgpu::Texture& output_texture() const override { return m_resources->output.texture; }
|
||||
const std::string& error() const override { return m_error; }
|
||||
|
||||
bool begin_input() override {
|
||||
if (m_resources->failed) return fail("Previous MetalFX GPU work failed");
|
||||
return begin_access(m_resources->input, m_value != 0, m_value);
|
||||
}
|
||||
|
||||
bool upscale() override {
|
||||
@autoreleasepool {
|
||||
// Input has already been submitted. Retain it even if an export or native
|
||||
// allocation fails and the caller immediately falls back to normal copy.
|
||||
retain_until_dawn_done();
|
||||
wgpu::SharedTextureMemoryEndAccessState inputReleased{};
|
||||
wgpu::Future inputScheduled{};
|
||||
if (!end_access(m_resources->input, inputReleased, inputScheduled) ||
|
||||
!wait_scheduled(inputScheduled)) return false;
|
||||
if (m_value && !wait_scheduled(m_outputScheduled)) return false;
|
||||
id<MTLCommandBuffer> commands = [m_resources->nativeQueue commandBuffer];
|
||||
if (!commands) return fail("Could not allocate a MetalFX command buffer");
|
||||
commands.label = @"MetalFX spatial upscale and return to Dawn";
|
||||
if (!encode_waits(commands, inputReleased) || !encode_waits(commands, m_outputReleased))
|
||||
return false;
|
||||
[m_resources->scaler encodeToCommandBuffer:commands];
|
||||
id<MTLBlitCommandEncoder> blit = [commands blitCommandEncoder];
|
||||
if (!blit) return fail("Could not allocate the MetalFX output blit");
|
||||
[blit copyFromTexture:m_resources->privateOutput sourceSlice:0 sourceLevel:0
|
||||
sourceOrigin:MTLOriginMake(0, 0, 0)
|
||||
sourceSize:MTLSizeMake(m_resources->privateOutput.width, m_resources->privateOutput.height, 1)
|
||||
toTexture:m_resources->output.metal destinationSlice:0 destinationLevel:0
|
||||
destinationOrigin:MTLOriginMake(0, 0, 0)];
|
||||
[blit endEncoding];
|
||||
++m_value;
|
||||
[commands encodeSignalEvent:m_resources->event value:m_value];
|
||||
const auto resources = m_resources;
|
||||
[commands addCompletedHandler:^(id<MTLCommandBuffer> completed) {
|
||||
if (completed.status == MTLCommandBufferStatusError) resources->failed = true;
|
||||
}];
|
||||
[commands commit];
|
||||
// Scheduling is required to order independent Metal queues. Completion
|
||||
// remains asynchronous; resource reuse is guarded by shared GPU events.
|
||||
[commands waitUntilScheduled];
|
||||
if (commands.status == MTLCommandBufferStatusError)
|
||||
return fail("MetalFX command buffer failed");
|
||||
return begin_access(m_resources->output, true, m_value);
|
||||
}
|
||||
}
|
||||
|
||||
bool end_output() override {
|
||||
retain_until_dawn_done();
|
||||
m_outputReleased = {};
|
||||
return end_access(m_resources->output, m_outputReleased, m_outputScheduled);
|
||||
}
|
||||
};
|
||||
|
||||
// Allocation failures must be caught here rather than reaching Aurora's fatal
|
||||
// uncaptured-error callback. Scope callbacks own their strings even on timeout.
|
||||
bool pop_scope(const wgpu::Instance& instance, const wgpu::Device& device, std::string& error) {
|
||||
auto message = std::make_shared<std::string>();
|
||||
auto future = device.PopErrorScope(wgpu::CallbackMode::WaitAnyOnly,
|
||||
[message](wgpu::PopErrorScopeStatus status, wgpu::ErrorType type, wgpu::StringView text) {
|
||||
if (status != wgpu::PopErrorScopeStatus::Success || type != wgpu::ErrorType::NoError) {
|
||||
const std::string_view detail{text};
|
||||
*message = detail.empty() ? "MetalFX texture allocation failed" : std::string(detail);
|
||||
}
|
||||
});
|
||||
if (instance.WaitAny(future, kScheduleTimeoutNs) != wgpu::WaitStatus::Success) {
|
||||
error = "Timed out checking MetalFX texture allocation";
|
||||
return false;
|
||||
}
|
||||
if (!message->empty()) { error = *message; return false; }
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool supported(const wgpu::Device& device, wgpu::BackendType backend) {
|
||||
if (@available(macOS 13.0, *)) {
|
||||
if (!device || backend != wgpu::BackendType::Metal ||
|
||||
!device.HasFeature(wgpu::FeatureName::SharedTextureMemoryIOSurface) ||
|
||||
!device.HasFeature(wgpu::FeatureName::SharedFenceMTLSharedEvent)) return false;
|
||||
auto native = dawn::native::metal::GetMTLDevice(device.Get());
|
||||
return native && [MTLFXSpatialScalerDescriptor supportsDevice:native];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<SpatialScaler> create(const wgpu::Instance& instance,
|
||||
const wgpu::Device& device, const Size& size,
|
||||
std::string& error) {
|
||||
error.clear();
|
||||
if (@available(macOS 13.0, *)) {
|
||||
@autoreleasepool {
|
||||
if (!supported(device, wgpu::BackendType::Metal)) {
|
||||
error = "MetalFX spatial scaling is unsupported";
|
||||
return {};
|
||||
}
|
||||
wgpu::Limits limits{};
|
||||
device.GetLimits(&limits);
|
||||
if (!size.inputWidth || !size.inputHeight || size.inputWidth >= size.outputWidth ||
|
||||
size.inputHeight >= size.outputHeight || size.outputWidth > limits.maxTextureDimension2D ||
|
||||
size.outputHeight > limits.maxTextureDimension2D ||
|
||||
(size.format != wgpu::TextureFormat::RGBA8Unorm && size.format != wgpu::TextureFormat::BGRA8Unorm)) {
|
||||
error = "MetalFX requires smaller input dimensions and an RGBA8/BGRA8 unorm target";
|
||||
return {};
|
||||
}
|
||||
if (g_liveResources.load() >= kMaxLiveResources) {
|
||||
return {};
|
||||
}
|
||||
auto native = dawn::native::metal::GetMTLDevice(device.Get());
|
||||
auto resources = std::make_shared<Resources>();
|
||||
auto descriptor = [MTLFXSpatialScalerDescriptor new];
|
||||
descriptor.inputWidth = size.inputWidth;
|
||||
descriptor.inputHeight = size.inputHeight;
|
||||
descriptor.outputWidth = size.outputWidth;
|
||||
descriptor.outputHeight = size.outputHeight;
|
||||
descriptor.colorTextureFormat = size.format == wgpu::TextureFormat::BGRA8Unorm
|
||||
? MTLPixelFormatBGRA8Unorm : MTLPixelFormatRGBA8Unorm;
|
||||
descriptor.outputTextureFormat = descriptor.colorTextureFormat;
|
||||
descriptor.colorProcessingMode = MTLFXSpatialScalerColorProcessingModePerceptual;
|
||||
resources->scaler = [descriptor newSpatialScalerWithDevice:native];
|
||||
resources->nativeQueue = [native newCommandQueue];
|
||||
resources->event = [native newSharedEvent];
|
||||
if (!resources->scaler || !resources->nativeQueue || !resources->event) {
|
||||
error = "Could not create MetalFX spatial resources";
|
||||
return {};
|
||||
}
|
||||
auto outputDescriptor = [MTLTextureDescriptor
|
||||
texture2DDescriptorWithPixelFormat:descriptor.outputTextureFormat
|
||||
width:size.outputWidth height:size.outputHeight mipmapped:NO];
|
||||
outputDescriptor.storageMode = MTLStorageModePrivate;
|
||||
outputDescriptor.usage = resources->scaler.outputTextureUsage;
|
||||
resources->privateOutput = [native newTextureWithDescriptor:outputDescriptor];
|
||||
if (!resources->privateOutput) { error = "Could not allocate MetalFX private output"; return {}; }
|
||||
|
||||
device.PushErrorScope(wgpu::ErrorFilter::Validation);
|
||||
device.PushErrorScope(wgpu::ErrorFilter::OutOfMemory);
|
||||
device.PushErrorScope(wgpu::ErrorFilter::Internal);
|
||||
bool allocated = resources->input.create(device, native, size.inputWidth, size.inputHeight,
|
||||
size.format, resources->scaler.colorTextureUsage, wgpu::TextureUsage::RenderAttachment);
|
||||
allocated = allocated && resources->output.create(device, native, size.outputWidth, size.outputHeight,
|
||||
size.format, MTLTextureUsageShaderRead, wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc);
|
||||
if (allocated) {
|
||||
wgpu::SharedFenceMTLSharedEventDescriptor event{};
|
||||
event.sharedEvent = (__bridge void*)resources->event;
|
||||
wgpu::SharedFenceDescriptor fence{};
|
||||
fence.nextInChain = &event;
|
||||
resources->fence = device.ImportSharedFence(&fence);
|
||||
allocated = resources->fence != nullptr;
|
||||
}
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (!pop_scope(instance, device, error)) allocated = false;
|
||||
}
|
||||
if (!allocated) {
|
||||
if (error.empty()) error = "Could not import MetalFX IOSurface textures into Dawn";
|
||||
return {};
|
||||
}
|
||||
resources->scaler.colorTexture = resources->input.metal;
|
||||
resources->scaler.outputTexture = resources->privateOutput;
|
||||
resources->scaler.inputContentWidth = size.inputWidth;
|
||||
resources->scaler.inputContentHeight = size.inputHeight;
|
||||
return std::make_unique<MetalSpatialScaler>(instance, device, std::move(resources));
|
||||
}
|
||||
}
|
||||
error = "MetalFX requires macOS 13 or newer";
|
||||
return {};
|
||||
}
|
||||
} // namespace aurora::webgpu::metalfx
|
||||
@@ -0,0 +1,11 @@
|
||||
#include "metalfx.hpp"
|
||||
|
||||
namespace aurora::webgpu::metalfx {
|
||||
bool supported(const wgpu::Device&, wgpu::BackendType) { return false; }
|
||||
|
||||
std::unique_ptr<SpatialScaler> create(const wgpu::Instance&, const wgpu::Device&,
|
||||
const Size&, std::string& error) {
|
||||
error = "MetalFX is not available in this build";
|
||||
return {};
|
||||
}
|
||||
} // namespace aurora::webgpu::metalfx
|
||||
@@ -46,6 +46,7 @@ SDL_Window* g_window;
|
||||
SDL_Renderer* g_renderer;
|
||||
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;
|
||||
@@ -525,7 +526,20 @@ 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_frameBufferAspectFit && baseW > 0 && baseH > 0) {
|
||||
if (g_forceAspect169.load(std::memory_order_acquire) && native_fb_w > 0 && native_fb_h > 0) {
|
||||
if (g_frameBufferScale > 0.f && baseW > 0 && baseH > 0) {
|
||||
const auto [scaledW, scaledH] =
|
||||
scale_frame_buffer_to_aspect(static_cast<int>(baseW), static_cast<int>(baseH),
|
||||
g_frameBufferScale, 16.f / 9.f);
|
||||
fb_w = scaledW;
|
||||
fb_h = scaledH;
|
||||
} else {
|
||||
fb_w = std::min(native_fb_w,
|
||||
std::max(1, static_cast<int>(std::lround(native_fb_h * (16.f / 9.f)))));
|
||||
fb_h = std::min(native_fb_h,
|
||||
std::max(1, static_cast<int>(std::lround(native_fb_w * (9.f / 16.f)))));
|
||||
}
|
||||
} else if (g_frameBufferAspectFit && baseW > 0 && baseH > 0) {
|
||||
float renderScale = g_frameBufferScale > 0.f ? g_frameBufferScale : 1.f;
|
||||
if (g_frameBufferScale <= 0.f) {
|
||||
renderScale = std::min(static_cast<float>(native_fb_w) / static_cast<float>(baseW),
|
||||
@@ -759,6 +773,14 @@ void set_frame_buffer_aspect_fit(bool fit) {
|
||||
request_frame_buffer_resize();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -781,6 +803,10 @@ 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,6 +56,7 @@ 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 TRUE
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(googletest)
|
||||
@@ -18,6 +18,7 @@ if (AURORA_ENABLE_GX)
|
||||
gx_fifo_test.cpp
|
||||
gx_test_stubs.cpp
|
||||
texture_bind_group_cache_key_test.cpp
|
||||
renderer_regression_test.cpp
|
||||
../lib/gfx/efb_ram_encoder.cpp
|
||||
# GX API implementations (encoders)
|
||||
../lib/dolphin/gx/GXBump.cpp
|
||||
@@ -66,6 +67,18 @@ if (AURORA_ENABLE_GX)
|
||||
)
|
||||
|
||||
gtest_discover_tests(gx_fifo_tests)
|
||||
|
||||
option(AURORA_BUILD_GPU_TESTS "Build renderer pixel tests requiring a graphics device" OFF)
|
||||
if (AURORA_BUILD_GPU_TESTS)
|
||||
add_executable(gx_readback_tests gpu_readback_test.cpp)
|
||||
target_compile_features(gx_readback_tests PRIVATE cxx_std_20)
|
||||
target_include_directories(gx_readback_tests PRIVATE ../lib)
|
||||
target_link_libraries(gx_readback_tests PRIVATE
|
||||
aurora::core aurora::gx aurora::pad aurora::vi aurora::mtx aurora::si
|
||||
dawn::dawncpp_headers absl::flat_hash_map absl::btree TracyClient)
|
||||
add_test(NAME gx_readback_tests COMMAND gx_readback_tests "${CMAKE_CURRENT_BINARY_DIR}/readback-cache")
|
||||
set_tests_properties(gx_readback_tests PROPERTIES TIMEOUT 90)
|
||||
endif ()
|
||||
endif () # AURORA_ENABLE_GX
|
||||
|
||||
# DVD API tests
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
// ROM-free integration probe. Links the actual maintained Aurora renderer.
|
||||
#include "gfx/common.hpp"
|
||||
#include "gfx/clear.hpp"
|
||||
#include "gfx/efb_ram_copy.hpp"
|
||||
#include "gfx/pipeline_cache.hpp"
|
||||
#include "gfx/texture.hpp"
|
||||
#include "gx/gx.hpp"
|
||||
#include "gx/fifo.hpp"
|
||||
#include "gx/command_processor.hpp"
|
||||
#include "gx/frame_interpolation.hpp"
|
||||
#include <dolphin/gx.h>
|
||||
#include <aurora/aurora.h>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
using namespace aurora;
|
||||
std::atomic<unsigned> errors{};
|
||||
std::atomic<unsigned> guestWrites{};
|
||||
// Keep destinations alive through shutdown, including any failing wait.
|
||||
std::array<uint8_t, 16 * 16 * 4 + 32> guarded;
|
||||
std::array<uint8_t, 16 * 16 * 4 + 32> guardedBake;
|
||||
void require(bool value, const char* message) {
|
||||
if (!value) throw std::runtime_error(message);
|
||||
}
|
||||
void submit(bool final, bool download = false, bool async = false) {
|
||||
auto encoder = webgpu::g_device.CreateCommandEncoder();
|
||||
if (final) gfx::end_frame(encoder); else gfx::end_batch(encoder);
|
||||
gfx::render(encoder);
|
||||
if (download) gfx::efb_ram::encode_downloads(encoder);
|
||||
if (async) gfx::efb_ram::encode_async_downloads(encoder);
|
||||
auto commands = encoder.Finish();
|
||||
webgpu::g_queue.Submit(1, &commands);
|
||||
if (download) require(gfx::efb_ram::complete_downloads(), "EFB readback failed");
|
||||
gfx::after_submit();
|
||||
if (!final) require(gfx::resume_frame(), "Batch resume failed");
|
||||
}
|
||||
|
||||
constexpr std::array<std::array<uint8_t, 4>, 4> colors{{
|
||||
{255, 0, 0, 255}, {0, 255, 0, 255}, {0, 0, 255, 255}, {255, 255, 0, 255}}};
|
||||
using Pixels = std::vector<uint8_t>;
|
||||
Pixels expected(unsigned extent) {
|
||||
Pixels bytes(extent * extent * 4);
|
||||
// GX RGBA8: 4x4 tiles, sixteen A/R pairs followed by sixteen G/B pairs.
|
||||
for (unsigned y = 0; y < extent; ++y) for (unsigned x = 0; x < extent; ++x) {
|
||||
const auto color = colors[y * 4 / extent];
|
||||
const auto tile = ((y / 4) * (extent / 4) + x / 4) * 64;
|
||||
const auto pair = ((y % 4) * 4 + x % 4) * 2;
|
||||
bytes[tile + pair] = color[3]; bytes[tile + pair + 1] = color[0];
|
||||
bytes[tile + 32 + pair] = color[1]; bytes[tile + 33 + pair] = color[2];
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
Pixels run(unsigned splitEvery, bool async = false, bool offscreen = false, unsigned geometry = 0, bool capacityStress = false, bool interpolate = false, bool frameWorker = false, unsigned copyCase = 0) {
|
||||
require(!async || !offscreen, "Combined probe mode is not supported");
|
||||
guardedBake.fill(0xa5);
|
||||
gx::g_gxState.clearColor = {0.f, 0.f, 0.f, 1.f};
|
||||
require(frameWorker ? aurora_begin_frame() : gfx::begin_frame(), "Frame begin failed");
|
||||
std::array<std::array<float, 3>, 4> positions{};
|
||||
if (geometry) {
|
||||
alignas(32) static std::array<uint8_t, 32768> fifo;
|
||||
GXInit(fifo.data(), fifo.size());
|
||||
gx::g_gxState.viewportPolicy = AURORA_VIEWPORT_NATIVE;
|
||||
GXSetViewport(0.f, 0.f, 64.f, 64.f, 0.f, 1.f);
|
||||
GXSetScissor(0, 0, 64, 64);
|
||||
GXSetCullMode(GX_CULL_NONE);
|
||||
GXSetZMode(false, GX_ALWAYS, false);
|
||||
GXSetBlendMode(GX_BM_NONE, GX_BL_ONE, GX_BL_ZERO, GX_LO_COPY);
|
||||
GXSetColorUpdate(true); GXSetAlphaUpdate(true);
|
||||
GXSetNumTexGens(0); GXSetNumChans(1); GXSetNumTevStages(copyCase ? copyCase : 1);
|
||||
for (unsigned stage = 1; stage < copyCase; ++stage) {
|
||||
GXSetTevOrder(static_cast<GXTevStageID>(stage), GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR0A0);
|
||||
GXSetTevOp(static_cast<GXTevStageID>(stage), GX_PASSCLR);
|
||||
}
|
||||
GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR0A0);
|
||||
GXSetTevOp(GX_TEVSTAGE0, GX_PASSCLR);
|
||||
GXSetChanCtrl(GX_COLOR0A0, false, GX_SRC_REG, GX_SRC_VTX, GX_LIGHT_NULL, GX_DF_NONE, GX_AF_NONE);
|
||||
const float projection[]{interpolate ? 0.f : 1.f, 1.f, 0.f, 1.f, 0.f, 0.f, -0.5f};
|
||||
GXSetProjectionv(projection);
|
||||
GXClearVtxDesc();
|
||||
GXSetVtxDesc(GX_VA_POS, geometry == 2 ? GX_INDEX8 : GX_DIRECT);
|
||||
GXSetVtxDesc(GX_VA_CLR0, GX_DIRECT);
|
||||
if (geometry == 2) GXSetArray(GX_VA_POS, positions.data(), sizeof(positions), sizeof(positions[0]), true);
|
||||
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0);
|
||||
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_CLR0, GX_CLR_RGBA, GX_RGBA8, 0);
|
||||
gx::fifo::drain();
|
||||
}
|
||||
const auto pipeline = gfx::pipeline_ref(gfx::clear::PipelineConfig{});
|
||||
for (unsigned band = 0; band < 4; ++band) {
|
||||
const auto c = colors[band];
|
||||
if (geometry) {
|
||||
const float top = 1.f - band * 0.5f;
|
||||
const float bottom = top - 0.5f;
|
||||
const float z = interpolate ? -1.f : 0.f;
|
||||
positions = {{{-1.f, top, z}, {1.f, top, z}, {1.f, bottom, z}, {-1.f, bottom, z}}};
|
||||
// Keep the same array address/format and change only its bytes between draws.
|
||||
if (geometry == 2) GXInvalidateVtxCache();
|
||||
if (geometry == 3) {
|
||||
std::array<uint8_t, 64> raw{};
|
||||
for (unsigned index = 0; index < positions.size(); ++index) {
|
||||
for (unsigned axis = 0; axis < 3; ++axis) {
|
||||
const auto bits = std::bit_cast<uint32_t>(positions[index][axis]);
|
||||
for (unsigned byte = 0; byte < 4; ++byte)
|
||||
raw[index * 16 + axis * 4 + byte] = bits >> (24 - byte * 8);
|
||||
}
|
||||
std::copy(c.begin(), c.end(), raw.begin() + index * 16 + 12);
|
||||
}
|
||||
require(gx::fifo::submit_raw_draw(GX_QUADS, GX_VTXFMT0, raw.data(), 4, raw.size()),
|
||||
"Raw bridge rejected valid quad");
|
||||
} else {
|
||||
GXBegin(GX_QUADS, GX_VTXFMT0, 4);
|
||||
for (unsigned index = 0; index < positions.size(); ++index) {
|
||||
if (geometry == 2) GXPosition1x8(index);
|
||||
else GXPosition3f32(positions[index][0], positions[index][1], positions[index][2]);
|
||||
GXColor4u8(c[0], c[1], c[2], 255);
|
||||
}
|
||||
GXEnd();
|
||||
}
|
||||
} else {
|
||||
gfx::push_draw_command(gfx::clear::DrawData{
|
||||
.pipeline = pipeline,
|
||||
.color = {c[0] / 255., c[1] / 255., c[2] / 255., 1.},
|
||||
.depth = 0.5f,
|
||||
.useScissor = true,
|
||||
.scissor = {0, static_cast<int32_t>(band * 16), 64, 16}});
|
||||
}
|
||||
if (offscreen && band == 0) {
|
||||
// Suspend a partially recorded EFB, bake an independently observable copy,
|
||||
// then resume it before a possible capacity-boundary submission.
|
||||
gfx::begin_offscreen(64, 64);
|
||||
gfx::push_draw_command(gfx::clear::DrawData{
|
||||
.pipeline = pipeline, .color = {1., 0., 1., 1.}, .depth = 0.25f});
|
||||
if (capacityStress) for (unsigned draw = 0; draw < 24; ++draw) {
|
||||
gfx::push_draw_command(gfx::clear::DrawData{
|
||||
.pipeline = pipeline, .color = {1., 0., 1., 1.}, .depth = 0.25f,
|
||||
.useScissor = true, .scissor = {0, 0, 4, 4}});
|
||||
}
|
||||
auto baked = gfx::new_render_texture(64, 64, GX_TF_RGBA8, "Aurora probe offscreen bake");
|
||||
gfx::resolve_pass(baked, {0, 0, 64, 64}, false, false, false,
|
||||
{0.f, 0.f, 0.f, 1.f}, 1.f, GX_TF_RGBA8, nullptr, false,
|
||||
nullptr, false, 1.f, false, false, true);
|
||||
gfx::efb_ram::schedule(guardedBake.data() + 16, 16, 16, GX_TF_RGBA8, baked);
|
||||
gfx::end_offscreen();
|
||||
if (capacityStress) {
|
||||
require(gfx::efb_ram::prepare_downloads(), "Early bake readback preparation failed");
|
||||
for (unsigned draw = 0; draw < 24; ++draw) {
|
||||
gfx::push_draw_command(gfx::clear::DrawData{
|
||||
.pipeline = pipeline, .color = {1., 0., 0., 1.}, .depth = 0.5f,
|
||||
.useScissor = true, .scissor = {0, 0, 4, 4}});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (splitEvery && band < 3 && (band + 1) % splitEvery == 0) submit(false);
|
||||
}
|
||||
auto texture = gfx::new_render_texture(64, 64, GX_TF_RGBA8, "Aurora probe persistent copy");
|
||||
static std::array<uint8_t, 64 * 64 * 4> copyDestination;
|
||||
if (copyCase) {
|
||||
gx::fifo::drain();
|
||||
GXSetTexCopySrc(0, 0, 64, 64);
|
||||
GXSetTexCopyDst(64, 64, GX_TF_RGBA8, GX_FALSE);
|
||||
GXCopyTex(copyDestination.data(), GX_FALSE);
|
||||
texture = gx::g_gxState.copyTextures.at(copyDestination.data()).handle;
|
||||
} else {
|
||||
// A partial clear forces the real snapshot and clear-uniform paths after the copy.
|
||||
gfx::resolve_pass(texture, {0, 0, 64, 64}, true, true, true, {0.f, 0.f, 0.f, 1.f},
|
||||
1.f, GX_TF_RGBA8, nullptr, false, nullptr, false, 1.f, false, false, true);
|
||||
}
|
||||
guarded.fill(0xa5);
|
||||
const unsigned extent = async ? 4 : 16;
|
||||
const unsigned bytes = extent * extent * 4;
|
||||
gfx::efb_ram::schedule(guarded.data() + 16, extent, extent, GX_TF_RGBA8, texture);
|
||||
const auto before = guestWrites.load(std::memory_order_acquire);
|
||||
if (async) gfx::efb_ram::seal_async_downloads();
|
||||
else require(gfx::efb_ram::prepare_downloads(), "Readback preparation failed");
|
||||
if (frameWorker) {
|
||||
require(!async && aurora_flush_efb_copies_to_ram(), "Worker-mode EFB readback failed");
|
||||
aurora_end_frame();
|
||||
} else submit(true, !async, async);
|
||||
if (async) {
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
while (guestWrites.load(std::memory_order_acquire) == before) {
|
||||
webgpu::g_instance.ProcessEvents();
|
||||
require(std::chrono::steady_clock::now() < deadline, "Async readback did not complete");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
}
|
||||
require(std::all_of(guarded.begin(), guarded.begin() + 16, [](auto b) { return b == 0xa5; }) &&
|
||||
std::all_of(guarded.begin() + 16 + bytes, guarded.end(), [](auto b) { return b == 0xa5; }),
|
||||
"Readback wrote outside its destination");
|
||||
if (offscreen) {
|
||||
require(std::all_of(guardedBake.begin(), guardedBake.begin() + 16, [](auto b) { return b == 0xa5; }) &&
|
||||
std::all_of(guardedBake.end() - 16, guardedBake.end(), [](auto b) { return b == 0xa5; }),
|
||||
"Offscreen readback wrote outside its destination");
|
||||
for (unsigned tile = 0; tile < 16; ++tile) for (unsigned pair = 0; pair < 16; ++pair) {
|
||||
const auto offset = 16 + tile * 64 + pair * 2;
|
||||
require(guardedBake[offset] == 255 && guardedBake[offset + 1] == 255 &&
|
||||
guardedBake[offset + 32] == 0 && guardedBake[offset + 33] == 255,
|
||||
"Offscreen bake did not preserve expected magenta pixels");
|
||||
}
|
||||
}
|
||||
Pixels pixels(guarded.begin() + 16, guarded.begin() + 16 + bytes);
|
||||
return pixels;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 2) return 2;
|
||||
std::filesystem::create_directories(argv[1]);
|
||||
AuroraConfig config{};
|
||||
config.appName = "Aurora readback regression tests";
|
||||
config.userPath = argv[1];
|
||||
config.cachePath = argv[1];
|
||||
config.resourcesPath = argv[1];
|
||||
config.desiredBackend = BACKEND_AUTO;
|
||||
config.windowWidth = 64;
|
||||
config.windowHeight = 64;
|
||||
config.msaa = 1;
|
||||
config.maxTextureAnisotropy = 1;
|
||||
config.logLevel = LOG_INFO;
|
||||
config.logCallback = [](AuroraLogLevel level, const char* module, const char* message, unsigned size) {
|
||||
if (level >= LOG_ERROR) ++errors;
|
||||
std::fprintf(stderr, "[%s] %.*s\n", module, static_cast<int>(size), message);
|
||||
};
|
||||
const auto initialized = aurora_initialize(1, argv, &config);
|
||||
if (initialized.initializationStatus != AURORA_INITIALIZATION_SUCCESS) return 3;
|
||||
aurora_set_skip_unready_pipelines(true);
|
||||
aurora_set_guest_write_hooks(nullptr, [](const void*, size_t) {
|
||||
guestWrites.fetch_add(1, std::memory_order_release);
|
||||
});
|
||||
try {
|
||||
const auto prewarmQueued = gfx::queued_pipeline_count();
|
||||
const auto prewarmDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(60);
|
||||
while (gfx::queued_pipeline_count() != 0) {
|
||||
require(std::chrono::steady_clock::now() < prewarmDeadline, "Seeded pipeline prewarm did not finish");
|
||||
webgpu::g_instance.ProcessEvents();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
std::printf("Actual Aurora completed seeded startup queue (%u observed pending)\n", prewarmQueued);
|
||||
const auto control = run(0);
|
||||
for (unsigned band = 0; band < 4; ++band) {
|
||||
const auto tile = band * 4 * 64;
|
||||
std::fprintf(stderr, "band=%u ARGB=%u,%u,%u,%u\n", band, control[tile],
|
||||
control[tile + 1], control[tile + 32], control[tile + 33]);
|
||||
}
|
||||
require(control == expected(16), "Unsplit pixels differ from independently expected GX data");
|
||||
for (unsigned iteration = 0; iteration < 9; ++iteration) {
|
||||
const auto splitEvery = iteration % 3 + 1;
|
||||
require(run(splitEvery) == control, "Split pixels differ from unsplit control");
|
||||
std::printf("Actual Aurora split=%u iteration=%u matched native tiled readback\n", splitEvery, iteration);
|
||||
}
|
||||
for (unsigned iteration = 0; iteration < 9; ++iteration) {
|
||||
require(run(iteration % 3 + 1, true) == expected(4), "Async pixels differ from expected GX data");
|
||||
std::printf("Actual Aurora async iteration=%u matched native tiled readback\n", iteration);
|
||||
}
|
||||
for (unsigned splitEvery = 0; splitEvery < 4; ++splitEvery) {
|
||||
require(run(splitEvery, false, true) == expected(16), "Offscreen interlude changed the suspended EFB");
|
||||
std::printf("Actual Aurora offscreen split=%u preserved bake and suspended EFB\n", splitEvery);
|
||||
}
|
||||
for (unsigned splitEvery = 0; splitEvery < 4; ++splitEvery) {
|
||||
require(run(splitEvery, false, false, true) == expected(16), "GX FIFO quad pixels differ from expected output");
|
||||
std::printf("Actual Aurora GX FIFO split=%u preserved direct vertices, indices and uniforms\n", splitEvery);
|
||||
}
|
||||
for (unsigned splitEvery = 0; splitEvery < 4; ++splitEvery) {
|
||||
require(run(splitEvery, false, false, 2) == expected(16), "Invalidated GX array pixels differ from expected output");
|
||||
std::printf("Actual Aurora GX invalidation split=%u refreshed the same array address\n", splitEvery);
|
||||
}
|
||||
const gfx::StagingSizes physical{gfx::VertexBufferSize, gfx::UniformBufferSize,
|
||||
gfx::IndexBufferSize, gfx::StorageBufferSize};
|
||||
const auto uniformTail = gx::MaxUniformSize + 32 * gfx::staging_uniform_bytes(48);
|
||||
for (unsigned buffer = 0; buffer < 4; ++buffer) {
|
||||
auto limits = physical;
|
||||
limits[buffer] = buffer == 0 ? 128 : buffer == 1 ? uniformTail + 512 :
|
||||
buffer == 2 ? 24 : 2 * gfx::staging_storage_bytes(48);
|
||||
gfx::set_staging_capacity_limits_for_testing(limits);
|
||||
const auto before = gfx::staging_split_count();
|
||||
require(run(0, false, false, buffer == 1 ? 0 : buffer == 3 ? 2 : 1) == expected(16),
|
||||
"Automatic capacity split changed pixels");
|
||||
require(gfx::staging_split_count() > before, "Forced capacity did not split");
|
||||
const auto highWater = gfx::staging_high_water();
|
||||
for (unsigned i = 0; i < limits.size(); ++i)
|
||||
require(highWater[i] <= limits[i], "Actual staging usage exceeded admission budget");
|
||||
std::printf("Actual staging high-water V/U/I/S=%llu/%llu/%llu/%llu bytes\n",
|
||||
static_cast<unsigned long long>(highWater[0]), static_cast<unsigned long long>(highWater[1]),
|
||||
static_cast<unsigned long long>(highWater[2]), static_cast<unsigned long long>(highWater[3]));
|
||||
std::printf("Actual Aurora automatic capacity buffer=%u splits=%llu matched pixels\n", buffer,
|
||||
static_cast<unsigned long long>(gfx::staging_split_count() - before));
|
||||
}
|
||||
auto limits = physical;
|
||||
limits[0] = 128;
|
||||
gfx::set_staging_capacity_limits_for_testing(limits);
|
||||
require(run(0, false, false, 3) == expected(16), "Raw bridge capacity split changed pixels");
|
||||
std::puts("Actual Aurora raw bridge capacity split preserved direct quad pixels");
|
||||
limits = physical;
|
||||
limits[1] = uniformTail + 768;
|
||||
gfx::set_staging_capacity_limits_for_testing(limits);
|
||||
const auto beforeBake = gfx::staging_split_count();
|
||||
require(run(0, false, true, 0, true) == expected(16),
|
||||
"Automatic offscreen split changed bake or suspended EFB");
|
||||
require(gfx::staging_split_count() - beforeBake >= 9, "Offscreen test did not reuse all staging slots");
|
||||
std::printf("Actual Aurora automatic offscreen/readback splits=%llu preserved all pixels\n",
|
||||
static_cast<unsigned long long>(gfx::staging_split_count() - beforeBake));
|
||||
gfx::set_staging_capacity_limits_for_testing(physical);
|
||||
aurora_set_frame_interpolation_fps(120);
|
||||
for (unsigned frame = 0; frame < 3; ++frame)
|
||||
require(run(0, false, false, 2, false, true) == expected(16), "Perspective warmup changed pixels");
|
||||
AuroraFrameInterpolationDiagnostics interpolation{};
|
||||
gx::get_frame_interpolation_diagnostics(interpolation);
|
||||
require(interpolation.matchable > 0 && interpolation.activeSamples > 0,
|
||||
"Interpolation probe did not establish matching perspective draws");
|
||||
limits = physical;
|
||||
limits[3] = 2 * gfx::staging_storage_bytes(48);
|
||||
gfx::set_staging_capacity_limits_for_testing(limits);
|
||||
require(run(0, false, false, 2, false, true) == expected(16), "Interpolated split changed native pixels");
|
||||
gx::get_frame_interpolation_diagnostics(interpolation);
|
||||
require(!interpolation.replaySafe, "Split frame incorrectly retained interpolation replay");
|
||||
aurora_set_frame_interpolation_fps(0);
|
||||
std::puts("Actual Aurora matched perspective interpolation survived capacity split and disabled replay");
|
||||
limits = physical;
|
||||
limits[0] = 32; // One quad needs 64 bytes: typed rejection before any draw allocation.
|
||||
gfx::set_staging_capacity_limits_for_testing(limits);
|
||||
bool oversized = false;
|
||||
try { run(0, false, false, 1); }
|
||||
catch (const gfx::StagingCapacityError&) { oversized = true; }
|
||||
require(oversized, "Oversized primitive was not rejected");
|
||||
require(gfx::staging_usage() == gfx::StagingSizes{}, "Oversized primitive partially allocated");
|
||||
gx::fifo::clear_buffer();
|
||||
gfx::abort_frame();
|
||||
gfx::set_staging_capacity_limits_for_testing(physical);
|
||||
std::puts("Actual Aurora oversized primitive rejected before staging mutation");
|
||||
require(run(0, false, false, 1) == expected(16), "Renderer failed after rejected primitive cleanup");
|
||||
limits = physical;
|
||||
limits[3] = 2 * gfx::staging_storage_bytes(48);
|
||||
gfx::set_staging_capacity_limits_for_testing(limits);
|
||||
aurora_set_frame_interpolation_fps(120);
|
||||
for (unsigned frame = 0; frame < 16; ++frame)
|
||||
require(run(0, false, false, 2, false, true, true) == expected(16),
|
||||
"Frame-worker capacity split changed pixels");
|
||||
// Grant preparation of the next frame before joining DONE, exactly as the
|
||||
// real producer does; leave no worker waiting for a future begin_frame.
|
||||
require(aurora_begin_frame(), "Final worker frame preparation failed");
|
||||
aurora::wait_for_frame_worker();
|
||||
gfx::abort_frame();
|
||||
aurora_set_frame_interpolation_fps(0);
|
||||
gfx::set_staging_capacity_limits_for_testing(physical);
|
||||
std::puts("Actual Aurora frame worker completed 16 capacity-split perspective frames");
|
||||
require(errors == 0, "Renderer reported an error");
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "FAIL: %s\n", error.what());
|
||||
aurora_shutdown();
|
||||
return 4;
|
||||
}
|
||||
aurora_shutdown();
|
||||
if (errors != 0) return 4;
|
||||
std::puts("Actual Aurora clear/resolve/snapshot/downsample/readback batches passed");
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user