Merge upstream main into openxr-work

This commit is contained in:
iChris4 committed 2026-09-03 15:08:50 +02:00
commit 60316f0115
69 files changed
+5581 -315

No files matched your search

+2 -2
View File
@@ -18,11 +18,11 @@ jobs:
name: Translator (build + test) name: Translator (build + test)
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-dotnet@v4 - uses: actions/setup-dotnet@v6
with: with:
dotnet-version: '8.0.x' dotnet-version: '8.0.x'
+158
View File
@@ -0,0 +1,158 @@
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:
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.
# patchelf: prepare-portable-tools.sh bundles the host libraries the llvm.org binaries link
# against and rewrites their RPATH to find them.
- name: Install dependencies (required by appimagetool, SDL3 build and toolchain bundling)
run: |
sudo apt-get update
sudo apt-get install -y libfuse2 patchelf 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]
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
+1
View File
@@ -25,6 +25,7 @@ Code.pul
# Build output # Build output
/build/ /build/
/build-*/ /build-*/
/native-build/
/dist/ /dist/
/out/ /out/
[Bb]in/ [Bb]in/
+13 -7
View File
@@ -88,17 +88,23 @@ Assert-File $setupProject '.NET setup project'
Assert-File $translatorProject 'Translator CLI project' Assert-File $translatorProject 'Translator CLI project'
if (-not (Test-Path -LiteralPath (Join-Path $portableTools 'llvm-mingw\bin\x86_64-w64-mingw32-clang++.exe'))) { 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 & (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 'CMake\bin\cmake.exe') 'Portable CMake'
Assert-File (Join-Path $portableTools 'Ninja\ninja.exe') 'Portable Ninja' 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 # native_prebuilt carries the aurora/third-party archives the user no longer has
# to compile (launcher/Prepare-NativePrebuilt.ps1). # to compile (launcher/Prepare-NativePrebuilt.ps1).
# Kept in step with InstalledLayout.DependencyNames by Test-PinnedFacts.ps1: the installed host # 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. # 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','openxr','png','SDL','sqlite3','tracy','xxhash','zlib','zstd') $requiredDependencies = @('abseil-cpp','cppwinrt','dawn_prebuilt','fmt','freetype','imgui','libusb','native_prebuilt','openxr','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 # 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 # 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 # 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 # 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 # NodToolProvider.cs always does (Launcher/artifacts/nodtool.exe)
# -DolphinToolPath handoff with an automated, pinned acquisition step.
$nodToolCliProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup.Common.Cli' $nodToolCliProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup.Common.Cli'
$nodTool = (& dotnet run --project $nodToolCliProject -c Release -- --workspace $repoRoot | Select-Object -Last 1) $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." } if ($LASTEXITCODE -ne 0) { throw "nodtool resolution failed with exit code $LASTEXITCODE." }
@@ -277,7 +282,7 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
$manifest = [ordered]@{ $manifest = [ordered]@{
SchemaVersion = 2 SchemaVersion = 2
ProductVersion = '0.2.25' ProductVersion = '0.2.26'
ExpectedGameId = $pins.GameId ExpectedGameId = $pins.GameId
ExpectedDolSha256 = $pins.DolSha256 ExpectedDolSha256 = $pins.DolSha256
ExpectedRelSha256 = $pins.RelSha256 ExpectedRelSha256 = $pins.RelSha256
@@ -293,11 +298,12 @@ $manifest = [ordered]@{
$manifest | ConvertTo-Json | Set-Content (Join-Path $payloadRoot 'payload-manifest.json') -Encoding UTF8 $manifest | ConvertTo-Json | Set-Content (Join-Path $payloadRoot 'payload-manifest.json') -Encoding UTF8
Write-Host '[4/6] Enforcing the copyright and generated-code boundary...' 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 & (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 ` & (Join-Path $PSScriptRoot 'Test-NativeDependencies.ps1') -PayloadRoot $payloadRoot -SetupHost $setupHost `
-LlvmReadobjPath (Join-Path $portableTools 'llvm-mingw\bin\llvm-readobj.exe') -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...' Write-Host '[5/6] Creating the canonical installer payload...'
$payloadZip = Join-Path $workRoot 'payload.zip' $payloadZip = Join-Path $workRoot 'payload.zip'
+203
View File
@@ -0,0 +1,203 @@
# 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
# its owning CMake file 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'
$runtimeCMake = Join-Path $repoRoot 'runtime\CMakeLists.txt'
# 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/encounter/dawn-build/releases/download/v20260603.191052/dawn-windows-amd64.tar.gz')
Pins = @(@{ File = $auroraCMake; Text = 'set(AURORA_DAWN_VERSION "v20260603.191052"' },
@{ File = $auroraDawn; Text = 'SHA256=7785373d569b3b0237918ec9c523239f7d0667857c5ea8242e3cdfde95e6aeab' })
},
[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 = 'openxr'; File = 'OpenXR-SDK-release-1.1.61.tar.gz'
Uris = @('https://github.com/KhronosGroup/OpenXR-SDK/archive/refs/tags/release-1.1.61.tar.gz')
Pins = @(@{ File = $runtimeCMake; Text = 'OpenXR-SDK/archive/refs/tags/release-1.1.61.tar.gz' },
@{ File = $runtimeCMake; Text = 'SHA256=d5e2773d282642e6e250bd10266f316fc3ff3b8602c83db009a1f434205998e6' })
},
[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"
-2
View File
@@ -61,8 +61,6 @@ Assert-File $windres 'Portable resource compiler'
Assert-File $clangBinary 'Portable clang driver binary' Assert-File $clangBinary 'Portable clang driver binary'
Assert-Directory $dependencies 'Pinned offline dependency sources' Assert-Directory $dependencies 'Pinned offline dependency sources'
Assert-Directory $auroraSource 'aurora-main source tree' 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 } if ($Parallel -le 0) { $Parallel = [Environment]::ProcessorCount }
+590
View File
@@ -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"
+19 -5
View File
@@ -77,14 +77,28 @@ if ($runtimeEntry -ne $pins.EntryPoint) {
Add-Failure "system_bridge.h enters at $runtimeEntry but recomp.yml translates from $($pins.EntryPoint)." Add-Failure "system_bridge.h enters at $runtimeEntry but recomp.yml translates from $($pins.EntryPoint)."
} }
# --- The pinned dependency set: Build-Installer.ps1 stages it, the installed host requires it. # --- The pinned dependency set: Build-Installer.ps1 stages it, the installed host requires it,
# --- and Prepare-Dependencies.ps1 must be able to supply every source tree. native_prebuilt is
# --- intentionally produced later by Prepare-NativePrebuilt.ps1; cppwinrt is generated separately
# --- by Prepare-Dependencies.ps1 rather than appearing in its downloaded $packages array.
$installedLayout = Read-SourceFile (Join-Path $setup 'InstalledLayout.cs') 'InstalledLayout.cs' $installedLayout = Read-SourceFile (Join-Path $setup 'InstalledLayout.cs') 'InstalledLayout.cs'
$buildInstaller = Read-SourceFile (Join-Path $launcher 'Build-Installer.ps1') 'Build-Installer.ps1' $buildInstaller = Read-SourceFile (Join-Path $launcher 'Build-Installer.ps1') 'Build-Installer.ps1'
Compare-Set (Get-QuotedList $installedLayout '(?s)DependencyNames\s*=\s*\[(.*?)\]' '"([^"]+)"' ` $dependencyNames = Get-QuotedList $installedLayout '(?s)DependencyNames\s*=\s*\[(.*?)\]' `
'InstalledLayout.DependencyNames') ` '"([^"]+)"' 'InstalledLayout.DependencyNames'
(Get-QuotedList $buildInstaller '(?s)\$requiredDependencies\s*=\s*@\((.*?)\)' "'([^']+)'" ` $installerDependencies = Get-QuotedList $buildInstaller `
'Build-Installer.ps1 $requiredDependencies') ` '(?s)\$requiredDependencies\s*=\s*@\((.*?)\)' "'([^']+)'" `
'Build-Installer.ps1 $requiredDependencies'
Compare-Set $dependencyNames $installerDependencies `
'InstalledLayout.DependencyNames' 'Build-Installer.ps1 $requiredDependencies' 'InstalledLayout.DependencyNames' 'Build-Installer.ps1 $requiredDependencies'
$dependencyPreparer = Read-SourceFile (Join-Path $launcher 'Prepare-Dependencies.ps1') `
'Prepare-Dependencies.ps1'
$downloadedDependencies = Get-QuotedList $dependencyPreparer `
'(?sm)\$packages\s*=\s*@\((.*?)^\)' "Name\s*=\s*'([^']+)'" `
'Prepare-Dependencies.ps1 $packages'
$preparedDependencies = @($downloadedDependencies) + @('cppwinrt')
$sourceDependencies = @($dependencyNames | Where-Object { $_ -ne 'native_prebuilt' })
Compare-Set $sourceDependencies $preparedDependencies `
'InstalledLayout.DependencyNames source entries' 'Prepare-Dependencies.ps1 outputs'
# --- The runtime assets copied beside a product: the host hashes exactly these names, so the build # --- The runtime assets copied beside a product: the host hashes exactly these names, so the build
# --- script must publish every one of them or every product reports its support files as stale. # --- script must publish every one of them or every product reports its support files as stale.
@@ -53,9 +53,22 @@ public static class RetroWfcPayload
var root = Path.GetFullPath(stagedDirectory); var root = Path.GetFullPath(stagedDirectory);
var payload = Path.Combine(root, RetroWfcOfflinePayloadFile); 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( throw new InvalidDataException(
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin."); "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); ValidateRetroWfcPayloadFile(payload, signingKey);
return root; return root;
} }
@@ -12,7 +12,9 @@ internal static class BuildRunner
public static async Task RunAsync( public static async Task RunAsync(
string workspace, string profile, string outputDir, string? baseOutputDir, string workspace, string profile, string outputDir, string? baseOutputDir,
string? retroDir, string? retroWfcOfflineDir, bool skipRetroWfcPayload, 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, IInstallReporter reporter,
CancellationToken cancellationToken)
{ {
var script = Path.Combine(workspace, "Launcher", "local-build.sh"); var script = Path.Combine(workspace, "Launcher", "local-build.sh");
if (!File.Exists(script)) throw new FileNotFoundException("local-build.sh is missing", script); if (!File.Exists(script)) throw new FileNotFoundException("local-build.sh is missing", script);
@@ -47,6 +49,34 @@ internal static class BuildRunner
{ {
startInfo.ArgumentList.Add("--translator-bin"); startInfo.ArgumentList.Add(translatorBin); 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);
}
using var process = new Process { StartInfo = startInfo }; using var process = new Process { StartInfo = startInfo };
var window = new BuildProgressWindow(reporter, InstallStages.Build, start: 6, end: 96); var window = new BuildProgressWindow(reporter, InstallStages.Build, start: 6, end: 96);
+8 -1
View File
@@ -150,6 +150,12 @@ internal static class Program
skipPayload, skipPayload,
flags.ContainsKey("force-clean-build"), flags.ContainsKey("force-clean-build"),
flags.GetValueOrDefault("translator-bin"), 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"),
reporter, token); reporter, token);
reporter.Progress(InstallStages.Shortcuts, "Creating shortcuts", 98); reporter.Progress(InstallStages.Shortcuts, "Creating shortcuts", 98);
@@ -317,7 +323,8 @@ internal static class Program
install [--game ISO_PATH] [--install-dir DIR] [--retro-dir DIR install [--game ISO_PATH] [--install-dir DIR] [--retro-dir DIR
{--download-retro-wfc-payload | --skip-retro-wfc-payload}] {--download-retro-wfc-payload | --skip-retro-wfc-payload}]
[--force-clean-build] [--translator-bin PATH] [--disc-tool-bin PATH] [--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] [--progress-json] [--workspace DIR]
uninstall uninstall
launch-base launch-base
launch-retro launch-retro
@@ -121,7 +121,7 @@ internal static class PlatformChecks
internal static class ProductInfo internal static class ProductInfo
{ {
public const string Name = "WiiCompiled"; public const string Name = "WiiCompiled";
public const string Version = "0.2.25"; public const string Version = "0.2.26";
/// <summary> /// <summary>
/// The setup executable is copied into the installation under this name. It is the launcher and /// The setup executable is copied into the installation under this name. It is the launcher and
@@ -7,7 +7,7 @@
<AssemblyName>WiiCompiled.Setup</AssemblyName> <AssemblyName>WiiCompiled.Setup</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace> <RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.2.25</Version> <Version>0.2.26</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled</Description> <Description>Command-line installer and launcher for WiiCompiled</Description>
+96 -5
View File
@@ -5,8 +5,12 @@
# self-contained binaries, and `nodtool` (a prebuilt MIT/Apache-2.0 CLI from encounter/nod, see # 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 # 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. # --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 # A pruned native clang/lld/cmake/ninja toolchain (see prepare-portable-tools.sh) is bundled the
# Launcher/local-build.sh's own remaining prerequisites. # 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. # 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 # into the workspace it's given. So AppRun (written below) copies the bundled workspace snapshot
@@ -15,7 +19,17 @@
# - generated/native-build/Assets/PulsarPacks live only in that writable cache and are never # - 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 # 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 # 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 set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
@@ -107,6 +121,47 @@ nodtool_path=$(dotnet run --project "$workspace/Launcher/WiiCompiled.Setup.Commo
cp "$nodtool_path" "$appdir/usr/bin/nodtool" cp "$nodtool_path" "$appdir/usr/bin/nodtool"
chmod +x "$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..." echo "Staging the bundled workspace snapshot..."
for dir in runtime aurora-main projects; do for dir in runtime aurora-main projects; do
cp -r "$workspace/$dir" "$appdir/workspace/$dir" cp -r "$workspace/$dir" "$appdir/workspace/$dir"
@@ -120,8 +175,21 @@ find "$appdir/workspace/aurora-main/extern" -mindepth 1 -maxdepth 1 -type d -exe
rm -rf "$appdir/workspace/runtime/build" rm -rf "$appdir/workspace/runtime/build"
cp "$workspace/Launcher/local-build.sh" "$appdir/workspace/Launcher/local-build.sh" 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 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 else
date -u +%s > "$appdir/workspace/.bundle-version" date -u +%s > "$appdir/workspace/.bundle-version"
fi fi
@@ -132,6 +200,7 @@ cat > "$appdir/AppRun" <<'APPRUN'
set -euo pipefail set -euo pipefail
HERE="$(dirname "$(readlink -f "$0")")" HERE="$(dirname "$(readlink -f "$0")")"
CACHE="${XDG_DATA_HOME:-$HOME/.local/share}/WiiCompiled/workspace" CACHE="${XDG_DATA_HOME:-$HOME/.local/share}/WiiCompiled/workspace"
mkdir -p "$CACHE"
if [ ! -f "$CACHE/.bundle-version" ] || \ if [ ! -f "$CACHE/.bundle-version" ] || \
[ "$(cat "$HERE/workspace/.bundle-version")" != "$(cat "$CACHE/.bundle-version")" ]; then [ "$(cat "$HERE/workspace/.bundle-version")" != "$(cat "$CACHE/.bundle-version")" ]; then
mkdir -p "$CACHE/Launcher" mkdir -p "$CACHE/Launcher"
@@ -142,9 +211,31 @@ if [ ! -f "$CACHE/.bundle-version" ] || \
cp "$HERE/workspace/Launcher/local-build.sh" "$CACHE/Launcher/local-build.sh" cp "$HERE/workspace/Launcher/local-build.sh" "$CACHE/Launcher/local-build.sh"
cp "$HERE/workspace/.bundle-version" "$CACHE/.bundle-version" cp "$HERE/workspace/.bundle-version" "$CACHE/.bundle-version"
fi 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" \ exec "$HERE/usr/bin/wiicompiled-setup" --workspace "$CACHE" \
--translator-bin "$HERE/usr/bin/translator-cli" \ --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 APPRUN
chmod +x "$appdir/AppRun" chmod +x "$appdir/AppRun"
+139
View File
@@ -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"
+72 -11
View File
@@ -2,11 +2,12 @@
# Linux build automation: translate -> emit build shards -> configure -> compile -> publish. # 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 # 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 # implementation, not a port of NativeBuildFlags.ps1: that file's canonical flags exist only for
# prebuilt-package fingerprinting exist only for the Windows/mingw toolchain (a precompiled # the Windows/mingw toolchain's offline pinned dependencies, which this script does not use.
# aurora/third-party package, offline pinned dependencies) that this script does not build. # Without --native-prebuilt-dir, aurora is built from source, letting its own CMake auto-detect
# Linux always builds aurora from source, letting its own CMake auto-detect Vulkan + vendor # Vulkan + vendor SDL3/Dawn via FetchContent - the same configuration already verified working by
# SDL3/Dawn via FetchContent - the same configuration already verified working by hand. # 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 set -euo pipefail
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -62,6 +63,8 @@ ninja_override=""
dotnet_override="" dotnet_override=""
translator_dll_override="" translator_dll_override=""
translator_bin_override="" translator_bin_override=""
fuse_ld_override=""
native_prebuilt_dir=""
usage() { usage() {
cat <<'EOF' cat <<'EOF'
@@ -77,10 +80,13 @@ Usage: local-build.sh --output-dir DIR [options]
--force-clean-build Discard every translation/build cache first --force-clean-build Discard every translation/build cache first
--parallel N Pin translator threads, translated-shard job pool, and Ninja parallelism to N --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) --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) --cmake PATH / --ninja PATH Build tools (default: on PATH)
--dotnet PATH dotnet executable (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-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) --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
EOF EOF
} }
@@ -97,11 +103,13 @@ while [[ $# -gt 0 ]]; do
--parallel) parallel_override=$2; shift 2 ;; --parallel) parallel_override=$2; shift 2 ;;
--cc) cc_override=$2; shift 2 ;; --cc) cc_override=$2; shift 2 ;;
--cxx) cxx_override=$2; shift 2 ;; --cxx) cxx_override=$2; shift 2 ;;
--fuse-ld) fuse_ld_override=$2; shift 2 ;;
--cmake) cmake_override=$2; shift 2 ;; --cmake) cmake_override=$2; shift 2 ;;
--ninja) ninja_override=$2; shift 2 ;; --ninja) ninja_override=$2; shift 2 ;;
--dotnet) dotnet_override=$2; shift 2 ;; --dotnet) dotnet_override=$2; shift 2 ;;
--translator-dll) translator_dll_override=$2; shift 2 ;; --translator-dll) translator_dll_override=$2; shift 2 ;;
--translator-bin) translator_bin_override=$2; shift 2 ;; --translator-bin) translator_bin_override=$2; shift 2 ;;
--native-prebuilt-dir) native_prebuilt_dir=$2; shift 2 ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
*) fail "unknown argument: $1" ;; *) fail "unknown argument: $1" ;;
esac esac
@@ -157,6 +165,10 @@ require_command "$ninja_bin" ninja
require_command "$cc_bin" cc require_command "$cc_bin" cc
require_command "$cxx_bin" cxx 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 project=$workspace/projects/mkwii/recomp.yml
assets=$workspace/Assets assets=$workspace/Assets
generated=$workspace/generated generated=$workspace/generated
@@ -336,26 +348,75 @@ translator "${shard_args[@]}"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
keep_native_build=0 keep_native_build=0
stale_reason=""
if [[ -f "$build/CMakeCache.txt" ]]; then if [[ -f "$build/CMakeCache.txt" ]]; then
expected_home=$workspace/runtime expected_home=$workspace/runtime
cache_home=$(grep '^CMAKE_HOME_DIRECTORY:INTERNAL=' "$build/CMakeCache.txt" | cut -d= -f2- || true) 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 if [[ -n "$cache_home" && "$(cd "$cache_home" 2>/dev/null && pwd)" == "$expected_home" ]]; then
keep_native_build=1 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
fi fi
if [[ -d "$build" && "$keep_native_build" -eq 0 ]]; then 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" rm -rf "$build"
elif [[ "$keep_native_build" -eq 1 ]]; then elif [[ "$keep_native_build" -eq 1 ]]; then
echo "MKWCBUILD: Reusing the incremental native build directory" echo "MKWCBUILD: Reusing the incremental native build directory"
fi 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
log_step configure-native "Configuring the native toolchain" log_step configure-native "Configuring the native toolchain"
"$cmake_bin" -S "$workspace/runtime" -B "$build" -G Ninja \ "$cmake_bin" "${configure_args[@]}"
-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"
case "$profile" in case "$profile" in
base) targets=(WiiCompiled) ;; base) targets=(WiiCompiled) ;;
+117
View File
@@ -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"
+81
View File
@@ -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'
+92
View File
@@ -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 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 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"
+140
View File
@@ -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" \
'http://nas.play.rwfc.net/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."
+340
View File
@@ -0,0 +1,340 @@
#!/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=df0e1ecf16caf3489a272a5eea4eec9b0d82878f6477fa309504f918a0006384
cmake_release_arch=x86_64
cmake_sha256=927b2368a946c37269c3a66225ab00544e756459cdd0b5d0da438694fb9ff802
ninja_asset=ninja-linux.zip
ninja_sha256=5749cbc4e668273514150a80e387a957f933c6ed3f5f11e03fb30955e2bbead6 ;;
aarch64) llvm_release_arch=ARM64; target_triple=aarch64-unknown-linux-gnu
llvm_release_sha256=805efad2bb91cb4967fa569e0881d10c0f69c04461cf671cccbae19f547acc34
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"
}
# lib/bundled is part of the check so a toolchain prepared before runtime-library bundling existed
# is rebuilt rather than reused.
if [[ -x "$toolchain_dir/bin/clang" && -x "$toolchain_dir/bin/ninja" && -x "$toolchain_dir/bin/cmake" && -d "$toolchain_dir/lib/bundled" ]]; then
echo "prepare-portable-tools.sh: reusing existing toolchain at $toolchain_dir"
exit 0
fi
for tool in patchelf ldd strip; do
command -v "$tool" >/dev/null || { echo "prepare-portable-tools.sh: '$tool' is required (apt-get install patchelf binutils libc-bin)" >&2; exit 1; }
done
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 ---
llvm_archive_name="LLVM-$llvm_version-Linux-$llvm_release_arch.tar.xz"
llvm_archive="$downloads/$llvm_archive_name"
download_verified "$llvm_archive" \
"https://github.com/llvm/llvm-project/releases/download/llvmorg-$llvm_version/$llvm_archive_name" \
"$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-$llvm_version-Linux-$llvm_release_arch"
[[ -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-$cmake_version-linux-$cmake_release_arch"
[[ -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"
# --- runtime libraries the llvm.org binaries expect the host to provide ---
#
# The official LLVM release binaries are dynamically linked against a few host libraries beyond
# glibc: lld needs libxml2, clang/lld need zlib and zstd, and whatever those pull in. A user's
# distro is not guaranteed to have them under the same soname - libxml2 2.14 renamed
# libxml2.so.2 to libxml2.so.16, so on current Arch/Fedora a fresh install died with
# "ld.lld: error while loading shared libraries: libxml2.so.2". The smoke test below never caught
# it because the build machine has every one of them. So: copy the transitive closure of non-core
# libraries next to the binaries and point the binaries at that directory through an RPATH, the
# way AppImages ship any dynamically linked tool. glibc and its companions stay on the host: they
# exist everywhere, and a bundled copy could only ever be older than the host's own.
bundled_lib_dir="$work/lib/bundled"
mkdir -p "$bundled_lib_dir"
is_host_provided_lib() {
case "$1" in
linux-vdso*|ld-linux*|libc.so*|libm.so*|libdl.so*|libpthread.so*|librt.so*|libresolv.so*|libutil.so*|\
libgcc_s.so*|libstdc++.so*) return 0 ;;
*) return 1 ;;
esac
}
# Prints "soname path" for every dynamically resolved dependency of $1 that the host must not be
# relied on for. ldd already reports the transitive closure, so no recursion is needed.
non_host_deps() {
local line soname path
ldd "$1" 2>/dev/null | while read -r line; do
[[ "$line" == *"=>"* ]] || continue
soname=${line%% =>*}; soname=${soname## }
path=${line#*=> }; path=${path%% (*}
is_host_provided_lib "$soname" && continue
echo "$soname $path"
done
}
# A dependency the build machine itself lacks cannot be bundled; say so instead of shipping a
# toolchain that is broken everywhere. (Checked separately because a failure inside the
# process-substituted reader above could not stop the script.)
assert_deps_resolved() {
if ldd "$1" 2>/dev/null | grep -q "not found"; then
echo "prepare-portable-tools.sh: $(basename "$1") has dependencies missing on this machine:" >&2
ldd "$1" | grep "not found" >&2
exit 1
fi
}
# Copies every non-core library $1 needs into lib/bundled, then does the same for each copy, so
# a library's own dependencies (libxml2 -> ICU, ...) are bundled even if ldd's listing for the
# executable did not already spell out the whole closure. The "already bundled" check ends the
# recursion.
bundle_deps_of() {
local file=$1 soname path
assert_deps_resolved "$file"
while read -r soname path; do
[[ -n "$soname" ]] || continue
[[ -e "$bundled_lib_dir/$soname" ]] && continue
cp -L "$path" "$bundled_lib_dir/$soname"
chmod u+w "$bundled_lib_dir/$soname"
# A bundled library must find its own dependencies beside itself: RUNPATH is not
# inherited from the executable that loaded it.
patchelf --set-rpath '$ORIGIN' "$bundled_lib_dir/$soname"
bundle_deps_of "$bundled_lib_dir/$soname"
done < <(non_host_deps "$file")
}
bundle_runtime_libs() {
local file=$1 existing
bundle_deps_of "$file"
# Prepend, keeping whatever RPATH the release build set (typically $ORIGIN/../lib).
existing=$(patchelf --print-rpath "$file" 2>/dev/null || true)
patchelf --set-rpath "\$ORIGIN/../lib/bundled${existing:+:$existing}" "$file"
}
echo "prepare-portable-tools.sh: bundling the host runtime libraries the toolchain binaries depend on..."
for binary in "$work/bin/clang-22" "$work/bin/lld" "$work/bin/llvm-ar" "$work/bin/cmake"; do
bundle_runtime_libs "$binary"
done
echo "prepare-portable-tools.sh: bundled $(ls "$bundled_lib_dir" | wc -l) librar(y/ies): $(ls "$bundled_lib_dir" | tr '\n' ' ')"
# Fail here, on the build machine, if any non-core dependency still resolves to a host path:
# that is exactly the file a user's machine may lack.
for binary in "$work/bin/clang-22" "$work/bin/lld" "$work/bin/llvm-ar" "$work/bin/cmake" "$bundled_lib_dir"/*; do
[[ -f "$binary" ]] || continue
while read -r soname path; do
[[ -n "$soname" ]] || continue
if [[ "$path" != "$bundled_lib_dir/"* ]]; then
echo "prepare-portable-tools.sh: $(basename "$binary") still resolves $soname from the host ($path); bundling failed" >&2
exit 1
fi
done < <(non_host_deps "$binary")
done
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
Host runtime libraries bundled for the binaries above (lib/bundled), copied unmodified apart from
their RPATH from the build machine's distribution packages; each is under its own license
(libxml2: MIT, zlib: zlib, zstd: BSD 3-Clause, ICU: Unicode License, ...):
$(ls "$bundled_lib_dir" | sed 's/^/ /')
EOF
echo "prepare-portable-tools.sh: smoke-testing the toolchain..."
smoke_dir=$(mktemp -d)
trap 'rm -rf "$smoke_dir"' EXIT
cat > "$smoke_dir/t.cpp" <<'EOF'
#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 "$smoke_dir/t.cpp" -o "$smoke_dir/t"
"$smoke_dir/t"
# Also exercised together through CMake+Ninja, exactly how local-build.sh drives them - a plain
# clang++ invocation above would not catch a broken CMAKE_ROOT (Modules/Templates) or a Ninja that
# can't find the compiler.
cat > "$smoke_dir/CMakeLists.txt" <<'EOF'
cmake_minimum_required(VERSION 3.16)
project(smoke CXX)
add_executable(smoke t.cpp)
EOF
"$work/bin/cmake" -S "$smoke_dir" -B "$smoke_dir/build" -G Ninja \
-DCMAKE_MAKE_PROGRAM="$work/bin/ninja" -DCMAKE_CXX_COMPILER="$work/bin/clang++" >/dev/null
"$work/bin/cmake" --build "$smoke_dir/build" >/dev/null
"$smoke_dir/build/smoke"
rm -rf "$smoke_dir"
trap - EXIT
mv "$work" "$toolchain_dir"
echo "prepare-portable-tools.sh: toolchain ready at $toolchain_dir ($(du -sh "$toolchain_dir" | cut -f1))"
+30 -1
View File
@@ -60,19 +60,47 @@ Everything you change is saved to `Config.toml` on the spot and restored next la
**Real controller support.** **Real controller support.**
Controllers are fed to the game as a GameCube controller. 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 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 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. inputs like paddles, touchpads and share buttons show up when the hardware reports them.
The official Wii U / Switch GameCube adapter (WUP-028) works too; as with Dolphin, on Windows the 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). adapter must be switched to the WinUSB driver once (Zadig).
**Real Wii Remotes over Bluetooth.**
Pair a Wii Remote with Windows (Settings > Bluetooth > Add device, press 1+2 or SYNC, leave the
PIN empty) and the game reads it as an actual Wii Remote through KPAD: Wii Remote icons and
prompts, Wii Wheel tilt steering, wheelies and tricks all come from the game's own motion code.
Nunchuk and Classic Controller are real Wii extensions too: the game gets the Nunchuk's stick,
C/Z and accelerometer, and the Classic Controller through `KPADGetUnifiedWpadStatus` with its own
layout and icons, so its buttons do what the game says they do and no mapping is involved. Plug an
extension in or pull it out mid-game and the game switches control scheme like on the console
(the runtime patches SDL's Wii driver, which otherwise loses the remote for good on an extension
change). Only the Wii U Pro Controller, which has no Wii-era equivalent, is fed to the game as a
GameCube pad with Nintendo's layout. If a remote drops out or was switched on after launch, the
runtime keeps rescanning Bluetooth until it comes back (F10 > Controller settings > Wii Remotes). SDL's read of
the remote's factory accelerometer calibration often times out over Bluetooth (`console.log`
then says "Using fallback accelerometer calibration") and it falls back to a nominal zero point,
so the same menu has a one-button calibration (remote flat, buttons up) that removes the small
tilt offset some remotes show.
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 ## Requirements
- Windows 10 or 11, 64-bit - Windows 10 or 11, 64-bit
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher - GPU: GTX 1650 / RX 6400 / Arc A310 or higher
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher - CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
- About 20 GB of free disk space during installation (Final game size ~5 GB) - About 20 GB of free disk space during installation (Final game size ~5 GB)
- macOS 14 (Sonoma) or later on Apple Silicon
- 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, - A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
GCZ, CISO, WBFS, WIA and RVZ are accepted. GCZ, CISO, WBFS, WIA and RVZ are accepted.
@@ -93,6 +121,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 Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it. launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
> [!CAUTION] > [!CAUTION]
> Only take builds from this repository's > Only take builds from this repository's
> [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an > [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an
+2 -5
View File
@@ -26,10 +26,6 @@ Aurora itself vendors:
(shagkur) and Dave Murphy (WinterMute). `aurora-main/lib/card/SRAM.hpp`. (shagkur) and Dave Murphy (WinterMute). `aurora-main/lib/card/SRAM.hpp`.
Source: <https://github.com/devkitPro/libogc> 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 ### Dolphin Emulator data files - GPL-2.0-or-later
Copyright (c) 2003+ Dolphin Emulator Project. Copyright (c) 2003+ Dolphin Emulator Project.
@@ -121,7 +117,8 @@ Source: <https://github.com/higan-emu/libco>. Full license text:
These are pinned in `aurora-main/extern/CMakeLists.txt`, `aurora-main/CMakeLists.txt` and 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 `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 downloads them, and release installers carry the resulting binaries. Their license texts are
included in the installer's `licenses/` folder. 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 | | Component | Version | License | Upstream |
| --- | --- | --- | --- | | --- | --- | --- | --- |
+126
View File
@@ -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 () endif ()
include(FetchContent) 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 FetchContent_Declare(SDL
URL "https://github.com/libsdl-org/SDL/releases/download/release-${AURORA_SDL3_VERSION}/SDL3-${AURORA_SDL3_VERSION}.tar.gz" URL "https://github.com/libsdl-org/SDL/releases/download/release-${AURORA_SDL3_VERSION}/SDL3-${AURORA_SDL3_VERSION}.tar.gz"
DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_EXTRACT_TIMESTAMP TRUE
PATCH_COMMAND "${CMAKE_COMMAND}" -DSDL_SOURCE_DIR=<SOURCE_DIR> -P "${_aurora_sdl3_patches}"
EXCLUDE_FROM_ALL EXCLUDE_FROM_ALL
) )
FetchContent_MakeAvailable(SDL) FetchContent_MakeAvailable(SDL)
+18
View File
@@ -277,6 +277,14 @@ FrameWorkerState g_frameWorker;
bool frame_worker_requested() noexcept { bool frame_worker_requested() noexcept {
#ifdef AURORA_ENABLE_GX #ifdef AURORA_ENABLE_GX
static const bool enabled = [] { 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) #if defined(_WIN32)
// RenderDoc's D3D12 layer is injected before Aurora starts and needs device and command // RenderDoc's D3D12 layer is injected before Aurora starts and needs device and command
// ownership on one thread, so keep frame submission synchronous there. // ownership on one thread, so keep frame submission synchronous there.
@@ -1824,6 +1832,15 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
// Phase 3: hand the encoded group to whoever owns presentation. // Phase 3: hand the encoded group to whoever owns presentation.
void publish_presentations(std::vector<PresentationJob>&& presentationJobs, bool interpolationActive) { 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 // 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. // interpolation off, so every mode shares one surface/resize path. RenderDoc keeps the sync path.
if (frame_worker_requested() || interpolationActive || if (frame_worker_requested() || interpolationActive ||
@@ -1834,6 +1851,7 @@ void publish_presentations(std::vector<PresentationJob>&& presentationJobs, bool
present_presentation_job(job); present_presentation_job(job);
} }
} }
#endif
} }
void record_frame_telemetry() { void record_frame_telemetry() {
+87
View File
@@ -4,6 +4,7 @@
#include <dolphin/pad.h> #include <dolphin/pad.h>
#include <dolphin/si.h> #include <dolphin/si.h>
#include <SDL3/SDL_mouse.h> #include <SDL3/SDL_mouse.h>
#include <SDL3/SDL_joystick.h>
#include <array> #include <array>
#include <atomic> #include <atomic>
@@ -193,6 +194,32 @@ std::array<PADButtonMapping, PAD_BUTTON_COUNT> g_defaultButtonsJoyPair{{
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT}, {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{{ std::array<PADKeyButtonBinding, PAD_BUTTON_COUNT> g_defaultKeys{{
{PAD_KEY_INVALID, PAD_BUTTON_A}, {PAD_KEY_INVALID, PAD_BUTTON_A},
{PAD_KEY_INVALID, PAD_BUTTON_B}, {PAD_KEY_INVALID, PAD_BUTTON_B},
@@ -409,8 +436,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) */ 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)) { switch (SDL_GetGamepadType(controller->m_controller)) {
case SDL_GAMEPAD_TYPE_XBOX360: case SDL_GAMEPAD_TYPE_XBOX360:
controller->m_buttonMapping = g_defaultButtonsXBox360; controller->m_buttonMapping = g_defaultButtonsXBox360;
@@ -745,6 +790,47 @@ u32 PADRead(PADStatus* status) {
if (controller) { if (controller) {
EnsureMappingLoaded(controller); 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 leftTriggerSet = false;
bool rightTriggerSet = false; bool rightTriggerSet = false;
std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet, std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet,
@@ -778,6 +864,7 @@ u32 PADRead(PADStatus* status) {
} }
}); });
// TODO: Add serializable mappings for these (probably not necessary)? // TODO: Add serializable mappings for these (probably not necessary)?
static constexpr std::array<std::pair<SDL_GamepadButton, PADExtButton>, PAD_EXT_BUTTON_COUNT> kExtButtonMappings{{ static constexpr std::array<std::pair<SDL_GamepadButton, PADExtButton>, PAD_EXT_BUTTON_COUNT> kExtButtonMappings{{
{SDL_GAMEPAD_BUTTON_BACK, PAD_BUTTON_BACK}, {SDL_GAMEPAD_BUTTON_BACK, PAD_BUTTON_BACK},
+160 -47
View File
@@ -1,18 +1,28 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(mkw_recompiled) project(mkw_recompiled)
if((NOT (WIN32 AND MINGW)) AND (NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")) if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
message(FATAL_ERROR "WiiCompiled requires Windows (LLVM-MinGW) or native Linux") message(FATAL_ERROR "WiiCompiled requires a 64-bit Clang toolchain")
endif() endif()
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR
NOT CMAKE_SIZEOF_VOID_P EQUAL 8 OR if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$") set(MKW_PLATFORM_WINDOWS TRUE)
message(FATAL_ERROR "WiiCompiled requires 64-bit Clang targeting x86_64 or aarch64") 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() endif()
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release") if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
message(FATAL_ERROR "WiiCompiled only supports Release builds") message(FATAL_ERROR "WiiCompiled only supports Release builds")
endif() endif()
option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON)
# Preprocessor definitions that belong to this project's own code (the runtime, # Preprocessor definitions that belong to this project's own code (the runtime,
# the translated shards and the product glue) and to nothing else. They are # the translated shards and the product glue) and to nothing else. They are
# applied directory-scoped, immediately after the aurora add_subdirectory() call, # 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) target_compile_features(mkw_pugixml PUBLIC cxx_std_17)
set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF) set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF)
# 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 # stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike # directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
# asymmetric resume/yield coroutine libraries, which would need every call site restructured). # asymmetric resume/yield coroutine libraries, which would need every call site restructured).
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is # Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
# separately BSD-style licensed, see third_party/libco/LICENSE) - all of libco's non-Windows # separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers
# CPU-architecture backends are kept, even though libco.c's own preprocessor dispatch # and macOS uses the project's x18-safe AArch64 assembly backend, so this target is Linux-only.
# (__amd64__/__i386__/__arm__/__aarch64__/etc.) only ever selects amd64.c for this project's if(MKW_PLATFORM_LINUX)
# 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)
add_library(mkw_libco STATIC third_party/libco/libco.c) add_library(mkw_libco STATIC third_party/libco/libco.c)
add_library(mkw::libco ALIAS mkw_libco) add_library(mkw::libco ALIAS mkw_libco)
target_include_directories(mkw_libco PUBLIC third_party/libco) target_include_directories(mkw_libco PUBLIC third_party/libco)
@@ -110,7 +117,7 @@ set(MKW_CPPWINRT_INCLUDE_DIR "" CACHE PATH "Optional self-contained C++/WinRT in
# available to explicit developer builds without making a nonfunctional SDK # available to explicit developer builds without making a nonfunctional SDK
# fetch part of every normal build. # fetch part of every normal build.
set(MKW_ENABLE_OPENXR_DEFAULT OFF) set(MKW_ENABLE_OPENXR_DEFAULT OFF)
if(WIN32) if(MKW_PLATFORM_WINDOWS)
set(MKW_ENABLE_OPENXR_DEFAULT ON) set(MKW_ENABLE_OPENXR_DEFAULT ON)
endif() endif()
option(MKW_ENABLE_OPENXR "Build OpenXR VR support" ${MKW_ENABLE_OPENXR_DEFAULT}) option(MKW_ENABLE_OPENXR "Build OpenXR VR support" ${MKW_ENABLE_OPENXR_DEFAULT})
@@ -143,31 +150,35 @@ else()
message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing") message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing")
endif() endif()
set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE) 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_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(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "" FORCE)
set(DAWN_USE_WINDOWS_UI OFF CACHE BOOL "" FORCE) elseif(MKW_PLATFORM_MACOS)
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).
set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE) 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) set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE)
endif() endif()
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE) set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE)
set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE)
# Provide a tiny stub for DXProgrammableCapture when the SDK/PIX headers are # Provide a tiny stub for DXProgrammableCapture when the SDK/PIX headers are
# missing (common on MinGW). Dawn only includes the header; no symbols are # missing (common on MinGW). Dawn only includes the header; no symbols are
# referenced when PIX isn't present. # referenced when PIX isn't present.
set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs") if(MKW_PLATFORM_WINDOWS)
if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h") set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs")
file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR}) if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h")
file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h" file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR})
"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX is absent.\n") 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() endif()
# Deliberately NOT injected project-wide. Only a from-source Dawn build ever # Deliberately NOT injected project-wide. Only a from-source Dawn build ever
# includes DXProgrammableCapture.h, and this tree consumes Dawn as a prebuilt # includes DXProgrammableCapture.h, and this tree consumes Dawn as a prebuilt
@@ -196,7 +207,9 @@ else()
if(TARGET ${t}) if(TARGET ${t})
set_target_properties(${t} PROPERTIES UNITY_BUILD OFF) set_target_properties(${t} PROPERTIES UNITY_BUILD OFF)
target_compile_options(${t} PRIVATE -O3 -ffast-math -w -pipe) 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; # Aurora's own sources include Windows headers and call std::min/max;
# they relied on the old project-wide NOMINMAX that no longer leaks # they relied on the old project-wide NOMINMAX that no longer leaks
# into this subtree, so the define is applied per target here. # into this subtree, so the define is applied per target here.
@@ -237,7 +250,7 @@ endif()
set(MKW_OPENXR_TARGET "") set(MKW_OPENXR_TARGET "")
if(MKW_ENABLE_OPENXR) if(MKW_ENABLE_OPENXR)
if(NOT WIN32 AND NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") if(NOT MKW_PLATFORM_WINDOWS AND NOT MKW_PLATFORM_LINUX)
message(WARNING "OpenXR is not wired for this platform; disabling MKW_ENABLE_OPENXR") message(WARNING "OpenXR is not wired for this platform; disabling MKW_ENABLE_OPENXR")
set(MKW_ENABLE_OPENXR OFF) set(MKW_ENABLE_OPENXR OFF)
else() else()
@@ -286,6 +299,18 @@ endif()
# a registration file is silently never compiled and never errors. The stale-glob # a registration file is silently never compiled and never errors. The stale-glob
# failure mode is worth far more than the milliseconds. # failure mode is worth far more than the milliseconds.
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp") 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_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") 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 # The host ISA guard is the one translation unit that must not receive the
@@ -293,25 +318,113 @@ 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. # 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") 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} 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)
# 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 # The translator emits the complete, content-addressed source graph. Consuming
# this one manifest keeps configure independent of the 28k generated function # this one manifest keeps configure independent of the 28k generated function
# files and of optional Retro Rewind artifacts such as code.map. # files and of optional Retro Rewind artifacts such as code.map.
set(MKW_TRANSLATED_SHARD_MANIFEST if(MKW_BUILD_PRODUCTS)
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake" set(MKW_TRANSLATED_SHARD_MANIFEST
CACHE FILEPATH "Translator-owned aggregate shard manifest") "${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}") CACHE FILEPATH "Translator-owned aggregate shard manifest")
message(FATAL_ERROR # The prebuilt export only needs the aurora/third-party closure configured above, so a
"Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. " # packaging machine without a translation stops here instead of failing.
"Run Translator.Cli emit-build-shards first; see translator/README.md.") if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
endif() message(STATUS "No translator shard manifest; configuring the native prebuilt export only")
include("${MKW_TRANSLATED_SHARD_MANIFEST}") return()
set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS}) endif()
message(STATUS if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
"Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; " message(FATAL_ERROR
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; " "Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. "
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions") "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}") set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake") 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()
+10
View File
@@ -51,6 +51,16 @@ foreach(_dir IN LISTS _mkw_np_includes)
endif() endif()
endforeach() 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 # Dawn is a prebuilt package on both sides; resolve the same install tree the
# package was built against so its imported target (and therefore # package was built against so its imported target (and therefore
# webgpu_dawn.dll / dxcompiler.dll / dxil.dll) is available exactly as a # webgpu_dawn.dll / dxcompiler.dll / dxil.dll) is available exactly as a
+34 -26
View File
@@ -25,6 +25,11 @@ if(EXISTS "${DATA_INIT_BLOB_ASM}")
endif() endif()
list(REMOVE_DUPLICATES SOURCES) list(REMOVE_DUPLICATES SOURCES)
if(MKW_PLATFORM_MACOS)
find_library(MKW_IOKIT_FRAMEWORK IOKit REQUIRED)
find_library(MKW_COREFOUNDATION_FRAMEWORK CoreFoundation REQUIRED)
endif()
function(mkw_apply_common_compile_options target) function(mkw_apply_common_compile_options target)
target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe) target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe)
endfunction() endfunction()
@@ -76,13 +81,13 @@ target_compile_definitions(mkw_runtime_common PRIVATE
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION) _DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
target_link_libraries(mkw_runtime_common PRIVATE target_link_libraries(mkw_runtime_common PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx) aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
target_link_libraries(mkw_runtime_common PRIVATE mkw::pugixml mkw::toml11 mkw::cryptopp) target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
if(MKW_ENABLE_OPENXR) if(MKW_ENABLE_OPENXR)
target_link_libraries(mkw_runtime_common PRIVATE ${MKW_OPENXR_TARGET}) target_link_libraries(mkw_runtime_common PRIVATE ${MKW_OPENXR_TARGET})
endif() endif()
if(WIN32) if(MKW_PLATFORM_WINDOWS)
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp) target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
else() elseif(MKW_PLATFORM_LINUX)
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS # ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
# media monitoring). Empty string on glibc >= 2.34 where dl* is in libc. # media monitoring). Empty string on glibc >= 2.34 where dl* is in libc.
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS}) target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS})
@@ -130,16 +135,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") target_precompile_headers(mkw_runtime_common PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
mkw_apply_common_compile_options(mkw_runtime_common) mkw_apply_common_compile_options(mkw_runtime_common)
# Host ISA guard. Everything in MKW_ALL_BUILD_TARGETS below is compiled with # Host ISA guard. Windows and Linux x86_64 product targets use x86-64-v3, so
# -march=x86-64-v3; this object library deliberately is not, which # this object deliberately keeps the plain baseline ISA and checks the CPU
# is the whole point of keeping it out of mkw_runtime_common. It runs a CPUID # before any AVX2/FMA code can execute. AArch64 has no equivalent optional ISA
# check from a C initializer so an unsupported machine gets a readable error # floor to probe: NEON/FMA are architectural requirements.
# instead of an illegal-instruction crash. Excluded from the unity build and the if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
# precompiled header because both are produced with the owning target's flags. add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}")
add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}") target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17)
target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17) set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF)
set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF) target_compile_options(mkw_cpu_baseline PRIVATE -w)
target_compile_options(mkw_cpu_baseline PRIVATE -w) endif()
if(NOT MKW_BASE_COMMON_SHARDS) if(NOT MKW_BASE_COMMON_SHARDS)
message(FATAL_ERROR "Translator build graph contains no shared base shards") message(FATAL_ERROR "Translator build graph contains no shared base shards")
@@ -179,7 +184,9 @@ function(mkw_configure_product target)
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_runtime_common>) target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_runtime_common>)
# Startup CPU check. Must stay a separate object library so it keeps the # 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. # 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 target_include_directories(${target} PRIVATE
"${MKW_RUNTIME_SOURCE_DIR}/include" "${MKW_RUNTIME_SOURCE_DIR}/include"
"${MKW_RUNTIME_SOURCE_DIR}/src" "${MKW_RUNTIME_SOURCE_DIR}/src"
@@ -195,7 +202,7 @@ function(mkw_configure_product target)
# include the same fat translated headers; bound them by the same pool. # include the same fat translated headers; bound them by the same pool.
mkw_bound_translated_compiles(${target}) mkw_bound_translated_compiles(${target})
target_link_libraries(${target} PRIVATE 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)
target_link_libraries(${target} PRIVATE target_link_libraries(${target} PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx) aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
@@ -204,6 +211,10 @@ function(mkw_configure_product target)
# loader dependency must also be present on each final product link. # loader dependency must also be present on each final product link.
target_link_libraries(${target} PRIVATE ${MKW_OPENXR_TARGET}) target_link_libraries(${target} PRIVATE ${MKW_OPENXR_TARGET})
endif() endif()
if(MKW_PLATFORM_MACOS)
target_link_libraries(${target} PRIVATE
"${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}")
endif()
if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake") if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake") include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
aurora_copy_runtime_dlls(${target}) aurora_copy_runtime_dlls(${target})
@@ -218,23 +229,23 @@ function(mkw_configure_product target)
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>) $<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
endif() endif()
if(WIN32) if(MKW_PLATFORM_WINDOWS)
target_link_libraries(${target} PRIVATE target_link_libraries(${target} PRIVATE
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp) dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE) set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
else() elseif(MKW_PLATFORM_LINUX)
# mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o # mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o
# files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own # files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own
# target_link_libraries (object libraries don't carry usage requirements to a consumer # target_link_libraries (object libraries don't carry usage requirements to a consumer
# that isn't itself linked against as a target). fiber_manager.cpp's co_* calls live in # 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 # those objects, so the actual executable link needs mkw::libco directly, same as it
# needs it independently of that first `if(WIN32)` branch above. ${CMAKE_DL_LIBS} is # needs it independently of the platform branch above. ${CMAKE_DL_LIBS} is
# here for the same reason: music_attenuation.cpp's dlopen(libdbus-1) lives in those # 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). # objects (empty string on glibc >= 2.34, where dl* is in libc).
target_link_libraries(${target} PRIVATE mkw::libco ${CMAKE_DL_LIBS}) target_link_libraries(${target} PRIVATE mkw::libco ${CMAKE_DL_LIBS})
endif() endif()
if(WIN32) if(MKW_PLATFORM_WINDOWS)
foreach(runtime_dll libc++.dll libunwind.dll) foreach(runtime_dll libc++.dll libunwind.dll)
execute_process( execute_process(
COMMAND "${CMAKE_CXX_COMPILER}" "--print-file-name=${runtime_dll}" COMMAND "${CMAKE_CXX_COMPILER}" "--print-file-name=${runtime_dll}"
@@ -310,13 +321,10 @@ else()
message(STATUS "RetroRewind target disabled (run translate-mod and emit-build-shards)") message(STATUS "RetroRewind target disabled (run translate-mod and emit-build-shards)")
endif() endif()
# x86-64-v3 (SSE3/SSSE3/SSE4.1/FMA/AVX2/BMI2) is the baseline runtime/src/host_cpu_baseline.cpp # Windows and Linux x86_64 share the x86-64-v3 floor that the CPU baseline
# guards against - a fixed, portable floor since an x86_64 build may run on a different machine # object above checks. AArch64 builds are compiled locally for the host that
# than the one that built it. AArch64 has no such redistribution path here: every build this # will run them, so both Linux and Apple Silicon use the compiler's native CPU
# project produces runs only on the machine that built it (local-build.sh, and the AppImage which # tuning rather than leaving target-specific performance on the table.
# wraps it, always build from source on the target), so -mcpu=native is safe and strictly better -
# real per-core tuning (scheduling, whatever NEON/atomic extensions that exact CPU actually has)
# instead of the generic armv8-a baseline Clang would otherwise assume.
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$") if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3) set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$") elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
+30
View File
@@ -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
+5 -10
View File
@@ -6,6 +6,7 @@
#include <mutex> #include <mutex>
#include <thread> #include <thread>
#include <unordered_map> #include <unordered_map>
#include <vector>
#if defined(_WIN32) #if defined(_WIN32)
#ifndef NOMINMAX #ifndef NOMINMAX
@@ -22,7 +23,7 @@
// Forward declarations // Forward declarations
struct CpuContext; 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 // 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 // threads only switch at explicit yield points (OSSleepThread, OSYieldThread, ...), matching
// Wii cooperative semantics exactly. // Wii cooperative semantics exactly.
@@ -39,7 +40,7 @@ enum class ThreadState : uint32_t {
// Information about a guest fiber // Information about a guest fiber
struct GuestFiber { struct GuestFiber {
void* fiber = nullptr; // Windows fiber handle void* fiber = nullptr; // Host context handle
uint32_t entryPoint = 0; // Thread entry function uint32_t entryPoint = 0; // Thread entry function
uint32_t entryArg = 0; // Argument to entry function uint32_t entryArg = 0; // Argument to entry function
CpuContext cpuContext{}; // Saved CPU context for this fiber CpuContext cpuContext{}; // Saved CPU context for this fiber
@@ -102,10 +103,6 @@ private:
static void CALLBACK FiberProc(void* param); static void CALLBACK FiberProc(void* param);
#else #else
static void FiberProc(void* param); 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 #endif
// Switch from whichever fiber is currently active straight to the scheduler fiber, without // Switch from whichever fiber is currently active straight to the scheduler fiber, without
// the SwitchToThread bookkeeping (CPU context save/restore, s_currentGuestThread). Used for // the SwitchToThread bookkeeping (CPU context save/restore, s_currentGuestThread). Used for
@@ -117,9 +114,8 @@ private:
static std::mutex s_mutex; static std::mutex s_mutex;
static std::unordered_map<uint32_t, GuestFiber> s_fibers; static std::unordered_map<uint32_t, GuestFiber> s_fibers;
static std::vector<void*> s_fibersPendingDelete; static std::vector<void*> s_fibersPendingDelete;
// The scheduler's own "fiber": a Windows HFIBER, or (non-Windows) libco's cothread_t for // The scheduler's own host context. Its opaque handle is supplied by the
// whichever native call stack first called GuestFiberManager::Initialize() - both are // active HostContext backend, so one field serves every supported host.
// plain void* handles, so one field serves both platforms.
static void* s_schedulerFiber; static void* s_schedulerFiber;
static uint32_t s_currentGuestThread; static uint32_t s_currentGuestThread;
static bool s_initialized; static bool s_initialized;
@@ -135,4 +131,3 @@ private:
extern std::atomic<uint32_t> g_viRetracePendingCount; extern std::atomic<uint32_t> g_viRetracePendingCount;
} // namespace Fiber } // namespace Fiber
+26
View File
@@ -14,10 +14,17 @@ namespace GuestFlat {
// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load // Fixed base so the emitted access is `[reg + imm64-in-register]` with no load
// of a global. // of a global.
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull; inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
inline constexpr size_t kGuestPageSize = 0x1000;
#if defined(__x86_64__) #if defined(__x86_64__)
// 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap // 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap
// placement. // placement.
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'1000'0000'0000ull; 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__) #elif defined(__aarch64__)
// 16 TiB (this arch's x86_64 sibling value) is unreachable on any 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 // kernel configured for 39-bit virtual addresses (512 GiB ceiling) - common on
@@ -58,6 +65,25 @@ struct FaultCounters {
// True once the reservation exists and translated code may use the flat path. // True once the reservation exists and translated code may use the flat path.
bool IsActive(); 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 // 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 // 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 // reservation, the section objects or a view cannot be created - a silent
+24
View File
@@ -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
+4
View File
@@ -264,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. // 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) MKW_PPC_FORCE_INLINE const uint8_t* PpcTryGetPsqReadableHostInline(uint32_t addr)
{ {
if (GuestFlat::RequiresCheckedAccess()) [[unlikely]]
return nullptr;
return MKW_FLAT_GUEST_BASE + addr; return MKW_FLAT_GUEST_BASE + addr;
} }
@@ -274,6 +276,8 @@ MKW_PPC_FORCE_INLINE const uint8_t* PpcTryGetPsqReadableHostInline(uint32_t addr
// executable, and unmapped pages still trap. // executable, and unmapped pages still trap.
MKW_PPC_FORCE_INLINE uint8_t* PpcTryGetPsqWritableHostInline(uint32_t addr) MKW_PPC_FORCE_INLINE uint8_t* PpcTryGetPsqWritableHostInline(uint32_t addr)
{ {
if (GuestFlat::RequiresCheckedAccess()) [[unlikely]]
return nullptr;
if (addr > UINT32_MAX - 7u) [[unlikely]] if (addr > UINT32_MAX - 7u) [[unlikely]]
return nullptr; return nullptr;
if (MemoryInline::FlatWriteNeedsPolicy(addr) || if (MemoryInline::FlatWriteNeedsPolicy(addr) ||
+34 -5
View File
@@ -231,6 +231,13 @@ MKW_MEMORY_FORCE_INLINE uint8_t* ResolveRangeHost(uint32_t base, int32_t minOffs
(void)needsRead; (void)needsRead;
const uint32_t guestStart = base + static_cast<uint32_t>(minOffset); const uint32_t guestStart = base + static_cast<uint32_t>(minOffset);
if (length == 0 || length > kPageSize || guestStart > UINT32_MAX - (length - 1)) return nullptr; 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 && if (needsWrite &&
(FlatWriteNeedsPolicy(guestStart) || FlatWriteNeedsPolicy(guestStart + (length - 1)))) (FlatWriteNeedsPolicy(guestStart) || FlatWriteNeedsPolicy(guestStart + (length - 1))))
[[unlikely]] return nullptr; [[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 // 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 // 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. // 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> template <typename T>
MKW_MEMORY_FORCE_INLINE T FlatLoad(uint32_t address) { 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)); 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 uint8_t FlatRead8(uint32_t address) {
MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) { return FlatLoad<uint16_t>(address); } if (GuestFlat::RequiresCheckedAccess()) return Memory::Read8(address);
MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) { return FlatLoad<uint32_t>(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) { 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; float value = 0.0f;
std::memcpy(&value, &bits, sizeof(value)); std::memcpy(&value, &bits, sizeof(value));
return value; return value;
} }
MKW_MEMORY_FORCE_INLINE double FlatReadFloat64(uint32_t address) { 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; double value = 0.0;
std::memcpy(&value, &bits, sizeof(value)); std::memcpy(&value, &bits, sizeof(value));
return value; return value;
} }
MKW_MEMORY_FORCE_INLINE void FlatWrite8(uint32_t address, uint8_t 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; } if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write8Slow(address, value); return; }
FlatStore<uint8_t>(address, value); FlatStore<uint8_t>(address, value);
} }
MKW_MEMORY_FORCE_INLINE void FlatWrite16(uint32_t address, uint16_t 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; } if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write16Slow(address, value); return; }
FlatStore<uint16_t>(address, value); FlatStore<uint16_t>(address, value);
} }
MKW_MEMORY_FORCE_INLINE void FlatWrite32(uint32_t address, uint32_t 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; } if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write32Slow(address, value); return; }
FlatStore<uint32_t>(address, value); FlatStore<uint32_t>(address, value);
} }
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat32(uint32_t address, double 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); const uint32_t bits = ConvertPpcDoubleToSingleBits(value);
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat32Slow(address, value); return; } if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat32Slow(address, value); return; }
FlatStore<uint32_t>(address, bits); FlatStore<uint32_t>(address, bits);
} }
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat64(uint32_t address, double value) { MKW_MEMORY_FORCE_INLINE void FlatWriteFloat64(uint32_t address, double value) {
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat64(address, value); return; }
uint64_t bits = 0; uint64_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits)); std::memcpy(&bits, &value, sizeof(bits));
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat64Slow(address, value); return; } if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat64Slow(address, value); return; }
+26
View File
@@ -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
+124
View File
@@ -8,6 +8,7 @@
#include <cstdint> #include <cstdint>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iomanip>
#include <iostream> #include <iostream>
#include <limits> #include <limits>
#include <optional> #include <optional>
@@ -17,6 +18,7 @@
#include <utility> #include <utility>
#include <vector> #include <vector>
#include <toml.hpp> #include <toml.hpp>
#include "platform/host_platform.h"
#ifdef _WIN32 #ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN #ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
@@ -59,7 +61,31 @@ struct RuntimeUserConfig {
std::optional<bool> audioMuted; std::optional<bool> audioMuted;
std::optional<bool> audioMixWorker; std::optional<bool> audioMixWorker;
std::optional<bool> attenuateMusicWhenMediaPlays; 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> 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> nandRoot;
std::optional<std::string> dvdRoot; std::optional<std::string> dvdRoot;
// The one canonical Retro Rewind installation, owned and updated by the frontend. Setup records // The one canonical Retro Rewind installation, owned and updated by the frontend. Setup records
@@ -143,7 +169,14 @@ inline bool IsSupportedResolutionMultiplier(float value) {
// Must stay in step with the backend table in main.cpp, which is what actually // Must stay in step with the backend table in main.cpp, which is what actually
// maps these to AuroraBackend. // maps these to AuroraBackend.
inline bool IsSupportedGraphicsApi(std::string_view value) { 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"}; static constexpr std::array<std::string_view, 3> values{"auto", "d3d12", "vulkan"};
#endif
return std::find(values.begin(), values.end(), value) != values.end(); return std::find(values.begin(), values.end(), value) != values.end();
} }
@@ -174,6 +207,8 @@ inline std::optional<std::filesystem::path> ExecutableDirectory() {
} }
buffer.resize(buffer.size() * 2); buffer.resize(buffer.size() * 2);
} }
#elif defined(__APPLE__)
return RuntimePlatform::ExecutableDirectory();
#else #else
// /proc/self/exe is a Linux-specific magic symlink to the running executable; readlink() // /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 // does not NUL-terminate and silently truncates if the buffer is too small, so this grows
@@ -231,6 +266,8 @@ inline std::filesystem::path ApplicationDataDirectory() {
CoTaskMemFree(rawPath); CoTaskMemFree(rawPath);
return directory; return directory;
} }
#elif defined(__APPLE__)
return RuntimePlatform::ApplicationDataDirectory(kApplicationDirectoryName);
#else #else
// XDG Base Directory spec equivalent of FOLDERID_LocalAppData: $XDG_DATA_HOME if set and // XDG Base Directory spec equivalent of FOLDERID_LocalAppData: $XDG_DATA_HOME if set and
// non-empty, otherwise its default of $HOME/.local/share. // non-empty, otherwise its default of $HOME/.local/share.
@@ -308,6 +345,12 @@ inline void EnsureConfigFile() {
"mix_worker = true\n\n" "mix_worker = true\n\n"
"[network]\n" "[network]\n"
"enabled = true\n\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" "[paths]\n"
"# dvd_root = \"D:\\\\MarioKartWii\\\\DATA\"\n" "# dvd_root = \"D:\\\\MarioKartWii\\\\DATA\"\n"
"# nand_root = \"D:\\\\WiiNand\"\n" "# nand_root = \"D:\\\\WiiNand\"\n"
@@ -374,6 +417,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) { inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
RuntimeUserConfig config; RuntimeUserConfig config;
@@ -456,7 +500,15 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.audioMixWorker = FindConfigValue<bool>(document, "audio", "mix_worker"); config.audioMixWorker = FindConfigValue<bool>(document, "audio", "mix_worker");
config.attenuateMusicWhenMediaPlays = config.attenuateMusicWhenMediaPlays =
FindConfigValue<bool>(document, "audio", "attenuate_music_when_media_plays"); 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.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.nandRoot = FindConfigValue<std::string>(document, "paths", "nand_root");
config.dvdRoot = FindConfigValue<std::string>(document, "paths", "dvd_root"); config.dvdRoot = FindConfigValue<std::string>(document, "paths", "dvd_root");
@@ -788,14 +840,75 @@ inline bool AudioMixWorkerEnabled(bool fallback = true) {
return Get().audioMixWorker.value_or(fallback); return Get().audioMixWorker.value_or(fallback);
} }
// Whether background music should duck automatically for other media playback.
inline bool AttenuateMusicWhenMediaPlays(bool fallback = false) { inline bool AttenuateMusicWhenMediaPlays(bool fallback = false) {
return Get().attenuateMusicWhenMediaPlays.value_or(fallback); 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 = true) {
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) { inline uint32_t FrameInterpolationFps(uint32_t fallback = 0) {
return Get().frameInterpolationFps.value_or(fallback); return Get().frameInterpolationFps.value_or(fallback);
} }
// Whether to skip draws whose graphics pipeline has not finished compiling yet.
inline bool SkipUnreadyPipelines(bool fallback = true) { inline bool SkipUnreadyPipelines(bool fallback = true) {
return Get().skipUnreadyPipelines.value_or(fallback); return Get().skipUnreadyPipelines.value_or(fallback);
} }
@@ -897,6 +1010,14 @@ inline std::string RetroRewindRoot(std::string fallback = "") {
return Get().retroRewindRoot.value_or(std::move(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() { inline const std::vector<std::string>& OverlayRoots() {
return Get().overlayRoots; return Get().overlayRoots;
} }
@@ -959,6 +1080,9 @@ inline void LogLoadedConfig() {
if (config.networkEnabled) { if (config.networkEnabled) {
std::cout << " network_enabled=" << (*config.networkEnabled ? "true" : "false"); std::cout << " network_enabled=" << (*config.networkEnabled ? "true" : "false");
} }
if (config.discordPresenceEnabled) {
std::cout << " discord_enabled=" << (*config.discordPresenceEnabled ? "true" : "false");
}
if (config.nandRoot) { if (config.nandRoot) {
std::cout << " nand_root=" << *config.nandRoot; std::cout << " nand_root=" << *config.nandRoot;
} }
+1
View File
@@ -36,6 +36,7 @@ extern thread_local uint32_t g_sehLastAccessType;
void WriteFatalLog(std::string_view reason); void WriteFatalLog(std::string_view reason);
void SetRuntimeExitCode(int code); void SetRuntimeExitCode(int code);
void MarkFatalErrorReported();
// Centralized crash reporting (defined in main.cpp). Every fatal path funnels // Centralized crash reporting (defined in main.cpp). Every fatal path funnels
// through these so the per-run log folder always receives the same artifact // through these so the per-run log folder always receives the same artifact
+110
View File
@@ -0,0 +1,110 @@
#pragma once
#include <cstdint>
struct PADStatus;
// Real Wii Remotes paired over Bluetooth.
//
// SDL 3 ships a HIDAPI driver for them (Wii Remote alone, with Nunchuk, with a
// Classic Controller, and the Wii U Pro Controller) that exposes each as a
// regular SDL gamepad; it is off by default on SDL's side and ConfigureSdlHints
// turns it on. A remote, alone or with a Nunchuk or Classic Controller, is
// handed to the game as a real Wii Remote with that extension: the KPAD/WPAD
// HLE builds a KPADStatus (and the WPADCLStatus behind KPADGetUnifiedWpadStatus)
// from it every frame (see ReadKpadSample), so the game's own code does wheelies,
// tricks, Wii Wheel steering and the Classic Controller layout, and plugging an
// extension in or out mid-game switches control scheme like on the console. Only
// the Wii U Pro Controller, which has no Wii-era equivalent, goes through
// aurora's PAD layer as a GameCube pad.
namespace WiiRemoteInput {
enum class Kind : uint8_t {
NotWii,
Remote, // Wii Remote with no extension
RemoteWithNunchuk,
RemoteWithClassic,
WiiUPro,
};
// Must run before SDL's joystick subsystem is initialized (aurora does that
// inside aurora_initialize); SDL only consults the hint on its first device scan.
void ConfigureSdlHints(bool enabled);
// Classifies a gamepad by the name SDL's Wii driver reports for it.
Kind KindForName(const char* gamepadName);
// Classification of the SDL gamepad currently assigned to a game port.
Kind KindForPort(uint32_t port);
const char* KindLabel(Kind kind);
// One frame of a Wii Remote in the units KPAD uses.
struct KpadSample {
uint32_t hold = 0; // WPAD button bits (WPAD_BUTTON_*), Nunchuk C/Z included
float acc[3] = {}; // remote accelerometer in g, KPAD frame (rest: y = -1)
bool hasNunchuk = false;
float stick[2] = {}; // Nunchuk stick, -1..1, +y up
float nunchukAcc[3] = {};
bool hasClassic = false;
uint32_t clHold = 0; // WPAD_CL_BUTTON_* bits
float clLStick[2] = {}; // Classic sticks, -1..1, +y up
float clRStick[2] = {};
int16_t clLStickRaw[2] = {}; // as WPADCLStatus reports them: -512..511, centre 0, +y up
int16_t clRStickRaw[2] = {};
uint8_t clTriggerL = 0; // 0..255; SDL only exposes the digital click
uint8_t clTriggerR = 0;
};
// What the game should see on `chan`: the controller SDL has there right now,
// or, for a few seconds after a remote vanished, the kind it had. SDL's driver
// destroys and re-creates the joystick when an extension is plugged in or out,
// and the console never disconnects for that, so the gap is papered over with
// neutral input instead of a "communications interrupted" prompt.
Kind EffectiveKind(uint32_t chan);
// True when the game reads `chan` through KPAD: a Wii Remote alone, with a
// Nunchuk or with a Classic Controller (live or within the swap grace period).
bool IsRemoteChannel(uint32_t chan);
// Reads the current state of the remote on `chan`; false when IsRemoteChannel
// is false. During the swap grace period the sample is neutral.
bool ReadKpadSample(uint32_t chan, KpadSample& sample);
// Remote accelerometer for the overlay readout: the SDL sample in g after the
// zero-point correction (x right across the face, y out of the button face, z
// towards the user) and the KPAD vector built from it. False when the port has
// no remote or SDL has not delivered a sample yet.
bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]);
// Accelerometer zero-point calibration. SDL's Wii driver tries to read the
// remote's factory calibration block, but over Bluetooth that read often times
// out ("Using fallback accelerometer calibration" in console.log) and it falls
// back to a nominal 0x200 zero point; a real remote then carries a per-axis
// bias of up to ~0.2 g, which held sideways as a wheel is a permanent steering
// offset.
// StartAccelCalibration expects the remote lying still with the buttons up;
// Poll() then collects samples for about a second and stores the difference to
// the ideal (0, 1, 0) g in Config.toml.
void StartAccelCalibration(uint32_t chan);
// Forgets the stored correction.
void ClearAccelCalibration();
bool IsAccelCalibrating();
// 0..1 while a calibration is collecting samples.
float AccelCalibrationProgress();
// Outcome of the last calibration attempt for the overlay, or nullptr.
const char* AccelCalibrationMessage();
// Reports "no controller" on the GameCube side for every port served through
// KPAD, so the game never sees the same remote twice. Runs after aurora's PADRead.
void HideRemotesFromPad(PADStatus* statuses, uint32_t count);
// Dolphin-style continuous scanning. SDL's HIDAPI Wii driver drops a remote on
// a failed Bluetooth read or an extension change and never re-adds it on its
// own. Poll() runs once per PADRead and, while no Wii controller is present,
// periodically forces SDL to re-enumerate HIDAPI by toggling the driver hint.
void Poll();
// Forces one re-enumeration right now (settings overlay "Rescan now").
void RescanNow();
// True while Poll() is actively rescanning (no Wii controller connected).
bool IsScanning();
// Rescans issued since a Wii controller was last seen.
uint32_t ScanCount();
} // namespace WiiRemoteInput
+466
View File
@@ -0,0 +1,466 @@
#include "discord_presence.h"
#include "runtime_log.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <optional>
#include <sstream>
#include <string_view>
#include <utility>
#include <vector>
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#else
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#endif
namespace DiscordPresence {
namespace {
constexpr uint32_t kHandshakeOpcode = 0;
constexpr uint32_t kFrameOpcode = 1;
constexpr size_t kMaxClientIdLength = 32;
constexpr auto kConnectionRetryCooldown = std::chrono::seconds(1);
bool IsClientId(std::string_view value) {
return !value.empty() && value.size() <= kMaxClientIdLength &&
std::all_of(value.begin(), value.end(), [](unsigned char ch) { return std::isdigit(ch) != 0; });
}
std::string EscapeJson(std::string_view value) {
std::string escaped;
escaped.reserve(value.size());
for (const unsigned char ch : value) {
switch (ch) {
case '\\': escaped += "\\\\"; break;
case '\"': escaped += "\\\""; break;
case '\b': escaped += "\\b"; break;
case '\f': escaped += "\\f"; break;
case '\n': escaped += "\\n"; break;
case '\r': escaped += "\\r"; break;
case '\t': escaped += "\\t"; break;
default:
if (ch < 0x20) {
static constexpr char kHex[] = "0123456789abcdef";
escaped += "\\u00";
escaped += kHex[ch >> 4];
escaped += kHex[ch & 0x0f];
} else {
escaped += static_cast<char>(ch);
}
}
}
return escaped;
}
void AppendJsonString(std::ostringstream& output, bool& hasValue, std::string_view key, std::string_view value) {
if (value.empty()) {
return;
}
if (hasValue) {
output << ',';
}
output << '\"' << key << "\":\"" << EscapeJson(value) << '\"';
hasValue = true;
}
std::string BuildActivityPayload(const Activity& activity) {
std::ostringstream json;
json << "{\"cmd\":\"SET_ACTIVITY\",\"nonce\":\"wiicompiled\",\"args\":{\"pid\":";
#if defined(_WIN32)
json << static_cast<unsigned long>(::GetCurrentProcessId());
#else
json << static_cast<long>(::getpid());
#endif
json << ",\"activity\":{";
bool hasActivityField = false;
AppendJsonString(json, hasActivityField, "details", activity.details);
AppendJsonString(json, hasActivityField, "state", activity.state);
const auto appendSection = [&](std::string_view name, const auto& append) {
if (hasActivityField) {
json << ',';
}
json << '\"' << name << "\":{";
append();
json << '}';
hasActivityField = true;
};
const bool hasAssets = !activity.largeImageKey.empty() || !activity.largeImageText.empty() ||
!activity.smallImageKey.empty() || !activity.smallImageText.empty();
if (hasAssets) {
appendSection("assets", [&] {
bool hasAsset = false;
AppendJsonString(json, hasAsset, "large_image", activity.largeImageKey);
AppendJsonString(json, hasAsset, "large_text", activity.largeImageText);
AppendJsonString(json, hasAsset, "small_image", activity.smallImageKey);
AppendJsonString(json, hasAsset, "small_text", activity.smallImageText);
});
}
if (activity.startTimestamp > 0 || activity.endTimestamp > 0) {
appendSection("timestamps", [&] {
if (activity.startTimestamp > 0) {
json << "\"start\":" << activity.startTimestamp;
}
if (activity.endTimestamp > 0) {
if (activity.startTimestamp > 0) {
json << ',';
}
json << "\"end\":" << activity.endTimestamp;
}
});
}
if (activity.partySize > 0 && activity.partyMax > 0) {
appendSection("party", [&] {
json << "\"size\":[" << activity.partySize << ',' << activity.partyMax << ']';
});
}
if (hasActivityField) {
json << ',';
}
json << "\"instance\":false}}}";
return json.str();
}
class Client {
public:
void Initialize(const std::string& clientId, const std::string& title) {
std::lock_guard<std::mutex> lock(mutex_);
basicClientId_ = IsClientId(clientId) ? clientId : std::string{};
basicActivity_ = {};
basicActivity_.details = title;
basicActivity_.startTimestamp = UnixSeconds();
customClient_ = false;
activity_ = basicActivity_;
ReconnectAndSendLocked();
}
void SetClient(const std::string& clientId) {
std::lock_guard<std::mutex> lock(mutex_);
if (!IsClientId(clientId)) {
RT_LOG(RT_TAG_RUNTIME) << "Ignoring invalid Discord client ID from /dev/dolphin" << std::endl;
return;
}
if (clientId_ == clientId && connected_) {
return;
}
customClient_ = true;
clientId_ = clientId;
CloseLocked();
ReconnectAndSendLocked();
}
void SetActivity(Activity activity) {
std::lock_guard<std::mutex> lock(mutex_);
if (!customClient_) {
return;
}
activity_ = std::move(activity);
SendActivityLocked();
}
void Reset() {
std::lock_guard<std::mutex> lock(mutex_);
customClient_ = false;
clientId_.clear();
activity_ = basicActivity_;
CloseLocked();
ReconnectAndSendLocked();
}
void Shutdown() {
std::lock_guard<std::mutex> lock(mutex_);
CloseLocked();
}
private:
static int64_t UnixSeconds() {
return std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
}
std::string ActiveClientIdLocked() const {
return customClient_ ? clientId_ : basicClientId_;
}
bool WriteFrameLocked(uint32_t opcode, std::string_view payload) {
std::array<uint8_t, 8> header{};
const uint32_t size = static_cast<uint32_t>(payload.size());
for (size_t i = 0; i < 4; ++i) {
header[i] = static_cast<uint8_t>(opcode >> (i * 8));
header[4 + i] = static_cast<uint8_t>(size >> (i * 8));
}
return WriteAllLocked(header.data(), header.size()) &&
WriteAllLocked(reinterpret_cast<const uint8_t*>(payload.data()), payload.size());
}
void ReconnectAndSendLocked() {
const std::string clientId = ActiveClientIdLocked();
if (clientId.empty() || !ConnectLocked()) {
return;
}
const std::string handshake = "{\"v\":1,\"client_id\":\"" + clientId + "\"}";
if (!WriteFrameLocked(kHandshakeOpcode, handshake)) {
RecordFailedConnectionLocked();
CloseLocked();
return;
}
if (!ReadReadyLocked()) {
RecordFailedConnectionLocked();
CloseLocked();
return;
}
SendActivityLocked();
}
void SendActivityLocked() {
if (!connected_) {
ReconnectAndSendLocked();
return;
}
if (ActiveClientIdLocked().empty()) {
return;
}
if (!WriteFrameLocked(kFrameOpcode, BuildActivityPayload(activity_))) {
RecordFailedConnectionLocked();
CloseLocked();
}
}
bool ConnectionRetryAllowedLocked() const {
return !lastFailedConnectionAttempt_ ||
std::chrono::steady_clock::now() - *lastFailedConnectionAttempt_ >= kConnectionRetryCooldown;
}
void RecordFailedConnectionLocked() {
lastFailedConnectionAttempt_ = std::chrono::steady_clock::now();
}
#if defined(_WIN32)
bool ConnectLocked() {
if (connected_) {
return true;
}
if (!ConnectionRetryAllowedLocked()) {
return false;
}
for (unsigned int index = 0; index < 10; ++index) {
const std::string name = "\\\\.\\pipe\\discord-ipc-" + std::to_string(index);
handle_ = ::CreateFileA(name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (handle_ != INVALID_HANDLE_VALUE) {
DWORD mode = PIPE_READMODE_BYTE;
::SetNamedPipeHandleState(handle_, &mode, nullptr, nullptr);
connected_ = true;
lastFailedConnectionAttempt_.reset();
return true;
}
}
RecordFailedConnectionLocked();
return false;
}
bool WriteAllLocked(const uint8_t* data, size_t size) {
while (size != 0) {
DWORD written = 0;
if (!::WriteFile(handle_, data, static_cast<DWORD>(size), &written, nullptr) || written == 0) {
return false;
}
data += written;
size -= written;
}
return true;
}
bool ReadAllLocked(uint8_t* data, size_t size) {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1);
while (size != 0) {
if (std::chrono::steady_clock::now() >= deadline) {
return false;
}
DWORD available = 0;
if (!::PeekNamedPipe(handle_, nullptr, 0, nullptr, &available, nullptr)) {
return false;
}
if (available == 0) {
if (std::chrono::steady_clock::now() >= deadline) {
return false;
}
::Sleep(10);
continue;
}
DWORD read = 0;
const DWORD requested = static_cast<DWORD>(std::min<size_t>(size, available));
if (!::ReadFile(handle_, data, requested, &read, nullptr) || read == 0) {
return false;
}
data += read;
size -= read;
}
return true;
}
void CloseLocked() {
if (handle_ != INVALID_HANDLE_VALUE) {
::CloseHandle(handle_);
handle_ = INVALID_HANDLE_VALUE;
}
connected_ = false;
}
HANDLE handle_ = INVALID_HANDLE_VALUE;
#else
bool ConnectLocked() {
if (connected_) {
return true;
}
if (!ConnectionRetryAllowedLocked()) {
return false;
}
std::array<std::string, 5> roots{};
size_t rootCount = 0;
if (const char* runtimeDir = std::getenv("XDG_RUNTIME_DIR"); runtimeDir && *runtimeDir) {
roots[rootCount++] = runtimeDir;
}
if (const char* tempDir = std::getenv("TMPDIR"); tempDir && *tempDir) {
roots[rootCount++] = tempDir;
}
if (const char* tempDir = std::getenv("TMP"); tempDir && *tempDir && rootCount < roots.size()) {
roots[rootCount++] = tempDir;
}
if (const char* tempDir = std::getenv("TEMP"); tempDir && *tempDir && rootCount < roots.size()) {
roots[rootCount++] = tempDir;
}
roots[rootCount++] = "/tmp";
for (size_t rootIndex = 0; rootIndex < rootCount; ++rootIndex) {
for (unsigned int index = 0; index < 10; ++index) {
const std::string path = roots[rootIndex] + "/discord-ipc-" + std::to_string(index);
if (path.size() >= sizeof(sockaddr_un::sun_path)) {
continue;
}
const int socketFd = ::socket(AF_UNIX, SOCK_STREAM, 0);
if (socketFd < 0) {
continue;
}
sockaddr_un address{};
address.sun_family = AF_UNIX;
std::memcpy(address.sun_path, path.c_str(), path.size() + 1);
if (::connect(socketFd, reinterpret_cast<const sockaddr*>(&address), sizeof(address)) == 0) {
timeval timeout{};
timeout.tv_sec = 1;
::setsockopt(socketFd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
fd_ = socketFd;
connected_ = true;
lastFailedConnectionAttempt_.reset();
return true;
}
::close(socketFd);
}
}
RecordFailedConnectionLocked();
return false;
}
bool WriteAllLocked(const uint8_t* data, size_t size) {
while (size != 0) {
const ssize_t written = ::send(fd_, data, size, MSG_NOSIGNAL);
if (written <= 0) {
return false;
}
data += written;
size -= static_cast<size_t>(written);
}
return true;
}
bool ReadAllLocked(uint8_t* data, size_t size) {
while (size != 0) {
const ssize_t read = ::recv(fd_, data, size, 0);
if (read <= 0) {
return false;
}
data += read;
size -= static_cast<size_t>(read);
}
return true;
}
void CloseLocked() {
if (fd_ >= 0) {
::close(fd_);
fd_ = -1;
}
connected_ = false;
}
int fd_ = -1;
#endif
bool ReadReadyLocked() {
std::array<uint8_t, 8> header{};
if (!ReadAllLocked(header.data(), header.size())) {
return false;
}
uint32_t opcode = 0;
uint32_t size = 0;
for (size_t i = 0; i < 4; ++i) {
opcode |= static_cast<uint32_t>(header[i]) << (i * 8);
size |= static_cast<uint32_t>(header[4 + i]) << (i * 8);
}
if (opcode != kFrameOpcode || size > 1024 * 1024) {
return false;
}
std::vector<uint8_t> payload(size);
if (!ReadAllLocked(payload.data(), payload.size())) {
return false;
}
const std::string_view json(reinterpret_cast<const char*>(payload.data()), payload.size());
return json.find("\"evt\":\"READY\"") != std::string_view::npos;
}
std::mutex mutex_;
bool connected_ = false;
bool customClient_ = false;
std::optional<std::chrono::steady_clock::time_point> lastFailedConnectionAttempt_;
std::string basicClientId_;
std::string clientId_;
Activity basicActivity_;
Activity activity_;
};
Client g_client;
} // namespace
void Initialize(const std::string& basicClientId, const std::string& basicTitle) {
g_client.Initialize(basicClientId, basicTitle);
}
void SetClient(const std::string& clientId) {
g_client.SetClient(clientId);
}
void SetActivity(Activity activity) {
g_client.SetActivity(std::move(activity));
}
void Reset() {
g_client.Reset();
}
void Shutdown() {
g_client.Shutdown();
}
} // namespace DiscordPresence
+24 -139
View File
@@ -2,6 +2,7 @@
#include "memory.h" #include "memory.h"
#include "abi_bridge.h" #include "abi_bridge.h"
#include "hle_stubs.h" #include "hle_stubs.h"
#include "host_context.h"
#include "runtime_log.h" #include "runtime_log.h"
// Defined in hle/os/os_sleep.cpp; the sleep-timer table is file-local there. // Defined in hle/os/os_sleep.cpp; the sleep-timer table is file-local there.
@@ -13,24 +14,8 @@
#include <iomanip> #include <iomanip>
#include <sstream> #include <sstream>
#if !defined(_WIN32)
#include "libco.h"
#endif
namespace Fiber { namespace Fiber {
#if !defined(_WIN32)
namespace {
// libco's co_create() entry points take no argument, unlike CreateFiber(size, FiberProc, param).
// CreateGuestFiber() stages the guest thread address here immediately before the first co_switch
// into a freshly created cothread; FiberProcTrampoline reads it exactly once, at the top of the
// fiber's very first activation. Safe because guest fibers are strictly cooperative on a single
// OS thread: nothing else can run (and so nothing else can overwrite this) between the staging
// write and the trampoline's read of it.
thread_local uint32_t s_pendingFiberArg = 0;
} // namespace
#endif
std::mutex GuestFiberManager::s_mutex; std::mutex GuestFiberManager::s_mutex;
std::unordered_map<uint32_t, GuestFiber> GuestFiberManager::s_fibers; std::unordered_map<uint32_t, GuestFiber> GuestFiberManager::s_fibers;
std::vector<void*> GuestFiberManager::s_fibersPendingDelete; std::vector<void*> GuestFiberManager::s_fibersPendingDelete;
@@ -45,21 +30,11 @@ void GuestFiberManager::PurgePendingFibers() {
std::lock_guard<std::mutex> lock(s_mutex); std::lock_guard<std::mutex> lock(s_mutex);
toDelete.swap(s_fibersPendingDelete); toDelete.swap(s_fibersPendingDelete);
} }
#if defined(_WIN32)
const void* current = GetCurrentFiber();
for (void* f : toDelete) { for (void* f : toDelete) {
if (f && f != current) { if (f && !HostContext::IsCurrent(f)) {
DeleteFiber(f); HostContext::Destroy(f);
} }
} }
#else
const void* current = co_active();
for (void* f : toDelete) {
if (f && f != current) {
co_delete(static_cast<cothread_t>(f));
}
}
#endif
} }
// Global VI retrace counter // Global VI retrace counter
@@ -212,27 +187,13 @@ void GuestFiberManager::Initialize() {
return; return;
} }
#if defined(_WIN32) if (!HostContext::InitializeScheduler(&s_schedulerFiber)) {
// Convert the main thread to a fiber (the scheduler fiber) RT_LOG(RT_TAG_OS) << "FATAL: Failed to initialize scheduler context!" << std::endl;
s_schedulerFiber = ConvertThreadToFiber(nullptr); ShowRuntimeFatalPopup("guest scheduler initialization failed",
if (!s_schedulerFiber) { "The host could not create the scheduler context required to run guest threads.");
// May already be a fiber std::abort();
s_schedulerFiber = GetCurrentFiber();
if (!s_schedulerFiber) {
RT_LOG(RT_TAG_OS) << "FATAL: Failed to initialize scheduler fiber!" << std::endl;
ShowRuntimeFatalPopup("guest scheduler initialization failed",
"Windows could not create the scheduler fiber required to run guest threads.");
std::abort();
}
} }
#else
// co_active() returns a handle for whichever native stack is currently running, creating one
// on first call if needed - the libco analogue of ConvertThreadToFiber(nullptr): it converts
// this call's own stack into a switchable target without altering control flow.
s_schedulerFiber = co_active();
#endif
s_currentGuestThread = 0; s_currentGuestThread = 0;
s_initialized = true; s_initialized = true;
} }
@@ -240,32 +201,18 @@ void GuestFiberManager::Initialize() {
void GuestFiberManager::Shutdown() { void GuestFiberManager::Shutdown() {
std::lock_guard<std::mutex> lock(s_mutex); std::lock_guard<std::mutex> lock(s_mutex);
#if defined(_WIN32)
for (auto& [addr, fiber] : s_fibers) { for (auto& [addr, fiber] : s_fibers) {
if (fiber.fiber && !fiber.isSchedulerFiber) { if (fiber.fiber && !fiber.isSchedulerFiber) {
DeleteFiber(fiber.fiber); HostContext::Destroy(fiber.fiber);
fiber.fiber = nullptr; fiber.fiber = nullptr;
} }
} }
s_fibers.clear(); s_fibers.clear();
// Convert scheduler fiber back to thread
if (s_schedulerFiber) { if (s_schedulerFiber) {
ConvertFiberToThread(); HostContext::ShutdownScheduler(s_schedulerFiber);
s_schedulerFiber = nullptr; s_schedulerFiber = nullptr;
} }
#else
for (auto& [addr, fiber] : s_fibers) {
if (fiber.fiber && !fiber.isSchedulerFiber) {
co_delete(static_cast<cothread_t>(fiber.fiber));
fiber.fiber = nullptr;
}
}
s_fibers.clear();
// Unlike ConvertFiberToThread, libco has no "undo" for co_active(): the scheduler's own
// stack was never separately allocated, so there is nothing to release here.
s_schedulerFiber = nullptr;
#endif
s_initialized = false; s_initialized = false;
} }
@@ -288,11 +235,7 @@ bool GuestFiberManager::CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entr
if (existingIt != s_fibers.end()) { if (existingIt != s_fibers.end()) {
// Delete the old fiber if it exists and is not the scheduler fiber // Delete the old fiber if it exists and is not the scheduler fiber
if (existingIt->second.fiber && !existingIt->second.isSchedulerFiber) { if (existingIt->second.fiber && !existingIt->second.isSchedulerFiber) {
#if defined(_WIN32) HostContext::Destroy(existingIt->second.fiber);
DeleteFiber(existingIt->second.fiber);
#else
co_delete(static_cast<cothread_t>(existingIt->second.fiber));
#endif
} }
s_fibers.erase(existingIt); s_fibers.erase(existingIt);
} }
@@ -312,31 +255,17 @@ bool GuestFiberManager::CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entr
gf.cpuContext.pc = entryPoint; gf.cpuContext.pc = entryPoint;
gf.cpuContext.srr0 = entryPoint; gf.cpuContext.srr0 = entryPoint;
#if defined(_WIN32) // The host stack models only translated host calls; the guest stack starts
// Create Windows fiber with reasonable stack size // at stackBase in the CPU context above.
// Use host stack size (64KB should be plenty for translated code)
constexpr size_t kHostStackSize = 64 * 1024; constexpr size_t kHostStackSize = 64 * 1024;
gf.fiber = CreateFiber(kHostStackSize, FiberProc, reinterpret_cast<void*>(static_cast<uintptr_t>(guestThreadAddr))); gf.fiber = HostContext::Create(kHostStackSize, FiberProc,
reinterpret_cast<void*>(static_cast<uintptr_t>(guestThreadAddr)));
if (!gf.fiber) { if (!gf.fiber) {
DWORD err = GetLastError(); RT_LOG(RT_TAG_OS) << "Failed to create host context for thread 0x"
RT_LOG(RT_TAG_OS) << "CreateFiber failed for thread 0x"
<< std::hex << guestThreadAddr
<< " error=" << std::dec << err << std::endl;
return false;
}
#else
// libco's co_create() entry point takes no argument; SwitchToThread() stages guestThreadAddr
// into s_pendingFiberArg immediately before the co_switch that first activates this handle.
constexpr unsigned int kHostStackSize = 64 * 1024;
gf.fiber = co_create(kHostStackSize, &FiberProcTrampoline);
if (!gf.fiber) {
RT_LOG(RT_TAG_OS) << "co_create failed for thread 0x"
<< std::hex << guestThreadAddr << std::dec << std::endl; << std::hex << guestThreadAddr << std::dec << std::endl;
return false; return false;
} }
#endif
s_fibers[guestThreadAddr] = gf; s_fibers[guestThreadAddr] = gf;
@@ -390,24 +319,11 @@ void GuestFiberManager::ExitGuestThread(uint32_t guestThreadAddr, ThreadState fi
} }
if (it->second.fiber && !it->second.isSchedulerFiber) { if (it->second.fiber && !it->second.isSchedulerFiber) {
#if defined(_WIN32) if (HostContext::IsCurrent(it->second.fiber)) {
const void* current = GetCurrentFiber();
if (it->second.fiber == current) {
s_fibersPendingDelete.push_back(it->second.fiber); s_fibersPendingDelete.push_back(it->second.fiber);
} else { } else {
DeleteFiber(it->second.fiber); HostContext::Destroy(it->second.fiber);
} }
#else
const void* current = co_active();
if (it->second.fiber == current) {
// Deleting the coroutine we're currently executing on would free the very stack
// this call is running on; defer it (PurgePendingFibers) until some other fiber is
// active, exactly like the Windows branch above.
s_fibersPendingDelete.push_back(it->second.fiber);
} else {
co_delete(static_cast<cothread_t>(it->second.fiber));
}
#endif
it->second.fiber = nullptr; it->second.fiber = nullptr;
} }
} }
@@ -474,12 +390,7 @@ void GuestFiberManager::SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu
// Check if we're already on the target fiber (e.g., switching to main thread // Check if we're already on the target fiber (e.g., switching to main thread
// when we're already on the scheduler fiber) // when we're already on the scheduler fiber)
#if defined(_WIN32) if (HostContext::IsCurrent(fiberHandle)) {
void* currentFiber = GetCurrentFiber();
#else
void* currentFiber = co_active();
#endif
if (currentFiber == fiberHandle) {
// Already executing on the target host fiber. This is common for the // Already executing on the target host fiber. This is common for the
// default guest thread, which also owns the scheduler fiber. Keep the // default guest thread, which also owns the scheduler fiber. Keep the
// live CPU context instead of restoring a possibly stale saved copy // live CPU context instead of restoring a possibly stale saved copy
@@ -496,15 +407,7 @@ void GuestFiberManager::SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu
} }
// Switch to the target fiber (the target fiber will load its own context) // Switch to the target fiber (the target fiber will load its own context)
#if defined(_WIN32) HostContext::Switch(fiberHandle);
SwitchToFiber(fiberHandle);
#else
// Staged for FiberProcTrampoline's first (and only) read; a no-op for a fiber that has
// already started, since resuming it re-enters mid-function rather than through the
// trampoline's entry point.
s_pendingFiberArg = guestThreadAddr;
co_switch(static_cast<cothread_t>(fiberHandle));
#endif
// When we return here, the fiber that issued SwitchToThread has resumed. // When we return here, the fiber that issued SwitchToThread has resumed.
// That does not automatically mean the previous guest thread became runnable // That does not automatically mean the previous guest thread became runnable
@@ -618,14 +521,6 @@ void GuestFiberManager::ProcessTimerEvents(CpuContext* cpu) {
} }
} }
void GuestFiberManager::SwitchToScheduler() {
#if defined(_WIN32)
SwitchToFiber(s_schedulerFiber);
#else
co_switch(static_cast<cothread_t>(s_schedulerFiber));
#endif
}
#if defined(_WIN32) #if defined(_WIN32)
void CALLBACK GuestFiberManager::FiberProc(void* param) void CALLBACK GuestFiberManager::FiberProc(void* param)
#else #else
@@ -634,6 +529,7 @@ void GuestFiberManager::FiberProc(void* param)
{ {
uint32_t guestThreadAddr = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(param)); uint32_t guestThreadAddr = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(param));
// Get our fiber info // Get our fiber info
GuestFiber* fiber = nullptr; GuestFiber* fiber = nullptr;
uint32_t entryPoint = 0; uint32_t entryPoint = 0;
@@ -644,7 +540,7 @@ void GuestFiberManager::FiberProc(void* param)
auto it = s_fibers.find(guestThreadAddr); auto it = s_fibers.find(guestThreadAddr);
if (it == s_fibers.end()) { if (it == s_fibers.end()) {
RT_LOG(RT_TAG_OS) << "FiberProc: fiber not found!" << std::endl; RT_LOG(RT_TAG_OS) << "FiberProc: fiber not found!" << std::endl;
SwitchToScheduler(); HostContext::Switch(s_schedulerFiber);
return; return;
} }
fiber = &it->second; fiber = &it->second;
@@ -707,7 +603,7 @@ void GuestFiberManager::FiberProc(void* param)
<< ", fn=0x" << startFn << ") after retries; continuing anyway." << std::dec << std::endl; << ", fn=0x" << startFn << ") after retries; continuing anyway." << std::dec << std::endl;
break; break;
} }
SwitchToScheduler(); HostContext::Switch(s_schedulerFiber);
} }
// The deferral loop above yields to the scheduler and therefore can resume // The deferral loop above yields to the scheduler and therefore can resume
@@ -764,18 +660,7 @@ void GuestFiberManager::FiberProc(void* param)
} }
// Return to scheduler // Return to scheduler
SwitchToScheduler(); HostContext::Switch(s_schedulerFiber);
} }
#if !defined(_WIN32)
void GuestFiberManager::FiberProcTrampoline() {
const uint32_t guestThreadAddr = s_pendingFiberArg;
FiberProc(reinterpret_cast<void*>(static_cast<uintptr_t>(guestThreadAddr)));
// FiberProc always calls SwitchToScheduler() on every exit path and never falls off its own
// end; this is only a safety net in case that ever changes; falling off co_create's entry
// function is otherwise undefined behavior (libco's own crash() fallback aborts instead).
SwitchToScheduler();
}
#endif
} // namespace Fiber } // namespace Fiber
+21 -4
View File
@@ -36,6 +36,9 @@
#endif #endif
namespace GuestFlat { namespace GuestFlat {
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
bool g_requiresCheckedAccess = false;
#endif
namespace { namespace {
#if defined(_WIN32) #if defined(_WIN32)
@@ -48,6 +51,16 @@ constexpr DWORD kMemPreservePlaceholder = 0x00000002;
constexpr size_t kAllocationGranularity = 0x10000; // 64 KiB constexpr size_t kAllocationGranularity = 0x10000; // 64 KiB
constexpr size_t kHostPageSize = 0x1000; constexpr size_t kHostPageSize = 0x1000;
// Only hosts that can expose a page larger than 4 KiB need to discover their
// size at runtime; see RequiresCheckedAccess() in guest_flat_memory.h.
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
size_t HostPageSize()
{
const long size = sysconf(_SC_PAGESIZE);
return size > 0 ? static_cast<size_t>(size) : kGuestPageSize;
}
#endif
// Named, platform-neutral protection modes so every fault-interception call site below (the // Named, platform-neutral protection modes so every fault-interception call site below (the
// MMIO window, the executable-write guard, deferred-EFB-read protection, the on-demand // MMIO window, the executable-write guard, deferred-EFB-read protection, the on-demand
// unmapped-block commit) can stay identical text on both platforms; only ProtectRange() and // unmapped-block commit) can stay identical text on both platforms; only ProtectRange() and
@@ -349,7 +362,7 @@ bool IsMmio(uint32_t address) { return MemoryInline::IsMmioAddress(address); }
bool IsGpuFifo(uint32_t address) { return MemoryInline::IsGpuFifoAddress(address); } bool IsGpuFifo(uint32_t address) { return MemoryInline::IsGpuFifoAddress(address); }
void ApplyExecutableProtectionLocked() { void ApplyExecutableProtectionLocked() {
if (g_base == nullptr) return; if (g_base == nullptr || RequiresCheckedAccess()) return;
auto& protectedPages = ExecutableProtectedPages(); auto& protectedPages = ExecutableProtectedPages();
for (const auto& range : ExecutableRanges()) { for (const auto& range : ExecutableRanges()) {
// Only pages fully inside the range are protected: edge pages often share a page with data // Only pages fully inside the range are protected: edge pages often share a page with data
@@ -497,6 +510,10 @@ bool IsActive() {
void Initialize(const std::vector<RegionRequest>& regions) { void Initialize(const std::vector<RegionRequest>& regions) {
std::lock_guard<std::mutex> lock(StateMutex()); std::lock_guard<std::mutex> lock(StateMutex());
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
g_requiresCheckedAccess = HostPageSize() > kGuestPageSize;
#endif
if (g_initialized) { if (g_initialized) {
if (!SameLayout(g_activeRegions, regions)) { if (!SameLayout(g_activeRegions, regions)) {
throw std::runtime_error( throw std::runtime_error(
@@ -615,7 +632,7 @@ uint8_t* HostPointer(uint32_t guestAddress) {
} }
void ProtectDeferredRange(uint32_t address, size_t length) { void ProtectDeferredRange(uint32_t address, size_t length) {
if (!g_initialized || length == 0) return; if (RequiresCheckedAccess() || !g_initialized || length == 0) return;
const uint64_t end = static_cast<uint64_t>(address) + length; const uint64_t end = static_cast<uint64_t>(address) + length;
if (end > kGuestSpaceSize) return; if (end > kGuestSpaceSize) return;
std::lock_guard<std::mutex> lock(StateMutex()); std::lock_guard<std::mutex> lock(StateMutex());
@@ -630,7 +647,7 @@ void ProtectDeferredRange(uint32_t address, size_t length) {
} }
void UnprotectDeferredRange(uint32_t address, size_t length) { void UnprotectDeferredRange(uint32_t address, size_t length) {
if (!g_initialized || length == 0) return; if (RequiresCheckedAccess() || !g_initialized || length == 0) return;
std::lock_guard<std::mutex> lock(StateMutex()); std::lock_guard<std::mutex> lock(StateMutex());
auto& ranges = DeferredRanges(); auto& ranges = DeferredRanges();
const uint64_t end = static_cast<uint64_t>(address) + length; const uint64_t end = static_cast<uint64_t>(address) + length;
@@ -645,7 +662,7 @@ void UnprotectDeferredRange(uint32_t address, size_t length) {
} }
void RegisterExecutableRange(uint32_t start, uint32_t end) { void RegisterExecutableRange(uint32_t start, uint32_t end) {
if (end <= start) return; if (RequiresCheckedAccess() || end <= start) return;
std::lock_guard<std::mutex> lock(StateMutex()); std::lock_guard<std::mutex> lock(StateMutex());
auto& ranges = ExecutableRanges(); auto& ranges = ExecutableRanges();
if (std::any_of(ranges.begin(), ranges.end(), [&](const GuardedRange& range) { if (std::any_of(ranges.begin(), ranges.end(), [&](const GuardedRange& range) {
+80
View File
@@ -0,0 +1,80 @@
#include "guest_flat_memory.h"
#include <mach/mach.h>
#include <mach/mach_vm.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <algorithm>
#include <cstdio>
#include <mutex>
#include <stdexcept>
#include <vector>
namespace GuestFlat {
bool g_requiresCheckedAccess = false;
namespace {
struct Mapping { uint32_t base; uint64_t size; uint8_t* host; };
std::mutex g_mutex;
std::vector<Mapping> g_mappings;
std::vector<RegionRequest> g_layout;
uint8_t* g_base = nullptr;
bool g_active = false;
uint64_t Offset(const RegionRequest& r) {
if (r.backing == Backing::Mem1) return r.base & 0x1fffffffu;
if (r.backing == Backing::Mem2) return (r.base & 0x1fffffffu) - 0x10000000u;
return 0;
}
bool Same(const std::vector<RegionRequest>& a, const std::vector<RegionRequest>& b) {
return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(),
[](const auto& x, const auto& y) { return x.base == y.base && x.size == y.size && x.backing == y.backing; });
}
int BackingFile(size_t size) {
char name[] = "/tmp/wiicompiled-guest-XXXXXX";
const int fd = mkstemp(name);
if (fd >= 0) { unlink(name); if (ftruncate(fd, static_cast<off_t>(size)) != 0) { close(fd); return -1; } }
return fd;
}
} // namespace
bool IsActive() { return g_active; }
void Initialize(const std::vector<RegionRequest>& regions) {
std::lock_guard lock(g_mutex);
g_requiresCheckedAccess = static_cast<size_t>(getpagesize()) > kGuestPageSize;
if (g_active) { if (!Same(g_layout, regions)) throw std::runtime_error("flat guest layout cannot be remapped"); return; }
mach_vm_address_t address = kFixedFlatGuestBase;
if (mach_vm_allocate(mach_task_self(), &address, kGuestSpaceSize, VM_FLAGS_FIXED) != KERN_SUCCESS || address != kFixedFlatGuestBase)
throw std::runtime_error("unable to reserve fixed 4 GiB macOS guest address space");
g_base = reinterpret_cast<uint8_t*>(address);
struct Store { Backing kind; uint32_t owned; uint64_t size; int fd; };
std::vector<Store> stores;
for (const auto& r : regions) {
if (!r.size) continue;
const uint32_t owned = r.backing == Backing::Owned ? r.base : 0;
auto it = std::find_if(stores.begin(), stores.end(), [&](const Store& s) { return s.kind == r.backing && s.owned == owned; });
const uint64_t need = Offset(r) + r.size;
if (it == stores.end()) stores.push_back({r.backing, owned, need, -1}); else it->size = std::max(it->size, need);
}
for (auto& s : stores) { s.fd = BackingFile(s.size); if (s.fd < 0) throw std::runtime_error("unable to create macOS guest backing store"); }
for (const auto& r : regions) {
if (!r.size) continue;
const uint32_t owned = r.backing == Backing::Owned ? r.base : 0;
const auto& s = *std::find_if(stores.begin(), stores.end(), [&](const Store& x) { return x.kind == r.backing && x.owned == owned; });
auto* host = static_cast<uint8_t*>(mmap(nullptr, r.size, PROT_READ | PROT_WRITE, MAP_SHARED, s.fd, Offset(r)));
auto* guest = mmap(g_base + r.base, r.size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, s.fd, Offset(r));
if (host == MAP_FAILED || guest != g_base + r.base) throw std::runtime_error("unable to map macOS guest alias");
g_mappings.push_back({r.base, r.size, host});
}
for (auto& s : stores) close(s.fd);
g_layout = regions; g_active = true;
}
uint8_t* HostPointer(uint32_t a) { for (const auto& m : g_mappings) if (a >= m.base && uint64_t(a - m.base) < m.size) return m.host + (a - m.base); return nullptr; }
void ProtectDeferredRange(uint32_t, size_t) {}
void UnprotectDeferredRange(uint32_t, size_t) {}
void RegisterExecutableRange(uint32_t, uint32_t) {}
FaultCounters Counters() { return {}; }
void LogFaultSummary() noexcept {}
bool HandleAccessViolation(void*, bool) noexcept { return false; }
} // namespace GuestFlat
+1
View File
@@ -394,3 +394,4 @@ extern std::mutex g_tlutObjMutex;
// that happens on another key: unordered_map keeps references valid across a // that happens on another key: unordered_map keeps references valid across a
// rehash, an open-addressed table would not. // rehash, an open-addressed table would not.
extern std::unordered_map<uint32_t, TexObjSlot> g_TexObjMeta; extern std::unordered_map<uint32_t, TexObjSlot> g_TexObjMeta;
extern std::map<uint32_t, TlutObjMeta> g_TlutObjMeta;
+9
View File
@@ -33,6 +33,15 @@ void WriteGuestFloat(uint32_t addr, float value, const char* label) {
void* GuestToHostPtr(uint32_t addr, size_t len) { void* GuestToHostPtr(uint32_t addr, size_t len) {
if (addr == 0) return nullptr; if (addr == 0) return nullptr;
#if defined(__APPLE__)
// The macOS flat guest map exposes separate host aliases for cached,
// uncached, and physical MEM1/MEM2 addresses. GX resources are identified
// by their host pointer, so all aliases of one guest allocation must use
// the same physical mapping before they reach Aurora. Kept macOS-only:
// changing which alias the other hosts hand out would re-key their existing
// GX resource identity.
addr = CanonicalizeGxMainRamAddress(addr);
#endif
try { return Memory::GetPointer(addr, len); } catch (const Memory::AccessViolation& e) { LogMemoryError(RT_TAG_GX, "GX guest pointer", e); return nullptr; } try { return Memory::GetPointer(addr, len); } catch (const Memory::AccessViolation& e) { LogMemoryError(RT_TAG_GX, "GX guest pointer", e); return nullptr; }
} }
+234 -8
View File
@@ -1,23 +1,249 @@
#include "hle_stubs.h" #include "hle_stubs.h"
#include "memory.h"
#include "wii_remote_input.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint> #include <cstdint>
// KPAD HLE fed by a real Bluetooth Wii Remote. The game calls KPADRead once per
// frame with room for 16 KPADStatus entries and only looks at entry 0; with a
// Classic Controller it also calls KPADGetUnifiedWpadStatus for the raw
// WPADCLStatus (buttons, sticks and triggers of the extension).
namespace {
constexpr uint32_t kKpadStatusSize = 0x84;
// KPADStatus field offsets (RVL SDK).
constexpr uint32_t kHold = 0x00, kTrig = 0x04, kRelease = 0x08, kAcc = 0x0C, kAccValue = 0x18,
kAccSpeed = 0x1C, kPos = 0x20, kAccVertical = 0x54, kDevType = 0x5C, kWpadErr = 0x5D,
kDpdValidFg = 0x5E, kDataFormat = 0x5F, kFsStick = 0x60, kFsAcc = 0x68, kFsAccValue = 0x74,
kFsAccSpeed = 0x78;
// KPADStatus.ex_status.cl (KPADEXStatus, Classic Controller view).
constexpr uint32_t kClHold = 0x60, kClTrig = 0x64, kClRelease = 0x68, kClLStick = 0x6C, kClRStick = 0x74,
kClLTrigger = 0x7C, kClRTrigger = 0x80;
// KPADUnifiedWpadStatus: WPADStatus / WPADFSStatus / WPADCLStatus union, then fmt.
constexpr uint32_t kUnifiedSize = 0x38;
constexpr uint32_t kUButton = 0x00, kUAccX = 0x02, kUAccY = 0x04, kUAccZ = 0x06, kUObj = 0x08, kUDev = 0x28,
kUErr = 0x29, kUFsStickX = 0x2A, kUFsStickY = 0x2B, kUFsAccX = 0x2C, kUFsAccY = 0x2E,
kUFsAccZ = 0x30, kUClButton = 0x2A, kUClLStickX = 0x2C, kUClLStickY = 0x2E, kUClRStickX = 0x30,
kUClRStickY = 0x32, kUClTriggerL = 0x34, kUClTriggerR = 0x35, kUFmt = 0x36;
// WPAD device types (WPAD_DEV_*) and the data formats KPAD runs each of them
// in (WPAD_FMT_*_ACC_DPD): the values KPADStatus.dev_type / data_format and
// KPADUnifiedWpadStatus.dev / fmt carry on the console.
constexpr uint8_t kDevCore = 0;
constexpr uint8_t kDevFreestyle = 1;
constexpr uint8_t kDevClassic = 2;
constexpr uint8_t kFmtCoreAccDpd = 2;
constexpr uint8_t kFmtFreestyleAccDpd = 5;
constexpr uint8_t kFmtClassicAccDpd = 8;
constexpr int8_t kWpadErrNone = 0;
constexpr int8_t kWpadErrNoController = -1;
// Raw accelerometer as WPADStatus carries it: 10 bits, 0x200 at 0 g, 100 per g.
constexpr float kRawAccZero = 512.0f;
constexpr float kRawAccPerG = 100.0f;
struct ChannelState {
uint32_t prevHold = 0;
uint32_t prevClHold = 0;
float prevAcc[3] = {0.0f, -1.0f, 0.0f};
float prevFsAcc[3] = {0.0f, -1.0f, 0.0f};
};
std::array<ChannelState, 4> g_channels{};
// Euclidean length of a 3-vector.
float Length(const float* v) {
return std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
}
// Euclidean distance between two 3-vectors.
float Distance(const float* a, const float* b) {
const float d[3] = {a[0] - b[0], a[1] - b[1], a[2] - b[2]};
return Length(d);
}
// Writes three big-endian floats to guest memory.
void WriteVec3(uint32_t addr, const float* v) {
Memory::WriteFloat32(addr, v[0]);
Memory::WriteFloat32(addr + 4, v[1]);
Memory::WriteFloat32(addr + 8, v[2]);
}
// Zeroes `count` consecutive floats in guest memory.
void WriteZeroFloats(uint32_t addr, uint32_t count) {
for (uint32_t i = 0; i < count; ++i) {
Memory::WriteFloat32(addr + i * 4, 0.0f);
}
}
// Fills one KPADStatus at `addr` and returns the number of valid entries (1),
// or writes an "unplugged" status and returns 0.
int32_t WriteStatus(uint32_t chan, uint32_t addr, const WiiRemoteInput::KpadSample* sample) {
ChannelState& state = g_channels[chan];
if (sample == nullptr) {
state = {};
Memory::Write32(addr + kHold, 0);
Memory::Write32(addr + kTrig, 0);
Memory::Write32(addr + kRelease, 0);
Memory::Write8(addr + kDevType, kDevCore);
Memory::Write8(addr + kWpadErr, static_cast<uint8_t>(kWpadErrNoController));
Memory::Write8(addr + kDpdValidFg, 0);
return 0;
}
const uint32_t hold = sample->hold;
Memory::Write32(addr + kHold, hold);
Memory::Write32(addr + kTrig, hold & ~state.prevHold);
Memory::Write32(addr + kRelease, state.prevHold & ~hold);
state.prevHold = hold;
WriteVec3(addr + kAcc, sample->acc);
Memory::WriteFloat32(addr + kAccValue, Length(sample->acc));
Memory::WriteFloat32(addr + kAccSpeed, Distance(sample->acc, state.prevAcc));
for (int i = 0; i < 3; ++i) state.prevAcc[i] = sample->acc[i];
// No IR pointer: pos .. acc_vertical zeroed and dpd_valid_fg clear, which
// the game treats as "pointing away from the screen".
WriteZeroFloats(addr + kPos, (kAccVertical + 8 - kPos) / 4);
Memory::Write8(addr + kDpdValidFg, 0);
const uint8_t devType = sample->hasClassic ? kDevClassic : sample->hasNunchuk ? kDevFreestyle : kDevCore;
const uint8_t dataFormat =
sample->hasClassic ? kFmtClassicAccDpd : sample->hasNunchuk ? kFmtFreestyleAccDpd : kFmtCoreAccDpd;
Memory::Write8(addr + kDevType, devType);
Memory::Write8(addr + kWpadErr, static_cast<uint8_t>(kWpadErrNone));
Memory::Write8(addr + kDataFormat, dataFormat);
if (sample->hasClassic) {
const uint32_t clHold = sample->clHold;
Memory::Write32(addr + kClHold, clHold);
Memory::Write32(addr + kClTrig, clHold & ~state.prevClHold);
Memory::Write32(addr + kClRelease, state.prevClHold & ~clHold);
state.prevClHold = clHold;
Memory::WriteFloat32(addr + kClLStick, sample->clLStick[0]);
Memory::WriteFloat32(addr + kClLStick + 4, sample->clLStick[1]);
Memory::WriteFloat32(addr + kClRStick, sample->clRStick[0]);
Memory::WriteFloat32(addr + kClRStick + 4, sample->clRStick[1]);
Memory::WriteFloat32(addr + kClLTrigger, sample->clTriggerL / 255.0f);
Memory::WriteFloat32(addr + kClRTrigger, sample->clTriggerR / 255.0f);
} else if (sample->hasNunchuk) {
state.prevClHold = 0;
Memory::WriteFloat32(addr + kFsStick, sample->stick[0]);
Memory::WriteFloat32(addr + kFsStick + 4, sample->stick[1]);
WriteVec3(addr + kFsAcc, sample->nunchukAcc);
Memory::WriteFloat32(addr + kFsAccValue, Length(sample->nunchukAcc));
Memory::WriteFloat32(addr + kFsAccSpeed, Distance(sample->nunchukAcc, state.prevFsAcc));
for (int i = 0; i < 3; ++i) state.prevFsAcc[i] = sample->nunchukAcc[i];
} else {
state.prevClHold = 0;
WriteZeroFloats(addr + kFsStick, (kKpadStatusSize - kFsStick) / 4);
}
return 1;
}
// One accelerometer axis of KPAD's g vector back to the 10-bit raw WPAD value.
uint16_t RawAcc(float g) {
const float raw = kRawAccZero + g * kRawAccPerG;
return static_cast<uint16_t>(std::clamp(raw, 0.0f, 1023.0f));
}
// Fills one KPADUnifiedWpadStatus at `addr` from the sample: the WPADStatus core
// (remote buttons, raw accelerometer, no IR objects), then the Nunchuk or Classic
// Controller tail, then the data format.
void WriteUnifiedStatus(uint32_t addr, const WiiRemoteInput::KpadSample* sample) {
for (uint32_t offset = 0; offset < kUnifiedSize; offset += 4) {
Memory::Write32(addr + offset, 0);
}
if (sample == nullptr) {
Memory::Write8(addr + kUDev, kDevCore);
Memory::Write8(addr + kUErr, static_cast<uint8_t>(kWpadErrNoController));
Memory::Write8(addr + kUFmt, kFmtCoreAccDpd);
return;
}
Memory::Write16(addr + kUButton, static_cast<uint16_t>(sample->hold & 0xFFFF));
// KPAD's acc is (-wiiX, -wiiZ, wiiY); WPADStatus keeps the remote's own axes.
Memory::Write16(addr + kUAccX, RawAcc(-sample->acc[0]));
Memory::Write16(addr + kUAccY, RawAcc(sample->acc[2]));
Memory::Write16(addr + kUAccZ, RawAcc(-sample->acc[1]));
// No IR: every DPDObject invalid (x/y at the sensor's out-of-range value).
for (uint32_t i = 0; i < 4; ++i) {
Memory::Write16(addr + kUObj + i * 8, 0x3FF);
Memory::Write16(addr + kUObj + i * 8 + 2, 0x3FF);
}
Memory::Write8(addr + kUErr, static_cast<uint8_t>(kWpadErrNone));
if (sample->hasClassic) {
Memory::Write8(addr + kUDev, kDevClassic);
Memory::Write16(addr + kUClButton, static_cast<uint16_t>(sample->clHold & 0xFFFF));
Memory::Write16(addr + kUClLStickX, static_cast<uint16_t>(sample->clLStickRaw[0]));
Memory::Write16(addr + kUClLStickY, static_cast<uint16_t>(sample->clLStickRaw[1]));
Memory::Write16(addr + kUClRStickX, static_cast<uint16_t>(sample->clRStickRaw[0]));
Memory::Write16(addr + kUClRStickY, static_cast<uint16_t>(sample->clRStickRaw[1]));
Memory::Write8(addr + kUClTriggerL, sample->clTriggerL);
Memory::Write8(addr + kUClTriggerR, sample->clTriggerR);
Memory::Write8(addr + kUFmt, kFmtClassicAccDpd);
} else if (sample->hasNunchuk) {
Memory::Write8(addr + kUDev, kDevFreestyle);
// WPADFSStatus: 8-bit stick (centre 128) and 10-bit Nunchuk accelerometer.
Memory::Write8(addr + kUFsStickX,
static_cast<uint8_t>(std::clamp(128.0f + sample->stick[0] * 100.0f, 0.0f, 255.0f)));
Memory::Write8(addr + kUFsStickY,
static_cast<uint8_t>(std::clamp(128.0f + sample->stick[1] * 100.0f, 0.0f, 255.0f)));
Memory::Write16(addr + kUFsAccX, RawAcc(-sample->nunchukAcc[0]));
Memory::Write16(addr + kUFsAccY, RawAcc(sample->nunchukAcc[2]));
Memory::Write16(addr + kUFsAccZ, RawAcc(-sample->nunchukAcc[1]));
Memory::Write8(addr + kUFmt, kFmtFreestyleAccDpd);
} else {
Memory::Write8(addr + kUDev, kDevCore);
Memory::Write8(addr + kUFmt, kFmtCoreAccDpd);
}
}
} // namespace
// KPADRead: fills KPADStatus[0] for `chan` from the Bluetooth remote, returns the entry count.
extern "C" int32_t KPAD__Read_HLE(uint32_t chan, uint32_t statusPtr, uint32_t count) extern "C" int32_t KPAD__Read_HLE(uint32_t chan, uint32_t statusPtr, uint32_t count)
{ {
(void)chan; if (chan >= g_channels.size() || statusPtr == 0 || count == 0) {
(void)statusPtr; return 0;
(void)count; }
return 0; WiiRemoteInput::KpadSample sample;
const bool have = WiiRemoteInput::ReadKpadSample(chan, sample);
try {
return WriteStatus(chan, statusPtr, have ? &sample : nullptr);
} catch (const Memory::AccessViolation&) {
return 0;
}
} }
PPC_NATIVE_OVERRIDE(80197380, KPAD__Read_HLE, int32_t, (uint32_t chan, uint32_t statusPtr, uint32_t count), PPC_NATIVE_OVERRIDE(80197380, KPAD__Read_HLE, int32_t, (uint32_t chan, uint32_t statusPtr, uint32_t count),
(chan, statusPtr, count)); (chan, statusPtr, count));
// KPADGetUnifiedWpadStatus: the raw WPAD status behind KPADStatus. The game
// reads the Classic Controller's buttons, sticks and triggers from here. The
// SDK fills `count` entries with the channel's recent samples (the game asks for
// as many as it asked KPADRead for and looks at entry 0); with one sample per
// frame here, every entry gets the current one.
extern "C" int32_t KPAD__GetUnifiedWpadStatus_HLE(uint32_t chan, uint32_t statusPtr, uint32_t count) extern "C" int32_t KPAD__GetUnifiedWpadStatus_HLE(uint32_t chan, uint32_t statusPtr, uint32_t count)
{ {
(void)chan; constexpr uint32_t kMaxEntries = 16; // KPAD_MAX_READ_BUFS
(void)statusPtr; if (chan >= g_channels.size() || statusPtr == 0 || count == 0) {
(void)count; return 0;
return 0; }
WiiRemoteInput::KpadSample sample;
const bool have = WiiRemoteInput::ReadKpadSample(chan, sample);
try {
const uint32_t entries = std::min(count, kMaxEntries);
for (uint32_t i = 0; i < entries; ++i) {
WriteUnifiedStatus(statusPtr + i * kUnifiedSize, have ? &sample : nullptr);
}
} catch (const Memory::AccessViolation&) {
return 0;
}
return have ? 1 : 0;
} }
PPC_NATIVE_OVERRIDE(8019812C, KPAD__GetUnifiedWpadStatus_HLE, int32_t, PPC_NATIVE_OVERRIDE(8019812C, KPAD__GetUnifiedWpadStatus_HLE, int32_t,
(uint32_t chan, uint32_t statusPtr, uint32_t count), (chan, statusPtr, count)); (uint32_t chan, uint32_t statusPtr, uint32_t count), (chan, statusPtr, count));
+8
View File
@@ -1,6 +1,7 @@
#include "hle_stubs.h" #include "hle_stubs.h"
#include "memory.h" #include "memory.h"
#include "hle/controller_status_contract.h" #include "hle/controller_status_contract.h"
#include "wii_remote_input.h"
#include <algorithm> #include <algorithm>
#include <cstdio> #include <cstdio>
@@ -37,6 +38,7 @@ extern "C" uint32_t PAD__Init_HLE()
} }
PPC_NATIVE_OVERRIDE(801AF2F0, PAD__Init_HLE, uint32_t, (), ()); PPC_NATIVE_OVERRIDE(801AF2F0, PAD__Init_HLE, uint32_t, (), ());
// PADRead: gathers every GameCube pad source for the frame and writes the statuses to guest memory.
extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr) extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
{ {
if (statusPtr == 0) { if (statusPtr == 0) {
@@ -44,7 +46,13 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
} }
PADStatus statuses[PAD_CHANMAX]{}; PADStatus statuses[PAD_CHANMAX]{};
// Keep looking for a Bluetooth Wii Remote that dropped out (or was turned on late).
WiiRemoteInput::Poll();
uint32_t rumbleMask = PADRead(statuses); uint32_t rumbleMask = PADRead(statuses);
// Wii Remotes reach the game through KPAD, not as GameCube pads. This also
// applies while input is blocked (overlay open) so the port does not flip
// between "connected" and "no controller" every time the overlay toggles.
WiiRemoteInput::HideRemotesFromPad(statuses, PAD_CHANMAX);
try { try {
for (uint32_t i = 0; i < PAD_CHANMAX; ++i) { for (uint32_t i = 0; i < PAD_CHANMAX; ++i) {
+24
View File
@@ -1,6 +1,7 @@
#include "hle_stubs.h" #include "hle_stubs.h"
#include "memory.h" #include "memory.h"
#include "hle/controller_status_contract.h" #include "hle/controller_status_contract.h"
#include "wii_remote_input.h"
#include <cstdint> #include <cstdint>
@@ -98,18 +99,41 @@ extern "C" int32_t WPADGetDataFormat_HLE(uint32_t chan)
} }
PPC_NATIVE_OVERRIDE(801C0B54, WPADGetDataFormat_HLE, int32_t, (uint32_t chan), (chan)); PPC_NATIVE_OVERRIDE(801C0B54, WPADGetDataFormat_HLE, int32_t, (uint32_t chan), (chan));
// WPADSetDataFormat: records the per-channel data format the game asked for.
extern "C" int32_t WPADSetDataFormat_HLE(uint32_t chan, int32_t format) extern "C" int32_t WPADSetDataFormat_HLE(uint32_t chan, int32_t format)
{ {
return g_state.contract.SetDataFormat(chan, format); return g_state.contract.SetDataFormat(chan, format);
} }
PPC_NATIVE_OVERRIDE(801C0B9C, WPADSetDataFormat_HLE, int32_t, (uint32_t chan, int32_t format), (chan, format)); PPC_NATIVE_OVERRIDE(801C0B9C, WPADSetDataFormat_HLE, int32_t, (uint32_t chan, int32_t format), (chan, format));
// WPADProbe: reports the extension type of a Bluetooth remote on `chan`, or no controller.
extern "C" int32_t WPADProbe_HLE(uint32_t chan, uint32_t typePtr) extern "C" int32_t WPADProbe_HLE(uint32_t chan, uint32_t typePtr)
{ {
if (chan >= WpadContract::kChannelCount) { if (chan >= WpadContract::kChannelCount) {
return WpadContract::kErrorBadChannel; return WpadContract::kErrorBadChannel;
} }
// Drive the rescan state machine here too: a reconnect probe can arrive
// before the next PADRead, and only Poll() brings a dropped remote back.
WiiRemoteInput::Poll();
// A real Bluetooth remote: WPAD_DEV_CORE (0) for a bare remote,
// WPAD_DEV_FREESTYLE (1) with a Nunchuk, WPAD_DEV_CLASSIC (2) with a Classic
// Controller. The game reads the type from here (not from
// KPADStatus.dev_type) to pick its control scheme, and re-reads it when it
// changes, which is what makes an extension swap mid-game work like on the
// console. EffectiveKind keeps the last type through SDL's re-creation of
// the joystick after a swap.
const WiiRemoteInput::Kind kind = WiiRemoteInput::EffectiveKind(chan);
if (WiiRemoteInput::IsRemoteChannel(chan)) {
if (typePtr != 0) {
uint32_t type = WpadContract::kExtensionCore;
if (kind == WiiRemoteInput::Kind::RemoteWithNunchuk) type = 1u;
if (kind == WiiRemoteInput::Kind::RemoteWithClassic) type = 2u;
Memory::Write32(typePtr, type);
}
return kStatusOk;
}
if (typePtr != 0) { if (typePtr != 0) {
Memory::Write32(typePtr, WpadContract::kExtensionCore); Memory::Write32(typePtr, WpadContract::kExtensionCore);
} }
+18 -6
View File
@@ -270,7 +270,13 @@ static int32_t EnsureSslCredentials(SslSession& ssl) {
SCHANNEL_CRED cred{}; SCHANNEL_CRED cred{};
cred.dwVersion = SCHANNEL_CRED_VERSION; cred.dwVersion = SCHANNEL_CRED_VERSION;
cred.dwFlags = SCH_USE_STRONG_CRYPTO | SCH_CRED_NO_DEFAULT_CREDS | SCH_CRED_MANUAL_CRED_VALIDATION; // Leave certificate validation to Schannel. SCH_CRED_MANUAL_CRED_VALIDATION
// suppresses that validation and requires an explicit CertGetCertificateChain
// implementation, which this HLE does not provide. The target hostname passed
// to InitializeSecurityContextA below is therefore checked together with the
// server certificate chain.
cred.dwFlags = SCH_USE_STRONG_CRYPTO | SCH_CRED_NO_DEFAULT_CREDS |
SCH_CRED_AUTO_CRED_VALIDATION;
TimeStamp expiry{}; TimeStamp expiry{};
const SECURITY_STATUS status = AcquireCredentialsHandleA( const SECURITY_STATUS status = AcquireCredentialsHandleA(
@@ -289,6 +295,13 @@ static int32_t SslHandshakeImpl(SslSession& ssl) {
return SSL_OK; return SSL_OK;
} }
// Schannel can authenticate a certificate chain without authenticating a
// server identity when no target name is supplied. Refuse that ambiguous
// mode rather than accepting a certificate for an unrelated endpoint.
if (ssl.hostname.empty()) {
return SSL_ERR_VCOMMONNAME;
}
const int32_t credRet = EnsureSslCredentials(ssl); const int32_t credRet = EnsureSslCredentials(ssl);
if (credRet != SSL_OK) { if (credRet != SSL_OK) {
return credRet; return credRet;
@@ -331,7 +344,7 @@ static int32_t SslHandshakeImpl(SslSession& ssl) {
const SECURITY_STATUS status = InitializeSecurityContextA( const SECURITY_STATUS status = InitializeSecurityContextA(
&ssl.cred, ssl.haveContext ? &ssl.context : nullptr, &ssl.cred, ssl.haveContext ? &ssl.context : nullptr,
ssl.hostname.empty() ? nullptr : const_cast<char*>(ssl.hostname.c_str()), flags, 0, SECURITY_NATIVE_DREP, const_cast<char*>(ssl.hostname.c_str()), flags, 0, SECURITY_NATIVE_DREP,
inDescPtr, 0, &ssl.context, &outDesc, &attrs, &expiry); inDescPtr, 0, &ssl.context, &outDesc, &attrs, &expiry);
if (status != SEC_E_INVALID_HANDLE) { if (status != SEC_E_INVALID_HANDLE) {
ssl.haveContext = true; ssl.haveContext = true;
@@ -567,10 +580,9 @@ int32_t HandleSslIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const std
switch (cmd) { switch (cmd) {
case IOCTLV_NET_SSL_NEW: { case IOCTLV_NET_SSL_NEW: {
// out[0] carries the verify option. Its value is not consulted - every // out[0] carries the guest verify option. Host TLS verification is
// certificate check is delegated to Schannel with // always enforced by Schannel, but still read this value so a request
// SCH_CRED_MANUAL_CRED_VALIDATION - but it is still read so that a // pointing outside guest memory faults here as it always has.
// request pointing outside guest memory faults here as it always has.
if (!out.empty() && out[0].address && out[0].size >= 4) { if (!out.empty() && out[0].address && out[0].size >= 4) {
(void)Memory::Read32(out[0].address); (void)Memory::Read32(out[0].address);
} }
+63 -2
View File
@@ -4,6 +4,7 @@
#include "nand_internal.h" #include "nand_internal.h"
#include "discord_presence.h"
#include "runtime_log.h" #include "runtime_log.h"
extern "C" void OSSleepThread_HLE_801aa9b8(CpuContext* ctx); extern "C" void OSSleepThread_HLE_801aa9b8(CpuContext* ctx);
@@ -201,9 +202,69 @@ static int32_t HandleDolphinIoctlv(uint32_t cmd, uint32_t numIn, uint32_t numOut
} }
case DOLPHIN_IOCTL_SET_SPEED_LIMIT: case DOLPHIN_IOCTL_SET_SPEED_LIMIT:
case DOLPHIN_IOCTL_DISCORD_SET_CLIENT: return ISFS_OK;
case DOLPHIN_IOCTL_DISCORD_SET_PRESENCE:
case DOLPHIN_IOCTL_DISCORD_SET_CLIENT: {
if (numIn != 1 || numOut != 0 || vectorPtr == 0) {
return ISFS_EINVAL;
}
const IosVector client = ReadIosVector(vectorPtr, 0);
if (!IsValidGuestRange(client.address, client.size)) {
return ISFS_EINVAL;
}
if (RuntimeConfigFile::DiscordPresenceEnabled()) {
DiscordPresence::SetClient(ReadGuestCString(client.address, client.size));
}
return ISFS_OK;
}
case DOLPHIN_IOCTL_DISCORD_SET_PRESENCE: {
if (numIn != 10 || numOut != 0 || vectorPtr == 0) {
return ISFS_EINVAL;
}
std::array<IosVector, 10> values{};
for (uint32_t index = 0; index < values.size(); ++index) {
values[index] = ReadIosVector(vectorPtr, index);
if (!IsValidGuestRange(values[index].address, values[index].size)) {
return ISFS_EINVAL;
}
}
if (RuntimeConfigFile::DiscordPresenceEnabled()) {
DiscordPresence::Activity activity;
activity.details = ReadGuestCString(values[0].address, values[0].size);
activity.state = ReadGuestCString(values[1].address, values[1].size);
activity.largeImageKey = ReadGuestCString(values[2].address, values[2].size);
activity.largeImageText = ReadGuestCString(values[3].address, values[3].size);
activity.smallImageKey = ReadGuestCString(values[4].address, values[4].size);
activity.smallImageText = ReadGuestCString(values[5].address, values[5].size);
if (values[6].size >= 8 && Memory::Contains(values[6].address, 8)) {
activity.startTimestamp = static_cast<int64_t>(
(static_cast<uint64_t>(Memory::Read32(values[6].address)) << 32) |
Memory::Read32(values[6].address + 4));
}
if (values[7].size >= 8 && Memory::Contains(values[7].address, 8)) {
activity.endTimestamp = static_cast<int64_t>(
(static_cast<uint64_t>(Memory::Read32(values[7].address)) << 32) |
Memory::Read32(values[7].address + 4));
}
if (values[8].size >= 4) {
activity.partySize = Memory::Read32(values[8].address);
}
if (values[9].size >= 4) {
activity.partyMax = Memory::Read32(values[9].address);
}
DiscordPresence::SetActivity(std::move(activity));
}
return ISFS_OK;
}
case DOLPHIN_IOCTL_DISCORD_RESET: case DOLPHIN_IOCTL_DISCORD_RESET:
if (numIn != 0 || numOut != 0) {
return ISFS_EINVAL;
}
if (RuntimeConfigFile::DiscordPresenceEnabled()) {
DiscordPresence::Reset();
}
return ISFS_OK; return ISFS_OK;
case DOLPHIN_IOCTL_GET_SYSTEM_TIME: { case DOLPHIN_IOCTL_GET_SYSTEM_TIME: {
+3 -5
View File
@@ -6,6 +6,7 @@
#include "aurora_events.h" #include "aurora_events.h"
#include "settings_overlay.h" #include "settings_overlay.h"
#include "fiber_manager.h" #include "fiber_manager.h"
#include "platform/host_platform.h"
#include "runtime_log.h" #include "runtime_log.h"
#include "vr/mkw_vr_policy.h" #include "vr/mkw_vr_policy.h"
#include "vr/openxr_integration.h" #include "vr/openxr_integration.h"
@@ -22,18 +23,15 @@
#include <mutex> #include <mutex>
#include <thread> #include <thread>
#include <aurora/aurora.h>
#if defined(_WIN32) #if defined(_WIN32)
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN #ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#endif #endif
#include <windows.h> #include <windows.h>
#endif #endif
#include <aurora/aurora.h>
// Forward declaration for OSWakeupThread - used to wake threads on VI retrace queue // Forward declaration for OSWakeupThread - used to wake threads on VI retrace queue
extern "C" void OSWakeupThread_HLE_801aaaa4(CpuContext* ctx); extern "C" void OSWakeupThread_HLE_801aaaa4(CpuContext* ctx);
+270
View File
@@ -0,0 +1,270 @@
#include "host_context.h"
#if defined(_WIN32)
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#elif defined(__APPLE__) && defined(__aarch64__)
#include <sys/mman.h>
#include <unistd.h>
extern "C" void mkw_co_switch(void** targetSp, void** sourceSp);
extern "C" void* mkw_co_init(void* stackTop, void (*entry)(void*), void* argument);
#elif defined(__linux__)
#include <libco.h>
#include <cstdlib>
#include <unordered_map>
#else
#error "HostContext needs a supported cooperative-context backend"
#endif
namespace HostContext {
#if defined(_WIN32)
namespace {
thread_local bool g_convertedScheduler = false;
}
bool InitializeScheduler(Handle* scheduler)
{
void* context = ConvertThreadToFiber(nullptr);
g_convertedScheduler = context != nullptr;
if (!context) {
context = GetCurrentFiber();
}
*scheduler = context;
return context != nullptr;
}
void ShutdownScheduler(Handle scheduler)
{
if (scheduler && g_convertedScheduler) {
ConvertFiberToThread();
}
g_convertedScheduler = false;
}
Handle Create(std::size_t stackSize, Entry entry, void* argument)
{
return CreateFiber(stackSize, entry, argument);
}
void Destroy(Handle context)
{
if (context) {
DeleteFiber(context);
}
}
bool IsCurrent(Handle context)
{
return context != nullptr && GetCurrentFiber() == context;
}
void Switch(Handle target)
{
SwitchToFiber(target);
}
#elif defined(__APPLE__) && defined(__aarch64__)
namespace {
struct Context {
void* savedStackPointer = nullptr;
void* stack = nullptr;
std::size_t stackSize = 0;
};
// Guest scheduling is confined to the initialized main host thread. Keeping
// this as ordinary process state also avoids relying on Darwin TLS internals
// while executing on a manually managed stack.
Context* g_current = nullptr;
}
bool InitializeScheduler(Handle* scheduler)
{
auto* context = new Context();
g_current = context;
*scheduler = context;
return true;
}
void ShutdownScheduler(Handle scheduler)
{
auto* context = static_cast<Context*>(scheduler);
if (g_current == context) {
g_current = nullptr;
}
delete context;
}
Handle Create(std::size_t stackSize, Entry entry, void* argument)
{
auto* context = new Context();
const std::size_t guardSize = static_cast<std::size_t>(getpagesize());
const std::size_t totalSize = stackSize + guardSize;
context->stack = mmap(nullptr, totalSize, PROT_READ | PROT_WRITE,
MAP_ANON | MAP_PRIVATE, -1, 0);
if (context->stack == MAP_FAILED) {
delete context;
return nullptr;
}
// Fault on stack overflow instead of corrupting the preceding mapping.
if (mprotect(context->stack, guardSize, PROT_NONE) != 0) {
munmap(context->stack, totalSize);
delete context;
return nullptr;
}
context->stackSize = totalSize;
auto* stackTop = static_cast<char*>(context->stack) + totalSize;
context->savedStackPointer = mkw_co_init(stackTop, entry, argument);
return context;
}
void Destroy(Handle context)
{
auto* nativeContext = static_cast<Context*>(context);
if (!nativeContext) {
return;
}
if (nativeContext->stack) {
munmap(nativeContext->stack, nativeContext->stackSize);
}
delete nativeContext;
}
bool IsCurrent(Handle context)
{
return context != nullptr && context == g_current;
}
void Switch(Handle target)
{
auto* destination = static_cast<Context*>(target);
Context* source = g_current;
if (!destination || destination == source) {
return;
}
g_current = destination;
mkw_co_switch(&destination->savedStackPointer, &source->savedStackPointer);
g_current = source;
}
#elif defined(__linux__)
namespace {
struct Context {
cothread_t native = nullptr;
Entry entry = nullptr;
void* argument = nullptr;
bool ownsNative = false;
};
thread_local Context* g_current = nullptr;
thread_local std::unordered_map<cothread_t, Context*> g_contexts;
void ContextEntry()
{
const auto found = g_contexts.find(co_active());
if (found == g_contexts.end() || !found->second || !found->second->entry) {
std::abort();
}
Context* context = found->second;
g_current = context;
context->entry(context->argument);
// A guest fiber must return through FiberProc's scheduler handoff. There
// is no valid native caller to return to from libco's entry trampoline.
std::abort();
}
} // namespace
bool InitializeScheduler(Handle* scheduler)
{
auto* context = new Context();
context->native = co_active();
if (!context->native) {
delete context;
return false;
}
g_current = context;
g_contexts.emplace(context->native, context);
*scheduler = context;
return true;
}
void ShutdownScheduler(Handle scheduler)
{
auto* context = static_cast<Context*>(scheduler);
if (!context) {
return;
}
g_contexts.erase(context->native);
if (g_current == context) {
g_current = nullptr;
}
delete context;
}
Handle Create(std::size_t stackSize, Entry entry, void* argument)
{
auto* context = new Context();
context->entry = entry;
context->argument = argument;
context->native = co_create(static_cast<unsigned int>(stackSize), ContextEntry);
context->ownsNative = context->native != nullptr;
if (!context->native) {
delete context;
return nullptr;
}
g_contexts.emplace(context->native, context);
return context;
}
void Destroy(Handle context)
{
auto* nativeContext = static_cast<Context*>(context);
if (!nativeContext) {
return;
}
g_contexts.erase(nativeContext->native);
if (nativeContext->ownsNative) {
co_delete(nativeContext->native);
}
delete nativeContext;
}
bool IsCurrent(Handle context)
{
return context != nullptr && context == g_current;
}
void Switch(Handle target)
{
auto* destination = static_cast<Context*>(target);
Context* source = g_current;
if (!destination || destination == source) {
return;
}
g_current = destination;
co_switch(destination->native);
g_current = source;
}
#endif
} // namespace HostContext
+60 -2
View File
@@ -23,6 +23,10 @@
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#if !defined(_WIN32)
#include <unistd.h>
#endif
#if defined(_WIN32) #if defined(_WIN32)
#ifndef NOMINMAX #ifndef NOMINMAX
#define NOMINMAX #define NOMINMAX
@@ -38,7 +42,12 @@
#include <dbghelp.h> #include <dbghelp.h>
#else #else
#include <signal.h> #include <signal.h>
#if defined(__x86_64__)
// Only the x86 POSIX fault path inspects ucontext_t to recover the page-fault
// write bit. macOS deprecates ucontext and requires _XOPEN_SOURCE just to
// include the header, while the arm64 handler does not use it at all.
#include <ucontext.h> #include <ucontext.h>
#endif
#include <unistd.h> #include <unistd.h>
#endif #endif
@@ -49,6 +58,8 @@
#include "system_bridge.h" #include "system_bridge.h"
#include "ppc_runtime.h" #include "ppc_runtime.h"
#include "aurora_events.h" #include "aurora_events.h"
#include "wii_remote_input.h"
#include "discord_presence.h"
#include "fiber_manager.h" #include "fiber_manager.h"
#include "hle_stubs.h" #include "hle_stubs.h"
#include "runtime_config.h" #include "runtime_config.h"
@@ -922,6 +933,7 @@ constexpr DWORD kCppExceptionCodeMsvc = 0xE06D7363;
// AddressSanitizer uses STATUS_FATAL_APP_EXIT when it detects an error and wants to report it. // AddressSanitizer uses STATUS_FATAL_APP_EXIT when it detects an error and wants to report it.
// We must let ASan's handler run so it can print file/line information. // We must let ASan's handler run so it can print file/line information.
constexpr DWORD kAsanFatalAppExit = 0x40000015; // STATUS_FATAL_APP_EXIT constexpr DWORD kAsanFatalAppExit = 0x40000015; // STATUS_FATAL_APP_EXIT
LONG ReportFatalSehAndExit(EXCEPTION_POINTERS* info);
void ReportStructuredException(EXCEPTION_POINTERS* info) { void ReportStructuredException(EXCEPTION_POINTERS* info) {
if (!info || !info->ExceptionRecord) { if (!info || !info->ExceptionRecord) {
@@ -1023,13 +1035,34 @@ LONG CALLBACK SehLogger(EXCEPTION_POINTERS* info) {
info->ExceptionRecord->ExceptionCode == kAsanFatalAppExit) { // ASan reporting - let it print first info->ExceptionRecord->ExceptionCode == kAsanFatalAppExit) { // ASan reporting - let it print first
return EXCEPTION_CONTINUE_SEARCH; return EXCEPTION_CONTINUE_SEARCH;
} }
// Software-raised exceptions (customer bit set) are used for internal control flow by
// system DLLs (e.g. msxml6 while mscms parses a display colour profile) and are caught
// by their own frame handlers. Only hardware faults are fatal at first chance; anything
// else that truly goes unhandled reaches UnhandledSehFilter.
if ((info->ExceptionRecord->ExceptionCode & 0x20000000u) != 0) {
return EXCEPTION_CONTINUE_SEARCH;
}
return ReportFatalSehAndExit(info);
}
LONG WINAPI UnhandledSehFilter(EXCEPTION_POINTERS* info) {
if (info == nullptr || info->ExceptionRecord == nullptr) {
return EXCEPTION_CONTINUE_SEARCH;
}
const DWORD code = info->ExceptionRecord->ExceptionCode;
if (code == kCppExceptionCodeGcc || code == kCppExceptionCodeMsvc || code == kAsanFatalAppExit) {
return EXCEPTION_CONTINUE_SEARCH;
}
return ReportFatalSehAndExit(info);
}
LONG ReportFatalSehAndExit(EXCEPTION_POINTERS* info) {
// Guard against re-entrancy: if we crash while reporting, don't recurse // Guard against re-entrancy: if we crash while reporting, don't recurse
static std::atomic_flag s_inCrashHandler = ATOMIC_FLAG_INIT; static std::atomic_flag s_inCrashHandler = ATOMIC_FLAG_INIT;
if (s_inCrashHandler.test_and_set()) { if (s_inCrashHandler.test_and_set()) {
std::_Exit(EXIT_FAILURE); std::_Exit(EXIT_FAILURE);
} }
// Report the structured exception with detailed information // Report the structured exception with detailed information
ReportStructuredException(info); ReportStructuredException(info);
const auto* record = info->ExceptionRecord; const auto* record = info->ExceptionRecord;
@@ -1064,6 +1097,7 @@ LONG CALLBACK SehLogger(EXCEPTION_POINTERS* info) {
void InstallSehLogger() { void InstallSehLogger() {
if (!g_vectoredSehHandle) { if (!g_vectoredSehHandle) {
g_vectoredSehHandle = AddVectoredExceptionHandler(1, SehLogger); g_vectoredSehHandle = AddVectoredExceptionHandler(1, SehLogger);
SetUnhandledExceptionFilter(UnhandledSehFilter);
} }
} }
#else #else
@@ -1269,6 +1303,7 @@ static void TerminateHandler() {
std::_Exit(EXIT_FAILURE); std::_Exit(EXIT_FAILURE);
} }
// Runtime entry point: loads the configuration, brings up aurora and runs the game.
int RuntimeMain(int argc, char** argv) { int RuntimeMain(int argc, char** argv) {
// Must run before the transcript duplicates stdout/stderr: it decides what // Must run before the transcript duplicates stdout/stderr: it decides what
// those descriptors are mirrored to now that the products are GUI-subsystem. // those descriptors are mirrored to now that the products are GUI-subsystem.
@@ -1293,6 +1328,9 @@ int RuntimeMain(int argc, char** argv) {
throw std::invalid_argument("The game runtime does not accept command-line options; use Config.toml through the installed host."); throw std::invalid_argument("The game runtime does not accept command-line options; use Config.toml through the installed host.");
} }
RuntimeConfigFile::LogLoadedConfig(); RuntimeConfigFile::LogLoadedConfig();
if (RuntimeConfigFile::DiscordPresenceEnabled()) {
DiscordPresence::Initialize(RuntimeConfigFile::DiscordClientId(), "Mario Kart Wii");
}
SystemBridge::Initialize(); SystemBridge::Initialize();
TranslatedFunctionRegistry::Finalize(); TranslatedFunctionRegistry::Finalize();
@@ -1344,9 +1382,21 @@ int RuntimeMain(int argc, char** argv) {
const char* configName; const char* configName;
AuroraBackend backend; AuroraBackend backend;
}; };
#if defined(__APPLE__)
static constexpr std::array<GraphicsBackendEntry, 2> kGraphicsBackends{{
{"auto", BACKEND_AUTO}, {"metal", BACKEND_METAL},
}};
// only vulkan for linux
#elif defined(__linux__)
static constexpr std::array<GraphicsBackendEntry, 2> kGraphicsBackends{{
{"auto", BACKEND_AUTO}, {"vulkan", BACKEND_VULKAN},
}};
#elif defined(_WIN32)
static constexpr std::array<GraphicsBackendEntry, 3> kGraphicsBackends{{ static constexpr std::array<GraphicsBackendEntry, 3> kGraphicsBackends{{
{"auto", BACKEND_AUTO}, {"d3d12", BACKEND_D3D12}, {"vulkan", BACKEND_VULKAN}, {"auto", BACKEND_AUTO}, {"d3d12", BACKEND_D3D12}, {"vulkan", BACKEND_VULKAN},
}}; }};
#endif
const auto backendDisplayName = [](AuroraBackend value) -> const char* { const auto backendDisplayName = [](AuroraBackend value) -> const char* {
for (const auto& entry : kGraphicsBackends) { for (const auto& entry : kGraphicsBackends) {
if (entry.backend == value) { if (entry.backend == value) {
@@ -1375,6 +1425,11 @@ int RuntimeMain(int argc, char** argv) {
} }
const AuroraBackend requestedBackend = auroraConfig.desiredBackend; const AuroraBackend requestedBackend = auroraConfig.desiredBackend;
// SDL only reads its Wii driver hint when the joystick subsystem starts, which
// aurora_initialize does; a Bluetooth Wii Remote paired before launch must be
// visible on that first scan.
WiiRemoteInput::ConfigureSdlHints(RuntimeConfigFile::WiiRemotesEnabled(true));
const AuroraInfo auroraInfo = aurora_initialize(0, nullptr, &auroraConfig); const AuroraInfo auroraInfo = aurora_initialize(0, nullptr, &auroraConfig);
if (requestedBackend != BACKEND_AUTO && auroraInfo.backend != requestedBackend) { if (requestedBackend != BACKEND_AUTO && auroraInfo.backend != requestedBackend) {
RT_LOG(RT_TAG_RUNTIME) << "graphics_api=\"" << backend RT_LOG(RT_TAG_RUNTIME) << "graphics_api=\"" << backend
@@ -1441,6 +1496,7 @@ int RuntimeMain(int argc, char** argv) {
WindowPlacementPersistence::Flush(true); WindowPlacementPersistence::Flush(true);
mkw::vr::OpenXRShutdownBeforeAurora(); mkw::vr::OpenXRShutdownBeforeAurora();
aurora_shutdown(); aurora_shutdown();
DiscordPresence::Shutdown();
SetRuntimeExitCodeImpl(0); SetRuntimeExitCodeImpl(0);
ShutdownProcessTranscript(); ShutdownProcessTranscript();
return 0; return 0;
@@ -1459,6 +1515,7 @@ int RuntimeMain(int argc, char** argv) {
WindowPlacementPersistence::Flush(true); WindowPlacementPersistence::Flush(true);
mkw::vr::OpenXRShutdownBeforeAurora(); mkw::vr::OpenXRShutdownBeforeAurora();
aurora_shutdown(); aurora_shutdown();
DiscordPresence::Shutdown();
ShutdownProcessTranscript(); ShutdownProcessTranscript();
return 1; return 1;
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
@@ -1471,6 +1528,7 @@ int RuntimeMain(int argc, char** argv) {
WindowPlacementPersistence::Flush(true); WindowPlacementPersistence::Flush(true);
mkw::vr::OpenXRShutdownBeforeAurora(); mkw::vr::OpenXRShutdownBeforeAurora();
aurora_shutdown(); aurora_shutdown();
DiscordPresence::Shutdown();
ShutdownProcessTranscript(); ShutdownProcessTranscript();
return 1; return 1;
} }
+85
View File
@@ -0,0 +1,85 @@
#include "platform/host_platform.h"
#include <cstdlib>
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <shlobj.h>
#else
#include <unistd.h>
#endif
#if defined(__APPLE__)
#include <mach-o/dyld.h>
#include <pwd.h>
#endif
namespace RuntimePlatform {
std::optional<std::filesystem::path> ExecutableDirectory() noexcept {
#if defined(_WIN32)
std::wstring buffer(MAX_PATH, L'\0');
for (;;) {
const DWORD length = GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
if (length == 0) {
return std::nullopt;
}
if (length < buffer.size() - 1) {
buffer.resize(length);
return std::filesystem::path(buffer).parent_path();
}
buffer.resize(buffer.size() * 2);
}
#elif defined(__APPLE__)
uint32_t size = 0;
if (_NSGetExecutablePath(nullptr, &size) != -1 || size == 0) {
return std::nullopt;
}
std::string path(size, '\0');
if (_NSGetExecutablePath(path.data(), &size) != 0) {
return std::nullopt;
}
path.resize(std::char_traits<char>::length(path.c_str()));
std::error_code ec;
const auto resolved = std::filesystem::weakly_canonical(path, ec);
return (ec ? std::filesystem::path(path) : resolved).parent_path();
#else
return std::nullopt;
#endif
}
std::filesystem::path ApplicationDataDirectory(std::string_view applicationName) {
#if defined(_WIN32)
PWSTR rawPath = nullptr;
if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_CREATE, nullptr, &rawPath)) && rawPath) {
const std::filesystem::path directory = std::filesystem::path(rawPath) / applicationName;
CoTaskMemFree(rawPath);
return directory;
}
#elif defined(__APPLE__)
if (const char* home = std::getenv("HOME"); home && *home) {
return std::filesystem::path(home) / "Library" / "Application Support" / applicationName;
}
if (const passwd* user = getpwuid(getuid()); user && user->pw_dir && *user->pw_dir) {
return std::filesystem::path(user->pw_dir) / "Library" / "Application Support" / applicationName;
}
#endif
return std::filesystem::current_path() / applicationName;
}
std::filesystem::path LogDirectory(std::string_view applicationName) {
return ApplicationDataDirectory(applicationName) / "Logs";
}
uint64_t CurrentProcessId() noexcept {
#if defined(_WIN32)
return static_cast<uint64_t>(::GetCurrentProcessId());
#else
return static_cast<uint64_t>(::getpid());
#endif
}
} // namespace RuntimePlatform
+59
View File
@@ -0,0 +1,59 @@
.text
.align 2
// AArch64 Darwin cooperative context frame (240 bytes): x18-x30, then v8-v15.
// x18 is platform-reserved on Darwin and is needed by code that accesses TLS.
// x0 = address holding the target frame pointer; x1 = address to receive the
// current frame pointer. This is intentionally leaf-only: it never calls C++.
.globl _mkw_co_switch
_mkw_co_switch:
sub sp, sp, #240
str x18, [sp, #0]
stp x19, x20, [sp, #16]
stp x21, x22, [sp, #32]
stp x23, x24, [sp, #48]
stp x25, x26, [sp, #64]
stp x27, x28, [sp, #80]
stp x29, x30, [sp, #96]
stp q8, q9, [sp, #112]
stp q10, q11, [sp, #144]
stp q12, q13, [sp, #176]
stp q14, q15, [sp, #208]
mov x2, sp
str x2, [x1]
ldr x2, [x0]
mov sp, x2
ldr x18, [sp, #0]
ldp x19, x20, [sp, #16]
ldp x21, x22, [sp, #32]
ldp x23, x24, [sp, #48]
ldp x25, x26, [sp, #64]
ldp x27, x28, [sp, #80]
ldp x29, x30, [sp, #96]
ldp q8, q9, [sp, #112]
ldp q10, q11, [sp, #144]
ldp q12, q13, [sp, #176]
ldp q14, q15, [sp, #208]
add sp, sp, #240
ret
// Creates a frame compatible with mkw_co_switch and returns its saved SP.
// x0 = one-past-end stack pointer, x1 = entry(void*), x2 = entry argument.
.globl _mkw_co_init
_mkw_co_init:
bic x0, x0, #0xf
sub x0, x0, #240
str x18, [x0, #0] // Darwin platform register / TLS base
str x1, [x0, #16] // x19: entry
str x2, [x0, #24] // x20: argument
str xzr, [x0, #96] // x29
adrp x3, _mkw_co_entry_trampoline@PAGE
add x3, x3, _mkw_co_entry_trampoline@PAGEOFF
str x3, [x0, #104] // x30
ret
_mkw_co_entry_trampoline:
mov x0, x20
blr x19
brk #0
+151
View File
@@ -5,6 +5,7 @@
#include "music_attenuation.h" #include "music_attenuation.h"
#include "runtime_config.h" #include "runtime_config.h"
#include "runtime_log.h" #include "runtime_log.h"
#include "wii_remote_input.h"
#include <imgui.h> #include <imgui.h>
#include <SDL3/SDL_events.h> #include <SDL3/SDL_events.h>
@@ -282,6 +283,15 @@ void ApplyConfiguredMappings() {
if (controllerIndex < 0) { if (controllerIndex < 0) {
continue; continue;
} }
// The [controller] bindings are positional and shared by every port, so
// they describe whatever pad the user set them up with (usually an Xbox
// layout: a = south). A Wii U Pro Controller has a fixed, known layout
// (A on the east position) that aurora already maps by name; applying
// the shared bindings on top swaps A/B and X/Y. (Wii Remotes with any
// extension never reach the PAD layer: the game reads them through KPAD.)
if (WiiRemoteInput::KindForPort(port) == WiiRemoteInput::Kind::WiiUPro) {
continue;
}
uint32_t count = 0; uint32_t count = 0;
if (PADGetButtonMappings(port, &count) == nullptr || count != PAD_BUTTON_COUNT) { if (PADGetButtonMappings(port, &count) == nullptr || count != PAD_BUTTON_COUNT) {
@@ -313,6 +323,141 @@ void ApplyConfiguredMappings() {
} }
} }
bool g_wiiRemotesEnabled = RuntimeConfigFile::WiiRemotesEnabled(true);
bool g_wiiContinuousScan = RuntimeConfigFile::WiiContinuousScanEnabled(true);
// Accelerometer readout and zero-point calibration for a bare remote / remote + Nunchuk.
void DrawWiiRemoteAccelerometer(uint32_t port) {
ImGui::SeparatorText("Accelerometer");
float sdlG[3] = {};
float kpad[3] = {};
if (WiiRemoteInput::ReadAccelDebug(port, sdlG, kpad)) {
ImGui::Text("KPAD acc: x %+.2f y %+.2f z %+.2f g", kpad[0], kpad[1], kpad[2]);
ImGui::TextDisabled("Flat, buttons up: (0, -1, 0). Sideways as a wheel: (1, 0, 0); z follows the turn.");
} else {
ImGui::TextDisabled("No accelerometer data yet.");
}
// SDL's read of the remote's calibration block often times out over Bluetooth
// and it falls back to a nominal zero point, leaving a small per-axis bias;
// measured here with the remote at rest.
if (WiiRemoteInput::IsAccelCalibrating()) {
ImGui::ProgressBar(WiiRemoteInput::AccelCalibrationProgress(), ImVec2(220.0f, 0.0f), "Hold still...");
} else if (ImGui::Button("Calibrate (remote lying flat, buttons up)")) {
WiiRemoteInput::StartAccelCalibration(port);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Put the remote down on a flat surface with the buttons facing up and do not touch it\n"
"for about two seconds. Corrects the steering offset of a remote held sideways.");
}
ImGui::SameLine();
ImGui::BeginDisabled(!RuntimeConfigFile::HasWiiAccelOffset() || WiiRemoteInput::IsAccelCalibrating());
if (ImGui::Button("Clear calibration")) {
WiiRemoteInput::ClearAccelCalibration();
}
ImGui::EndDisabled();
if (const char* message = WiiRemoteInput::AccelCalibrationMessage()) {
ImGui::TextWrapped("%s", message);
} else if (RuntimeConfigFile::HasWiiAccelOffset()) {
const std::array<double, 3> offset = RuntimeConfigFile::WiiAccelOffset();
ImGui::TextDisabled("Stored offset: x %+.3f y %+.3f z %+.3f g", offset[0], offset[1], offset[2]);
} else {
ImGui::TextDisabled("Not calibrated (using SDL's zero point; see console.log for \"fallback accelerometer calibration\").");
}
}
// Wii Remotes (Bluetooth) menu: driver switch, pairing help, continuous scanning and the port's controller kind.
void DrawWiiRemoteSettings(uint32_t selectedGamePort) {
if (!ImGui::BeginMenu("Wii Remotes (Bluetooth)")) {
return;
}
if (ImGui::Checkbox("Use Wii Remotes / Wii U Pro Controllers", &g_wiiRemotesEnabled)) {
RuntimeConfigFile::SetWiiRemotesEnabled(g_wiiRemotesEnabled);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Takes effect on the next launch. Turn this off if you use a Mayflash DolphinBar.");
}
ImGui::TextDisabled("Pairing: Windows Settings > Bluetooth > Add device, then press 1+2");
ImGui::TextDisabled("(or the red SYNC button) on the remote. Leave the PIN empty.");
ImGui::TextDisabled("A remote that was paired before also needs to be turned on with 1+2/SYNC.");
if (ImGui::Checkbox("Keep scanning for Wii Remotes (like Dolphin's Continuous Scanning)",
&g_wiiContinuousScan)) {
RuntimeConfigFile::SetWiiContinuousScanEnabled(g_wiiContinuousScan);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("While no Wii controller is connected, re-check Bluetooth every 2 seconds so a\n"
"remote that dropped out (\"Communications with the controller have been\n"
"interrupted\") or was turned on after launch comes back by itself.");
}
// The driver hint is only read at launch, so a rescan after the user turned
// the setting off would still re-enumerate Wii devices in this session.
ImGui::BeginDisabled(!g_wiiRemotesEnabled);
if (ImGui::Button("Rescan now")) {
WiiRemoteInput::RescanNow();
}
ImGui::EndDisabled();
ImGui::SameLine();
if (WiiRemoteInput::IsScanning()) {
ImGui::TextDisabled("Scanning... (%u so far) - press 1+2 on the remote", WiiRemoteInput::ScanCount());
} else {
ImGui::TextDisabled("Not scanning");
}
ImGui::Separator();
const WiiRemoteInput::Kind kind = WiiRemoteInput::KindForPort(selectedGamePort);
ImGui::Text("Port %u: %s", static_cast<unsigned>(selectedGamePort + 1), WiiRemoteInput::KindLabel(kind));
if (kind == WiiRemoteInput::Kind::RemoteWithClassic) {
WiiRemoteInput::KpadSample sample;
if (WiiRemoteInput::ReadKpadSample(selectedGamePort, sample)) {
// WPAD_CL_BUTTON_* bits, in the game's own layout (no mapping involved).
const auto held = [&](uint32_t bit, const char* on, const char* off) { return (sample.clHold & bit) ? on : off; };
ImGui::Text("Classic: %s %s %s %s %s %s %s %s %s %s %s %s %s %s", held(0x0010, "A", "a"),
held(0x0040, "B", "b"), held(0x0008, "X", "x"), held(0x0020, "Y", "y"), held(0x2000, "L", "l"),
held(0x0200, "R", "r"), held(0x0080, "ZL", "zl"), held(0x0004, "ZR", "zr"),
held(0x0400, "PLUS", "plus"), held(0x1000, "MINUS", "minus"), held(0x0001, "UP", "up"),
held(0x4000, "DOWN", "down"), held(0x0002, "LEFT", "left"), held(0x8000, "RIGHT", "right"));
ImGui::Text("Sticks: L %+.2f %+.2f (WPAD %+d %+d) R %+.2f %+.2f (WPAD %+d %+d)", sample.clLStick[0],
sample.clLStick[1], static_cast<int>(sample.clLStickRaw[0]),
static_cast<int>(sample.clLStickRaw[1]), sample.clRStick[0], sample.clRStick[1],
static_cast<int>(sample.clRStickRaw[0]), static_cast<int>(sample.clRStickRaw[1]));
ImGui::TextDisabled("Capitals = held. The game reads this Classic Controller through KPAD, as on the");
ImGui::TextDisabled("console: its buttons mean what the game says they mean, no mapping applies.");
}
}
if (kind == WiiRemoteInput::Kind::WiiUPro) {
if (SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(selectedGamePort))) {
// SDL's Wii driver posts the D-pad as joystick buttons 11-14 (the
// SDL_GAMEPAD_BUTTON_DPAD_* values) while its default HIDAPI mapping
// expects a hat, so SDL_GetGamepadButton never sees them; read the
// joystick directly, like the fallback in aurora's PADRead does.
SDL_Joystick* joystick = SDL_GetGamepadJoystick(gamepad);
const auto rawButton = [&](int index) {
return joystick != nullptr && SDL_GetJoystickButton(joystick, index);
};
ImGui::Text("Raw D-pad: %s %s %s %s", rawButton(SDL_GAMEPAD_BUTTON_DPAD_UP) ? "UP" : "up",
rawButton(SDL_GAMEPAD_BUTTON_DPAD_DOWN) ? "DOWN" : "down",
rawButton(SDL_GAMEPAD_BUTTON_DPAD_LEFT) ? "LEFT" : "left",
rawButton(SDL_GAMEPAD_BUTTON_DPAD_RIGHT) ? "RIGHT" : "right");
ImGui::Text("Raw face buttons: %s %s %s %s", rawButton(SDL_GAMEPAD_BUTTON_EAST) ? "A" : "a",
rawButton(SDL_GAMEPAD_BUTTON_SOUTH) ? "B" : "b", rawButton(SDL_GAMEPAD_BUTTON_NORTH) ? "X" : "x",
rawButton(SDL_GAMEPAD_BUTTON_WEST) ? "Y" : "y");
ImGui::Text("Raw ZL/ZR: %d / %d (pressed above 0)",
SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER),
SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER));
ImGui::TextDisabled("Capitals = held. If a button never turns to capitals while physically held,");
ImGui::TextDisabled("that press is not reaching SDL at all (a driver-level issue, not a mapping one).");
ImGui::TextDisabled("This pad uses Nintendo's own layout (a/b/x/y as labelled); the shared");
ImGui::TextDisabled("button mapping above does not apply to it.");
}
}
if (kind == WiiRemoteInput::Kind::Remote || kind == WiiRemoteInput::Kind::RemoteWithNunchuk ||
kind == WiiRemoteInput::Kind::RemoteWithClassic) {
DrawWiiRemoteAccelerometer(selectedGamePort);
}
ImGui::EndMenu();
}
// Controller settings menu: port selection, controller assignment and button mapping.
void DrawControllerSettings() { void DrawControllerSettings() {
for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) { for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
const std::string label = "Port " + std::to_string(port + 1); const std::string label = "Port " + std::to_string(port + 1);
@@ -332,6 +477,7 @@ void DrawControllerSettings() {
} }
ImGui::Separator(); ImGui::Separator();
controller_mapping_wizard::DrawSetupList(); controller_mapping_wizard::DrawSetupList();
DrawWiiRemoteSettings(selectedGamePort);
const uint32_t controllerCount = PADCount(); const uint32_t controllerCount = PADCount();
if (controllerCount == 0) { if (controllerCount == 0) {
ImGui::TextDisabled("No controller connected"); ImGui::TextDisabled("No controller connected");
@@ -931,6 +1077,11 @@ void Draw() noexcept {
// Wait for the frame worker's DONE phase: it has replayed the previous frame's ImGui draw lists // Wait for the frame worker's DONE phase: it has replayed the previous frame's ImGui draw lists
// and started the next ImGui frame, so all overlay callers can now safely issue ImGui commands. // and started the next ImGui frame, so all overlay callers can now safely issue ImGui commands.
aurora_wait_for_frame_worker(); aurora_wait_for_frame_worker();
// Also drive the Wii Remote rescan from here: PADRead runs it too, but this
// runs once per presented frame whatever the game is doing (e.g. sitting in
// its "communications interrupted" prompt without polling pads). Same guest
// thread as PADRead, so no concurrent access to the scanner's state.
WiiRemoteInput::Poll();
ApplyConfiguredMappings(); ApplyConfiguredMappings();
PersistDisplayModeIfChanged(); PersistDisplayModeIfChanged();
UpdateCursorAutoHide(); UpdateCursorAutoHide();
+741
View File
@@ -0,0 +1,741 @@
#include "wii_remote_input.h"
#include "runtime_config.h"
#include "runtime_log.h"
#include <dolphin/pad.h>
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_hints.h>
#include <SDL3/SDL_log.h>
#include <SDL3/SDL_sensor.h>
#include <SDL3/SDL_timer.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <fstream>
namespace WiiRemoteInput {
namespace {
// Dolphin's continuous scanning polls Bluetooth about once a second; SDL's
// enumeration walks every HID device on the main thread, so stay a bit lazier.
constexpr uint64_t kScanIntervalMs = 2000;
// Right after a remote drops it is almost certainly still there, but the first
// re-open attempts tend to fail on timed-out reads, so retry quickly for a while.
constexpr uint64_t kFastScanIntervalMs = 500;
constexpr uint64_t kFastScanWindowMs = 15000;
// How long the Wii driver hint stays at "0" during a rescan; SDL applies hint
// changes on its next joystick update, once per pumped frame.
constexpr uint64_t kRescanDriverOffMs = 100;
constexpr float kStandardGravity = 9.80665f;
// SDL's Wii driver posts the remote's own buttons as raw joystick buttons
// starting at SDL_GAMEPAD_BUTTON_MISC1, in this order (SDL_hidapi_wii.c,
// EWiiButtons), whatever the extension.
enum RawWiiButton : int {
kRawA = SDL_GAMEPAD_BUTTON_MISC1,
kRawB,
kRawOne,
kRawTwo,
kRawPlus,
kRawMinus,
kRawHome,
kRawDpadUp,
kRawDpadDown,
kRawDpadLeft,
kRawDpadRight,
};
// WPAD_BUTTON_* bits as the game reads them from KPADStatus.hold.
constexpr uint32_t kWpadLeft = 0x0001, kWpadRight = 0x0002, kWpadDown = 0x0004, kWpadUp = 0x0008,
kWpadPlus = 0x0010, kWpadTwo = 0x0100, kWpadOne = 0x0200, kWpadB = 0x0400, kWpadA = 0x0800,
kWpadMinus = 0x1000, kWpadZ = 0x2000, kWpadC = 0x4000, kWpadHome = 0x8000;
// WPAD_CL_BUTTON_* bits (WPADCLStatus.clButton / KPADStatus.ex_status.cl.hold):
// the Classic Controller's two button bytes, inverted, high byte first.
constexpr uint32_t kClUp = 0x0001, kClLeft = 0x0002, kClZR = 0x0004, kClX = 0x0008, kClA = 0x0010, kClY = 0x0020,
kClB = 0x0040, kClZL = 0x0080, kClR = 0x0200, kClPlus = 0x0400, kClHome = 0x0800,
kClMinus = 0x1000, kClL = 0x2000, kClDown = 0x4000, kClRight = 0x8000;
// How long a vanished remote keeps its channel alive with neutral input. SDL's
// in-place reconnect after an extension change takes well under a second; a
// remote that is really gone shows up as disconnected after this.
constexpr uint64_t kExtensionSwapGraceMs = 3000;
// Rescanning closes and re-opens the Bluetooth HID handle, which some Windows
// stacks answer by dropping the link; leave SDL's own reconnect this long first.
constexpr uint64_t kScanStartDelayMs = 3000;
// Per-port memory of the last Wii controller seen there, for EffectiveKind.
struct PortMemory {
Kind lastKind = Kind::NotWii;
uint64_t lastSeenMs = 0;
};
std::array<PortMemory, PAD_MAX_CONTROLLERS> g_ports{};
bool g_wiiDriverEnabled = false;
uint64_t g_lastScanMs = 0;
uint32_t g_scanCount = 0;
bool g_scanning = false;
// Non-zero while a rescan has the Wii driver hint switched off (see RescanNow).
uint64_t g_driverOffSinceMs = 0;
// When the current scan started (last Wii controller seen).
uint64_t g_lostAtMs = 0;
// Instance ids whose accelerometers have been switched on. SDL keeps sensors
// off until asked and forgets that when the gamepad is closed, so a re-paired
// remote gets a fresh id and is enabled again.
std::array<SDL_JoystickID, PAD_MAX_CONTROLLERS> g_sensorsEnabledFor{};
// Zero-point correction subtracted from the remote's accelerometer, in g and in
// SDL's sensor frame; loaded from Config.toml on first use, replaced by a
// calibration run. The Nunchuk accelerometer is left uncorrected.
std::array<float, 3> g_accelOffset{};
bool g_accelOffsetLoaded = false;
// Frames sampled by a calibration run (about 1.5 s at 60 Hz) and how far a
// sample may stray from the first one before the run is declared "moved".
constexpr int kCalibrationSamples = 90;
constexpr float kCalibrationMaxDeviationG = 0.15f;
// |mean| outside this range means the remote was not at rest or the
// accelerometer is far off its nominal scale; either way the offset is useless.
constexpr float kCalibrationMinGravityG = 0.8f;
constexpr float kCalibrationMaxGravityG = 1.2f;
struct AccelCalibration {
bool active = false;
uint32_t chan = 0;
int count = 0;
double sum[3] = {};
float first[3] = {};
};
AccelCalibration g_calibration;
char g_calibrationMessage[160] = {};
// Last accepted KPAD acc per port, for the remote and for the Nunchuk. Reused on
// frames that bring no sample or a glitched one, so the game never sees a jump.
struct LastAcc {
bool valid = false;
float acc[3] = {0.0f, -1.0f, 0.0f};
};
std::array<LastAcc, PAD_MAX_CONTROLLERS> g_lastAcc{};
std::array<LastAcc, PAD_MAX_CONTROLLERS> g_lastNunchukAcc{};
// Any axis beyond this is not a reading the remote's +-3 g sensor can produce.
constexpr float kMaxPlausibleRemoteG = 4.0f;
// Case-sensitive substring test that tolerates a null name.
bool NameContains(const char* name, const char* needle) {
return name != nullptr && std::strstr(name, needle) != nullptr;
}
// Turns on the remote (and Nunchuk) accelerometers once per gamepad instance.
void EnsureSensors(SDL_Gamepad* gamepad, uint32_t port) {
const SDL_JoystickID id = SDL_GetGamepadID(gamepad);
if (g_sensorsEnabledFor[port] == id) {
return;
}
bool allEnabled = true;
for (SDL_SensorType sensor : {SDL_SENSOR_ACCEL, SDL_SENSOR_ACCEL_L}) {
if (SDL_GamepadHasSensor(gamepad, sensor) && !SDL_SetGamepadSensorEnabled(gamepad, sensor, true)) {
RT_LOG(RT_TAG_CONFIG) << "Wii Remote on port " << (port + 1)
<< ": could not enable an accelerometer: " << SDL_GetError() << std::endl;
allEnabled = false;
}
}
// Only remember the instance once every sensor is on, so a failed attempt
// is retried on the next sample instead of leaving the accelerometer off.
if (allEnabled) {
g_sensorsEnabledFor[port] = id;
}
}
// Loads the stored zero-point correction once.
const std::array<float, 3>& AccelOffset() {
if (!g_accelOffsetLoaded) {
const std::array<double, 3> stored = RuntimeConfigFile::WiiAccelOffset();
for (size_t i = 0; i < 3; ++i) g_accelOffset[i] = static_cast<float>(stored[i]);
g_accelOffsetLoaded = true;
}
return g_accelOffset;
}
// Raw SDL sample in m/s^2, rejecting anything SDL has not delivered yet.
bool ReadSdlAccel(SDL_Gamepad* gamepad, SDL_SensorType sensor, float* sdl) {
return SDL_GamepadSensorEnabled(gamepad, sensor) && SDL_GetGamepadSensorData(gamepad, sensor, sdl, 3) &&
std::isfinite(sdl[0]) && std::isfinite(sdl[1]) && std::isfinite(sdl[2]);
}
// True for a sample that cannot have come from the sensor. Over Bluetooth on
// Windows the remote delivers, a few times a minute, a report whose
// accelerometer bytes are all zero; SDL decodes that as -0x200 on every axis,
// i.e. (+5.12, -5.12, -5.12) g for the remote (100 units/g) and
// (+2.56, -2.56, -2.56) g for the Nunchuk (200 units/g). Handed to the game as
// is, one such frame is a full-lock steer plus a 9 g "shake". `g` is the
// uncorrected SDL sample.
bool IsGlitchedSample(SDL_SensorType sensor, const float* g) {
// An exact zero vector is SDL's sensor buffer before the first report, not
// a reading (the remote never delivers 0 g on all three axes at once).
if (g[0] == 0.0f && g[1] == 0.0f && g[2] == 0.0f) {
return true;
}
const float zero = sensor == SDL_SENSOR_ACCEL ? 5.12f : 2.56f;
if (std::fabs(g[0] - zero) < 0.03f && std::fabs(g[1] + zero) < 0.03f && std::fabs(g[2] + zero) < 0.03f) {
return true;
}
if (sensor == SDL_SENSOR_ACCEL) {
for (int i = 0; i < 3; ++i) {
if (std::fabs(g[i]) > kMaxPlausibleRemoteG) return true;
}
}
return false;
}
// One accelerometer sample in g, SDL's sensor frame. The remote's own axes are
// +x left, +y towards the user, +z out of the button face (wiibrew, Dolphin);
// SDL_hidapi_wii.c posts (-wiiX, wiiZ, wiiY): x right across the face, y out of
// the face (+1 at rest, buttons up), z towards the user, i.e. away from the tip.
// The remote's sample gets the zero-point correction; the Nunchuk's does not.
// False when there is no sample yet or the sample is a glitch (see above).
bool ReadAccelG(SDL_Gamepad* gamepad, SDL_SensorType sensor, float* g) {
float sdl[3] = {};
if (!ReadSdlAccel(gamepad, sensor, sdl)) {
return false;
}
for (int i = 0; i < 3; ++i) g[i] = sdl[i] / kStandardGravity;
if (IsGlitchedSample(sensor, g)) {
return false;
}
if (sensor == SDL_SENSOR_ACCEL) {
const std::array<float, 3>& offset = AccelOffset();
for (int i = 0; i < 3; ++i) g[i] -= offset[i];
}
return true;
}
// KPAD's acc is the remote reading as (-wiiX, -wiiZ, wiiY): x right across the
// face, y through the back of the remote (rest: -1 with the buttons up), z
// towards the user. That is SDL's frame with y negated. Held sideways as a
// wheel the rest vector is (1, 0, 0) and a turn shows up as z = sin(angle), so
// the sign of z is the direction of the turn; Cemu does the same conversion for
// real remotes on the Wii U.
void AccelGToKpad(const float* g, float* kpad) {
kpad[0] = g[0];
kpad[1] = -g[1];
kpad[2] = g[2];
}
// Sensor -> KPAD acc for ReadKpadSample.
bool ReadAccelAsKpad(SDL_Gamepad* gamepad, SDL_SensorType sensor, float* kpad) {
float g[3] = {};
if (!ReadAccelG(gamepad, sensor, g)) {
return false;
}
AccelGToKpad(g, kpad);
return true;
}
// Debug trace of every remote sample (controller.wii_accel_trace = true):
// milliseconds, port, WPAD hold bits, the uncorrected SDL sample in g and the
// KPAD acc handed to the game. One CSV per run, truncated at startup.
void TraceSample(uint32_t chan, const KpadSample& sample, const float* rawG, bool haveRaw, bool accepted) {
static std::ofstream trace;
static bool opened = false;
if (!opened) {
opened = true;
const std::filesystem::path path = RuntimeConfigFile::ApplicationDataDirectory() / "wii_accel_trace.csv";
trace.open(path, std::ios::trunc);
if (trace) {
trace << "ms,port,hold,raw_x,raw_y,raw_z,ok,kpad_x,kpad_y,kpad_z\n";
RT_LOG(RT_TAG_CONFIG) << "Wii Remote accelerometer trace: " << path.string() << std::endl;
} else {
RT_LOG(RT_TAG_CONFIG) << "Wii Remote accelerometer trace: could not open " << path.string() << std::endl;
}
}
if (!trace) {
return;
}
char line[192];
if (haveRaw) {
std::snprintf(line, sizeof(line), "%llu,%u,%04x,%.4f,%.4f,%.4f,%d,%.4f,%.4f,%.4f\n",
static_cast<unsigned long long>(SDL_GetTicks()), chan + 1, sample.hold, rawG[0], rawG[1],
rawG[2], accepted ? 1 : 0, sample.acc[0], sample.acc[1], sample.acc[2]);
} else {
std::snprintf(line, sizeof(line), "%llu,%u,%04x,,,,0,%.4f,%.4f,%.4f\n",
static_cast<unsigned long long>(SDL_GetTicks()), chan + 1, sample.hold, sample.acc[0],
sample.acc[1], sample.acc[2]);
}
trace << line;
// A frame per line; flush so a crash or a killed process keeps the tail.
trace.flush();
}
// Ends a calibration run with a message for the overlay.
void FinishAccelCalibration(const char* message) {
g_calibration.active = false;
std::snprintf(g_calibrationMessage, sizeof(g_calibrationMessage), "%s", message);
RT_LOG(RT_TAG_CONFIG) << "Wii Remote accelerometer calibration: " << message << std::endl;
}
// One frame of a calibration run: accumulates the uncorrected sample and, once
// enough frames are in, stores the mean minus the ideal rest vector (0, 1, 0).
void StepAccelCalibration() {
if (!g_calibration.active) {
return;
}
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(g_calibration.chan));
if (gamepad == nullptr || !IsRemoteChannel(g_calibration.chan)) {
FinishAccelCalibration("Cancelled: the Wii Remote went away.");
return;
}
EnsureSensors(gamepad, g_calibration.chan);
float sdl[3] = {};
if (!ReadSdlAccel(gamepad, SDL_SENSOR_ACCEL, sdl)) {
return; // no sample this frame; keep waiting
}
float g[3];
for (int i = 0; i < 3; ++i) g[i] = sdl[i] / kStandardGravity;
if (IsGlitchedSample(SDL_SENSOR_ACCEL, g)) {
return; // a zeroed report, not a movement
}
if (g_calibration.count == 0) {
for (int i = 0; i < 3; ++i) g_calibration.first[i] = g[i];
} else {
for (int i = 0; i < 3; ++i) {
if (std::fabs(g[i] - g_calibration.first[i]) > kCalibrationMaxDeviationG) {
FinishAccelCalibration("Failed: the remote moved. Put it down, buttons up, and try again.");
return;
}
}
}
for (int i = 0; i < 3; ++i) g_calibration.sum[i] += g[i];
if (++g_calibration.count < kCalibrationSamples) {
return;
}
std::array<double, 3> mean{};
for (int i = 0; i < 3; ++i) mean[i] = g_calibration.sum[i] / g_calibration.count;
const double length = std::sqrt(mean[0] * mean[0] + mean[1] * mean[1] + mean[2] * mean[2]);
if (length < kCalibrationMinGravityG || length > kCalibrationMaxGravityG || mean[1] < 0.5) {
FinishAccelCalibration("Failed: the remote was not resting flat with the buttons up.");
return;
}
const std::array<double, 3> offset = {mean[0], mean[1] - 1.0, mean[2]};
for (int i = 0; i < 3; ++i) g_accelOffset[i] = static_cast<float>(offset[i]);
g_accelOffsetLoaded = true;
const bool saved = RuntimeConfigFile::SetWiiAccelOffset(offset);
char message[160];
std::snprintf(message, sizeof(message), "%s offset x %+.3f y %+.3f z %+.3f g",
saved ? "Calibrated:" : "Calibrated (could not write Config.toml):", offset[0], offset[1],
offset[2]);
FinishAccelCalibration(message);
}
// True when any controller aurora knows about is a Wii device.
bool AnyWiiControllerConnected() {
const uint32_t count = PADCount();
for (uint32_t index = 0; index < count; ++index) {
if (KindForName(PADGetNameForControllerIndex(index)) != Kind::NotWii) {
return true;
}
}
return false;
}
// Route SDL's input diagnostics (HIDAPI open failures, the Wii driver's
// extension/status messages) into console.log, minus the periodic chatter.
void SDLCALL LogSdlMessage(void*, int category, SDL_LogPriority priority, const char* message) {
if (message == nullptr) {
return;
}
if (category == SDL_LOG_CATEGORY_INPUT && priority < SDL_LOG_PRIORITY_WARN &&
(std::strstr(message, "Motion Plus") != nullptr || std::strstr(message, "Resetting report mode") != nullptr)) {
return;
}
if (category == SDL_LOG_CATEGORY_INPUT || priority >= SDL_LOG_PRIORITY_WARN) {
RT_LOG("sdl") << message << std::endl;
}
}
// Second half of a rescan: re-enables the Wii driver once SDL has seen it off.
void FinishRescan(uint64_t now) {
if (g_driverOffSinceMs == 0 || now - g_driverOffSinceMs < kRescanDriverOffMs) {
return;
}
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII, "1");
g_driverOffSinceMs = 0;
g_lastScanMs = now;
++g_scanCount;
// The first few and then every tenth, so a remote that never comes back
// leaves a trail in console.log without flooding it.
if (g_scanCount <= 3 || g_scanCount % 10 == 0) {
RT_LOG(RT_TAG_CONFIG) << "Wii Remote rescan #" << g_scanCount << ": HIDAPI Wii driver re-enabled ("
<< PADCount() << " controller(s) known to aurora)" << std::endl;
}
}
} // namespace
// Enables SDL's HIDAPI Wii driver and player LEDs, and routes SDL's input log.
void ConfigureSdlHints(bool enabled) {
// A rescan may be mid-flight; drop its bookkeeping so Poll() is not left
// waiting for a FinishRescan() that can no longer happen.
g_driverOffSinceMs = 0;
if (!SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII, enabled ? "1" : "0")) {
RT_LOG(RT_TAG_CONFIG) << "Failed to set " << SDL_HINT_JOYSTICK_HIDAPI_WII << ": " << SDL_GetError()
<< std::endl;
}
// Light the player LED that matches the SDL player index, like the console does.
if (!SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII_PLAYER_LED, "1")) {
RT_LOG(RT_TAG_CONFIG) << "Failed to set " << SDL_HINT_JOYSTICK_HIDAPI_WII_PLAYER_LED << ": "
<< SDL_GetError() << std::endl;
}
RT_LOG(RT_TAG_CONFIG) << "Bluetooth Wii Remote support " << (enabled ? "enabled" : "disabled") << std::endl;
g_wiiDriverEnabled = enabled;
if (enabled) {
SDL_SetLogPriority(SDL_LOG_CATEGORY_INPUT, SDL_LOG_PRIORITY_DEBUG);
SDL_SetLogOutputFunction(LogSdlMessage, nullptr);
}
}
// Starts a rescan by disabling the Wii driver hint; Poll() finishes it.
void RescanNow() {
if (!g_wiiDriverEnabled || g_driverOffSinceMs != 0) {
return;
}
// SDL only closes the HID handle of a remote it dropped while the Wii driver
// is disabled, and only re-opens it when the driver is enabled again; both
// must happen on separate joystick updates, so the hint stays at "0" until
// FinishRescan() a few frames later. Flipping 1->0->1 within one frame does
// nothing: SDL only ever sees the final "1".
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII, "0");
g_driverOffSinceMs = SDL_GetTicks();
if (g_scanCount < 3) {
RT_LOG(RT_TAG_CONFIG) << "Wii Remote rescan #" << (g_scanCount + 1) << ": HIDAPI Wii driver disabled"
<< std::endl;
}
}
// Per-frame scanning state machine: rescans while no Wii controller is present.
// Also advances an accelerometer calibration run, which needs a sample per frame
// whether or not the game is reading KPAD at that moment.
void Poll() {
StepAccelCalibration();
// Remember what each port had, so EffectiveKind can bridge a swap.
for (uint32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
(void)EffectiveKind(port);
}
if (!g_wiiDriverEnabled) {
return;
}
// Always complete a rescan in progress so the driver is never left disabled.
FinishRescan(SDL_GetTicks());
if (g_driverOffSinceMs != 0) {
return;
}
if (AnyWiiControllerConnected()) {
if (g_scanning) {
RT_LOG(RT_TAG_CONFIG) << "Wii Remote found after " << g_scanCount << " rescan(s)" << std::endl;
}
g_scanning = false;
g_scanCount = 0;
g_lastScanMs = SDL_GetTicks();
return;
}
if (!RuntimeConfigFile::WiiContinuousScanEnabled(true)) {
g_scanning = false;
return;
}
const uint64_t now = SDL_GetTicks();
if (!g_scanning) {
RT_LOG(RT_TAG_CONFIG) << "No Wii Remote connected; scanning for one (press 1+2 on the remote)"
<< std::endl;
g_scanning = true;
g_lostAtMs = now;
}
if (now - g_lostAtMs < kScanStartDelayMs) {
return;
}
const uint64_t interval = now - g_lostAtMs < kFastScanWindowMs ? kFastScanIntervalMs : kScanIntervalMs;
if (now - g_lastScanMs < interval) {
return;
}
RescanNow();
}
// True while Poll() is looking for a remote.
bool IsScanning() {
return g_scanning;
}
// Number of rescans since a Wii controller was last seen.
uint32_t ScanCount() {
return g_scanCount;
}
// Maps the gamepad name SDL's Wii driver reports to a Kind.
Kind KindForName(const char* name) {
// Names come from SDL's hidapi Wii driver: "Nintendo Wii Remote",
// "Nintendo Wii Remote with Nunchuk", "Nintendo Wii Remote with Classic
// Controller" and "Nintendo Wii U Pro Controller".
if (NameContains(name, "Wii U Pro Controller")) return Kind::WiiUPro;
if (!NameContains(name, "Wii Remote")) return Kind::NotWii;
if (NameContains(name, "Nunchuk")) return Kind::RemoteWithNunchuk;
if (NameContains(name, "Classic Controller")) return Kind::RemoteWithClassic;
return Kind::Remote;
}
// Kind of the SDL gamepad assigned to a game port, NotWii when empty.
Kind KindForPort(uint32_t port) {
if (port >= PAD_MAX_CONTROLLERS) return Kind::NotWii;
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(port));
if (gamepad == nullptr) return Kind::NotWii;
return KindForName(SDL_GetGamepadName(gamepad));
}
// Human-readable name of a Kind for the settings overlay.
const char* KindLabel(Kind kind) {
switch (kind) {
case Kind::Remote: return "Wii Remote";
case Kind::RemoteWithNunchuk: return "Wii Remote + Nunchuk";
case Kind::RemoteWithClassic: return "Wii Remote + Classic Controller";
case Kind::WiiUPro: return "Wii U Pro Controller";
default: return "Not a Wii controller";
}
}
// True for the kinds the game reads through KPAD.
static bool IsKpadKind(Kind kind) {
return kind == Kind::Remote || kind == Kind::RemoteWithNunchuk || kind == Kind::RemoteWithClassic;
}
// Live kind of the port, or the remembered one while a swap is in flight.
// Called from the guest thread only (PADRead, KPADRead, WPADProbe and the
// overlay's Draw all run there), so the port memory needs no locking.
Kind EffectiveKind(uint32_t chan) {
if (chan >= PAD_MAX_CONTROLLERS) return Kind::NotWii;
PortMemory& memory = g_ports[chan];
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(chan));
const Kind live = gamepad != nullptr ? KindForName(SDL_GetGamepadName(gamepad)) : Kind::NotWii;
const uint64_t now = SDL_GetTicks();
if (live != Kind::NotWii) {
memory.lastKind = live;
memory.lastSeenMs = now;
return live;
}
if (gamepad != nullptr) {
// Another controller took the port: the remote is not coming back here.
memory.lastKind = Kind::NotWii;
return Kind::NotWii;
}
if (IsKpadKind(memory.lastKind) && memory.lastSeenMs != 0 && now - memory.lastSeenMs < kExtensionSwapGraceMs) {
return memory.lastKind;
}
return Kind::NotWii;
}
// True when the game reads the port through KPAD (live or bridging a swap).
bool IsRemoteChannel(uint32_t chan) {
return IsKpadKind(EffectiveKind(chan));
}
// Marks KPAD-served ports as "no controller" in the GameCube pad statuses.
void HideRemotesFromPad(PADStatus* statuses, uint32_t count) {
for (uint32_t port = 0; port < count && port < PAD_MAX_CONTROLLERS; ++port) {
if (IsRemoteChannel(port)) {
statuses[port] = {};
statuses[port].err = PAD_ERR_NO_CONTROLLER;
}
}
}
// Neutral sample of the remembered kind, for the frames of an extension swap.
void FillGraceSample(uint32_t chan, Kind kind, KpadSample& sample) {
sample = {};
for (int i = 0; i < 3; ++i) sample.acc[i] = g_lastAcc[chan].acc[i];
sample.hasNunchuk = kind == Kind::RemoteWithNunchuk;
sample.hasClassic = kind == Kind::RemoteWithClassic;
}
// SDL's stick axis (-32767..32767, y down) as WPADCLStatus carries it: WPAD
// normalises every Classic Controller stick, whatever the report's resolution,
// to a signed 10-bit value, -512..511 with 0 at the centre and +y up (RVL SDK
// WPAD.h; wut's WPADStatusClassic documents the same range).
int16_t ClassicStickRaw(Sint16 axis, bool invert) {
float value = static_cast<float>(axis) / 32767.0f;
if (invert) value = -value;
value = std::clamp(value, -1.0f, 1.0f);
return static_cast<int16_t>(std::clamp(std::lround(value * 512.0f), -512L, 511L));
}
// Samples buttons, accelerometers and the extension of the remote on a port.
bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
if (chan >= PAD_MAX_CONTROLLERS) {
return false;
}
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(chan));
const Kind kind = gamepad != nullptr ? KindForName(SDL_GetGamepadName(gamepad)) : Kind::NotWii;
if (!IsKpadKind(kind)) {
const Kind remembered = EffectiveKind(chan);
if (!IsKpadKind(remembered)) {
return false;
}
FillGraceSample(chan, remembered, sample);
return true;
}
SDL_Joystick* joystick = SDL_GetGamepadJoystick(gamepad);
if (joystick == nullptr) {
return false;
}
EnsureSensors(gamepad, chan);
sample = {};
const auto raw = [&](int index, uint32_t bit) {
if (SDL_GetJoystickButton(joystick, index)) sample.hold |= bit;
};
raw(kRawA, kWpadA);
raw(kRawB, kWpadB);
raw(kRawOne, kWpadOne);
raw(kRawTwo, kWpadTwo);
raw(kRawPlus, kWpadPlus);
raw(kRawMinus, kWpadMinus);
raw(kRawHome, kWpadHome);
raw(kRawDpadUp, kWpadUp);
raw(kRawDpadDown, kWpadDown);
raw(kRawDpadLeft, kWpadLeft);
raw(kRawDpadRight, kWpadRight);
float rawG[3] = {};
const bool haveRaw = ReadSdlAccel(gamepad, SDL_SENSOR_ACCEL, rawG);
for (float& v : rawG) v /= kStandardGravity;
LastAcc& last = g_lastAcc[chan];
const bool accepted = ReadAccelAsKpad(gamepad, SDL_SENSOR_ACCEL, sample.acc);
if (accepted) {
last.valid = true;
for (int i = 0; i < 3; ++i) last.acc[i] = sample.acc[i];
} else {
// No sample this frame or a glitched one: repeat the last good reading
// (rest pose, buttons up, until there is one).
for (int i = 0; i < 3; ++i) sample.acc[i] = last.acc[i];
}
if (RuntimeConfigFile::WiiAccelTraceEnabled(false)) {
TraceSample(chan, sample, rawG, haveRaw, accepted);
}
if (kind == Kind::RemoteWithClassic) {
sample.hasClassic = true;
// The driver posts the extension's buttons as joystick buttons numbered
// by SDL_GAMEPAD_BUTTON_*: a/b/x/y by position (a on the east), +/-,
// Home, the L/R clicks as shoulders, the D-pad as buttons 11-14 (never
// through SDL's gamepad mapping, which expects a hat), ZL/ZR as the
// trigger axes.
const auto cl = [&](int index, uint32_t bit) {
if (SDL_GetJoystickButton(joystick, index)) sample.clHold |= bit;
};
cl(SDL_GAMEPAD_BUTTON_EAST, kClA);
cl(SDL_GAMEPAD_BUTTON_SOUTH, kClB);
cl(SDL_GAMEPAD_BUTTON_NORTH, kClX);
cl(SDL_GAMEPAD_BUTTON_WEST, kClY);
cl(SDL_GAMEPAD_BUTTON_START, kClPlus);
cl(SDL_GAMEPAD_BUTTON_BACK, kClMinus);
cl(SDL_GAMEPAD_BUTTON_GUIDE, kClHome);
cl(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, kClL);
cl(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, kClR);
cl(SDL_GAMEPAD_BUTTON_DPAD_UP, kClUp);
cl(SDL_GAMEPAD_BUTTON_DPAD_DOWN, kClDown);
cl(SDL_GAMEPAD_BUTTON_DPAD_LEFT, kClLeft);
cl(SDL_GAMEPAD_BUTTON_DPAD_RIGHT, kClRight);
if (SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0) sample.clHold |= kClZL;
if (SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > 0) sample.clHold |= kClZR;
const Sint16 lx = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
const Sint16 ly = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
const Sint16 rx = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX);
const Sint16 ry = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY);
sample.clLStick[0] = std::clamp(static_cast<float>(lx) / 32767.0f, -1.0f, 1.0f);
sample.clLStick[1] = std::clamp(-static_cast<float>(ly) / 32767.0f, -1.0f, 1.0f);
sample.clRStick[0] = std::clamp(static_cast<float>(rx) / 32767.0f, -1.0f, 1.0f);
sample.clRStick[1] = std::clamp(-static_cast<float>(ry) / 32767.0f, -1.0f, 1.0f);
sample.clLStickRaw[0] = ClassicStickRaw(lx, false);
sample.clLStickRaw[1] = ClassicStickRaw(ly, true);
sample.clRStickRaw[0] = ClassicStickRaw(rx, false);
sample.clRStickRaw[1] = ClassicStickRaw(ry, true);
// Only the full-press click of L/R reaches SDL; report it as a full pull.
sample.clTriggerL = (sample.clHold & kClL) ? 255 : 0;
sample.clTriggerR = (sample.clHold & kClR) ? 255 : 0;
}
if (kind == Kind::RemoteWithNunchuk) {
sample.hasNunchuk = true;
if (SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)) sample.hold |= kWpadC;
if (SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0) sample.hold |= kWpadZ;
sample.stick[0] = static_cast<float>(SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX)) / 32767.0f;
// SDL's y grows downwards; KPAD's stick y is up-positive.
sample.stick[1] = -static_cast<float>(SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY)) / 32767.0f;
for (float& v : sample.stick) v = std::clamp(v, -1.0f, 1.0f);
LastAcc& lastNunchuk = g_lastNunchukAcc[chan];
if (ReadAccelAsKpad(gamepad, SDL_SENSOR_ACCEL_L, sample.nunchukAcc)) {
lastNunchuk.valid = true;
for (int i = 0; i < 3; ++i) lastNunchuk.acc[i] = sample.nunchukAcc[i];
} else {
for (int i = 0; i < 3; ++i) sample.nunchukAcc[i] = lastNunchuk.acc[i];
}
}
return true;
}
// Corrected SDL sample and KPAD vector of the remote on a port, for the overlay.
bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]) {
if (!IsRemoteChannel(chan)) {
return false;
}
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(chan));
if (gamepad == nullptr) {
return false;
}
EnsureSensors(gamepad, chan);
if (!ReadAccelG(gamepad, SDL_SENSOR_ACCEL, sdlG)) {
return false;
}
AccelGToKpad(sdlG, kpadAcc);
return true;
}
// Begins collecting rest samples from the remote on `chan`.
void StartAccelCalibration(uint32_t chan) {
if (!IsRemoteChannel(chan)) {
FinishAccelCalibration("No Wii Remote on this port.");
return;
}
g_calibration = {};
g_calibration.active = true;
g_calibration.chan = chan;
g_calibrationMessage[0] = '\0';
}
// Drops the stored correction and goes back to SDL's raw reading.
void ClearAccelCalibration() {
g_calibration.active = false;
g_accelOffset = {};
g_accelOffsetLoaded = true;
RuntimeConfigFile::SetWiiAccelOffset({0.0, 0.0, 0.0});
std::snprintf(g_calibrationMessage, sizeof(g_calibrationMessage), "Calibration cleared.");
}
// True while a calibration run is collecting samples.
bool IsAccelCalibrating() {
return g_calibration.active;
}
// Share of the calibration samples collected so far, 0..1; 0 when idle.
float AccelCalibrationProgress() {
return g_calibration.active ? static_cast<float>(g_calibration.count) / kCalibrationSamples : 0.0f;
}
// Outcome of the last calibration run for the overlay, or nullptr before any.
const char* AccelCalibrationMessage() {
return g_calibrationMessage[0] != '\0' ? g_calibrationMessage : nullptr;
}
} // namespace WiiRemoteInput
+49
View File
@@ -0,0 +1,49 @@
#include "host_context.h"
#include <cstdlib>
namespace {
// The worker yields twice; each return to the scheduler must preserve both
// context identities and the worker's continuation point.
HostContext::Handle g_scheduler = nullptr;
HostContext::Handle g_worker = nullptr;
int g_steps = 0;
void Worker(void*)
{
if (!HostContext::IsCurrent(g_worker)) {
std::abort();
}
++g_steps;
HostContext::Switch(g_scheduler);
if (!HostContext::IsCurrent(g_worker)) {
std::abort();
}
++g_steps;
HostContext::Switch(g_scheduler);
}
} // namespace
int main()
{
if (!HostContext::InitializeScheduler(&g_scheduler) ||
!HostContext::IsCurrent(g_scheduler)) {
return 1;
}
g_worker = HostContext::Create(64 * 1024, Worker, nullptr);
if (!g_worker) {
return 1;
}
HostContext::Switch(g_worker);
if (g_steps != 1 || !HostContext::IsCurrent(g_scheduler)) {
return 1;
}
HostContext::Switch(g_worker);
if (g_steps != 2 || !HostContext::IsCurrent(g_scheduler)) {
return 1;
}
HostContext::Destroy(g_worker);
HostContext::ShutdownScheduler(g_scheduler);
}
+46
View File
@@ -0,0 +1,46 @@
#include <array>
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <vector>
extern "C" void mkw_co_switch(void** targetSp, void** sourceSp);
extern "C" void* mkw_co_init(void* stackTop, void (*entry)(void*), void* argument);
namespace {
// Exercise the raw AArch64 context ABI independently of HostContext so a
// callee-saved-register or stack-frame regression is localized to this layer.
std::array<std::byte, 64 * 1024> g_workerStack{};
void* g_schedulerSp = nullptr;
void* g_workerSp = nullptr;
std::vector<int> g_events;
void Worker(void*) {
g_events.push_back(1);
mkw_co_switch(&g_schedulerSp, &g_workerSp);
g_events.push_back(2);
mkw_co_switch(&g_schedulerSp, &g_workerSp);
std::abort();
}
} // namespace
int main() {
g_workerSp = mkw_co_init(g_workerStack.data() + g_workerStack.size(), Worker, nullptr);
if (!g_workerSp) {
std::cerr << "failed to create AArch64 context frame\n";
return 1;
}
mkw_co_switch(&g_workerSp, &g_schedulerSp);
if (g_events != std::vector<int>{1}) {
std::cerr << "worker did not yield to scheduler\n";
return 1;
}
mkw_co_switch(&g_workerSp, &g_schedulerSp);
if (g_events != std::vector<int>{1, 2}) {
std::cerr << "worker did not resume from saved context\n";
return 1;
}
return 0;
}
@@ -0,0 +1,66 @@
#include "guest_flat_memory.h"
#include <cstdint>
#include <iostream>
#include <stdexcept>
#include <unistd.h>
int main() {
GuestFlat::Initialize({
{0x00000000u, 0x4000u, GuestFlat::Backing::Mem1},
{0x80000000u, 0x4000u, GuestFlat::Backing::Mem1},
{0x10000000u, 0x4000u, GuestFlat::Backing::Mem2},
{0x90000000u, 0x4000u, GuestFlat::Backing::Mem2},
});
if (!GuestFlat::IsActive()) {
std::cerr << "guest address space did not become active\n";
return 1;
}
if (GuestFlat::RequiresCheckedAccess() !=
(static_cast<size_t>(getpagesize()) > GuestFlat::kGuestPageSize)) {
std::cerr << "guest access mode does not reflect the host page size\n";
return 1;
}
auto* mem1Physical = GuestFlat::HostPointer(0x00000000u);
auto* mem1Cached = GuestFlat::HostPointer(0x80000000u);
auto* mem2Physical = GuestFlat::HostPointer(0x10000000u);
auto* mem2Cached = GuestFlat::HostPointer(0x90000000u);
if (!mem1Physical || !mem1Cached || !mem2Physical || !mem2Cached) {
std::cerr << "missing host alias\n";
return 1;
}
if (GuestFlat::HostPointer(0x4000u) != nullptr ||
GuestFlat::HostPointer(0xa0000000u) != nullptr) {
std::cerr << "unmapped guest address resolved to host memory\n";
return 1;
}
mem1Physical[7] = 0x5a;
mem2Cached[9] = 0xa5;
const auto* guest = reinterpret_cast<const uint8_t*>(GuestFlat::kFixedFlatGuestBase);
if (mem1Cached[7] != 0x5a || guest[0x80000007u] != 0x5a ||
mem2Physical[9] != 0xa5 || guest[0x10000009u] != 0xa5) {
std::cerr << "guest aliases are not coherent\n";
return 1;
}
auto* guestWritable = reinterpret_cast<uint8_t*>(GuestFlat::kFixedFlatGuestBase);
guestWritable[0x80000008u] = 0x3c;
guestWritable[0x1000000au] = 0xc3;
if (mem1Physical[8] != 0x3c || mem2Cached[10] != 0xc3) {
std::cerr << "guest writes were not visible through host aliases\n";
return 1;
}
GuestFlat::Initialize({
{0x00000000u, 0x4000u, GuestFlat::Backing::Mem1},
{0x80000000u, 0x4000u, GuestFlat::Backing::Mem1},
{0x10000000u, 0x4000u, GuestFlat::Backing::Mem2},
{0x90000000u, 0x4000u, GuestFlat::Backing::Mem2},
});
try {
GuestFlat::Initialize({{0x00000000u, 0x8000u, GuestFlat::Backing::Mem1}});
std::cerr << "guest address space accepted a different layout\n";
return 1;
} catch (const std::runtime_error&) {
}
return 0;
}
+30
View File
@@ -0,0 +1,30 @@
#include "platform/host_platform.h"
#include <iostream>
int main() {
if (!RuntimePlatform::ExecutableDirectory()) {
std::cerr << "unable to resolve the current executable directory\n";
return 1;
}
const auto userData = RuntimePlatform::ApplicationDataDirectory("WiiCompiledPlatformPathsTest");
if (userData.filename() != "WiiCompiledPlatformPathsTest") {
std::cerr << "application-data directory lost its application name: " << userData << '\n';
return 1;
}
if (RuntimePlatform::LogDirectory("WiiCompiledPlatformPathsTest") != userData / "Logs") {
std::cerr << "log directory is not derived from application data\n";
return 1;
}
#if defined(__APPLE__)
if (userData.parent_path().filename() != "Application Support" ||
userData.parent_path().parent_path().filename() != "Library") {
std::cerr << "macOS application-data directory is not under ~/Library/Application Support: "
<< userData << '\n';
return 1;
}
#endif
return 0;
}
+34 -2
View File
@@ -172,6 +172,7 @@ return command switch
"emit-build-shards" => RunEmitBuildShards(tail), "emit-build-shards" => RunEmitBuildShards(tail),
"emit-base-manifest" => RunEmitBaseManifest(tail), "emit-base-manifest" => RunEmitBaseManifest(tail),
"check-base-mod-awareness" => RunCheckBaseModAwareness(tail), "check-base-mod-awareness" => RunCheckBaseModAwareness(tail),
"validate-retro-wfc-payload" => RunValidateRetroWfcPayload(tail),
_ => ShowHelp(command) _ => ShowHelp(command)
}; };
@@ -360,6 +361,33 @@ int RunInfo()
return 0; return 0;
} }
int RunValidateRetroWfcPayload(string[] argsTail)
{
var directory = OptionValue(argsTail, "--directory");
if (string.IsNullOrWhiteSpace(directory))
{
Console.Error.WriteLine("--directory is required.");
return 1;
}
try
{
WiiCompiled.Setup.Common.RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(directory);
Console.WriteLine("[translator] Retro WFC payload signature validated.");
return 0;
}
catch (InvalidDataException ex)
{
Console.Error.WriteLine($"[translator] Retro WFC payload validation failed: {ex.Message}");
return 2;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
Console.Error.WriteLine($"[translator] Could not read Retro WFC payload: {ex.Message}");
return 1;
}
}
int RunTranslateRecursive(string[] argsTail) int RunTranslateRecursive(string[] argsTail)
{ {
if (argsTail.Length == 0 || argsTail[0].StartsWith("--", StringComparison.Ordinal)) if (argsTail.Length == 0 || argsTail[0].StartsWith("--", StringComparison.Ordinal))
@@ -3671,7 +3699,8 @@ static string[] KnownCommands() => new[]
"translate-mod", "translate-mod",
"emit-base-manifest", "emit-base-manifest",
"emit-build-shards", "emit-build-shards",
"check-base-mod-awareness" "check-base-mod-awareness",
"validate-retro-wfc-payload"
}; };
/// <summary> /// <summary>
@@ -3714,6 +3743,10 @@ static (string? Positional, CommandOption[] Options)? CommandSpec(string command
new("--translation-output-metadata", "path"), new("--translation-output-metadata", "path"),
new("--code-pul", "path") new("--code-pul", "path")
}), }),
"validate-retro-wfc-payload" => (null, new CommandOption[]
{
new("--directory", "directory", Required: true)
}),
"emit-base-manifest" => (null, new CommandOption[] "emit-base-manifest" => (null, new CommandOption[]
{ {
new("--out", "path"), new("--out", "path"),
@@ -4110,4 +4143,3 @@ sealed record ResolvedDispatchEntry(
bool MustRemainDynamicallyDispatchable, bool MustRemainDynamicallyDispatchable,
string SourceFile); string SourceFile);
@@ -15,6 +15,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Translator.Core\Translator.Core.csproj" /> <ProjectReference Include="..\Translator.Core\Translator.Core.csproj" />
<ProjectReference Include="..\..\..\Launcher\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -22,14 +22,15 @@ internal static class AssemblyBlobWriter
var expectedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var expectedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var assembly = new StringBuilder(); var assembly = new StringBuilder();
foreach (var header in headerLines) assembly.AppendLine(header); foreach (var header in headerLines) assembly.AppendLine(header);
// PE/COFF (Windows) and ELF (Linux) spell a read-only data section differently in GNU-as // PE/COFF (Windows), Mach-O (macOS), and ELF (Linux) spell a read-only data section
// syntax - COFF section flags ("dr" = data, read-only) versus an ELF section needing an // differently in GNU-as syntax. The build always targets
// allocatable-only flag plus an explicit @progbits type. The build always targets
// whichever platform the translator itself runs on (there is no cross-compilation // whichever platform the translator itself runs on (there is no cross-compilation
// support), so that's what this picks the section syntax from. // support), so that's what this picks the section syntax from.
assembly.AppendLine(OperatingSystem.IsWindows() assembly.AppendLine(OperatingSystem.IsWindows()
? ".section .rdata,\"dr\"" ? ".section .rdata,\"dr\""
: ".section .rodata,\"a\",@progbits"); : OperatingSystem.IsMacOS()
? ".section __TEXT,__const"
: ".section .rodata,\"a\",@progbits");
assembly.AppendLine(); assembly.AppendLine();
foreach (var blob in blobs) foreach (var blob in blobs)
@@ -40,8 +41,12 @@ internal static class AssemblyBlobWriter
var hash = ChecksumUtilities.Sha256Hex(blob.Data.Span); var hash = ChecksumUtilities.Sha256Hex(blob.Data.Span);
assembly.AppendLine($"// {blob.Comment}; sha256={hash}"); assembly.AppendLine($"// {blob.Comment}; sha256={hash}");
assembly.AppendLine(".p2align 4"); assembly.AppendLine(".p2align 4");
assembly.AppendLine($".globl {blob.Symbol}"); // C/C++ external symbols carry a leading underscore in Mach-O,
assembly.AppendLine($"{blob.Symbol}:"); // unlike ELF and COFF. The generated C++ still names the symbol
// without that ABI decoration, so emit the platform spelling here.
var assemblySymbol = OperatingSystem.IsMacOS() ? $"_{blob.Symbol}" : blob.Symbol;
assembly.AppendLine($".globl {assemblySymbol}");
assembly.AppendLine($"{assemblySymbol}:");
var referencePath = Path.Combine(blobReferenceDirectory, blob.FileName); var referencePath = Path.Combine(blobReferenceDirectory, blob.FileName);
assembly.AppendLine($".incbin \"{SanitizeAssemblyPath(referencePath)}\""); assembly.AppendLine($".incbin \"{SanitizeAssemblyPath(referencePath)}\"");
assembly.AppendLine(); assembly.AppendLine();
@@ -51,7 +56,7 @@ internal static class AssemblyBlobWriter
{ {
if (!expectedFiles.Contains(Path.GetFullPath(stalePath))) File.Delete(stalePath); if (!expectedFiles.Contains(Path.GetFullPath(stalePath))) File.Delete(stalePath);
} }
if (!OperatingSystem.IsWindows()) if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
{ {
// Absence of a .note.GNU-stack section makes the linker assume the oldest, most // Absence of a .note.GNU-stack section makes the linker assume the oldest, most
// conservative default for this object (an executable stack) and warn about it; this // conservative default for this object (an executable stack) and warn about it; this
@@ -6,6 +6,12 @@ namespace Translator.Core.Parsing.Kamek;
public sealed class KamekChunk public sealed class KamekChunk
{ {
public const uint Magic0 = 0x4B616D65; public const uint Magic0 = 0x4B616D65;
/// <summary>
/// Kamek v2 is still emitted by current Retro Rewind releases. Its command
/// stream matches v3, but combined v2 files carry each chunk length only in
/// the outer size table.
/// </summary>
public const uint Magic1V2 = 0x6B000002;
public const uint Magic1 = 0x6B000003; public const uint Magic1 = 0x6B000003;
public const int HeaderSize = 0x20; public const int HeaderSize = 0x20;
@@ -58,7 +64,7 @@ public sealed class KamekChunk
} }
return BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset, 4)) == Magic0 && return BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset, 4)) == Magic0 &&
BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset + 4, 4)) == Magic1; IsSupportedFormatVersion(BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset + 4, 4)));
} }
public static KamekChunk Parse(byte[] data, int offset, int expectedSize, int index) public static KamekChunk Parse(byte[] data, int offset, int expectedSize, int index)
@@ -74,7 +80,7 @@ public sealed class KamekChunk
var magic0 = reader.ReadUInt32(); var magic0 = reader.ReadUInt32();
var magic1 = reader.ReadUInt32(); var magic1 = reader.ReadUInt32();
if (magic0 != Magic0 || magic1 != Magic1) if (magic0 != Magic0 || !IsSupportedFormatVersion(magic1))
{ {
throw new InvalidDataException($"Kamek chunk {index} at 0x{offset:X} has invalid magic 0x{magic0:X8}/0x{magic1:X8}."); throw new InvalidDataException($"Kamek chunk {index} at 0x{offset:X} has invalid magic 0x{magic0:X8}/0x{magic1:X8}.");
} }
@@ -83,9 +89,15 @@ public sealed class KamekChunk
var codeSize = reader.ReadUInt32(); var codeSize = reader.ReadUInt32();
var ctorStart = reader.ReadUInt32(); var ctorStart = reader.ReadUInt32();
var ctorEnd = reader.ReadUInt32(); var ctorEnd = reader.ReadUInt32();
var chunkSize = reader.ReadUInt32(); var declaredChunkSize = reader.ReadUInt32();
_ = reader.ReadUInt32(); // reserved _ = reader.ReadUInt32(); // reserved
// v3 writes each chunk's length in its header. v2 leaves that field at
// zero and relies on the combined-file table (or the raw file length).
var chunkSize = magic1 == Magic1V2
? checked((uint)(expectedSize != 0 ? expectedSize : data.Length - offset))
: declaredChunkSize;
if (chunkSize < HeaderSize + codeSize) if (chunkSize < HeaderSize + codeSize)
{ {
throw new InvalidDataException($"Kamek chunk {index} chunkSize 0x{chunkSize:X} is smaller than header+code."); throw new InvalidDataException($"Kamek chunk {index} chunkSize 0x{chunkSize:X} is smaller than header+code.");
@@ -171,4 +183,7 @@ public sealed class KamekChunk
KamekCommandId.BranchLink => 1, KamekCommandId.BranchLink => 1,
_ => throw new ArgumentOutOfRangeException(nameof(id), id, "Unsupported Kamek command id") _ => throw new ArgumentOutOfRangeException(nameof(id), id, "Unsupported Kamek command id")
}; };
private static bool IsSupportedFormatVersion(uint magic1) =>
magic1 is Magic1V2 or Magic1;
} }
@@ -33,7 +33,7 @@ public sealed class KamekPulFile
if (!KamekChunk.HasMagic(data, 0)) if (!KamekChunk.HasMagic(data, 0))
{ {
throw new InvalidDataException("File is neither a combined Code.pul nor a raw Kamek v3 chunk."); throw new InvalidDataException("File is neither a combined Code.pul nor a raw supported Kamek chunk.");
} }
var rawChunk = KamekChunk.Parse(data, 0, expectedSize: 0, index: 0); var rawChunk = KamekChunk.Parse(data, 0, expectedSize: 0, index: 0);
@@ -49,6 +49,27 @@ public class KamekPulFileTests
Assert.Equal(0x20u, pul.SelectRegion("E").BssSize); Assert.Equal(0x20u, pul.SelectRegion("E").BssSize);
} }
[Fact]
public void ParsesKamekV2CombinedChunks()
{
var p = BuildChunk(0x10, [0x4E, 0x80, 0x00, 0x20],
[BuildCommand(KamekCommandId.Branch, true, 0x8053369C, [0x100])], KamekChunk.Magic1V2);
var combined = new byte[0x10 + p.Length];
WriteU32(combined, 0, (uint)p.Length);
p.CopyTo(combined, 0x10);
var pul = KamekPulFile.Parse(combined);
Assert.True(pul.IsCombined);
Assert.Equal(0x10u, pul.SelectRegion("P").BssSize);
Assert.Equal(KamekCommandId.Branch, pul.SelectRegion("P").Commands.Single().Id);
var raw = KamekPulFile.Parse(p);
Assert.False(raw.IsCombined);
Assert.Equal((uint)p.Length, raw.Chunks.Single().ChunkSize);
}
[Fact] [Fact]
public void EncodesPpcBranchLikeKamek() public void EncodesPpcBranchLikeKamek()
{ {
@@ -83,18 +104,19 @@ public class KamekPulFileTests
Assert.Equal(chunk.Commands.Count, chunk.CommandCounts.Values.Sum()); Assert.Equal(chunk.Commands.Count, chunk.CommandCounts.Values.Sum());
} }
private static byte[] BuildChunk(uint bssSize, byte[] code, byte[][] commands) private static byte[] BuildChunk(uint bssSize, byte[] code, byte[][] commands,
uint magic1 = KamekChunk.Magic1)
{ {
var commandSize = commands.Sum(c => c.Length); var commandSize = commands.Sum(c => c.Length);
var chunkSize = KamekChunk.HeaderSize + code.Length + commandSize; var chunkSize = KamekChunk.HeaderSize + code.Length + commandSize;
var data = new byte[chunkSize]; var data = new byte[chunkSize];
WriteU32(data, 0x00, KamekChunk.Magic0); WriteU32(data, 0x00, KamekChunk.Magic0);
WriteU32(data, 0x04, KamekChunk.Magic1); WriteU32(data, 0x04, magic1);
WriteU32(data, 0x08, bssSize); WriteU32(data, 0x08, bssSize);
WriteU32(data, 0x0C, (uint)code.Length); WriteU32(data, 0x0C, (uint)code.Length);
WriteU32(data, 0x10, 0); WriteU32(data, 0x10, 0);
WriteU32(data, 0x14, 0); WriteU32(data, 0x14, 0);
WriteU32(data, 0x18, (uint)chunkSize); WriteU32(data, 0x18, magic1 == KamekChunk.Magic1V2 ? 0u : (uint)chunkSize);
code.CopyTo(data, KamekChunk.HeaderSize); code.CopyTo(data, KamekChunk.HeaderSize);
var offset = KamekChunk.HeaderSize + code.Length; var offset = KamekChunk.HeaderSize + code.Length;
@@ -28,8 +28,10 @@ public sealed class ModDataPatchWriterTests
var assemblyPath = Path.Combine(root, "cpp", "mod_data_patches_blobs.S"); var assemblyPath = Path.Combine(root, "cpp", "mod_data_patches_blobs.S");
var assembly = File.ReadAllText(assemblyPath); var assembly = File.ReadAllText(assemblyPath);
Assert.Contains(".globl kModuleImage", assembly, StringComparison.Ordinal); var symbolPrefix = OperatingSystem.IsMacOS() ? "_" : "";
Assert.Contains(".globl kKamekCodeSha1Digest", assembly, StringComparison.Ordinal); Assert.Contains($".globl {symbolPrefix}kModuleImage", assembly, StringComparison.Ordinal);
Assert.Contains($".globl {symbolPrefix}kKamekCodeSha1Digest", assembly, StringComparison.Ordinal);
Assert.DoesNotContain("__APPLE__", assembly, StringComparison.Ordinal);
Assert.Contains(".incbin", assembly, StringComparison.Ordinal); Assert.Contains(".incbin", assembly, StringComparison.Ordinal);
var timestamps = new[] { cppPath, assemblyPath } var timestamps = new[] { cppPath, assemblyPath }