mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-26 07:01:08 -04:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 987aef0b6a | |||
| 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 | |||
| 38a11b7b2c | |||
| 7a13696a8b | |||
| 2bcfdca199 | |||
| f2165a5aa6 | |||
| ffc7244277 | |||
| dec9ed8df1 | |||
| 8dc9bb67b3 | |||
| ca7d126a13 | |||
| 5c76e2b0df | |||
| ae3096c89b | |||
| 6f4dd2f470 | |||
| 2b5b889249 | |||
| fbfb741b49 | |||
| e630132dd3 | |||
| a6f3720598 | |||
| abe7cd2b61 | |||
| 7aa04bfe80 | |||
| 1af7cd6d9b | |||
| d31942c356 | |||
| 13b6d0b447 | |||
| db576cd5cb | |||
| edacfcb8bc | |||
| 36faa76dc0 | |||
| 3fc1555871 | |||
| be691bd08d | |||
| 243eb86021 | |||
| 7b750e96ac | |||
| e4e388295b | |||
| 2c38db4174 | |||
| 1c7e4e79c5 | |||
| 6ddc153e44 | |||
| cb812c1fd0 |
@@ -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,18 +14,30 @@ 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
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
- uses: actions/setup-dotnet@v6
|
||||
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
|
||||
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
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
|
||||
# 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.
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: package-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
recompilation:
|
||||
name: Recompilation test
|
||||
uses: ./.github/workflows/recomp-test.yml
|
||||
|
||||
linux-appimage:
|
||||
name: Linux (AppImage, ${{ matrix.arch }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-22.04
|
||||
arch: x86_64
|
||||
- runner: ubuntu-22.04-arm
|
||||
arch: aarch64
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
# appimagetool ships as an AppImage itself and needs to self-mount via FUSE to run directly;
|
||||
# GitHub's ubuntu-latest runners don't have libfuse2 preinstalled.
|
||||
- name: Install dependencies (required by appimagetool and SDL3 build)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libfuse2 build-essential git make \
|
||||
pkg-config cmake ninja-build gnome-desktop-testing libasound2-dev libpulse-dev \
|
||||
libaudio-dev libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
|
||||
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \
|
||||
libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
|
||||
libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev libthai-dev libusb-1.0-0-dev \
|
||||
libpipewire-0.3-dev libwayland-dev libdecor-0-dev liburing-dev
|
||||
|
||||
- name: Build AppImage
|
||||
run: bash Launcher/build-appimage.sh
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: WiiCompiled-Setup-linux-${{ matrix.arch }}
|
||||
path: Launcher/dist/WiiCompiled-Setup-${{ matrix.arch }}.AppImage
|
||||
if-no-files-found: error
|
||||
archive: false
|
||||
|
||||
windows-installer:
|
||||
name: Windows (installer exe)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Build installer
|
||||
shell: pwsh
|
||||
run: ./Launcher/Build-Installer.ps1
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: WiiCompiled-Setup-windows-x64
|
||||
path: Launcher/dist/WiiCompiled-Setup.exe
|
||||
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.
|
||||
release:
|
||||
name: Publish GitHub Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [linux-appimage, windows-installer, recompilation]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
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 would ship an update nobody can install. Catch that before anything is published.
|
||||
- name: Verify the tag matches the pinned setup version
|
||||
env:
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="${TAG#v}"
|
||||
status=0
|
||||
check() {
|
||||
if ! grep -Fq "$2" "$1"; then
|
||||
echo "::error file=$1::expected '$2' for tag $TAG"
|
||||
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'"
|
||||
exit $status
|
||||
|
||||
# The build jobs upload with `archive: false`, which stores each installer as a raw file
|
||||
# rather than a zip (and names the artifact after the file). Only download-artifact v8 and
|
||||
# later understand such direct uploads; v7 tries to unzip everything and fails on them.
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
|
||||
- name: Publish release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ls -lR artifacts
|
||||
assets=()
|
||||
for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage; do
|
||||
found="$(find artifacts -type f -name "$name" | head -n 1)"
|
||||
[ -n "$found" ] && [ -s "$found" ] || { echo "::error::missing release asset $name"; exit 1; }
|
||||
assets+=("$found")
|
||||
done
|
||||
# A re-run of a tag whose release already exists only refreshes the assets.
|
||||
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
gh release upload "$TAG" "${assets[@]}" --repo "$GITHUB_REPOSITORY" --clobber
|
||||
else
|
||||
gh release create "$TAG" "${assets[@]}" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--title "$TAG" \
|
||||
--verify-tag \
|
||||
--generate-notes
|
||||
fi
|
||||
@@ -0,0 +1,61 @@
|
||||
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: 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
|
||||
@@ -25,6 +25,9 @@ Code.pul
|
||||
# Build output
|
||||
/build/
|
||||
/build-*/
|
||||
/native-build/
|
||||
/native-build-macos/
|
||||
/local-products/
|
||||
/dist/
|
||||
/out/
|
||||
[Bb]in/
|
||||
@@ -69,3 +72,5 @@ project.lock.json
|
||||
*.log
|
||||
output.txt
|
||||
|
||||
# Operating System
|
||||
.DS_Store
|
||||
|
||||
@@ -88,17 +88,23 @@ Assert-File $setupProject '.NET setup project'
|
||||
Assert-File $translatorProject 'Translator CLI project'
|
||||
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $portableTools 'llvm-mingw\bin\x86_64-w64-mingw32-clang++.exe'))) {
|
||||
# A PowerShell script, not a native executable - it never touches $LASTEXITCODE, and its own
|
||||
# $ErrorActionPreference = 'Stop' + throw already aborts this run on failure.
|
||||
& (Join-Path $PSScriptRoot 'Prepare-PortableTools.ps1') -Destination $portableTools
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Portable tool preparation failed.' }
|
||||
}
|
||||
Assert-File (Join-Path $portableTools 'CMake\bin\cmake.exe') 'Portable CMake'
|
||||
Assert-File (Join-Path $portableTools 'Ninja\ninja.exe') 'Portable Ninja'
|
||||
Assert-Directory $dependencySources 'Pinned offline dependency sources'
|
||||
# native_prebuilt carries the aurora/third-party archives the user no longer has
|
||||
# to compile (launcher/Prepare-NativePrebuilt.ps1).
|
||||
# Kept in step with InstalledLayout.DependencyNames by Test-PinnedFacts.ps1: the installed host
|
||||
# refuses to call a toolkit complete unless every one of these directories is present.
|
||||
$requiredDependencies = @('abseil-cpp','cppwinrt','dawn_prebuilt','fmt','freetype','imgui','libusb','native_prebuilt','png','SDL','sqlite3','tracy','xxhash','zlib','zstd')
|
||||
$missingSources = @($requiredDependencies | Where-Object { $_ -ne 'native_prebuilt' } |
|
||||
Where-Object { -not (Test-Path -LiteralPath (Join-Path $dependencySources $_) -PathType Container) })
|
||||
if ($missingSources.Count -gt 0) {
|
||||
& (Join-Path $PSScriptRoot 'Prepare-Dependencies.ps1') -Destination $dependencySources
|
||||
}
|
||||
Assert-Directory $dependencySources 'Pinned offline dependency sources'
|
||||
|
||||
# The precompiled archives are only interchangeable with what the user's machine
|
||||
# compiles if both came from this toolchain and this flag set, so a stale package
|
||||
@@ -165,8 +171,7 @@ Assert-File $translator 'Self-contained translator'
|
||||
|
||||
# Resolved via the shared WiiCompiled.Setup.Common.Cli helper (also used by build-appimage.sh on
|
||||
# Linux) rather than a separate download/version-pin copy here: it downloads and caches the same way
|
||||
# NodToolProvider.cs always does (Launcher/artifacts/nodtool.exe), replacing the old manual
|
||||
# -DolphinToolPath handoff with an automated, pinned acquisition step.
|
||||
# NodToolProvider.cs always does (Launcher/artifacts/nodtool.exe)
|
||||
$nodToolCliProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup.Common.Cli'
|
||||
$nodTool = (& dotnet run --project $nodToolCliProject -c Release -- --workspace $repoRoot | Select-Object -Last 1)
|
||||
if ($LASTEXITCODE -ne 0) { throw "nodtool resolution failed with exit code $LASTEXITCODE." }
|
||||
@@ -276,7 +281,7 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
|
||||
|
||||
$manifest = [ordered]@{
|
||||
SchemaVersion = 2
|
||||
ProductVersion = '0.2.25'
|
||||
ProductVersion = '0.2.32'
|
||||
ExpectedGameId = $pins.GameId
|
||||
ExpectedDolSha256 = $pins.DolSha256
|
||||
ExpectedRelSha256 = $pins.RelSha256
|
||||
@@ -292,11 +297,12 @@ $manifest = [ordered]@{
|
||||
$manifest | ConvertTo-Json | Set-Content (Join-Path $payloadRoot 'payload-manifest.json') -Encoding UTF8
|
||||
|
||||
Write-Host '[4/6] Enforcing the copyright and generated-code boundary...'
|
||||
# Both audits are PowerShell scripts that throw directly on failure (Test-PayloadBoundary.ps1
|
||||
# never invokes a native command at all, so it never touches $LASTEXITCODE); there is no exit
|
||||
# code to check here, and doing so risks reading a stale value from an unrelated earlier command.
|
||||
& (Join-Path $PSScriptRoot 'Test-PayloadBoundary.ps1') -PayloadRoot $payloadRoot
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Payload boundary audit failed.' }
|
||||
& (Join-Path $PSScriptRoot 'Test-NativeDependencies.ps1') -PayloadRoot $payloadRoot -SetupHost $setupHost `
|
||||
-LlvmReadobjPath (Join-Path $portableTools 'llvm-mingw\bin\llvm-readobj.exe')
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Native dependency audit failed.' }
|
||||
|
||||
Write-Host '[5/6] Creating the canonical installer payload...'
|
||||
$payloadZip = Join-Path $workRoot 'payload.zip'
|
||||
|
||||
@@ -117,12 +117,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.
|
||||
#>
|
||||
@@ -132,9 +133,14 @@ function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Descri
|
||||
if ($_.Contains('"')) { throw "A native build argument contains an unsupported quote: $_" }
|
||||
'"' + $_ + '"'
|
||||
})
|
||||
$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." }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
# Populates Launcher/artifacts/dependencies: the pinned, unpatched upstream source trees (plus the
|
||||
# prebuilt Dawn package and generated C++/WinRT headers) that Build-Installer.ps1 ships inside the
|
||||
# installer so the user's build runs with FETCHCONTENT_FULLY_DISCONNECTED=ON. Every pin is the one
|
||||
# aurora-main's own CMake declares; the Pin fields below are asserted against those files so the
|
||||
# two cannot drift apart silently. native_prebuilt is not fetched here - Prepare-NativePrebuilt.ps1
|
||||
# compiles it from these trees.
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Destination,
|
||||
# Windows metadata source for cppwinrt.exe: 'local' (this machine's WinMetadata), 'sdk', or an
|
||||
# installed SDK version such as 10.0.26100.0.
|
||||
[string]$CppWinRtInput = 'local'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 3.0
|
||||
|
||||
$repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
||||
$auroraCMake = Join-Path $repoRoot 'aurora-main\CMakeLists.txt'
|
||||
$auroraExtern = Join-Path $repoRoot 'aurora-main\extern\CMakeLists.txt'
|
||||
$auroraDawn = Join-Path $repoRoot 'aurora-main\cmake\AuroraDawnProvider.cmake'
|
||||
$auroraLibUsb = Join-Path $repoRoot 'aurora-main\cmake\AuroraLibUSB.cmake'
|
||||
$auroraSdl = Join-Path $repoRoot 'aurora-main\cmake\AuroraSDL3Provider.cmake'
|
||||
|
||||
# Name = directory under the destination; FetchContent maps it back through
|
||||
# FETCHCONTENT_SOURCE_DIR_<UPPERCASE NAME> (NativeBuildFlags.ps1), so the names are the declared
|
||||
# FetchContent names, not the upstream project names.
|
||||
$packages = @(
|
||||
[pscustomobject]@{
|
||||
Name = 'SDL'; File = 'SDL3-3.4.4.tar.gz'
|
||||
Uris = @('https://github.com/libsdl-org/SDL/releases/download/release-3.4.4/SDL3-3.4.4.tar.gz')
|
||||
Pins = @(@{ File = $auroraCMake; Text = 'set(AURORA_SDL3_VERSION "3.4.4"' },
|
||||
@{ File = $auroraSdl; Text = 'releases/download/release-${AURORA_SDL3_VERSION}/SDL3-${AURORA_SDL3_VERSION}.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'abseil-cpp'; File = 'abseil-cpp-20240722.0.tar.gz'
|
||||
Uris = @('https://github.com/abseil/abseil-cpp/archive/refs/tags/20240722.0.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/abseil/abseil-cpp/archive/refs/tags/20240722.0.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'dawn_prebuilt'; File = 'dawn-v20260603.191052-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=13be9cff8b9b179c42dcd16aeabb6effcc8f0dfdcc14463eda2a5caeda225142' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'fmt'; File = 'fmt-11.1.4.tar.gz'
|
||||
Uris = @('https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'freetype'; File = 'freetype-2.14.3.tar.gz'
|
||||
Uris = @('https://files.twilitrealm.dev/freetype-2.14.3.tar.gz',
|
||||
'https://download.savannah.gnu.org/releases/freetype/freetype-2.14.3.tar.gz',
|
||||
'https://downloads.sourceforge.net/project/freetype/freetype2/2.14.3/freetype-2.14.3.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://files.twilitrealm.dev/freetype-2.14.3.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'imgui'; File = 'imgui-1.91.9b-docking.tar.gz'
|
||||
Uris = @('https://github.com/ocornut/imgui/archive/refs/tags/v1.91.9b-docking.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/ocornut/imgui/archive/refs/tags/v1.91.9b-docking.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'libusb'; File = 'libusb-1.0.30.tar.bz2'
|
||||
Uris = @('https://github.com/libusb/libusb/releases/download/v1.0.30/libusb-1.0.30.tar.bz2')
|
||||
Pins = @(@{ File = $auroraCMake; Text = 'set(AURORA_LIBUSB_VERSION "1.0.30"' },
|
||||
@{ File = $auroraLibUsb; Text = 'releases/download/v${AURORA_LIBUSB_VERSION}/libusb-${AURORA_LIBUSB_VERSION}.tar.bz2' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'png'; File = 'libpng-1.6.58.tar.gz'
|
||||
Uris = @('https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.58.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.58.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'sqlite3'; File = 'sqlite-amalgamation-3510300.zip'
|
||||
Uris = @('https://sqlite.org/2026/sqlite-amalgamation-3510300.zip')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://sqlite.org/2026/sqlite-amalgamation-3510300.zip' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'tracy'; File = 'tracy-a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz'
|
||||
Uris = @('https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz')
|
||||
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' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'zlib'; File = 'zlib-1.3.2.tar.gz'
|
||||
Uris = @('https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'zstd'; File = 'zstd-1.5.7.tar.gz'
|
||||
Uris = @('https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz' })
|
||||
}
|
||||
)
|
||||
|
||||
# The C++/WinRT compiler (NuGet package = zip) that generates the projection headers.
|
||||
$cppWinRtTool = [pscustomobject]@{
|
||||
Name = 'cppwinrt-tool'; File = 'Microsoft.Windows.CppWinRT.3.0.260818.1.nupkg'
|
||||
Uris = @('https://www.nuget.org/api/v2/package/Microsoft.Windows.CppWinRT/3.0.260818.1')
|
||||
Pins = @()
|
||||
}
|
||||
|
||||
if (-not $Destination) { $Destination = Join-Path $PSScriptRoot 'artifacts\dependencies' }
|
||||
$Destination = [IO.Path]::GetFullPath($Destination)
|
||||
$downloads = Join-Path $PSScriptRoot 'artifacts\downloads'
|
||||
$tar = Join-Path $env:SystemRoot 'System32\tar.exe'
|
||||
if (-not (Test-Path -LiteralPath $tar -PathType Leaf)) { throw "Windows archive tool is missing: $tar" }
|
||||
[IO.Directory]::CreateDirectory($Destination) | Out-Null
|
||||
[IO.Directory]::CreateDirectory($downloads) | Out-Null
|
||||
|
||||
function Assert-Pinned($Package) {
|
||||
foreach ($pin in $Package.Pins) {
|
||||
if (-not (Test-Path -LiteralPath $pin.File -PathType Leaf)) { throw "Pin source is missing: $($pin.File)" }
|
||||
$content = [IO.File]::ReadAllText($pin.File)
|
||||
if (-not $content.Contains($pin.Text)) {
|
||||
throw "$($Package.Name) is pinned to '$($pin.Text)' here but $($pin.File) no longer declares it; update both."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-Archive($Package) {
|
||||
$archive = Join-Path $downloads $Package.File
|
||||
if (Test-Path -LiteralPath $archive -PathType Leaf) { return $archive }
|
||||
$temporary = $archive + '.partial'
|
||||
$failures = @()
|
||||
foreach ($uri in $Package.Uris) {
|
||||
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
|
||||
Write-Host "Downloading $($Package.Name) from $uri..."
|
||||
try {
|
||||
Invoke-WebRequest -UseBasicParsing -Uri $uri -OutFile $temporary
|
||||
} catch {
|
||||
$failures += "$uri ($($_.Exception.Message))"
|
||||
continue
|
||||
}
|
||||
Move-Item -LiteralPath $temporary -Destination $archive -Force
|
||||
return $archive
|
||||
}
|
||||
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
|
||||
throw "$($Package.Name) could not be fetched: $($failures -join '; ')"
|
||||
}
|
||||
|
||||
function Expand-Package([string]$Archive, [string]$Target) {
|
||||
# Release archives wrap everything in one versioned directory (SDL3-3.4.4/, tracy-<sha>/) while
|
||||
# dawn-build's package is flat; either way the tree lands directly under the target.
|
||||
$extract = Join-Path (Split-Path -Parent $Target) ('.extract-' + (Split-Path -Leaf $Target))
|
||||
if (Test-Path -LiteralPath $extract) { Remove-Item -LiteralPath $extract -Recurse -Force }
|
||||
[IO.Directory]::CreateDirectory($extract) | Out-Null
|
||||
# Symbolic links need a privilege Windows tar usually lacks; the only ones in these archives
|
||||
# are zstd test aliases, and none are build inputs.
|
||||
$listing = & $tar -tvf $Archive
|
||||
if ($LASTEXITCODE -ne 0) { throw "Listing $Archive failed with exit code $LASTEXITCODE." }
|
||||
$excludes = @($listing | ForEach-Object { if ($_ -match '^l.*\s(\S+)\s->\s') { '--exclude=' + $Matches[1] } })
|
||||
& $tar -xf $Archive -C $extract @excludes
|
||||
if ($LASTEXITCODE -ne 0) { throw "Extracting $Archive failed with exit code $LASTEXITCODE." }
|
||||
$entries = @(Get-ChildItem -LiteralPath $extract -Force)
|
||||
$source = $extract
|
||||
if ($entries.Count -eq 1 -and $entries[0].PSIsContainer) { $source = $entries[0].FullName }
|
||||
if (Test-Path -LiteralPath $Target) { Remove-Item -LiteralPath $Target -Recurse -Force }
|
||||
Move-Item -LiteralPath $source -Destination $Target
|
||||
if (Test-Path -LiteralPath $extract) { Remove-Item -LiteralPath $extract -Recurse -Force }
|
||||
}
|
||||
|
||||
foreach ($package in $packages) {
|
||||
Assert-Pinned $package
|
||||
$target = Join-Path $Destination $package.Name
|
||||
if (Test-Path -LiteralPath $target -PathType Container) { continue }
|
||||
Expand-Package (Get-Archive $package) $target
|
||||
Write-Host "Prepared $($package.Name)"
|
||||
}
|
||||
|
||||
$cppWinRt = Join-Path $Destination 'cppwinrt'
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType Leaf)) {
|
||||
$toolRoot = Join-Path $PSScriptRoot 'artifacts\cppwinrt-tool'
|
||||
$compiler = Join-Path $toolRoot 'bin\cppwinrt.exe'
|
||||
if (-not (Test-Path -LiteralPath $compiler -PathType Leaf)) {
|
||||
Expand-Package (Get-Archive $cppWinRtTool) $toolRoot
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $compiler -PathType Leaf)) { throw "cppwinrt.exe is missing: $compiler" }
|
||||
if (Test-Path -LiteralPath $cppWinRt) { Remove-Item -LiteralPath $cppWinRt -Recurse -Force }
|
||||
[IO.Directory]::CreateDirectory($cppWinRt) | Out-Null
|
||||
Write-Host "Generating C++/WinRT headers (-input $CppWinRtInput)..."
|
||||
& $compiler -input $CppWinRtInput -output $cppWinRt
|
||||
if ($LASTEXITCODE -ne 0) { throw "cppwinrt.exe failed with exit code $LASTEXITCODE." }
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType Leaf)) {
|
||||
throw "cppwinrt.exe produced no winrt\base.h under $cppWinRt"
|
||||
}
|
||||
Copy-Item -LiteralPath (Join-Path $toolRoot 'LICENSE') -Destination (Join-Path $cppWinRt 'LICENSE.txt')
|
||||
Write-Host 'Prepared cppwinrt'
|
||||
}
|
||||
|
||||
Write-Host "Pinned dependency sources ready: $Destination"
|
||||
@@ -61,8 +61,6 @@ Assert-File $windres 'Portable resource compiler'
|
||||
Assert-File $clangBinary 'Portable clang driver binary'
|
||||
Assert-Directory $dependencies 'Pinned offline dependency sources'
|
||||
Assert-Directory $auroraSource 'aurora-main source tree'
|
||||
Assert-File (Join-Path $repoRoot 'generated\build_shards\shards.cmake') `
|
||||
'Translator shard manifest (run the developer build once so runtime/ can configure)'
|
||||
|
||||
if ($Parallel -le 0) { $Parallel = [Environment]::ProcessorCount }
|
||||
|
||||
|
||||
Executable
+590
@@ -0,0 +1,590 @@
|
||||
#!/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
|
||||
# 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
|
||||
# Launcher/Prepare-NativePrebuilt.ps1, consumed by the same platform-agnostic
|
||||
# runtime/cmake/NativePrebuilt.cmake either script's package works with unmodified.
|
||||
#
|
||||
# Not a byte-for-byte port of the Windows script: Linux needs no offline pinned dependency cache.
|
||||
# Every FetchContent dependency in aurora-main/extern/CMakeLists.txt is a fixed-version URL already
|
||||
# (verified directly), and aurora-main/CMakeLists.txt's own _default_linkage is "static" on any
|
||||
# non-Windows platform. Windows pins Launcher/artifacts/dependencies because its installer ships
|
||||
# pre-fetched sources to end users for a fully offline build; this harvest only ever runs on a
|
||||
# maintainer's own machine (network access needed once, here, not shipped to anyone), so a plain
|
||||
# networked configure is exactly as reproducible - what actually makes the harvested archives safe
|
||||
# to link into any consumer's own build is the pinned compiler below, not an offline source cache.
|
||||
set -euo pipefail
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
workspace=$(cd "$script_dir/.." && pwd)
|
||||
|
||||
arch=""
|
||||
output_dir=""
|
||||
stage_dir="$workspace/build/native-prebuilt-stage"
|
||||
keep_stage=0
|
||||
reuse_stage=0
|
||||
parallel=0
|
||||
print_fingerprint_only=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: Prepare-NativePrebuilt.sh --arch {x86_64|aarch64} [options]
|
||||
|
||||
--arch ARCH Target architecture; selects the matching bundled toolchain (required)
|
||||
--output-dir DIR Where the package is written (default: Launcher/artifacts/native-prebuilt-ARCH)
|
||||
--stage-dir DIR Staging build directory (default: build/native-prebuilt-stage)
|
||||
--keep-stage Do not delete the staging build directory afterward
|
||||
--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
|
||||
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.
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--arch) arch=$2; shift 2 ;;
|
||||
--output-dir) output_dir=$2; shift 2 ;;
|
||||
--stage-dir) stage_dir=$2; shift 2 ;;
|
||||
--keep-stage) keep_stage=1; shift ;;
|
||||
--reuse-stage) reuse_stage=1; shift ;;
|
||||
--parallel) parallel=$2; shift 2 ;;
|
||||
--print-fingerprint-only) print_fingerprint_only=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Prepare-NativePrebuilt.sh: unknown argument: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$arch" in
|
||||
x86_64|aarch64) ;;
|
||||
*) echo "Prepare-NativePrebuilt.sh: --arch must be x86_64 or aarch64" >&2; usage; exit 1 ;;
|
||||
esac
|
||||
|
||||
fail() { echo "Prepare-NativePrebuilt.sh: error: $*" >&2; exit 1; }
|
||||
assert_file() { [[ -f "$1" ]] || fail "$2 is missing: $1"; }
|
||||
assert_dir() { [[ -d "$1" ]] || fail "$2 is missing: $1"; }
|
||||
sha256_of() { sha256sum "$1" | awk '{print $1}'; }
|
||||
normalize() { readlink -f "$1"; }
|
||||
|
||||
[[ -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"
|
||||
runtime_source="$workspace/runtime"
|
||||
aurora_source="$workspace/aurora-main"
|
||||
|
||||
assert_file "$cmake_bin" "Portable CMake (run prepare-portable-tools.sh --arch $arch first)"
|
||||
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")
|
||||
assert_file "$clang_binary" "Portable clang driver binary"
|
||||
|
||||
(( parallel > 0 )) || parallel=$(nproc)
|
||||
|
||||
fingerprint_tree() {
|
||||
# $1 = root dir, remaining args = top-level subdirectory names to exclude
|
||||
local root=$1; shift
|
||||
root=$(normalize "$root")
|
||||
[[ -d "$root" ]] || return 0
|
||||
local find_args=("$root")
|
||||
local ex
|
||||
for ex in "$@"; do find_args+=(-path "$root/$ex" -prune -o); done
|
||||
find_args+=(-type f -print)
|
||||
find "${find_args[@]}" | LC_ALL=C sort | while IFS= read -r abs; do
|
||||
printf '%s %s\n' "${abs#$root/}" "$(sha256_of "$abs")"
|
||||
done | sha256sum | awk '{print $1}'
|
||||
}
|
||||
|
||||
# The fixed (path-independent) half of the configure command line - identical for every
|
||||
# harvest, which is what lets one package be valid regardless of where it was built.
|
||||
# -DBUILD_SHARED_LIBS=OFF: aurora-main/extern/CMakeLists.txt derives its own _USE_SHARED from
|
||||
# whether BUILD_SHARED_LIBS is *defined at all* (not its value), so leaving it unset would default
|
||||
# zlib/libpng to shared. -DAURORA_SDL3_PROVIDER=vendor: without it, "auto" resolves to "system" on
|
||||
# any machine that happens to have an SDL3 dev package installed. -DCMAKE_DISABLE_FIND_PACKAGE_*:
|
||||
# absl/PNG/Freetype each call find_package() unconditionally, with no provider flag to gate them
|
||||
# (unlike SDL3/Dawn) - verified directly that this machine's system libpng-dev/freetype-dev get
|
||||
# linked in shared instead of the pinned vendored source otherwise. Disabling find_package for
|
||||
# these three forces the same FetchContent-vendored, statically-built result regardless of what a
|
||||
# given maintainer's machine happens to have installed. ZLIB is deliberately NOT disabled the same
|
||||
# way: libpng's own vendored CMakeLists.txt calls find_package(ZLIB REQUIRED) internally even when
|
||||
# zlib came from aurora's own FetchContent (extern/CMakeLists.txt writes a redirect config for
|
||||
# exactly this), and CMAKE_DISABLE_FIND_PACKAGE_ZLIB errors out on any REQUIRED call site outright
|
||||
# (verified directly) - it cannot be scoped to only aurora's own initial, non-required check.
|
||||
# Dawn stays a prebuilt package regardless (Linux x86_64/aarch64 always auto-resolve to "package" -
|
||||
# see AuroraDawnProvider.cmake).
|
||||
fixed_configure_flags=(
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DBUILD_SHARED_LIBS=OFF
|
||||
-DAURORA_SDL3_PROVIDER=vendor
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_absl=ON
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_PNG=ON
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_Freetype=ON
|
||||
# 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)
|
||||
# from picking up a system Freetype - it does not reach into the FetchContent-built copy's own
|
||||
# internal find_package(BZip2) call. FT_DISABLE_BZIP2 is Freetype's own documented flag for
|
||||
# exactly this (verified in its CMakeLists.txt), unlike the ZLIB/libpng situation where no such
|
||||
# source-level flag exists.
|
||||
-DFT_DISABLE_BZIP2=ON
|
||||
-DCMAKE_POLICY_DEFAULT_CMP0168=NEW
|
||||
)
|
||||
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
|
||||
# whole by build-appimage.sh); build/ is a plain developer build directory.
|
||||
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.
|
||||
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"
|
||||
|
||||
compiler_sha256=$(sha256_of "$clang_binary")
|
||||
|
||||
if [[ "$print_fingerprint_only" -eq 1 ]]; then
|
||||
printf 'compiler_sha256=%s\n' "$compiler_sha256"
|
||||
printf 'flag_fingerprint=%s\n' "$flag_fingerprint"
|
||||
printf 'aurora_fingerprint=%s\n' "$aurora_fingerprint"
|
||||
printf 'third_party_fingerprint=%s\n' "$third_party_fingerprint"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# The package must never contain a stale mixture of two builds.
|
||||
rm -rf "$output_dir"
|
||||
mkdir -p "$output_dir/lib" "$output_dir/bin" "$output_dir/include"
|
||||
|
||||
# The staging build has to be configured without the package present, otherwise the runtime would
|
||||
# consume the very package this script is producing.
|
||||
if [[ "$reuse_stage" -eq 0 ]]; then rm -rf "$stage_dir"; fi
|
||||
export_dir="$stage_dir/export"
|
||||
mkdir -p "$export_dir"
|
||||
|
||||
# fixed_configure_flags/flag_fingerprint were already computed above (needed before the
|
||||
# --print-fingerprint-only early exit).
|
||||
echo "Prepare-NativePrebuilt.sh: configuring the aurora/third-party staging build..."
|
||||
"$cmake_bin" -S "$runtime_source" -B "$stage_dir" -G Ninja \
|
||||
"${fixed_configure_flags[@]}" \
|
||||
-DCMAKE_C_COMPILER="$cc" -DCMAKE_CXX_COMPILER="$cxx" \
|
||||
-DCMAKE_MAKE_PROGRAM="$ninja_bin" \
|
||||
-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"; }
|
||||
|
||||
build_ninja="$stage_dir/build.ninja"
|
||||
assert_file "$build_ninja" "Generated build.ninja"
|
||||
|
||||
extract_ninja_vars() {
|
||||
# $1 = ninja file, $2 = exact rule name. Prints "KEY\tVALUE" for every " KEY = VALUE" line
|
||||
# following the first "build ...: RULE ..." statement that uses that rule.
|
||||
awk -v rule="$2" '
|
||||
BEGIN { in_stmt = 0 }
|
||||
/^build / {
|
||||
if (in_stmt) exit
|
||||
colon = index($0, ": ")
|
||||
if (colon == 0) next
|
||||
rest = substr($0, colon + 2)
|
||||
sp = index(rest, " ")
|
||||
stmt_rule = (sp > 0) ? substr(rest, 1, sp - 1) : rest
|
||||
if (stmt_rule == rule) in_stmt = 1
|
||||
next
|
||||
}
|
||||
in_stmt {
|
||||
if (substr($0, 1, 2) != " ") exit
|
||||
line = substr($0, 3)
|
||||
eq = index(line, " = ")
|
||||
if (eq == 0) exit
|
||||
print substr(line, 1, eq - 1) "\t" substr(line, eq + 3)
|
||||
}
|
||||
' "$1"
|
||||
}
|
||||
|
||||
declare -A probe_compile=() control_compile=() probe_link=() control_link=()
|
||||
load_vars() {
|
||||
local -n dest=$1
|
||||
local k v
|
||||
while IFS=$'\t' read -r k v; do dest["$k"]=$v; done < <(extract_ninja_vars "$build_ninja" "$2")
|
||||
}
|
||||
load_vars probe_compile "CXX_COMPILER__mkw_np_probe_unscanned_Release"
|
||||
load_vars control_compile "CXX_COMPILER__mkw_np_probe_control_unscanned_Release"
|
||||
load_vars probe_link "CXX_EXECUTABLE_LINKER__mkw_np_probe_Release"
|
||||
load_vars control_link "CXX_EXECUTABLE_LINKER__mkw_np_probe_control_Release"
|
||||
[[ -n "${probe_compile[INCLUDES]+x}" ]] || fail "No Ninja statement used rule CXX_COMPILER__mkw_np_probe_unscanned_Release."
|
||||
[[ -n "${probe_link[LINK_LIBRARIES]+x}" ]] || fail "No Ninja statement used rule CXX_EXECUTABLE_LINKER__mkw_np_probe_Release."
|
||||
|
||||
split_list() {
|
||||
# $1 = string value, $2 = name of the array to fill (nameref)
|
||||
local -n out=$2
|
||||
out=()
|
||||
[[ -n "$1" ]] || return 0
|
||||
read -ra out <<< "$1"
|
||||
}
|
||||
|
||||
subtract_multiset() {
|
||||
# $1 = "all" array name, $2 = "remove" array name, $3 = result array name (all namerefs)
|
||||
local -n all_ref=$1 remove_ref=$2 result_ref=$3
|
||||
local -A pending=()
|
||||
local item
|
||||
for item in "${remove_ref[@]}"; do
|
||||
pending["$item"]=$(( ${pending["$item"]:-0} + 1 ))
|
||||
done
|
||||
result_ref=()
|
||||
for item in "${all_ref[@]}"; do
|
||||
if [[ "${pending["$item"]:-0}" -gt 0 ]]; then
|
||||
pending["$item"]=$(( pending["$item"] - 1 ))
|
||||
continue
|
||||
fi
|
||||
result_ref+=("$item")
|
||||
done
|
||||
}
|
||||
|
||||
split_list "${probe_compile[INCLUDES]:-}" _probe_includes
|
||||
split_list "${control_compile[INCLUDES]:-}" _control_includes
|
||||
subtract_multiset _probe_includes _control_includes include_arguments
|
||||
|
||||
split_list "${probe_compile[DEFINES]:-}" _probe_defines
|
||||
split_list "${control_compile[DEFINES]:-}" _control_defines
|
||||
subtract_multiset _probe_defines _control_defines define_arguments
|
||||
|
||||
split_list "${probe_compile[FLAGS]:-}" _probe_flags
|
||||
split_list "${control_compile[FLAGS]:-}" _control_flags
|
||||
subtract_multiset _probe_flags _control_flags compile_options_raw
|
||||
|
||||
split_list "${probe_link[LINK_LIBRARIES]:-}" _probe_link
|
||||
split_list "${control_link[LINK_LIBRARIES]:-}" _control_link
|
||||
subtract_multiset _probe_link _control_link link_items
|
||||
|
||||
[[ ${#include_arguments[@]} -gt 0 ]] || fail "The aurora compile interface is empty; the probe did not link aurora."
|
||||
[[ ${#link_items[@]} -gt 0 ]] || fail "The aurora link interface is empty; the probe did not link aurora."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Harvest the built libraries.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
binary_root=$(normalize "$stage_dir")
|
||||
aurora_root=$(normalize "$(get_meta aurora_dir)")
|
||||
runtime_root=$(normalize "$(get_meta runtime_dir)")
|
||||
|
||||
generated_include_index=0
|
||||
copy_harvested() {
|
||||
# $1 = source path, $2 = subdirectory under the package. Echoes "subdir/name".
|
||||
local source=$1 subdir=$2
|
||||
local name destination
|
||||
name=$(basename "$source")
|
||||
destination="$output_dir/$subdir/$name"
|
||||
[[ ! -e "$destination" ]] || fail "Two harvested files collide on the name $name: $source"
|
||||
cp "$source" "$destination"
|
||||
printf '%s/%s' "$subdir" "$name"
|
||||
}
|
||||
declare -A generated_include_tokens=()
|
||||
convert_to_token() {
|
||||
# $1 = absolute path, $2 = 'include' or another kind. Sets $TOKEN_RESULT - NOT echoed/command-
|
||||
# substituted: this needs to mutate generated_include_tokens/generated_include_index, and a
|
||||
# $(...) call runs in a subshell, silently discarding that mutation once it returns (verified
|
||||
# directly: every build-tree include directory collided into the same "generated01" until this
|
||||
# was written this way instead).
|
||||
local path kind=$2
|
||||
path=$(normalize "$1")
|
||||
case "$path" in
|
||||
"$aurora_root"/*) TOKEN_RESULT="@AURORA@/${path#$aurora_root/}"; return ;;
|
||||
"$runtime_root"/*) TOKEN_RESULT="@RUNTIME@/${path#$runtime_root/}"; return ;;
|
||||
"$binary_root"/*)
|
||||
[[ "$kind" == include ]] || fail "Unhandled build-tree artifact: $path"
|
||||
if [[ -n "${generated_include_tokens[$path]:-}" ]]; then
|
||||
TOKEN_RESULT=${generated_include_tokens[$path]}
|
||||
return
|
||||
fi
|
||||
generated_include_index=$((generated_include_index + 1))
|
||||
local name destination
|
||||
name=$(printf 'generated%02d' "$generated_include_index")
|
||||
destination="$output_dir/include/$name"
|
||||
mkdir -p "$destination"
|
||||
cp -a "$path/." "$destination/"
|
||||
TOKEN_RESULT="@PKG@/include/$name"
|
||||
generated_include_tokens[$path]=$TOKEN_RESULT
|
||||
return ;;
|
||||
*) fail "Path escapes every shippable root ($kind): $path" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
declare -A linker_file_to_reference=()
|
||||
shared_import_targets=() shared_import_runtime=() shared_import_implib=()
|
||||
targets_txt="$export_dir/targets.txt"
|
||||
assert_file "$targets_txt" "Native prebuilt export targets list"
|
||||
while IFS='|' read -r name type file linkerfile; do
|
||||
[[ -n "$name" ]] || continue
|
||||
[[ -f "$linkerfile" ]] || continue # not part of the closure mkw_np_probe pulled in
|
||||
linkerfile=$(normalize "$linkerfile")
|
||||
relative_linker=$(copy_harvested "$linkerfile" lib)
|
||||
if [[ "$type" == "SHARED_LIBRARY" ]]; then
|
||||
file_abs=$(normalize "$file")
|
||||
# Unlike Windows (a .dll + a separate .dll.a import library), a Linux shared object's
|
||||
# TARGET_FILE and TARGET_LINKER_FILE are the same path - one file, harvested once.
|
||||
if [[ "$file_abs" == "$linkerfile" ]]; then
|
||||
relative_runtime=$relative_linker
|
||||
else
|
||||
[[ -f "$file_abs" ]] || fail "Shared library $name has no runtime file: $file_abs"
|
||||
relative_runtime=$(copy_harvested "$file_abs" bin)
|
||||
fi
|
||||
shared_import_targets+=("mkw_np::$name")
|
||||
shared_import_runtime+=("$relative_runtime")
|
||||
shared_import_implib+=("$relative_linker")
|
||||
linker_file_to_reference["$linkerfile"]="mkw_np::$name"
|
||||
else
|
||||
linker_file_to_reference["$linkerfile"]="@PKG@/$relative_linker"
|
||||
fi
|
||||
done < "$targets_txt"
|
||||
# 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.
|
||||
echo "Prepare-NativePrebuilt.sh: harvested ${#linker_file_to_reference[@]} archive(s) (${#shared_import_targets[@]} shared)"
|
||||
|
||||
dawn_linker_file="" dawn_runtime_file=""
|
||||
dawn_txt="$export_dir/dawn.txt"
|
||||
if [[ -s "$dawn_txt" ]]; then
|
||||
IFS='|' read -r _ dawn_runtime_file dawn_linker_file < "$dawn_txt"
|
||||
dawn_runtime_file=$(normalize "$dawn_runtime_file")
|
||||
dawn_linker_file=$(normalize "$dawn_linker_file")
|
||||
fi
|
||||
[[ -n "$dawn_linker_file" ]] && linker_file_to_reference["$dawn_linker_file"]="dawn::webgpu_dawn"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rewrite the compile/link interface into workspace-relative tokens.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
package_include_dirs=()
|
||||
include_count=${#include_arguments[@]}
|
||||
i=0
|
||||
while (( i < include_count )); do
|
||||
entry=${include_arguments[i]}
|
||||
case "$entry" in
|
||||
-I*) convert_to_token "${entry#-I}" include; package_include_dirs+=("$TOKEN_RESULT") ;;
|
||||
-isystem)
|
||||
i=$((i + 1))
|
||||
(( i < include_count )) || fail "Trailing -isystem with no argument"
|
||||
convert_to_token "${include_arguments[i]}" include; package_include_dirs+=("$TOKEN_RESULT") ;;
|
||||
-isystem*) convert_to_token "${entry#-isystem}" include; package_include_dirs+=("$TOKEN_RESULT") ;;
|
||||
*) fail "Unexpected include argument: $entry" ;;
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
package_definitions=()
|
||||
for entry in "${define_arguments[@]}"; do
|
||||
case "$entry" in
|
||||
# Ninja records a quoted define's value with its quotes backslash-escaped (verified
|
||||
# directly: IMGUI_USER_CONFIG=\"aurora/imgui_config.h\" in build.ninja) regardless of
|
||||
# platform - format_cmake_block below re-escapes for CMake's own string syntax, so the
|
||||
# literal backslash has to come out here first or the result is double-escaped and the
|
||||
# define expands to literal backslash-quote characters instead of a quoted string.
|
||||
-D*) package_definitions+=("${entry#-D}") ;;
|
||||
*) fail "Unexpected define argument: $entry" ;;
|
||||
esac
|
||||
done
|
||||
package_definitions=("${package_definitions[@]//\\\"/\"}")
|
||||
|
||||
# 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 through the same targets, and a forced
|
||||
# C++ header include would break them.
|
||||
package_compile_options=()
|
||||
for entry in "${compile_options_raw[@]}"; do
|
||||
package_compile_options+=("\$<\$<COMPILE_LANGUAGE:CXX>:$entry>")
|
||||
done
|
||||
|
||||
package_link_items=()
|
||||
for item in "${link_items[@]}"; do
|
||||
case "$item" in
|
||||
-*) package_link_items+=("$item"); continue ;;
|
||||
esac
|
||||
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
|
||||
package_link_items+=("$ref")
|
||||
continue
|
||||
fi
|
||||
case "$item_abs" in
|
||||
*/libz.so*)
|
||||
# The one unavoidable system reference: libpng's own vendored CMakeLists.txt calls
|
||||
# find_package(ZLIB REQUIRED) internally even when zlib came from aurora's own
|
||||
# FetchContent (extern/CMakeLists.txt writes a redirect config for exactly this case),
|
||||
# and CMAKE_DISABLE_FIND_PACKAGE_ZLIB errors out on any REQUIRED call site outright
|
||||
# (verified directly), so it cannot be forced to vendor/static the way absl/PNG/Freetype
|
||||
# are above. zlib is as close to a universal baseline as a Linux shared library gets
|
||||
# (glibc-adjacent - practically every distro has it already), so this is recorded as a
|
||||
# portable `-lz` link flag instead of the harvesting machine's absolute path.
|
||||
package_link_items+=("-lz")
|
||||
continue ;;
|
||||
esac
|
||||
fail "Link item is neither a system library nor a harvested archive: $item"
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Emit the consumer-facing CMake description.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
format_cmake_block() {
|
||||
# $1 = CMake variable name, remaining args = list items
|
||||
local name=$1; shift
|
||||
if [[ $# -eq 0 ]]; then
|
||||
printf 'set(%s "")\n' "$name"
|
||||
return
|
||||
fi
|
||||
printf 'set(%s\n' "$name"
|
||||
local item escaped
|
||||
for item in "$@"; do
|
||||
escaped=${item//\\/\\\\}
|
||||
escaped=${escaped//\"/\\\"}
|
||||
printf ' "%s"\n' "$escaped"
|
||||
done
|
||||
printf ')\n'
|
||||
}
|
||||
|
||||
dawn_config_dir_meta=$(get_meta dawn_config_dir)
|
||||
dawn_config_token=""
|
||||
if [[ -n "$dawn_config_dir_meta" ]]; then
|
||||
dawn_config_dir_abs=$(normalize "$dawn_config_dir_meta")
|
||||
case "$dawn_config_dir_abs" in
|
||||
"$binary_root"/*)
|
||||
# Dawn's find_package() config directory (lib/cmake/Dawn under its own fetched package
|
||||
# root) cannot go through the generic single-directory copy other build-tree artifacts
|
||||
# use: DawnConfig.cmake/DawnTargets.cmake derive _IMPORT_PREFIX/PACKAGE_PREFIX_DIR from
|
||||
# their OWN file location by walking up three parent directories (verified directly), so
|
||||
# the whole package root - include/, lib/, lib/cmake/Dawn/ together - must be copied as
|
||||
# one self-contained unit for that relative navigation to still resolve once moved.
|
||||
dawn_package_root=$(normalize "$dawn_config_dir_abs/../../..")
|
||||
dawn_config_relative=${dawn_config_dir_abs#$dawn_package_root/}
|
||||
mkdir -p "$output_dir/include/dawn_package"
|
||||
cp -a "$dawn_package_root/." "$output_dir/include/dawn_package/"
|
||||
dawn_config_token="@PKG@/include/dawn_package/$dawn_config_relative"
|
||||
;;
|
||||
*) convert_to_token "$dawn_config_dir_abs" include; dawn_config_token=$TOKEN_RESULT ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
generated_cmake="$output_dir/native_prebuilt.cmake"
|
||||
{
|
||||
echo "# Generated by Launcher/Prepare-NativePrebuilt.sh - do not edit."
|
||||
echo "#"
|
||||
echo "# Describes the precompiled aurora + third-party archives that replace a"
|
||||
echo "# from-source aurora-main build. Every path is a token resolved against the"
|
||||
echo "# consuming workspace, because the source trees still ship next to this"
|
||||
echo "# package and the runtime compiles against their headers."
|
||||
echo ""
|
||||
echo 'if(NOT MKW_NP_PACKAGE_DIR OR NOT MKW_NP_AURORA_DIR OR NOT MKW_NP_RUNTIME_DIR OR NOT MKW_NP_DEPS_DIR)'
|
||||
echo ' message(FATAL_ERROR "native_prebuilt.cmake must be included by runtime/cmake/NativePrebuilt.cmake")'
|
||||
echo 'endif()'
|
||||
echo ""
|
||||
format_cmake_block MKW_NP_INCLUDE_DIRECTORIES "${package_include_dirs[@]}"
|
||||
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_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"
|
||||
printf 'set(MKW_NP_DAWN_VERSION "%s")\n' "$(get_meta aurora_dawn_version)"
|
||||
echo ""
|
||||
echo "# Shared third-party libraries keep imported SHARED targets so that"
|
||||
echo '# $<TARGET_RUNTIME_DLLS> still copies them next to the game executable.'
|
||||
for idx in "${!shared_import_targets[@]}"; do
|
||||
printf 'add_library(%s SHARED IMPORTED GLOBAL)\n' "${shared_import_targets[$idx]}"
|
||||
printf 'set_target_properties(%s PROPERTIES\n' "${shared_import_targets[$idx]}"
|
||||
printf ' IMPORTED_LOCATION "${MKW_NP_PACKAGE_DIR}/%s"\n' "${shared_import_runtime[$idx]}"
|
||||
printf ' IMPORTED_IMPLIB "${MKW_NP_PACKAGE_DIR}/%s")\n' "${shared_import_implib[$idx]}"
|
||||
done
|
||||
} > "$generated_cmake"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Provenance: the pinned toolchain is what makes a precompiled archive interchangeable with
|
||||
# locally compiled objects (bit-for-bit the compiler and flag set the user's machine will use);
|
||||
# LocalBuild.ps1's Windows equivalent re-checks both before consuming a package, and a Linux
|
||||
# consumer-side check (once local-build.sh gains --native-prebuilt-dir) should do the same.
|
||||
# compiler_sha256/flag_fingerprint/aurora_fingerprint/third_party_fingerprint were already computed
|
||||
# above (needed before the --print-fingerprint-only early exit).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
dawn_runtime_sha256=""
|
||||
[[ -n "$dawn_runtime_file" && -f "$dawn_runtime_file" ]] && dawn_runtime_sha256=$(sha256_of "$dawn_runtime_file")
|
||||
[[ -n "$dawn_runtime_sha256" ]] || fail "The Dawn runtime could not be hashed: $dawn_runtime_file"
|
||||
|
||||
compiler_version=$(get_meta cxx_compiler_version)
|
||||
sdl3_target=$(get_meta aurora_sdl3_target)
|
||||
dawn_version=$(get_meta aurora_dawn_version)
|
||||
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'
|
||||
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:]
|
||||
|
||||
contents = []
|
||||
for root, dirs, files in os.walk(output_dir):
|
||||
dirs.sort()
|
||||
for name in sorted(files):
|
||||
if name == "provenance.json":
|
||||
continue
|
||||
path = os.path.join(root, name)
|
||||
rel = os.path.relpath(path, output_dir).replace(os.sep, "/")
|
||||
with open(path, "rb") as fh:
|
||||
digest = hashlib.sha256(fh.read()).hexdigest()
|
||||
contents.append({"Path": rel, "Bytes": os.path.getsize(path), "Sha256": digest})
|
||||
contents.sort(key=lambda c: c["Path"])
|
||||
|
||||
provenance = {
|
||||
"SchemaVersion": 1,
|
||||
"BuiltUtc": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
||||
"CompilerSha256": compiler_sha256,
|
||||
"CompilerVersion": compiler_version,
|
||||
"FlagFingerprint": flag_fingerprint,
|
||||
"DawnVersion": dawn_version,
|
||||
"DawnRuntimeSha256": dawn_runtime_sha256,
|
||||
"AuroraSourceFingerprint": aurora_fingerprint,
|
||||
"ThirdPartySourceFingerprint": third_party_fingerprint,
|
||||
"Sdl3Target": sdl3_target,
|
||||
"HarvestedLibraryCount": int(harvested_count),
|
||||
"Contents": contents,
|
||||
}
|
||||
with open(os.path.join(output_dir, "provenance.json"), "w", encoding="utf-8") as fh:
|
||||
json.dump(provenance, fh, indent=2)
|
||||
fh.write("\n")
|
||||
|
||||
total_bytes = sum(c["Bytes"] for c in contents)
|
||||
print(f" files: {len(contents)}; size: {total_bytes / (1024 * 1024):.1f} MiB")
|
||||
PY
|
||||
|
||||
if [[ "$keep_stage" -eq 0 ]]; then rm -rf "$stage_dir"; fi
|
||||
|
||||
echo ""
|
||||
echo "Native prebuilt package: $output_dir"
|
||||
echo " archives: $(find "$output_dir/lib" -maxdepth 1 -type f | wc -l); shared imports: ${#shared_import_targets[@]}"
|
||||
echo " flag fingerprint: $flag_fingerprint"
|
||||
@@ -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.
|
||||
|
||||
@@ -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,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace>
|
||||
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
|
||||
<Version>0.2.22</Version>
|
||||
<Version>0.2.32</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -53,9 +53,22 @@ public static class RetroWfcPayload
|
||||
|
||||
var root = Path.GetFullPath(stagedDirectory);
|
||||
var payload = Path.Combine(root, RetroWfcOfflinePayloadFile);
|
||||
if (!File.Exists(payload))
|
||||
try
|
||||
{
|
||||
if ((File.GetAttributes(payload) & FileAttributes.Directory) != 0)
|
||||
throw new InvalidDataException(
|
||||
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
|
||||
}
|
||||
ValidateRetroWfcPayloadFile(payload, signingKey);
|
||||
return root;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
|
||||
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
|
||||
<Version>0.2.22</Version>
|
||||
<Version>0.2.32</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description>
|
||||
|
||||
@@ -12,7 +12,10 @@ internal static class BuildRunner
|
||||
public static async Task RunAsync(
|
||||
string workspace, string profile, string outputDir, string? baseOutputDir,
|
||||
string? retroDir, string? retroWfcOfflineDir, bool skipRetroWfcPayload,
|
||||
bool forceCleanBuild, string? translatorBin, IInstallReporter reporter, CancellationToken cancellationToken)
|
||||
bool forceCleanBuild, string? translatorBin, string? ccBin, string? cxxBin, string? fuseLd,
|
||||
string? cmakeBin, string? ninjaBin, string? nativePrebuiltDir, string? sysroot,
|
||||
IInstallReporter reporter,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var script = Path.Combine(workspace, "Launcher", "local-build.sh");
|
||||
if (!File.Exists(script)) throw new FileNotFoundException("local-build.sh is missing", script);
|
||||
@@ -47,6 +50,38 @@ internal static class BuildRunner
|
||||
{
|
||||
startInfo.ArgumentList.Add("--translator-bin"); startInfo.ArgumentList.Add(translatorBin);
|
||||
}
|
||||
// Forwarded by AppRun so the AppImage's bundled clang/lld (see prepare-portable-clang.sh)
|
||||
// is used instead of local-build.sh's own default of whatever clang is on $PATH.
|
||||
if (!string.IsNullOrEmpty(ccBin))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--cc"); startInfo.ArgumentList.Add(ccBin);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(cxxBin))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--cxx"); startInfo.ArgumentList.Add(cxxBin);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(fuseLd))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--fuse-ld"); startInfo.ArgumentList.Add(fuseLd);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(cmakeBin))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--cmake"); startInfo.ArgumentList.Add(cmakeBin);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(ninjaBin))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--ninja"); startInfo.ArgumentList.Add(ninjaBin);
|
||||
}
|
||||
// Forwarded by AppRun so the AppImage's bundled precompiled aurora/third-party package (see
|
||||
// Prepare-NativePrebuilt.sh) is used instead of local-build.sh compiling aurora-main itself.
|
||||
if (!string.IsNullOrEmpty(nativePrebuiltDir))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--native-prebuilt-dir"); startInfo.ArgumentList.Add(nativePrebuiltDir);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(sysroot))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--sysroot"); startInfo.ArgumentList.Add(sysroot);
|
||||
}
|
||||
|
||||
using var process = new Process { StartInfo = startInfo };
|
||||
var window = new BuildProgressWindow(reporter, InstallStages.Build, start: 6, end: 96);
|
||||
|
||||
@@ -3,7 +3,7 @@ namespace WiiCompiled.Setup.Linux;
|
||||
internal static class ProductInfo
|
||||
{
|
||||
public const string Name = "WiiCompiled";
|
||||
public const string Version = "0.2.22";
|
||||
public const string Version = "0.2.32";
|
||||
}
|
||||
|
||||
/// <summary>One installed product's record inside install-state.json.</summary>
|
||||
|
||||
@@ -143,6 +143,15 @@ internal static class Program
|
||||
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,
|
||||
@@ -150,6 +159,13 @@ internal static class Program
|
||||
skipPayload,
|
||||
flags.ContainsKey("force-clean-build"),
|
||||
flags.GetValueOrDefault("translator-bin"),
|
||||
flags.GetValueOrDefault("cc"),
|
||||
flags.GetValueOrDefault("cxx"),
|
||||
flags.GetValueOrDefault("fuse-ld"),
|
||||
flags.GetValueOrDefault("cmake"),
|
||||
flags.GetValueOrDefault("ninja"),
|
||||
flags.GetValueOrDefault("native-prebuilt-dir"),
|
||||
sysroot,
|
||||
reporter, token);
|
||||
|
||||
reporter.Progress(InstallStages.Shortcuts, "Creating shortcuts", 98);
|
||||
@@ -317,7 +333,8 @@ internal static class Program
|
||||
install [--game ISO_PATH] [--install-dir DIR] [--retro-dir DIR
|
||||
{--download-retro-wfc-payload | --skip-retro-wfc-payload}]
|
||||
[--force-clean-build] [--translator-bin PATH] [--disc-tool-bin PATH]
|
||||
[--progress-json] [--workspace DIR]
|
||||
[--cc PATH] [--cxx PATH] [--fuse-ld NAME_OR_PATH] [--cmake PATH] [--ninja PATH]
|
||||
[--native-prebuilt-dir DIR] [--sysroot PATH] [--progress-json] [--workspace DIR]
|
||||
uninstall
|
||||
launch-base
|
||||
launch-retro
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>WiiCompiled.Setup.Linux</AssemblyName>
|
||||
<RootNamespace>WiiCompiled.Setup.Linux</RootNamespace>
|
||||
<Version>0.2.22</Version>
|
||||
<Version>0.2.32</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Command-line installer and launcher for WiiCompiled on Linux</Description>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -121,7 +121,7 @@ internal static class PlatformChecks
|
||||
internal static class ProductInfo
|
||||
{
|
||||
public const string Name = "WiiCompiled";
|
||||
public const string Version = "0.2.25";
|
||||
public const string Version = "0.2.32";
|
||||
|
||||
/// <summary>
|
||||
/// The setup executable is copied into the installation under this name. It is the launcher and
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<AssemblyName>WiiCompiled.Setup</AssemblyName>
|
||||
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Version>0.2.25</Version>
|
||||
<Version>0.2.32</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Command-line installer and launcher for WiiCompiled</Description>
|
||||
|
||||
Regular → Executable
+144
-14
@@ -5,8 +5,12 @@
|
||||
# self-contained binaries, and `nodtool` (a prebuilt MIT/Apache-2.0 CLI from encounter/nod, see
|
||||
# NodToolProvider.cs) is downloaded and bundled too - AppRun passes --translator-bin and
|
||||
# --disc-tool-bin so local-build.sh/DiscTool.cs skip their from-source/download fallbacks entirely.
|
||||
# It still shells out to system clang/cmake/ninja - no C/C++ toolchain is bundled, matching
|
||||
# Launcher/local-build.sh's own remaining prerequisites.
|
||||
# A pruned native clang/lld/cmake/ninja toolchain (see prepare-portable-tools.sh) is bundled the
|
||||
# same way - AppRun passes --cc/--cxx/--fuse-ld/--cmake/--ninja so local-build.sh never has to find
|
||||
# a system compiler, CMake, or Ninja. It still shells out to system pkg-config and Vulkan headers,
|
||||
# matching Launcher/local-build.sh's own remaining prerequisites. A precompiled aurora +
|
||||
# third-party package (see Prepare-NativePrebuilt.sh) is bundled the same way too - AppRun passes
|
||||
# --native-prebuilt-dir so local-build.sh never compiles aurora-main from source at all.
|
||||
#
|
||||
# An AppImage mounts read-only, but local-build.sh writes generated/, native-build/, Assets/, etc.
|
||||
# into the workspace it's given. So AppRun (written below) copies the bundled workspace snapshot
|
||||
@@ -15,12 +19,58 @@
|
||||
# - generated/native-build/Assets/PulsarPacks live only in that writable cache and are never
|
||||
# touched by the sync, so local-build.sh's own incremental caching survives across runs and across
|
||||
# AppImage updates. translator/ isn't part of this snapshot at all: it's published as its own
|
||||
# self-contained binary (usr/bin/translator-cli) below and never needs a writable copy.
|
||||
# self-contained binary (usr/bin/translator-cli) below and never needs a writable copy. Neither
|
||||
# native-prebuilt/ nor the toolchain are copied into the cache either - both are large
|
||||
# (~90 MiB / ~500 MiB) and local-build.sh only ever reads from them - but AppRun does point
|
||||
# $CACHE/toolchain and $CACHE/native-prebuilt symlinks at the current mount on every single launch
|
||||
# (see AppRun's own comment): an AppImage's FUSE mount is at a fresh random /tmp/.mount_XXXXXX
|
||||
# every run, and CMake bakes whatever compiler/tool path it's given directly into each
|
||||
# build.ninja rule's command line, so referencing $HERE straight would change that command line -
|
||||
# and Ninja reruns any rule whose command line changed - forcing a full rebuild on every single
|
||||
# launch even though the compiler itself never actually changed. The symlink keeps the path
|
||||
# string CMake/Ninja see identical across runs while what it resolves to tracks the current mount
|
||||
# underneath.
|
||||
set -euo pipefail
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
workspace=$(cd "$script_dir/.." && pwd)
|
||||
|
||||
# `uname -m` reports the *kernel's* architecture, which can differ from userspace - an aarch64
|
||||
# kernel can run a 32-bit armhf userland (as shipped by 32-bit Raspberry Pi OS), same as an x86_64
|
||||
# kernel can run an i686 one. What matters here is which userspace binaries (dotnet, appimagetool)
|
||||
# will actually run, so this reads the ELF header of this script's own running bash interpreter -
|
||||
# real userspace - rather than trusting the kernel's self-report. /proc/$$/exe (not /proc/self/exe:
|
||||
# that would resolve inside the readlink subprocess below, to readlink itself, not to bash) is this
|
||||
# shell's own PID. EI_CLASS (byte 4: 1=32-bit, 2=64-bit) and e_machine (bytes 18-19: 3=EM_386,
|
||||
# 40=EM_ARM, 62=EM_X86_64, 183=EM_AARCH64) are read as plain little-endian bytes, which every
|
||||
# real-world x86/ARM Linux userland uses; ELF's big-endian encoding is a non-issue here since no
|
||||
# Linux distro ships a big-endian x86 or ARM userland.
|
||||
elf_exe=$(readlink -f "/proc/$$/exe")
|
||||
elf_class=$(od -An -t u1 -j 4 -N 1 "$elf_exe" | tr -d ' ')
|
||||
elf_machine_lo=$(od -An -t u1 -j 18 -N 1 "$elf_exe" | tr -d ' ')
|
||||
elf_machine_hi=$(od -An -t u1 -j 19 -N 1 "$elf_exe" | tr -d ' ')
|
||||
elf_machine=$(( elf_machine_hi * 256 + elf_machine_lo ))
|
||||
|
||||
# Mirrors the host-architecture detection NodToolProvider.cs already does (RuntimeInformation.
|
||||
# OSArchitecture) so this script's own dotnet RID and appimagetool selection agree with the
|
||||
# nodtool binary that same code path resolves below. local-build.sh needs no such mapping itself:
|
||||
# it just drives the native CMake configure, which already accepts x86_64 or aarch64 natively
|
||||
# (see runtime/CMakeLists.txt's CMAKE_SYSTEM_PROCESSOR check).
|
||||
case "$elf_class:$elf_machine" in
|
||||
2:62)
|
||||
dotnet_rid=linux-x64
|
||||
appimagetool_arch=x86_64
|
||||
;;
|
||||
2:183)
|
||||
dotnet_rid=linux-arm64
|
||||
appimagetool_arch=aarch64
|
||||
;;
|
||||
*)
|
||||
echo "build-appimage.sh: unsupported userspace architecture (ELF class $elf_class, machine $elf_machine) - WiiCompiled requires a 64-bit x86_64 or aarch64 userland" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
output_dir="$workspace/Launcher/dist"
|
||||
appimagetool_override=""
|
||||
|
||||
@@ -40,10 +90,10 @@ appdir="$workspace/Launcher/artifacts/appimage-build/AppDir"
|
||||
rm -rf "$appdir"
|
||||
mkdir -p "$appdir/usr/bin" "$appdir/workspace/Launcher"
|
||||
|
||||
echo "Publishing the installer (self-contained linux-x64)..."
|
||||
echo "Publishing the installer (self-contained $dotnet_rid)..."
|
||||
publish_tmp="$workspace/Launcher/artifacts/appimage-build/publish"
|
||||
rm -rf "$publish_tmp"
|
||||
dotnet publish "$workspace/Launcher/WiiCompiled.Setup.Linux" -c Release -r linux-x64 \
|
||||
dotnet publish "$workspace/Launcher/WiiCompiled.Setup.Linux" -c Release -r "$dotnet_rid" \
|
||||
--self-contained -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \
|
||||
-o "$publish_tmp"
|
||||
cp "$publish_tmp/WiiCompiled.Setup.Linux" "$appdir/usr/bin/wiicompiled-setup"
|
||||
@@ -52,10 +102,10 @@ chmod +x "$appdir/usr/bin/wiicompiled-setup"
|
||||
# Published as a self-contained binary too, so an AppImage user never needs a `dotnet` SDK on
|
||||
# PATH at all - local-build.sh is told about it via --translator-bin and skips its own
|
||||
# dotnet-build-from-source step entirely (see local-build.sh's translator resolution branch).
|
||||
echo "Publishing the translator (self-contained linux-x64)..."
|
||||
echo "Publishing the translator (self-contained $dotnet_rid)..."
|
||||
translator_publish_tmp="$workspace/Launcher/artifacts/appimage-build/publish-translator"
|
||||
rm -rf "$translator_publish_tmp"
|
||||
dotnet publish "$workspace/translator/src/Translator.Cli" -c Release -r linux-x64 \
|
||||
dotnet publish "$workspace/translator/src/Translator.Cli" -c Release -r "$dotnet_rid" \
|
||||
--self-contained -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \
|
||||
-o "$translator_publish_tmp"
|
||||
cp "$translator_publish_tmp/Translator.Cli" "$appdir/usr/bin/translator-cli"
|
||||
@@ -71,6 +121,47 @@ nodtool_path=$(dotnet run --project "$workspace/Launcher/WiiCompiled.Setup.Commo
|
||||
cp "$nodtool_path" "$appdir/usr/bin/nodtool"
|
||||
chmod +x "$appdir/usr/bin/nodtool"
|
||||
|
||||
echo "Preparing the portable clang/lld/cmake/ninja toolchain ($appimagetool_arch)..."
|
||||
bash "$script_dir/prepare-portable-tools.sh" --arch "$appimagetool_arch"
|
||||
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.
|
||||
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)
|
||||
package_current=0
|
||||
if [[ -f "$native_prebuilt_dir/provenance.json" ]]; then
|
||||
package_current=$(CURRENT_FINGERPRINT="$current_fingerprint" python3 - "$native_prebuilt_dir/provenance.json" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
provenance = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||
current = dict(line.split("=", 1) for line in os.environ["CURRENT_FINGERPRINT"].splitlines() if line)
|
||||
fields = {
|
||||
"compiler_sha256": "CompilerSha256",
|
||||
"flag_fingerprint": "FlagFingerprint",
|
||||
"aurora_fingerprint": "AuroraSourceFingerprint",
|
||||
"third_party_fingerprint": "ThirdPartySourceFingerprint",
|
||||
}
|
||||
print(1 if all(provenance.get(v) == current.get(k) for k, v in fields.items()) else 0)
|
||||
PY
|
||||
)
|
||||
fi
|
||||
if [[ "$package_current" == "1" ]]; then
|
||||
echo "Native prebuilt package is current; reusing $native_prebuilt_dir"
|
||||
else
|
||||
echo "Native prebuilt package is missing or stale; harvesting a fresh one (compiles aurora once, can take a while)..."
|
||||
bash "$script_dir/Prepare-NativePrebuilt.sh" --arch "$appimagetool_arch"
|
||||
fi
|
||||
mkdir -p "$appdir/native-prebuilt"
|
||||
cp -a "$native_prebuilt_dir/." "$appdir/native-prebuilt/"
|
||||
|
||||
echo "Staging the bundled workspace snapshot..."
|
||||
for dir in runtime aurora-main projects; do
|
||||
cp -r "$workspace/$dir" "$appdir/workspace/$dir"
|
||||
@@ -84,8 +175,21 @@ find "$appdir/workspace/aurora-main/extern" -mindepth 1 -maxdepth 1 -type d -exe
|
||||
rm -rf "$appdir/workspace/runtime/build"
|
||||
cp "$workspace/Launcher/local-build.sh" "$appdir/workspace/Launcher/local-build.sh"
|
||||
|
||||
# AppRun re-syncs runtime/aurora-main/projects/local-build.sh into the writable cache only when
|
||||
# this changes, so it must change whenever any of those bundled paths actually did - a bare commit
|
||||
# hash gets this wrong for an uncommitted change (verified directly: rebuilding after editing
|
||||
# local-build.sh with no commit produced the same hash as the stale cache, so AppRun kept serving
|
||||
# the old script and failed on a flag that didn't exist yet). `git status --porcelain` catches both
|
||||
# modified tracked files and new untracked ones; appending a fresh timestamp when it's non-empty
|
||||
# guarantees this never matches a previous build's stamp, forcing a resync every time the tree is
|
||||
# dirty. A clean tree (a real tagged release) keeps the stable commit-hash behavior, so identical
|
||||
# reruns of the same release AppImage don't resync needlessly.
|
||||
if git -C "$workspace" rev-parse HEAD >/dev/null 2>&1; then
|
||||
git -C "$workspace" rev-parse HEAD > "$appdir/workspace/.bundle-version"
|
||||
version=$(git -C "$workspace" rev-parse HEAD)
|
||||
if [[ -n "$(git -C "$workspace" status --porcelain 2>/dev/null)" ]]; then
|
||||
version="$version-dirty-$(date -u +%s)"
|
||||
fi
|
||||
echo "$version" > "$appdir/workspace/.bundle-version"
|
||||
else
|
||||
date -u +%s > "$appdir/workspace/.bundle-version"
|
||||
fi
|
||||
@@ -96,6 +200,7 @@ cat > "$appdir/AppRun" <<'APPRUN'
|
||||
set -euo pipefail
|
||||
HERE="$(dirname "$(readlink -f "$0")")"
|
||||
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"
|
||||
@@ -106,9 +211,31 @@ if [ ! -f "$CACHE/.bundle-version" ] || \
|
||||
cp "$HERE/workspace/Launcher/local-build.sh" "$CACHE/Launcher/local-build.sh"
|
||||
cp "$HERE/workspace/.bundle-version" "$CACHE/.bundle-version"
|
||||
fi
|
||||
# toolchain/ and native-prebuilt/ are NOT copied into the cache (they're large - ~500 MiB /
|
||||
# ~90 MiB - and local-build.sh only ever reads from them): $CACHE/toolchain and
|
||||
# $CACHE/native-prebuilt are symlinks re-pointed at the current mount on every single launch
|
||||
# (unconditionally, not gated on .bundle-version above, since the mount path itself - unlike the
|
||||
# bundled content - changes every run regardless). CMake bakes a compiler/tool path directly into
|
||||
# each build.ninja rule's command line and Ninja reruns any rule whose command line changed since
|
||||
# the last build (verified directly) - an AppImage's FUSE mount is at a fresh random
|
||||
# /tmp/.mount_XXXXXX every launch, so referencing $HERE straight would change that command line,
|
||||
# and therefore force a full rebuild, on every single run even though the compiler itself never
|
||||
# actually changed. A symlink keeps the *path string* CMake/Ninja see identical across runs while
|
||||
# what it resolves to tracks the current mount underneath (verified directly: CMake records
|
||||
# whatever path it's given as-is - including a symlink - without resolving it first).
|
||||
[ -L "$CACHE/toolchain" ] || rm -rf "$CACHE/toolchain"
|
||||
[ -L "$CACHE/native-prebuilt" ] || rm -rf "$CACHE/native-prebuilt"
|
||||
ln -sfn "$HERE/usr/toolchain" "$CACHE/toolchain"
|
||||
ln -sfn "$HERE/native-prebuilt" "$CACHE/native-prebuilt"
|
||||
exec "$HERE/usr/bin/wiicompiled-setup" --workspace "$CACHE" \
|
||||
--translator-bin "$HERE/usr/bin/translator-cli" \
|
||||
--disc-tool-bin "$HERE/usr/bin/nodtool" "$@"
|
||||
--disc-tool-bin "$HERE/usr/bin/nodtool" \
|
||||
--cc "$CACHE/toolchain/bin/clang" \
|
||||
--cxx "$CACHE/toolchain/bin/clang++" \
|
||||
--fuse-ld lld \
|
||||
--cmake "$CACHE/toolchain/bin/cmake" \
|
||||
--ninja "$CACHE/toolchain/bin/ninja" \
|
||||
--native-prebuilt-dir "$CACHE/native-prebuilt" "$@"
|
||||
APPRUN
|
||||
chmod +x "$appdir/AppRun"
|
||||
|
||||
@@ -155,11 +282,13 @@ PY
|
||||
echo "Resolving appimagetool..."
|
||||
appimagetool="$appimagetool_override"
|
||||
if [[ -z "$appimagetool" ]]; then
|
||||
appimagetool="$workspace/Launcher/artifacts/appimagetool"
|
||||
# Cache path is arch-tagged so a workspace shared or synced across an x86_64 and an aarch64
|
||||
# machine never picks up the wrong architecture's cached binary.
|
||||
appimagetool="$workspace/Launcher/artifacts/appimagetool-$appimagetool_arch"
|
||||
if [[ ! -x "$appimagetool" ]]; then
|
||||
echo "Downloading appimagetool..."
|
||||
echo "Downloading appimagetool ($appimagetool_arch)..."
|
||||
mkdir -p "$(dirname "$appimagetool")"
|
||||
curl -fsSL "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" \
|
||||
curl -fsSL "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-$appimagetool_arch.AppImage" \
|
||||
-o "$appimagetool"
|
||||
chmod +x "$appimagetool"
|
||||
fi
|
||||
@@ -169,5 +298,6 @@ mkdir -p "$output_dir"
|
||||
echo "Packaging..."
|
||||
# appimagetool detects the target architecture from the first ELF executable it finds in the
|
||||
# AppDir; AppRun here is a shell script, not ELF, so ARCH must be set explicitly.
|
||||
ARCH=x86_64 "$appimagetool" "$appdir" "$output_dir/WiiCompiled-Setup-x86_64.AppImage"
|
||||
echo "Built: $output_dir/WiiCompiled-Setup-x86_64.AppImage"
|
||||
output_name="WiiCompiled-Setup-$appimagetool_arch.AppImage"
|
||||
ARCH="$appimagetool_arch" "$appimagetool" "$appdir" "$output_dir/$output_name"
|
||||
echo "Built: $output_dir/$output_name"
|
||||
|
||||
Executable
+139
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env bash
|
||||
# Native macOS build automation: optionally extract -> translate -> compile -> publish .app.
|
||||
set -euo pipefail
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
default_workspace=$(cd "$script_dir/.." && pwd)
|
||||
|
||||
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'
|
||||
Usage: local-build-macos.command --output-dir DIR [options]
|
||||
|
||||
--workspace DIR Repository root (default: this script's parent directory)
|
||||
--profile {base|retro-rewind|both} Product to build (default: base)
|
||||
--output-dir DIR Output .app directory (required; Retro Rewind for both)
|
||||
--base-output-dir DIR Base .app directory (required with --profile both)
|
||||
--game IMAGE --nodtool PATH Extract and verify a clean PAL RMCP01 disc image first
|
||||
--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
|
||||
--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
|
||||
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=""
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--workspace) workspace=${2:-}; shift 2 ;;
|
||||
--profile) profile=${2:-}; shift 2 ;;
|
||||
--output-dir) output_dir=${2:-}; shift 2 ;;
|
||||
--base-output-dir) base_output_dir=${2:-}; shift 2 ;;
|
||||
--game) game=${2:-}; shift 2 ;;
|
||||
--nodtool) nodtool=${2:-}; shift 2 ;;
|
||||
--retro-rewind-package-dir) retro_root=${2:-}; shift 2 ;;
|
||||
--retro-wfc-offline-dir) retro_wfc=${2:-}; shift 2 ;;
|
||||
--skip-retro-wfc-payload) skip_retro_wfc=1; shift ;;
|
||||
--force-clean-build) force_clean=1; shift ;;
|
||||
--parallel) parallel=${2:-}; shift 2 ;;
|
||||
--cmake) cmake_bin=${2:-}; shift 2 ;;
|
||||
--ninja) ninja_bin=${2:-}; shift 2 ;;
|
||||
--dotnet) dotnet_bin=${2:-}; shift 2 ;;
|
||||
--translator-bin) translator_bin=${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'
|
||||
workspace=$(cd "$workspace" && pwd)
|
||||
[[ -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
|
||||
if [[ "$profile" == both && -z "$base_output_dir" ]]; then fail '--base-output-dir is required with --profile both'; fi
|
||||
if [[ "$profile" != both && -n "$base_output_dir" ]]; then fail '--base-output-dir is valid only with --profile both'; fi
|
||||
if [[ -n "$game" || -n "$nodtool" ]]; then [[ -n "$game" && -n "$nodtool" ]] || fail '--game and --nodtool must be supplied together'; fi
|
||||
if (( builds_retro )); then
|
||||
[[ -n "$retro_root" ]] || fail '--retro-rewind-package-dir is required for Retro Rewind'
|
||||
[[ -n "$retro_wfc" ]] && (( skip_retro_wfc )) && fail 'choose only one Retro-WFC mode'
|
||||
[[ -n "$retro_wfc" || $skip_retro_wfc -eq 1 ]] || fail 'choose a Retro-WFC payload directory or --skip-retro-wfc-payload'
|
||||
fi
|
||||
for tool in "$cmake_bin" "$ninja_bin" clang clang++ shasum; do command -v "$tool" >/dev/null || fail "required tool not found: $tool"; done
|
||||
|
||||
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"
|
||||
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'
|
||||
|
||||
if (( force_clean )); then
|
||||
step force-clean 'Discarding translation and native build caches'
|
||||
rm -rf "$generated" "$manifest_dir" "$native_build"
|
||||
fi
|
||||
|
||||
if [[ -n "$translator_bin" ]]; then
|
||||
assert_file "$translator_bin" 'Translator.Cli executable'
|
||||
translator() { "$translator_bin" "$@"; }
|
||||
else
|
||||
command -v "$dotnet_bin" >/dev/null || fail "required tool not found: $dotnet_bin"
|
||||
translator_dll="$workspace/translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll"
|
||||
if [[ ! -f "$translator_dll" ]]; then
|
||||
step build-translator 'Building the translator'
|
||||
"$dotnet_bin" build "$workspace/translator/src/Translator.Cli/Translator.Cli.csproj" -c Release
|
||||
fi
|
||||
translator() { "$dotnet_bin" "$translator_dll" "$@"; }
|
||||
fi
|
||||
|
||||
entry_point=$(awk '/^translation:/{inside=1} inside && /^[[:space:]]*-[[:space:]]*0[xX][0-9a-fA-F]+[[:space:]]*$/{gsub(/^[[:space:]]*-[[:space:]]*/, ""); print; exit}' "$project")
|
||||
[[ -n "$entry_point" ]] || fail 'could not find the translation entry point'
|
||||
cpu=$(sysctl -n hw.ncpu); mem_gib=$(( $(sysctl -n hw.memsize) / 1024 / 1024 / 1024 )); (( mem_gib < 1 )) && mem_gib=1
|
||||
if (( parallel > 0 )); then translator_threads=$parallel; translated_jobs=$parallel; global_jobs=$parallel
|
||||
else translator_threads=$(( cpu < 16 ? cpu : 16 )); translated_jobs=$(( cpu < mem_gib / 2 ? cpu : mem_gib / 2 )); (( translated_jobs < 1 )) && translated_jobs=1; global_jobs=$cpu; fi
|
||||
|
||||
if (( builds_retro )); then
|
||||
retro_root=$(cd "$retro_root" && pwd)
|
||||
if [[ ! -f "$retro_root/Binaries/Code.pul" && -f "$retro_root/RetroRewind6/Binaries/Code.pul" ]]; then
|
||||
retro_root="$retro_root/RetroRewind6"
|
||||
fi
|
||||
assert_file "$retro_root/Binaries/Code.pul" 'Retro Rewind Code.pul'
|
||||
stage="$workspace/PulsarPacks/completed/RetroRewind/RetroRewind6/Binaries"
|
||||
mkdir -p "$stage"
|
||||
if [[ ! "$retro_root/Binaries/Code.pul" -ef "$stage/Code.pul" ]]; then
|
||||
cp -f "$retro_root/Binaries/Code.pul" "$stage/Code.pul"
|
||||
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
|
||||
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
|
||||
step translate-mod 'Translating Retro Rewind'; translator "${args[@]}"
|
||||
fi
|
||||
step generate-data-init 'Generating local game data initialization'; translator generate-data-init --project "$project"
|
||||
args=(emit-build-shards --project "$project" --base-metadata "$metadata" --base-functions-dir "$functions" --native-source-dir "$workspace/runtime/src" --out "$shards")
|
||||
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[@]}"
|
||||
|
||||
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"
|
||||
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
|
||||
printf 'MKWCBUILD:OUTPUT=%s\n' "$output_dir"
|
||||
+111
-12
@@ -2,11 +2,12 @@
|
||||
# Linux build automation: translate -> emit build shards -> configure -> compile -> publish.
|
||||
#
|
||||
# This is the native-Linux counterpart to Launcher/LocalBuild.ps1. It is a from-scratch parallel
|
||||
# implementation, not a port of NativeBuildFlags.ps1: that file's canonical flags and
|
||||
# prebuilt-package fingerprinting exist only for the Windows/mingw toolchain (a precompiled
|
||||
# aurora/third-party package, offline pinned dependencies) that this script does not build.
|
||||
# Linux always builds aurora from source, letting its own CMake auto-detect Vulkan + vendor
|
||||
# SDL3/Dawn via FetchContent - the same configuration already verified working by hand.
|
||||
# implementation, not a port of NativeBuildFlags.ps1: that file's canonical flags exist only for
|
||||
# the Windows/mingw toolchain's offline pinned dependencies, which this script does not use.
|
||||
# Without --native-prebuilt-dir, aurora is built from source, letting its own CMake auto-detect
|
||||
# Vulkan + vendor SDL3/Dawn via FetchContent - the same configuration already verified working by
|
||||
# hand. With it, aurora is not compiled at all - see Launcher/Prepare-NativePrebuilt.sh, which
|
||||
# harvests exactly that package (the Linux counterpart to Prepare-NativePrebuilt.ps1).
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -62,6 +63,9 @@ ninja_override=""
|
||||
dotnet_override=""
|
||||
translator_dll_override=""
|
||||
translator_bin_override=""
|
||||
fuse_ld_override=""
|
||||
native_prebuilt_dir=""
|
||||
sysroot=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
@@ -77,10 +81,15 @@ Usage: local-build.sh --output-dir DIR [options]
|
||||
--force-clean-build Discard every translation/build cache first
|
||||
--parallel N Pin translator threads, translated-shard job pool, and Ninja parallelism to N
|
||||
--cc PATH / --cxx PATH C/C++ compiler (default: cc/c++ on PATH)
|
||||
--fuse-ld NAME_OR_PATH Linker passed to clang as -fuse-ld=NAME_OR_PATH (default: clang's own default linker)
|
||||
--cmake PATH / --ninja PATH Build tools (default: on PATH)
|
||||
--dotnet PATH dotnet executable (default: on PATH)
|
||||
--translator-dll PATH Pre-built Translator.Cli.dll (skips building the translator; still needs --dotnet to run it)
|
||||
--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
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -97,11 +106,14 @@ while [[ $# -gt 0 ]]; do
|
||||
--parallel) parallel_override=$2; shift 2 ;;
|
||||
--cc) cc_override=$2; shift 2 ;;
|
||||
--cxx) cxx_override=$2; shift 2 ;;
|
||||
--fuse-ld) fuse_ld_override=$2; shift 2 ;;
|
||||
--cmake) cmake_override=$2; shift 2 ;;
|
||||
--ninja) ninja_override=$2; shift 2 ;;
|
||||
--dotnet) dotnet_override=$2; shift 2 ;;
|
||||
--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 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown argument: $1" ;;
|
||||
esac
|
||||
@@ -157,6 +169,10 @@ require_command "$ninja_bin" ninja
|
||||
require_command "$cc_bin" cc
|
||||
require_command "$cxx_bin" cxx
|
||||
|
||||
if [[ -n "$native_prebuilt_dir" ]]; then
|
||||
assert_file "$native_prebuilt_dir/native_prebuilt.cmake" "Native prebuilt package"
|
||||
fi
|
||||
|
||||
project=$workspace/projects/mkwii/recomp.yml
|
||||
assets=$workspace/Assets
|
||||
generated=$workspace/generated
|
||||
@@ -174,6 +190,30 @@ assert_file "$project" "Translation project"
|
||||
assert_file "$assets/main.dol" "Extracted main.dol (see translator/README.md - owning the game is required)"
|
||||
assert_file "$assets/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.
|
||||
@@ -336,26 +376,83 @@ translator "${shard_args[@]}"
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
keep_native_build=0
|
||||
stale_reason=""
|
||||
if [[ -f "$build/CMakeCache.txt" ]]; then
|
||||
expected_home=$workspace/runtime
|
||||
cache_home=$(grep '^CMAKE_HOME_DIRECTORY:INTERNAL=' "$build/CMakeCache.txt" | cut -d= -f2- || true)
|
||||
if [[ -n "$cache_home" && "$(cd "$cache_home" 2>/dev/null && pwd)" == "$expected_home" ]]; then
|
||||
keep_native_build=1
|
||||
# CMake auto-detects and caches auxiliary tool paths (CMAKE_AR/RANLIB/LINKER/ASM_COMPILER
|
||||
# and their per-language *_AR/*_RANLIB variants) only once, the first time a language's
|
||||
# compiler is checked - unlike CMAKE_C_COMPILER/CMAKE_CXX_COMPILER, which get overwritten by
|
||||
# the -D flags below on every configure, these are never refreshed on a plain reconfigure.
|
||||
# An AppImage's own AppRun works around this at the source by keeping --cc/--cxx/--cmake/
|
||||
# --ninja pointed at a stable symlink it re-targets at the current mount every launch
|
||||
# (see build-appimage.sh), so the *path string* CMake caches never actually changes run to
|
||||
# run - but this check stays as a general fallback for any tool path that goes stale some
|
||||
# other way (a moved/removed system toolchain, a relocated portable-tools directory, etc):
|
||||
# any :FILEPATH= cache entry whose recorded path no longer exists means this cache belongs
|
||||
# to a toolchain location that's gone - not just a workspace/path mismatch.
|
||||
while IFS= read -r tool_path; do
|
||||
[[ -n "$tool_path" ]] || continue
|
||||
# CMake's own sentinel for "this optional tool was legitimately never found" (e.g.
|
||||
# clang-scan-deps, not required here) - not a path at all, and not a sign of anything
|
||||
# stale. Without this exclusion, every configure wiped the cache unconditionally.
|
||||
[[ "$tool_path" != *-NOTFOUND ]] || continue
|
||||
if [[ ! -e "$tool_path" ]]; then
|
||||
keep_native_build=0
|
||||
stale_reason=" (cached tool path no longer exists: $tool_path)"
|
||||
break
|
||||
fi
|
||||
done < <(grep -o ':FILEPATH=.*' "$build/CMakeCache.txt" | sed 's/^:FILEPATH=//')
|
||||
fi
|
||||
fi
|
||||
if [[ -d "$build" && "$keep_native_build" -eq 0 ]]; then
|
||||
echo "MKWCBUILD: The native build cache does not belong to this workspace path; rebuilding from scratch"
|
||||
echo "MKWCBUILD: The native build cache does not belong to this workspace path or toolchain; rebuilding from scratch$stale_reason"
|
||||
rm -rf "$build"
|
||||
elif [[ "$keep_native_build" -eq 1 ]]; then
|
||||
echo "MKWCBUILD: Reusing the incremental native build directory"
|
||||
fi
|
||||
|
||||
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")
|
||||
# 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
|
||||
# inherit that stability just because $cc_bin does: verified directly that even with a stable
|
||||
# --cc symlink, CMake's own auto-detection of these still resolved to the *real*, ephemeral mount
|
||||
# path underneath (clang's own driver locates its sibling llvm-ar/ld.lld tools by resolving its own
|
||||
# invoked path, symlinks included, rather than trusting the symlink CMake invoked it through) -
|
||||
# each subsequent run's differing command line then made Ninja rebuild every object from scratch
|
||||
# even though nothing had actually changed. Deriving these from $cc_bin (itself already stable)
|
||||
# and re-passing them explicitly every configure keeps them pinned to the same stable value too.
|
||||
if [[ "$cc_bin" == */* ]]; then
|
||||
toolchain_bin=$(dirname "$cc_bin")
|
||||
[[ -x "$toolchain_bin/llvm-ar" ]] && configure_args+=(-DCMAKE_AR="$toolchain_bin/llvm-ar" -DCMAKE_ASM_COMPILER_AR="$toolchain_bin/llvm-ar" -DCMAKE_C_COMPILER_AR="$toolchain_bin/llvm-ar" -DCMAKE_CXX_COMPILER_AR="$toolchain_bin/llvm-ar")
|
||||
[[ -x "$toolchain_bin/llvm-ranlib" ]] && configure_args+=(-DCMAKE_RANLIB="$toolchain_bin/llvm-ranlib" -DCMAKE_ASM_COMPILER_RANLIB="$toolchain_bin/llvm-ranlib" -DCMAKE_C_COMPILER_RANLIB="$toolchain_bin/llvm-ranlib" -DCMAKE_CXX_COMPILER_RANLIB="$toolchain_bin/llvm-ranlib")
|
||||
[[ -x "$toolchain_bin/ld.lld" ]] && configure_args+=(-DCMAKE_LINKER="$toolchain_bin/ld.lld")
|
||||
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")
|
||||
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" -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"
|
||||
"$cmake_bin" "${configure_args[@]}"
|
||||
|
||||
case "$profile" in
|
||||
base) targets=(WiiCompiled) ;;
|
||||
@@ -384,7 +481,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/"
|
||||
|
||||
Executable
+117
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
# Maintainer release builder. It packages setup/source/tooling only -- never a
|
||||
# translated executable, extracted DATA tree, disc image, or Retro Rewind data.
|
||||
set -euo pipefail
|
||||
|
||||
fail() { printf 'build-setup-pkg.command: error: %s\n' "$*" >&2; exit 1; }
|
||||
# Release payloads must not inherit Finder metadata, resource forks, or a
|
||||
# downloaded-file quarantine bit from a maintainer's working volume.
|
||||
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]
|
||||
|
||||
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.
|
||||
|
||||
--workspace DIR Repository root (default: script's grandparent)
|
||||
--version VERSION Bundle/package version (default: 0.1.0)
|
||||
--installer-identity NAME Developer ID Installer identity for productbuild
|
||||
EOF
|
||||
}
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
workspace=$(cd "$script_dir/../.." && pwd); nodtool=""; translator=""; cmake_root=""; ninja=""; output=""; version=0.1.0; identity=""
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--workspace) workspace=${2:-}; shift 2 ;;
|
||||
--nodtool) nodtool=${2:-}; shift 2 ;;
|
||||
--translator) translator=${2:-}; shift 2 ;;
|
||||
--cmake-root) cmake_root=${2:-}; shift 2 ;;
|
||||
--ninja) ninja=${2:-}; shift 2 ;;
|
||||
--output) output=${2:-}; shift 2 ;;
|
||||
--version) version=${2:-}; shift 2 ;;
|
||||
--installer-identity) identity=${2:-}; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
version=${version#v}
|
||||
[[ "$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'
|
||||
[[ -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'
|
||||
workspace=$(cd "$workspace" && pwd); output=$(cd "$(dirname "$output")" && pwd)/$(basename "$output")
|
||||
stage=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-pkg.XXXXXX")
|
||||
trap 'rm -rf "$stage"' EXIT
|
||||
app="$stage/root/Applications/WiiCompiled Setup.app"
|
||||
resources="$app/Contents/Resources"
|
||||
mkdir -p "$app/Contents/MacOS" "$resources/tools"
|
||||
cat > "$app/Contents/Info.plist" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict>
|
||||
<key>CFBundleExecutable</key><string>WiiCompiledSetup</string>
|
||||
<key>CFBundleIdentifier</key><string>org.wiicompiled.setup</string>
|
||||
<key>CFBundleName</key><string>WiiCompiled Setup</string>
|
||||
<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>
|
||||
</dict></plist>
|
||||
EOF
|
||||
cat > "$app/Contents/MacOS/WiiCompiledSetup" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
resources="$(cd "$(dirname "$0")/../Resources" && pwd)"
|
||||
|
||||
# Finder launches an app with no terminal attached. The setup work deliberately
|
||||
# writes human-readable build progress to stdout, so run its .command entry
|
||||
# point in Terminal instead of discarding that output behind an inert app icon.
|
||||
exec /usr/bin/osascript - "$resources/setup.command" "$@" <<'APPLESCRIPT'
|
||||
on run argv
|
||||
set commandLine to quoted form of (item 1 of argv)
|
||||
if (count of argv) > 1 then
|
||||
repeat with argumentIndex from 2 to (count of argv)
|
||||
set commandLine to commandLine & " " & quoted form of (item argumentIndex of argv)
|
||||
end repeat
|
||||
end if
|
||||
tell application "Terminal"
|
||||
activate
|
||||
do script commandLine
|
||||
end tell
|
||||
end run
|
||||
APPLESCRIPT
|
||||
EOF
|
||||
chmod +x "$app/Contents/MacOS/WiiCompiledSetup"
|
||||
copy_clean "$script_dir/setup.command" "$resources/setup.command"; chmod +x "$resources/setup.command"
|
||||
# Copy only the build inputs. This deliberately avoids a maintainer's ignored
|
||||
# output directories, local disc extraction, and developer-only packaging.
|
||||
mkdir -p "$resources/workspace"
|
||||
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
|
||||
mkdir -p "$resources/workspace/Launcher/macos"
|
||||
copy_clean "$workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/local-build-macos.command"
|
||||
copy_clean "$workspace/Launcher/macos/extract-disc.command" "$resources/workspace/Launcher/macos/extract-disc.command"
|
||||
copy_clean "$workspace/Launcher/macos/publish-app.command" "$resources/workspace/Launcher/macos/publish-app.command"
|
||||
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"
|
||||
copy_clean "$cmake_root" "$resources/tools/cmake"
|
||||
copy_clean "$ninja" "$resources/tools/ninja"; chmod +x "$resources/tools/ninja"
|
||||
copy_clean "$workspace/LICENSE" "$resources/LICENSE"
|
||||
copy_clean "$workspace/THIRD-PARTY-NOTICES.md" "$resources/THIRD-PARTY-NOTICES.md"
|
||||
codesign --force --deep --sign - "$app"
|
||||
pkg="$stage/WiiCompiled-Setup-unsigned.pkg"
|
||||
DITTONORSRC=1 COPYFILE_DISABLE=1 pkgbuild --root "$stage/root" --identifier org.wiicompiled.setup --version "$version" --install-location / "$pkg"
|
||||
if [[ -n "$identity" ]]; then productbuild --sign "$identity" --package "$pkg" "$output"; else ditto "$pkg" "$output"; fi
|
||||
printf 'Created game-code-free package: %s\n' "$output"
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# Extract a user-owned Mario Kart Wii PAL (RMCP01) disc image for a local build.
|
||||
# This script deliberately contains no game data and is intended for the macOS
|
||||
# setup application and for maintainers testing that setup path.
|
||||
set -euo pipefail
|
||||
|
||||
readonly EXPECTED_DOL_SHA256=80d18895b39c63bd80f457398bfcbb91b7d16ac116a41a88967e954080155b05
|
||||
readonly EXPECTED_REL_SHA256=16d9d146112541fefea701ecb5bc1a496f9d50e4a752fbb5b6778e7c6399f67d
|
||||
|
||||
fail() { printf 'extract-disc.command: error: %s\n' "$*" >&2; exit 1; }
|
||||
sha256() { shasum -a 256 "$1" | awk '{ print $1 }'; }
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: extract-disc.command --game IMAGE --assets-dir DIR --nodtool PATH
|
||||
|
||||
Extracts a user-owned Mario Kart Wii PAL RMCP01 image into DIR. The final
|
||||
layout is DIR/main.dol, DIR/StaticR.rel, and DIR/DATA. Existing data is left
|
||||
untouched unless the complete new extraction passes both content hash checks.
|
||||
EOF
|
||||
}
|
||||
|
||||
game=""
|
||||
assets_dir=""
|
||||
nodtool=""
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--game) game=${2:-}; shift 2 ;;
|
||||
--assets-dir) assets_dir=${2:-}; shift 2 ;;
|
||||
--nodtool) nodtool=${2:-}; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -f "$game" ]] || fail "disc image does not exist: $game"
|
||||
[[ -n "$assets_dir" ]] || fail "--assets-dir is required"
|
||||
[[ -x "$nodtool" ]] || fail "nodtool is not executable: $nodtool"
|
||||
|
||||
assets_dir=$(mkdir -p "$assets_dir" && cd "$assets_dir" && pwd)
|
||||
scratch=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-disc.XXXXXX")
|
||||
cleanup() { rm -rf "$scratch"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
printf 'MKWCBUILD:STEP:validate-disc Checking the selected disc image\n'
|
||||
"$nodtool" info "$game" >/dev/null
|
||||
printf 'MKWCBUILD:STEP:extract-disc Extracting the user-owned disc image\n'
|
||||
"$nodtool" extract "$game" "$scratch/extracted"
|
||||
|
||||
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'
|
||||
|
||||
data_root=$(dirname "$(dirname "$dol")")
|
||||
[[ -d "$data_root/files" ]] || fail 'nodtool extraction did not contain the Wii files directory'
|
||||
|
||||
# Stage beside the destination so the final replacement stays on one volume.
|
||||
stage="$assets_dir/.extract-stage-$$"
|
||||
rm -rf "$stage"
|
||||
mkdir -p "$stage"
|
||||
ditto "$data_root" "$stage/DATA"
|
||||
ditto "$dol" "$stage/main.dol"
|
||||
ditto "$rel" "$stage/StaticR.rel"
|
||||
|
||||
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
|
||||
mkdir -p "$backup"
|
||||
for item in DATA main.dol StaticR.rel; do
|
||||
[[ -e "$assets_dir/$item" ]] && mv "$assets_dir/$item" "$backup/$item"
|
||||
done
|
||||
fi
|
||||
mv "$stage/DATA" "$assets_dir/DATA"
|
||||
mv "$stage/main.dol" "$assets_dir/main.dol"
|
||||
mv "$stage/StaticR.rel" "$assets_dir/StaticR.rel"
|
||||
rmdir "$stage"
|
||||
rm -rf "$backup"
|
||||
|
||||
printf 'MKWCBUILD:STEP:disc-ready Verified and extracted clean PAL RMCP01 game assets\n'
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
# Turn one locally compiled macOS product into a self-contained .app bundle.
|
||||
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
|
||||
|
||||
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.
|
||||
EOF
|
||||
}
|
||||
|
||||
build_dir=""; product=""; output_dir=""
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--build-dir) build_dir=${2:-}; shift 2 ;;
|
||||
--product) product=${2:-}; shift 2 ;;
|
||||
--output-dir) output_dir=${2:-}; shift 2 ;;
|
||||
-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
|
||||
[[ -x "$build_dir/$product" ]] || fail "missing compiled product: $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"
|
||||
frameworks="$app/Contents/Frameworks"
|
||||
resources="$app/Contents/Resources"
|
||||
rm -rf "$app"
|
||||
mkdir -p "$macos" "$frameworks" "$resources"
|
||||
cat > "$app/Contents/Info.plist" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict>
|
||||
<key>CFBundleDevelopmentRegion</key><string>en</string>
|
||||
<key>CFBundleExecutable</key><string>$product</string>
|
||||
<key>CFBundleIdentifier</key><string>org.wiicompiled.$product</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
|
||||
<key>CFBundleName</key><string>$product</string>
|
||||
<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>NSHighResolutionCapable</key><true/>
|
||||
</dict></plist>
|
||||
EOF
|
||||
ditto "$build_dir/$product" "$macos/$product"
|
||||
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")
|
||||
while ((${#queue[@]})); do
|
||||
current=${queue[0]}
|
||||
queue=("${queue[@]:1}")
|
||||
while IFS= read -r dependency; do
|
||||
[[ "$dependency" == /opt/homebrew/* || "$dependency" == /usr/local/* ]] || continue
|
||||
[[ -f "$dependency" ]] || continue
|
||||
name=$(basename "$dependency")
|
||||
if [[ ! -f "$frameworks/$name" ]]; then
|
||||
ditto "$dependency" "$frameworks/$name"
|
||||
install_name_tool -id "@rpath/$name" "$frameworks/$name"
|
||||
queue+=("$frameworks/$name")
|
||||
fi
|
||||
done < <(otool -L "$current" | tail -n +2 | awk '{print $1}')
|
||||
done
|
||||
while IFS= read -r binary; do
|
||||
while IFS= read -r old; do
|
||||
[[ "$old" == /opt/homebrew/* || "$old" == /usr/local/* ]] || continue
|
||||
name=$(basename "$old")
|
||||
[[ -f "$frameworks/$name" ]] || continue
|
||||
if [[ "$binary" == "$macos/$product" ]]; then
|
||||
install_name_tool -change "$old" "@executable_path/../Frameworks/$name" "$binary"
|
||||
else
|
||||
install_name_tool -change "$old" "@loader_path/$name" "$binary"
|
||||
fi
|
||||
done < <(otool -L "$binary" | tail -n +2 | awk '{print $1}')
|
||||
done < <(find "$frameworks" -type f -print; printf '%s\n' "$macos/$product")
|
||||
|
||||
find "$frameworks" -type f -exec codesign --force --sign - {} +
|
||||
codesign --force --deep --sign - "$app"
|
||||
codesign --verify --deep --strict "$app"
|
||||
printf 'MKWCBUILD:APP=%s\n' "$app"
|
||||
Executable
+140
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env bash
|
||||
# Entry point bundled in WiiCompiled Setup.app. The package contains source and
|
||||
# tools only; a user's own verified disc is extracted into Application Support.
|
||||
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; }
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: setup.command --game IMAGE [--retro-dir DIR] [--install-location {user|applications}]
|
||||
|
||||
Without arguments this script opens file pickers. It is normally launched by
|
||||
WiiCompiled Setup.app, not run directly.
|
||||
EOF
|
||||
}
|
||||
|
||||
game=""; retro_dir=""; install_location=applications
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--game) game=${2:-}; shift 2 ;;
|
||||
--retro-dir) retro_dir=${2:-}; shift 2 ;;
|
||||
--install-location) install_location=${2:-}; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
[[ "$install_location" == user || "$install_location" == applications ]] || fail '--install-location must be user or applications'
|
||||
|
||||
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"
|
||||
POSIX path of selectedFile
|
||||
APPLESCRIPT
|
||||
) || exit 0
|
||||
choice=$(/usr/bin/osascript -e 'button returned of (display dialog "Would you like to build Retro Rewind too?" buttons {"Base game only", "Choose Retro Rewind folder"} default button "Base game only")')
|
||||
if [[ "$choice" == 'Choose Retro Rewind folder' ]]; then
|
||||
retro_dir=$(/usr/bin/osascript <<'APPLESCRIPT'
|
||||
set selectedFolder to choose folder with prompt "Choose the RetroRewind6 folder (or its parent folder)"
|
||||
POSIX path of selectedFolder
|
||||
APPLESCRIPT
|
||||
) || exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ -x "$nodtool" ]] || fail 'the packaged nodtool is missing or not executable'
|
||||
[[ -x "$translator" ]] || fail 'the packaged Translator.Cli is missing or not executable'
|
||||
[[ -x "$cmake_bin" && -x "$ninja_bin" ]] || fail 'the packaged CMake or Ninja tool is missing'
|
||||
if ! /usr/bin/xcode-select -p >/dev/null 2>&1; then
|
||||
notice 'Xcode Command Line Tools are required once to compile WiiCompiled. Click OK, complete the Apple installer, then run WiiCompiled Setup again.'
|
||||
/usr/bin/xcode-select --install || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$support_root" "$products"
|
||||
if [[ ! -d "$workspace/.git" && ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then
|
||||
printf 'Preparing the local build workspace...\n'
|
||||
rm -rf "$workspace"
|
||||
/usr/bin/ditto "$workspace_source" "$workspace"
|
||||
fi
|
||||
|
||||
profile=base
|
||||
build_args=(--workspace "$workspace" --game "$game" --nodtool "$nodtool" --output-dir "$products")
|
||||
if [[ -n "$retro_dir" ]]; then
|
||||
profile=both
|
||||
# Online play needs the shared Retro-WFC payload. Keep it in the per-user
|
||||
# support directory rather than the packaged app or build workspace, 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"
|
||||
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" \
|
||||
'https://rwfc.net/api/wfc/payload?g=RMCPD00' || fail 'could not download the Retro-WFC payload needed for online play'
|
||||
"$translator" validate-retro-wfc-payload --directory "$payload_stage" || \
|
||||
fail 'downloaded Retro-WFC payload failed signature validation'
|
||||
mkdir -p "$retro_wfc_dir/binary"
|
||||
mv "$temporary_payload" "$retro_wfc_payload"
|
||||
rmdir "$payload_stage/binary" "$payload_stage"
|
||||
trap - EXIT
|
||||
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'; }
|
||||
set_path() {
|
||||
local key=$1 value=$2 encoded line temporary
|
||||
encoded=$(toml_string "$value"); line="$key = \"$encoded\""; temporary="$config.tmp"
|
||||
touch "$config"
|
||||
if grep -q '^[[:space:]]*\[paths\][[:space:]]*$' "$config"; then
|
||||
awk -v key="$key" -v line="$line" '
|
||||
/^[[:space:]]*\[paths\][[:space:]]*$/ { print; print line; inside = 1; next }
|
||||
inside && /^[[:space:]]*\[/ { inside = 0 }
|
||||
inside && $0 ~ "^[[:space:]]*" key "[[:space:]]*=" { next }
|
||||
{ print }
|
||||
' "$config" > "$temporary"
|
||||
else
|
||||
{ cat "$config"; printf '\n[paths]\n%s\n' "$line"; } > "$temporary"
|
||||
fi
|
||||
mv "$temporary" "$config"
|
||||
}
|
||||
set_path dvd_root "$workspace/Assets/DATA"
|
||||
[[ -n "$retro_dir" ]] && set_path retro_rewind_root "$retro_dir"
|
||||
|
||||
destination="$HOME/Applications"
|
||||
if [[ "$install_location" == applications ]]; then destination=/Applications; fi
|
||||
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"
|
||||
else
|
||||
mkdir -p "$destination"; rm -rf "$destination/$app"; /usr/bin/ditto "$products/$app" "$destination/$app"
|
||||
fi
|
||||
}
|
||||
install_app WiiCompiled.app
|
||||
[[ "$profile" == both ]] && install_app RetroRewind.app
|
||||
notice "Installation complete. Your apps are in $destination."
|
||||
Executable
+242
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prepares a self-contained native-Linux build toolchain bundled into the AppImage by
|
||||
# build-appimage.sh, so a user needs no `clang`/`cmake`/`ninja` of their own to build the
|
||||
# translated game (mirrors why Windows bundles llvm-mingw + CMake + Ninja via
|
||||
# Prepare-PortableTools.ps1 - this is that script's Linux counterpart, for the same reason).
|
||||
#
|
||||
# clang/lld/llvm-ar: pruned from the official llvm.org GitHub release tarball (NOT llvm-mingw -
|
||||
# that project targets Windows/mingw, never native Linux) down to just what's needed to compile
|
||||
# and link: clang, lld, llvm-ar, the clang resource dir (builtin headers + compiler-rt), and
|
||||
# libc++/libc++abi/libunwind (so the toolchain never has to fall back to the host's system
|
||||
# libstdc++ headers). The raw release is ~1.9 GiB per arch (every LLVM backend, mlir, flang, lldb,
|
||||
# docs, tests); pruned it is ~500 MiB uncompressed / ~100 MiB compressed, verified against a real
|
||||
# build of this project.
|
||||
#
|
||||
# cmake: pruned from the official Kitware GitHub release tarball down to bin/cmake (not
|
||||
# ccmake/cmake-gui/cpack/ctest, which local-build.sh never invokes) plus the Modules/Templates
|
||||
# CMake needs at runtime (found relative to bin/cmake via CMAKE_ROOT auto-detection - Help/doc/man/
|
||||
# the desktop-integration files under share/ are documentation/GUI-only and dropped). Verified with
|
||||
# a real configure+build using the pruned cmake+ninja+clang together.
|
||||
#
|
||||
# ninja: the official ninja-build GitHub release zip, used as-is - it is already a single small
|
||||
# (~130 KiB compressed) static-ish binary with nothing to prune.
|
||||
set -euo pipefail
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
workspace=$(cd "$script_dir/.." && pwd)
|
||||
|
||||
llvm_version=22.1.8
|
||||
cmake_version=4.3.3
|
||||
ninja_version=1.13.2
|
||||
destination="$script_dir/artifacts/portable-tools"
|
||||
arch=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: prepare-portable-tools.sh --arch {x86_64|aarch64} [--destination DIR]
|
||||
|
||||
--arch ARCH Target architecture (required)
|
||||
--destination DIR Where the toolchain is written, as DIR/toolchain-ARCH
|
||||
(default: Launcher/artifacts/portable-tools)
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--arch) arch=$2; shift 2 ;;
|
||||
--destination) destination=$2; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "prepare-portable-tools.sh: unknown argument: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$arch" in
|
||||
x86_64) llvm_release_arch=X64; target_triple=x86_64-unknown-linux-gnu
|
||||
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=d431eff9f064c86ee7c4c94af570a8f74fcccd1f74c6f0da3af32ce34a1e1b05
|
||||
cmake_release_arch=aarch64
|
||||
cmake_sha256=9ea38356dbd3e32e51029a3e09a0f2f8e117ef4fbcaad7a21ffb36409bbd5cb4
|
||||
ninja_asset=ninja-linux-aarch64.zip
|
||||
ninja_sha256=fd2cacc8050a7f12a16a2e48f9e06fca5c14fc4c2bee2babb67b58be17a607fc ;;
|
||||
*) echo "prepare-portable-tools.sh: --arch must be x86_64 or aarch64" >&2; usage; exit 1 ;;
|
||||
esac
|
||||
|
||||
destination=$(mkdir -p "$destination" && cd "$destination" && pwd)
|
||||
toolchain_dir="$destination/toolchain-$arch"
|
||||
downloads="$script_dir/artifacts/downloads"
|
||||
mkdir -p "$downloads"
|
||||
|
||||
sha256_of() { sha256sum "$1" | awk '{print $1}'; }
|
||||
|
||||
download_verified() {
|
||||
# $1 = destination path, $2 = URL, $3 = expected sha256
|
||||
local dest=$1 url=$2 expected=$3
|
||||
if [[ -f "$dest" ]] && [[ "$(sha256_of "$dest")" == "$expected" ]]; then return; fi
|
||||
echo "prepare-portable-tools.sh: downloading $(basename "$dest")..."
|
||||
local tmp="$dest.partial"
|
||||
rm -f "$tmp"
|
||||
curl -fL --progress-bar -o "$tmp" "$url"
|
||||
local actual
|
||||
actual=$(sha256_of "$tmp")
|
||||
if [[ "$actual" != "$expected" ]]; then
|
||||
echo "prepare-portable-tools.sh: $(basename "$dest") hash mismatch: expected $expected, got $actual" >&2
|
||||
rm -f "$tmp"
|
||||
exit 1
|
||||
fi
|
||||
mv "$tmp" "$dest"
|
||||
}
|
||||
|
||||
if [[ -x "$toolchain_dir/bin/clang" && -x "$toolchain_dir/bin/ninja" && -x "$toolchain_dir/bin/cmake" ]]; then
|
||||
echo "prepare-portable-tools.sh: reusing existing toolchain at $toolchain_dir"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
work="$destination/.building-toolchain-$arch"
|
||||
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 ---
|
||||
# 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/theofficialgman/llvm-project/releases/download/llvmorg-22.1.8-patched/$llvm_archive_name" \
|
||||
"$llvm_release_sha256"
|
||||
|
||||
extract_root="$script_dir/artifacts/.extract-clang-$arch"
|
||||
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_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..."
|
||||
|
||||
# clang: the real driver executable plus the clang/clang++ symlinks CMake/local-build.sh invoke.
|
||||
# Stripped: debug symbols are dead weight for a bundled compiler nobody will debug.
|
||||
cp -a "$src/bin/clang-22" "$work/bin/"
|
||||
strip "$work/bin/clang-22"
|
||||
ln -s clang-22 "$work/bin/clang"
|
||||
ln -s clang "$work/bin/clang++"
|
||||
|
||||
# lld: linked via -fuse-ld=lld, which clang resolves by looking for ld.lld next to itself first -
|
||||
# see local-build.sh's --fuse-ld option.
|
||||
cp -a "$src/bin/lld" "$work/bin/"
|
||||
strip "$work/bin/lld"
|
||||
ln -s lld "$work/bin/ld.lld"
|
||||
|
||||
# llvm-ar/llvm-ranlib: CMake's archiver for the many static libraries this project builds
|
||||
# (aurora, Crypto++, SDL3, Dawn's dependency closure, the translated game shards).
|
||||
cp -a "$src/bin/llvm-ar" "$work/bin/"
|
||||
strip "$work/bin/llvm-ar"
|
||||
ln -s llvm-ar "$work/bin/llvm-ranlib"
|
||||
|
||||
# Clang's resource directory: builtin headers (stddef.h, immintrin.h, ...) and compiler-rt
|
||||
# (builtins, sanitizer runtimes). `clang -print-resource-dir` must find this at lib/clang/<ver>/.
|
||||
cp -a "$src/lib/clang" "$work/lib/"
|
||||
|
||||
# libc++/libc++abi/libunwind: so this toolchain never has to fall back to whatever libstdc++ the
|
||||
# host distro happens to have installed. Not the default yet (local-build.sh still resolves the
|
||||
# system libstdc++ unless -stdlib=libc++ is passed), but bundled so that option exists.
|
||||
cp -a "$src/include/c++" "$work/include/"
|
||||
cp -a "$src/include/$target_triple/c++/v1/__config_site" "$work/include/$target_triple/c++/v1/"
|
||||
cp -a "$src/lib/$target_triple"/libc++.a "$src/lib/$target_triple"/libc++abi.a "$src/lib/$target_triple"/libunwind.a "$work/lib/$target_triple/"
|
||||
cp -a "$src/lib/$target_triple"/libc++.so* "$src/lib/$target_triple"/libc++abi.so* "$src/lib/$target_triple"/libunwind.so* "$work/lib/$target_triple/"
|
||||
|
||||
rm -rf "$extract_root"
|
||||
|
||||
# --- cmake, pruned from the official Kitware release ---
|
||||
|
||||
cmake_share_version=${cmake_version%.*}
|
||||
cmake_archive_name="cmake-$cmake_version-linux-$cmake_release_arch.tar.gz"
|
||||
cmake_archive="$downloads/$cmake_archive_name"
|
||||
download_verified "$cmake_archive" \
|
||||
"https://github.com/Kitware/CMake/releases/download/v$cmake_version/$cmake_archive_name" \
|
||||
"$cmake_sha256"
|
||||
|
||||
cmake_extract_root="$script_dir/artifacts/.extract-cmake-$arch"
|
||||
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_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"
|
||||
cp -a "$cmake_src/bin/cmake" "$work/bin/"
|
||||
cp -a "$cmake_src/share/cmake-$cmake_share_version/Modules" "$cmake_src/share/cmake-$cmake_share_version/Templates" \
|
||||
"$work/share/cmake-$cmake_share_version/"
|
||||
rm -rf "$cmake_extract_root"
|
||||
|
||||
# --- ninja, used as-is ---
|
||||
|
||||
ninja_archive="$downloads/ninja-$ninja_version-$arch.zip"
|
||||
download_verified "$ninja_archive" \
|
||||
"https://github.com/ninja-build/ninja/releases/download/v$ninja_version/$ninja_asset" \
|
||||
"$ninja_sha256"
|
||||
echo "prepare-portable-tools.sh: staging ninja $ninja_version..."
|
||||
unzip -oq "$ninja_archive" -d "$work/bin"
|
||||
chmod +x "$work/bin/ninja"
|
||||
|
||||
cat > "$work/LICENSE.txt" <<EOF
|
||||
Portable build tools bundled by WiiCompiled
|
||||
|
||||
clang/lld/llvm-ar $llvm_version (pruned from the official LLVM release for Linux/$llvm_release_arch)
|
||||
https://github.com/llvm/llvm-project/releases/tag/llvmorg-$llvm_version
|
||||
Apache License v2.0 with LLVM Exceptions:
|
||||
https://github.com/llvm/llvm-project/blob/llvmorg-$llvm_version/LICENSE.TXT
|
||||
|
||||
CMake $cmake_version
|
||||
https://github.com/Kitware/CMake
|
||||
BSD 3-Clause License
|
||||
|
||||
Ninja $ninja_version
|
||||
https://github.com/ninja-build/ninja
|
||||
Apache License 2.0
|
||||
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() {
|
||||
std::vector<int> v{1, 2, 3};
|
||||
int sum = 0;
|
||||
for (int x : v) sum += x;
|
||||
return sum == 6 ? 0 : 1;
|
||||
}
|
||||
EOF
|
||||
"$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 > "$test_dir/CMakeLists.txt" <<'EOF'
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(test CXX)
|
||||
add_executable(test t.cpp)
|
||||
EOF
|
||||
"$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 "$test_dir/build" >/dev/null
|
||||
"$test_dir/build/test"
|
||||
|
||||
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,19 @@
|
||||
<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="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
|
||||
@@ -46,26 +59,45 @@ Press **F10** while the game window has focus:
|
||||
- Internal resolution
|
||||
- FPS counter
|
||||
- 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.
|
||||
The port does NOT pretend to be a Wii Remote or Classic 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)
|
||||
|
||||
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
|
||||
pointing away from the screen).
|
||||
- Battery level is not reported to the game and the remote's speaker is not implemented.
|
||||
- Only the Wii Remote's own accelerometer is calibrated; the Nunchuk's uses SDL's fixed zero point.
|
||||
- The Classic Controller's L/R triggers reach the game as digital (full pull on click): SDL does not
|
||||
expose their analog travel.
|
||||
- Turn the Wii Remote support off in that menu if you use a Mayflash DolphinBar, which already
|
||||
presents the remote as a regular gamepad.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Windows 10 or 11, 64-bit
|
||||
- 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)
|
||||
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
|
||||
GCZ, CISO, WBFS, WIA and RVZ are accepted.
|
||||
|
||||
@@ -86,6 +118,7 @@ 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.
|
||||
|
||||
|
||||
> [!CAUTION]
|
||||
> Only take builds from this repository's
|
||||
> [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an
|
||||
@@ -124,7 +157,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
|
||||
|
||||
@@ -173,7 +208,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
|
||||
|
||||
@@ -26,10 +26,6 @@ Aurora itself vendors:
|
||||
(shagkur) and Dave Murphy (WinterMute). `aurora-main/lib/card/SRAM.hpp`.
|
||||
Source: <https://github.com/devkitPro/libogc>
|
||||
|
||||
> [!NOTE]
|
||||
> Upstream aurora ships `assets/screenshot.png`, a rendered frame from a different Nintendo
|
||||
> title. It is intentionally omitted from this repository.
|
||||
|
||||
### Dolphin Emulator data files - GPL-2.0-or-later
|
||||
|
||||
Copyright (c) 2003+ Dolphin Emulator Project.
|
||||
@@ -118,13 +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
|
||||
included in the installer's `licenses/` folder.
|
||||
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> |
|
||||
|
||||
@@ -151,15 +151,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}")
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# Source-level fixes applied to the vendored SDL3 tree before it is built.
|
||||
#
|
||||
# Each patch is an exact string replacement, checked and idempotent: a tree that
|
||||
# already carries the fix is left alone, a tree where the anchor text is missing
|
||||
# (a different SDL version) stops the configure with a clear message instead of
|
||||
# silently building without the fix.
|
||||
#
|
||||
# Used two ways:
|
||||
# - included by AuroraSDL3Provider.cmake, which calls aurora_sdl3_apply_patches()
|
||||
# on a pre-provided source tree (FETCHCONTENT_SOURCE_DIR_SDL);
|
||||
# - run as `cmake -DSDL_SOURCE_DIR=<dir> -P AuroraSDL3Patches.cmake` from the
|
||||
# FetchContent PATCH_COMMAND for a freshly extracted tarball.
|
||||
|
||||
function(_aurora_sdl3_replace file description old new)
|
||||
file(READ "${file}" _content)
|
||||
string(FIND "${_content}" "${new}" _already)
|
||||
if (NOT _already EQUAL -1)
|
||||
return()
|
||||
endif ()
|
||||
string(FIND "${_content}" "${old}" _anchor)
|
||||
if (_anchor EQUAL -1)
|
||||
message(FATAL_ERROR "aurora: SDL3 patch '${description}' does not apply to ${file}; "
|
||||
"the vendored SDL version changed, review AuroraSDL3Patches.cmake")
|
||||
endif ()
|
||||
string(REPLACE "${old}" "${new}" _content "${_content}")
|
||||
file(WRITE "${file}" "${_content}")
|
||||
message(STATUS "aurora: SDL3 patch applied: ${description}")
|
||||
endfunction()
|
||||
|
||||
function(aurora_sdl3_apply_patches sdl_source_dir)
|
||||
set(_wii "${sdl_source_dir}/src/joystick/hidapi/SDL_hidapi_wii.c")
|
||||
if (NOT EXISTS "${_wii}")
|
||||
message(FATAL_ERROR "aurora: SDL3 source tree at ${sdl_source_dir} has no SDL_hidapi_wii.c")
|
||||
endif ()
|
||||
|
||||
# Wii Remote: rebuild the joystick in place after an extension change.
|
||||
#
|
||||
# When a Nunchuk or Classic Controller is plugged in or pulled out, the driver
|
||||
# flags the joystick as disconnected so it can come back with the new name and
|
||||
# capabilities. But the HID device stays open, and HIDAPI only removes a
|
||||
# joystick-less device once its handle is closed, so nothing ever re-creates
|
||||
# the joystick: the remote is gone for good until the application toggles the
|
||||
# driver hint (which closes and re-opens the Bluetooth HID handle, something
|
||||
# some Windows Bluetooth stacks answer by dropping the link). Instead, when the
|
||||
# device has no joystick, probe the extension again (two bounded attempts, at
|
||||
# most once a second) and connect a new joystick on the still-open handle.
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote in-place reconnect (context field)"
|
||||
[==[ Uint64 m_ulNextMotionPlusCheck;
|
||||
bool m_bDisconnected;
|
||||
]==]
|
||||
[==[ Uint64 m_ulNextMotionPlusCheck;
|
||||
bool m_bDisconnected;
|
||||
Uint64 m_ulNextReconnect; /* WiiCompiled: see HIDAPI_DriverWii_UpdateDevice */
|
||||
]==])
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote in-place reconnect (bounded extension probe)"
|
||||
[==[static EWiiExtensionControllerType ReadExtensionControllerType(SDL_HIDAPI_Device *device)
|
||||
{
|
||||
SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)device->context;
|
||||
EWiiExtensionControllerType eExtensionControllerType = k_eWiiExtensionControllerType_Unknown;
|
||||
const int MAX_ATTEMPTS = 20;
|
||||
int attempts = 0;
|
||||
]==]
|
||||
[==[static EWiiExtensionControllerType ReadExtensionControllerTypeAttempts(SDL_HIDAPI_Device *device, int MAX_ATTEMPTS)
|
||||
{
|
||||
SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)device->context;
|
||||
EWiiExtensionControllerType eExtensionControllerType = k_eWiiExtensionControllerType_Unknown;
|
||||
int attempts = 0;
|
||||
]==])
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote in-place reconnect (probe wrapper)"
|
||||
[==[static void UpdateDeviceIdentity(SDL_HIDAPI_Device *device)
|
||||
{
|
||||
]==]
|
||||
[==[static EWiiExtensionControllerType ReadExtensionControllerType(SDL_HIDAPI_Device *device)
|
||||
{
|
||||
return ReadExtensionControllerTypeAttempts(device, 20);
|
||||
}
|
||||
|
||||
static void UpdateDeviceIdentity(SDL_HIDAPI_Device *device)
|
||||
{
|
||||
]==])
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote in-place reconnect (UpdateDevice)"
|
||||
[==[ if (device->num_joysticks > 0) {
|
||||
joystick = SDL_GetJoystickFromID(device->joysticks[0]);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
now = SDL_GetTicks();
|
||||
]==]
|
||||
[==[ if (device->num_joysticks > 0) {
|
||||
joystick = SDL_GetJoystickFromID(device->joysticks[0]);
|
||||
} else {
|
||||
/* WiiCompiled: the joystick was dropped (extension plugged or unplugged,
|
||||
* or a failed read) but the HID device is still open, and the device
|
||||
* list only removes a joystick-less device once its handle is closed.
|
||||
* Rebuild the joystick in place, so an extension change behaves like it
|
||||
* does on the console: the remote never goes away, it just changes
|
||||
* type. If the remote does not answer, keep the device and retry. */
|
||||
now = SDL_GetTicks();
|
||||
if (ctx->m_ulNextReconnect != 0 && now < ctx->m_ulNextReconnect) {
|
||||
return false;
|
||||
}
|
||||
ctx->m_ulNextReconnect = now + 1000;
|
||||
{
|
||||
EWiiExtensionControllerType type = ReadExtensionControllerTypeAttempts(device, 2);
|
||||
if (type == k_eWiiExtensionControllerType_Unknown) {
|
||||
return false;
|
||||
}
|
||||
ctx->m_bDisconnected = false;
|
||||
ctx->m_ulLastInput = now;
|
||||
ctx->m_ulNextReconnect = 0;
|
||||
ctx->m_eExtensionControllerType = type;
|
||||
UpdateDeviceIdentity(device);
|
||||
SDL_LogDebug(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Reconnected joystick as %s", device->name);
|
||||
return HIDAPI_JoystickConnected(device, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
now = SDL_GetTicks();
|
||||
]==])
|
||||
endfunction()
|
||||
|
||||
# Script mode (FetchContent PATCH_COMMAND).
|
||||
if (CMAKE_SCRIPT_MODE_FILE AND DEFINED SDL_SOURCE_DIR)
|
||||
aurora_sdl3_apply_patches("${SDL_SOURCE_DIR}")
|
||||
endif ()
|
||||
@@ -135,9 +135,18 @@ elseif (_aurora_sdl3_provider STREQUAL "vendor")
|
||||
endif ()
|
||||
|
||||
include(FetchContent)
|
||||
# Source fixes for the vendored SDL (see AuroraSDL3Patches.cmake). A freshly
|
||||
# downloaded tarball is patched by the PATCH_COMMAND; a tree handed in through
|
||||
# FETCHCONTENT_SOURCE_DIR_SDL skips the download steps, so patch it here.
|
||||
set(_aurora_sdl3_patches "${CMAKE_CURRENT_LIST_DIR}/AuroraSDL3Patches.cmake")
|
||||
include("${_aurora_sdl3_patches}")
|
||||
if (DEFINED FETCHCONTENT_SOURCE_DIR_SDL AND EXISTS "${FETCHCONTENT_SOURCE_DIR_SDL}")
|
||||
aurora_sdl3_apply_patches("${FETCHCONTENT_SOURCE_DIR_SDL}")
|
||||
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
|
||||
PATCH_COMMAND "${CMAKE_COMMAND}" -DSDL_SOURCE_DIR=<SOURCE_DIR> -P "${_aurora_sdl3_patches}"
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(SDL)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
+49
-17
@@ -225,7 +225,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.
|
||||
@@ -255,6 +255,14 @@ FrameWorkerState g_frameWorker;
|
||||
bool frame_worker_requested() noexcept {
|
||||
#ifdef AURORA_ENABLE_GX
|
||||
static const bool enabled = [] {
|
||||
#if defined(__APPLE__)
|
||||
// ImGui's SDL backend may raise an SDL window from ImGui::NewFrame(). On
|
||||
// macOS that reaches AppKit, whose window operations are main-thread-only;
|
||||
// doing it on the frame worker terminates the process with EXC_BREAKPOINT.
|
||||
// Keep all SDL/ImGui work on the calling thread until the worker no longer
|
||||
// owns frame preparation on Apple platforms.
|
||||
return false;
|
||||
#endif
|
||||
#if defined(_WIN32)
|
||||
// RenderDoc's D3D12 layer is injected before Aurora starts and needs device and command
|
||||
// ownership on one thread, so keep frame submission synchronous there.
|
||||
@@ -681,15 +689,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());
|
||||
}
|
||||
@@ -706,18 +722,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 {}",
|
||||
@@ -1516,6 +1542,15 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
|
||||
|
||||
// Phase 3: hand the encoded group to whoever owns presentation.
|
||||
void publish_presentations(std::vector<PresentationJob>&& presentationJobs, bool interpolationActive) {
|
||||
#if defined(__APPLE__)
|
||||
(void)interpolationActive;
|
||||
// Presenting reaches SDL/AppKit, whose window operations must stay on the
|
||||
// main thread. Interpolation normally starts the presenter worker, so keep
|
||||
// its jobs synchronous on Apple platforms.
|
||||
for (const auto& job : presentationJobs) {
|
||||
present_presentation_job(job);
|
||||
}
|
||||
#else
|
||||
// Keep presentation on the presenter whenever the async frame worker runs, even with
|
||||
// interpolation off, so every mode shares one surface/resize path. RenderDoc keeps the sync path.
|
||||
if (frame_worker_requested() || interpolationActive ||
|
||||
@@ -1526,6 +1561,7 @@ void publish_presentations(std::vector<PresentationJob>&& presentationJobs, bool
|
||||
present_presentation_job(job);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void record_frame_telemetry() {
|
||||
@@ -1643,7 +1679,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();
|
||||
@@ -1653,11 +1689,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);
|
||||
}
|
||||
@@ -1734,7 +1768,7 @@ bool begin_frame() noexcept {
|
||||
return prepared;
|
||||
}
|
||||
|
||||
void end_frame() noexcept {
|
||||
void end_frame() {
|
||||
#ifdef AURORA_ENABLE_GX
|
||||
webgpu::fail_if_device_lost();
|
||||
#endif
|
||||
@@ -1750,10 +1784,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;
|
||||
@@ -1779,6 +1810,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
|
||||
@@ -1841,10 +1876,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();
|
||||
@@ -1852,6 +1883,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",
|
||||
};
|
||||
@@ -1877,8 +1909,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;
|
||||
}
|
||||
|
||||
@@ -1889,6 +1920,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",
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -520,9 +520,11 @@ void GXCopyTex(void* dest, GXBool clear) {
|
||||
clearState.clearAlpha = clear && alphaUpdate;
|
||||
}
|
||||
const auto copyFilter = combined_copy_filter_coefficients(g_gxState.copyFilterVFilter);
|
||||
// Skip only recurring color copies so one-shot copies are never lost.
|
||||
const bool producedConsecutively = handle.revision != 0 && currentFrame - handle.lastProducedFrame <= 1;
|
||||
const bool persistentCopy = !aurora::gx::is_depth_format(texCopyFmt) && !producedConsecutively;
|
||||
// Every GXCopyTex is observable texture data. Reusing a destination in this
|
||||
// or the previous frame does not guarantee another redraw: menu thumbnail
|
||||
// scratch targets can be reused and then retained. Depth copies have the
|
||||
// same requirement. Only display presentation may skip unfinished draws.
|
||||
const bool persistentCopy = true;
|
||||
aurora::gfx::resolve_pass(handle.handle, rect, clearState.clearColor, clearState.clearAlpha, clearState.clearDepth,
|
||||
clearState.clearColorValue, aurora::gx::clear_depth_value(), resolveFmt,
|
||||
&sourceRect.sampleRect, g_gxState.texCopyHalfScale, ©Filter, forceOpaqueAlpha,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <dolphin/pad.h>
|
||||
#include <dolphin/si.h>
|
||||
#include <SDL3/SDL_mouse.h>
|
||||
#include <SDL3/SDL_joystick.h>
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
@@ -193,6 +194,32 @@ std::array<PADButtonMapping, PAD_BUTTON_COUNT> g_defaultButtonsJoyPair{{
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT},
|
||||
}};
|
||||
|
||||
// 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
|
||||
// __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).
|
||||
|
||||
// Nintendo's labelled layout: A on the right accelerates, B at the bottom
|
||||
// brakes. SDL's Wii driver reports ZL/ZR as the LEFT_TRIGGER/RIGHT_TRIGGER
|
||||
// axes, never as shoulder buttons, so they are left unbound here and picked up
|
||||
// by aurora's default axis mapping (g_defaultAxes) the same way every
|
||||
// analog-trigger pad's L/R is.
|
||||
std::array<PADButtonMapping, PAD_BUTTON_COUNT> g_defaultButtonsWiiUPro{{
|
||||
{SDL_GAMEPAD_BUTTON_EAST, PAD_BUTTON_A},
|
||||
{SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_B},
|
||||
{SDL_GAMEPAD_BUTTON_NORTH, PAD_BUTTON_X},
|
||||
{SDL_GAMEPAD_BUTTON_WEST, PAD_BUTTON_Y},
|
||||
{SDL_GAMEPAD_BUTTON_START, PAD_BUTTON_START},
|
||||
{SDL_GAMEPAD_BUTTON_BACK, PAD_TRIGGER_Z},
|
||||
{PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_L},
|
||||
{PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_R},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_UP, PAD_BUTTON_UP},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_DOWN, PAD_BUTTON_DOWN},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_LEFT, PAD_BUTTON_LEFT},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT},
|
||||
}};
|
||||
|
||||
std::array<PADKeyButtonBinding, PAD_BUTTON_COUNT> g_defaultKeys{{
|
||||
{PAD_KEY_INVALID, PAD_BUTTON_A},
|
||||
{PAD_KEY_INVALID, PAD_BUTTON_B},
|
||||
@@ -292,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) {
|
||||
@@ -409,8 +448,26 @@ 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.
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
out = g_defaultButtonsWiiUPro;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Picks the default button table for a controller by name (Wii pads) or SDL gamepad type.
|
||||
void __PADSetDefaultMapping(aurora::input::GameController* controller) /* NOLINT(*-reserved-identifier) */
|
||||
{
|
||||
if (wii_default_mapping(controller, controller->m_buttonMapping)) {
|
||||
reset_alt_button_mapping(controller);
|
||||
return;
|
||||
}
|
||||
switch (SDL_GetGamepadType(controller->m_controller)) {
|
||||
case SDL_GAMEPAD_TYPE_XBOX360:
|
||||
controller->m_buttonMapping = g_defaultButtonsXBox360;
|
||||
@@ -679,10 +736,10 @@ u32 PADRead(PADStatus* status) {
|
||||
}
|
||||
|
||||
status[i].err = PAD_ERR_NONE;
|
||||
if (g_keyboardBindings[i].m_mappingsSet) {
|
||||
if (g_keyboardBindings[i].m_mappingsSet && SDL_GetKeyboardFocus() != nullptr) {
|
||||
std::ranges::for_each(
|
||||
g_keyboardBindings[i].m_buttonMapping, [&kbState, &i, &status](const PADKeyButtonBinding& mapping) {
|
||||
if (mapping.scancode > PAD_KEY_INVALID && kbState[mapping.scancode]) {
|
||||
g_keyboardBindings[i].m_buttonMapping, [&kbState, &numKeys, &i, &status](const PADKeyButtonBinding& mapping) {
|
||||
if (mapping.scancode > PAD_KEY_INVALID && mapping.scancode < numKeys && kbState[mapping.scancode]) {
|
||||
status[i].button |= mapping.padButton;
|
||||
} else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) {
|
||||
status[i].button |= mapping.padButton;
|
||||
@@ -743,13 +800,54 @@ 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
|
||||
// the D-pad as joystick buttons 11-14 (the SDL_GAMEPAD_BUTTON_DPAD_*
|
||||
// values) and never as a hat, but the mapping SDL generates for HIDAPI
|
||||
// pads binds the D-pad to hat 0, so SDL_GetGamepadButton(DPAD_*) stays
|
||||
// false. Keep this restricted to the Wii driver's pad so raw button
|
||||
// indices don't interfere with other controller types.
|
||||
const char* name = SDL_GetGamepadName(controller->m_controller);
|
||||
const bool isWiiUPro = name != nullptr && SDL_strstr(name, "Wii U Pro Controller") != nullptr;
|
||||
|
||||
if (isWiiUPro) {
|
||||
SDL_Joystick* joystick =
|
||||
SDL_GetGamepadJoystick(controller->m_controller);
|
||||
|
||||
uint32_t raw = 0;
|
||||
const int buttonCount = SDL_GetNumJoystickButtons(joystick);
|
||||
|
||||
for (int b = 0; b < buttonCount && b < 32; ++b) {
|
||||
if (SDL_GetJoystickButton(joystick, b)) {
|
||||
raw |= (1u << b);
|
||||
}
|
||||
}
|
||||
|
||||
// Up = button 11
|
||||
if (raw & (1u << 11)) {
|
||||
status[i].button |= PAD_BUTTON_UP;
|
||||
}
|
||||
// Down = button 12
|
||||
if (raw & (1u << 12)) {
|
||||
status[i].button |= PAD_BUTTON_DOWN;
|
||||
}
|
||||
// Left = button 13
|
||||
if (raw & (1u << 13)) {
|
||||
status[i].button |= PAD_BUTTON_LEFT;
|
||||
}
|
||||
// Right = button 14
|
||||
if (raw & (1u << 14)) {
|
||||
status[i].button |= PAD_BUTTON_RIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
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))) {
|
||||
if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) {
|
||||
status[i].button |= mapping.padButton;
|
||||
}
|
||||
|
||||
@@ -766,7 +864,7 @@ u32 PADRead(PADStatus* status) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -778,6 +876,7 @@ u32 PADRead(PADStatus* status) {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// TODO: Add serializable mappings for these (probably not necessary)?
|
||||
static constexpr std::array<std::pair<SDL_GamepadButton, PADExtButton>, PAD_EXT_BUTTON_COUNT> kExtButtonMappings{{
|
||||
{SDL_GAMEPAD_BUTTON_BACK, PAD_BUTTON_BACK},
|
||||
@@ -859,6 +958,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;
|
||||
@@ -903,12 +1013,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) {
|
||||
@@ -1191,6 +1302,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;
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
+197
-53
@@ -1,4 +1,5 @@
|
||||
#include "common.hpp"
|
||||
#include "staging_map.hpp"
|
||||
#include "../gx/shader_info.hpp"
|
||||
|
||||
#include "clear.hpp"
|
||||
@@ -36,10 +37,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 +132,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 +167,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 +233,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 +256,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 +325,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 +472,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 +532,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 +559,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 +592,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 +788,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 +812,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 +901,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 +1007,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 +1027,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 +1072,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 +1101,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 +1140,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 +1162,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 +1171,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 +1184,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 +1227,68 @@ 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]);
|
||||
}
|
||||
}
|
||||
// GXCopyTex can capture the ordinary EFB as well as an explicit offscreen
|
||||
// target. Its command may arrive in the next batch, after this prefix has
|
||||
// already been submitted. Preserve every prefix; target kind cannot tell
|
||||
// us whether the guest will later retain these pixels in a texture.
|
||||
for (auto& pass : g_renderPasses) pass.requireReadyPipelines = true;
|
||||
auto encoder = g_device.CreateCommandEncoder();
|
||||
end_batch(encoder);
|
||||
render(encoder);
|
||||
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 +1321,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 +1374,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 +1452,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)) {
|
||||
if (finalize && !debugFrame.groups.empty()) {
|
||||
for (auto& it : std::ranges::reverse_view(debugFrame.groups)) {
|
||||
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 +1476,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 +1511,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 +1545,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 +1591,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;
|
||||
}
|
||||
@@ -1470,8 +1614,8 @@ bool bind_pipeline(PipelineRef ref, const wgpu::RenderPassEncoder& pass, Pipelin
|
||||
if (!skip_unready_pipelines()) {
|
||||
pipelineReady = wait_pipeline(ref, pipeline);
|
||||
} else if (requireReady) {
|
||||
// The pass resolves into a persistent texture (a one-shot bake such as MKW's minimap), so a
|
||||
// skipped draw would never be re-issued. These run behind loads, not mid-race.
|
||||
// Texture copies and capacity prefixes must retain complete draw results.
|
||||
// A future display frame cannot repair a texture that already captured them.
|
||||
pipelineReady = wait_pipeline_for_persistent_pass(ref, pipeline);
|
||||
} else {
|
||||
pipelineReady = try_pipeline(ref, pipeline);
|
||||
@@ -1600,8 +1744,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 +1754,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,16 @@ 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;
|
||||
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 +337,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 +354,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 +362,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
|
||||
|
||||
@@ -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,7 +82,18 @@ void drain() {
|
||||
if (detail::sBufferSize == 0) {
|
||||
return;
|
||||
}
|
||||
process(detail::sBufferData, detail::sBufferSize, true);
|
||||
uint32_t consumed = 0;
|
||||
bool retried = false;
|
||||
while (consumed < detail::sBufferSize) {
|
||||
const auto count = process(detail::sBufferData + consumed, detail::sBufferSize - consumed, true);
|
||||
if (count == 0 && retried)
|
||||
throw gfx::StagingCapacityError("FIFO draw does not fit after capacity submission");
|
||||
consumed += count;
|
||||
if (consumed == detail::sBufferSize) break;
|
||||
// process returned with its renderer lock released. No recursive drain.
|
||||
gfx::split_staging_batch();
|
||||
retried = true;
|
||||
}
|
||||
detail::sBufferSize = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -993,11 +993,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 +1467,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 +1507,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 +1648,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 +1708,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 +1763,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 {
|
||||
|
||||
@@ -401,6 +401,8 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
|
||||
return -1;
|
||||
}
|
||||
controller.m_isGameCube = controller.m_vid == 0x057E && controller.m_pid == 0x0337;
|
||||
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;
|
||||
@@ -481,6 +483,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ 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;
|
||||
bool m_hasRumble = false;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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) {
|
||||
@@ -738,11 +742,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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -462,8 +462,18 @@ TEST(FrameInterpolationContract, IndexedPaletteHistoryKeepsAbsoluteVertexSlots)
|
||||
std::array<uint8_t, uniformSize> changedSource{};
|
||||
aurora::gx::begin_frame_interpolation();
|
||||
const auto changedRanges = recordFrame(changedTopology, 91.0f, 9.0f, changedSource);
|
||||
EXPECT_EQ(changedRanges[0].size, 0u);
|
||||
// Staging may reserve a copy for sibling matching; the correctness contract
|
||||
// is that an unmatched topology receives the current pose unchanged.
|
||||
const auto expectedCurrent = changedSource;
|
||||
aurora::gx::finalize_frame_interpolation();
|
||||
EXPECT_EQ(changedSource, expectedCurrent);
|
||||
if (changedRanges[0].size != 0) {
|
||||
// No replacement range also correctly selects the original current uniform.
|
||||
ASSERT_EQ(changedRanges[0].size, uniformSize);
|
||||
const auto& duplicated = aurora::gfx::testing::uniform_allocation(changedRanges[0].offset);
|
||||
ASSERT_EQ(duplicated.size(), expectedCurrent.size());
|
||||
EXPECT_EQ(std::memcmp(duplicated.data(), expectedCurrent.data(), expectedCurrent.size()), 0);
|
||||
}
|
||||
|
||||
aurora::gx::set_frame_interpolation_fps(0);
|
||||
aurora::gx::begin_frame_interpolation();
|
||||
@@ -646,12 +656,34 @@ TEST(TevRegisterLivenessContract, PacksOneUniformWhenBothHalvesNeedInitialValue)
|
||||
auto config = baseline;
|
||||
config.tevStages[0].colorPass.a = GX_CC_C0;
|
||||
config.tevStages[0].alphaPass.a = GX_CA_A0;
|
||||
config.tevStages[0].colorPass.b = GX_CC_KONST;
|
||||
config.tevStages[0].kcSel = GX_TEV_KCSEL_K0;
|
||||
|
||||
const auto baselineInfo = aurora::gx::build_shader_info(baseline);
|
||||
const auto info = aurora::gx::build_shader_info(config);
|
||||
EXPECT_TRUE(info.loadsTevRegRgb.test(GX_TEVREG0));
|
||||
EXPECT_TRUE(info.loadsTevRegAlpha.test(GX_TEVREG0));
|
||||
EXPECT_EQ(info.uniformSize, baselineInfo.uniformSize + sizeof(aurora::Vec4<float>));
|
||||
// The final allocation is alignment-rounded, so adding one register need
|
||||
// not increase it. Verify actual packing with a distinct following K color.
|
||||
const auto savedReg = g_gxState.colorRegs[GX_TEVREG0];
|
||||
const auto savedKColor = g_gxState.kcolors[GX_KCOLOR0];
|
||||
g_gxState.colorRegs[GX_TEVREG0] = {11.f, 22.f, 33.f, 44.f};
|
||||
g_gxState.kcolors[GX_KCOLOR0] = {55.f, 66.f, 77.f, 88.f};
|
||||
EXPECT_TRUE(info.sampledKColors.test(GX_KCOLOR0));
|
||||
aurora::gfx::testing::reset_uniform_allocations();
|
||||
aurora::gx::build_uniform(info, 0, {}, {}, false);
|
||||
const auto expectedReg = g_gxState.colorRegs[GX_TEVREG0];
|
||||
const auto expectedKColor = g_gxState.kcolors[GX_KCOLOR0];
|
||||
g_gxState.colorRegs[GX_TEVREG0] = savedReg;
|
||||
g_gxState.kcolors[GX_KCOLOR0] = savedKColor;
|
||||
const auto& bytes = aurora::gfx::testing::uniform_allocation(0);
|
||||
const auto* reg = reinterpret_cast<const uint8_t*>(&expectedReg);
|
||||
const auto found = std::search(bytes.begin(), bytes.end(), reg, reg + sizeof(aurora::Vec4<float>));
|
||||
ASSERT_NE(found, bytes.end());
|
||||
const size_t offset = static_cast<size_t>(found - bytes.begin());
|
||||
ASSERT_LE(offset + 2 * sizeof(aurora::Vec4<float>), bytes.size());
|
||||
EXPECT_EQ(std::memcmp(bytes.data() + offset + sizeof(aurora::Vec4<float>),
|
||||
&expectedKColor, sizeof(aurora::Vec4<float>)), 0);
|
||||
aurora::gfx::testing::reset_uniform_allocations();
|
||||
}
|
||||
|
||||
// BP registers (direct FIFO writes, no dirty state flush needed)
|
||||
@@ -708,6 +740,52 @@ TEST_F(GXFifoTest, BlendMode_Logic) {
|
||||
EXPECT_EQ(g_gxState.blendOp, GX_LO_XOR);
|
||||
}
|
||||
|
||||
|
||||
TEST_F(GXFifoTest, GenMode_FirstZeroWriteDecodesAndRepeatDeduplicates) {
|
||||
reset_gx_state();
|
||||
const auto before = g_gxState.pipelineStateGeneration;
|
||||
decode_fifo(bp_cmd(0, 0));
|
||||
EXPECT_EQ(g_gxState.numTevStages, 1u);
|
||||
EXPECT_EQ(g_gxState.cullMode, GX_CULL_NONE);
|
||||
EXPECT_EQ(g_gxState.numChans, 0u);
|
||||
EXPECT_EQ(g_gxState.numTexGens, 0u);
|
||||
EXPECT_EQ(g_gxState.numIndStages, 0u);
|
||||
EXPECT_EQ(g_gxState.bpRegCache[0], 0u);
|
||||
EXPECT_NE(g_gxState.pipelineStateGeneration, before);
|
||||
const auto decoded = g_gxState.pipelineStateGeneration;
|
||||
decode_fifo(bp_cmd(0, 0));
|
||||
EXPECT_EQ(g_gxState.pipelineStateGeneration, decoded);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, GenMode_FirstMaskedWritePreservesZeroResetBits) {
|
||||
for (const u32 mask : {0u, 1u << 10}) {
|
||||
reset_gx_state();
|
||||
const auto before = g_gxState.pipelineStateGeneration;
|
||||
decode_fifo(bp_cmd(0xFE, mask));
|
||||
decode_fifo(bp_cmd(0, 0xFFFFFF));
|
||||
EXPECT_EQ(g_gxState.bpRegCache[0], mask);
|
||||
EXPECT_EQ(g_gxState.bpRegCache[0xFE], 0xFFFFFFu);
|
||||
EXPECT_EQ(g_gxState.numTevStages, mask ? 2u : 1u);
|
||||
EXPECT_EQ(g_gxState.cullMode, GX_CULL_NONE);
|
||||
EXPECT_NE(g_gxState.pipelineStateGeneration, before);
|
||||
decode_fifo(bp_cmd(0, 0));
|
||||
EXPECT_EQ(g_gxState.numTevStages, 1u);
|
||||
EXPECT_EQ(g_gxState.bpRegCache[0], 0u);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, GenMode_ColdSingleStageApiSetupDecodes) {
|
||||
reset_gx_state();
|
||||
GXSetNumTevStages(1);
|
||||
GXSetNumTexGens(0);
|
||||
GXSetNumChans(0);
|
||||
GXSetCullMode(GX_CULL_NONE);
|
||||
const auto bytes = flush_and_capture();
|
||||
decode_fifo(bytes);
|
||||
EXPECT_EQ(g_gxState.numTevStages, 1u);
|
||||
EXPECT_EQ(g_gxState.cullMode, GX_CULL_NONE);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, BpMask_AppliesOnlyToNextWrite) {
|
||||
std::vector<u8> bytes;
|
||||
auto mask = bp_cmd(0xFE, 1u << 19);
|
||||
@@ -2179,6 +2257,7 @@ TEST_F(GXFifoTest, DrawTopologyTemplatesPreserveExactGxIndexOrder) {
|
||||
const auto decodeAndReadIndices = [&](GXPrimitive primitive, u16 count) {
|
||||
std::vector<u8> fifo;
|
||||
append_test_draw(fifo, primitive, count);
|
||||
aurora::gfx::testing::reset_vertex_push_record();
|
||||
decode_fifo(fifo);
|
||||
return aurora::gfx::testing::last_pushed_indices();
|
||||
};
|
||||
@@ -2193,7 +2272,7 @@ TEST_F(GXFifoTest, DrawTopologyTemplatesPreserveExactGxIndexOrder) {
|
||||
(std::vector<u16>{0, 1, 2, 0, 2, 3, 0, 3, 4}));
|
||||
g_gxState.stateDirty = true;
|
||||
EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLEFAN, 2),
|
||||
(std::vector<u16>{0, 1}));
|
||||
(std::vector<u16>{}));
|
||||
g_gxState.stateDirty = true;
|
||||
EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLESTRIP, 6),
|
||||
(std::vector<u16>{0, 1, 2, 2, 1, 3, 2, 3, 4, 4, 3, 5}));
|
||||
@@ -4166,7 +4245,9 @@ TEST_F(GXFifoTest, CopyTexClearTruePassesScratchRectAndUpdateMasksToResolve) {
|
||||
EXPECT_NEAR(resolve.clearColorValue.y(), 128.f / 255.f, 1.f / 255.f);
|
||||
EXPECT_NEAR(resolve.clearColorValue.z(), 192.f / 255.f, 1.f / 255.f);
|
||||
EXPECT_NEAR(resolve.clearColorValue.w(), 32.f / 255.f, 1.f / 255.f);
|
||||
EXPECT_NEAR(resolve.clearDepthValue, 0x123456 / 16777216.f, 1.f / 16777216.f);
|
||||
const float gxDepth = 0x123456 / 16777216.f;
|
||||
EXPECT_NEAR(resolve.clearDepthValue, aurora::gx::UseReversedZ ? 1.f - gxDepth : gxDepth,
|
||||
1.f / 16777216.f);
|
||||
EXPECT_EQ(resolve.resolveFormat, GX_TF_RGBA8);
|
||||
EXPECT_FALSE(resolve.halfScale);
|
||||
EXPECT_FALSE(resolve.forceOpaqueAlpha);
|
||||
@@ -4186,7 +4267,7 @@ TEST_F(GXFifoTest, CopyTexColorFormatMarksResolvePersistent) {
|
||||
EXPECT_TRUE(records.front().persistentCopy);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, RecurringColorCopyKeepsLaterResolveSkippable) {
|
||||
TEST_F(GXFifoTest, RecurringColorCopyPreservesEveryResolve) {
|
||||
std::array<u8, 152 * 114 * 4> image{};
|
||||
gxState().pixelFmt = GX_PF_RGBA6_Z24;
|
||||
|
||||
@@ -4200,7 +4281,7 @@ TEST_F(GXFifoTest, RecurringColorCopyKeepsLaterResolveSkippable) {
|
||||
const auto& records = aurora::gfx::testing::resolve_pass_records();
|
||||
ASSERT_EQ(records.size(), 2u);
|
||||
EXPECT_TRUE(records[0].persistentCopy);
|
||||
EXPECT_FALSE(records[1].persistentCopy);
|
||||
EXPECT_TRUE(records[1].persistentCopy);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, ColorCopyAfterFrameGapRegainsPersistentProtection) {
|
||||
@@ -4220,7 +4301,7 @@ TEST_F(GXFifoTest, ColorCopyAfterFrameGapRegainsPersistentProtection) {
|
||||
EXPECT_TRUE(records[1].persistentCopy);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, CopyTexDepthFormatKeepsResolveSkippable) {
|
||||
TEST_F(GXFifoTest, CopyTexDepthFormatPreservesResolve) {
|
||||
std::array<u8, 4 * 4 * 4> image{};
|
||||
gxState().pixelFmt = GX_PF_RGBA6_Z24;
|
||||
|
||||
@@ -4230,7 +4311,7 @@ TEST_F(GXFifoTest, CopyTexDepthFormatKeepsResolveSkippable) {
|
||||
|
||||
const auto& records = aurora::gfx::testing::resolve_pass_records();
|
||||
ASSERT_EQ(records.size(), 1u);
|
||||
EXPECT_FALSE(records.front().persistentCopy);
|
||||
EXPECT_TRUE(records.front().persistentCopy);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, CopyDispResolveIsNotPersistent) {
|
||||
|
||||
@@ -299,6 +299,10 @@ std::pair<ByteBuffer, Range> copy_uniform(Range source) {
|
||||
return map_uniform(source.size);
|
||||
}
|
||||
uint32_t align_uniform(uint32_t value) { return (value + 255u) & ~255u; }
|
||||
uint64_t staging_uniform_bytes(uint64_t value) { return staging_padded(value, 256); }
|
||||
uint64_t staging_storage_bytes(uint64_t value) { return staging_padded(value, 256); }
|
||||
bool staging_has_space(const StagingSizes&) { return true; }
|
||||
void split_staging_batch() { throw StagingCapacityError("Unexpected split in FIFO unit test"); }
|
||||
|
||||
Vec2<uint32_t> get_render_target_size() noexcept { return s_renderTargetSize; }
|
||||
Vec2<uint32_t> get_frame_buffer_size() noexcept { return s_renderTargetSize; }
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
#include "gx_test_common.hpp"
|
||||
#include "gfx/staging_map.hpp"
|
||||
#include "gx/pipeline.hpp"
|
||||
|
||||
#include <thread>
|
||||
|
||||
using aurora::gx::g_gxState;
|
||||
|
||||
namespace {
|
||||
std::vector<u8> draw(GXPrimitive primitive, u16 count, GXVtxFmt format = GX_VTXFMT0) {
|
||||
std::vector<u8> bytes{static_cast<u8>(primitive | format), static_cast<u8>(count >> 8),
|
||||
static_cast<u8>(count)};
|
||||
bytes.resize(3 + count);
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, MaximumQuadCountTerminatesWithoutOutOfRangeIndices) {
|
||||
g_gxState.lastVtxFmt = GX_VTXFMT0;
|
||||
g_gxState.lastVtxSize = 1;
|
||||
for (const u16 count : {65532, 65533, 65534, 65535}) {
|
||||
g_gxState.stateDirty = true;
|
||||
decode_fifo(draw(GX_QUADS, count));
|
||||
const auto& indices = aurora::gfx::testing::last_pushed_indices();
|
||||
ASSERT_EQ(indices.size(), (count / 4) * 6 + (count % 4 == 3 ? 3 : 0));
|
||||
for (const auto index : indices) ASSERT_LT(index, count);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, IncompletePrimitivesNeverJoinAcrossDraws) {
|
||||
g_gxState.lastVtxFmt = GX_VTXFMT0;
|
||||
g_gxState.lastVtxSize = 1;
|
||||
aurora::gfx::testing::use_draw_command_tracking(true);
|
||||
decode_fifo(draw(GX_TRIANGLES, 4));
|
||||
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{0, 1, 2}));
|
||||
decode_fifo(draw(GX_TRIANGLES, 5));
|
||||
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{4, 5, 6}));
|
||||
const auto before = aurora::gfx::testing::last_pushed_indices();
|
||||
decode_fifo(draw(GX_TRIANGLEFAN, 2));
|
||||
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), before);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, MergeStopsBeforeSixteenBitIndexOverflow) {
|
||||
g_gxState.lastVtxFmt = GX_VTXFMT0;
|
||||
g_gxState.lastVtxSize = 1;
|
||||
aurora::gfx::testing::use_draw_command_tracking(true);
|
||||
decode_fifo(draw(GX_TRIANGLES, 65535));
|
||||
decode_fifo(draw(GX_TRIANGLES, 3));
|
||||
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 0u);
|
||||
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{0, 1, 2}));
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, VertexCacheInvalidationBreaksDrawMerging) {
|
||||
g_gxState.lastVtxFmt = GX_VTXFMT0;
|
||||
g_gxState.lastVtxSize = 1;
|
||||
aurora::gfx::testing::use_draw_command_tracking(true);
|
||||
decode_fifo(draw(GX_TRIANGLES, 3));
|
||||
decode_fifo({GX_CMD_INVL_VC});
|
||||
EXPECT_TRUE(g_gxState.stateDirty);
|
||||
decode_fifo(draw(GX_TRIANGLES, 3));
|
||||
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 0u);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, EqualStrideVertexFormatChangeBreaksDrawMerging) {
|
||||
aurora::gfx::testing::use_real_vertex_format_helpers(true);
|
||||
g_gxState.vtxDesc[GX_VA_POS] = GX_DIRECT;
|
||||
for (const auto format : {GX_VTXFMT0, GX_VTXFMT1}) {
|
||||
g_gxState.vtxFmts[format].attrs[GX_VA_POS].cnt = GX_POS_XY;
|
||||
g_gxState.vtxFmts[format].attrs[GX_VA_POS].type = GX_U8;
|
||||
}
|
||||
g_gxState.vtxFmts[GX_VTXFMT1].attrs[GX_VA_POS].frac = 1;
|
||||
aurora::gfx::testing::use_draw_command_tracking(true);
|
||||
for (const auto format : {GX_VTXFMT0, GX_VTXFMT1}) {
|
||||
auto bytes = draw(GX_TRIANGLES, 3, format);
|
||||
bytes.resize(9);
|
||||
decode_fifo(bytes);
|
||||
}
|
||||
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 0u);
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, SingleExpandedPrimitiveCannotMergeWithTriangles) {
|
||||
g_gxState.lastVtxFmt = GX_VTXFMT0;
|
||||
g_gxState.lastVtxSize = 1;
|
||||
aurora::gfx::testing::use_draw_command_tracking(true);
|
||||
decode_fifo(draw(GX_POINTS, 1));
|
||||
decode_fifo(draw(GX_TRIANGLES, 3));
|
||||
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 0u);
|
||||
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{0, 1, 2}));
|
||||
}
|
||||
|
||||
TEST(StagingMapping, RetiredCallbacksCannotPublishAnotherBuffersReadiness) {
|
||||
using namespace aurora::gfx;
|
||||
StagingMapState state;
|
||||
const auto old = state.request();
|
||||
EXPECT_EQ(state.request(), 0u);
|
||||
state.reset();
|
||||
const auto current = state.request();
|
||||
EXPECT_FALSE(state.complete(old, BufferMapState::Mapped));
|
||||
EXPECT_FALSE(state.complete(old, BufferMapState::Unmapped));
|
||||
EXPECT_EQ(state.state(), BufferMapState::Mapping);
|
||||
EXPECT_TRUE(state.complete(current, BufferMapState::Mapped));
|
||||
EXPECT_FALSE(state.complete(current, BufferMapState::Unmapped));
|
||||
EXPECT_EQ(state.state(), BufferMapState::Mapped);
|
||||
}
|
||||
|
||||
TEST(StagingMapping, AsyncCompletionWakesWaiters) {
|
||||
using namespace aurora::gfx;
|
||||
StagingMapState state;
|
||||
const auto generation = state.request();
|
||||
std::thread callback([&] {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
state.complete(generation, BufferMapState::Mapped);
|
||||
});
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
||||
while (state.state() == BufferMapState::Mapping && std::chrono::steady_clock::now() < deadline)
|
||||
state.wait_for_progress();
|
||||
callback.join();
|
||||
EXPECT_EQ(state.state(), BufferMapState::Mapped);
|
||||
}
|
||||
|
||||
TEST(StagingCapacity, ReservesPaddingAndRejectsOverflow) {
|
||||
using namespace aurora::gfx;
|
||||
EXPECT_EQ(staging_padded(257, 256), 512u);
|
||||
EXPECT_THROW(staging_padded(UINT64_MAX, 256), StagingCapacityError);
|
||||
const StagingSizes used{0, 256, 0, 0}, demand{0, 256, 0, 0}, tail{0, 3840, 0, 0};
|
||||
EXPECT_TRUE(staging_fits(used, demand, tail, {4, 4352, 4, 4}));
|
||||
EXPECT_FALSE(staging_fits(used, demand, tail, {4, 4351, 4, 4}));
|
||||
EXPECT_FALSE(staging_fits({UINT64_MAX, 0, 0, 0}, {1, 0, 0, 0}, {},
|
||||
{UINT64_MAX, UINT64_MAX, UINT64_MAX, UINT64_MAX}));
|
||||
}
|
||||
|
||||
TEST(FrameInterpolationContract, IdenticalMeshesInDifferentViewportsDoNotShareHistory) {
|
||||
using namespace aurora;
|
||||
const auto savedViewport = gx::g_gxState.logicalViewport;
|
||||
constexpr size_t positionOffset = sizeof(Mat4x4<float>);
|
||||
constexpr size_t normalOffset = positionOffset + gx::MaxPnMtx * sizeof(Mat3x4<float>);
|
||||
constexpr size_t uniformSize = normalOffset + gx::MaxPnMtx * sizeof(Mat3x4<float>);
|
||||
const gx::FrameInterpolationDrawIdentity identity{0x1234, 0x5678, 0x9abc, 0xdef0};
|
||||
const Mat4x4<float> projection{};
|
||||
const auto record = [&](float x, std::array<uint8_t, uniformSize>& source) {
|
||||
gx::g_gxState.pnMtx[0].pos = {{1.f, 0.f, 0.f, x}, {0.f, 1.f, 0.f, 0.f}, {0.f, 0.f, 1.f, 0.f}};
|
||||
gx::g_gxState.pnMtx[0].nrm = {{1.f, 0.f, 0.f, 0.f}, {0.f, 1.f, 0.f, 0.f}, {0.f, 0.f, 1.f, 0.f}};
|
||||
std::memcpy(source.data() + positionOffset, &gx::g_gxState.pnMtx[0].pos, sizeof(Mat3x4<float>));
|
||||
std::memcpy(source.data() + normalOffset, &gx::g_gxState.pnMtx[0].nrm, sizeof(Mat3x4<float>));
|
||||
return gx::record_interpolation_draw(identity, projection, 1, {
|
||||
.sourceUniformData = source.data(), .uniformSize = source.size(), .projectionOffset = 0,
|
||||
.positionOffset = positionOffset, .normalOffset = normalOffset, .currentMatrix = 0,
|
||||
.indexedMatrices = true});
|
||||
};
|
||||
gx::set_frame_interpolation_fps(0);
|
||||
gx::begin_frame_interpolation();
|
||||
gx::set_frame_interpolation_fps(120);
|
||||
gx::g_gxState.logicalViewport = {0.f, 0.f, 640.f, 240.f, 0.f, 1.f};
|
||||
std::array<uint8_t, uniformSize> previous{};
|
||||
gx::begin_frame_interpolation();
|
||||
record(0.f, previous);
|
||||
gx::finalize_frame_interpolation();
|
||||
gfx::testing::reset_uniform_allocations();
|
||||
gx::g_gxState.logicalViewport.top = 240.f;
|
||||
std::array<uint8_t, uniformSize> current{};
|
||||
gx::begin_frame_interpolation();
|
||||
const auto ranges = record(20.f, current);
|
||||
const auto expected = current;
|
||||
gx::finalize_frame_interpolation();
|
||||
EXPECT_EQ(current, expected);
|
||||
if (ranges[0].size) {
|
||||
const auto& duplicate = gfx::testing::uniform_allocation(ranges[0].offset);
|
||||
ASSERT_EQ(duplicate.size(), expected.size());
|
||||
EXPECT_EQ(std::memcmp(duplicate.data(), expected.data(), expected.size()), 0);
|
||||
}
|
||||
gx::g_gxState.logicalViewport = savedViewport;
|
||||
gx::set_frame_interpolation_fps(0);
|
||||
gx::begin_frame_interpolation();
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
# Building WiiCompiled and Retro Rewind on macOS
|
||||
|
||||
This guide covers building **WiiCompiled** (base game) and **Retro Rewind** from source on macOS for Apple Silicon (`arm64`). Follow these instructions to compile the native executables directly.
|
||||
|
||||
> [!NOTE]
|
||||
> If you only want to build the base game (**WiiCompiled**), look for sections marked **`(Skip if only building WiiCompiled)`** to bypass Retro Rewind and online payload steps.
|
||||
|
||||
---
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
### System Requirements
|
||||
- **Hardware**: Apple Silicon Mac (M1/M2/M3/M4)
|
||||
- **Operating System**: macOS 14 (Sonoma) or later
|
||||
- **Xcode Command Line Tools**:
|
||||
```bash
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
### Toolchain Dependencies
|
||||
Install the required tools using [Homebrew](https://brew.sh):
|
||||
```bash
|
||||
brew install cmake ninja
|
||||
brew install --cask dotnet-sdk@8
|
||||
```
|
||||
|
||||
Verify that Clang, CMake, Ninja, and the .NET 8 runtime are available:
|
||||
```bash
|
||||
clang --version
|
||||
cmake --version
|
||||
ninja --version
|
||||
dotnet --list-runtimes # Must list Microsoft.NETCore.App 8.x
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Required Game and Mod Assets
|
||||
|
||||
Due to legal requirements, no proprietary Nintendo assets or code are included in this repository. You must provide your own legally dumped game files.
|
||||
|
||||
1. **Mario Kart Wii PAL (`RMCP01`) Disc Image** *(Required)*:
|
||||
- Supported formats: `.iso`, `.wbfs`, `.ciso`, `.rvz`, `.gcm`, `.gcz`.
|
||||
2. **nodtool** *(Required for disc extraction)*:
|
||||
- Download the macOS Apple Silicon binary of [nodtool](https://github.com/encounter/nod/releases):
|
||||
```bash
|
||||
curl -fsSL "https://github.com/encounter/nod/releases/download/v2.0.0-alpha.10/nodtool-macos-arm64" -o nodtool
|
||||
chmod +x nodtool
|
||||
```
|
||||
3. **Retro Rewind Distribution** *(Skip if only building WiiCompiled)*:
|
||||
- Download the [Retro Rewind](https://wiki.tockdom.com/wiki/Retro_Rewind) release package. You will need the `RetroRewind6` folder (which contains `Binaries/Code.pul`).
|
||||
4. **Retro-WFC Payload** *(Skip if only building WiiCompiled or building offline)*:
|
||||
- Required for online multiplayer on Retro Rewind. Downloaded during setup from `https://rwfc.net/api/wfc/payload?g=RMCPD00`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Step 1: Extract Disc Assets
|
||||
|
||||
Extract your clean PAL `RMCP01` disc into the `Assets/` directory of the repository:
|
||||
|
||||
```bash
|
||||
# Using nodtool directly into a temporary scratch directory
|
||||
mkdir -p /tmp/mkw-extract
|
||||
./nodtool extract /path/to/RMCP01.iso /tmp/mkw-extract
|
||||
|
||||
# Copy extracted assets into the repository Assets directory
|
||||
rm -rf Assets/DATA/files Assets/DATA/sys
|
||||
mkdir -p Assets/DATA
|
||||
cp /tmp/mkw-extract/*/sys/main.dol Assets/main.dol
|
||||
cp /tmp/mkw-extract/*/files/rel/StaticR.rel Assets/StaticR.rel
|
||||
cp -R /tmp/mkw-extract/*/files Assets/DATA/files
|
||||
cp -R /tmp/mkw-extract/*/sys Assets/DATA/sys
|
||||
|
||||
# Clean up temporary files
|
||||
rm -rf /tmp/mkw-extract
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> Alternatively, you can use the repository's helper script:
|
||||
> ```bash
|
||||
> Launcher/macos/extract-disc.command --game /path/to/RMCP01.iso --assets-dir Assets --nodtool ./nodtool
|
||||
> ```
|
||||
|
||||
### Verify Extracted Asset Hashes
|
||||
Confirm that the extracted files match the expected clean PAL revision:
|
||||
```bash
|
||||
shasum -a 256 Assets/main.dol Assets/StaticR.rel
|
||||
```
|
||||
- `Assets/main.dol`: `80d18895b39c63bd80f457398bfcbb91b7d16ac116a41a88967e954080155b05`
|
||||
- `Assets/StaticR.rel`: `16d9d146112541fefea701ecb5bc1a496f9d50e4a752fbb5b6778e7c6399f67d`
|
||||
|
||||
---
|
||||
|
||||
## 4. Step 2: Build the Translator CLI
|
||||
|
||||
Compile the static recompiler CLI:
|
||||
|
||||
```bash
|
||||
dotnet build translator/src/Translator.Cli/Translator.Cli.csproj -c Release
|
||||
```
|
||||
|
||||
Define a shell function to invoke the translator (ensuring paths with spaces are handled safely):
|
||||
```bash
|
||||
translator() {
|
||||
dotnet "$(pwd)/translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll" "$@"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Step 3: Translation
|
||||
|
||||
### A. Translate Base Game Functions
|
||||
```bash
|
||||
mkdir -p generated/functions build/base
|
||||
|
||||
translator translate-recursive 0x800060A4 \
|
||||
--project projects/mkwii/recomp.yml \
|
||||
--outdir generated/functions \
|
||||
--output-metadata generated/base_translation_output.json \
|
||||
--production-source-bundle generated/base_translation_sources.bin \
|
||||
--no-function-files \
|
||||
--prune-stale \
|
||||
--threads $(sysctl -n hw.ncpu)
|
||||
```
|
||||
|
||||
### B. Emit Base Manifest
|
||||
```bash
|
||||
translator emit-base-manifest \
|
||||
--project projects/mkwii/recomp.yml \
|
||||
--out build/base \
|
||||
--functions-dir generated/functions \
|
||||
--translation-output-metadata generated/base_translation_output.json \
|
||||
--region P
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### C. Stage and Translate Retro Rewind *(Skip this step if you only want to build WiiCompiled)*
|
||||
|
||||
1. Stage `Code.pul`:
|
||||
```bash
|
||||
RETRO_DIR="/path/to/RetroRewind6"
|
||||
mkdir -p PulsarPacks/completed/RetroRewind/RetroRewind6/Binaries
|
||||
cp "$RETRO_DIR/Binaries/Code.pul" PulsarPacks/completed/RetroRewind/RetroRewind6/Binaries/Code.pul
|
||||
```
|
||||
|
||||
2. **Retro-WFC Payload Setup (for Online Multiplayer)**:
|
||||
Online play in Retro Rewind requires the shared Retro-WFC payload. Download and validate it:
|
||||
```bash
|
||||
mkdir -p build/retro-wfc/binary
|
||||
curl -fsSL --retry 3 "https://rwfc.net/api/wfc/payload?g=RMCPD00" \
|
||||
-o build/retro-wfc/binary/payload.RMCPD00.bin
|
||||
|
||||
# Validate payload signature and integrity
|
||||
translator validate-retro-wfc-payload --directory build/retro-wfc
|
||||
```
|
||||
|
||||
3. Run Retro Rewind translation:
|
||||
```bash
|
||||
mkdir -p build/mods/retro_rewind_full_cpp
|
||||
|
||||
translator translate-mod \
|
||||
--project projects/mkwii/recomp.yml \
|
||||
--profile retro-rewind \
|
||||
--base-manifest build/base/mkwii_base_manifest.json \
|
||||
--base-translation-output-metadata generated/base_translation_output.json \
|
||||
--code-pul "$RETRO_DIR/Binaries/Code.pul" \
|
||||
--mod-root "$RETRO_DIR" \
|
||||
--mod-name "Retro Rewind" \
|
||||
--region P \
|
||||
--out build/mods/retro_rewind_full_cpp \
|
||||
--prefer-cached-inputs \
|
||||
--emit-cpp \
|
||||
--threads $(sysctl -n hw.ncpu) \
|
||||
--retro-wfc-payload build/retro-wfc/binary/payload.RMCPD00.bin
|
||||
```
|
||||
> [!TIP]
|
||||
> If you do not want online play or do not have an internet connection, replace `--retro-wfc-payload ...` with `--skip-retro-wfc`.
|
||||
|
||||
---
|
||||
|
||||
### D. Generate Data Initialization and Build Shards
|
||||
|
||||
First, generate the embedded game data initializer:
|
||||
```bash
|
||||
translator generate-data-init --project projects/mkwii/recomp.yml
|
||||
```
|
||||
|
||||
Next, generate the CMake build shards using **one** of the following options:
|
||||
|
||||
#### Option 1: Base Game Only (WiiCompiled)
|
||||
```bash
|
||||
mkdir -p generated/build_shards
|
||||
translator emit-build-shards \
|
||||
--project projects/mkwii/recomp.yml \
|
||||
--base-metadata generated/base_translation_output.json \
|
||||
--base-functions-dir generated/functions \
|
||||
--native-source-dir runtime/src \
|
||||
--out generated/build_shards
|
||||
```
|
||||
|
||||
#### Option 2: Base Game + Retro Rewind
|
||||
```bash
|
||||
mkdir -p generated/build_shards
|
||||
translator emit-build-shards \
|
||||
--project projects/mkwii/recomp.yml \
|
||||
--base-metadata generated/base_translation_output.json \
|
||||
--base-functions-dir generated/functions \
|
||||
--native-source-dir runtime/src \
|
||||
--out generated/build_shards \
|
||||
--resolved-profile build/mods/retro_rewind_full_cpp/resolved_dispatch_profile.json \
|
||||
--retro-cpp-dir build/mods/retro_rewind_full_cpp/cpp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Step 4: Configure and Compile with CMake & Ninja
|
||||
|
||||
Configure the native C++ build targeting Apple Silicon:
|
||||
|
||||
```bash
|
||||
cmake -S runtime -B build-macos -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_COMPILER=clang \
|
||||
-DCMAKE_CXX_COMPILER=clang++ \
|
||||
-DAURORA_SDL3_PROVIDER=vendor
|
||||
```
|
||||
|
||||
Compile the desired target:
|
||||
|
||||
```bash
|
||||
# To build WiiCompiled only:
|
||||
cmake --build build-macos --target WiiCompiled --parallel $(sysctl -n hw.ncpu)
|
||||
|
||||
# OR to build both WiiCompiled and Retro Rewind:
|
||||
cmake --build build-macos --target WiiCompiled RetroRewind --parallel $(sysctl -n hw.ncpu)
|
||||
```
|
||||
|
||||
Once compilation completes, the executables are ready in your build directory:
|
||||
- `build-macos/WiiCompiled`
|
||||
- `build-macos/RetroRewind` (if built)
|
||||
|
||||
During the build, CMake automatically copies the required runtime assets into `build-macos/`:
|
||||
- `build-macos/dsp_coef.bin`
|
||||
- `build-macos/initial_pipeline_cache.db`
|
||||
- `build-macos/wii_bootstrap/`
|
||||
|
||||
---
|
||||
|
||||
## 7. Step 5: Running Executables from the Build Folder
|
||||
|
||||
### Configure `Config.toml`
|
||||
The runtime reads configuration from `~/Library/Application Support/WiiCompiled/Config.toml`.
|
||||
|
||||
Create the directory and configuration file:
|
||||
|
||||
```bash
|
||||
mkdir -p "$HOME/Library/Application Support/WiiCompiled"
|
||||
```
|
||||
|
||||
#### For Base Game Only (WiiCompiled):
|
||||
```toml
|
||||
# ~/Library/Application Support/WiiCompiled/Config.toml
|
||||
[video]
|
||||
widescreen = true
|
||||
resolution_multiplier = 1.0
|
||||
graphics_api = "metal"
|
||||
|
||||
[paths]
|
||||
dvd_root = "/absolute/path/to/Wiicompiled/Assets/DATA"
|
||||
```
|
||||
|
||||
#### For Base Game and Retro Rewind:
|
||||
```toml
|
||||
# ~/Library/Application Support/WiiCompiled/Config.toml
|
||||
[video]
|
||||
widescreen = true
|
||||
resolution_multiplier = 1.0
|
||||
graphics_api = "metal"
|
||||
|
||||
[paths]
|
||||
dvd_root = "/absolute/path/to/Wiicompiled/Assets/DATA"
|
||||
retro_rewind_root = "/path/to/RetroRewind6"
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Ensure `dvd_root` points to the directory containing `files` and `sys/fst.bin`.
|
||||
|
||||
### Launching the Game
|
||||
Run the compiled binaries directly from your terminal or by double clicking:
|
||||
|
||||
```bash
|
||||
# Run base WiiCompiled
|
||||
./build-macos/WiiCompiled
|
||||
|
||||
# Run Retro Rewind
|
||||
./build-macos/RetroRewind
|
||||
```
|
||||
|
||||
|
||||
|
||||
Press **F10** in-game at any time to open the configuration bar (controls, resolution, display settings, audio).
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Automated Helper Script
|
||||
|
||||
The repository provides a script (`Launcher/local-build-macos.command`) that handles extraction, translation, and compilation in a single command.
|
||||
|
||||
### Building Base Game Only:
|
||||
```bash
|
||||
Launcher/local-build-macos.command \
|
||||
--profile base \
|
||||
--output-dir build-macos/Products \
|
||||
--game /path/to/RMCP01.iso \
|
||||
--nodtool ./nodtool
|
||||
```
|
||||
|
||||
### Building Both (with Online Retro-WFC Payload):
|
||||
```bash
|
||||
# 1. Download Retro-WFC payload into a staging directory:
|
||||
mkdir -p build/retro-wfc/binary
|
||||
curl -fsSL --retry 3 "https://rwfc.net/api/wfc/payload?g=RMCPD00" \
|
||||
-o build/retro-wfc/binary/payload.RMCPD00.bin
|
||||
|
||||
# 2. Run the automated build with the payload directory:
|
||||
Launcher/local-build-macos.command \
|
||||
--profile both \
|
||||
--output-dir build-macos/Products \
|
||||
--base-output-dir build-macos/Products \
|
||||
--game /path/to/RMCP01.iso \
|
||||
--nodtool ./nodtool \
|
||||
--retro-rewind-package-dir /path/to/RetroRewind6 \
|
||||
--retro-wfc-offline-dir build/retro-wfc
|
||||
```
|
||||
|
||||
### Building Both (Offline, Skipping Payload):
|
||||
```bash
|
||||
Launcher/local-build-macos.command \
|
||||
--profile both \
|
||||
--output-dir build-macos/Products \
|
||||
--base-output-dir build-macos/Products \
|
||||
--game /path/to/RMCP01.iso \
|
||||
--nodtool ./nodtool \
|
||||
--retro-rewind-package-dir /path/to/RetroRewind6 \
|
||||
--skip-retro-wfc-payload
|
||||
```
|
||||
|
||||
When finished, the compiled executables reside in `native-build-macos/` and the bundled `.app` packages are placed in `build-macos/Products/`.
|
||||
@@ -58,7 +58,7 @@ profiles:
|
||||
module_link_base: 0x803992E0
|
||||
output: build/mods/retro_rewind_full_cpp
|
||||
enable_retro_wfc: true
|
||||
retro_wfc_payload: http://nas.play.rwfc.net/payload?g=RMCPD00
|
||||
retro_wfc_payload: https://rwfc.net/api/wfc/payload?g=RMCPD00
|
||||
retro_wfc_legacy_bootstrap_hook: 0x800ED6E8
|
||||
riivolution:
|
||||
xml: xml/RetroRewind6.xml
|
||||
|
||||
+221
-45
@@ -1,18 +1,28 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(mkw_recompiled)
|
||||
|
||||
if((NOT (WIN32 AND MINGW)) AND (NOT CMAKE_SYSTEM_NAME STREQUAL "Linux"))
|
||||
message(FATAL_ERROR "WiiCompiled requires Windows (LLVM-MinGW) or native Linux")
|
||||
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
message(FATAL_ERROR "WiiCompiled requires a 64-bit Clang toolchain")
|
||||
endif()
|
||||
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR
|
||||
NOT CMAKE_SIZEOF_VOID_P EQUAL 8 OR
|
||||
NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
message(FATAL_ERROR "WiiCompiled requires 64-bit Clang targeting x86_64")
|
||||
|
||||
if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
set(MKW_PLATFORM_WINDOWS TRUE)
|
||||
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64)$")
|
||||
# The first native macOS target is Apple Silicon. Intel and universal
|
||||
# binaries remain future compatibility work; do not silently claim them.
|
||||
set(MKW_PLATFORM_MACOS TRUE)
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$")
|
||||
set(MKW_PLATFORM_LINUX TRUE)
|
||||
else()
|
||||
message(FATAL_ERROR
|
||||
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS arm64")
|
||||
endif()
|
||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
message(FATAL_ERROR "WiiCompiled only supports Release builds")
|
||||
endif()
|
||||
|
||||
option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON)
|
||||
|
||||
# Preprocessor definitions that belong to this project's own code (the runtime,
|
||||
# the translated shards and the product glue) and to nothing else. They are
|
||||
# applied directory-scoped, immediately after the aurora add_subdirectory() call,
|
||||
@@ -49,17 +59,14 @@ target_include_directories(mkw_pugixml PUBLIC third_party/pugixml)
|
||||
target_compile_features(mkw_pugixml PUBLIC cxx_std_17)
|
||||
set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF)
|
||||
|
||||
# Non-Windows guest-fiber scheduling (runtime/src/fiber_manager.cpp) needs a symmetric
|
||||
# Linux guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric
|
||||
# stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
|
||||
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
|
||||
# asymmetric resume/yield coroutine libraries, which would need every call site restructured).
|
||||
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
|
||||
# separately BSD-style licensed, see third_party/libco/LICENSE) - all of libco's non-Windows
|
||||
# CPU-architecture backends are kept, even though libco.c's own preprocessor dispatch
|
||||
# (__amd64__/__i386__/__arm__/__aarch64__/etc.) only ever selects amd64.c for this project's
|
||||
# x86_64-only target (see the platform/arch check above). Windows keeps using native Fibers
|
||||
# untouched, so this target is never built there.
|
||||
if(NOT WIN32)
|
||||
# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers
|
||||
# and macOS uses the project's x18-safe AArch64 assembly backend, so this target is Linux-only.
|
||||
if(MKW_PLATFORM_LINUX)
|
||||
add_library(mkw_libco STATIC third_party/libco/libco.c)
|
||||
add_library(mkw::libco ALIAS mkw_libco)
|
||||
target_include_directories(mkw_libco PUBLIC third_party/libco)
|
||||
@@ -100,6 +107,43 @@ if(NOT MKW_NATIVE_PREBUILT_DIR)
|
||||
set_target_properties(mkw_cryptopp PROPERTIES UNITY_BUILD OFF)
|
||||
endif()
|
||||
|
||||
# TLS for non-Windows guest network HLE (runtime/src/hle/net/network_ssl.cpp) - the Windows path
|
||||
# uses Schannel (a Windows-only OS API), which has no equivalent on Linux/Android, so this project
|
||||
# needs its own TLS library there. mbed TLS was chosen over OpenSSL specifically because it cross-
|
||||
# compiles cleanly for Android with nothing beyond a plain C toolchain (no perl/asm build-script
|
||||
# dependency the way OpenSSL's build has), matching how this project already prefers toolchain-
|
||||
# simple libraries (see Crypto++ above, similarly stripped of ASM/SIMD for portability).
|
||||
# Fetched at build time from a pinned upstream release tarball with a checked SHA-256, the same way
|
||||
# aurora-main's own dependencies (SDL, zlib, etc.) are pulled in - not committed as a vendored
|
||||
# source tree, so the repository ships the compiled dependency rather than ~280 tracked upstream
|
||||
# files. Bump MKW_MBEDTLS_VERSION/MKW_MBEDTLS_SHA256 together when updating; the hash comes from
|
||||
# upstream's own signed `mbedtls-<version>-sha256sum.txt` release asset.
|
||||
#
|
||||
# The alias exists on every platform so the link lines in cmake/PublicProducts.cmake stay
|
||||
# platform-independent, but it is only populated where network_ssl.cpp actually compiles the mbed
|
||||
# TLS path (`#ifndef _WIN32`). Windows keeps Schannel and must not fetch anything: its builds run
|
||||
# with FETCHCONTENT_FULLY_DISCONNECTED=ON against the offline dependency set prepared by
|
||||
# Launcher/Prepare-Dependencies.ps1, so an unconditional fetch would fail a clean configure there
|
||||
# and would also add a dependency Windows never links.
|
||||
add_library(mkw_mbedtls INTERFACE)
|
||||
add_library(mkw::mbedtls ALIAS mkw_mbedtls)
|
||||
if(NOT MKW_PLATFORM_WINDOWS)
|
||||
include(FetchContent)
|
||||
set(MKW_MBEDTLS_VERSION "3.6.7")
|
||||
set(MKW_MBEDTLS_SHA256 "a7e8bcbec0e6f761b4af24f25677626b35f762f68eef79c08677a363212d11f6")
|
||||
FetchContent_Declare(mkw_mbedtls_upstream
|
||||
URL "https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-${MKW_MBEDTLS_VERSION}/mbedtls-${MKW_MBEDTLS_VERSION}.tar.bz2"
|
||||
URL_HASH SHA256=${MKW_MBEDTLS_SHA256})
|
||||
# Subproject mode already defaults ENABLE_TESTING off and skips codegen (GEN_FILES), but
|
||||
# ENABLE_PROGRAMS defaults on and installation/package-config isn't wanted for a linked-in copy.
|
||||
set(ENABLE_PROGRAMS OFF CACHE BOOL "" FORCE)
|
||||
set(ENABLE_TESTING OFF CACHE BOOL "" FORCE)
|
||||
set(MBEDTLS_FATAL_WARNINGS OFF CACHE BOOL "" FORCE)
|
||||
set(DISABLE_PACKAGE_CONFIG_AND_INSTALL ON CACHE BOOL "" FORCE)
|
||||
FetchContent_MakeAvailable(mkw_mbedtls_upstream)
|
||||
target_link_libraries(mkw_mbedtls INTERFACE MbedTLS::mbedtls MbedTLS::mbedx509 MbedTLS::mbedcrypto)
|
||||
endif()
|
||||
|
||||
set(MKW_TRANSLATED_COMPILE_JOBS 0 CACHE STRING
|
||||
"Cap on concurrently compiling translated shard TUs via a Ninja job pool (0 = uncapped). \
|
||||
Scheduling only - never affects output bytes, so it is deliberately outside the canonical flag fingerprint.")
|
||||
@@ -130,31 +174,35 @@ else()
|
||||
message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing")
|
||||
endif()
|
||||
set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE)
|
||||
if(WIN32)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE)
|
||||
set(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_USE_WINDOWS_UI OFF CACHE BOOL "" FORCE)
|
||||
else()
|
||||
# Non-Windows (Linux): mirrors aurora-main's own
|
||||
# _aurora_dawn_set_platform_backends() choice for this platform - Vulkan only, no
|
||||
# D3D/HLSL. Kept in sync here because this project's own CMake FORCEs these cache
|
||||
# variables before aurora-main's add_subdirectory() runs, which pre-empts aurora's
|
||||
# auto-detection (CACHE ... INTERNAL "" without FORCE never overrides an existing value).
|
||||
elseif(MKW_PLATFORM_MACOS)
|
||||
set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_VULKAN OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_METAL ON CACHE BOOL "" FORCE)
|
||||
set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE)
|
||||
else()
|
||||
set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE)
|
||||
set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
|
||||
# Provide a tiny stub for DXProgrammableCapture when the SDK/PIX headers are
|
||||
# missing (common on MinGW). Dawn only includes the header; no symbols are
|
||||
# referenced when PIX isn't present.
|
||||
set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs")
|
||||
if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h")
|
||||
file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR})
|
||||
file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h"
|
||||
"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX is absent.\n")
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs")
|
||||
if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h")
|
||||
file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR})
|
||||
file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h"
|
||||
"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX isn't present.\n")
|
||||
endif()
|
||||
endif()
|
||||
# Deliberately NOT injected project-wide. Only a from-source Dawn build ever
|
||||
# includes DXProgrammableCapture.h, and this tree consumes Dawn as a prebuilt
|
||||
@@ -183,7 +231,9 @@ else()
|
||||
if(TARGET ${t})
|
||||
set_target_properties(${t} PROPERTIES UNITY_BUILD OFF)
|
||||
target_compile_options(${t} PRIVATE -O3 -ffast-math -w -pipe)
|
||||
target_include_directories(${t} PRIVATE ${MKW_DX_STUB_DIR})
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_include_directories(${t} PRIVATE ${MKW_DX_STUB_DIR})
|
||||
endif()
|
||||
# Aurora's own sources include Windows headers and call std::min/max;
|
||||
# they relied on the old project-wide NOMINMAX that no longer leaks
|
||||
# into this subtree, so the define is applied per target here.
|
||||
@@ -228,6 +278,18 @@ endif()
|
||||
# a registration file is silently never compiled and never errors. The stale-glob
|
||||
# failure mode is worth far more than the milliseconds.
|
||||
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory.cpp")
|
||||
# HostContext's Apple Silicon backend is implemented in a small assembly
|
||||
# companion. It must be part of the product runtime as well as the
|
||||
# standalone context test; otherwise the final executable is missing
|
||||
# mkw_co_init/mkw_co_switch at link time.
|
||||
enable_language(ASM)
|
||||
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
|
||||
else()
|
||||
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
|
||||
endif()
|
||||
set(MKW_PLATFORM_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/platform/host_platform.cpp")
|
||||
set(MKW_BASE_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/base_product.cpp")
|
||||
set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro_rewind_product.cpp")
|
||||
# The host ISA guard is the one translation unit that must not receive the
|
||||
@@ -235,25 +297,139 @@ set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro
|
||||
# mkw_runtime_common. See cmake/PublicProducts.cmake and the file's own header.
|
||||
set(MKW_CPU_BASELINE_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/host_cpu_baseline.cpp")
|
||||
list(REMOVE_ITEM SOURCES ${MKW_BASE_PRODUCT_SOURCE} ${MKW_RETRO_REWIND_PRODUCT_SOURCE}
|
||||
${MKW_CPU_BASELINE_SOURCE})
|
||||
${MKW_CPU_BASELINE_SOURCE} ${MKW_PLATFORM_SOURCE})
|
||||
|
||||
# This deliberately small library contains host services that are safe to
|
||||
# validate before guest memory and fiber work makes a full runtime build viable.
|
||||
add_library(mkw_platform STATIC "${MKW_PLATFORM_SOURCE}")
|
||||
target_include_directories(mkw_platform PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_platform PUBLIC cxx_std_17)
|
||||
set_target_properties(mkw_platform PROPERTIES UNITY_BUILD OFF)
|
||||
|
||||
# Keep these independent from Aurora's BUILD_TESTING option: they validate the
|
||||
# project's host-platform contracts, not Aurora's third-party test suite.
|
||||
enable_testing()
|
||||
add_executable(mkw_platform_paths_tests "${CMAKE_CURRENT_LIST_DIR}/tests/platform_paths_tests.cpp")
|
||||
target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
|
||||
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
|
||||
|
||||
add_executable(mkw_nand_save_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_save_tests.cpp")
|
||||
target_include_directories(mkw_nand_save_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_nand_save_tests COMMAND mkw_nand_save_tests)
|
||||
|
||||
add_executable(mkw_nand_settings_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_settings_tests.cpp")
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(mkw_nand_settings_tests PRIVATE Threads::Threads)
|
||||
target_include_directories(mkw_nand_settings_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
|
||||
|
||||
add_executable(mkw_sc_serial_tests "${CMAKE_CURRENT_LIST_DIR}/tests/sc_serial_tests.cpp")
|
||||
target_include_directories(mkw_sc_serial_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_sc_serial_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_sc_serial_tests COMMAND mkw_sc_serial_tests)
|
||||
|
||||
# The input expression engine is self-contained, so it can be exercised without
|
||||
# linking the runtime or SDL.
|
||||
add_executable(mkw_input_expr_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/test_expr.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/input_expr.cpp")
|
||||
target_include_directories(mkw_input_expr_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
|
||||
|
||||
# HostContext deliberately keeps the platform-specific context primitive out
|
||||
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
|
||||
# refactors cannot silently remove its headers, implementation, or link edge.
|
||||
if(MKW_PLATFORM_LINUX)
|
||||
add_executable(mkw_linux_host_context_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp")
|
||||
target_include_directories(mkw_linux_host_context_tests PRIVATE
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/third_party/libco")
|
||||
target_compile_features(mkw_linux_host_context_tests PRIVATE cxx_std_17)
|
||||
target_link_libraries(mkw_linux_host_context_tests PRIVATE mkw::libco)
|
||||
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
|
||||
endif()
|
||||
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
# Exercise the Apple Silicon context ABI and the public host-memory
|
||||
# contracts separately from translated products.
|
||||
enable_language(ASM)
|
||||
add_executable(mkw_macos_context_abi_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
|
||||
target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
|
||||
|
||||
add_executable(mkw_macos_host_context_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
|
||||
target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
|
||||
|
||||
add_executable(mkw_macos_guest_flat_memory_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_guest_flat_memory_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
|
||||
target_include_directories(mkw_macos_guest_flat_memory_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_macos_guest_flat_memory_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests)
|
||||
endif()
|
||||
|
||||
# The translator emits the complete, content-addressed source graph. Consuming
|
||||
# this one manifest keeps configure independent of the 28k generated function
|
||||
# files and of optional Retro Rewind artifacts such as code.map.
|
||||
set(MKW_TRANSLATED_SHARD_MANIFEST
|
||||
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
|
||||
CACHE FILEPATH "Translator-owned aggregate shard manifest")
|
||||
if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
message(FATAL_ERROR
|
||||
"Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. "
|
||||
"Run Translator.Cli emit-build-shards first; see translator/README.md.")
|
||||
endif()
|
||||
include("${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS})
|
||||
message(STATUS
|
||||
"Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; "
|
||||
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; "
|
||||
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions")
|
||||
if(MKW_BUILD_PRODUCTS)
|
||||
set(MKW_TRANSLATED_SHARD_MANIFEST
|
||||
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
|
||||
CACHE FILEPATH "Translator-owned aggregate shard manifest")
|
||||
# The prebuilt export only needs the aurora/third-party closure configured above, so a
|
||||
# packaging machine without a translation stops here instead of failing.
|
||||
if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
message(STATUS "No translator shard manifest; configuring the native prebuilt export only")
|
||||
return()
|
||||
endif()
|
||||
if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
message(FATAL_ERROR
|
||||
"Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. "
|
||||
"Run Translator.Cli emit-build-shards first; see translator/README.md.")
|
||||
endif()
|
||||
include("${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS})
|
||||
message(STATUS
|
||||
"Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; "
|
||||
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; "
|
||||
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions")
|
||||
|
||||
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake")
|
||||
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake")
|
||||
else()
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
# Compile-only audit of native runtime sources. It deliberately avoids
|
||||
# translated products until their host dependencies are portable.
|
||||
# These sources depend on generated/RuntimeConfig.h, which is emitted for
|
||||
# a particular game by the translator and is intentionally unavailable
|
||||
# in this platform-only configuration.
|
||||
set(MKW_MACOS_NATIVE_AUDIT_SOURCES ${SOURCES})
|
||||
list(REMOVE_ITEM MKW_MACOS_NATIVE_AUDIT_SOURCES
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/abi_bridge.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/hle/os/os_alarm.cpp")
|
||||
add_library(mkw_macos_native_compile OBJECT ${MKW_MACOS_NATIVE_AUDIT_SOURCES})
|
||||
target_include_directories(mkw_macos_native_compile PRIVATE
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include" "${CMAKE_CURRENT_LIST_DIR}/src"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/.." "${CMAKE_CURRENT_LIST_DIR}/../aurora-main/include")
|
||||
target_compile_features(mkw_macos_native_compile PRIVATE cxx_std_20)
|
||||
target_compile_definitions(mkw_macos_native_compile PRIVATE SDL_MAIN_HANDLED TARGET_PC)
|
||||
target_link_libraries(mkw_macos_native_compile PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
|
||||
mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
|
||||
endif()
|
||||
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
|
||||
message(STATUS "Translated product targets disabled (MKW_BUILD_PRODUCTS=OFF)")
|
||||
endif()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -51,6 +51,16 @@ foreach(_dir IN LISTS _mkw_np_includes)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Dawn's own packaged config (DawnTargets.cmake) links dawn::webgpu_dawn against
|
||||
# Threads::Threads directly. A from-source aurora build resolves that as a side effect of
|
||||
# add_subdirectory(aurora-main) pulling in Dawn's own CMakeLists.txt; this mode never runs that
|
||||
# subdirectory at all, so nothing else would ever define it (verified directly: configuring without
|
||||
# this fails with "The link interface of target dawn::webgpu_dawn contains: Threads::Threads but
|
||||
# the target was not found"). find_package(Threads) is one of CMake's most basic finder modules and
|
||||
# is a no-op on Windows (its threading support is already part of the CRT), so this is safe on
|
||||
# every platform this package format targets, not just the one that first hit the failure.
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
# Dawn is a prebuilt package on both sides; resolve the same install tree the
|
||||
# package was built against so its imported target (and therefore
|
||||
# webgpu_dawn.dll / dxcompiler.dll / dxil.dll) is available exactly as a
|
||||
|
||||
@@ -25,6 +25,11 @@ if(EXISTS "${DATA_INIT_BLOB_ASM}")
|
||||
endif()
|
||||
list(REMOVE_DUPLICATES SOURCES)
|
||||
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
find_library(MKW_IOKIT_FRAMEWORK IOKit REQUIRED)
|
||||
find_library(MKW_COREFOUNDATION_FRAMEWORK CoreFoundation REQUIRED)
|
||||
endif()
|
||||
|
||||
function(mkw_apply_common_compile_options target)
|
||||
target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe)
|
||||
endfunction()
|
||||
@@ -76,11 +81,13 @@ target_compile_definitions(mkw_runtime_common PRIVATE
|
||||
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
if(WIN32)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
|
||||
else()
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco)
|
||||
elseif(MKW_PLATFORM_LINUX)
|
||||
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
|
||||
# media monitoring). Empty string on glibc >= 2.34 where dl* is in libc.
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
if(MKW_CPPWINRT_INCLUDE_DIR)
|
||||
if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
|
||||
@@ -125,16 +132,16 @@ set_target_properties(mkw_runtime_common PROPERTIES UNITY_BUILD ON UNITY_BUILD_M
|
||||
target_precompile_headers(mkw_runtime_common PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
|
||||
mkw_apply_common_compile_options(mkw_runtime_common)
|
||||
|
||||
# Host ISA guard. Everything in MKW_ALL_BUILD_TARGETS below is compiled with
|
||||
# -march=x86-64-v3; this object library deliberately is not, which
|
||||
# is the whole point of keeping it out of mkw_runtime_common. It runs a CPUID
|
||||
# check from a C initializer so an unsupported machine gets a readable error
|
||||
# instead of an illegal-instruction crash. Excluded from the unity build and the
|
||||
# precompiled header because both are produced with the owning target's flags.
|
||||
add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}")
|
||||
target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17)
|
||||
set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF)
|
||||
target_compile_options(mkw_cpu_baseline PRIVATE -w)
|
||||
# Host ISA guard. Windows and Linux x86_64 product targets use x86-64-v3, so
|
||||
# this object deliberately keeps the plain baseline ISA and checks the CPU
|
||||
# before any AVX2/FMA code can execute. AArch64 has no equivalent optional ISA
|
||||
# floor to probe: NEON/FMA are architectural requirements.
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}")
|
||||
target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17)
|
||||
set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF)
|
||||
target_compile_options(mkw_cpu_baseline PRIVATE -w)
|
||||
endif()
|
||||
|
||||
if(NOT MKW_BASE_COMMON_SHARDS)
|
||||
message(FATAL_ERROR "Translator build graph contains no shared base shards")
|
||||
@@ -174,7 +181,9 @@ function(mkw_configure_product target)
|
||||
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_runtime_common>)
|
||||
# Startup CPU check. Must stay a separate object library so it keeps the
|
||||
# plain baseline ISA while everything around it is built for x86-64-v3.
|
||||
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_cpu_baseline>)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_cpu_baseline>)
|
||||
endif()
|
||||
target_include_directories(${target} PRIVATE
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/include"
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/src"
|
||||
@@ -190,10 +199,14 @@ function(mkw_configure_product target)
|
||||
# include the same fat translated headers; bound them by the same pool.
|
||||
mkw_bound_translated_compiles(${target})
|
||||
target_link_libraries(${target} PRIVATE
|
||||
mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
mkw_platform mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
|
||||
|
||||
target_link_libraries(${target} PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
target_link_libraries(${target} PRIVATE
|
||||
"${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}")
|
||||
endif()
|
||||
if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
|
||||
include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
|
||||
aurora_copy_runtime_dlls(${target})
|
||||
@@ -208,21 +221,23 @@ function(mkw_configure_product target)
|
||||
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_link_libraries(${target} PRIVATE
|
||||
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
|
||||
|
||||
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
else()
|
||||
elseif(MKW_PLATFORM_LINUX)
|
||||
# mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o
|
||||
# files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own
|
||||
# target_link_libraries (object libraries don't carry usage requirements to a consumer
|
||||
# that isn't itself linked against as a target). fiber_manager.cpp's co_* calls live in
|
||||
# those objects, so the actual executable link needs mkw::libco directly, same as it
|
||||
# needs it independently of that first `if(WIN32)` branch above.
|
||||
target_link_libraries(${target} PRIVATE mkw::libco)
|
||||
# needs it independently of that first `if(WIN32)` branch above. ${CMAKE_DL_LIBS} is
|
||||
# here for the same reason: music_attenuation.cpp's dlopen(libdbus-1) lives in those
|
||||
# objects (empty string on glibc >= 2.34, where dl* is in libc).
|
||||
target_link_libraries(${target} PRIVATE mkw::libco ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
if(WIN32)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
foreach(runtime_dll libc++.dll libunwind.dll)
|
||||
execute_process(
|
||||
COMMAND "${CMAKE_CXX_COMPILER}" "--print-file-name=${runtime_dll}"
|
||||
@@ -271,6 +286,21 @@ function(mkw_configure_product target)
|
||||
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MKW_INITIAL_PIPELINE_CACHE}"
|
||||
"$<TARGET_FILE_DIR:${target}>/initial_pipeline_cache.db")
|
||||
|
||||
# Non-Windows TLS (runtime/src/hle/net/network_ssl.cpp's mbed TLS path) needs a trusted root
|
||||
# CA bundle to verify server certificates against - Windows gets this for free from the OS via
|
||||
# Schannel, mbed TLS does not ship one itself. Not SHA256-pinned like the DSP ROM above: unlike
|
||||
# a fixed hardware ROM, this bundle is expected to be refreshed periodically as CAs rotate.
|
||||
# Windows gets its trust store from Schannel, so only the platforms that actually build the
|
||||
# mbed TLS path need the bundle beside the executable.
|
||||
if(NOT MKW_PLATFORM_WINDOWS)
|
||||
set(MKW_CA_CERTIFICATE_BUNDLE "${MKW_RUNTIME_SOURCE_DIR}/assets/certs/cacert.pem")
|
||||
if(NOT EXISTS "${MKW_CA_CERTIFICATE_BUNDLE}")
|
||||
message(FATAL_ERROR "Missing TLS root CA bundle: ${MKW_CA_CERTIFICATE_BUNDLE}")
|
||||
endif()
|
||||
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MKW_CA_CERTIFICATE_BUNDLE}" "$<TARGET_FILE_DIR:${target}>/cacert.pem")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
add_executable(WiiCompiled "${MKW_BASE_PRODUCT_SOURCE}" ${MKW_BASE_REGISTRATION_SOURCES})
|
||||
@@ -298,11 +328,23 @@ else()
|
||||
message(STATUS "RetroRewind target disabled (run translate-mod and emit-build-shards)")
|
||||
endif()
|
||||
|
||||
# Windows and Linux x86_64 share the x86-64-v3 floor that the CPU baseline
|
||||
# object above checks. AArch64 builds are compiled locally for the host that
|
||||
# will run them, so both Linux and Apple Silicon use the compiler's native CPU
|
||||
# tuning rather than leaving target-specific performance on the table.
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3)
|
||||
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
|
||||
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
|
||||
else()
|
||||
set(MKW_BASELINE_ARCH_FLAG "")
|
||||
endif()
|
||||
|
||||
set(MKW_ALL_BUILD_TARGETS
|
||||
mkw_runtime_common mkw_base_shared mkw_base_sensitive mkw_retro_sensitive
|
||||
mkw_retro_rewind_functions WiiCompiled RetroRewind)
|
||||
foreach(target IN LISTS MKW_ALL_BUILD_TARGETS)
|
||||
if(TARGET ${target})
|
||||
target_compile_options(${target} PRIVATE -march=x86-64-v3)
|
||||
if(TARGET ${target} AND MKW_BASELINE_ARCH_FLAG)
|
||||
target_compile_options(${target} PRIVATE ${MKW_BASELINE_ARCH_FLAG})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
@@ -58,6 +58,7 @@ inline void Flush(bool force = false) {
|
||||
// still active. A window close is an intentional successful exit, so end the
|
||||
// process directly and do not run the crash/atexit paths.
|
||||
[[noreturn]] inline void ExitForAuroraWindowClose() noexcept {
|
||||
settings_overlay::ReleaseControllers();
|
||||
WindowPlacementPersistence::Flush(true);
|
||||
#if defined(_WIN32)
|
||||
::ExitProcess(0);
|
||||
|
||||
@@ -1,38 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "runtime_config.h"
|
||||
#include "nand_path.h"
|
||||
#include "nand_settings.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace RuntimeConsoleIdentity {
|
||||
|
||||
struct Identity {
|
||||
std::string serial;
|
||||
std::string productCode;
|
||||
std::string area;
|
||||
std::string gameRegion;
|
||||
std::array<uint8_t, 6> mac;
|
||||
};
|
||||
|
||||
inline bool IsValidSerial(const std::string& serial) {
|
||||
return serial.size() == 9 &&
|
||||
serial != "000000000" &&
|
||||
std::all_of(serial.begin(), serial.end(),
|
||||
[](unsigned char value) { return std::isdigit(value) != 0; });
|
||||
}
|
||||
|
||||
inline Identity FromSerial(std::string serial) {
|
||||
// Keep Nintendo's Wii OUI. The suffix is derived from the persisted serial
|
||||
// Keep Nintendo's Wii OUI. The suffix is derived from the NAND serial
|
||||
// so every API exposes one coherent, stable virtual-console identity.
|
||||
uint32_t hash = 2166136261u;
|
||||
for (const unsigned char value : serial) {
|
||||
@@ -46,6 +36,7 @@ inline Identity FromSerial(std::string serial) {
|
||||
|
||||
return {
|
||||
std::move(serial),
|
||||
{}, {}, {},
|
||||
{
|
||||
0x00,
|
||||
0x09,
|
||||
@@ -57,83 +48,23 @@ inline Identity FromSerial(std::string serial) {
|
||||
};
|
||||
}
|
||||
|
||||
inline std::optional<std::string> ReadSerial(const std::filesystem::path& path) {
|
||||
std::ifstream input(path);
|
||||
std::string line;
|
||||
if (!input || !std::getline(input, line)) {
|
||||
return std::nullopt;
|
||||
inline Identity LoadFromNand() {
|
||||
const auto root = RuntimeNandPath::DiscoverNandRootPath();
|
||||
const auto settings = RuntimeNandSettings::Read(root);
|
||||
if (!settings || !RuntimeNandSettings::HasIdentity(*settings)) {
|
||||
RuntimeNandPath::FailNandRoot(
|
||||
"NAND setting.txt is missing or has invalid console identity fields (SERNO, CODE, AREA, GAME)",
|
||||
root / "title/00000001/00000002/data/setting.txt");
|
||||
}
|
||||
constexpr std::string_view prefix = "serial=";
|
||||
if (line.rfind(prefix, 0) != 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::string serial = line.substr(prefix.size());
|
||||
if (!IsValidSerial(serial)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return serial;
|
||||
}
|
||||
|
||||
inline bool WriteSerial(const std::filesystem::path& path, const std::string& serial) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
if (ec) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::path temporary = path;
|
||||
temporary += ".tmp";
|
||||
{
|
||||
std::ofstream output(temporary, std::ios::trunc);
|
||||
if (!output) {
|
||||
return false;
|
||||
}
|
||||
output << "serial=" << serial << '\n';
|
||||
output.close();
|
||||
if (!output) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::rename(temporary, path, ec);
|
||||
if (!ec) {
|
||||
return true;
|
||||
}
|
||||
std::filesystem::remove(temporary, ec);
|
||||
return false;
|
||||
}
|
||||
|
||||
inline std::string GenerateSerial() {
|
||||
std::random_device entropy;
|
||||
std::seed_seq seed{
|
||||
entropy(),
|
||||
entropy(),
|
||||
entropy(),
|
||||
entropy(),
|
||||
};
|
||||
std::mt19937 generator(seed);
|
||||
std::uniform_int_distribution<uint32_t> distribution(100000000u, 999999999u);
|
||||
return std::to_string(distribution(generator));
|
||||
}
|
||||
|
||||
inline Identity LoadOrCreate(const std::filesystem::path& path) {
|
||||
if (const auto serial = ReadSerial(path)) {
|
||||
return FromSerial(*serial);
|
||||
}
|
||||
|
||||
const std::string generated = GenerateSerial();
|
||||
if (WriteSerial(path, generated)) {
|
||||
return FromSerial(generated);
|
||||
}
|
||||
|
||||
// Remain operational in a read-only environment. This fallback matches
|
||||
// Dolphin's deterministic serial while keeping the same valid identity shape.
|
||||
return FromSerial("123456789");
|
||||
Identity identity = FromSerial(settings->at("SERNO"));
|
||||
identity.productCode = settings->at("CODE");
|
||||
identity.area = settings->at("AREA");
|
||||
identity.gameRegion = settings->at("GAME");
|
||||
return identity;
|
||||
}
|
||||
|
||||
inline const Identity& Current() {
|
||||
static const Identity identity =
|
||||
LoadOrCreate(RuntimeConfigFile::ApplicationDataDirectory() / "ConsoleIdentity.txt");
|
||||
static const Identity identity = LoadFromNand();
|
||||
return identity;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
#pragma once
|
||||
|
||||
// The single vocabulary shared by everything that has to turn a Config.toml
|
||||
// controller name into a real button: the F10 settings bar, the macro engine,
|
||||
// and the startup mapping pass. Keeping one table here means a name that the
|
||||
// settings bar offers is always a name the config parser accepts, and vice
|
||||
// versa; the two used to drift because each side carried its own copy.
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <dolphin/pad.h>
|
||||
|
||||
namespace ControllerNames {
|
||||
|
||||
// A GameCube button as the game sees it, with the Config.toml key that selects
|
||||
// it. Order matches RuntimeConfigFile::kControllerButtonKeys.
|
||||
struct GameCubeButtonItem {
|
||||
const char* configKey;
|
||||
const char* label;
|
||||
PADButton padButton;
|
||||
};
|
||||
|
||||
inline constexpr std::array<GameCubeButtonItem, PAD_BUTTON_COUNT> kGameCubeButtons = {{
|
||||
{"a", "A", PAD_BUTTON_A},
|
||||
{"b", "B", PAD_BUTTON_B},
|
||||
{"x", "X", PAD_BUTTON_X},
|
||||
{"y", "Y", PAD_BUTTON_Y},
|
||||
{"start", "Start", PAD_BUTTON_START},
|
||||
{"z", "Z", PAD_TRIGGER_Z},
|
||||
{"l", "L", PAD_TRIGGER_L},
|
||||
{"r", "R", PAD_TRIGGER_R},
|
||||
{"up", "D-pad Up", PAD_BUTTON_UP},
|
||||
{"down", "D-pad Down", PAD_BUTTON_DOWN},
|
||||
{"left", "D-pad Left", PAD_BUTTON_LEFT},
|
||||
{"right", "D-pad Right", PAD_BUTTON_RIGHT},
|
||||
}};
|
||||
|
||||
// A physical button on the host pad. Names are positional (south/east/...)
|
||||
// rather than Xbox-labelled so one config reads the same on any hardware.
|
||||
struct NativeButtonItem {
|
||||
const char* configName;
|
||||
const char* label;
|
||||
uint32_t nativeButton;
|
||||
};
|
||||
|
||||
inline constexpr auto kNativeButtons = std::to_array<NativeButtonItem>({
|
||||
{"disabled", "Unmapped", PAD_NATIVE_BUTTON_DISABLED},
|
||||
{"left_trigger", "Left trigger (LT / L2)", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFT_TRIGGER, false)},
|
||||
{"right_trigger", "Right trigger (RT / R2)", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, false)},
|
||||
{"left_stick_left", "Left stick left", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTX, true)},
|
||||
{"left_stick_right", "Left stick right", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTX, false)},
|
||||
{"left_stick_up", "Left stick up", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTY, true)},
|
||||
{"left_stick_down", "Left stick down", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTY, false)},
|
||||
{"right_stick_left", "Right stick left", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTX, true)},
|
||||
{"right_stick_right", "Right stick right", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTX, false)},
|
||||
{"right_stick_up", "Right stick up", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTY, true)},
|
||||
{"right_stick_down", "Right stick down", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTY, false)},
|
||||
{"unmapped", "Default", PAD_NATIVE_BUTTON_INVALID},
|
||||
{"south", "South (A / Cross)", SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
{"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST},
|
||||
{"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST},
|
||||
{"north", "North (Y / Triangle)", SDL_GAMEPAD_BUTTON_NORTH},
|
||||
{"back", "Back / Select / Create", SDL_GAMEPAD_BUTTON_BACK},
|
||||
{"guide", "Guide / Home / PS", SDL_GAMEPAD_BUTTON_GUIDE},
|
||||
{"start", "Start / Options", SDL_GAMEPAD_BUTTON_START},
|
||||
{"left_stick", "Left stick click (L3)", SDL_GAMEPAD_BUTTON_LEFT_STICK},
|
||||
{"right_stick", "Right stick click (R3)", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
|
||||
{"left_shoulder", "Left bumper (LB / L1)", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
|
||||
{"right_shoulder", "Right bumper (RB / R1)", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
|
||||
{"dpad_up", "D-pad Up", SDL_GAMEPAD_BUTTON_DPAD_UP},
|
||||
{"dpad_down", "D-pad Down", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
|
||||
{"dpad_left", "D-pad Left", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
|
||||
{"dpad_right", "D-pad Right", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
||||
{"misc1", "Misc 1 / Share / Mic", SDL_GAMEPAD_BUTTON_MISC1},
|
||||
{"right_paddle1", "Right paddle 1", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
|
||||
{"left_paddle1", "Left paddle 1", SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
|
||||
{"right_paddle2", "Right paddle 2", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
|
||||
{"left_paddle2", "Left paddle 2", SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
|
||||
{"touchpad", "Touchpad click", SDL_GAMEPAD_BUTTON_TOUCHPAD},
|
||||
{"misc2", "Misc 2", SDL_GAMEPAD_BUTTON_MISC2},
|
||||
{"misc3", "Misc 3 / GC L click", SDL_GAMEPAD_BUTTON_MISC3},
|
||||
{"misc4", "Misc 4 / GC R click", SDL_GAMEPAD_BUTTON_MISC4},
|
||||
{"misc5", "Misc 5", SDL_GAMEPAD_BUTTON_MISC5},
|
||||
{"misc6", "Misc 6", SDL_GAMEPAD_BUTTON_MISC6},
|
||||
});
|
||||
|
||||
inline std::string TrimToken(std::string_view token) {
|
||||
const size_t begin = token.find_first_not_of(" \t");
|
||||
if (begin == std::string_view::npos) {
|
||||
return {};
|
||||
}
|
||||
const size_t end = token.find_last_not_of(" \t");
|
||||
return std::string(token.substr(begin, end - begin + 1));
|
||||
}
|
||||
|
||||
inline const NativeButtonItem* FindNativeButton(std::string_view configName) {
|
||||
const std::string name = TrimToken(configName.substr(0, configName.find('@')));
|
||||
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
|
||||
[&](const NativeButtonItem& item) { return name == item.configName; });
|
||||
return it == kNativeButtons.end() ? nullptr : &*it;
|
||||
}
|
||||
|
||||
// Falls back to the "unmapped" entry so callers always have a label to draw.
|
||||
inline const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) {
|
||||
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
|
||||
[&](const NativeButtonItem& item) { return PADAxisButtonIdentity(nativeButton) == PADAxisButtonIdentity(item.nativeButton); });
|
||||
return it == kNativeButtons.end() ? *FindNativeButton("unmapped") : *it;
|
||||
}
|
||||
|
||||
inline const GameCubeButtonItem* FindGameCubeButton(std::string_view configKey) {
|
||||
const std::string key = TrimToken(configKey);
|
||||
const auto it = std::find_if(kGameCubeButtons.begin(), kGameCubeButtons.end(),
|
||||
[&](const GameCubeButtonItem& item) { return key == item.configKey; });
|
||||
return it == kGameCubeButtons.end() ? nullptr : &*it;
|
||||
}
|
||||
|
||||
// "up" or "up,a" -> the OR of those GC button bits. Unknown names are skipped so
|
||||
// a typo costs one button instead of the whole macro.
|
||||
inline uint16_t GameCubeMaskFromKeys(std::string_view keys) {
|
||||
uint16_t mask = 0;
|
||||
size_t begin = 0;
|
||||
while (begin <= keys.size()) {
|
||||
const size_t comma = keys.find(',', begin);
|
||||
const std::string_view token =
|
||||
keys.substr(begin, comma == std::string_view::npos ? std::string_view::npos : comma - begin);
|
||||
if (const GameCubeButtonItem* item = FindGameCubeButton(token)) {
|
||||
mask |= static_cast<uint16_t>(item->padButton);
|
||||
}
|
||||
if (comma == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
begin = comma + 1;
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
inline std::string GameCubeKeysFromMask(uint16_t mask) {
|
||||
std::string keys;
|
||||
for (const auto& item : kGameCubeButtons) {
|
||||
if ((mask & static_cast<uint16_t>(item.padButton)) == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!keys.empty()) {
|
||||
keys += ',';
|
||||
}
|
||||
keys += item.configKey;
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
inline std::string GameCubeLabelsFromMask(uint16_t mask) {
|
||||
std::string labels;
|
||||
for (const auto& item : kGameCubeButtons) {
|
||||
if ((mask & static_cast<uint16_t>(item.padButton)) == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!labels.empty()) {
|
||||
labels += " + ";
|
||||
}
|
||||
labels += item.label;
|
||||
}
|
||||
return labels.empty() ? std::string("None") : labels;
|
||||
}
|
||||
|
||||
} // namespace ControllerNames
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
// Host implementation of Dolphin's /dev/dolphin Discord contract. The guest
|
||||
// sends only strings and big-endian integer fields; the IPC wire protocol is
|
||||
// owned here so translated game code never needs host SDK headers.
|
||||
namespace DiscordPresence {
|
||||
|
||||
struct Activity {
|
||||
std::string details;
|
||||
std::string state;
|
||||
std::string largeImageKey;
|
||||
std::string largeImageText;
|
||||
std::string smallImageKey;
|
||||
std::string smallImageText;
|
||||
int64_t startTimestamp = 0;
|
||||
int64_t endTimestamp = 0;
|
||||
uint32_t partySize = 0;
|
||||
uint32_t partyMax = 0;
|
||||
};
|
||||
|
||||
void Initialize(const std::string& basicClientId, const std::string& basicTitle);
|
||||
void SetClient(const std::string& clientId);
|
||||
void SetActivity(Activity activity);
|
||||
void Reset();
|
||||
void Shutdown();
|
||||
|
||||
} // namespace DiscordPresence
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#ifndef NOMINMAX
|
||||
@@ -22,7 +23,7 @@
|
||||
// Forward declarations
|
||||
struct CpuContext;
|
||||
|
||||
// GuestFiberManager: each guest OSThread maps to a Windows Fiber. A scheduler fiber picks
|
||||
// GuestFiberManager: each guest OSThread maps to a host context. A scheduler context picks
|
||||
// which guest fiber runs; a real timer thread queues VI retraces at the VI cadence. Guest
|
||||
// threads only switch at explicit yield points (OSSleepThread, OSYieldThread, ...), matching
|
||||
// Wii cooperative semantics exactly.
|
||||
@@ -39,7 +40,7 @@ enum class ThreadState : uint32_t {
|
||||
|
||||
// Information about a guest fiber
|
||||
struct GuestFiber {
|
||||
void* fiber = nullptr; // Windows fiber handle
|
||||
void* fiber = nullptr; // Host context handle
|
||||
uint32_t entryPoint = 0; // Thread entry function
|
||||
uint32_t entryArg = 0; // Argument to entry function
|
||||
CpuContext cpuContext{}; // Saved CPU context for this fiber
|
||||
@@ -102,10 +103,6 @@ private:
|
||||
static void CALLBACK FiberProc(void* param);
|
||||
#else
|
||||
static void FiberProc(void* param);
|
||||
// libco's co_create() entry points take no argument (unlike CreateFiber's FiberProc(void*)),
|
||||
// so this trampoline reads the guest thread address staged by CreateGuestFiber() and forwards
|
||||
// into the (platform-neutral-bodied) FiberProc above. See fiber_manager.cpp.
|
||||
static void FiberProcTrampoline();
|
||||
#endif
|
||||
// Switch from whichever fiber is currently active straight to the scheduler fiber, without
|
||||
// the SwitchToThread bookkeeping (CPU context save/restore, s_currentGuestThread). Used for
|
||||
@@ -117,9 +114,8 @@ private:
|
||||
static std::mutex s_mutex;
|
||||
static std::unordered_map<uint32_t, GuestFiber> s_fibers;
|
||||
static std::vector<void*> s_fibersPendingDelete;
|
||||
// The scheduler's own "fiber": a Windows HFIBER, or (non-Windows) libco's cothread_t for
|
||||
// whichever native call stack first called GuestFiberManager::Initialize() - both are
|
||||
// plain void* handles, so one field serves both platforms.
|
||||
// The scheduler's own host context. Its opaque handle is supplied by the
|
||||
// active HostContext backend, so one field serves every supported host.
|
||||
static void* s_schedulerFiber;
|
||||
static uint32_t s_currentGuestThread;
|
||||
static bool s_initialized;
|
||||
@@ -135,4 +131,3 @@ private:
|
||||
extern std::atomic<uint32_t> g_viRetracePendingCount;
|
||||
|
||||
} // namespace Fiber
|
||||
|
||||
|
||||
@@ -12,10 +12,33 @@
|
||||
namespace GuestFlat {
|
||||
|
||||
// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load
|
||||
// of a global. 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the
|
||||
// usual image/heap placement.
|
||||
// of a global.
|
||||
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
|
||||
inline constexpr size_t kGuestPageSize = 0x1000;
|
||||
#if defined(__x86_64__)
|
||||
// 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap
|
||||
// placement.
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'1000'0000'0000ull;
|
||||
#elif defined(__aarch64__) && defined(__APPLE__)
|
||||
// Keep this well above the low address ranges that Darwin's ASLR may use for
|
||||
// a PIE executable and its shared cache. Apple Silicon's user VA is wider
|
||||
// than Linux's 39-bit minimum, so this 512 GiB region is available while the
|
||||
// Linux AArch64 target retains its 64 GiB placement below.
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0080'0000'0000ull;
|
||||
#elif defined(__aarch64__)
|
||||
// 16 TiB (this arch's x86_64 sibling value) is unreachable on any AArch64
|
||||
// kernel configured for 39-bit virtual addresses (512 GiB ceiling) - common on
|
||||
// older/embedded targets (e.g. this project's own tested Jetson/L4T board, kernel
|
||||
// 4.9). There mmap() silently ignores a hint above the ceiling and hands back an
|
||||
// address near the top of the real range instead, which this module's caller
|
||||
// then rejects as "already occupied". 64 GiB is reachable on every AArch64 VA
|
||||
// width in real use (39-bit minimum and up) and was confirmed via direct mmap
|
||||
// probing to sit far below where the PIE image, heap, shared libraries and
|
||||
// stack actually land (all clustered above ~340 GiB on a 39-bit/512 GiB system).
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0010'0000'0000ull;
|
||||
#else
|
||||
#error "guest_flat_memory.h has no fixed flat guest base chosen for this architecture"
|
||||
#endif
|
||||
|
||||
#define MKW_FLAT_GUEST_BASE (reinterpret_cast<uint8_t*>(GuestFlat::kFixedFlatGuestBase))
|
||||
|
||||
@@ -42,6 +65,25 @@ struct FaultCounters {
|
||||
// True once the reservation exists and translated code may use the flat path.
|
||||
bool IsActive();
|
||||
|
||||
// True when a host VM page covers more than one 4 KiB Wii page. In that
|
||||
// configuration, guest-view page protection cannot safely represent per-Wii-
|
||||
// page MMIO, deferred-read, or executable-write state, so general translated
|
||||
// accesses must use the checked Memory::* path.
|
||||
// Windows user mode and x86-64 always use a 4 KiB base page, so those builds
|
||||
// fold this to a compile-time false: it appears in every flat access and must
|
||||
// not become a hot-path load. Only AArch64, where the page size is a kernel
|
||||
// configuration (4/16/64 KiB), has to probe it at runtime.
|
||||
#if defined(_WIN32) || defined(__x86_64__)
|
||||
#define MKW_GUEST_FLAT_FIXED_PAGE_SIZE 1
|
||||
#endif
|
||||
|
||||
#if defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
|
||||
inline constexpr bool RequiresCheckedAccess() noexcept { return false; }
|
||||
#else
|
||||
extern bool g_requiresCheckedAccess;
|
||||
inline bool RequiresCheckedAccess() noexcept { return g_requiresCheckedAccess; }
|
||||
#endif
|
||||
|
||||
// Reserves the 4 GiB space (once per process) and maps every requested region
|
||||
// into both views. Throws std::runtime_error with a precise diagnosis when the
|
||||
// reservation, the section objects or a view cannot be created - a silent
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
// HostContext is the deliberately small boundary between the guest scheduler
|
||||
// and the host's cooperative-context facility. Windows uses native Fibers and
|
||||
// Linux uses libco; macOS AArch64 uses the local assembly backend because it
|
||||
// must preserve Darwin's platform-reserved x18 register, which libco's AArch64
|
||||
// backend does not save. Its handles are only valid on the thread that
|
||||
// initialized the scheduler.
|
||||
namespace HostContext {
|
||||
|
||||
using Handle = void*;
|
||||
using Entry = void (*)(void*);
|
||||
|
||||
bool InitializeScheduler(Handle* scheduler);
|
||||
void ShutdownScheduler(Handle scheduler);
|
||||
|
||||
Handle Create(std::size_t stackSize, Entry entry, void* argument);
|
||||
void Destroy(Handle context);
|
||||
bool IsCurrent(Handle context);
|
||||
void Switch(Handle target);
|
||||
|
||||
} // namespace HostContext
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
// Per-port expression bindings for the GameCube controls, plus import of a
|
||||
// Dolphin GCPadNew.ini.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include <dolphin/pad.h>
|
||||
|
||||
namespace InputBindings {
|
||||
|
||||
// The controls an expression can drive, in Dolphin's own naming so an
|
||||
// imported config maps across without translation.
|
||||
struct ControlInfo {
|
||||
const char* dolphinName;
|
||||
const char* label;
|
||||
uint16_t padButton; // 0 for the analog-only controls below
|
||||
int analog; // 0 none, 1 trigger L, 2 trigger R
|
||||
};
|
||||
|
||||
inline constexpr std::array<ControlInfo, 14> kControls = {{
|
||||
{"Buttons/A", "A", PAD_BUTTON_A, 0},
|
||||
{"Buttons/B", "B", PAD_BUTTON_B, 0},
|
||||
{"Buttons/X", "X", PAD_BUTTON_X, 0},
|
||||
{"Buttons/Y", "Y", PAD_BUTTON_Y, 0},
|
||||
{"Buttons/Z", "Z", PAD_TRIGGER_Z, 0},
|
||||
{"Buttons/Start", "Start", PAD_BUTTON_START, 0},
|
||||
{"D-Pad/Up", "D-pad Up", PAD_BUTTON_UP, 0},
|
||||
{"D-Pad/Down", "D-pad Down", PAD_BUTTON_DOWN, 0},
|
||||
{"D-Pad/Left", "D-pad Left", PAD_BUTTON_LEFT, 0},
|
||||
{"D-Pad/Right", "D-pad Right", PAD_BUTTON_RIGHT, 0},
|
||||
{"Triggers/L", "L", PAD_TRIGGER_L, 1},
|
||||
{"Triggers/R", "R", PAD_TRIGGER_R, 2},
|
||||
{"Triggers/L-Analog", "L analog", 0, 1},
|
||||
{"Triggers/R-Analog", "R analog", 0, 2},
|
||||
}};
|
||||
|
||||
void Reload() noexcept;
|
||||
|
||||
// The pad library has PADBlockInput but no matching query, so the settings
|
||||
// overlay reports its own state here.
|
||||
void SetInputBlocked(bool blocked) noexcept;
|
||||
bool InputBlocked() noexcept;
|
||||
|
||||
// Mix expression output into a freshly read status set. Call once per guest
|
||||
// PADRead, after every other input source has been merged.
|
||||
void Apply(PADStatus* statuses) noexcept;
|
||||
|
||||
std::string GetExpression(uint32_t port, size_t control) noexcept;
|
||||
// Returns false and fills error if the text does not parse; the binding is
|
||||
// left unchanged in that case.
|
||||
bool SetExpression(uint32_t port, size_t control, const std::string& text, std::string& error) noexcept;
|
||||
|
||||
// True while the control's expression is above the press threshold.
|
||||
bool IsActive(uint32_t port, size_t control) noexcept;
|
||||
|
||||
// The default Dolphin config location on Windows, then next to the executable.
|
||||
std::string DefaultDolphinConfigPath() noexcept;
|
||||
|
||||
// Imports [GCPad<padIndex>] into the given port. Returns the number of controls
|
||||
// imported, or -1 on failure with error filled.
|
||||
int ImportDolphinConfig(const std::string& path, int padIndex, uint32_t port,
|
||||
std::string& summary, std::string& error) noexcept;
|
||||
|
||||
} // namespace InputBindings
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
// Dolphin-compatible input expressions.
|
||||
//
|
||||
// Values are doubles in Dolphin's ControlState style; a control counts as
|
||||
// pressed above kConditionThreshold. Timing matches Dolphin: wall-clock
|
||||
// seconds on a steady clock, so an expression copied from GCPadNew.ini
|
||||
// behaves the same here as it does there.
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace InputExpr {
|
||||
|
||||
inline constexpr double kConditionThreshold = 0.5;
|
||||
|
||||
// Resolves a backtick-quoted input name to its current value.
|
||||
using InputSource = std::function<double(const std::string&)>;
|
||||
|
||||
struct Node;
|
||||
|
||||
class Expression {
|
||||
public:
|
||||
Expression();
|
||||
~Expression();
|
||||
Expression(Expression&&) noexcept;
|
||||
Expression& operator=(Expression&&) noexcept;
|
||||
|
||||
// Returns false and fills error on a syntax problem.
|
||||
static bool Parse(const std::string& text, Expression& out, std::string& error);
|
||||
|
||||
bool Empty() const { return m_root == nullptr; }
|
||||
double Evaluate(const InputSource& source) const;
|
||||
|
||||
// Input names the expression references, for diagnostics.
|
||||
std::vector<std::string> ReferencedInputs() const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Node> m_root;
|
||||
};
|
||||
|
||||
// Parses a Dolphin GCPadNew.ini and returns the expression text for each
|
||||
// control of the requested pad, keyed by Dolphin's own control names
|
||||
// ("Buttons/A", "D-Pad/Up", "Triggers/L", ...). Returns false if the file
|
||||
// cannot be read or the section is missing.
|
||||
bool ReadDolphinConfig(const std::filesystem::path& path, int padIndex,
|
||||
std::vector<std::pair<std::string, std::string>>& controls,
|
||||
std::string& deviceName, std::string& error);
|
||||
|
||||
} // namespace InputExpr
|
||||
@@ -4,7 +4,13 @@
|
||||
#include <cstdint>
|
||||
|
||||
#define MKW_RESTRICT __restrict
|
||||
#if defined(__x86_64__)
|
||||
#include <immintrin.h>
|
||||
#elif defined(__aarch64__)
|
||||
#include <arm_neon.h>
|
||||
#else
|
||||
#error "ppc_isa_config.h has no SIMD intrinsics header for this architecture"
|
||||
#endif
|
||||
|
||||
inline constexpr bool MkwStateFreeAbiEnabled(uint32_t) noexcept
|
||||
{
|
||||
|
||||
@@ -62,7 +62,7 @@ public:
|
||||
{
|
||||
g_currentCpuContext = ctx;
|
||||
|
||||
savedMxcsr_ = _mm_getcsr();
|
||||
savedMxcsr_ = MkwGetHostFpControl();
|
||||
if (ctx != nullptr)
|
||||
MkwApplyHostNiMode(ctx->fpscr);
|
||||
}
|
||||
|
||||
@@ -78,37 +78,71 @@ inline void PpcSetPairedFprInline(PPC_FPR& fpr, double packed)
|
||||
fpr.d = packed;
|
||||
}
|
||||
|
||||
// Must stay inside the XMM register domain. Bitcasting through a 64-bit GPR added a movq
|
||||
#if defined(__x86_64__)
|
||||
using PpcPairVec = __m128;
|
||||
#elif defined(__aarch64__)
|
||||
// Only 2 lanes are ever meaningful (a PPC paired-single register), so a 2-lane float32x2_t
|
||||
// (one 64-bit D register) is a more natural fit than mirroring x86's 128-bit register usage.
|
||||
using PpcPairVec = float32x2_t;
|
||||
#endif
|
||||
|
||||
// Must stay inside the vector register domain. Bitcasting through a 64-bit GPR added a movq
|
||||
// domain crossing on every paired-single op (630 in the THP IDCT region alone); a double
|
||||
// local already lives in an XMM register, so these casts compile to nothing.
|
||||
inline __m128 PpcPsToM128Inline(double value)
|
||||
// local already lives in a vector register, so these casts compile to nothing.
|
||||
inline PpcPairVec PpcPsToM128Inline(double value)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
return _mm_castpd_ps(_mm_set_sd(value));
|
||||
#elif defined(__aarch64__)
|
||||
return vreinterpret_f32_f64(vdup_n_f64(value));
|
||||
#endif
|
||||
}
|
||||
|
||||
inline double PpcM128ToPsInline(__m128 value)
|
||||
inline double PpcM128ToPsInline(PpcPairVec value)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
return _mm_cvtsd_f64(_mm_castps_pd(value));
|
||||
#elif defined(__aarch64__)
|
||||
return vget_lane_f64(vreinterpret_f64_f32(value), 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
inline __m128 PpcBroadcastPs0Inline(double value)
|
||||
inline PpcPairVec PpcBroadcastPs0Inline(double value)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
const __m128 lanes = PpcPsToM128Inline(value);
|
||||
return _mm_shuffle_ps(lanes, lanes, _MM_SHUFFLE(1, 1, 1, 1));
|
||||
#elif defined(__aarch64__)
|
||||
// ps0 lives in lane 1 (see the lane-accessor comment below).
|
||||
return vdup_lane_f32(PpcPsToM128Inline(value), 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
inline __m128 PpcBroadcastPs1Inline(double value)
|
||||
inline PpcPairVec PpcBroadcastPs1Inline(double value)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
const __m128 lanes = PpcPsToM128Inline(value);
|
||||
return _mm_shuffle_ps(lanes, lanes, _MM_SHUFFLE(0, 0, 0, 0));
|
||||
#elif defined(__aarch64__)
|
||||
// ps1 is already lane 0.
|
||||
return vdup_lane_f32(PpcPsToM128Inline(value), 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
inline __m128 PpcNegateNonNanLanesInline(__m128 value)
|
||||
inline PpcPairVec PpcNegateNonNanLanesInline(PpcPairVec value)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
const __m128 signMask = _mm_castsi128_ps(_mm_set1_epi32(static_cast<int>(0x80000000u)));
|
||||
const __m128 negated = _mm_xor_ps(value, signMask);
|
||||
const __m128 ordered = _mm_cmpord_ps(value, value);
|
||||
return _mm_or_ps(_mm_and_ps(ordered, negated), _mm_andnot_ps(ordered, value));
|
||||
#elif defined(__aarch64__)
|
||||
const uint32x2_t signMask = vdup_n_u32(0x80000000u);
|
||||
const float32x2_t negated = vreinterpret_f32_u32(veor_u32(vreinterpret_u32_f32(value), signMask));
|
||||
// NEON has no direct "ordered compare"; a value compares equal to itself iff it's not NaN.
|
||||
const uint32x2_t ordered = vceq_f32(value, value);
|
||||
return vbsl_f32(ordered, negated, value);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Paired-single lane accessors. The packed double's LOW 32 bits hold ps1 and HIGH 32 bits
|
||||
@@ -119,20 +153,34 @@ inline __m128 PpcNegateNonNanLanesInline(__m128 value)
|
||||
inline float PpcGetPs0Inline(double value)
|
||||
{
|
||||
// ps0 lives in lane 1; PpcBroadcastPs0Inline already splats it.
|
||||
#if defined(__x86_64__)
|
||||
return _mm_cvtss_f32(PpcBroadcastPs0Inline(value));
|
||||
#elif defined(__aarch64__)
|
||||
return vget_lane_f32(PpcBroadcastPs0Inline(value), 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
inline float PpcGetPs1Inline(double value)
|
||||
{
|
||||
// ps1 is already lane 0 of the packed representation.
|
||||
#if defined(__x86_64__)
|
||||
return _mm_cvtss_f32(PpcPsToM128Inline(value));
|
||||
#elif defined(__aarch64__)
|
||||
return vget_lane_f32(PpcPsToM128Inline(value), 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
inline double PpcPackPairedInline(float ps0, float ps1)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
// _mm_unpacklo_ps(x, y) -> { x[0], y[0], x[1], y[1] }, so lane 0 becomes
|
||||
// ps1 and lane 1 becomes ps0, matching the union layout bit for bit.
|
||||
return PpcM128ToPsInline(_mm_unpacklo_ps(_mm_set_ss(ps1), _mm_set_ss(ps0)));
|
||||
#elif defined(__aarch64__)
|
||||
// Lane 0 = ps1, lane 1 = ps0, matching the union layout bit for bit.
|
||||
const float32x2_t lane0 = vdup_n_f32(ps1);
|
||||
return PpcM128ToPsInline(vset_lane_f32(ps0, lane0, 1));
|
||||
#endif
|
||||
}
|
||||
|
||||
// FPSCR[NI] is modeled by MXCSR FTZ/DAZ, so arithmetic output flushing compiles to nothing.
|
||||
@@ -148,14 +196,24 @@ inline float PpcForceSingleValueInline(double value)
|
||||
// FPSCR[NI] flushes an exact pre-round single-subnormal even when rounding would promote it
|
||||
// to the smallest normal. g_mkwNiFlushThreshold (2^-126 active, 0.0 inactive) turns the
|
||||
// flush into a branchless mask: a set compare lane keeps just the sign bit, a clear lane
|
||||
// passes the value to CVTSD2SS. DAZ (set exactly when NI is) makes the compare itself treat
|
||||
// a subnormal as zero, matching the mask's answer.
|
||||
// passes the value to the double->float conversion. DAZ/FZ (set exactly when NI is) makes
|
||||
// the compare itself treat a subnormal as zero, matching the mask's answer.
|
||||
#if defined(__x86_64__)
|
||||
const __m128d v = _mm_set_sd(value);
|
||||
const __m128d signMask = _mm_set_sd(-0.0);
|
||||
const __m128d magnitude = _mm_andnot_pd(signMask, v);
|
||||
const __m128d flush = _mm_cmplt_sd(magnitude, _mm_set_sd(g_mkwNiFlushThreshold));
|
||||
const __m128d kept = _mm_andnot_pd(_mm_andnot_pd(signMask, flush), v);
|
||||
return static_cast<float>(_mm_cvtsd_f64(kept));
|
||||
#elif defined(__aarch64__)
|
||||
const float64x1_t v = vdup_n_f64(value);
|
||||
const uint64x1_t signMask = vdup_n_u64(0x8000000000000000ULL);
|
||||
const float64x1_t magnitude = vreinterpret_f64_u64(vbic_u64(vreinterpret_u64_f64(v), signMask));
|
||||
const uint64x1_t flush = vclt_f64(magnitude, vdup_n_f64(g_mkwNiFlushThreshold));
|
||||
const uint64x1_t signOnly = vand_u64(vreinterpret_u64_f64(v), signMask);
|
||||
const uint64x1_t kept = vbsl_u64(flush, signOnly, vreinterpret_u64_f64(v));
|
||||
return static_cast<float>(vget_lane_f64(vreinterpret_f64_u64(kept), 0));
|
||||
#endif
|
||||
}
|
||||
|
||||
inline float PpcFlushSingleForNiInline(float value)
|
||||
@@ -468,15 +526,72 @@ inline double PpcFnmsubsInline(double a, double c, double b)
|
||||
return static_cast<double>(std::isnan(result) ? result : -result);
|
||||
}
|
||||
|
||||
#if defined(__aarch64__)
|
||||
// x86 resolves a single NaN source operand (SNaN or QNaN alike) to that operand quieted with
|
||||
// sign and payload kept, and an invalid op with no NaN input to 0xFFC00000; NEON prefers SNaNs,
|
||||
// checks the accumulator first, and yields the positive default NaN 0x7FC00000. NaN result
|
||||
// lanes (rare: one branch on the pair) are therefore rewritten to the x86 answer, keeping the
|
||||
// two hosts bit-identical for every case where x86 itself is deterministic. With two or more
|
||||
// NaN operands even x86 is not: the winning operand depends on which FMA form / commuted
|
||||
// operand order clang picked per call site, so this resolver's fixed first-operand priority is
|
||||
// one of the answers a real x86 build can give, not a guaranteed match.
|
||||
inline uint64_t PpcPairNanLaneBitsInline(PpcPairVec value)
|
||||
{
|
||||
return vget_lane_u64(vreinterpret_u64_u32(vmvn_u32(vceq_f32(value, value))), 0);
|
||||
}
|
||||
|
||||
inline PpcPairVec PpcQuietPairInline(PpcPairVec value)
|
||||
{
|
||||
return vreinterpret_f32_u32(vorr_u32(vreinterpret_u32_f32(value), vdup_n_u32(0x00400000u)));
|
||||
}
|
||||
|
||||
inline PpcPairVec PpcResolveNanLanesInline(PpcPairVec result, PpcPairVec op1, PpcPairVec op2)
|
||||
{
|
||||
const uint32x2_t op1Nan = vmvn_u32(vceq_f32(op1, op1));
|
||||
const uint32x2_t op2Nan = vmvn_u32(vceq_f32(op2, op2));
|
||||
PpcPairVec replacement = vreinterpret_f32_u32(vdup_n_u32(0xFFC00000u));
|
||||
replacement = vbsl_f32(op2Nan, PpcQuietPairInline(op2), replacement);
|
||||
replacement = vbsl_f32(op1Nan, PpcQuietPairInline(op1), replacement);
|
||||
const uint32x2_t resultNan = vmvn_u32(vceq_f32(result, result));
|
||||
return vbsl_f32(resultNan, replacement, result);
|
||||
}
|
||||
|
||||
inline PpcPairVec PpcResolveNanLanes3Inline(
|
||||
PpcPairVec result, PpcPairVec op1, PpcPairVec op2, PpcPairVec op3)
|
||||
{
|
||||
const uint32x2_t op3Nan = vmvn_u32(vceq_f32(op3, op3));
|
||||
PpcPairVec replacement = vreinterpret_f32_u32(vdup_n_u32(0xFFC00000u));
|
||||
replacement = vbsl_f32(op3Nan, PpcQuietPairInline(op3), replacement);
|
||||
const uint32x2_t op2Nan = vmvn_u32(vceq_f32(op2, op2));
|
||||
replacement = vbsl_f32(op2Nan, PpcQuietPairInline(op2), replacement);
|
||||
const uint32x2_t op1Nan = vmvn_u32(vceq_f32(op1, op1));
|
||||
replacement = vbsl_f32(op1Nan, PpcQuietPairInline(op1), replacement);
|
||||
const uint32x2_t resultNan = vmvn_u32(vceq_f32(result, result));
|
||||
return vbsl_f32(resultNan, replacement, result);
|
||||
}
|
||||
#endif // defined(__aarch64__)
|
||||
|
||||
inline PpcPairVec PpcMulPairInline(PpcPairVec lhs, PpcPairVec rhs)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
return _mm_mul_ps(lhs, rhs);
|
||||
#elif defined(__aarch64__)
|
||||
const PpcPairVec result = vmul_f32(lhs, rhs);
|
||||
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
|
||||
return PpcResolveNanLanesInline(result, lhs, rhs);
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline double PPC_PsMulInline(double lhs, double rhs)
|
||||
{
|
||||
return PpcFlushPairedForNiInline(
|
||||
PpcM128ToPsInline(_mm_mul_ps(PpcPsToM128Inline(lhs), PpcPsToM128Inline(rhs))));
|
||||
PpcM128ToPsInline(PpcMulPairInline(PpcPsToM128Inline(lhs), PpcPsToM128Inline(rhs))));
|
||||
}
|
||||
|
||||
inline double PPC_PsMulNoNiInline(double lhs, double rhs)
|
||||
{
|
||||
return PpcM128ToPsInline(_mm_mul_ps(PpcPsToM128Inline(lhs), PpcPsToM128Inline(rhs)));
|
||||
return PpcM128ToPsInline(PpcMulPairInline(PpcPsToM128Inline(lhs), PpcPsToM128Inline(rhs)));
|
||||
}
|
||||
|
||||
// The paired madd family lowers to one hardware FMA. Semantics match the scalar lanes exactly: a
|
||||
@@ -485,9 +600,37 @@ inline double PPC_PsMulNoNiInline(double lhs, double rhs)
|
||||
// PpcNegateNonNanLanesInline. NI flushing is handled by MXCSR (see
|
||||
// MkwApplyHostNiMode), so the NI and NoNi entry points are identical here.
|
||||
|
||||
// Fused multiply-add/subtract on a pair. NEON's vfma_f32(acc, a, b) = acc + a*b has an
|
||||
// accumulator-first operand order, unlike x86's _mm_fmadd_ps(a, b, c) = a*b + c - msub is
|
||||
// therefore an fma against a negated accumulator. That vneg would flip a sole-NaN subtractor's
|
||||
// sign, which x86's vfmsub does not do, so the NaN resolver receives the original subtractor.
|
||||
inline PpcPairVec PpcFmaddPairInline(PpcPairVec multiplicand, PpcPairVec multiplier, PpcPairVec addend)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
return _mm_fmadd_ps(multiplicand, multiplier, addend);
|
||||
#elif defined(__aarch64__)
|
||||
const PpcPairVec result = vfma_f32(addend, multiplicand, multiplier);
|
||||
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
|
||||
return PpcResolveNanLanes3Inline(result, multiplicand, multiplier, addend);
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline PpcPairVec PpcFmsubPairInline(PpcPairVec multiplicand, PpcPairVec multiplier, PpcPairVec subtractor)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
return _mm_fmsub_ps(multiplicand, multiplier, subtractor);
|
||||
#elif defined(__aarch64__)
|
||||
const PpcPairVec result = vfma_f32(vneg_f32(subtractor), multiplicand, multiplier);
|
||||
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
|
||||
return PpcResolveNanLanes3Inline(result, multiplicand, multiplier, subtractor);
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline double PPC_PsMsubInline(double multiplicand, double multiplier, double subtractor)
|
||||
{
|
||||
return PpcM128ToPsInline(_mm_fmsub_ps(
|
||||
return PpcM128ToPsInline(PpcFmsubPairInline(
|
||||
PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(subtractor)));
|
||||
}
|
||||
|
||||
@@ -498,7 +641,7 @@ inline double PPC_PsMsubNoNiInline(double multiplicand, double multiplier, doubl
|
||||
|
||||
inline double PPC_PsMaddInline(double multiplicand, double multiplier, double addend)
|
||||
{
|
||||
return PpcM128ToPsInline(_mm_fmadd_ps(
|
||||
return PpcM128ToPsInline(PpcFmaddPairInline(
|
||||
PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(addend)));
|
||||
}
|
||||
|
||||
@@ -509,19 +652,19 @@ inline double PPC_PsMaddNoNiInline(double multiplicand, double multiplier, doubl
|
||||
|
||||
inline double PPC_PsMadds0Inline(double multiplicand, double multiplier, double addend)
|
||||
{
|
||||
return PpcM128ToPsInline(_mm_fmadd_ps(
|
||||
return PpcM128ToPsInline(PpcFmaddPairInline(
|
||||
PpcPsToM128Inline(multiplicand), PpcBroadcastPs0Inline(multiplier), PpcPsToM128Inline(addend)));
|
||||
}
|
||||
|
||||
inline double PPC_PsMadds1Inline(double multiplicand, double multiplier, double addend)
|
||||
{
|
||||
return PpcM128ToPsInline(_mm_fmadd_ps(
|
||||
return PpcM128ToPsInline(PpcFmaddPairInline(
|
||||
PpcPsToM128Inline(multiplicand), PpcBroadcastPs1Inline(multiplier), PpcPsToM128Inline(addend)));
|
||||
}
|
||||
|
||||
inline double PPC_PsNmsubInline(double multiplicand, double multiplier, double subtractor)
|
||||
{
|
||||
return PpcM128ToPsInline(PpcNegateNonNanLanesInline(_mm_fmsub_ps(
|
||||
return PpcM128ToPsInline(PpcNegateNonNanLanesInline(PpcFmsubPairInline(
|
||||
PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(subtractor))));
|
||||
}
|
||||
|
||||
@@ -532,20 +675,20 @@ inline double PPC_PsNmsubNoNiInline(double multiplicand, double multiplier, doub
|
||||
|
||||
inline double PPC_PsNmaddInline(double multiplicand, double multiplier, double addend)
|
||||
{
|
||||
return PpcM128ToPsInline(PpcNegateNonNanLanesInline(_mm_fmadd_ps(
|
||||
return PpcM128ToPsInline(PpcNegateNonNanLanesInline(PpcFmaddPairInline(
|
||||
PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(addend))));
|
||||
}
|
||||
|
||||
inline double PPC_PsMuls0Inline(double aValue, double cValue)
|
||||
{
|
||||
return PpcFlushPairedForNiInline(PpcM128ToPsInline(
|
||||
_mm_mul_ps(PpcPsToM128Inline(aValue), PpcBroadcastPs0Inline(cValue))));
|
||||
PpcMulPairInline(PpcPsToM128Inline(aValue), PpcBroadcastPs0Inline(cValue))));
|
||||
}
|
||||
|
||||
inline double PPC_PsMuls1Inline(double aValue, double cValue)
|
||||
{
|
||||
return PpcFlushPairedForNiInline(PpcM128ToPsInline(
|
||||
_mm_mul_ps(PpcPsToM128Inline(aValue), PpcBroadcastPs1Inline(cValue))));
|
||||
PpcMulPairInline(PpcPsToM128Inline(aValue), PpcBroadcastPs1Inline(cValue))));
|
||||
}
|
||||
|
||||
inline PPC_FPR PpcMakePairedResultInline(float ps0, float ps1);
|
||||
@@ -571,50 +714,105 @@ inline double PPC_PsToScalarInline(double value)
|
||||
return static_cast<double>(PpcGetPs0Inline(value));
|
||||
}
|
||||
|
||||
// ps_merge* are pure lane selections (result.ps0 from frA, result.ps1 from frB); with lane 0
|
||||
// == ps1 and lane 1 == ps0, two shuffles build the result bit-exact instead of round-tripping
|
||||
// through the pack helper.
|
||||
// ps_merge* are pure lane selections (result.ps0 from frA, result.ps1 from frB). On x86 two
|
||||
// shuffles build the result bit-exact without round-tripping through the pack helper; NEON has
|
||||
// no equally cheap 2-lane general shuffle, so its branch expresses the exact same selection
|
||||
// (verified against the x86 comments below, lane for lane) directly in terms of the portable
|
||||
// Get/Pack accessors instead.
|
||||
inline double PPC_PsMerge00Inline(double aValue, double bValue)
|
||||
{
|
||||
// lane0 = b.ps0 (b lane 1), lane1 = a.ps0 (a lane 1)
|
||||
// result.ps0 = a.ps0, result.ps1 = b.ps0 (lane0 = b.ps0/b lane1, lane1 = a.ps0/a lane1)
|
||||
#if defined(__x86_64__)
|
||||
const __m128 gathered = _mm_shuffle_ps(
|
||||
PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(1, 1, 1, 1));
|
||||
return PpcM128ToPsInline(_mm_shuffle_ps(gathered, gathered, _MM_SHUFFLE(0, 0, 2, 0)));
|
||||
#elif defined(__aarch64__)
|
||||
return PpcPackPairedInline(PpcGetPs0Inline(aValue), PpcGetPs0Inline(bValue));
|
||||
#endif
|
||||
}
|
||||
|
||||
inline double PPC_PsMerge01Inline(double aValue, double bValue)
|
||||
{
|
||||
// lane0 = b.ps1 (b lane 0), lane1 = a.ps0 (a lane 1)
|
||||
// result.ps0 = a.ps0, result.ps1 = b.ps1 (lane0 = b.ps1/b lane0, lane1 = a.ps0/a lane1)
|
||||
#if defined(__x86_64__)
|
||||
const __m128 gathered = _mm_shuffle_ps(
|
||||
PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(1, 1, 0, 0));
|
||||
return PpcM128ToPsInline(_mm_shuffle_ps(gathered, gathered, _MM_SHUFFLE(0, 0, 2, 0)));
|
||||
#elif defined(__aarch64__)
|
||||
return PpcPackPairedInline(PpcGetPs0Inline(aValue), PpcGetPs1Inline(bValue));
|
||||
#endif
|
||||
}
|
||||
|
||||
inline double PPC_PsMerge10Inline(double aValue, double bValue)
|
||||
{
|
||||
// lane0 = b.ps0 (b lane 1), lane1 = a.ps1 (a lane 0)
|
||||
// result.ps0 = a.ps1, result.ps1 = b.ps0 (lane0 = b.ps0/b lane1, lane1 = a.ps1/a lane0)
|
||||
#if defined(__x86_64__)
|
||||
const __m128 gathered = _mm_shuffle_ps(
|
||||
PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(0, 0, 1, 1));
|
||||
return PpcM128ToPsInline(_mm_shuffle_ps(gathered, gathered, _MM_SHUFFLE(0, 0, 2, 0)));
|
||||
#elif defined(__aarch64__)
|
||||
return PpcPackPairedInline(PpcGetPs1Inline(aValue), PpcGetPs0Inline(bValue));
|
||||
#endif
|
||||
}
|
||||
|
||||
inline double PPC_PsMerge11Inline(double aValue, double bValue)
|
||||
{
|
||||
// lane0 = b.ps1 (b lane 0), lane1 = a.ps1 (a lane 0): plain unpcklps.
|
||||
// result.ps0 = a.ps1, result.ps1 = b.ps1 (lane0 = b.ps1/b lane0, lane1 = a.ps1/a lane0):
|
||||
// plain unpcklps on x86.
|
||||
#if defined(__x86_64__)
|
||||
return PpcM128ToPsInline(
|
||||
_mm_unpacklo_ps(PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue)));
|
||||
#elif defined(__aarch64__)
|
||||
return PpcPackPairedInline(PpcGetPs1Inline(aValue), PpcGetPs1Inline(bValue));
|
||||
#endif
|
||||
}
|
||||
|
||||
inline PpcPairVec PpcAddPairInline(PpcPairVec lhs, PpcPairVec rhs)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
return _mm_add_ps(lhs, rhs);
|
||||
#elif defined(__aarch64__)
|
||||
const PpcPairVec result = vadd_f32(lhs, rhs);
|
||||
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
|
||||
return PpcResolveNanLanesInline(result, lhs, rhs);
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline PpcPairVec PpcSubPairInline(PpcPairVec lhs, PpcPairVec rhs)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
return _mm_sub_ps(lhs, rhs);
|
||||
#elif defined(__aarch64__)
|
||||
const PpcPairVec result = vsub_f32(lhs, rhs);
|
||||
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
|
||||
return PpcResolveNanLanesInline(result, lhs, rhs);
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline PpcPairVec PpcDivPairInline(PpcPairVec lhs, PpcPairVec rhs)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
return _mm_div_ps(lhs, rhs);
|
||||
#elif defined(__aarch64__)
|
||||
const PpcPairVec result = vdiv_f32(lhs, rhs);
|
||||
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
|
||||
return PpcResolveNanLanesInline(result, lhs, rhs);
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline double PPC_PsAddInline(double aValue, double bValue)
|
||||
{
|
||||
return PpcFlushPairedForNiInline(
|
||||
PpcM128ToPsInline(_mm_add_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
|
||||
PpcM128ToPsInline(PpcAddPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
|
||||
}
|
||||
|
||||
inline double PPC_PsAddNoNiInline(double aValue, double bValue)
|
||||
{
|
||||
return PpcM128ToPsInline(
|
||||
_mm_add_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)));
|
||||
PpcAddPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)));
|
||||
}
|
||||
|
||||
inline double PPC_PsSelInline(double lhsValue, double controlValue, double rhsValue)
|
||||
@@ -633,19 +831,19 @@ inline double PPC_PsSelInline(double lhsValue, double controlValue, double rhsVa
|
||||
inline double PPC_PsSubInline(double aValue, double bValue)
|
||||
{
|
||||
return PpcFlushPairedForNiInline(
|
||||
PpcM128ToPsInline(_mm_sub_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
|
||||
PpcM128ToPsInline(PpcSubPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
|
||||
}
|
||||
|
||||
inline double PPC_PsSubNoNiInline(double aValue, double bValue)
|
||||
{
|
||||
return PpcM128ToPsInline(
|
||||
_mm_sub_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)));
|
||||
PpcSubPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)));
|
||||
}
|
||||
|
||||
inline double PPC_PsDivInline(double aValue, double bValue)
|
||||
{
|
||||
return PpcFlushPairedForNiInline(
|
||||
PpcM128ToPsInline(_mm_div_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
|
||||
PpcM128ToPsInline(PpcDivPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
|
||||
}
|
||||
|
||||
inline double PPC_PsNegInline(double value)
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
#pragma once
|
||||
// FPSCR[NI] (non-IEEE flush-to-zero) modeled on the host FP environment, plus
|
||||
// the thread-local mirror of that state the hot paths read instead of MXCSR.
|
||||
// the thread-local mirror of that state the hot paths read instead of MXCSR/FPCR.
|
||||
|
||||
#include "ppc_isa_config.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Software-flushing Gekko's single-precision denormals per op roughly doubled the THP IDCT
|
||||
// kernel's cycle count, so instead the runtime mirrors guest FPSCR[NI] into host MXCSR FTZ+DAZ
|
||||
// wherever FPSCR can change (PPC_Mtfs*, fiber context switches, CpuContextScope), making per-op
|
||||
// flushes free. Accepted deviations (same trade Dolphin makes): FTZ also flushes double
|
||||
// denormals unlike real NI, and a pre-round-flush edge near FLT_MIN rounds via cvtsd2ss instead.
|
||||
inline constexpr uint32_t kMkwMxcsrFlushToZeroBits = (1u << 15) | (1u << 6); // FTZ | DAZ
|
||||
|
||||
// kernel's cycle count, so instead the runtime mirrors guest FPSCR[NI] into the host FP control
|
||||
// register's flush-to-zero bit(s) wherever FPSCR can change (PPC_Mtfs*, fiber context switches,
|
||||
// CpuContextScope), making per-op flushes free. Accepted deviations (same trade Dolphin makes):
|
||||
// flush-to-zero also flushes double denormals unlike real NI, and a pre-round-flush edge near
|
||||
// FLT_MIN rounds via cvtsd2ss (or its AArch64 equivalent) instead.
|
||||
#if defined(__x86_64__)
|
||||
// MXCSR: FTZ (bit 15, flushes arithmetic *output* denormals) and DAZ (bit 6, treats denormal
|
||||
// *inputs* as zero) are two separate bits on x86; both are set/cleared together here.
|
||||
inline constexpr uint32_t kMkwFpControlFlushToZeroBits = (1u << 15) | (1u << 6); // FTZ | DAZ
|
||||
#elif defined(__aarch64__)
|
||||
// FPCR: a single FZ bit (bit 24) flushes both single- and double-precision denormals, both as
|
||||
// inputs and outputs - actually a cleaner match to the "flush everything" trade above than x86's
|
||||
// two-bit combo, not an extra deviation.
|
||||
inline constexpr uint32_t kMkwFpControlFlushToZeroBits = (1u << 24); // FZ
|
||||
#else
|
||||
#error "ppc_isa_fpenv.h has no host FP control register mapping for this architecture"
|
||||
#endif
|
||||
|
||||
inline thread_local bool g_mkwHostNiActive = false;
|
||||
|
||||
@@ -22,32 +33,60 @@ inline thread_local bool g_mkwHostNiActive = false;
|
||||
inline constexpr double kMkwNiFlushThreshold = 0x1p-126; // 0x3810000000000000
|
||||
inline thread_local double g_mkwNiFlushThreshold = 0.0;
|
||||
|
||||
// Host FP control register access (MXCSR on x86_64, FPCR on AArch64), abstracted so
|
||||
// MkwApplyHostNiMode/MkwRestoreHostMxcsr and CpuContextScope (ppc_isa_context.h) don't each need
|
||||
// their own per-arch branch.
|
||||
inline uint32_t MkwGetHostFpControl() noexcept
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
return _mm_getcsr();
|
||||
#elif defined(__aarch64__)
|
||||
uint64_t fpcr = 0;
|
||||
__asm__ __volatile__("mrs %0, fpcr" : "=r"(fpcr));
|
||||
return static_cast<uint32_t>(fpcr);
|
||||
#endif
|
||||
}
|
||||
|
||||
inline void MkwSetHostFpControl(uint32_t value) noexcept
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
_mm_setcsr(value);
|
||||
#elif defined(__aarch64__)
|
||||
// Read-modify-write the full 64-bit FPCR: only the low 32 bits are architecturally defined
|
||||
// today, but a bare 32-bit write would zero the (reserved) upper half of the real register.
|
||||
uint64_t fpcr = 0;
|
||||
__asm__ __volatile__("mrs %0, fpcr" : "=r"(fpcr));
|
||||
fpcr = (fpcr & ~static_cast<uint64_t>(0xFFFFFFFFu)) | value;
|
||||
__asm__ __volatile__("msr fpcr, %0" :: "r"(fpcr));
|
||||
#endif
|
||||
}
|
||||
|
||||
inline void MkwApplyHostNiMode(uint32_t fpscr) noexcept
|
||||
{
|
||||
const uint32_t csr = _mm_getcsr();
|
||||
const uint32_t csr = MkwGetHostFpControl();
|
||||
const bool wantNi = (fpscr & 0x4u) != 0;
|
||||
const uint32_t want = wantNi
|
||||
? (csr | kMkwMxcsrFlushToZeroBits)
|
||||
: (csr & ~kMkwMxcsrFlushToZeroBits);
|
||||
? (csr | kMkwFpControlFlushToZeroBits)
|
||||
: (csr & ~kMkwFpControlFlushToZeroBits);
|
||||
if (want != csr)
|
||||
_mm_setcsr(want);
|
||||
// `want` has both bits set or both clear, so this is exactly
|
||||
// `(_mm_getcsr() & kMkwMxcsrFlushToZeroBits) != 0` after the write - the
|
||||
// mirror cannot disagree with the register even if the incoming CSR held
|
||||
// only one of the two bits.
|
||||
MkwSetHostFpControl(want);
|
||||
// `want` has every flush-to-zero bit set or every one clear, so this is exactly
|
||||
// `(MkwGetHostFpControl() & kMkwFpControlFlushToZeroBits) != 0` after the write - the
|
||||
// mirror cannot disagree with the register even if the incoming value held only some of
|
||||
// those bits.
|
||||
g_mkwHostNiActive = wantNi;
|
||||
g_mkwNiFlushThreshold = wantNi ? kMkwNiFlushThreshold : 0.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores a previously captured MXCSR value and re-derives the mirror from
|
||||
/// it. Every raw restore has to go through here; a bare _mm_setcsr would leave
|
||||
/// the mirror describing the FP environment that was just replaced.
|
||||
/// Restores a previously captured MXCSR/FPCR value and re-derives the mirror from it. Every raw
|
||||
/// restore has to go through here; a bare MkwSetHostFpControl would leave the mirror describing
|
||||
/// the FP environment that was just replaced.
|
||||
/// </summary>
|
||||
inline void MkwRestoreHostMxcsr(uint32_t csr) noexcept
|
||||
{
|
||||
_mm_setcsr(csr);
|
||||
const bool niActive = (csr & kMkwMxcsrFlushToZeroBits) != 0;
|
||||
MkwSetHostFpControl(csr);
|
||||
const bool niActive = (csr & kMkwFpControlFlushToZeroBits) != 0;
|
||||
g_mkwHostNiActive = niActive;
|
||||
g_mkwNiFlushThreshold = niActive ? kMkwNiFlushThreshold : 0.0;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
@@ -38,16 +39,26 @@ inline uint32_t PpcStorePsqFloatBitsInline(uint32_t value)
|
||||
|
||||
inline uint64_t PpcLoadPairPsqFloatBitsPackedInline(uint64_t value)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
const __m128i lanes = _mm_cvtsi64_si128(static_cast<long long>(value));
|
||||
const __m128i magnitude = _mm_and_si128(lanes, _mm_set1_epi32(0x7FFFFFFF));
|
||||
const __m128i nanMask = _mm_cmpgt_epi32(magnitude, _mm_set1_epi32(0x7F800000));
|
||||
const __m128i result = _mm_or_si128(
|
||||
lanes, _mm_and_si128(nanMask, _mm_set1_epi32(0x00400000)));
|
||||
return static_cast<uint64_t>(_mm_cvtsi128_si64(result));
|
||||
#elif defined(__aarch64__)
|
||||
// Equivalent to applying PpcLoadPsqFloatBitsInline to each 32-bit lane: the
|
||||
// x86 body above only ever acts on these same two lanes (the upper 64 bits
|
||||
// _mm_cvtsi64_si128 zero-fills never survive the final truncating extract).
|
||||
const uint32_t lo = PpcLoadPsqFloatBitsInline(static_cast<uint32_t>(value));
|
||||
const uint32_t hi = PpcLoadPsqFloatBitsInline(static_cast<uint32_t>(value >> 32));
|
||||
return (static_cast<uint64_t>(hi) << 32) | lo;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline uint64_t PpcStorePairPsqFloatBitsPackedInline(uint64_t value)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
const __m128i lanes = _mm_cvtsi64_si128(static_cast<long long>(value));
|
||||
const __m128i magnitude = _mm_and_si128(lanes, _mm_set1_epi32(0x7FFFFFFF));
|
||||
const __m128i subnormalMask = _mm_cmplt_epi32(magnitude, _mm_set1_epi32(0x00800000));
|
||||
@@ -60,8 +71,16 @@ inline uint64_t PpcStorePairPsqFloatBitsPackedInline(uint64_t value)
|
||||
_mm_and_si128(subnormalMask, signedZero),
|
||||
_mm_andnot_si128(subnormalMask, quieted));
|
||||
return static_cast<uint64_t>(_mm_cvtsi128_si64(result));
|
||||
#elif defined(__aarch64__)
|
||||
// Equivalent to applying PpcStorePsqFloatBitsInline to each 32-bit lane;
|
||||
// same reasoning as the load-side port above.
|
||||
const uint32_t lo = PpcStorePsqFloatBitsInline(static_cast<uint32_t>(value));
|
||||
const uint32_t hi = PpcStorePsqFloatBitsInline(static_cast<uint32_t>(value >> 32));
|
||||
return (static_cast<uint64_t>(hi) << 32) | lo;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(__x86_64__)
|
||||
inline __m128i PpcPsqSwapPairBytesInline(__m128i lanes)
|
||||
{
|
||||
const __m128i order = _mm_setr_epi8(
|
||||
@@ -95,21 +114,42 @@ inline __m128i PpcStorePairPsqFloatBitsLanesInline(__m128i lanes)
|
||||
_mm_and_si128(subnormalMask, signedZero),
|
||||
_mm_andnot_si128(subnormalMask, quieted));
|
||||
}
|
||||
#endif // defined(__x86_64__)
|
||||
|
||||
// host -> packed FPR double, guard already proven by the caller.
|
||||
inline double PpcLoadPairPsqFloatFromHostInline(const uint8_t* host)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
const __m128i raw = _mm_loadl_epi64(reinterpret_cast<const __m128i*>(host));
|
||||
return PpcM128ToPsInline(_mm_castsi128_ps(
|
||||
PpcLoadPairPsqFloatBitsLanesInline(PpcPsqSwapPairBytesInline(raw))));
|
||||
#elif defined(__aarch64__)
|
||||
// The x86 path's full 8-byte pshufb reversal plus a same-endian load is,
|
||||
// taken together, exactly a 64-bit byteswap of a plain little-endian load:
|
||||
// it turns the on-disk [ps0 big-endian][ps1 big-endian] byte layout into a
|
||||
// native uint64 with low 32 bits = ps1, high 32 bits = ps0 (this file's
|
||||
// documented packed-double lane convention).
|
||||
uint64_t raw = 0;
|
||||
std::memcpy(&raw, host, sizeof(raw));
|
||||
const uint64_t swapped = __builtin_bswap64(raw);
|
||||
return PpcBitCastToDoubleInline(PpcLoadPairPsqFloatBitsPackedInline(swapped));
|
||||
#endif
|
||||
}
|
||||
|
||||
// packed FPR double -> host, guard already proven by the caller.
|
||||
inline void PpcStorePairPsqFloatToHostInline(uint8_t* host, double value)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
const __m128i lanes = PpcStorePairPsqFloatBitsLanesInline(
|
||||
_mm_castps_si128(PpcPsToM128Inline(value)));
|
||||
_mm_storel_epi64(reinterpret_cast<__m128i*>(host), PpcPsqSwapPairBytesInline(lanes));
|
||||
#elif defined(__aarch64__)
|
||||
// Inverse of the load path above: bswap64 is its own inverse, so applying
|
||||
// it to the quieted packed value reproduces the on-disk big-endian bytes.
|
||||
const uint64_t quieted = PpcStorePairPsqFloatBitsPackedInline(PpcBitCastToU64Inline(value));
|
||||
const uint64_t swapped = __builtin_bswap64(quieted);
|
||||
std::memcpy(host, &swapped, sizeof(swapped));
|
||||
#endif
|
||||
}
|
||||
|
||||
template <typename SignedType>
|
||||
@@ -224,6 +264,8 @@ inline void PpcWritePairPsqInline(uint32_t addr, T first, T second)
|
||||
// reading stale bytes, and unmapped pages commit on demand, same as MemoryInline::Flat* loads.
|
||||
MKW_PPC_FORCE_INLINE const uint8_t* PpcTryGetPsqReadableHostInline(uint32_t addr)
|
||||
{
|
||||
if (GuestFlat::RequiresCheckedAccess()) [[unlikely]]
|
||||
return nullptr;
|
||||
return MKW_FLAT_GUEST_BASE + addr;
|
||||
}
|
||||
|
||||
@@ -234,6 +276,8 @@ MKW_PPC_FORCE_INLINE const uint8_t* PpcTryGetPsqReadableHostInline(uint32_t addr
|
||||
// executable, and unmapped pages still trap.
|
||||
MKW_PPC_FORCE_INLINE uint8_t* PpcTryGetPsqWritableHostInline(uint32_t addr)
|
||||
{
|
||||
if (GuestFlat::RequiresCheckedAccess()) [[unlikely]]
|
||||
return nullptr;
|
||||
if (addr > UINT32_MAX - 7u) [[unlikely]]
|
||||
return nullptr;
|
||||
if (MemoryInline::FlatWriteNeedsPolicy(addr) ||
|
||||
@@ -329,6 +373,7 @@ inline uint8_t PpcQuantizePsqU8Scale61Inline(float value)
|
||||
// matching PpcQuantizePsqU8Scale61Inline's !(scaled > 0) rule.
|
||||
inline uint16_t PpcQuantizePairPsqU8Scale61PackedInline(double value)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
const __m128 scaled = _mm_mul_ps(PpcPsToM128Inline(value), _mm_set1_ps(0.125f));
|
||||
const __m128 nonNegative = _mm_max_ps(scaled, _mm_setzero_ps());
|
||||
const __m128 clamped = _mm_min_ps(nonNegative, _mm_set1_ps(255.0f));
|
||||
@@ -338,6 +383,17 @@ inline uint16_t PpcQuantizePairPsqU8Scale61PackedInline(double value)
|
||||
// Native lane 0 is ps1 and lane 1 is ps0. Packing to the low uint16_t
|
||||
// therefore produces the guest-order numeric value (ps0 << 8) | ps1.
|
||||
return static_cast<uint16_t>(_mm_cvtsi128_si32(lanes8));
|
||||
#elif defined(__aarch64__)
|
||||
// Equivalent to two calls of the already-portable scalar quantizer above
|
||||
// (its own !(scaled > 0) rule maps NaN to 0, matching what MAXPS-with-zero
|
||||
// does on the x86 path per the comment there), packed the same way the
|
||||
// fallback path just below already does for the non-SIMD case.
|
||||
PPC_FPR fpr{};
|
||||
fpr.d = value;
|
||||
const uint8_t q0 = PpcQuantizePsqU8Scale61Inline(fpr.paired.ps0);
|
||||
const uint8_t q1 = PpcQuantizePsqU8Scale61Inline(fpr.paired.ps1);
|
||||
return static_cast<uint16_t>((static_cast<uint16_t>(q0) << 8) | static_cast<uint16_t>(q1));
|
||||
#endif
|
||||
}
|
||||
|
||||
// Preserve the complete memory/MMIO/executable-write behavior off the leaf
|
||||
|
||||
@@ -231,6 +231,13 @@ MKW_MEMORY_FORCE_INLINE uint8_t* ResolveRangeHost(uint32_t base, int32_t minOffs
|
||||
(void)needsRead;
|
||||
const uint32_t guestStart = base + static_cast<uint32_t>(minOffset);
|
||||
if (length == 0 || length > kPageSize || guestStart > UINT32_MAX - (length - 1)) return nullptr;
|
||||
if (GuestFlat::RequiresCheckedAccess()) {
|
||||
// A host page can cover multiple independently-special Wii pages.
|
||||
// Returning null keeps resolved accesses on the checked Memory::*
|
||||
// path, which materializes deferred reads and applies write policy.
|
||||
(void)needsWrite;
|
||||
return nullptr;
|
||||
}
|
||||
if (needsWrite &&
|
||||
(FlatWriteNeedsPolicy(guestStart) || FlatWriteNeedsPolicy(guestStart + (length - 1))))
|
||||
[[unlikely]] return nullptr;
|
||||
@@ -522,6 +529,13 @@ MKW_MEMORY_FORCE_INLINE void WriteResolvedFloat64(uint8_t* r, uint32_t o, uint32
|
||||
// around `*(T*)(base + addr)`, no page-table load or limit check (interception model documented
|
||||
// in guest_flat_memory.h). The one exception kept inline is the MMIO write policy, since the
|
||||
// written value can't be recovered from a fault record.
|
||||
//
|
||||
// When a host VM page is larger than a 4 KiB Wii page, guest-view protections
|
||||
// cannot distinguish adjacent special Wii pages. The general FlatRead*/
|
||||
// FlatWrite* helpers then use the checked page-table path, which materializes
|
||||
// deferred reads and applies executable-write/MMIO policy before touching RAM.
|
||||
// FlatWriteRam* remains direct because the translator emits it only for
|
||||
// addresses it has proven are ordinary RAM.
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE T FlatLoad(uint32_t address) {
|
||||
@@ -536,47 +550,62 @@ MKW_MEMORY_FORCE_INLINE void FlatStore(uint32_t address, T value) {
|
||||
std::memcpy(MKW_FLAT_GUEST_BASE + address, &swapped, sizeof(T));
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) { return FlatLoad<uint8_t>(address); }
|
||||
MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) { return FlatLoad<uint16_t>(address); }
|
||||
MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) { return FlatLoad<uint32_t>(address); }
|
||||
MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read8(address);
|
||||
return FlatLoad<uint8_t>(address);
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read16(address);
|
||||
return FlatLoad<uint16_t>(address);
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read32(address);
|
||||
return FlatLoad<uint32_t>(address);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE float FlatReadFloat32(uint32_t address) {
|
||||
const uint32_t bits = FlatLoad<uint32_t>(address);
|
||||
const uint32_t bits = FlatRead32(address);
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE double FlatReadFloat64(uint32_t address) {
|
||||
const uint64_t bits = FlatLoad<uint64_t>(address);
|
||||
const uint64_t bits = GuestFlat::RequiresCheckedAccess()
|
||||
? Memory::Read64(address) : FlatLoad<uint64_t>(address);
|
||||
double value = 0.0;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite8(uint32_t address, uint8_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write8(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write8Slow(address, value); return; }
|
||||
FlatStore<uint8_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite16(uint32_t address, uint16_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write16(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write16Slow(address, value); return; }
|
||||
FlatStore<uint16_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite32(uint32_t address, uint32_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write32(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write32Slow(address, value); return; }
|
||||
FlatStore<uint32_t>(address, value);
|
||||
}
|
||||
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat32(uint32_t address, double value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat32(address, value); return; }
|
||||
const uint32_t bits = ConvertPpcDoubleToSingleBits(value);
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat32Slow(address, value); return; }
|
||||
FlatStore<uint32_t>(address, bits);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat64(uint32_t address, double value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat64(address, value); return; }
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat64Slow(address, value); return; }
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
|
||||
namespace MusicAttenuation {
|
||||
|
||||
// Enables the optional Windows media-session integration. The monitor is
|
||||
// started lazily the first time this is enabled.
|
||||
// Enables the optional external-media integration (Windows media sessions,
|
||||
// or MPRIS over D-Bus on Linux). The monitor is started lazily the first time
|
||||
// this is enabled.
|
||||
void SetEnabled(bool enabled) noexcept;
|
||||
void SetMusicVolume(float volume) noexcept;
|
||||
void SetSoundEffectsVolume(float volume) noexcept;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "runtime_config.h"
|
||||
#include "nand_settings.h"
|
||||
#include "runtime_log.h"
|
||||
#include "system_bridge.h"
|
||||
|
||||
@@ -163,7 +164,7 @@ inline std::filesystem::path CreateManagedNandRoot() {
|
||||
return root;
|
||||
}
|
||||
|
||||
inline std::filesystem::path DiscoverNandRootPath() {
|
||||
inline std::filesystem::path ResolveNandRootPath() {
|
||||
const std::string configPath = RuntimeConfigFile::NandRoot();
|
||||
if (!configPath.empty()) {
|
||||
const auto path = ResolveConfiguredPath(configPath);
|
||||
@@ -179,4 +180,16 @@ inline std::filesystem::path DiscoverNandRootPath() {
|
||||
return CreateManagedNandRoot();
|
||||
}
|
||||
|
||||
inline std::filesystem::path DiscoverNandRootPath() {
|
||||
static const auto root = [] {
|
||||
const auto resolved = ResolveNandRootPath();
|
||||
std::string error;
|
||||
if (!RuntimeNandSettings::Ensure(resolved, error)) {
|
||||
FailNandRoot(error.c_str(), RuntimeNandSettings::FilePath(resolved));
|
||||
}
|
||||
return resolved;
|
||||
}();
|
||||
return root;
|
||||
}
|
||||
|
||||
} // namespace RuntimeNandPath
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <istream>
|
||||
|
||||
namespace RuntimeNandSave {
|
||||
|
||||
enum class Contents { Missing, Blank, Nonzero, Error };
|
||||
enum class ReadAction { Proceed, Missing, Error, RecoveryNeeded };
|
||||
|
||||
// A failed read is not evidence that a save is blank. Check badbit before EOF:
|
||||
// an I/O failure may set both, whereas a successful short final read sets EOF.
|
||||
inline Contents InspectStream(std::istream& input) {
|
||||
if (!input) return Contents::Error;
|
||||
char block[4096];
|
||||
for (;;) {
|
||||
input.read(block, sizeof(block));
|
||||
if (input.bad() || (input.fail() && !input.eof())) return Contents::Error;
|
||||
for (std::streamsize i = 0; i < input.gcount(); ++i) {
|
||||
if (block[i] != 0) return Contents::Nonzero;
|
||||
}
|
||||
if (input.eof()) return Contents::Blank;
|
||||
}
|
||||
}
|
||||
|
||||
inline Contents InspectFile(const std::filesystem::path& path) {
|
||||
std::error_code ec;
|
||||
const auto status = std::filesystem::symlink_status(path, ec);
|
||||
if (ec && ec != std::errc::no_such_file_or_directory) return Contents::Error;
|
||||
if (!std::filesystem::exists(status)) return Contents::Missing;
|
||||
if (!std::filesystem::is_regular_file(path, ec) || ec) return Contents::Error;
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
return InspectStream(input);
|
||||
}
|
||||
|
||||
// Probe only read-only opens of the actual save and its exact write shadow.
|
||||
// No probe writes, removes, or repairs data, and backups are not save aliases.
|
||||
inline ReadAction CheckRead(const std::filesystem::path& path, int mode) {
|
||||
const auto name = path.filename();
|
||||
const bool isMain = name == "rksys.dat";
|
||||
if (mode != 1 || (!isMain && name != "rksys.dat.nandsafe.tmp")) return ReadAction::Proceed;
|
||||
const auto contents = InspectFile(path);
|
||||
if (contents == Contents::Error) return ReadAction::Error;
|
||||
if (contents == Contents::Nonzero) return ReadAction::Proceed;
|
||||
if (isMain) {
|
||||
auto shadow = path;
|
||||
shadow += ".nandsafe.tmp";
|
||||
const auto shadowContents = InspectFile(shadow);
|
||||
if (shadowContents == Contents::Error) return ReadAction::Error;
|
||||
// The next write normally discards an old shadow. Preserve a possible
|
||||
// recovery source when there is no usable original, without promoting
|
||||
// an uncommitted (and potentially incomplete) shadow to the real save.
|
||||
if (shadowContents == Contents::Nonzero) return ReadAction::RecoveryNeeded;
|
||||
}
|
||||
return contents == Contents::Blank ? ReadAction::Missing : ReadAction::Proceed;
|
||||
}
|
||||
|
||||
} // namespace RuntimeNandSave
|
||||
@@ -0,0 +1,220 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace RuntimeNandSettings {
|
||||
|
||||
using Settings = std::map<std::string, std::string>;
|
||||
|
||||
inline std::filesystem::path FilePath(const std::filesystem::path& root) {
|
||||
return root / "title/00000001/00000002/data/setting.txt";
|
||||
}
|
||||
|
||||
// Wii setting.txt is a 256-byte buffer encrypted with a rotating XOR key.
|
||||
inline std::optional<Settings> Read(const std::filesystem::path& nandRoot) {
|
||||
std::ifstream input(FilePath(nandRoot), std::ios::binary);
|
||||
std::array<uint8_t, 256> bytes{};
|
||||
if (!input.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
uint32_t key = 0x73B5DBFAu;
|
||||
std::string decoded;
|
||||
for (const uint8_t byte : bytes) {
|
||||
const char value = static_cast<char>(byte ^ static_cast<uint8_t>(key));
|
||||
key = (key << 1) | (key >> 31);
|
||||
if (value == '\0') {
|
||||
break;
|
||||
}
|
||||
if (value != '\r') {
|
||||
decoded += value;
|
||||
}
|
||||
}
|
||||
Settings settings;
|
||||
for (size_t start = 0; start < decoded.size();) {
|
||||
const size_t end = decoded.find('\n', start);
|
||||
const std::string line = decoded.substr(start, end - start);
|
||||
const size_t equals = line.find('=');
|
||||
if (equals != std::string::npos && equals != 0) {
|
||||
settings.emplace(line.substr(0, equals), line.substr(equals + 1));
|
||||
}
|
||||
if (end == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
start = end + 1;
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
inline bool HasIdentity(const Settings& settings) {
|
||||
const auto serial = settings.find("SERNO");
|
||||
if (serial == settings.end() || serial->second.empty() || serial->second.size() > 9 ||
|
||||
serial->second.find_first_not_of("0123456789") != std::string::npos ||
|
||||
serial->second.find_first_not_of('0') == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& field : {std::pair{"CODE", 5u}, {"AREA", 3u}, {"GAME", 2u}}) {
|
||||
const auto value = settings.find(field.first);
|
||||
if (value == settings.end() || value->second.empty() ||
|
||||
value->second.size() > field.second) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Dolphin's normal (non-deterministic) first-boot algorithm. It is independent
|
||||
// of the ES device ID. Matching another NAND requires that NAND's saved serial.
|
||||
inline std::string GenerateSerial(std::time_t now) {
|
||||
if (now < 0) {
|
||||
return {};
|
||||
}
|
||||
const auto digits = std::to_string(now % 1000000000);
|
||||
return std::string(9 - digits.size(), '0') + digits;
|
||||
}
|
||||
|
||||
// This recompilation targets the European disc. These are Dolphin's PAL boot
|
||||
// defaults; an existing setting.txt always takes precedence, in every region.
|
||||
inline std::optional<std::array<uint8_t, 256>> EncodeNew(const std::string& serial) {
|
||||
const Settings identity{{"SERNO", serial}, {"CODE", "LEH"}, {"AREA", "EUR"}, {"GAME", "EU"}};
|
||||
if (!HasIdentity(identity)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::array<uint8_t, 256> bytes{};
|
||||
size_t position = 0;
|
||||
uint32_t key = 0x73B5DBFAu;
|
||||
const auto writeByte = [&](char value) {
|
||||
bytes[position++] = static_cast<uint8_t>(value) ^ static_cast<uint8_t>(key);
|
||||
key = (key << 1) | (key >> 31);
|
||||
};
|
||||
for (const std::string& line : {std::string("AREA=EUR\r\n"), std::string("MODEL=RVL-001(EUR)\r\n"),
|
||||
std::string("DVD=0\r\n"), std::string("MPCH=0x7FFE\r\n"), std::string("CODE=LEH\r\n"),
|
||||
"SERNO=" + serial + "\r\n", std::string("VIDEO=PAL\r\n"), std::string("GAME=EU\r\n")}) {
|
||||
for (;;) {
|
||||
if (position + line.size() > bytes.size()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto start = position;
|
||||
const auto savedKey = key;
|
||||
bool hasNull = false;
|
||||
for (const char value : line) {
|
||||
writeByte(value);
|
||||
hasNull |= bytes[position - 1] == 0;
|
||||
}
|
||||
if (!hasNull) {
|
||||
break;
|
||||
}
|
||||
// Nintendo stops at an encoded NUL. Dolphin inserts an extra LF
|
||||
// before this line and retries with the shifted encryption key.
|
||||
position = start;
|
||||
key = savedKey;
|
||||
writeByte('\n');
|
||||
}
|
||||
}
|
||||
return bytes; // The unused tail stays raw zero, as in Dolphin.
|
||||
}
|
||||
|
||||
// Atomically claim our own scratch directory. A collision belongs to another
|
||||
// launch (or a previous crashed launch); leave it untouched and try another name.
|
||||
inline std::optional<std::filesystem::path> CreateScratchDirectory(
|
||||
const std::filesystem::path& parent, const std::string& token, std::error_code& ec) {
|
||||
for (unsigned attempt = 0; attempt < 128; ++attempt) {
|
||||
const auto candidate = parent / (".setting-init-" + token + "-" + std::to_string(attempt));
|
||||
ec.clear();
|
||||
if (std::filesystem::create_directory(candidate, ec)) return candidate;
|
||||
if (ec && ec != std::errc::file_exists) return std::nullopt;
|
||||
}
|
||||
ec = std::make_error_code(std::errc::file_exists);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Never replace an existing file, including an unreadable or damaged one.
|
||||
// Publish a complete file atomically so simultaneous launches use one identity.
|
||||
inline bool Ensure(const std::filesystem::path& root, std::string& error,
|
||||
std::time_t now = std::time(nullptr)) {
|
||||
const auto path = FilePath(root);
|
||||
std::error_code ec;
|
||||
const auto status = std::filesystem::symlink_status(path, ec);
|
||||
if (ec && ec != std::errc::no_such_file_or_directory) {
|
||||
error = "Cannot inspect NAND setting.txt: " + ec.message();
|
||||
return false;
|
||||
}
|
||||
if (std::filesystem::exists(status)) {
|
||||
const auto existing = Read(root);
|
||||
if (existing && HasIdentity(*existing)) {
|
||||
return true;
|
||||
}
|
||||
error = "Existing NAND setting.txt is unreadable or invalid; restore it from this console's backup";
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto bytes = EncodeNew(GenerateSerial(now));
|
||||
if (!bytes) {
|
||||
error = "Cannot initialize NAND settings: invalid system clock";
|
||||
return false;
|
||||
}
|
||||
ec.clear();
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
if (ec) {
|
||||
error = "Cannot create NAND settings directory: " + ec.message();
|
||||
return false;
|
||||
}
|
||||
static std::atomic<unsigned> sequence{0};
|
||||
#ifdef _WIN32
|
||||
const auto processId = GetCurrentProcessId();
|
||||
#else
|
||||
const auto processId = getpid();
|
||||
#endif
|
||||
const auto scratch = CreateScratchDirectory(path.parent_path(),
|
||||
std::to_string(processId) + "-" + std::to_string(
|
||||
std::chrono::steady_clock::now().time_since_epoch().count()) + "-" +
|
||||
std::to_string(sequence++), ec);
|
||||
if (!scratch) {
|
||||
error = "Cannot create temporary NAND settings directory: " + ec.message();
|
||||
return false;
|
||||
}
|
||||
const auto temporary = *scratch / "setting.txt";
|
||||
bool written = false;
|
||||
{
|
||||
std::ofstream output(temporary, std::ios::binary);
|
||||
output.write(reinterpret_cast<const char*>(bytes->data()), bytes->size());
|
||||
output.close();
|
||||
written = static_cast<bool>(output);
|
||||
}
|
||||
bool published = false;
|
||||
if (written) {
|
||||
#ifdef _WIN32
|
||||
published = MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_WRITE_THROUGH) != 0;
|
||||
#else
|
||||
published = ::link(temporary.c_str(), path.c_str()) == 0;
|
||||
#endif
|
||||
}
|
||||
std::filesystem::remove(temporary, ec);
|
||||
std::filesystem::remove(*scratch, ec);
|
||||
// A competing launcher may have published its settings first. Always read
|
||||
// the winner from NAND rather than using our unpersisted candidate serial.
|
||||
const auto persisted = Read(root);
|
||||
if (persisted && HasIdentity(*persisted)) {
|
||||
return true;
|
||||
}
|
||||
error = published ? "Cannot read newly initialized NAND setting.txt" :
|
||||
"Cannot persist NAND setting.txt; check NAND directory permissions";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace RuntimeNandSettings
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
// Small host-services boundary for functionality that must not leak Win32
|
||||
// assumptions into runtime or game code. Guest execution, virtual memory, and
|
||||
// cooperative contexts remain outside this layer until dedicated macOS
|
||||
// prototypes establish a safe abstraction.
|
||||
namespace RuntimePlatform {
|
||||
|
||||
std::optional<std::filesystem::path> ExecutableDirectory() noexcept;
|
||||
|
||||
// Returns the platform's conventional per-user application-data directory.
|
||||
// It does not create the directory, leaving that policy to the caller.
|
||||
std::filesystem::path ApplicationDataDirectory(std::string_view applicationName);
|
||||
|
||||
// The root for per-run diagnostics. Keeping this here ensures log placement
|
||||
// follows the same host convention as configuration and other user data.
|
||||
std::filesystem::path LogDirectory(std::string_view applicationName);
|
||||
|
||||
uint64_t CurrentProcessId() noexcept;
|
||||
|
||||
} // namespace RuntimePlatform
|
||||
@@ -8,8 +8,10 @@
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -17,6 +19,7 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <toml.hpp>
|
||||
#include "platform/host_platform.h"
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
@@ -52,7 +55,31 @@ struct RuntimeUserConfig {
|
||||
std::optional<bool> audioMuted;
|
||||
std::optional<bool> audioMixWorker;
|
||||
std::optional<bool> attenuateMusicWhenMediaPlays;
|
||||
// Real Wii Remotes (with or without Nunchuk / Classic Controller) and Wii U Pro
|
||||
// Controllers paired over Bluetooth, driven by SDL's HIDAPI Wii driver. The driver
|
||||
// is opt-in on SDL's side, so this decides whether the runtime turns it on.
|
||||
std::optional<bool> wiiRemotes;
|
||||
// Keep re-enumerating Bluetooth HID devices while no Wii controller is connected
|
||||
// (Dolphin's "continuous scanning"), so a remote that dropped or was switched on
|
||||
// after launch shows up without restarting.
|
||||
std::optional<bool> wiiContinuousScan;
|
||||
// Accelerometer zero-point correction for the Bluetooth Wii Remote, in g and in
|
||||
// SDL's sensor frame (x right, y out of the button face, z towards the user).
|
||||
// SDL's Wii driver falls back to a nominal zero point when its read of the
|
||||
// remote's calibration block times out (common over Bluetooth), so this is
|
||||
// measured in the overlay with the remote at rest.
|
||||
std::optional<double> wiiAccelOffsetX;
|
||||
std::optional<double> wiiAccelOffsetY;
|
||||
std::optional<double> wiiAccelOffsetZ;
|
||||
// Debugging aid: append every KPAD sample of the Bluetooth remote (raw and
|
||||
// corrected accelerometer, buttons) to wii_accel_trace.csv next to Config.toml.
|
||||
std::optional<bool> wiiAccelTrace;
|
||||
std::optional<bool> networkEnabled;
|
||||
std::optional<bool> discordPresenceEnabled;
|
||||
// The application ID of the WiiCompiled Discord application. This is only
|
||||
// used by the base product; Retro Rewind supplies its own ID through the
|
||||
// standard Dolphin /dev/dolphin interface.
|
||||
std::optional<std::string> discordClientId;
|
||||
std::optional<std::string> nandRoot;
|
||||
std::optional<std::string> dvdRoot;
|
||||
// The one canonical Retro Rewind installation, owned and updated by the frontend. Setup records
|
||||
@@ -63,6 +90,9 @@ struct RuntimeUserConfig {
|
||||
// comma-separated SDL-style physical button names ("south", or
|
||||
// "dpad_up,left_shoulder") as values; pressing either bound button counts.
|
||||
std::array<std::optional<std::string>, 12> controllerButtons;
|
||||
std::optional<bool> rumbleEnabled;
|
||||
std::optional<int32_t> muteHotkey;
|
||||
std::map<std::string, std::string> controllerExpressions;
|
||||
};
|
||||
|
||||
namespace RuntimeConfigFile {
|
||||
@@ -136,7 +166,14 @@ inline bool IsSupportedResolutionMultiplier(float value) {
|
||||
// Must stay in step with the backend table in main.cpp, which is what actually
|
||||
// maps these to AuroraBackend.
|
||||
inline bool IsSupportedGraphicsApi(std::string_view value) {
|
||||
#if defined(__APPLE__)
|
||||
static constexpr std::array<std::string_view, 2> values{"auto", "metal"};
|
||||
// only vulkan for linux
|
||||
#elif defined(__linux__)
|
||||
static constexpr std::array<std::string_view, 2> values{"auto", "vulkan"};
|
||||
#elif defined(_WIN32)
|
||||
static constexpr std::array<std::string_view, 3> values{"auto", "d3d12", "vulkan"};
|
||||
#endif
|
||||
return std::find(values.begin(), values.end(), value) != values.end();
|
||||
}
|
||||
|
||||
@@ -167,6 +204,8 @@ inline std::optional<std::filesystem::path> ExecutableDirectory() {
|
||||
}
|
||||
buffer.resize(buffer.size() * 2);
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
return RuntimePlatform::ExecutableDirectory();
|
||||
#else
|
||||
// /proc/self/exe is a Linux-specific magic symlink to the running executable; readlink()
|
||||
// does not NUL-terminate and silently truncates if the buffer is too small, so this grows
|
||||
@@ -224,6 +263,8 @@ inline std::filesystem::path ApplicationDataDirectory() {
|
||||
CoTaskMemFree(rawPath);
|
||||
return directory;
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
return RuntimePlatform::ApplicationDataDirectory(kApplicationDirectoryName);
|
||||
#else
|
||||
// XDG Base Directory spec equivalent of FOLDERID_LocalAppData: $XDG_DATA_HOME if set and
|
||||
// non-empty, otherwise its default of $HOME/.local/share.
|
||||
@@ -286,6 +327,12 @@ inline void EnsureConfigFile() {
|
||||
"mix_worker = true\n\n"
|
||||
"[network]\n"
|
||||
"enabled = true\n\n"
|
||||
"[discord]\n"
|
||||
"# Rich Presence talks only to a locally-running Discord client.\n"
|
||||
"# Retro Rewind supplies its official app ID automatically. Set this\n"
|
||||
"# to WiiCompiled's Discord application ID for basic base-game presence.\n"
|
||||
"enabled = true\n"
|
||||
"# client_id = \"123456789012345678\"\n\n"
|
||||
"[paths]\n"
|
||||
"# dvd_root = \"D:\\\\MarioKartWii\\\\DATA\"\n"
|
||||
"# nand_root = \"D:\\\\WiiNand\"\n"
|
||||
@@ -352,6 +399,7 @@ inline void AppendOverlayRoots(RuntimeUserConfig& config, const std::string& roo
|
||||
}
|
||||
}
|
||||
|
||||
// Reads every supported setting out of a parsed Config.toml document.
|
||||
inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
RuntimeUserConfig config;
|
||||
|
||||
@@ -363,6 +411,20 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
FindConfigValue<std::string>(document, "controller", buttonKeys[index]);
|
||||
}
|
||||
|
||||
config.rumbleEnabled = FindConfigValue<bool>(document, "controller", "rumble");
|
||||
if (auto value = FindConfigInt(document, "audio", "mute_key")) {
|
||||
config.muteHotkey = *value;
|
||||
}
|
||||
|
||||
if (const auto* section = document.contains("controller") ? &document.at("controller") : nullptr;
|
||||
section != nullptr && section->is_table()) {
|
||||
for (const auto& [key, value] : section->as_table()) {
|
||||
if (key.rfind("expr_", 0) == 0 && value.is_string()) {
|
||||
config.controllerExpressions[key] = value.as_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
|
||||
config.windowPosX = FindConfigInt(document, "video", "window_x");
|
||||
config.windowPosY = FindConfigInt(document, "video", "window_y");
|
||||
@@ -417,7 +479,15 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
config.audioMixWorker = FindConfigValue<bool>(document, "audio", "mix_worker");
|
||||
config.attenuateMusicWhenMediaPlays =
|
||||
FindConfigValue<bool>(document, "audio", "attenuate_music_when_media_plays");
|
||||
config.wiiRemotes = FindConfigValue<bool>(document, "controller", "wii_remotes");
|
||||
config.wiiContinuousScan = FindConfigValue<bool>(document, "controller", "wii_continuous_scan");
|
||||
config.wiiAccelOffsetX = FindConfigValue<double>(document, "controller", "wii_accel_offset_x");
|
||||
config.wiiAccelOffsetY = FindConfigValue<double>(document, "controller", "wii_accel_offset_y");
|
||||
config.wiiAccelOffsetZ = FindConfigValue<double>(document, "controller", "wii_accel_offset_z");
|
||||
config.wiiAccelTrace = FindConfigValue<bool>(document, "controller", "wii_accel_trace");
|
||||
config.networkEnabled = FindConfigValue<bool>(document, "network", "enabled");
|
||||
config.discordPresenceEnabled = FindConfigValue<bool>(document, "discord", "enabled");
|
||||
config.discordClientId = FindConfigValue<std::string>(document, "discord", "client_id");
|
||||
|
||||
config.nandRoot = FindConfigValue<std::string>(document, "paths", "nand_root");
|
||||
config.dvdRoot = FindConfigValue<std::string>(document, "paths", "dvd_root");
|
||||
@@ -625,6 +695,34 @@ inline bool SetControllerButton(size_t index, std::string value) {
|
||||
return WriteSetting("controller", kControllerButtonKeys[index], FormatString(value));
|
||||
}
|
||||
|
||||
inline std::string ControllerExpression(const std::string& key) {
|
||||
const auto it = Get().controllerExpressions.find(key);
|
||||
return it == Get().controllerExpressions.end() ? std::string() : it->second;
|
||||
}
|
||||
|
||||
inline bool SetControllerExpression(const std::string& key, const std::string& value) {
|
||||
Mutable().controllerExpressions[key] = value;
|
||||
return WriteSetting("controller", key, FormatString(value));
|
||||
}
|
||||
|
||||
inline bool RumbleEnabled(bool fallback = true) {
|
||||
return Get().rumbleEnabled.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool SetRumbleEnabled(bool value) {
|
||||
Mutable().rumbleEnabled = value;
|
||||
return WriteSetting("controller", "rumble", value ? "true" : "false");
|
||||
}
|
||||
|
||||
inline int32_t MuteHotkey(int32_t fallback) {
|
||||
return Get().muteHotkey.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool SetMuteHotkey(int32_t value) {
|
||||
Mutable().muteHotkey = value;
|
||||
return WriteSetting("audio", "mute_key", std::to_string(value));
|
||||
}
|
||||
|
||||
inline bool SetAudioVolume(float value) {
|
||||
value = std::clamp(value, 0.0f, 1.0f);
|
||||
Mutable().audioVolume = value;
|
||||
@@ -734,14 +832,75 @@ inline bool AudioMixWorkerEnabled(bool fallback = true) {
|
||||
return Get().audioMixWorker.value_or(fallback);
|
||||
}
|
||||
|
||||
// Whether background music should duck automatically for other media playback.
|
||||
inline bool AttenuateMusicWhenMediaPlays(bool fallback = false) {
|
||||
return Get().attenuateMusicWhenMediaPlays.value_or(fallback);
|
||||
}
|
||||
|
||||
// Bluetooth Wii Remotes / Wii U Pro Controllers. Read once before SDL's joystick
|
||||
// subsystem comes up, so a change only takes effect on the next launch.
|
||||
inline bool WiiRemotesEnabled(bool fallback = true) {
|
||||
return Get().wiiRemotes.value_or(fallback);
|
||||
}
|
||||
|
||||
// Persists the Bluetooth Wii Remote driver switch.
|
||||
inline bool SetWiiRemotesEnabled(bool value) {
|
||||
Mutable().wiiRemotes = value;
|
||||
return WriteSetting("controller", "wii_remotes", value ? "true" : "false");
|
||||
}
|
||||
|
||||
// Whether to keep rescanning Bluetooth while no Wii controller is connected.
|
||||
inline bool WiiContinuousScanEnabled(bool fallback = false) {
|
||||
return Get().wiiContinuousScan.value_or(fallback);
|
||||
}
|
||||
|
||||
// Persists the continuous scanning switch.
|
||||
inline bool SetWiiContinuousScanEnabled(bool value) {
|
||||
Mutable().wiiContinuousScan = value;
|
||||
return WriteSetting("controller", "wii_continuous_scan", value ? "true" : "false");
|
||||
}
|
||||
|
||||
// Wii Remote accelerometer zero-point correction (g, SDL sensor frame); all zero
|
||||
// when the remote has not been calibrated.
|
||||
inline std::array<double, 3> WiiAccelOffset() {
|
||||
const RuntimeUserConfig& config = Get();
|
||||
return {config.wiiAccelOffsetX.value_or(0.0), config.wiiAccelOffsetY.value_or(0.0),
|
||||
config.wiiAccelOffsetZ.value_or(0.0)};
|
||||
}
|
||||
|
||||
// Whether to write the per-frame accelerometer trace (off unless asked for).
|
||||
inline bool WiiAccelTraceEnabled(bool fallback = false) {
|
||||
return Get().wiiAccelTrace.value_or(fallback);
|
||||
}
|
||||
|
||||
// True while a non-zero correction is stored ("Clear calibration" writes zeros).
|
||||
inline bool HasWiiAccelOffset() {
|
||||
const std::array<double, 3> offset = WiiAccelOffset();
|
||||
return offset[0] != 0.0 || offset[1] != 0.0 || offset[2] != 0.0;
|
||||
}
|
||||
|
||||
// Persists the accelerometer correction measured by the overlay's calibration.
|
||||
inline bool SetWiiAccelOffset(const std::array<double, 3>& offset) {
|
||||
Mutable().wiiAccelOffsetX = offset[0];
|
||||
Mutable().wiiAccelOffsetY = offset[1];
|
||||
Mutable().wiiAccelOffsetZ = offset[2];
|
||||
bool ok = true;
|
||||
const char* keys[3] = {"wii_accel_offset_x", "wii_accel_offset_y", "wii_accel_offset_z"};
|
||||
for (size_t i = 0; i < 3; ++i) {
|
||||
// Always a float literal, so a whole-number offset does not come back as a TOML integer.
|
||||
std::ostringstream formatted;
|
||||
formatted << std::fixed << std::setprecision(4) << offset[i];
|
||||
ok = WriteSetting("controller", keys[i], formatted.str()) && ok;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Target frame rate for frame interpolation, or 0 to disable it.
|
||||
inline uint32_t FrameInterpolationFps(uint32_t fallback = 0) {
|
||||
return Get().frameInterpolationFps.value_or(fallback);
|
||||
}
|
||||
|
||||
// Whether to skip draws whose graphics pipeline has not finished compiling yet.
|
||||
inline bool SkipUnreadyPipelines(bool fallback = true) {
|
||||
return Get().skipUnreadyPipelines.value_or(fallback);
|
||||
}
|
||||
@@ -815,6 +974,14 @@ inline std::string RetroRewindRoot(std::string fallback = "") {
|
||||
return Get().retroRewindRoot.value_or(std::move(fallback));
|
||||
}
|
||||
|
||||
inline bool DiscordPresenceEnabled(bool fallback = true) {
|
||||
return Get().discordPresenceEnabled.value_or(fallback);
|
||||
}
|
||||
|
||||
inline std::string DiscordClientId(std::string fallback = "1543984562369990706") {
|
||||
return Get().discordClientId.value_or(std::move(fallback));
|
||||
}
|
||||
|
||||
inline const std::vector<std::string>& OverlayRoots() {
|
||||
return Get().overlayRoots;
|
||||
}
|
||||
@@ -871,6 +1038,9 @@ inline void LogLoadedConfig() {
|
||||
if (config.networkEnabled) {
|
||||
std::cout << " network_enabled=" << (*config.networkEnabled ? "true" : "false");
|
||||
}
|
||||
if (config.discordPresenceEnabled) {
|
||||
std::cout << " discord_enabled=" << (*config.discordPresenceEnabled ? "true" : "false");
|
||||
}
|
||||
if (config.nandRoot) {
|
||||
std::cout << " nand_root=" << *config.nandRoot;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <charconv>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
|
||||
namespace RuntimeScSerial {
|
||||
|
||||
// SCGetProductSN's output is a u32, not a character buffer. DWC loads
|
||||
// that word and formats it with the product code to construct csnum.
|
||||
template <typename RangeValidator, typename WordWriter>
|
||||
uint32_t Write(std::string_view serial, uint32_t address,
|
||||
RangeValidator&& contains, WordWriter&& write32) {
|
||||
if (serial.empty() || serial.size() > 9 ||
|
||||
serial.find_first_not_of("0123456789") != std::string_view::npos) return 0;
|
||||
uint32_t number = 0;
|
||||
const auto parsed = std::from_chars(serial.data(), serial.data() + serial.size(), number);
|
||||
if (parsed.ec != std::errc{} || parsed.ptr != serial.data() + serial.size() ||
|
||||
!address || !contains(address, sizeof(uint32_t))) return 0;
|
||||
write32(address, number);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace RuntimeScSerial
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user