mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-12 01:38:50 -04:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d2c1fd8b8 | |||
| 0e647b3dbb | |||
| 28cbea46ab | |||
| 55ea5dfbda | |||
| 4a7f592b8a | |||
| c95f1c95b7 | |||
| 8e57cc162f | |||
| 1c0a3edee9 | |||
| d1d80613cc | |||
| 5d67b229f6 | |||
| 56db6ba641 | |||
| a67069afd3 | |||
| 009697fb97 | |||
| 3f7fed48c9 | |||
| 6eba523d70 | |||
| 8c6c177857 | |||
| be153e0fa0 | |||
| e34f055b3a | |||
| 602348f905 | |||
| e6f9b2197e | |||
| 65047bc7b7 | |||
| 989d5e00da | |||
| efc44b0482 | |||
| d3d0de62a6 | |||
| c6ef17378e | |||
| 38a11b7b2c | |||
| 7a13696a8b | |||
| 2bcfdca199 | |||
| f2165a5aa6 | |||
| ffc7244277 | |||
| dec9ed8df1 | |||
| 8dc9bb67b3 | |||
| ca7d126a13 | |||
| 5c76e2b0df | |||
| ae3096c89b | |||
| 6f4dd2f470 | |||
| 2b5b889249 | |||
| fbfb741b49 | |||
| e630132dd3 | |||
| a6f3720598 | |||
| abe7cd2b61 | |||
| 7aa04bfe80 | |||
| 1af7cd6d9b | |||
| d31942c356 | |||
| 13b6d0b447 | |||
| db576cd5cb | |||
| edacfcb8bc | |||
| 36faa76dc0 | |||
| 3fc1555871 | |||
| be691bd08d | |||
| 243eb86021 | |||
| 7b750e96ac | |||
| e4e388295b | |||
| 2c38db4174 | |||
| 1c7e4e79c5 | |||
| 6ddc153e44 | |||
| cb812c1fd0 |
@@ -14,15 +14,19 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
recompilation:
|
||||
name: Recompilation test
|
||||
uses: ./.github/workflows/recomp-test.yml
|
||||
|
||||
translator:
|
||||
name: Translator (build + test)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
- uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
name: Package installers
|
||||
|
||||
# Builds the per-platform installer/setup tool (WiiCompiled-Setup.exe /
|
||||
# WiiCompiled-Setup-x86_64.AppImage) via Launcher/Build-Installer.ps1 and
|
||||
# Launcher/build-appimage.sh respectively - the same scripts a maintainer runs by hand today to
|
||||
# produce a GitHub Release asset. This does NOT build the actual translated game executable:
|
||||
# that step requires the end user's own Mario Kart Wii dump (Assets/main.dol, Assets/StaticR.rel),
|
||||
# which is proprietary and not present in this repository or in CI.
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: package-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
recompilation:
|
||||
name: Recompilation test
|
||||
uses: ./.github/workflows/recomp-test.yml
|
||||
|
||||
linux-appimage:
|
||||
name: Linux (AppImage, ${{ matrix.arch }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-22.04
|
||||
arch: x86_64
|
||||
- runner: ubuntu-22.04-arm
|
||||
arch: aarch64
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
# appimagetool ships as an AppImage itself and needs to self-mount via FUSE to run directly;
|
||||
# GitHub's ubuntu-latest runners don't have libfuse2 preinstalled.
|
||||
- name: Install dependencies (required by appimagetool and SDL3 build)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libfuse2 build-essential git make \
|
||||
pkg-config cmake ninja-build gnome-desktop-testing libasound2-dev libpulse-dev \
|
||||
libaudio-dev libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
|
||||
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \
|
||||
libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
|
||||
libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev libthai-dev libusb-1.0-0-dev \
|
||||
libpipewire-0.3-dev libwayland-dev libdecor-0-dev liburing-dev
|
||||
|
||||
- name: Build AppImage
|
||||
run: bash Launcher/build-appimage.sh
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: WiiCompiled-Setup-linux-${{ matrix.arch }}
|
||||
path: Launcher/dist/WiiCompiled-Setup-${{ matrix.arch }}.AppImage
|
||||
if-no-files-found: error
|
||||
archive: false
|
||||
|
||||
windows-installer:
|
||||
name: Windows (installer exe)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Build installer
|
||||
shell: pwsh
|
||||
run: ./Launcher/Build-Installer.ps1
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: WiiCompiled-Setup-windows-x64
|
||||
path: Launcher/dist/WiiCompiled-Setup.exe
|
||||
if-no-files-found: error
|
||||
archive: false
|
||||
|
||||
# Publishes the packaged installers as a GitHub Release whenever a v* tag is pushed. Wheel Wizard
|
||||
# discovers updates from these releases, so the contract it relies on is enforced here: a full
|
||||
# (non-prerelease) release whose tag is v<semver>, carrying an asset named exactly
|
||||
# WiiCompiled-Setup.exe, produced by a setup host that reports that same version.
|
||||
release:
|
||||
name: Publish GitHub Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [linux-appimage, windows-installer, recompilation]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# Wheel Wizard runs the downloaded setup with --version and refuses it when the reported
|
||||
# version differs from the release tag, so a tag that was pushed without bumping every pinned
|
||||
# version would ship an update nobody can install. Catch that before anything is published.
|
||||
- name: Verify the tag matches the pinned setup version
|
||||
env:
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="${TAG#v}"
|
||||
status=0
|
||||
check() {
|
||||
if ! grep -Fq "$2" "$1"; then
|
||||
echo "::error file=$1::expected '$2' for tag $TAG"
|
||||
status=1
|
||||
fi
|
||||
}
|
||||
check Launcher/WiiCompiled.Setup.Windows/Program.cs "public const string Version = \"$version\";"
|
||||
check Launcher/WiiCompiled.Setup.Windows/WiiCompiled.Setup.Windows.csproj "<Version>$version</Version>"
|
||||
check Launcher/Build-Installer.ps1 "ProductVersion = '$version'"
|
||||
exit $status
|
||||
|
||||
# The build jobs upload with `archive: false`, which stores each installer as a raw file
|
||||
# rather than a zip (and names the artifact after the file). Only download-artifact v8 and
|
||||
# later understand such direct uploads; v7 tries to unzip everything and fails on them.
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
|
||||
- name: Publish release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ls -lR artifacts
|
||||
assets=()
|
||||
for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage; do
|
||||
found="$(find artifacts -type f -name "$name" | head -n 1)"
|
||||
[ -n "$found" ] && [ -s "$found" ] || { echo "::error::missing release asset $name"; exit 1; }
|
||||
assets+=("$found")
|
||||
done
|
||||
# A re-run of a tag whose release already exists only refreshes the assets.
|
||||
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
gh release upload "$TAG" "${assets[@]}" --repo "$GITHUB_REPOSITORY" --clobber
|
||||
else
|
||||
gh release create "$TAG" "${assets[@]}" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--title "$TAG" \
|
||||
--verify-tag \
|
||||
--generate-notes
|
||||
fi
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Synthetic recompilation
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
windows:
|
||||
name: Windows runtime (synthetic DOL)
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
# Cache downloads only. Preparation still validates pins, and every run
|
||||
# compiles current Aurora, runtime, and generated sources from scratch.
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: Launcher/artifacts/downloads
|
||||
key: windows-recomp-downloads-${{ hashFiles('Launcher/Prepare-PortableTools.ps1', 'Launcher/Prepare-Dependencies.ps1') }}
|
||||
|
||||
- name: Prepare the shipped Windows toolchain
|
||||
shell: pwsh
|
||||
run: ./Launcher/Prepare-PortableTools.ps1
|
||||
|
||||
- name: Prepare pinned native dependencies
|
||||
shell: pwsh
|
||||
run: ./Launcher/Prepare-Dependencies.ps1
|
||||
|
||||
- name: Translate, compile the full runtime, and link
|
||||
shell: pwsh
|
||||
run: ./Launcher/Test-Recompilation.ps1 -Parallel 3
|
||||
@@ -25,6 +25,9 @@ Code.pul
|
||||
# Build output
|
||||
/build/
|
||||
/build-*/
|
||||
/native-build/
|
||||
/native-build-macos/
|
||||
/local-products/
|
||||
/dist/
|
||||
/out/
|
||||
[Bb]in/
|
||||
|
||||
@@ -88,17 +88,23 @@ Assert-File $setupProject '.NET setup project'
|
||||
Assert-File $translatorProject 'Translator CLI project'
|
||||
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $portableTools 'llvm-mingw\bin\x86_64-w64-mingw32-clang++.exe'))) {
|
||||
# A PowerShell script, not a native executable - it never touches $LASTEXITCODE, and its own
|
||||
# $ErrorActionPreference = 'Stop' + throw already aborts this run on failure.
|
||||
& (Join-Path $PSScriptRoot 'Prepare-PortableTools.ps1') -Destination $portableTools
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Portable tool preparation failed.' }
|
||||
}
|
||||
Assert-File (Join-Path $portableTools 'CMake\bin\cmake.exe') 'Portable CMake'
|
||||
Assert-File (Join-Path $portableTools 'Ninja\ninja.exe') 'Portable Ninja'
|
||||
Assert-Directory $dependencySources 'Pinned offline dependency sources'
|
||||
# native_prebuilt carries the aurora/third-party archives the user no longer has
|
||||
# to compile (launcher/Prepare-NativePrebuilt.ps1).
|
||||
# Kept in step with InstalledLayout.DependencyNames by Test-PinnedFacts.ps1: the installed host
|
||||
# refuses to call a toolkit complete unless every one of these directories is present.
|
||||
$requiredDependencies = @('abseil-cpp','cppwinrt','dawn_prebuilt','fmt','freetype','imgui','libusb','native_prebuilt','png','SDL','sqlite3','tracy','xxhash','zlib','zstd')
|
||||
$missingSources = @($requiredDependencies | Where-Object { $_ -ne 'native_prebuilt' } |
|
||||
Where-Object { -not (Test-Path -LiteralPath (Join-Path $dependencySources $_) -PathType Container) })
|
||||
if ($missingSources.Count -gt 0) {
|
||||
& (Join-Path $PSScriptRoot 'Prepare-Dependencies.ps1') -Destination $dependencySources
|
||||
}
|
||||
Assert-Directory $dependencySources 'Pinned offline dependency sources'
|
||||
|
||||
# The precompiled archives are only interchangeable with what the user's machine
|
||||
# compiles if both came from this toolchain and this flag set, so a stale package
|
||||
@@ -165,8 +171,7 @@ Assert-File $translator 'Self-contained translator'
|
||||
|
||||
# Resolved via the shared WiiCompiled.Setup.Common.Cli helper (also used by build-appimage.sh on
|
||||
# Linux) rather than a separate download/version-pin copy here: it downloads and caches the same way
|
||||
# NodToolProvider.cs always does (Launcher/artifacts/nodtool.exe), replacing the old manual
|
||||
# -DolphinToolPath handoff with an automated, pinned acquisition step.
|
||||
# NodToolProvider.cs always does (Launcher/artifacts/nodtool.exe)
|
||||
$nodToolCliProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup.Common.Cli'
|
||||
$nodTool = (& dotnet run --project $nodToolCliProject -c Release -- --workspace $repoRoot | Select-Object -Last 1)
|
||||
if ($LASTEXITCODE -ne 0) { throw "nodtool resolution failed with exit code $LASTEXITCODE." }
|
||||
@@ -276,7 +281,7 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
|
||||
|
||||
$manifest = [ordered]@{
|
||||
SchemaVersion = 2
|
||||
ProductVersion = '0.2.25'
|
||||
ProductVersion = '0.2.29'
|
||||
ExpectedGameId = $pins.GameId
|
||||
ExpectedDolSha256 = $pins.DolSha256
|
||||
ExpectedRelSha256 = $pins.RelSha256
|
||||
@@ -292,11 +297,12 @@ $manifest = [ordered]@{
|
||||
$manifest | ConvertTo-Json | Set-Content (Join-Path $payloadRoot 'payload-manifest.json') -Encoding UTF8
|
||||
|
||||
Write-Host '[4/6] Enforcing the copyright and generated-code boundary...'
|
||||
# Both audits are PowerShell scripts that throw directly on failure (Test-PayloadBoundary.ps1
|
||||
# never invokes a native command at all, so it never touches $LASTEXITCODE); there is no exit
|
||||
# code to check here, and doing so risks reading a stale value from an unrelated earlier command.
|
||||
& (Join-Path $PSScriptRoot 'Test-PayloadBoundary.ps1') -PayloadRoot $payloadRoot
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Payload boundary audit failed.' }
|
||||
& (Join-Path $PSScriptRoot 'Test-NativeDependencies.ps1') -PayloadRoot $payloadRoot -SetupHost $setupHost `
|
||||
-LlvmReadobjPath (Join-Path $portableTools 'llvm-mingw\bin\llvm-readobj.exe')
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Native dependency audit failed.' }
|
||||
|
||||
Write-Host '[5/6] Creating the canonical installer payload...'
|
||||
$payloadZip = Join-Path $workRoot 'payload.zip'
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
# Populates Launcher/artifacts/dependencies: the pinned, unpatched upstream source trees (plus the
|
||||
# prebuilt Dawn package and generated C++/WinRT headers) that Build-Installer.ps1 ships inside the
|
||||
# installer so the user's build runs with FETCHCONTENT_FULLY_DISCONNECTED=ON. Every pin is the one
|
||||
# aurora-main's own CMake declares; the Pin fields below are asserted against those files so the
|
||||
# two cannot drift apart silently. native_prebuilt is not fetched here - Prepare-NativePrebuilt.ps1
|
||||
# compiles it from these trees.
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Destination,
|
||||
# Windows metadata source for cppwinrt.exe: 'local' (this machine's WinMetadata), 'sdk', or an
|
||||
# installed SDK version such as 10.0.26100.0.
|
||||
[string]$CppWinRtInput = 'local'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 3.0
|
||||
|
||||
$repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
||||
$auroraCMake = Join-Path $repoRoot 'aurora-main\CMakeLists.txt'
|
||||
$auroraExtern = Join-Path $repoRoot 'aurora-main\extern\CMakeLists.txt'
|
||||
$auroraDawn = Join-Path $repoRoot 'aurora-main\cmake\AuroraDawnProvider.cmake'
|
||||
$auroraLibUsb = Join-Path $repoRoot 'aurora-main\cmake\AuroraLibUSB.cmake'
|
||||
$auroraSdl = Join-Path $repoRoot 'aurora-main\cmake\AuroraSDL3Provider.cmake'
|
||||
|
||||
# Name = directory under the destination; FetchContent maps it back through
|
||||
# FETCHCONTENT_SOURCE_DIR_<UPPERCASE NAME> (NativeBuildFlags.ps1), so the names are the declared
|
||||
# FetchContent names, not the upstream project names.
|
||||
$packages = @(
|
||||
[pscustomobject]@{
|
||||
Name = 'SDL'; File = 'SDL3-3.4.4.tar.gz'
|
||||
Uris = @('https://github.com/libsdl-org/SDL/releases/download/release-3.4.4/SDL3-3.4.4.tar.gz')
|
||||
Pins = @(@{ File = $auroraCMake; Text = 'set(AURORA_SDL3_VERSION "3.4.4"' },
|
||||
@{ File = $auroraSdl; Text = 'releases/download/release-${AURORA_SDL3_VERSION}/SDL3-${AURORA_SDL3_VERSION}.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'abseil-cpp'; File = 'abseil-cpp-20240722.0.tar.gz'
|
||||
Uris = @('https://github.com/abseil/abseil-cpp/archive/refs/tags/20240722.0.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/abseil/abseil-cpp/archive/refs/tags/20240722.0.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'dawn_prebuilt'; File = 'dawn-v20260603.191052-windows-amd64.tar.gz'
|
||||
Uris = @('https://github.com/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 = 'png'; File = 'libpng-1.6.58.tar.gz'
|
||||
Uris = @('https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.58.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.58.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'sqlite3'; File = 'sqlite-amalgamation-3510300.zip'
|
||||
Uris = @('https://sqlite.org/2026/sqlite-amalgamation-3510300.zip')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://sqlite.org/2026/sqlite-amalgamation-3510300.zip' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'tracy'; File = 'tracy-a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz'
|
||||
Uris = @('https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'xxhash'; File = 'xxHash-0.8.3.tar.gz'
|
||||
Uris = @('https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.3.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.3.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'zlib'; File = 'zlib-1.3.2.tar.gz'
|
||||
Uris = @('https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'zstd'; File = 'zstd-1.5.7.tar.gz'
|
||||
Uris = @('https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz')
|
||||
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz' })
|
||||
}
|
||||
)
|
||||
|
||||
# The C++/WinRT compiler (NuGet package = zip) that generates the projection headers.
|
||||
$cppWinRtTool = [pscustomobject]@{
|
||||
Name = 'cppwinrt-tool'; File = 'Microsoft.Windows.CppWinRT.3.0.260818.1.nupkg'
|
||||
Uris = @('https://www.nuget.org/api/v2/package/Microsoft.Windows.CppWinRT/3.0.260818.1')
|
||||
Pins = @()
|
||||
}
|
||||
|
||||
if (-not $Destination) { $Destination = Join-Path $PSScriptRoot 'artifacts\dependencies' }
|
||||
$Destination = [IO.Path]::GetFullPath($Destination)
|
||||
$downloads = Join-Path $PSScriptRoot 'artifacts\downloads'
|
||||
$tar = Join-Path $env:SystemRoot 'System32\tar.exe'
|
||||
if (-not (Test-Path -LiteralPath $tar -PathType Leaf)) { throw "Windows archive tool is missing: $tar" }
|
||||
[IO.Directory]::CreateDirectory($Destination) | Out-Null
|
||||
[IO.Directory]::CreateDirectory($downloads) | Out-Null
|
||||
|
||||
function Assert-Pinned($Package) {
|
||||
foreach ($pin in $Package.Pins) {
|
||||
if (-not (Test-Path -LiteralPath $pin.File -PathType Leaf)) { throw "Pin source is missing: $($pin.File)" }
|
||||
$content = [IO.File]::ReadAllText($pin.File)
|
||||
if (-not $content.Contains($pin.Text)) {
|
||||
throw "$($Package.Name) is pinned to '$($pin.Text)' here but $($pin.File) no longer declares it; update both."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-Archive($Package) {
|
||||
$archive = Join-Path $downloads $Package.File
|
||||
if (Test-Path -LiteralPath $archive -PathType Leaf) { return $archive }
|
||||
$temporary = $archive + '.partial'
|
||||
$failures = @()
|
||||
foreach ($uri in $Package.Uris) {
|
||||
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
|
||||
Write-Host "Downloading $($Package.Name) from $uri..."
|
||||
try {
|
||||
Invoke-WebRequest -UseBasicParsing -Uri $uri -OutFile $temporary
|
||||
} catch {
|
||||
$failures += "$uri ($($_.Exception.Message))"
|
||||
continue
|
||||
}
|
||||
Move-Item -LiteralPath $temporary -Destination $archive -Force
|
||||
return $archive
|
||||
}
|
||||
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
|
||||
throw "$($Package.Name) could not be fetched: $($failures -join '; ')"
|
||||
}
|
||||
|
||||
function Expand-Package([string]$Archive, [string]$Target) {
|
||||
# Release archives wrap everything in one versioned directory (SDL3-3.4.4/, tracy-<sha>/) while
|
||||
# dawn-build's package is flat; either way the tree lands directly under the target.
|
||||
$extract = Join-Path (Split-Path -Parent $Target) ('.extract-' + (Split-Path -Leaf $Target))
|
||||
if (Test-Path -LiteralPath $extract) { Remove-Item -LiteralPath $extract -Recurse -Force }
|
||||
[IO.Directory]::CreateDirectory($extract) | Out-Null
|
||||
# Symbolic links need a privilege Windows tar usually lacks; the only ones in these archives
|
||||
# are zstd test aliases, and none are build inputs.
|
||||
$listing = & $tar -tvf $Archive
|
||||
if ($LASTEXITCODE -ne 0) { throw "Listing $Archive failed with exit code $LASTEXITCODE." }
|
||||
$excludes = @($listing | ForEach-Object { if ($_ -match '^l.*\s(\S+)\s->\s') { '--exclude=' + $Matches[1] } })
|
||||
& $tar -xf $Archive -C $extract @excludes
|
||||
if ($LASTEXITCODE -ne 0) { throw "Extracting $Archive failed with exit code $LASTEXITCODE." }
|
||||
$entries = @(Get-ChildItem -LiteralPath $extract -Force)
|
||||
$source = $extract
|
||||
if ($entries.Count -eq 1 -and $entries[0].PSIsContainer) { $source = $entries[0].FullName }
|
||||
if (Test-Path -LiteralPath $Target) { Remove-Item -LiteralPath $Target -Recurse -Force }
|
||||
Move-Item -LiteralPath $source -Destination $Target
|
||||
if (Test-Path -LiteralPath $extract) { Remove-Item -LiteralPath $extract -Recurse -Force }
|
||||
}
|
||||
|
||||
foreach ($package in $packages) {
|
||||
Assert-Pinned $package
|
||||
$target = Join-Path $Destination $package.Name
|
||||
if (Test-Path -LiteralPath $target -PathType Container) { continue }
|
||||
Expand-Package (Get-Archive $package) $target
|
||||
Write-Host "Prepared $($package.Name)"
|
||||
}
|
||||
|
||||
$cppWinRt = Join-Path $Destination 'cppwinrt'
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType Leaf)) {
|
||||
$toolRoot = Join-Path $PSScriptRoot 'artifacts\cppwinrt-tool'
|
||||
$compiler = Join-Path $toolRoot 'bin\cppwinrt.exe'
|
||||
if (-not (Test-Path -LiteralPath $compiler -PathType Leaf)) {
|
||||
Expand-Package (Get-Archive $cppWinRtTool) $toolRoot
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $compiler -PathType Leaf)) { throw "cppwinrt.exe is missing: $compiler" }
|
||||
if (Test-Path -LiteralPath $cppWinRt) { Remove-Item -LiteralPath $cppWinRt -Recurse -Force }
|
||||
[IO.Directory]::CreateDirectory($cppWinRt) | Out-Null
|
||||
Write-Host "Generating C++/WinRT headers (-input $CppWinRtInput)..."
|
||||
& $compiler -input $CppWinRtInput -output $cppWinRt
|
||||
if ($LASTEXITCODE -ne 0) { throw "cppwinrt.exe failed with exit code $LASTEXITCODE." }
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType Leaf)) {
|
||||
throw "cppwinrt.exe produced no winrt\base.h under $cppWinRt"
|
||||
}
|
||||
Copy-Item -LiteralPath (Join-Path $toolRoot 'LICENSE') -Destination (Join-Path $cppWinRt 'LICENSE.txt')
|
||||
Write-Host 'Prepared cppwinrt'
|
||||
}
|
||||
|
||||
Write-Host "Pinned dependency sources ready: $Destination"
|
||||
@@ -61,8 +61,6 @@ Assert-File $windres 'Portable resource compiler'
|
||||
Assert-File $clangBinary 'Portable clang driver binary'
|
||||
Assert-Directory $dependencies 'Pinned offline dependency sources'
|
||||
Assert-Directory $auroraSource 'aurora-main source tree'
|
||||
Assert-File (Join-Path $repoRoot 'generated\build_shards\shards.cmake') `
|
||||
'Translator shard manifest (run the developer build once so runtime/ can configure)'
|
||||
|
||||
if ($Parallel -le 0) { $Parallel = [Environment]::ProcessorCount }
|
||||
|
||||
|
||||
Executable
+590
@@ -0,0 +1,590 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the redistributable precompiled aurora + third-party package for native Linux: aurora
|
||||
# (~43% of local build CPU time per Prepare-NativePrebuilt.ps1) and vendored Crypto++ are identical
|
||||
# for every user under the pinned toolchain prepare-portable-tools.sh bundles, so this configures
|
||||
# runtime/ against that toolchain, builds just that closure, and harvests the archives plus a
|
||||
# generated CMake description into an output package - the Linux counterpart to
|
||||
# Launcher/Prepare-NativePrebuilt.ps1, consumed by the same platform-agnostic
|
||||
# runtime/cmake/NativePrebuilt.cmake either script's package works with unmodified.
|
||||
#
|
||||
# Not a byte-for-byte port of the Windows script: Linux needs no offline pinned dependency cache.
|
||||
# Every FetchContent dependency in aurora-main/extern/CMakeLists.txt is a fixed-version URL already
|
||||
# (verified directly), and aurora-main/CMakeLists.txt's own _default_linkage is "static" on any
|
||||
# non-Windows platform. Windows pins Launcher/artifacts/dependencies because its installer ships
|
||||
# pre-fetched sources to end users for a fully offline build; this harvest only ever runs on a
|
||||
# maintainer's own machine (network access needed once, here, not shipped to anyone), so a plain
|
||||
# networked configure is exactly as reproducible - what actually makes the harvested archives safe
|
||||
# to link into any consumer's own build is the pinned compiler below, not an offline source cache.
|
||||
set -euo pipefail
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
workspace=$(cd "$script_dir/.." && pwd)
|
||||
|
||||
arch=""
|
||||
output_dir=""
|
||||
stage_dir="$workspace/build/native-prebuilt-stage"
|
||||
keep_stage=0
|
||||
reuse_stage=0
|
||||
parallel=0
|
||||
print_fingerprint_only=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: Prepare-NativePrebuilt.sh --arch {x86_64|aarch64} [options]
|
||||
|
||||
--arch ARCH Target architecture; selects the matching bundled toolchain (required)
|
||||
--output-dir DIR Where the package is written (default: Launcher/artifacts/native-prebuilt-ARCH)
|
||||
--stage-dir DIR Staging build directory (default: build/native-prebuilt-stage)
|
||||
--keep-stage Do not delete the staging build directory afterward
|
||||
--reuse-stage Reuse an existing staging build directory (maintainer iteration aid: a
|
||||
re-harvest does not recompile aurora from scratch)
|
||||
--parallel N Ninja build parallelism (default: nproc)
|
||||
--print-fingerprint-only Print the four provenance inputs (compiler_sha256, flag_fingerprint,
|
||||
aurora_fingerprint, third_party_fingerprint) as "key=value" lines and
|
||||
exit, without configuring/building/harvesting anything - lets a caller
|
||||
(build-appimage.sh) decide whether an existing package is still current
|
||||
without paying for a full aurora rebuild just to find out.
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--arch) arch=$2; shift 2 ;;
|
||||
--output-dir) output_dir=$2; shift 2 ;;
|
||||
--stage-dir) stage_dir=$2; shift 2 ;;
|
||||
--keep-stage) keep_stage=1; shift ;;
|
||||
--reuse-stage) reuse_stage=1; shift ;;
|
||||
--parallel) parallel=$2; shift 2 ;;
|
||||
--print-fingerprint-only) print_fingerprint_only=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Prepare-NativePrebuilt.sh: unknown argument: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$arch" in
|
||||
x86_64|aarch64) ;;
|
||||
*) echo "Prepare-NativePrebuilt.sh: --arch must be x86_64 or aarch64" >&2; usage; exit 1 ;;
|
||||
esac
|
||||
|
||||
fail() { echo "Prepare-NativePrebuilt.sh: error: $*" >&2; exit 1; }
|
||||
assert_file() { [[ -f "$1" ]] || fail "$2 is missing: $1"; }
|
||||
assert_dir() { [[ -d "$1" ]] || fail "$2 is missing: $1"; }
|
||||
sha256_of() { sha256sum "$1" | awk '{print $1}'; }
|
||||
normalize() { readlink -f "$1"; }
|
||||
|
||||
[[ -n "$output_dir" ]] || output_dir="$script_dir/artifacts/native-prebuilt-$arch"
|
||||
[[ "$stage_dir" = /* ]] || stage_dir="$workspace/$stage_dir"
|
||||
|
||||
toolchain_dir="$script_dir/artifacts/portable-tools/toolchain-$arch"
|
||||
cc="$toolchain_dir/bin/clang"
|
||||
cxx="$toolchain_dir/bin/clang++"
|
||||
cmake_bin="$toolchain_dir/bin/cmake"
|
||||
ninja_bin="$toolchain_dir/bin/ninja"
|
||||
runtime_source="$workspace/runtime"
|
||||
aurora_source="$workspace/aurora-main"
|
||||
|
||||
assert_file "$cmake_bin" "Portable CMake (run prepare-portable-tools.sh --arch $arch first)"
|
||||
assert_file "$ninja_bin" "Portable Ninja"
|
||||
assert_file "$cc" "Portable C compiler"
|
||||
assert_file "$cxx" "Portable C++ compiler"
|
||||
assert_dir "$aurora_source" "aurora-main source tree"
|
||||
clang_binary=$(normalize "$toolchain_dir/bin/clang-22")
|
||||
assert_file "$clang_binary" "Portable clang driver binary"
|
||||
|
||||
(( parallel > 0 )) || parallel=$(nproc)
|
||||
|
||||
fingerprint_tree() {
|
||||
# $1 = root dir, remaining args = top-level subdirectory names to exclude
|
||||
local root=$1; shift
|
||||
root=$(normalize "$root")
|
||||
[[ -d "$root" ]] || return 0
|
||||
local find_args=("$root")
|
||||
local ex
|
||||
for ex in "$@"; do find_args+=(-path "$root/$ex" -prune -o); done
|
||||
find_args+=(-type f -print)
|
||||
find "${find_args[@]}" | LC_ALL=C sort | while IFS= read -r abs; do
|
||||
printf '%s %s\n' "${abs#$root/}" "$(sha256_of "$abs")"
|
||||
done | sha256sum | awk '{print $1}'
|
||||
}
|
||||
|
||||
# The fixed (path-independent) half of the configure command line - identical for every
|
||||
# harvest, which is what lets one package be valid regardless of where it was built.
|
||||
# -DBUILD_SHARED_LIBS=OFF: aurora-main/extern/CMakeLists.txt derives its own _USE_SHARED from
|
||||
# whether BUILD_SHARED_LIBS is *defined at all* (not its value), so leaving it unset would default
|
||||
# zlib/libpng to shared. -DAURORA_SDL3_PROVIDER=vendor: without it, "auto" resolves to "system" on
|
||||
# any machine that happens to have an SDL3 dev package installed. -DCMAKE_DISABLE_FIND_PACKAGE_*:
|
||||
# absl/PNG/Freetype each call find_package() unconditionally, with no provider flag to gate them
|
||||
# (unlike SDL3/Dawn) - verified directly that this machine's system libpng-dev/freetype-dev get
|
||||
# linked in shared instead of the pinned vendored source otherwise. Disabling find_package for
|
||||
# these three forces the same FetchContent-vendored, statically-built result regardless of what a
|
||||
# given maintainer's machine happens to have installed. ZLIB is deliberately NOT disabled the same
|
||||
# way: libpng's own vendored CMakeLists.txt calls find_package(ZLIB REQUIRED) internally even when
|
||||
# zlib came from aurora's own FetchContent (extern/CMakeLists.txt writes a redirect config for
|
||||
# exactly this), and CMAKE_DISABLE_FIND_PACKAGE_ZLIB errors out on any REQUIRED call site outright
|
||||
# (verified directly) - it cannot be scoped to only aurora's own initial, non-required check.
|
||||
# Dawn stays a prebuilt package regardless (Linux x86_64/aarch64 always auto-resolve to "package" -
|
||||
# see AuroraDawnProvider.cmake).
|
||||
fixed_configure_flags=(
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DBUILD_SHARED_LIBS=OFF
|
||||
-DAURORA_SDL3_PROVIDER=vendor
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_absl=ON
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_PNG=ON
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_Freetype=ON
|
||||
# Freetype's own vendored CMakeLists.txt separately probes for system BZip2 (optional
|
||||
# bzip2-compressed-font support aurora-main never asked for) regardless of the Freetype
|
||||
# find_package disable above, since that only stops aurora's own outer find_package(Freetype)
|
||||
# from picking up a system Freetype - it does not reach into the FetchContent-built copy's own
|
||||
# internal find_package(BZip2) call. FT_DISABLE_BZIP2 is Freetype's own documented flag for
|
||||
# exactly this (verified in its CMakeLists.txt), unlike the ZLIB/libpng situation where no such
|
||||
# source-level flag exists.
|
||||
-DFT_DISABLE_BZIP2=ON
|
||||
-DCMAKE_POLICY_DEFAULT_CMP0168=NEW
|
||||
)
|
||||
flag_fingerprint=$(printf '%s\n' "${fixed_configure_flags[@]}" | sha256sum | awk '{print $1}')
|
||||
|
||||
# extern/ is excluded because the payload ships that tree separately (aurora-main/extern is bundled
|
||||
# whole by build-appimage.sh); build/ is a plain developer build directory.
|
||||
aurora_fingerprint=$(fingerprint_tree "$aurora_source" extern build)
|
||||
[[ -n "$aurora_fingerprint" ]] || fail "The aurora source tree could not be fingerprinted: $aurora_source"
|
||||
|
||||
# The harvested Crypto++ archive is consumed against this tree's headers, so it is fingerprinted
|
||||
# for the same reason as aurora above. No exclusions: unlike aurora's extern/, nothing under
|
||||
# runtime/third_party is shipped separately.
|
||||
third_party_fingerprint=$(fingerprint_tree "$runtime_source/third_party")
|
||||
[[ -n "$third_party_fingerprint" ]] || fail "The vendored third-party tree could not be fingerprinted: $runtime_source/third_party"
|
||||
|
||||
compiler_sha256=$(sha256_of "$clang_binary")
|
||||
|
||||
if [[ "$print_fingerprint_only" -eq 1 ]]; then
|
||||
printf 'compiler_sha256=%s\n' "$compiler_sha256"
|
||||
printf 'flag_fingerprint=%s\n' "$flag_fingerprint"
|
||||
printf 'aurora_fingerprint=%s\n' "$aurora_fingerprint"
|
||||
printf 'third_party_fingerprint=%s\n' "$third_party_fingerprint"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# The package must never contain a stale mixture of two builds.
|
||||
rm -rf "$output_dir"
|
||||
mkdir -p "$output_dir/lib" "$output_dir/bin" "$output_dir/include"
|
||||
|
||||
# The staging build has to be configured without the package present, otherwise the runtime would
|
||||
# consume the very package this script is producing.
|
||||
if [[ "$reuse_stage" -eq 0 ]]; then rm -rf "$stage_dir"; fi
|
||||
export_dir="$stage_dir/export"
|
||||
mkdir -p "$export_dir"
|
||||
|
||||
# fixed_configure_flags/flag_fingerprint were already computed above (needed before the
|
||||
# --print-fingerprint-only early exit).
|
||||
echo "Prepare-NativePrebuilt.sh: configuring the aurora/third-party staging build..."
|
||||
"$cmake_bin" -S "$runtime_source" -B "$stage_dir" -G Ninja \
|
||||
"${fixed_configure_flags[@]}" \
|
||||
-DCMAKE_C_COMPILER="$cc" -DCMAKE_CXX_COMPILER="$cxx" \
|
||||
-DCMAKE_MAKE_PROGRAM="$ninja_bin" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=lld" \
|
||||
-DMKW_NATIVE_PREBUILT_EXPORT_DIR="$export_dir"
|
||||
|
||||
# mkw_np_probe links exactly what mkw_runtime_common and the public products link, so building it
|
||||
# compiles the whole redistributable closure and nothing else - and a successful link proves the
|
||||
# harvested archives are complete.
|
||||
echo "Prepare-NativePrebuilt.sh: building the aurora/third-party closure..."
|
||||
"$cmake_bin" --build "$stage_dir" --target mkw_np_probe --parallel "$parallel"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Read the description CMake wrote, plus the resolved command lines Ninja holds.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
meta_path="$export_dir/meta.txt"
|
||||
assert_file "$meta_path" "Native prebuilt export metadata"
|
||||
get_meta() { awk -F= -v k="$1" '$0 ~ "^" k "=" { sub(/^[^=]*=/, ""); print; exit }' "$meta_path"; }
|
||||
|
||||
build_ninja="$stage_dir/build.ninja"
|
||||
assert_file "$build_ninja" "Generated build.ninja"
|
||||
|
||||
extract_ninja_vars() {
|
||||
# $1 = ninja file, $2 = exact rule name. Prints "KEY\tVALUE" for every " KEY = VALUE" line
|
||||
# following the first "build ...: RULE ..." statement that uses that rule.
|
||||
awk -v rule="$2" '
|
||||
BEGIN { in_stmt = 0 }
|
||||
/^build / {
|
||||
if (in_stmt) exit
|
||||
colon = index($0, ": ")
|
||||
if (colon == 0) next
|
||||
rest = substr($0, colon + 2)
|
||||
sp = index(rest, " ")
|
||||
stmt_rule = (sp > 0) ? substr(rest, 1, sp - 1) : rest
|
||||
if (stmt_rule == rule) in_stmt = 1
|
||||
next
|
||||
}
|
||||
in_stmt {
|
||||
if (substr($0, 1, 2) != " ") exit
|
||||
line = substr($0, 3)
|
||||
eq = index(line, " = ")
|
||||
if (eq == 0) exit
|
||||
print substr(line, 1, eq - 1) "\t" substr(line, eq + 3)
|
||||
}
|
||||
' "$1"
|
||||
}
|
||||
|
||||
declare -A probe_compile=() control_compile=() probe_link=() control_link=()
|
||||
load_vars() {
|
||||
local -n dest=$1
|
||||
local k v
|
||||
while IFS=$'\t' read -r k v; do dest["$k"]=$v; done < <(extract_ninja_vars "$build_ninja" "$2")
|
||||
}
|
||||
load_vars probe_compile "CXX_COMPILER__mkw_np_probe_unscanned_Release"
|
||||
load_vars control_compile "CXX_COMPILER__mkw_np_probe_control_unscanned_Release"
|
||||
load_vars probe_link "CXX_EXECUTABLE_LINKER__mkw_np_probe_Release"
|
||||
load_vars control_link "CXX_EXECUTABLE_LINKER__mkw_np_probe_control_Release"
|
||||
[[ -n "${probe_compile[INCLUDES]+x}" ]] || fail "No Ninja statement used rule CXX_COMPILER__mkw_np_probe_unscanned_Release."
|
||||
[[ -n "${probe_link[LINK_LIBRARIES]+x}" ]] || fail "No Ninja statement used rule CXX_EXECUTABLE_LINKER__mkw_np_probe_Release."
|
||||
|
||||
split_list() {
|
||||
# $1 = string value, $2 = name of the array to fill (nameref)
|
||||
local -n out=$2
|
||||
out=()
|
||||
[[ -n "$1" ]] || return 0
|
||||
read -ra out <<< "$1"
|
||||
}
|
||||
|
||||
subtract_multiset() {
|
||||
# $1 = "all" array name, $2 = "remove" array name, $3 = result array name (all namerefs)
|
||||
local -n all_ref=$1 remove_ref=$2 result_ref=$3
|
||||
local -A pending=()
|
||||
local item
|
||||
for item in "${remove_ref[@]}"; do
|
||||
pending["$item"]=$(( ${pending["$item"]:-0} + 1 ))
|
||||
done
|
||||
result_ref=()
|
||||
for item in "${all_ref[@]}"; do
|
||||
if [[ "${pending["$item"]:-0}" -gt 0 ]]; then
|
||||
pending["$item"]=$(( pending["$item"] - 1 ))
|
||||
continue
|
||||
fi
|
||||
result_ref+=("$item")
|
||||
done
|
||||
}
|
||||
|
||||
split_list "${probe_compile[INCLUDES]:-}" _probe_includes
|
||||
split_list "${control_compile[INCLUDES]:-}" _control_includes
|
||||
subtract_multiset _probe_includes _control_includes include_arguments
|
||||
|
||||
split_list "${probe_compile[DEFINES]:-}" _probe_defines
|
||||
split_list "${control_compile[DEFINES]:-}" _control_defines
|
||||
subtract_multiset _probe_defines _control_defines define_arguments
|
||||
|
||||
split_list "${probe_compile[FLAGS]:-}" _probe_flags
|
||||
split_list "${control_compile[FLAGS]:-}" _control_flags
|
||||
subtract_multiset _probe_flags _control_flags compile_options_raw
|
||||
|
||||
split_list "${probe_link[LINK_LIBRARIES]:-}" _probe_link
|
||||
split_list "${control_link[LINK_LIBRARIES]:-}" _control_link
|
||||
subtract_multiset _probe_link _control_link link_items
|
||||
|
||||
[[ ${#include_arguments[@]} -gt 0 ]] || fail "The aurora compile interface is empty; the probe did not link aurora."
|
||||
[[ ${#link_items[@]} -gt 0 ]] || fail "The aurora link interface is empty; the probe did not link aurora."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Harvest the built libraries.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
binary_root=$(normalize "$stage_dir")
|
||||
aurora_root=$(normalize "$(get_meta aurora_dir)")
|
||||
runtime_root=$(normalize "$(get_meta runtime_dir)")
|
||||
|
||||
generated_include_index=0
|
||||
copy_harvested() {
|
||||
# $1 = source path, $2 = subdirectory under the package. Echoes "subdir/name".
|
||||
local source=$1 subdir=$2
|
||||
local name destination
|
||||
name=$(basename "$source")
|
||||
destination="$output_dir/$subdir/$name"
|
||||
[[ ! -e "$destination" ]] || fail "Two harvested files collide on the name $name: $source"
|
||||
cp "$source" "$destination"
|
||||
printf '%s/%s' "$subdir" "$name"
|
||||
}
|
||||
declare -A generated_include_tokens=()
|
||||
convert_to_token() {
|
||||
# $1 = absolute path, $2 = 'include' or another kind. Sets $TOKEN_RESULT - NOT echoed/command-
|
||||
# substituted: this needs to mutate generated_include_tokens/generated_include_index, and a
|
||||
# $(...) call runs in a subshell, silently discarding that mutation once it returns (verified
|
||||
# directly: every build-tree include directory collided into the same "generated01" until this
|
||||
# was written this way instead).
|
||||
local path kind=$2
|
||||
path=$(normalize "$1")
|
||||
case "$path" in
|
||||
"$aurora_root"/*) TOKEN_RESULT="@AURORA@/${path#$aurora_root/}"; return ;;
|
||||
"$runtime_root"/*) TOKEN_RESULT="@RUNTIME@/${path#$runtime_root/}"; return ;;
|
||||
"$binary_root"/*)
|
||||
[[ "$kind" == include ]] || fail "Unhandled build-tree artifact: $path"
|
||||
if [[ -n "${generated_include_tokens[$path]:-}" ]]; then
|
||||
TOKEN_RESULT=${generated_include_tokens[$path]}
|
||||
return
|
||||
fi
|
||||
generated_include_index=$((generated_include_index + 1))
|
||||
local name destination
|
||||
name=$(printf 'generated%02d' "$generated_include_index")
|
||||
destination="$output_dir/include/$name"
|
||||
mkdir -p "$destination"
|
||||
cp -a "$path/." "$destination/"
|
||||
TOKEN_RESULT="@PKG@/include/$name"
|
||||
generated_include_tokens[$path]=$TOKEN_RESULT
|
||||
return ;;
|
||||
*) fail "Path escapes every shippable root ($kind): $path" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
declare -A linker_file_to_reference=()
|
||||
shared_import_targets=() shared_import_runtime=() shared_import_implib=()
|
||||
targets_txt="$export_dir/targets.txt"
|
||||
assert_file "$targets_txt" "Native prebuilt export targets list"
|
||||
while IFS='|' read -r name type file linkerfile; do
|
||||
[[ -n "$name" ]] || continue
|
||||
[[ -f "$linkerfile" ]] || continue # not part of the closure mkw_np_probe pulled in
|
||||
linkerfile=$(normalize "$linkerfile")
|
||||
relative_linker=$(copy_harvested "$linkerfile" lib)
|
||||
if [[ "$type" == "SHARED_LIBRARY" ]]; then
|
||||
file_abs=$(normalize "$file")
|
||||
# Unlike Windows (a .dll + a separate .dll.a import library), a Linux shared object's
|
||||
# TARGET_FILE and TARGET_LINKER_FILE are the same path - one file, harvested once.
|
||||
if [[ "$file_abs" == "$linkerfile" ]]; then
|
||||
relative_runtime=$relative_linker
|
||||
else
|
||||
[[ -f "$file_abs" ]] || fail "Shared library $name has no runtime file: $file_abs"
|
||||
relative_runtime=$(copy_harvested "$file_abs" bin)
|
||||
fi
|
||||
shared_import_targets+=("mkw_np::$name")
|
||||
shared_import_runtime+=("$relative_runtime")
|
||||
shared_import_implib+=("$relative_linker")
|
||||
linker_file_to_reference["$linkerfile"]="mkw_np::$name"
|
||||
else
|
||||
linker_file_to_reference["$linkerfile"]="@PKG@/$relative_linker"
|
||||
fi
|
||||
done < "$targets_txt"
|
||||
# Unlike Windows (SDL/zlib/libpng ship as DLLs by default), everything here was forced static above
|
||||
# and Dawn's own Linux package (verified directly) ships libwebgpu_dawn.a, also static - so zero
|
||||
# shared imports is the expected, normal outcome, not a failure.
|
||||
echo "Prepare-NativePrebuilt.sh: harvested ${#linker_file_to_reference[@]} archive(s) (${#shared_import_targets[@]} shared)"
|
||||
|
||||
dawn_linker_file="" dawn_runtime_file=""
|
||||
dawn_txt="$export_dir/dawn.txt"
|
||||
if [[ -s "$dawn_txt" ]]; then
|
||||
IFS='|' read -r _ dawn_runtime_file dawn_linker_file < "$dawn_txt"
|
||||
dawn_runtime_file=$(normalize "$dawn_runtime_file")
|
||||
dawn_linker_file=$(normalize "$dawn_linker_file")
|
||||
fi
|
||||
[[ -n "$dawn_linker_file" ]] && linker_file_to_reference["$dawn_linker_file"]="dawn::webgpu_dawn"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rewrite the compile/link interface into workspace-relative tokens.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
package_include_dirs=()
|
||||
include_count=${#include_arguments[@]}
|
||||
i=0
|
||||
while (( i < include_count )); do
|
||||
entry=${include_arguments[i]}
|
||||
case "$entry" in
|
||||
-I*) convert_to_token "${entry#-I}" include; package_include_dirs+=("$TOKEN_RESULT") ;;
|
||||
-isystem)
|
||||
i=$((i + 1))
|
||||
(( i < include_count )) || fail "Trailing -isystem with no argument"
|
||||
convert_to_token "${include_arguments[i]}" include; package_include_dirs+=("$TOKEN_RESULT") ;;
|
||||
-isystem*) convert_to_token "${entry#-isystem}" include; package_include_dirs+=("$TOKEN_RESULT") ;;
|
||||
*) fail "Unexpected include argument: $entry" ;;
|
||||
esac
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
package_definitions=()
|
||||
for entry in "${define_arguments[@]}"; do
|
||||
case "$entry" in
|
||||
# Ninja records a quoted define's value with its quotes backslash-escaped (verified
|
||||
# directly: IMGUI_USER_CONFIG=\"aurora/imgui_config.h\" in build.ninja) regardless of
|
||||
# platform - format_cmake_block below re-escapes for CMake's own string syntax, so the
|
||||
# literal backslash has to come out here first or the result is double-escaped and the
|
||||
# define expands to literal backslash-quote characters instead of a quoted string.
|
||||
-D*) package_definitions+=("${entry#-D}") ;;
|
||||
*) fail "Unexpected define argument: $entry" ;;
|
||||
esac
|
||||
done
|
||||
package_definitions=("${package_definitions[@]//\\\"/\"}")
|
||||
|
||||
# fmt's `-include cstdlib` (and anything like it) is recorded from a C++ compile line, so re-scope
|
||||
# it to C++. The runtime also assembles generated .S blobs through the same targets, and a forced
|
||||
# C++ header include would break them.
|
||||
package_compile_options=()
|
||||
for entry in "${compile_options_raw[@]}"; do
|
||||
package_compile_options+=("\$<\$<COMPILE_LANGUAGE:CXX>:$entry>")
|
||||
done
|
||||
|
||||
package_link_items=()
|
||||
for item in "${link_items[@]}"; do
|
||||
case "$item" in
|
||||
-*) package_link_items+=("$item"); continue ;;
|
||||
esac
|
||||
if [[ "$item" = /* ]]; then item_abs=$(normalize "$item"); else item_abs=$(normalize "$stage_dir/$item"); fi
|
||||
ref=${linker_file_to_reference["$item_abs"]:-}
|
||||
if [[ -n "$ref" ]]; then
|
||||
package_link_items+=("$ref")
|
||||
continue
|
||||
fi
|
||||
case "$item_abs" in
|
||||
*/libz.so*)
|
||||
# The one unavoidable system reference: libpng's own vendored CMakeLists.txt calls
|
||||
# find_package(ZLIB REQUIRED) internally even when zlib came from aurora's own
|
||||
# FetchContent (extern/CMakeLists.txt writes a redirect config for exactly this case),
|
||||
# and CMAKE_DISABLE_FIND_PACKAGE_ZLIB errors out on any REQUIRED call site outright
|
||||
# (verified directly), so it cannot be forced to vendor/static the way absl/PNG/Freetype
|
||||
# are above. zlib is as close to a universal baseline as a Linux shared library gets
|
||||
# (glibc-adjacent - practically every distro has it already), so this is recorded as a
|
||||
# portable `-lz` link flag instead of the harvesting machine's absolute path.
|
||||
package_link_items+=("-lz")
|
||||
continue ;;
|
||||
esac
|
||||
fail "Link item is neither a system library nor a harvested archive: $item"
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Emit the consumer-facing CMake description.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
format_cmake_block() {
|
||||
# $1 = CMake variable name, remaining args = list items
|
||||
local name=$1; shift
|
||||
if [[ $# -eq 0 ]]; then
|
||||
printf 'set(%s "")\n' "$name"
|
||||
return
|
||||
fi
|
||||
printf 'set(%s\n' "$name"
|
||||
local item escaped
|
||||
for item in "$@"; do
|
||||
escaped=${item//\\/\\\\}
|
||||
escaped=${escaped//\"/\\\"}
|
||||
printf ' "%s"\n' "$escaped"
|
||||
done
|
||||
printf ')\n'
|
||||
}
|
||||
|
||||
dawn_config_dir_meta=$(get_meta dawn_config_dir)
|
||||
dawn_config_token=""
|
||||
if [[ -n "$dawn_config_dir_meta" ]]; then
|
||||
dawn_config_dir_abs=$(normalize "$dawn_config_dir_meta")
|
||||
case "$dawn_config_dir_abs" in
|
||||
"$binary_root"/*)
|
||||
# Dawn's find_package() config directory (lib/cmake/Dawn under its own fetched package
|
||||
# root) cannot go through the generic single-directory copy other build-tree artifacts
|
||||
# use: DawnConfig.cmake/DawnTargets.cmake derive _IMPORT_PREFIX/PACKAGE_PREFIX_DIR from
|
||||
# their OWN file location by walking up three parent directories (verified directly), so
|
||||
# the whole package root - include/, lib/, lib/cmake/Dawn/ together - must be copied as
|
||||
# one self-contained unit for that relative navigation to still resolve once moved.
|
||||
dawn_package_root=$(normalize "$dawn_config_dir_abs/../../..")
|
||||
dawn_config_relative=${dawn_config_dir_abs#$dawn_package_root/}
|
||||
mkdir -p "$output_dir/include/dawn_package"
|
||||
cp -a "$dawn_package_root/." "$output_dir/include/dawn_package/"
|
||||
dawn_config_token="@PKG@/include/dawn_package/$dawn_config_relative"
|
||||
;;
|
||||
*) convert_to_token "$dawn_config_dir_abs" include; dawn_config_token=$TOKEN_RESULT ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
generated_cmake="$output_dir/native_prebuilt.cmake"
|
||||
{
|
||||
echo "# Generated by Launcher/Prepare-NativePrebuilt.sh - do not edit."
|
||||
echo "#"
|
||||
echo "# Describes the precompiled aurora + third-party archives that replace a"
|
||||
echo "# from-source aurora-main build. Every path is a token resolved against the"
|
||||
echo "# consuming workspace, because the source trees still ship next to this"
|
||||
echo "# package and the runtime compiles against their headers."
|
||||
echo ""
|
||||
echo 'if(NOT MKW_NP_PACKAGE_DIR OR NOT MKW_NP_AURORA_DIR OR NOT MKW_NP_RUNTIME_DIR OR NOT MKW_NP_DEPS_DIR)'
|
||||
echo ' message(FATAL_ERROR "native_prebuilt.cmake must be included by runtime/cmake/NativePrebuilt.cmake")'
|
||||
echo 'endif()'
|
||||
echo ""
|
||||
format_cmake_block MKW_NP_INCLUDE_DIRECTORIES "${package_include_dirs[@]}"
|
||||
format_cmake_block MKW_NP_COMPILE_DEFINITIONS "${package_definitions[@]}"
|
||||
format_cmake_block MKW_NP_COMPILE_OPTIONS "${package_compile_options[@]}"
|
||||
format_cmake_block MKW_NP_LINK_LIBRARIES "${package_link_items[@]}"
|
||||
format_cmake_block MKW_NP_AURORA_TARGETS "aurora::gx" "aurora::pad" "aurora::si" "aurora::vi" "aurora::mtx"
|
||||
echo ""
|
||||
printf 'set(MKW_NP_DAWN_CONFIG_DIR "%s")\n' "$dawn_config_token"
|
||||
printf 'set(MKW_NP_DAWN_VERSION "%s")\n' "$(get_meta aurora_dawn_version)"
|
||||
echo ""
|
||||
echo "# Shared third-party libraries keep imported SHARED targets so that"
|
||||
echo '# $<TARGET_RUNTIME_DLLS> still copies them next to the game executable.'
|
||||
for idx in "${!shared_import_targets[@]}"; do
|
||||
printf 'add_library(%s SHARED IMPORTED GLOBAL)\n' "${shared_import_targets[$idx]}"
|
||||
printf 'set_target_properties(%s PROPERTIES\n' "${shared_import_targets[$idx]}"
|
||||
printf ' IMPORTED_LOCATION "${MKW_NP_PACKAGE_DIR}/%s"\n' "${shared_import_runtime[$idx]}"
|
||||
printf ' IMPORTED_IMPLIB "${MKW_NP_PACKAGE_DIR}/%s")\n' "${shared_import_implib[$idx]}"
|
||||
done
|
||||
} > "$generated_cmake"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Provenance: the pinned toolchain is what makes a precompiled archive interchangeable with
|
||||
# locally compiled objects (bit-for-bit the compiler and flag set the user's machine will use);
|
||||
# LocalBuild.ps1's Windows equivalent re-checks both before consuming a package, and a Linux
|
||||
# consumer-side check (once local-build.sh gains --native-prebuilt-dir) should do the same.
|
||||
# compiler_sha256/flag_fingerprint/aurora_fingerprint/third_party_fingerprint were already computed
|
||||
# above (needed before the --print-fingerprint-only early exit).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
dawn_runtime_sha256=""
|
||||
[[ -n "$dawn_runtime_file" && -f "$dawn_runtime_file" ]] && dawn_runtime_sha256=$(sha256_of "$dawn_runtime_file")
|
||||
[[ -n "$dawn_runtime_sha256" ]] || fail "The Dawn runtime could not be hashed: $dawn_runtime_file"
|
||||
|
||||
compiler_version=$(get_meta cxx_compiler_version)
|
||||
sdl3_target=$(get_meta aurora_sdl3_target)
|
||||
dawn_version=$(get_meta aurora_dawn_version)
|
||||
harvested_count=${#linker_file_to_reference[@]}
|
||||
|
||||
python3 - "$output_dir" "$compiler_sha256" "$compiler_version" "$flag_fingerprint" \
|
||||
"$dawn_version" "$dawn_runtime_sha256" "$aurora_fingerprint" "$third_party_fingerprint" \
|
||||
"$sdl3_target" "$harvested_count" <<'PY'
|
||||
import hashlib, json, os, sys, datetime
|
||||
|
||||
(output_dir, compiler_sha256, compiler_version, flag_fingerprint, dawn_version,
|
||||
dawn_runtime_sha256, aurora_fingerprint, third_party_fingerprint, sdl3_target,
|
||||
harvested_count) = sys.argv[1:]
|
||||
|
||||
contents = []
|
||||
for root, dirs, files in os.walk(output_dir):
|
||||
dirs.sort()
|
||||
for name in sorted(files):
|
||||
if name == "provenance.json":
|
||||
continue
|
||||
path = os.path.join(root, name)
|
||||
rel = os.path.relpath(path, output_dir).replace(os.sep, "/")
|
||||
with open(path, "rb") as fh:
|
||||
digest = hashlib.sha256(fh.read()).hexdigest()
|
||||
contents.append({"Path": rel, "Bytes": os.path.getsize(path), "Sha256": digest})
|
||||
contents.sort(key=lambda c: c["Path"])
|
||||
|
||||
provenance = {
|
||||
"SchemaVersion": 1,
|
||||
"BuiltUtc": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
||||
"CompilerSha256": compiler_sha256,
|
||||
"CompilerVersion": compiler_version,
|
||||
"FlagFingerprint": flag_fingerprint,
|
||||
"DawnVersion": dawn_version,
|
||||
"DawnRuntimeSha256": dawn_runtime_sha256,
|
||||
"AuroraSourceFingerprint": aurora_fingerprint,
|
||||
"ThirdPartySourceFingerprint": third_party_fingerprint,
|
||||
"Sdl3Target": sdl3_target,
|
||||
"HarvestedLibraryCount": int(harvested_count),
|
||||
"Contents": contents,
|
||||
}
|
||||
with open(os.path.join(output_dir, "provenance.json"), "w", encoding="utf-8") as fh:
|
||||
json.dump(provenance, fh, indent=2)
|
||||
fh.write("\n")
|
||||
|
||||
total_bytes = sum(c["Bytes"] for c in contents)
|
||||
print(f" files: {len(contents)}; size: {total_bytes / (1024 * 1024):.1f} MiB")
|
||||
PY
|
||||
|
||||
if [[ "$keep_stage" -eq 0 ]]; then rm -rf "$stage_dir"; fi
|
||||
|
||||
echo ""
|
||||
echo "Native prebuilt package: $output_dir"
|
||||
echo " archives: $(find "$output_dir/lib" -maxdepth 1 -type f | wc -l); shared imports: ${#shared_import_targets[@]}"
|
||||
echo " flag fingerprint: $flag_fingerprint"
|
||||
@@ -0,0 +1,146 @@
|
||||
# Build the real Windows runtime with translated, entirely synthetic PowerPC code.
|
||||
# No game dump, game symbol map, REL, mod download, or existing generated/ output is used.
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$PortableToolsDirectory = 'Launcher/artifacts/portable-tools',
|
||||
[string]$DependencySourceDirectory = 'Launcher/artifacts/dependencies',
|
||||
[string]$StageDirectory = 'build/recomp-test',
|
||||
[ValidateRange(1, 64)] [int]$Parallel = 3
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 3.0
|
||||
. (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1')
|
||||
|
||||
$repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
||||
function Full([string]$Path) {
|
||||
if ([IO.Path]::IsPathRooted($Path)) { return [IO.Path]::GetFullPath($Path) }
|
||||
return [IO.Path]::GetFullPath((Join-Path $repoRoot $Path))
|
||||
}
|
||||
$portableTools = Full $PortableToolsDirectory
|
||||
$dependencies = Full $DependencySourceDirectory
|
||||
$stage = Full $StageDirectory
|
||||
$dotnet = (Get-Command dotnet -CommandType Application).Source
|
||||
$cmake = Join-Path $portableTools 'CMake/bin/cmake.exe'
|
||||
$ninja = Join-Path $portableTools 'Ninja/ninja.exe'
|
||||
$compilerBin = Join-Path $portableTools 'llvm-mingw/bin'
|
||||
Assert-File $cmake 'Pinned CMake (run Prepare-PortableTools.ps1 first)'
|
||||
Assert-File $ninja 'Pinned Ninja'
|
||||
Assert-File (Join-Path $dependencies 'cppwinrt/winrt/base.h') 'Pinned dependencies (run Prepare-Dependencies.ps1 first)'
|
||||
|
||||
# Refuse reuse so a developer's game translation or an earlier build cannot make
|
||||
# the test pass. Keep the staging tree after the run for diagnostics.
|
||||
if (Test-Path -LiteralPath $stage) { throw "Test stage already exists; choose a fresh -StageDirectory: $stage" }
|
||||
[IO.Directory]::CreateDirectory($stage) | Out-Null
|
||||
Write-Host "Synthetic recompilation workspace: $stage"
|
||||
|
||||
# Copy current sources, including uncommitted edits, but no ignored build output.
|
||||
# An isolated workspace preserves the developer's real generated/ directory.
|
||||
$sourceFiles = & git -C $repoRoot -c core.quotepath=false ls-files --cached --others --exclude-standard -- runtime aurora-main
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not enumerate runtime and Aurora sources.' }
|
||||
foreach ($relative in $sourceFiles | Sort-Object -Unique) {
|
||||
$destination = Join-Path $stage $relative
|
||||
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination)) | Out-Null
|
||||
Copy-Item -LiteralPath (Join-Path $repoRoot $relative) -Destination $destination
|
||||
}
|
||||
|
||||
# One synthetic text section: li r3,40; addi r3,r3,2; nop; blr.
|
||||
# Native HLE wrappers also call these eight guest symbols directly. Give each
|
||||
# its own generated blr function so the real product can link without game code.
|
||||
# Keep this list explicit: a new unresolved guest dependency must fail the test.
|
||||
[uint32]$entry = 0x80001000L
|
||||
[uint32[]]$guestCallbacks = @(
|
||||
0x8012B830L, 0x801A0620L, 0x801A1ED8L, 0x801A961CL,
|
||||
0x801AADE0L, 0x801D8D30L, 0x801D9E94L, 0x8055531CL
|
||||
)
|
||||
$textSize = [int]($guestCallbacks[-1] - $entry + 4)
|
||||
$dataOffset = 0x100 + $textSize
|
||||
$dol = [byte[]]::new($dataOffset + 4)
|
||||
function Write-BigEndian32([int]$Offset, [uint32]$Value) {
|
||||
$dol[$Offset] = [byte](($Value -shr 24) -band 255)
|
||||
$dol[$Offset + 1] = [byte](($Value -shr 16) -band 255)
|
||||
$dol[$Offset + 2] = [byte](($Value -shr 8) -band 255)
|
||||
$dol[$Offset + 3] = [byte]($Value -band 255)
|
||||
}
|
||||
Write-BigEndian32 0x00 0x100 # text[0] file offset
|
||||
Write-BigEndian32 0x48 $entry # text[0] guest address
|
||||
Write-BigEndian32 0x90 $textSize # text[0] length (unreachable gaps are zero)
|
||||
Write-BigEndian32 0x1C $dataOffset # data[0] file offset
|
||||
Write-BigEndian32 0x64 0x80600000L # data[0] guest address
|
||||
Write-BigEndian32 0xAC 4 # data[0] length
|
||||
Write-BigEndian32 0xD8 0x80601000L # BSS address
|
||||
Write-BigEndian32 0xDC 32 # BSS length
|
||||
Write-BigEndian32 0xE0 $entry # entry point
|
||||
Write-BigEndian32 0x100 0x38600028 # li r3,40
|
||||
Write-BigEndian32 0x104 0x38630002 # addi r3,r3,2
|
||||
Write-BigEndian32 0x108 0x60000000 # nop
|
||||
Write-BigEndian32 0x10C 0x4E800020 # blr
|
||||
foreach ($address in $guestCallbacks) {
|
||||
Write-BigEndian32 ([int](0x100 + $address - $entry)) 0x4E800020
|
||||
}
|
||||
Write-BigEndian32 $dataOffset 0x12345678
|
||||
[IO.File]::WriteAllBytes((Join-Path $stage 'synthetic.dol'), $dol)
|
||||
$entryPoints = (@($entry) + $guestCallbacks | ForEach-Object { '0x{0:X8}' -f $_ }) -join ', '
|
||||
$functionMap = (@($entry) + $guestCallbacks | ForEach-Object { '{0:X8} func_{0:X8}' -f $_ }) -join "`n"
|
||||
[IO.File]::WriteAllText((Join-Path $stage 'synthetic-functions.txt'), $functionMap)
|
||||
$manifest = Join-Path $stage 'recomp.yml'
|
||||
[IO.File]::WriteAllText($manifest, @"
|
||||
schema_version: 1
|
||||
workspace_root: .
|
||||
project:
|
||||
id: ci-synthetic-dol
|
||||
display_name: CI Synthetic DOL
|
||||
memory:
|
||||
base: 0x80000000
|
||||
size: 0x01800000
|
||||
sda_base: 0x80600000
|
||||
sda2_base: 0x80600000
|
||||
inputs:
|
||||
dol:
|
||||
path: synthetic.dol
|
||||
translation:
|
||||
entry_points: [$entryPoints]
|
||||
function_map:
|
||||
path: synthetic-functions.txt
|
||||
allow_unsupported_instructions: false
|
||||
runtime:
|
||||
native_abi_directories: []
|
||||
native_registration_root: runtime/src
|
||||
output:
|
||||
root: generated
|
||||
"@)
|
||||
|
||||
$translatorProject = Join-Path $repoRoot 'translator/src/Translator.Cli/Translator.Cli.csproj'
|
||||
Invoke-Checked $dotnet @('build', $translatorProject, '-c', 'Release', '--disable-build-servers') 'Building the translator'
|
||||
$translator = Join-Path $repoRoot 'translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll'
|
||||
$metadata = Join-Path $stage 'generated/base_translation_output.json'
|
||||
Invoke-Checked $dotnet @($translator, 'translate-recursive', '0x80001000', '--project', $manifest,
|
||||
'--output-metadata', $metadata, '--threads', "$Parallel") `
|
||||
'Translating the synthetic DOL'
|
||||
# Function-map seeds can be skipped by discovery; do not accept a partial fixture.
|
||||
$translated = Get-Content -LiteralPath $metadata -Raw | ConvertFrom-Json
|
||||
foreach ($address in @($entry) + $guestCallbacks) {
|
||||
if ($address -notin $translated.functions.entryPoint) {
|
||||
throw ('Synthetic function 0x{0:X8} was not translated.' -f $address)
|
||||
}
|
||||
}
|
||||
Invoke-Checked $dotnet @($translator, 'generate-data-init', '--project', $manifest) 'Generating synthetic data and runtime configuration'
|
||||
Invoke-Checked $dotnet @($translator, 'emit-build-shards', '--project', $manifest) 'Emitting the production build graph'
|
||||
|
||||
$nativeBuild = Join-Path $stage 'native-build'
|
||||
$oldPath = $env:PATH
|
||||
try {
|
||||
$env:PATH = Get-MkwToolchainPath $portableTools
|
||||
$configure = Get-MkwNativeConfigureArguments -SourceDirectory (Join-Path $stage 'runtime') -BuildDirectory $nativeBuild `
|
||||
-Ninja $ninja -CCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang.exe') `
|
||||
-CxxCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang++.exe') `
|
||||
-ResourceCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-windres.exe') `
|
||||
-DependenciesDirectory $dependencies -AdditionalArguments @('-DMKW_BUILD_PRODUCTS=ON')
|
||||
Invoke-Checked $cmake $configure 'Configuring the production Windows runtime'
|
||||
Invoke-Checked $cmake @('--build', $nativeBuild, '--target', 'WiiCompiled', '--parallel', "$Parallel") `
|
||||
'Compiling and linking the synthetic product with the full runtime'
|
||||
Assert-File (Join-Path $nativeBuild 'WiiCompiled.exe') 'Linked synthetic product'
|
||||
} finally {
|
||||
$env:PATH = $oldPath
|
||||
}
|
||||
Write-Host 'Synthetic recompilation passed (translation, data generation, runtime compilation, and product link).'
|
||||
@@ -6,7 +6,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace>
|
||||
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
|
||||
<Version>0.2.22</Version>
|
||||
<Version>0.2.29</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description>
|
||||
|
||||
@@ -53,9 +53,22 @@ public static class RetroWfcPayload
|
||||
|
||||
var root = Path.GetFullPath(stagedDirectory);
|
||||
var payload = Path.Combine(root, RetroWfcOfflinePayloadFile);
|
||||
if (!File.Exists(payload))
|
||||
try
|
||||
{
|
||||
if ((File.GetAttributes(payload) & FileAttributes.Directory) != 0)
|
||||
throw new InvalidDataException(
|
||||
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
|
||||
}
|
||||
ValidateRetroWfcPayloadFile(payload, signingKey);
|
||||
return root;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
|
||||
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
|
||||
<Version>0.2.22</Version>
|
||||
<Version>0.2.29</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description>
|
||||
|
||||
@@ -12,7 +12,9 @@ internal static class BuildRunner
|
||||
public static async Task RunAsync(
|
||||
string workspace, string profile, string outputDir, string? baseOutputDir,
|
||||
string? retroDir, string? retroWfcOfflineDir, bool skipRetroWfcPayload,
|
||||
bool forceCleanBuild, string? translatorBin, IInstallReporter reporter, CancellationToken cancellationToken)
|
||||
bool forceCleanBuild, string? translatorBin, string? ccBin, string? cxxBin, string? fuseLd,
|
||||
string? cmakeBin, string? ninjaBin, string? nativePrebuiltDir, IInstallReporter reporter,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var script = Path.Combine(workspace, "Launcher", "local-build.sh");
|
||||
if (!File.Exists(script)) throw new FileNotFoundException("local-build.sh is missing", script);
|
||||
@@ -47,6 +49,34 @@ internal static class BuildRunner
|
||||
{
|
||||
startInfo.ArgumentList.Add("--translator-bin"); startInfo.ArgumentList.Add(translatorBin);
|
||||
}
|
||||
// Forwarded by AppRun so the AppImage's bundled clang/lld (see prepare-portable-clang.sh)
|
||||
// is used instead of local-build.sh's own default of whatever clang is on $PATH.
|
||||
if (!string.IsNullOrEmpty(ccBin))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--cc"); startInfo.ArgumentList.Add(ccBin);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(cxxBin))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--cxx"); startInfo.ArgumentList.Add(cxxBin);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(fuseLd))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--fuse-ld"); startInfo.ArgumentList.Add(fuseLd);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(cmakeBin))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--cmake"); startInfo.ArgumentList.Add(cmakeBin);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(ninjaBin))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--ninja"); startInfo.ArgumentList.Add(ninjaBin);
|
||||
}
|
||||
// Forwarded by AppRun so the AppImage's bundled precompiled aurora/third-party package (see
|
||||
// Prepare-NativePrebuilt.sh) is used instead of local-build.sh compiling aurora-main itself.
|
||||
if (!string.IsNullOrEmpty(nativePrebuiltDir))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--native-prebuilt-dir"); startInfo.ArgumentList.Add(nativePrebuiltDir);
|
||||
}
|
||||
|
||||
using var process = new Process { StartInfo = startInfo };
|
||||
var window = new BuildProgressWindow(reporter, InstallStages.Build, start: 6, end: 96);
|
||||
|
||||
@@ -3,7 +3,7 @@ namespace WiiCompiled.Setup.Linux;
|
||||
internal static class ProductInfo
|
||||
{
|
||||
public const string Name = "WiiCompiled";
|
||||
public const string Version = "0.2.22";
|
||||
public const string Version = "0.2.29";
|
||||
}
|
||||
|
||||
/// <summary>One installed product's record inside install-state.json.</summary>
|
||||
|
||||
@@ -150,6 +150,12 @@ internal static class Program
|
||||
skipPayload,
|
||||
flags.ContainsKey("force-clean-build"),
|
||||
flags.GetValueOrDefault("translator-bin"),
|
||||
flags.GetValueOrDefault("cc"),
|
||||
flags.GetValueOrDefault("cxx"),
|
||||
flags.GetValueOrDefault("fuse-ld"),
|
||||
flags.GetValueOrDefault("cmake"),
|
||||
flags.GetValueOrDefault("ninja"),
|
||||
flags.GetValueOrDefault("native-prebuilt-dir"),
|
||||
reporter, token);
|
||||
|
||||
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
|
||||
{--download-retro-wfc-payload | --skip-retro-wfc-payload}]
|
||||
[--force-clean-build] [--translator-bin PATH] [--disc-tool-bin PATH]
|
||||
[--progress-json] [--workspace DIR]
|
||||
[--cc PATH] [--cxx PATH] [--fuse-ld NAME_OR_PATH] [--cmake PATH] [--ninja PATH]
|
||||
[--native-prebuilt-dir DIR] [--progress-json] [--workspace DIR]
|
||||
uninstall
|
||||
launch-base
|
||||
launch-retro
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>WiiCompiled.Setup.Linux</AssemblyName>
|
||||
<RootNamespace>WiiCompiled.Setup.Linux</RootNamespace>
|
||||
<Version>0.2.22</Version>
|
||||
<Version>0.2.29</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Command-line installer and launcher for WiiCompiled on Linux</Description>
|
||||
|
||||
@@ -22,6 +22,9 @@ internal sealed class InstallerEngine
|
||||
var existing = new Installation(installDirectory);
|
||||
|
||||
PortableInstallHealing.HealMovedInstall(existing, _reporter);
|
||||
// Capture this before publishing anything. An existing installation may be repaired or
|
||||
// updated by this invocation, but those operations must not recreate the user's shortcuts.
|
||||
var firstInstall = !existing.IsPresent;
|
||||
var previousState = existing.ReadInstallState();
|
||||
using var scratch = Directory.Exists(installDirectory)
|
||||
? InstallScratchSpace.CreateInsideInstall(installDirectory, _reporter)
|
||||
@@ -139,7 +142,7 @@ internal sealed class InstallerEngine
|
||||
updatedState.RetroRewindInstalled, candidateRuntimeAssetsFingerprint,
|
||||
remainingCancellation);
|
||||
Publish(staging, installDirectory, canonicalRetroRoot, updatedState,
|
||||
releaseEntries, remainingCancellation);
|
||||
releaseEntries, remainingCancellation, createShortcuts: firstInstall);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,7 +164,8 @@ internal sealed class InstallerEngine
|
||||
|
||||
await PublishToolkitAndReconcileProductsAsync(existing, staging, workspace, manifest,
|
||||
previousState, options, canonicalRetroRoot, retroCompileInputs,
|
||||
publishGameAssets: reusableGameAssets is null, cancellationToken);
|
||||
publishGameAssets: reusableGameAssets is null, createShortcuts: firstInstall,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -186,7 +190,7 @@ internal sealed class InstallerEngine
|
||||
string stagedWorkspace, PayloadManifest manifest, InstallState? previousState,
|
||||
InstallOptions options, string? canonicalRetroRoot,
|
||||
RetroRewindCompileInputs? retroCompileInputs,
|
||||
bool publishGameAssets, CancellationToken cancellationToken)
|
||||
bool publishGameAssets, bool createShortcuts, CancellationToken cancellationToken)
|
||||
{
|
||||
var installDirectory = existing.Root;
|
||||
|
||||
@@ -214,7 +218,8 @@ internal sealed class InstallerEngine
|
||||
if (publishGameAssets) AddComponent(entries, staging, installDirectory, "GameAssets");
|
||||
|
||||
Publish(staging, installDirectory, canonicalRetroRoot, state,
|
||||
entries, cancellationToken, progressPercent: 8, completionPercent: 10);
|
||||
entries, cancellationToken, progressPercent: 8, completionPercent: 10,
|
||||
createShortcuts: createShortcuts);
|
||||
|
||||
_reporter.Progress(InstallStages.BuildBase,
|
||||
"Producing the installed products with the published toolkit...", 11);
|
||||
@@ -277,7 +282,7 @@ internal sealed class InstallerEngine
|
||||
private void Publish(string staging, string installDirectory,
|
||||
string? canonicalRetroRoot, InstallState state,
|
||||
List<InstallTransactionEntry> entries, CancellationToken cancellationToken,
|
||||
int progressPercent = 95, int completionPercent = 99)
|
||||
int progressPercent = 95, int completionPercent = 99, bool createShortcuts = false)
|
||||
{
|
||||
entries.Add(InstallTransactionEntry.Directory(Path.Combine(staging, "licenses"),
|
||||
Path.Combine(installDirectory, "licenses")));
|
||||
@@ -322,7 +327,8 @@ internal sealed class InstallerEngine
|
||||
{
|
||||
ShellIntegration.RegisterUninstaller(installDirectory, state.RetroRewindInstalled);
|
||||
}
|
||||
ShellIntegration.CreateShortcuts(installDirectory);
|
||||
if (createShortcuts)
|
||||
ShellIntegration.CreateShortcuts(installDirectory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -121,7 +121,7 @@ internal static class PlatformChecks
|
||||
internal static class ProductInfo
|
||||
{
|
||||
public const string Name = "WiiCompiled";
|
||||
public const string Version = "0.2.25";
|
||||
public const string Version = "0.2.29";
|
||||
|
||||
/// <summary>
|
||||
/// The setup executable is copied into the installation under this name. It is the launcher and
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<AssemblyName>WiiCompiled.Setup</AssemblyName>
|
||||
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Version>0.2.25</Version>
|
||||
<Version>0.2.29</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Command-line installer and launcher for WiiCompiled</Description>
|
||||
|
||||
Regular → Executable
+144
-14
@@ -5,8 +5,12 @@
|
||||
# self-contained binaries, and `nodtool` (a prebuilt MIT/Apache-2.0 CLI from encounter/nod, see
|
||||
# NodToolProvider.cs) is downloaded and bundled too - AppRun passes --translator-bin and
|
||||
# --disc-tool-bin so local-build.sh/DiscTool.cs skip their from-source/download fallbacks entirely.
|
||||
# It still shells out to system clang/cmake/ninja - no C/C++ toolchain is bundled, matching
|
||||
# Launcher/local-build.sh's own remaining prerequisites.
|
||||
# A pruned native clang/lld/cmake/ninja toolchain (see prepare-portable-tools.sh) is bundled the
|
||||
# same way - AppRun passes --cc/--cxx/--fuse-ld/--cmake/--ninja so local-build.sh never has to find
|
||||
# a system compiler, CMake, or Ninja. It still shells out to system pkg-config and Vulkan headers,
|
||||
# matching Launcher/local-build.sh's own remaining prerequisites. A precompiled aurora +
|
||||
# third-party package (see Prepare-NativePrebuilt.sh) is bundled the same way too - AppRun passes
|
||||
# --native-prebuilt-dir so local-build.sh never compiles aurora-main from source at all.
|
||||
#
|
||||
# An AppImage mounts read-only, but local-build.sh writes generated/, native-build/, Assets/, etc.
|
||||
# into the workspace it's given. So AppRun (written below) copies the bundled workspace snapshot
|
||||
@@ -15,12 +19,58 @@
|
||||
# - generated/native-build/Assets/PulsarPacks live only in that writable cache and are never
|
||||
# touched by the sync, so local-build.sh's own incremental caching survives across runs and across
|
||||
# AppImage updates. translator/ isn't part of this snapshot at all: it's published as its own
|
||||
# self-contained binary (usr/bin/translator-cli) below and never needs a writable copy.
|
||||
# self-contained binary (usr/bin/translator-cli) below and never needs a writable copy. Neither
|
||||
# native-prebuilt/ nor the toolchain are copied into the cache either - both are large
|
||||
# (~90 MiB / ~500 MiB) and local-build.sh only ever reads from them - but AppRun does point
|
||||
# $CACHE/toolchain and $CACHE/native-prebuilt symlinks at the current mount on every single launch
|
||||
# (see AppRun's own comment): an AppImage's FUSE mount is at a fresh random /tmp/.mount_XXXXXX
|
||||
# every run, and CMake bakes whatever compiler/tool path it's given directly into each
|
||||
# build.ninja rule's command line, so referencing $HERE straight would change that command line -
|
||||
# and Ninja reruns any rule whose command line changed - forcing a full rebuild on every single
|
||||
# launch even though the compiler itself never actually changed. The symlink keeps the path
|
||||
# string CMake/Ninja see identical across runs while what it resolves to tracks the current mount
|
||||
# underneath.
|
||||
set -euo pipefail
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
workspace=$(cd "$script_dir/.." && pwd)
|
||||
|
||||
# `uname -m` reports the *kernel's* architecture, which can differ from userspace - an aarch64
|
||||
# kernel can run a 32-bit armhf userland (as shipped by 32-bit Raspberry Pi OS), same as an x86_64
|
||||
# kernel can run an i686 one. What matters here is which userspace binaries (dotnet, appimagetool)
|
||||
# will actually run, so this reads the ELF header of this script's own running bash interpreter -
|
||||
# real userspace - rather than trusting the kernel's self-report. /proc/$$/exe (not /proc/self/exe:
|
||||
# that would resolve inside the readlink subprocess below, to readlink itself, not to bash) is this
|
||||
# shell's own PID. EI_CLASS (byte 4: 1=32-bit, 2=64-bit) and e_machine (bytes 18-19: 3=EM_386,
|
||||
# 40=EM_ARM, 62=EM_X86_64, 183=EM_AARCH64) are read as plain little-endian bytes, which every
|
||||
# real-world x86/ARM Linux userland uses; ELF's big-endian encoding is a non-issue here since no
|
||||
# Linux distro ships a big-endian x86 or ARM userland.
|
||||
elf_exe=$(readlink -f "/proc/$$/exe")
|
||||
elf_class=$(od -An -t u1 -j 4 -N 1 "$elf_exe" | tr -d ' ')
|
||||
elf_machine_lo=$(od -An -t u1 -j 18 -N 1 "$elf_exe" | tr -d ' ')
|
||||
elf_machine_hi=$(od -An -t u1 -j 19 -N 1 "$elf_exe" | tr -d ' ')
|
||||
elf_machine=$(( elf_machine_hi * 256 + elf_machine_lo ))
|
||||
|
||||
# Mirrors the host-architecture detection NodToolProvider.cs already does (RuntimeInformation.
|
||||
# OSArchitecture) so this script's own dotnet RID and appimagetool selection agree with the
|
||||
# nodtool binary that same code path resolves below. local-build.sh needs no such mapping itself:
|
||||
# it just drives the native CMake configure, which already accepts x86_64 or aarch64 natively
|
||||
# (see runtime/CMakeLists.txt's CMAKE_SYSTEM_PROCESSOR check).
|
||||
case "$elf_class:$elf_machine" in
|
||||
2:62)
|
||||
dotnet_rid=linux-x64
|
||||
appimagetool_arch=x86_64
|
||||
;;
|
||||
2:183)
|
||||
dotnet_rid=linux-arm64
|
||||
appimagetool_arch=aarch64
|
||||
;;
|
||||
*)
|
||||
echo "build-appimage.sh: unsupported userspace architecture (ELF class $elf_class, machine $elf_machine) - WiiCompiled requires a 64-bit x86_64 or aarch64 userland" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
output_dir="$workspace/Launcher/dist"
|
||||
appimagetool_override=""
|
||||
|
||||
@@ -40,10 +90,10 @@ appdir="$workspace/Launcher/artifacts/appimage-build/AppDir"
|
||||
rm -rf "$appdir"
|
||||
mkdir -p "$appdir/usr/bin" "$appdir/workspace/Launcher"
|
||||
|
||||
echo "Publishing the installer (self-contained linux-x64)..."
|
||||
echo "Publishing the installer (self-contained $dotnet_rid)..."
|
||||
publish_tmp="$workspace/Launcher/artifacts/appimage-build/publish"
|
||||
rm -rf "$publish_tmp"
|
||||
dotnet publish "$workspace/Launcher/WiiCompiled.Setup.Linux" -c Release -r linux-x64 \
|
||||
dotnet publish "$workspace/Launcher/WiiCompiled.Setup.Linux" -c Release -r "$dotnet_rid" \
|
||||
--self-contained -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \
|
||||
-o "$publish_tmp"
|
||||
cp "$publish_tmp/WiiCompiled.Setup.Linux" "$appdir/usr/bin/wiicompiled-setup"
|
||||
@@ -52,10 +102,10 @@ chmod +x "$appdir/usr/bin/wiicompiled-setup"
|
||||
# Published as a self-contained binary too, so an AppImage user never needs a `dotnet` SDK on
|
||||
# PATH at all - local-build.sh is told about it via --translator-bin and skips its own
|
||||
# dotnet-build-from-source step entirely (see local-build.sh's translator resolution branch).
|
||||
echo "Publishing the translator (self-contained linux-x64)..."
|
||||
echo "Publishing the translator (self-contained $dotnet_rid)..."
|
||||
translator_publish_tmp="$workspace/Launcher/artifacts/appimage-build/publish-translator"
|
||||
rm -rf "$translator_publish_tmp"
|
||||
dotnet publish "$workspace/translator/src/Translator.Cli" -c Release -r linux-x64 \
|
||||
dotnet publish "$workspace/translator/src/Translator.Cli" -c Release -r "$dotnet_rid" \
|
||||
--self-contained -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \
|
||||
-o "$translator_publish_tmp"
|
||||
cp "$translator_publish_tmp/Translator.Cli" "$appdir/usr/bin/translator-cli"
|
||||
@@ -71,6 +121,47 @@ nodtool_path=$(dotnet run --project "$workspace/Launcher/WiiCompiled.Setup.Commo
|
||||
cp "$nodtool_path" "$appdir/usr/bin/nodtool"
|
||||
chmod +x "$appdir/usr/bin/nodtool"
|
||||
|
||||
echo "Preparing the portable clang/lld/cmake/ninja toolchain ($appimagetool_arch)..."
|
||||
bash "$script_dir/prepare-portable-tools.sh" --arch "$appimagetool_arch"
|
||||
mkdir -p "$appdir/usr/toolchain"
|
||||
cp -a "$workspace/Launcher/artifacts/portable-tools/toolchain-$appimagetool_arch"/. "$appdir/usr/toolchain/"
|
||||
|
||||
# Precompiled aurora + third-party package (see Prepare-NativePrebuilt.sh) so a user's own
|
||||
# local-build.sh never has to compile aurora itself (~43% of local build CPU time). Re-harvesting
|
||||
# recompiles the whole aurora/Crypto++ closure with the toolchain above, so this is skipped unless
|
||||
# --print-fingerprint-only (a fast, build-free check) says the existing package no longer matches
|
||||
# the current compiler/flags/aurora/third_party sources.
|
||||
native_prebuilt_dir="$workspace/Launcher/artifacts/native-prebuilt-$appimagetool_arch"
|
||||
echo "Checking whether the precompiled aurora + third-party package ($appimagetool_arch) is current..."
|
||||
current_fingerprint=$(bash "$script_dir/Prepare-NativePrebuilt.sh" --arch "$appimagetool_arch" --print-fingerprint-only)
|
||||
package_current=0
|
||||
if [[ -f "$native_prebuilt_dir/provenance.json" ]]; then
|
||||
package_current=$(CURRENT_FINGERPRINT="$current_fingerprint" python3 - "$native_prebuilt_dir/provenance.json" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
provenance = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||
current = dict(line.split("=", 1) for line in os.environ["CURRENT_FINGERPRINT"].splitlines() if line)
|
||||
fields = {
|
||||
"compiler_sha256": "CompilerSha256",
|
||||
"flag_fingerprint": "FlagFingerprint",
|
||||
"aurora_fingerprint": "AuroraSourceFingerprint",
|
||||
"third_party_fingerprint": "ThirdPartySourceFingerprint",
|
||||
}
|
||||
print(1 if all(provenance.get(v) == current.get(k) for k, v in fields.items()) else 0)
|
||||
PY
|
||||
)
|
||||
fi
|
||||
if [[ "$package_current" == "1" ]]; then
|
||||
echo "Native prebuilt package is current; reusing $native_prebuilt_dir"
|
||||
else
|
||||
echo "Native prebuilt package is missing or stale; harvesting a fresh one (compiles aurora once, can take a while)..."
|
||||
bash "$script_dir/Prepare-NativePrebuilt.sh" --arch "$appimagetool_arch"
|
||||
fi
|
||||
mkdir -p "$appdir/native-prebuilt"
|
||||
cp -a "$native_prebuilt_dir/." "$appdir/native-prebuilt/"
|
||||
|
||||
echo "Staging the bundled workspace snapshot..."
|
||||
for dir in runtime aurora-main projects; do
|
||||
cp -r "$workspace/$dir" "$appdir/workspace/$dir"
|
||||
@@ -84,8 +175,21 @@ find "$appdir/workspace/aurora-main/extern" -mindepth 1 -maxdepth 1 -type d -exe
|
||||
rm -rf "$appdir/workspace/runtime/build"
|
||||
cp "$workspace/Launcher/local-build.sh" "$appdir/workspace/Launcher/local-build.sh"
|
||||
|
||||
# AppRun re-syncs runtime/aurora-main/projects/local-build.sh into the writable cache only when
|
||||
# this changes, so it must change whenever any of those bundled paths actually did - a bare commit
|
||||
# hash gets this wrong for an uncommitted change (verified directly: rebuilding after editing
|
||||
# local-build.sh with no commit produced the same hash as the stale cache, so AppRun kept serving
|
||||
# the old script and failed on a flag that didn't exist yet). `git status --porcelain` catches both
|
||||
# modified tracked files and new untracked ones; appending a fresh timestamp when it's non-empty
|
||||
# guarantees this never matches a previous build's stamp, forcing a resync every time the tree is
|
||||
# dirty. A clean tree (a real tagged release) keeps the stable commit-hash behavior, so identical
|
||||
# reruns of the same release AppImage don't resync needlessly.
|
||||
if git -C "$workspace" rev-parse HEAD >/dev/null 2>&1; then
|
||||
git -C "$workspace" rev-parse HEAD > "$appdir/workspace/.bundle-version"
|
||||
version=$(git -C "$workspace" rev-parse HEAD)
|
||||
if [[ -n "$(git -C "$workspace" status --porcelain 2>/dev/null)" ]]; then
|
||||
version="$version-dirty-$(date -u +%s)"
|
||||
fi
|
||||
echo "$version" > "$appdir/workspace/.bundle-version"
|
||||
else
|
||||
date -u +%s > "$appdir/workspace/.bundle-version"
|
||||
fi
|
||||
@@ -96,6 +200,7 @@ cat > "$appdir/AppRun" <<'APPRUN'
|
||||
set -euo pipefail
|
||||
HERE="$(dirname "$(readlink -f "$0")")"
|
||||
CACHE="${XDG_DATA_HOME:-$HOME/.local/share}/WiiCompiled/workspace"
|
||||
mkdir -p "$CACHE"
|
||||
if [ ! -f "$CACHE/.bundle-version" ] || \
|
||||
[ "$(cat "$HERE/workspace/.bundle-version")" != "$(cat "$CACHE/.bundle-version")" ]; then
|
||||
mkdir -p "$CACHE/Launcher"
|
||||
@@ -106,9 +211,31 @@ if [ ! -f "$CACHE/.bundle-version" ] || \
|
||||
cp "$HERE/workspace/Launcher/local-build.sh" "$CACHE/Launcher/local-build.sh"
|
||||
cp "$HERE/workspace/.bundle-version" "$CACHE/.bundle-version"
|
||||
fi
|
||||
# toolchain/ and native-prebuilt/ are NOT copied into the cache (they're large - ~500 MiB /
|
||||
# ~90 MiB - and local-build.sh only ever reads from them): $CACHE/toolchain and
|
||||
# $CACHE/native-prebuilt are symlinks re-pointed at the current mount on every single launch
|
||||
# (unconditionally, not gated on .bundle-version above, since the mount path itself - unlike the
|
||||
# bundled content - changes every run regardless). CMake bakes a compiler/tool path directly into
|
||||
# each build.ninja rule's command line and Ninja reruns any rule whose command line changed since
|
||||
# the last build (verified directly) - an AppImage's FUSE mount is at a fresh random
|
||||
# /tmp/.mount_XXXXXX every launch, so referencing $HERE straight would change that command line,
|
||||
# and therefore force a full rebuild, on every single run even though the compiler itself never
|
||||
# actually changed. A symlink keeps the *path string* CMake/Ninja see identical across runs while
|
||||
# what it resolves to tracks the current mount underneath (verified directly: CMake records
|
||||
# whatever path it's given as-is - including a symlink - without resolving it first).
|
||||
[ -L "$CACHE/toolchain" ] || rm -rf "$CACHE/toolchain"
|
||||
[ -L "$CACHE/native-prebuilt" ] || rm -rf "$CACHE/native-prebuilt"
|
||||
ln -sfn "$HERE/usr/toolchain" "$CACHE/toolchain"
|
||||
ln -sfn "$HERE/native-prebuilt" "$CACHE/native-prebuilt"
|
||||
exec "$HERE/usr/bin/wiicompiled-setup" --workspace "$CACHE" \
|
||||
--translator-bin "$HERE/usr/bin/translator-cli" \
|
||||
--disc-tool-bin "$HERE/usr/bin/nodtool" "$@"
|
||||
--disc-tool-bin "$HERE/usr/bin/nodtool" \
|
||||
--cc "$CACHE/toolchain/bin/clang" \
|
||||
--cxx "$CACHE/toolchain/bin/clang++" \
|
||||
--fuse-ld lld \
|
||||
--cmake "$CACHE/toolchain/bin/cmake" \
|
||||
--ninja "$CACHE/toolchain/bin/ninja" \
|
||||
--native-prebuilt-dir "$CACHE/native-prebuilt" "$@"
|
||||
APPRUN
|
||||
chmod +x "$appdir/AppRun"
|
||||
|
||||
@@ -155,11 +282,13 @@ PY
|
||||
echo "Resolving appimagetool..."
|
||||
appimagetool="$appimagetool_override"
|
||||
if [[ -z "$appimagetool" ]]; then
|
||||
appimagetool="$workspace/Launcher/artifacts/appimagetool"
|
||||
# Cache path is arch-tagged so a workspace shared or synced across an x86_64 and an aarch64
|
||||
# machine never picks up the wrong architecture's cached binary.
|
||||
appimagetool="$workspace/Launcher/artifacts/appimagetool-$appimagetool_arch"
|
||||
if [[ ! -x "$appimagetool" ]]; then
|
||||
echo "Downloading appimagetool..."
|
||||
echo "Downloading appimagetool ($appimagetool_arch)..."
|
||||
mkdir -p "$(dirname "$appimagetool")"
|
||||
curl -fsSL "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" \
|
||||
curl -fsSL "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-$appimagetool_arch.AppImage" \
|
||||
-o "$appimagetool"
|
||||
chmod +x "$appimagetool"
|
||||
fi
|
||||
@@ -169,5 +298,6 @@ mkdir -p "$output_dir"
|
||||
echo "Packaging..."
|
||||
# appimagetool detects the target architecture from the first ELF executable it finds in the
|
||||
# AppDir; AppRun here is a shell script, not ELF, so ARCH must be set explicitly.
|
||||
ARCH=x86_64 "$appimagetool" "$appdir" "$output_dir/WiiCompiled-Setup-x86_64.AppImage"
|
||||
echo "Built: $output_dir/WiiCompiled-Setup-x86_64.AppImage"
|
||||
output_name="WiiCompiled-Setup-$appimagetool_arch.AppImage"
|
||||
ARCH="$appimagetool_arch" "$appimagetool" "$appdir" "$output_dir/$output_name"
|
||||
echo "Built: $output_dir/$output_name"
|
||||
|
||||
Executable
+139
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env bash
|
||||
# Native macOS build automation: optionally extract -> translate -> compile -> publish .app.
|
||||
set -euo pipefail
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
default_workspace=$(cd "$script_dir/.." && pwd)
|
||||
|
||||
fail() { printf 'local-build-macos.command: error: %s\n' "$*" >&2; exit 1; }
|
||||
step() { printf 'MKWCBUILD:STEP:%s %s\n' "$1" "$2"; }
|
||||
assert_file() { [[ -f "$1" ]] || fail "$2 is missing: $1"; }
|
||||
sha256() { shasum -a 256 "$1" | awk '{ print $1 }'; }
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: local-build-macos.command --output-dir DIR [options]
|
||||
|
||||
--workspace DIR Repository root (default: this script's parent directory)
|
||||
--profile {base|retro-rewind|both} Product to build (default: base)
|
||||
--output-dir DIR Output .app directory (required; Retro Rewind for both)
|
||||
--base-output-dir DIR Base .app directory (required with --profile both)
|
||||
--game IMAGE --nodtool PATH Extract and verify a clean PAL RMCP01 disc image first
|
||||
--retro-rewind-package-dir DIR RetroRewind6 directory (required for Retro Rewind)
|
||||
--retro-wfc-offline-dir DIR Directory containing binary/payload.RMCPD00.bin
|
||||
--skip-retro-wfc-payload Build Retro Rewind without the shared Retro-WFC payload
|
||||
--force-clean-build Delete local generated and native-build-macos caches
|
||||
--parallel N Pin translation and build parallelism
|
||||
--cmake PATH --ninja PATH Override build tools
|
||||
--dotnet PATH Override dotnet
|
||||
--translator-bin PATH Use a self-contained Translator.Cli executable
|
||||
EOF
|
||||
}
|
||||
|
||||
workspace="$default_workspace"; profile=base; output_dir=""; base_output_dir=""
|
||||
game=""; nodtool=""; retro_root=""; retro_wfc=""; skip_retro_wfc=0; force_clean=0
|
||||
parallel=0; cmake_bin=cmake; ninja_bin=ninja; dotnet_bin=dotnet; translator_bin=""
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--workspace) workspace=${2:-}; shift 2 ;;
|
||||
--profile) profile=${2:-}; shift 2 ;;
|
||||
--output-dir) output_dir=${2:-}; shift 2 ;;
|
||||
--base-output-dir) base_output_dir=${2:-}; shift 2 ;;
|
||||
--game) game=${2:-}; shift 2 ;;
|
||||
--nodtool) nodtool=${2:-}; shift 2 ;;
|
||||
--retro-rewind-package-dir) retro_root=${2:-}; shift 2 ;;
|
||||
--retro-wfc-offline-dir) retro_wfc=${2:-}; shift 2 ;;
|
||||
--skip-retro-wfc-payload) skip_retro_wfc=1; shift ;;
|
||||
--force-clean-build) force_clean=1; shift ;;
|
||||
--parallel) parallel=${2:-}; shift 2 ;;
|
||||
--cmake) cmake_bin=${2:-}; shift 2 ;;
|
||||
--ninja) ninja_bin=${2:-}; shift 2 ;;
|
||||
--dotnet) dotnet_bin=${2:-}; shift 2 ;;
|
||||
--translator-bin) translator_bin=${2:-}; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ $(uname -s) == Darwin ]] || fail 'this build script is for macOS only'
|
||||
[[ $(uname -m) == arm64 ]] || fail 'the current macOS product target is Apple Silicon only'
|
||||
workspace=$(cd "$workspace" && pwd)
|
||||
[[ -n "$output_dir" ]] || fail '--output-dir is required'
|
||||
case "$profile" in base|retro-rewind|both) ;; *) fail '--profile must be base, retro-rewind, or both' ;; esac
|
||||
builds_retro=0; [[ "$profile" != base ]] && builds_retro=1
|
||||
if [[ "$profile" == both && -z "$base_output_dir" ]]; then fail '--base-output-dir is required with --profile both'; fi
|
||||
if [[ "$profile" != both && -n "$base_output_dir" ]]; then fail '--base-output-dir is valid only with --profile both'; fi
|
||||
if [[ -n "$game" || -n "$nodtool" ]]; then [[ -n "$game" && -n "$nodtool" ]] || fail '--game and --nodtool must be supplied together'; fi
|
||||
if (( builds_retro )); then
|
||||
[[ -n "$retro_root" ]] || fail '--retro-rewind-package-dir is required for Retro Rewind'
|
||||
[[ -n "$retro_wfc" ]] && (( skip_retro_wfc )) && fail 'choose only one Retro-WFC mode'
|
||||
[[ -n "$retro_wfc" || $skip_retro_wfc -eq 1 ]] || fail 'choose a Retro-WFC payload directory or --skip-retro-wfc-payload'
|
||||
fi
|
||||
for tool in "$cmake_bin" "$ninja_bin" clang clang++ shasum; do command -v "$tool" >/dev/null || fail "required tool not found: $tool"; done
|
||||
|
||||
project="$workspace/projects/mkwii/recomp.yml"; assets="$workspace/Assets"; generated="$workspace/generated"
|
||||
functions="$generated/functions"; metadata="$generated/base_translation_output.json"; manifest_dir="$workspace/build/base"
|
||||
manifest="$manifest_dir/mkwii_base_manifest.json"; shards="$generated/build_shards"; native_build="$workspace/native-build-macos"
|
||||
assert_file "$project" 'translation project'
|
||||
if [[ -n "$game" ]]; then "$script_dir/macos/extract-disc.command" --game "$game" --assets-dir "$assets" --nodtool "$nodtool"; fi
|
||||
assert_file "$assets/main.dol" 'extracted main.dol'; assert_file "$assets/StaticR.rel" 'extracted StaticR.rel'
|
||||
|
||||
if (( force_clean )); then
|
||||
step force-clean 'Discarding translation and native build caches'
|
||||
rm -rf "$generated" "$manifest_dir" "$native_build"
|
||||
fi
|
||||
|
||||
if [[ -n "$translator_bin" ]]; then
|
||||
assert_file "$translator_bin" 'Translator.Cli executable'
|
||||
translator() { "$translator_bin" "$@"; }
|
||||
else
|
||||
command -v "$dotnet_bin" >/dev/null || fail "required tool not found: $dotnet_bin"
|
||||
translator_dll="$workspace/translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll"
|
||||
if [[ ! -f "$translator_dll" ]]; then
|
||||
step build-translator 'Building the translator'
|
||||
"$dotnet_bin" build "$workspace/translator/src/Translator.Cli/Translator.Cli.csproj" -c Release
|
||||
fi
|
||||
translator() { "$dotnet_bin" "$translator_dll" "$@"; }
|
||||
fi
|
||||
|
||||
entry_point=$(awk '/^translation:/{inside=1} inside && /^[[:space:]]*-[[:space:]]*0[xX][0-9a-fA-F]+[[:space:]]*$/{gsub(/^[[:space:]]*-[[:space:]]*/, ""); print; exit}' "$project")
|
||||
[[ -n "$entry_point" ]] || fail 'could not find the translation entry point'
|
||||
cpu=$(sysctl -n hw.ncpu); mem_gib=$(( $(sysctl -n hw.memsize) / 1024 / 1024 / 1024 )); (( mem_gib < 1 )) && mem_gib=1
|
||||
if (( parallel > 0 )); then translator_threads=$parallel; translated_jobs=$parallel; global_jobs=$parallel
|
||||
else translator_threads=$(( cpu < 16 ? cpu : 16 )); translated_jobs=$(( cpu < mem_gib / 2 ? cpu : mem_gib / 2 )); (( translated_jobs < 1 )) && translated_jobs=1; global_jobs=$cpu; fi
|
||||
|
||||
if (( builds_retro )); then
|
||||
retro_root=$(cd "$retro_root" && pwd)
|
||||
if [[ ! -f "$retro_root/Binaries/Code.pul" && -f "$retro_root/RetroRewind6/Binaries/Code.pul" ]]; then
|
||||
retro_root="$retro_root/RetroRewind6"
|
||||
fi
|
||||
assert_file "$retro_root/Binaries/Code.pul" 'Retro Rewind Code.pul'
|
||||
stage="$workspace/PulsarPacks/completed/RetroRewind/RetroRewind6/Binaries"
|
||||
mkdir -p "$stage"
|
||||
if [[ ! "$retro_root/Binaries/Code.pul" -ef "$stage/Code.pul" ]]; then
|
||||
cp -f "$retro_root/Binaries/Code.pul" "$stage/Code.pul"
|
||||
fi
|
||||
fi
|
||||
|
||||
step translate-base 'Translating the user-owned base game'
|
||||
rm -rf "$functions" "$metadata" "$manifest_dir"; mkdir -p "$functions" "$manifest_dir"
|
||||
translator translate-recursive "$entry_point" --project "$project" --outdir "$functions" --output-metadata "$metadata" --production-source-bundle "$generated/base_translation_sources.bin" --no-function-files --prune-stale --threads "$translator_threads"
|
||||
step emit-base-manifest 'Creating the local base translation manifest'
|
||||
translator emit-base-manifest --project "$project" --out "$manifest_dir" --functions-dir "$functions" --translation-output-metadata "$metadata" --region P
|
||||
if (( builds_retro )); then
|
||||
mod_out="$workspace/build/mods/retro_rewind_full_cpp"; args=(translate-mod --project "$project" --profile retro-rewind --base-manifest "$manifest" --base-translation-output-metadata "$metadata" --code-pul "$retro_root/Binaries/Code.pul" --mod-root "$retro_root" --mod-name 'Retro Rewind' --region P --out "$mod_out" --prefer-cached-inputs --emit-cpp --threads "$translator_threads")
|
||||
if (( skip_retro_wfc )); then args+=(--skip-retro-wfc); else offline_payload="$retro_wfc/binary/payload.RMCPD00.bin"; assert_file "$offline_payload" 'Offline Retro-WFC payload'; args+=(--retro-wfc-payload "$offline_payload"); fi
|
||||
step translate-mod 'Translating Retro Rewind'; translator "${args[@]}"
|
||||
fi
|
||||
step generate-data-init 'Generating local game data initialization'; translator generate-data-init --project "$project"
|
||||
args=(emit-build-shards --project "$project" --base-metadata "$metadata" --base-functions-dir "$functions" --native-source-dir "$workspace/runtime/src" --out "$shards")
|
||||
if (( builds_retro )); then args+=(--resolved-profile "$mod_out/resolved_dispatch_profile.json" --retro-cpp-dir "$mod_out/cpp"); fi
|
||||
step emit-build-shards 'Preparing native build shards'; translator "${args[@]}"
|
||||
|
||||
step configure-native 'Configuring the native toolchain'
|
||||
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs"
|
||||
targets=(); [[ "$profile" != retro-rewind ]] && targets+=(WiiCompiled); [[ "$profile" != base ]] && targets+=(RetroRewind)
|
||||
step compile "Compiling ${targets[*]} locally"; "$cmake_bin" --build "$native_build" --target "${targets[@]}" --parallel "$global_jobs"
|
||||
if [[ "$profile" != retro-rewind ]]; then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product WiiCompiled --output-dir "${base_output_dir:-$output_dir}"; fi
|
||||
if (( builds_retro )); then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product RetroRewind --output-dir "$output_dir"; fi
|
||||
printf 'MKWCBUILD:OUTPUT=%s\n' "$output_dir"
|
||||
+72
-11
@@ -2,11 +2,12 @@
|
||||
# Linux build automation: translate -> emit build shards -> configure -> compile -> publish.
|
||||
#
|
||||
# This is the native-Linux counterpart to Launcher/LocalBuild.ps1. It is a from-scratch parallel
|
||||
# implementation, not a port of NativeBuildFlags.ps1: that file's canonical flags and
|
||||
# prebuilt-package fingerprinting exist only for the Windows/mingw toolchain (a precompiled
|
||||
# aurora/third-party package, offline pinned dependencies) that this script does not build.
|
||||
# Linux always builds aurora from source, letting its own CMake auto-detect Vulkan + vendor
|
||||
# SDL3/Dawn via FetchContent - the same configuration already verified working by hand.
|
||||
# implementation, not a port of NativeBuildFlags.ps1: that file's canonical flags exist only for
|
||||
# the Windows/mingw toolchain's offline pinned dependencies, which this script does not use.
|
||||
# Without --native-prebuilt-dir, aurora is built from source, letting its own CMake auto-detect
|
||||
# Vulkan + vendor SDL3/Dawn via FetchContent - the same configuration already verified working by
|
||||
# hand. With it, aurora is not compiled at all - see Launcher/Prepare-NativePrebuilt.sh, which
|
||||
# harvests exactly that package (the Linux counterpart to Prepare-NativePrebuilt.ps1).
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -62,6 +63,8 @@ ninja_override=""
|
||||
dotnet_override=""
|
||||
translator_dll_override=""
|
||||
translator_bin_override=""
|
||||
fuse_ld_override=""
|
||||
native_prebuilt_dir=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
@@ -77,10 +80,13 @@ Usage: local-build.sh --output-dir DIR [options]
|
||||
--force-clean-build Discard every translation/build cache first
|
||||
--parallel N Pin translator threads, translated-shard job pool, and Ninja parallelism to N
|
||||
--cc PATH / --cxx PATH C/C++ compiler (default: cc/c++ on PATH)
|
||||
--fuse-ld NAME_OR_PATH Linker passed to clang as -fuse-ld=NAME_OR_PATH (default: clang's own default linker)
|
||||
--cmake PATH / --ninja PATH Build tools (default: on PATH)
|
||||
--dotnet PATH dotnet executable (default: on PATH)
|
||||
--translator-dll PATH Pre-built Translator.Cli.dll (skips building the translator; still needs --dotnet to run it)
|
||||
--translator-bin PATH Self-contained Translator.Cli executable (skips building AND needs no dotnet at all)
|
||||
--native-prebuilt-dir DIR Precompiled aurora/third-party package (see Prepare-NativePrebuilt.sh);
|
||||
skips compiling aurora-main from source entirely
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -97,11 +103,13 @@ while [[ $# -gt 0 ]]; do
|
||||
--parallel) parallel_override=$2; shift 2 ;;
|
||||
--cc) cc_override=$2; shift 2 ;;
|
||||
--cxx) cxx_override=$2; shift 2 ;;
|
||||
--fuse-ld) fuse_ld_override=$2; shift 2 ;;
|
||||
--cmake) cmake_override=$2; shift 2 ;;
|
||||
--ninja) ninja_override=$2; shift 2 ;;
|
||||
--dotnet) dotnet_override=$2; shift 2 ;;
|
||||
--translator-dll) translator_dll_override=$2; shift 2 ;;
|
||||
--translator-bin) translator_bin_override=$2; shift 2 ;;
|
||||
--native-prebuilt-dir) native_prebuilt_dir=$2; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown argument: $1" ;;
|
||||
esac
|
||||
@@ -157,6 +165,10 @@ require_command "$ninja_bin" ninja
|
||||
require_command "$cc_bin" cc
|
||||
require_command "$cxx_bin" cxx
|
||||
|
||||
if [[ -n "$native_prebuilt_dir" ]]; then
|
||||
assert_file "$native_prebuilt_dir/native_prebuilt.cmake" "Native prebuilt package"
|
||||
fi
|
||||
|
||||
project=$workspace/projects/mkwii/recomp.yml
|
||||
assets=$workspace/Assets
|
||||
generated=$workspace/generated
|
||||
@@ -336,26 +348,75 @@ translator "${shard_args[@]}"
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
keep_native_build=0
|
||||
stale_reason=""
|
||||
if [[ -f "$build/CMakeCache.txt" ]]; then
|
||||
expected_home=$workspace/runtime
|
||||
cache_home=$(grep '^CMAKE_HOME_DIRECTORY:INTERNAL=' "$build/CMakeCache.txt" | cut -d= -f2- || true)
|
||||
if [[ -n "$cache_home" && "$(cd "$cache_home" 2>/dev/null && pwd)" == "$expected_home" ]]; then
|
||||
keep_native_build=1
|
||||
# CMake auto-detects and caches auxiliary tool paths (CMAKE_AR/RANLIB/LINKER/ASM_COMPILER
|
||||
# and their per-language *_AR/*_RANLIB variants) only once, the first time a language's
|
||||
# compiler is checked - unlike CMAKE_C_COMPILER/CMAKE_CXX_COMPILER, which get overwritten by
|
||||
# the -D flags below on every configure, these are never refreshed on a plain reconfigure.
|
||||
# An AppImage's own AppRun works around this at the source by keeping --cc/--cxx/--cmake/
|
||||
# --ninja pointed at a stable symlink it re-targets at the current mount every launch
|
||||
# (see build-appimage.sh), so the *path string* CMake caches never actually changes run to
|
||||
# run - but this check stays as a general fallback for any tool path that goes stale some
|
||||
# other way (a moved/removed system toolchain, a relocated portable-tools directory, etc):
|
||||
# any :FILEPATH= cache entry whose recorded path no longer exists means this cache belongs
|
||||
# to a toolchain location that's gone - not just a workspace/path mismatch.
|
||||
while IFS= read -r tool_path; do
|
||||
[[ -n "$tool_path" ]] || continue
|
||||
# CMake's own sentinel for "this optional tool was legitimately never found" (e.g.
|
||||
# clang-scan-deps, not required here) - not a path at all, and not a sign of anything
|
||||
# stale. Without this exclusion, every configure wiped the cache unconditionally.
|
||||
[[ "$tool_path" != *-NOTFOUND ]] || continue
|
||||
if [[ ! -e "$tool_path" ]]; then
|
||||
keep_native_build=0
|
||||
stale_reason=" (cached tool path no longer exists: $tool_path)"
|
||||
break
|
||||
fi
|
||||
done < <(grep -o ':FILEPATH=.*' "$build/CMakeCache.txt" | sed 's/^:FILEPATH=//')
|
||||
fi
|
||||
fi
|
||||
if [[ -d "$build" && "$keep_native_build" -eq 0 ]]; then
|
||||
echo "MKWCBUILD: The native build cache does not belong to this workspace path; rebuilding from scratch"
|
||||
echo "MKWCBUILD: The native build cache does not belong to this workspace path or toolchain; rebuilding from scratch$stale_reason"
|
||||
rm -rf "$build"
|
||||
elif [[ "$keep_native_build" -eq 1 ]]; then
|
||||
echo "MKWCBUILD: Reusing the incremental native build directory"
|
||||
fi
|
||||
|
||||
configure_args=(-S "$workspace/runtime" -B "$build" -G Ninja
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DCMAKE_C_COMPILER="$cc_bin" -DCMAKE_CXX_COMPILER="$cxx_bin"
|
||||
-DCMAKE_MAKE_PROGRAM="$ninja_bin"
|
||||
-DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs")
|
||||
# CMAKE_C_COMPILER/CXX_COMPILER stay stable across AppImage runs on their own (they're exactly
|
||||
# what's passed via -D above, and AppRun points --cc/--cxx at a symlink it re-targets at the
|
||||
# current mount every launch - see build-appimage.sh). CMAKE_AR/RANLIB/LINKER/ASM_COMPILER do NOT
|
||||
# inherit that stability just because $cc_bin does: verified directly that even with a stable
|
||||
# --cc symlink, CMake's own auto-detection of these still resolved to the *real*, ephemeral mount
|
||||
# path underneath (clang's own driver locates its sibling llvm-ar/ld.lld tools by resolving its own
|
||||
# invoked path, symlinks included, rather than trusting the symlink CMake invoked it through) -
|
||||
# each subsequent run's differing command line then made Ninja rebuild every object from scratch
|
||||
# even though nothing had actually changed. Deriving these from $cc_bin (itself already stable)
|
||||
# and re-passing them explicitly every configure keeps them pinned to the same stable value too.
|
||||
if [[ "$cc_bin" == */* ]]; then
|
||||
toolchain_bin=$(dirname "$cc_bin")
|
||||
[[ -x "$toolchain_bin/llvm-ar" ]] && configure_args+=(-DCMAKE_AR="$toolchain_bin/llvm-ar" -DCMAKE_ASM_COMPILER_AR="$toolchain_bin/llvm-ar" -DCMAKE_C_COMPILER_AR="$toolchain_bin/llvm-ar" -DCMAKE_CXX_COMPILER_AR="$toolchain_bin/llvm-ar")
|
||||
[[ -x "$toolchain_bin/llvm-ranlib" ]] && configure_args+=(-DCMAKE_RANLIB="$toolchain_bin/llvm-ranlib" -DCMAKE_ASM_COMPILER_RANLIB="$toolchain_bin/llvm-ranlib" -DCMAKE_C_COMPILER_RANLIB="$toolchain_bin/llvm-ranlib" -DCMAKE_CXX_COMPILER_RANLIB="$toolchain_bin/llvm-ranlib")
|
||||
[[ -x "$toolchain_bin/ld.lld" ]] && configure_args+=(-DCMAKE_LINKER="$toolchain_bin/ld.lld")
|
||||
configure_args+=(-DCMAKE_ASM_COMPILER="$cc_bin")
|
||||
fi
|
||||
if [[ -n "$fuse_ld_override" ]]; then
|
||||
configure_args+=(-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=$fuse_ld_override")
|
||||
fi
|
||||
if [[ -n "$native_prebuilt_dir" ]]; then
|
||||
configure_args+=(-DMKW_NATIVE_PREBUILT_DIR="$native_prebuilt_dir")
|
||||
fi
|
||||
|
||||
log_step configure-native "Configuring the native toolchain"
|
||||
"$cmake_bin" -S "$workspace/runtime" -B "$build" -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_COMPILER="$cc_bin" -DCMAKE_CXX_COMPILER="$cxx_bin" \
|
||||
-DCMAKE_MAKE_PROGRAM="$ninja_bin" \
|
||||
-DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs"
|
||||
"$cmake_bin" "${configure_args[@]}"
|
||||
|
||||
case "$profile" in
|
||||
base) targets=(WiiCompiled) ;;
|
||||
|
||||
Executable
+117
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
# Maintainer release builder. It packages setup/source/tooling only -- never a
|
||||
# translated executable, extracted DATA tree, disc image, or Retro Rewind data.
|
||||
set -euo pipefail
|
||||
|
||||
fail() { printf 'build-setup-pkg.command: error: %s\n' "$*" >&2; exit 1; }
|
||||
# Release payloads must not inherit Finder metadata, resource forks, or a
|
||||
# downloaded-file quarantine bit from a maintainer's working volume.
|
||||
copy_clean() { DITTONORSRC=1 ditto --norsrc --noqtn "$@"; }
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: build-setup-pkg.command --nodtool PATH --translator PATH --cmake-root DIR --ninja PATH --output PKG [options]
|
||||
|
||||
Creates a game-code-free WiiCompiled Setup.pkg. The supplied tools must be
|
||||
maintainer-verified, redistributable macOS arm64 artifacts. The resulting pkg
|
||||
is unsigned unless --installer-identity is supplied; releases should sign and
|
||||
notarize it with a Developer ID Installer certificate.
|
||||
|
||||
--workspace DIR Repository root (default: script's grandparent)
|
||||
--version VERSION Bundle/package version (default: 0.1.0)
|
||||
--installer-identity NAME Developer ID Installer identity for productbuild
|
||||
EOF
|
||||
}
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
workspace=$(cd "$script_dir/../.." && pwd); nodtool=""; translator=""; cmake_root=""; ninja=""; output=""; version=0.1.0; identity=""
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--workspace) workspace=${2:-}; shift 2 ;;
|
||||
--nodtool) nodtool=${2:-}; shift 2 ;;
|
||||
--translator) translator=${2:-}; shift 2 ;;
|
||||
--cmake-root) cmake_root=${2:-}; shift 2 ;;
|
||||
--ninja) ninja=${2:-}; shift 2 ;;
|
||||
--output) output=${2:-}; shift 2 ;;
|
||||
--version) version=${2:-}; shift 2 ;;
|
||||
--installer-identity) identity=${2:-}; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
version=${version#v}
|
||||
[[ "$version" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]] || fail '--version must contain one to three period-separated integers'
|
||||
IFS=. read -r version_major version_minor version_patch <<< "$version"
|
||||
short_version="$version_major.${version_minor:-0}.${version_patch:-0}"
|
||||
for tool in pkgbuild productbuild ditto codesign; do command -v "$tool" >/dev/null || fail "required macOS tool unavailable: $tool"; done
|
||||
[[ -x "$nodtool" ]] || fail '--nodtool must name an executable'
|
||||
[[ -x "$translator" ]] || fail '--translator must name an executable'
|
||||
[[ -x "$cmake_root/bin/cmake" ]] || fail '--cmake-root must contain bin/cmake'
|
||||
[[ -x "$ninja" ]] || fail '--ninja must name an executable'
|
||||
"$nodtool" --version >/dev/null || fail '--nodtool did not run successfully'
|
||||
workspace=$(cd "$workspace" && pwd); output=$(cd "$(dirname "$output")" && pwd)/$(basename "$output")
|
||||
stage=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-pkg.XXXXXX")
|
||||
trap 'rm -rf "$stage"' EXIT
|
||||
app="$stage/root/Applications/WiiCompiled Setup.app"
|
||||
resources="$app/Contents/Resources"
|
||||
mkdir -p "$app/Contents/MacOS" "$resources/tools"
|
||||
cat > "$app/Contents/Info.plist" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict>
|
||||
<key>CFBundleExecutable</key><string>WiiCompiledSetup</string>
|
||||
<key>CFBundleIdentifier</key><string>org.wiicompiled.setup</string>
|
||||
<key>CFBundleName</key><string>WiiCompiled Setup</string>
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key><string>$short_version</string>
|
||||
<key>CFBundleVersion</key><string>$version</string>
|
||||
<key>LSMinimumSystemVersion</key><string>14.0</string>
|
||||
</dict></plist>
|
||||
EOF
|
||||
cat > "$app/Contents/MacOS/WiiCompiledSetup" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
resources="$(cd "$(dirname "$0")/../Resources" && pwd)"
|
||||
|
||||
# Finder launches an app with no terminal attached. The setup work deliberately
|
||||
# writes human-readable build progress to stdout, so run its .command entry
|
||||
# point in Terminal instead of discarding that output behind an inert app icon.
|
||||
exec /usr/bin/osascript - "$resources/setup.command" "$@" <<'APPLESCRIPT'
|
||||
on run argv
|
||||
set commandLine to quoted form of (item 1 of argv)
|
||||
if (count of argv) > 1 then
|
||||
repeat with argumentIndex from 2 to (count of argv)
|
||||
set commandLine to commandLine & " " & quoted form of (item argumentIndex of argv)
|
||||
end repeat
|
||||
end if
|
||||
tell application "Terminal"
|
||||
activate
|
||||
do script commandLine
|
||||
end tell
|
||||
end run
|
||||
APPLESCRIPT
|
||||
EOF
|
||||
chmod +x "$app/Contents/MacOS/WiiCompiledSetup"
|
||||
copy_clean "$script_dir/setup.command" "$resources/setup.command"; chmod +x "$resources/setup.command"
|
||||
# Copy only the build inputs. This deliberately avoids a maintainer's ignored
|
||||
# output directories, local disc extraction, and developer-only packaging.
|
||||
mkdir -p "$resources/workspace"
|
||||
for source in aurora-main projects runtime translator; do
|
||||
[[ -d "$workspace/$source" ]] || fail "required workspace directory is missing: $source"
|
||||
copy_clean "$workspace/$source" "$resources/workspace/$source"
|
||||
done
|
||||
mkdir -p "$resources/workspace/Launcher/macos"
|
||||
copy_clean "$workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/local-build-macos.command"
|
||||
copy_clean "$workspace/Launcher/macos/extract-disc.command" "$resources/workspace/Launcher/macos/extract-disc.command"
|
||||
copy_clean "$workspace/Launcher/macos/publish-app.command" "$resources/workspace/Launcher/macos/publish-app.command"
|
||||
chmod +x "$resources/workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/macos/"*.command
|
||||
mkdir -p "$resources/tools/cmake"
|
||||
copy_clean "$nodtool" "$resources/tools/nodtool"; chmod +x "$resources/tools/nodtool"
|
||||
copy_clean "$translator" "$resources/tools/Translator.Cli"; chmod +x "$resources/tools/Translator.Cli"
|
||||
copy_clean "$cmake_root" "$resources/tools/cmake"
|
||||
copy_clean "$ninja" "$resources/tools/ninja"; chmod +x "$resources/tools/ninja"
|
||||
copy_clean "$workspace/LICENSE" "$resources/LICENSE"
|
||||
copy_clean "$workspace/THIRD-PARTY-NOTICES.md" "$resources/THIRD-PARTY-NOTICES.md"
|
||||
codesign --force --deep --sign - "$app"
|
||||
pkg="$stage/WiiCompiled-Setup-unsigned.pkg"
|
||||
DITTONORSRC=1 COPYFILE_DISABLE=1 pkgbuild --root "$stage/root" --identifier org.wiicompiled.setup --version "$version" --install-location / "$pkg"
|
||||
if [[ -n "$identity" ]]; then productbuild --sign "$identity" --package "$pkg" "$output"; else ditto "$pkg" "$output"; fi
|
||||
printf 'Created game-code-free package: %s\n' "$output"
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# Extract a user-owned Mario Kart Wii PAL (RMCP01) disc image for a local build.
|
||||
# This script deliberately contains no game data and is intended for the macOS
|
||||
# setup application and for maintainers testing that setup path.
|
||||
set -euo pipefail
|
||||
|
||||
readonly EXPECTED_DOL_SHA256=80d18895b39c63bd80f457398bfcbb91b7d16ac116a41a88967e954080155b05
|
||||
readonly EXPECTED_REL_SHA256=16d9d146112541fefea701ecb5bc1a496f9d50e4a752fbb5b6778e7c6399f67d
|
||||
|
||||
fail() { printf 'extract-disc.command: error: %s\n' "$*" >&2; exit 1; }
|
||||
sha256() { shasum -a 256 "$1" | awk '{ print $1 }'; }
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: extract-disc.command --game IMAGE --assets-dir DIR --nodtool PATH
|
||||
|
||||
Extracts a user-owned Mario Kart Wii PAL RMCP01 image into DIR. The final
|
||||
layout is DIR/main.dol, DIR/StaticR.rel, and DIR/DATA. Existing data is left
|
||||
untouched unless the complete new extraction passes both content hash checks.
|
||||
EOF
|
||||
}
|
||||
|
||||
game=""
|
||||
assets_dir=""
|
||||
nodtool=""
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--game) game=${2:-}; shift 2 ;;
|
||||
--assets-dir) assets_dir=${2:-}; shift 2 ;;
|
||||
--nodtool) nodtool=${2:-}; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -f "$game" ]] || fail "disc image does not exist: $game"
|
||||
[[ -n "$assets_dir" ]] || fail "--assets-dir is required"
|
||||
[[ -x "$nodtool" ]] || fail "nodtool is not executable: $nodtool"
|
||||
|
||||
assets_dir=$(mkdir -p "$assets_dir" && cd "$assets_dir" && pwd)
|
||||
scratch=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-disc.XXXXXX")
|
||||
cleanup() { rm -rf "$scratch"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
printf 'MKWCBUILD:STEP:validate-disc Checking the selected disc image\n'
|
||||
"$nodtool" info "$game" >/dev/null
|
||||
printf 'MKWCBUILD:STEP:extract-disc Extracting the user-owned disc image\n'
|
||||
"$nodtool" extract "$game" "$scratch/extracted"
|
||||
|
||||
dol=$(find "$scratch/extracted" -type f -path '*/sys/main.dol' -print -quit)
|
||||
rel=$(find "$scratch/extracted" -type f -path '*/files/rel/StaticR.rel' -print -quit)
|
||||
[[ -n "$dol" ]] || fail 'nodtool extraction did not contain sys/main.dol'
|
||||
[[ -n "$rel" ]] || fail 'nodtool extraction did not contain files/rel/StaticR.rel'
|
||||
[[ $(sha256 "$dol") == "$EXPECTED_DOL_SHA256" ]] || fail 'disc is not the supported clean PAL RMCP01 Mario Kart Wii image'
|
||||
[[ $(sha256 "$rel") == "$EXPECTED_REL_SHA256" ]] || fail 'disc has an unexpected StaticR.rel; use a clean PAL RMCP01 image'
|
||||
|
||||
data_root=$(dirname "$(dirname "$dol")")
|
||||
[[ -d "$data_root/files" ]] || fail 'nodtool extraction did not contain the Wii files directory'
|
||||
|
||||
# Stage beside the destination so the final replacement stays on one volume.
|
||||
stage="$assets_dir/.extract-stage-$$"
|
||||
rm -rf "$stage"
|
||||
mkdir -p "$stage"
|
||||
ditto "$data_root" "$stage/DATA"
|
||||
ditto "$dol" "$stage/main.dol"
|
||||
ditto "$rel" "$stage/StaticR.rel"
|
||||
|
||||
backup="$assets_dir/.previous-extraction-$(date +%Y%m%d-%H%M%S)"
|
||||
if [[ -e "$assets_dir/DATA" || -e "$assets_dir/main.dol" || -e "$assets_dir/StaticR.rel" ]]; then
|
||||
mkdir -p "$backup"
|
||||
for item in DATA main.dol StaticR.rel; do
|
||||
[[ -e "$assets_dir/$item" ]] && mv "$assets_dir/$item" "$backup/$item"
|
||||
done
|
||||
fi
|
||||
mv "$stage/DATA" "$assets_dir/DATA"
|
||||
mv "$stage/main.dol" "$assets_dir/main.dol"
|
||||
mv "$stage/StaticR.rel" "$assets_dir/StaticR.rel"
|
||||
rmdir "$stage"
|
||||
rm -rf "$backup"
|
||||
|
||||
printf 'MKWCBUILD:STEP:disc-ready Verified and extracted clean PAL RMCP01 game assets\n'
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
# Turn one locally compiled macOS product into a self-contained .app bundle.
|
||||
set -euo pipefail
|
||||
|
||||
fail() { printf 'publish-app.command: error: %s\n' "$*" >&2; exit 1; }
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: publish-app.command --build-dir DIR --product {WiiCompiled|RetroRewind} --output-dir DIR
|
||||
|
||||
Copies a locally built product and its runtime assets into OUTPUT-DIR/<product>.app.
|
||||
It bundles non-system dylibs, rewrites their install names, and ad-hoc signs the
|
||||
result. This is suitable for local use; a release must replace ad-hoc signing
|
||||
with the project's Developer ID signing and notarization process.
|
||||
EOF
|
||||
}
|
||||
|
||||
build_dir=""; product=""; output_dir=""
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--build-dir) build_dir=${2:-}; shift 2 ;;
|
||||
--product) product=${2:-}; shift 2 ;;
|
||||
--output-dir) output_dir=${2:-}; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
[[ "$product" == WiiCompiled || "$product" == RetroRewind ]] || fail '--product must be WiiCompiled or RetroRewind'
|
||||
for tool in codesign ditto install_name_tool otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
|
||||
[[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product"
|
||||
for asset in dsp_coef.bin initial_pipeline_cache.db 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"
|
||||
Executable
+140
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env bash
|
||||
# Entry point bundled in WiiCompiled Setup.app. The package contains source and
|
||||
# tools only; a user's own verified disc is extracted into Application Support.
|
||||
set -euo pipefail
|
||||
|
||||
resources=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
workspace_source="$resources/workspace"
|
||||
nodtool="$resources/tools/nodtool"
|
||||
translator="$resources/tools/Translator.Cli"
|
||||
cmake_bin="$resources/tools/cmake/bin/cmake"
|
||||
ninja_bin="$resources/tools/ninja"
|
||||
support_root="$HOME/Library/Application Support/WiiCompiled"
|
||||
workspace="$support_root/BuildWorkspace"
|
||||
products="$support_root/Products"
|
||||
|
||||
fail() { printf 'WiiCompiled Setup: %s\n' "$*" >&2; exit 1; }
|
||||
notice() { /usr/bin/osascript -e "display dialog \"${1//\"/\\\"}\" buttons {\"OK\"} default button \"OK\" with icon caution" >/dev/null; }
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: setup.command --game IMAGE [--retro-dir DIR] [--install-location {user|applications}]
|
||||
|
||||
Without arguments this script opens file pickers. It is normally launched by
|
||||
WiiCompiled Setup.app, not run directly.
|
||||
EOF
|
||||
}
|
||||
|
||||
game=""; retro_dir=""; install_location=applications
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--game) game=${2:-}; shift 2 ;;
|
||||
--retro-dir) retro_dir=${2:-}; shift 2 ;;
|
||||
--install-location) install_location=${2:-}; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
[[ "$install_location" == user || "$install_location" == applications ]] || fail '--install-location must be user or applications'
|
||||
|
||||
if [[ -z "$game" ]]; then
|
||||
game=$(/usr/bin/osascript <<'APPLESCRIPT'
|
||||
set selectedFile to choose file with prompt "Choose your clean Mario Kart Wii PAL (RMCP01) disc image"
|
||||
POSIX path of selectedFile
|
||||
APPLESCRIPT
|
||||
) || exit 0
|
||||
choice=$(/usr/bin/osascript -e 'button returned of (display dialog "Would you like to build Retro Rewind too?" buttons {"Base game only", "Choose Retro Rewind folder"} default button "Base game only")')
|
||||
if [[ "$choice" == 'Choose Retro Rewind folder' ]]; then
|
||||
retro_dir=$(/usr/bin/osascript <<'APPLESCRIPT'
|
||||
set selectedFolder to choose folder with prompt "Choose the RetroRewind6 folder (or its parent folder)"
|
||||
POSIX path of selectedFolder
|
||||
APPLESCRIPT
|
||||
) || exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ -x "$nodtool" ]] || fail 'the packaged nodtool is missing or not executable'
|
||||
[[ -x "$translator" ]] || fail 'the packaged Translator.Cli is missing or not executable'
|
||||
[[ -x "$cmake_bin" && -x "$ninja_bin" ]] || fail 'the packaged CMake or Ninja tool is missing'
|
||||
if ! /usr/bin/xcode-select -p >/dev/null 2>&1; then
|
||||
notice 'Xcode Command Line Tools are required once to compile WiiCompiled. Click OK, complete the Apple installer, then run WiiCompiled Setup again.'
|
||||
/usr/bin/xcode-select --install || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$support_root" "$products"
|
||||
if [[ ! -d "$workspace/.git" && ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then
|
||||
printf 'Preparing the local build workspace...\n'
|
||||
rm -rf "$workspace"
|
||||
/usr/bin/ditto "$workspace_source" "$workspace"
|
||||
fi
|
||||
|
||||
profile=base
|
||||
build_args=(--workspace "$workspace" --game "$game" --nodtool "$nodtool" --output-dir "$products")
|
||||
if [[ -n "$retro_dir" ]]; then
|
||||
profile=both
|
||||
# Online play needs the shared Retro-WFC payload. Keep it in the per-user
|
||||
# support directory rather than the packaged app or build workspace, then
|
||||
# verify its pinned signature before publishing it into the local cache.
|
||||
retro_wfc_dir="$support_root/RetroWfcPayload"
|
||||
retro_wfc_payload="$retro_wfc_dir/binary/payload.RMCPD00.bin"
|
||||
if [[ -f "$retro_wfc_payload" ]] && ! "$translator" validate-retro-wfc-payload --directory "$retro_wfc_dir"; then
|
||||
printf 'Discarding an invalid cached Retro-WFC payload...\n' >&2
|
||||
rm -f "$retro_wfc_payload"
|
||||
fi
|
||||
if [[ ! -f "$retro_wfc_payload" ]]; then
|
||||
printf 'Downloading the Retro-WFC payload needed for online play...\n'
|
||||
mkdir -p "$retro_wfc_dir"
|
||||
payload_stage=$(mktemp -d "$retro_wfc_dir/.payload-download.XXXXXX")
|
||||
temporary_payload="$payload_stage/binary/payload.RMCPD00.bin"
|
||||
mkdir -p "$(dirname "$temporary_payload")"
|
||||
trap 'rm -rf "$payload_stage"' EXIT
|
||||
/usr/bin/curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \
|
||||
--retry 1 --output "$temporary_payload" \
|
||||
'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."
|
||||
Executable
+237
@@ -0,0 +1,237 @@
|
||||
#!/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"
|
||||
}
|
||||
|
||||
if [[ -x "$toolchain_dir/bin/clang" && -x "$toolchain_dir/bin/ninja" && -x "$toolchain_dir/bin/cmake" ]]; then
|
||||
echo "prepare-portable-tools.sh: reusing existing toolchain at $toolchain_dir"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
work="$destination/.building-toolchain-$arch"
|
||||
rm -rf "$work"
|
||||
mkdir -p "$work/bin" "$work/lib/$target_triple" "$work/include/$target_triple/c++/v1"
|
||||
|
||||
# --- clang/lld/llvm-ar, pruned from the official LLVM release ---
|
||||
|
||||
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"
|
||||
|
||||
cat > "$work/LICENSE.txt" <<EOF
|
||||
Portable build tools bundled by WiiCompiled
|
||||
|
||||
clang/lld/llvm-ar $llvm_version (pruned from the official LLVM release for Linux/$llvm_release_arch)
|
||||
https://github.com/llvm/llvm-project/releases/tag/llvmorg-$llvm_version
|
||||
Apache License v2.0 with LLVM Exceptions:
|
||||
https://github.com/llvm/llvm-project/blob/llvmorg-$llvm_version/LICENSE.TXT
|
||||
|
||||
CMake $cmake_version
|
||||
https://github.com/Kitware/CMake
|
||||
BSD 3-Clause License
|
||||
|
||||
Ninja $ninja_version
|
||||
https://github.com/ninja-build/ninja
|
||||
Apache License 2.0
|
||||
EOF
|
||||
|
||||
echo "prepare-portable-tools.sh: testing the toolchain..."
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
cat > "$test_dir/t.cpp" <<'EOF'
|
||||
#include <vector>
|
||||
#include <cstdio>
|
||||
int main() {
|
||||
std::vector<int> v{1, 2, 3};
|
||||
int sum = 0;
|
||||
for (int x : v) sum += x;
|
||||
return sum == 6 ? 0 : 1;
|
||||
}
|
||||
EOF
|
||||
"$work/bin/clang++" -std=c++20 -fuse-ld=lld "$test_dir/t.cpp" -o "$test_dir/t"
|
||||
"$test_dir/t"
|
||||
|
||||
# Also exercised together through CMake+Ninja, exactly how local-build.sh drives them - a plain
|
||||
# clang++ invocation above would not catch a broken CMAKE_ROOT (Modules/Templates) or a Ninja that
|
||||
# can't find the compiler.
|
||||
cat > "$test_dir/CMakeLists.txt" <<'EOF'
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(test CXX)
|
||||
add_executable(test t.cpp)
|
||||
EOF
|
||||
"$work/bin/cmake" -S "$test_dir" -B "$test_dir/build" -G Ninja \
|
||||
-DCMAKE_MAKE_PROGRAM="$work/bin/ninja" -DCMAKE_CXX_COMPILER="$work/bin/clang++" >/dev/null
|
||||
"$work/bin/cmake" --build "$test_dir/build" >/dev/null
|
||||
"$test_dir/build/test"
|
||||
|
||||
rm -rf "$test_dir"
|
||||
trap - EXIT
|
||||
|
||||
mv "$work" "$toolchain_dir"
|
||||
echo "prepare-portable-tools.sh: toolchain ready at $toolchain_dir ($(du -sh "$toolchain_dir" | cut -f1))"
|
||||
@@ -46,26 +46,51 @@ Press **F10** while the game window has focus:
|
||||
- Internal resolution
|
||||
- FPS counter
|
||||
- Controller assignment for all four ports
|
||||
- Full per-controller button mapping
|
||||
- Full per-controller button mapping, including the bumpers
|
||||
- Dolphin-syntax input expressions and GCPadNew.ini import
|
||||
- Controller vibration on/off
|
||||
- Volume, instant mute, and the music ducking toggle
|
||||
|
||||
Everything you change is saved to `Config.toml` on the spot and restored next launch.
|
||||
|
||||
**Real controller support.**
|
||||
Controllers are fed to the game as a GameCube controller.
|
||||
The port does NOT pretend to be a Wii Remote or Classic Controller.
|
||||
Mappings are positional (`south`, `east`, `west`, `north`) rather than Xbox-labelled, so the
|
||||
same config makes sense on Xbox, PlayStation, Nintendo and generic SDL pads alike, and extra
|
||||
inputs like paddles, touchpads and share buttons show up when the hardware reports them.
|
||||
|
||||
**Dolphin-compatible input expressions.**
|
||||
Each GameCube control can carry an expression in Dolphin's input syntax, with the same operators
|
||||
and the same functions.
|
||||
A Dolphin `GCPadNew.ini` can be imported directly from the F10 bar.
|
||||
|
||||
**Vibration toggle.**
|
||||
Force feedback can be turned off for every port at once.
|
||||
The official Wii U / Switch GameCube adapter (WUP-028) works too; as with Dolphin, on Windows the
|
||||
adapter must be switched to the WinUSB driver once (Zadig).
|
||||
|
||||
**Real Wii Remotes over Bluetooth.**
|
||||
Pair a Wii Remote with Windows (Settings > Bluetooth > Add device, press 1+2 or SYNC, leave the
|
||||
PIN empty)
|
||||
|
||||
Known limitations of the Wii Remote path:
|
||||
- No IR pointer yet: menus are navigated with the D-pad and A (the game treats the remote as
|
||||
pointing away from the screen).
|
||||
- Battery level is not reported to the game and the remote's speaker is not implemented.
|
||||
- Only the Wii Remote's own accelerometer is calibrated; the Nunchuk's uses SDL's fixed zero point.
|
||||
- The Classic Controller's L/R triggers reach the game as digital (full pull on click): SDL does not
|
||||
expose their analog travel.
|
||||
- Turn the Wii Remote support off in that menu if you use a Mayflash DolphinBar, which already
|
||||
presents the remote as a regular gamepad.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Windows 10 or 11, 64-bit
|
||||
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher
|
||||
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
|
||||
- About 20 GB of free disk space during installation (Final game size ~5 GB)
|
||||
- macOS 14 (Sonoma) or later on Apple Silicon
|
||||
- On macOS, Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
|
||||
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
|
||||
GCZ, CISO, WBFS, WIA and RVZ are accepted.
|
||||
|
||||
@@ -86,6 +111,7 @@ image under Settings, turn on **WiiCompiled (beta)**, and hit install from the H
|
||||
Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and
|
||||
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
|
||||
|
||||
|
||||
> [!CAUTION]
|
||||
> Only take builds from this repository's
|
||||
> [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an
|
||||
|
||||
@@ -26,10 +26,6 @@ Aurora itself vendors:
|
||||
(shagkur) and Dave Murphy (WinterMute). `aurora-main/lib/card/SRAM.hpp`.
|
||||
Source: <https://github.com/devkitPro/libogc>
|
||||
|
||||
> [!NOTE]
|
||||
> Upstream aurora ships `assets/screenshot.png`, a rendered frame from a different Nintendo
|
||||
> title. It is intentionally omitted from this repository.
|
||||
|
||||
### Dolphin Emulator data files - GPL-2.0-or-later
|
||||
|
||||
Copyright (c) 2003+ Dolphin Emulator Project.
|
||||
@@ -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
|
||||
`aurora-main/cmake/AuroraDawnProvider.cmake`. They are not stored in this repository; the build
|
||||
downloads them, and release installers carry the resulting binaries. Their license texts are
|
||||
included in the installer's `licenses/` folder.
|
||||
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 |
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# Source-level fixes applied to the vendored SDL3 tree before it is built.
|
||||
#
|
||||
# Each patch is an exact string replacement, checked and idempotent: a tree that
|
||||
# already carries the fix is left alone, a tree where the anchor text is missing
|
||||
# (a different SDL version) stops the configure with a clear message instead of
|
||||
# silently building without the fix.
|
||||
#
|
||||
# Used two ways:
|
||||
# - included by AuroraSDL3Provider.cmake, which calls aurora_sdl3_apply_patches()
|
||||
# on a pre-provided source tree (FETCHCONTENT_SOURCE_DIR_SDL);
|
||||
# - run as `cmake -DSDL_SOURCE_DIR=<dir> -P AuroraSDL3Patches.cmake` from the
|
||||
# FetchContent PATCH_COMMAND for a freshly extracted tarball.
|
||||
|
||||
function(_aurora_sdl3_replace file description old new)
|
||||
file(READ "${file}" _content)
|
||||
string(FIND "${_content}" "${new}" _already)
|
||||
if (NOT _already EQUAL -1)
|
||||
return()
|
||||
endif ()
|
||||
string(FIND "${_content}" "${old}" _anchor)
|
||||
if (_anchor EQUAL -1)
|
||||
message(FATAL_ERROR "aurora: SDL3 patch '${description}' does not apply to ${file}; "
|
||||
"the vendored SDL version changed, review AuroraSDL3Patches.cmake")
|
||||
endif ()
|
||||
string(REPLACE "${old}" "${new}" _content "${_content}")
|
||||
file(WRITE "${file}" "${_content}")
|
||||
message(STATUS "aurora: SDL3 patch applied: ${description}")
|
||||
endfunction()
|
||||
|
||||
function(aurora_sdl3_apply_patches sdl_source_dir)
|
||||
set(_wii "${sdl_source_dir}/src/joystick/hidapi/SDL_hidapi_wii.c")
|
||||
if (NOT EXISTS "${_wii}")
|
||||
message(FATAL_ERROR "aurora: SDL3 source tree at ${sdl_source_dir} has no SDL_hidapi_wii.c")
|
||||
endif ()
|
||||
|
||||
# Wii Remote: rebuild the joystick in place after an extension change.
|
||||
#
|
||||
# When a Nunchuk or Classic Controller is plugged in or pulled out, the driver
|
||||
# flags the joystick as disconnected so it can come back with the new name and
|
||||
# capabilities. But the HID device stays open, and HIDAPI only removes a
|
||||
# joystick-less device once its handle is closed, so nothing ever re-creates
|
||||
# the joystick: the remote is gone for good until the application toggles the
|
||||
# driver hint (which closes and re-opens the Bluetooth HID handle, something
|
||||
# some Windows Bluetooth stacks answer by dropping the link). Instead, when the
|
||||
# device has no joystick, probe the extension again (two bounded attempts, at
|
||||
# most once a second) and connect a new joystick on the still-open handle.
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote in-place reconnect (context field)"
|
||||
[==[ Uint64 m_ulNextMotionPlusCheck;
|
||||
bool m_bDisconnected;
|
||||
]==]
|
||||
[==[ Uint64 m_ulNextMotionPlusCheck;
|
||||
bool m_bDisconnected;
|
||||
Uint64 m_ulNextReconnect; /* WiiCompiled: see HIDAPI_DriverWii_UpdateDevice */
|
||||
]==])
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote in-place reconnect (bounded extension probe)"
|
||||
[==[static EWiiExtensionControllerType ReadExtensionControllerType(SDL_HIDAPI_Device *device)
|
||||
{
|
||||
SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)device->context;
|
||||
EWiiExtensionControllerType eExtensionControllerType = k_eWiiExtensionControllerType_Unknown;
|
||||
const int MAX_ATTEMPTS = 20;
|
||||
int attempts = 0;
|
||||
]==]
|
||||
[==[static EWiiExtensionControllerType ReadExtensionControllerTypeAttempts(SDL_HIDAPI_Device *device, int MAX_ATTEMPTS)
|
||||
{
|
||||
SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)device->context;
|
||||
EWiiExtensionControllerType eExtensionControllerType = k_eWiiExtensionControllerType_Unknown;
|
||||
int attempts = 0;
|
||||
]==])
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote in-place reconnect (probe wrapper)"
|
||||
[==[static void UpdateDeviceIdentity(SDL_HIDAPI_Device *device)
|
||||
{
|
||||
]==]
|
||||
[==[static EWiiExtensionControllerType ReadExtensionControllerType(SDL_HIDAPI_Device *device)
|
||||
{
|
||||
return ReadExtensionControllerTypeAttempts(device, 20);
|
||||
}
|
||||
|
||||
static void UpdateDeviceIdentity(SDL_HIDAPI_Device *device)
|
||||
{
|
||||
]==])
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote in-place reconnect (UpdateDevice)"
|
||||
[==[ if (device->num_joysticks > 0) {
|
||||
joystick = SDL_GetJoystickFromID(device->joysticks[0]);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
now = SDL_GetTicks();
|
||||
]==]
|
||||
[==[ if (device->num_joysticks > 0) {
|
||||
joystick = SDL_GetJoystickFromID(device->joysticks[0]);
|
||||
} else {
|
||||
/* WiiCompiled: the joystick was dropped (extension plugged or unplugged,
|
||||
* or a failed read) but the HID device is still open, and the device
|
||||
* list only removes a joystick-less device once its handle is closed.
|
||||
* Rebuild the joystick in place, so an extension change behaves like it
|
||||
* does on the console: the remote never goes away, it just changes
|
||||
* type. If the remote does not answer, keep the device and retry. */
|
||||
now = SDL_GetTicks();
|
||||
if (ctx->m_ulNextReconnect != 0 && now < ctx->m_ulNextReconnect) {
|
||||
return false;
|
||||
}
|
||||
ctx->m_ulNextReconnect = now + 1000;
|
||||
{
|
||||
EWiiExtensionControllerType type = ReadExtensionControllerTypeAttempts(device, 2);
|
||||
if (type == k_eWiiExtensionControllerType_Unknown) {
|
||||
return false;
|
||||
}
|
||||
ctx->m_bDisconnected = false;
|
||||
ctx->m_ulLastInput = now;
|
||||
ctx->m_ulNextReconnect = 0;
|
||||
ctx->m_eExtensionControllerType = type;
|
||||
UpdateDeviceIdentity(device);
|
||||
SDL_LogDebug(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Reconnected joystick as %s", device->name);
|
||||
return HIDAPI_JoystickConnected(device, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
now = SDL_GetTicks();
|
||||
]==])
|
||||
endfunction()
|
||||
|
||||
# Script mode (FetchContent PATCH_COMMAND).
|
||||
if (CMAKE_SCRIPT_MODE_FILE AND DEFINED SDL_SOURCE_DIR)
|
||||
aurora_sdl3_apply_patches("${SDL_SOURCE_DIR}")
|
||||
endif ()
|
||||
@@ -135,9 +135,18 @@ elseif (_aurora_sdl3_provider STREQUAL "vendor")
|
||||
endif ()
|
||||
|
||||
include(FetchContent)
|
||||
# Source fixes for the vendored SDL (see AuroraSDL3Patches.cmake). A freshly
|
||||
# downloaded tarball is patched by the PATCH_COMMAND; a tree handed in through
|
||||
# FETCHCONTENT_SOURCE_DIR_SDL skips the download steps, so patch it here.
|
||||
set(_aurora_sdl3_patches "${CMAKE_CURRENT_LIST_DIR}/AuroraSDL3Patches.cmake")
|
||||
include("${_aurora_sdl3_patches}")
|
||||
if (DEFINED FETCHCONTENT_SOURCE_DIR_SDL AND EXISTS "${FETCHCONTENT_SOURCE_DIR_SDL}")
|
||||
aurora_sdl3_apply_patches("${FETCHCONTENT_SOURCE_DIR_SDL}")
|
||||
endif ()
|
||||
FetchContent_Declare(SDL
|
||||
URL "https://github.com/libsdl-org/SDL/releases/download/release-${AURORA_SDL3_VERSION}/SDL3-${AURORA_SDL3_VERSION}.tar.gz"
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
PATCH_COMMAND "${CMAKE_COMMAND}" -DSDL_SOURCE_DIR=<SOURCE_DIR> -P "${_aurora_sdl3_patches}"
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(SDL)
|
||||
|
||||
@@ -255,6 +255,14 @@ FrameWorkerState g_frameWorker;
|
||||
bool frame_worker_requested() noexcept {
|
||||
#ifdef AURORA_ENABLE_GX
|
||||
static const bool enabled = [] {
|
||||
#if defined(__APPLE__)
|
||||
// ImGui's SDL backend may raise an SDL window from ImGui::NewFrame(). On
|
||||
// macOS that reaches AppKit, whose window operations are main-thread-only;
|
||||
// doing it on the frame worker terminates the process with EXC_BREAKPOINT.
|
||||
// Keep all SDL/ImGui work on the calling thread until the worker no longer
|
||||
// owns frame preparation on Apple platforms.
|
||||
return false;
|
||||
#endif
|
||||
#if defined(_WIN32)
|
||||
// RenderDoc's D3D12 layer is injected before Aurora starts and needs device and command
|
||||
// ownership on one thread, so keep frame submission synchronous there.
|
||||
@@ -1516,6 +1524,15 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
|
||||
|
||||
// Phase 3: hand the encoded group to whoever owns presentation.
|
||||
void publish_presentations(std::vector<PresentationJob>&& presentationJobs, bool interpolationActive) {
|
||||
#if defined(__APPLE__)
|
||||
(void)interpolationActive;
|
||||
// Presenting reaches SDL/AppKit, whose window operations must stay on the
|
||||
// main thread. Interpolation normally starts the presenter worker, so keep
|
||||
// its jobs synchronous on Apple platforms.
|
||||
for (const auto& job : presentationJobs) {
|
||||
present_presentation_job(job);
|
||||
}
|
||||
#else
|
||||
// Keep presentation on the presenter whenever the async frame worker runs, even with
|
||||
// interpolation off, so every mode shares one surface/resize path. RenderDoc keeps the sync path.
|
||||
if (frame_worker_requested() || interpolationActive ||
|
||||
@@ -1526,6 +1543,7 @@ void publish_presentations(std::vector<PresentationJob>&& presentationJobs, bool
|
||||
present_presentation_job(job);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void record_frame_telemetry() {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <dolphin/pad.h>
|
||||
#include <dolphin/si.h>
|
||||
#include <SDL3/SDL_mouse.h>
|
||||
#include <SDL3/SDL_joystick.h>
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
@@ -193,6 +194,32 @@ std::array<PADButtonMapping, PAD_BUTTON_COUNT> g_defaultButtonsJoyPair{{
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT},
|
||||
}};
|
||||
|
||||
// Wii U Pro Controllers through SDL's HIDAPI Wii driver. No SDL_GamepadType
|
||||
// singles them out, so they are picked by the name the driver gives them (see
|
||||
// __PADSetDefaultMapping). Wii Remotes, with or without a Nunchuk or Classic
|
||||
// Controller, are read by the game through KPAD instead and never get a
|
||||
// GameCube mapping (the runtime hides those ports from PADRead).
|
||||
|
||||
// Nintendo's labelled layout: A on the right accelerates, B at the bottom
|
||||
// brakes. SDL's Wii driver reports ZL/ZR as the LEFT_TRIGGER/RIGHT_TRIGGER
|
||||
// axes, never as shoulder buttons, so they are left unbound here and picked up
|
||||
// by aurora's default axis mapping (g_defaultAxes) the same way every
|
||||
// analog-trigger pad's L/R is.
|
||||
std::array<PADButtonMapping, PAD_BUTTON_COUNT> g_defaultButtonsWiiUPro{{
|
||||
{SDL_GAMEPAD_BUTTON_EAST, PAD_BUTTON_A},
|
||||
{SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_B},
|
||||
{SDL_GAMEPAD_BUTTON_NORTH, PAD_BUTTON_X},
|
||||
{SDL_GAMEPAD_BUTTON_WEST, PAD_BUTTON_Y},
|
||||
{SDL_GAMEPAD_BUTTON_START, PAD_BUTTON_START},
|
||||
{SDL_GAMEPAD_BUTTON_BACK, PAD_TRIGGER_Z},
|
||||
{PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_L},
|
||||
{PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_R},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_UP, PAD_BUTTON_UP},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_DOWN, PAD_BUTTON_DOWN},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_LEFT, PAD_BUTTON_LEFT},
|
||||
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT},
|
||||
}};
|
||||
|
||||
std::array<PADKeyButtonBinding, PAD_BUTTON_COUNT> g_defaultKeys{{
|
||||
{PAD_KEY_INVALID, PAD_BUTTON_A},
|
||||
{PAD_KEY_INVALID, PAD_BUTTON_B},
|
||||
@@ -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) */
|
||||
{
|
||||
if (wii_default_mapping(controller, controller->m_buttonMapping)) {
|
||||
reset_alt_button_mapping(controller);
|
||||
return;
|
||||
}
|
||||
switch (SDL_GetGamepadType(controller->m_controller)) {
|
||||
case SDL_GAMEPAD_TYPE_XBOX360:
|
||||
controller->m_buttonMapping = g_defaultButtonsXBox360;
|
||||
@@ -745,6 +790,47 @@ u32 PADRead(PADStatus* status) {
|
||||
|
||||
if (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 rightTriggerSet = false;
|
||||
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)?
|
||||
static constexpr std::array<std::pair<SDL_GamepadButton, PADExtButton>, PAD_EXT_BUTTON_COUNT> kExtButtonMappings{{
|
||||
{SDL_GAMEPAD_BUTTON_BACK, PAD_BUTTON_BACK},
|
||||
|
||||
@@ -401,6 +401,8 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
|
||||
return -1;
|
||||
}
|
||||
controller.m_isGameCube = controller.m_vid == 0x057E && controller.m_pid == 0x0337;
|
||||
const char* serial = SDL_GetGamepadSerial(ctrl);
|
||||
controller.m_gameCubeUseOrdinaryStop = controller.m_isGameCube && serial && "GCP+"sv == serial;
|
||||
if (controller.m_isGameCube ||
|
||||
(SDL_GetGamepadType(ctrl) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO && controller.m_pid == 0x2073)) {
|
||||
controller.m_deadZones.emulateTriggers = false;
|
||||
@@ -481,6 +483,13 @@ bool controller_has_rumble(Uint32 instance) noexcept {
|
||||
void controller_rumble(uint32_t instance, uint16_t low_freq_intensity, uint16_t high_freq_intensity,
|
||||
uint16_t duration_ms) noexcept {
|
||||
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
|
||||
// GC Pocket+ has been observed continuing to vibrate after a hard stop;
|
||||
// an ordinary stop cleared it. With GAMECUBE_RUMBLE_BRAKE enabled, SDL
|
||||
// encodes (0, 1) as adapter command 0, whereas (0, 0) sends command 2.
|
||||
// Apply the workaround here so shutdown uses the same stop as PAD calls.
|
||||
if (it->second.m_gameCubeUseOrdinaryStop && low_freq_intensity == 0 && high_freq_intensity == 0) {
|
||||
high_freq_intensity = 1;
|
||||
}
|
||||
SDL_RumbleGamepad(it->second.m_controller, low_freq_intensity, high_freq_intensity, duration_ms);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ extern Module Log;
|
||||
struct GameController {
|
||||
SDL_Gamepad* m_controller = nullptr;
|
||||
bool m_isGameCube = false;
|
||||
bool m_gameCubeUseOrdinaryStop = false;
|
||||
Sint32 m_index = -1;
|
||||
Sint32 m_playerIndex = -1;
|
||||
bool m_hasRumble = false;
|
||||
|
||||
+179
-45
@@ -1,18 +1,28 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(mkw_recompiled)
|
||||
|
||||
if((NOT (WIN32 AND MINGW)) AND (NOT CMAKE_SYSTEM_NAME STREQUAL "Linux"))
|
||||
message(FATAL_ERROR "WiiCompiled requires Windows (LLVM-MinGW) or native Linux")
|
||||
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
message(FATAL_ERROR "WiiCompiled requires a 64-bit Clang toolchain")
|
||||
endif()
|
||||
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR
|
||||
NOT CMAKE_SIZEOF_VOID_P EQUAL 8 OR
|
||||
NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
message(FATAL_ERROR "WiiCompiled requires 64-bit Clang targeting x86_64")
|
||||
|
||||
if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
set(MKW_PLATFORM_WINDOWS TRUE)
|
||||
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64)$")
|
||||
# The first native macOS target is Apple Silicon. Intel and universal
|
||||
# binaries remain future compatibility work; do not silently claim them.
|
||||
set(MKW_PLATFORM_MACOS TRUE)
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$")
|
||||
set(MKW_PLATFORM_LINUX TRUE)
|
||||
else()
|
||||
message(FATAL_ERROR
|
||||
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS arm64")
|
||||
endif()
|
||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
message(FATAL_ERROR "WiiCompiled only supports Release builds")
|
||||
endif()
|
||||
|
||||
option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON)
|
||||
|
||||
# Preprocessor definitions that belong to this project's own code (the runtime,
|
||||
# the translated shards and the product glue) and to nothing else. They are
|
||||
# applied directory-scoped, immediately after the aurora add_subdirectory() call,
|
||||
@@ -49,17 +59,14 @@ target_include_directories(mkw_pugixml PUBLIC third_party/pugixml)
|
||||
target_compile_features(mkw_pugixml PUBLIC cxx_std_17)
|
||||
set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF)
|
||||
|
||||
# Non-Windows guest-fiber scheduling (runtime/src/fiber_manager.cpp) needs a symmetric
|
||||
# Linux guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric
|
||||
# stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
|
||||
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
|
||||
# asymmetric resume/yield coroutine libraries, which would need every call site restructured).
|
||||
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
|
||||
# separately BSD-style licensed, see third_party/libco/LICENSE) - all of libco's non-Windows
|
||||
# CPU-architecture backends are kept, even though libco.c's own preprocessor dispatch
|
||||
# (__amd64__/__i386__/__arm__/__aarch64__/etc.) only ever selects amd64.c for this project's
|
||||
# x86_64-only target (see the platform/arch check above). Windows keeps using native Fibers
|
||||
# untouched, so this target is never built there.
|
||||
if(NOT WIN32)
|
||||
# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers
|
||||
# and macOS uses the project's x18-safe AArch64 assembly backend, so this target is Linux-only.
|
||||
if(MKW_PLATFORM_LINUX)
|
||||
add_library(mkw_libco STATIC third_party/libco/libco.c)
|
||||
add_library(mkw::libco ALIAS mkw_libco)
|
||||
target_include_directories(mkw_libco PUBLIC third_party/libco)
|
||||
@@ -130,31 +137,35 @@ else()
|
||||
message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing")
|
||||
endif()
|
||||
set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE)
|
||||
if(WIN32)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE)
|
||||
set(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_USE_WINDOWS_UI OFF CACHE BOOL "" FORCE)
|
||||
else()
|
||||
# Non-Windows (Linux): mirrors aurora-main's own
|
||||
# _aurora_dawn_set_platform_backends() choice for this platform - Vulkan only, no
|
||||
# D3D/HLSL. Kept in sync here because this project's own CMake FORCEs these cache
|
||||
# variables before aurora-main's add_subdirectory() runs, which pre-empts aurora's
|
||||
# auto-detection (CACHE ... INTERNAL "" without FORCE never overrides an existing value).
|
||||
elseif(MKW_PLATFORM_MACOS)
|
||||
set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_VULKAN OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_METAL ON CACHE BOOL "" FORCE)
|
||||
set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE)
|
||||
else()
|
||||
set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE)
|
||||
set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
|
||||
# Provide a tiny stub for DXProgrammableCapture when the SDK/PIX headers are
|
||||
# missing (common on MinGW). Dawn only includes the header; no symbols are
|
||||
# referenced when PIX isn't present.
|
||||
set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs")
|
||||
if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h")
|
||||
file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR})
|
||||
file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h"
|
||||
"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX is absent.\n")
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs")
|
||||
if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h")
|
||||
file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR})
|
||||
file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h"
|
||||
"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX isn't present.\n")
|
||||
endif()
|
||||
endif()
|
||||
# Deliberately NOT injected project-wide. Only a from-source Dawn build ever
|
||||
# includes DXProgrammableCapture.h, and this tree consumes Dawn as a prebuilt
|
||||
@@ -183,7 +194,9 @@ else()
|
||||
if(TARGET ${t})
|
||||
set_target_properties(${t} PROPERTIES UNITY_BUILD OFF)
|
||||
target_compile_options(${t} PRIVATE -O3 -ffast-math -w -pipe)
|
||||
target_include_directories(${t} PRIVATE ${MKW_DX_STUB_DIR})
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_include_directories(${t} PRIVATE ${MKW_DX_STUB_DIR})
|
||||
endif()
|
||||
# Aurora's own sources include Windows headers and call std::min/max;
|
||||
# they relied on the old project-wide NOMINMAX that no longer leaks
|
||||
# into this subtree, so the define is applied per target here.
|
||||
@@ -228,6 +241,18 @@ endif()
|
||||
# a registration file is silently never compiled and never errors. The stale-glob
|
||||
# failure mode is worth far more than the milliseconds.
|
||||
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory.cpp")
|
||||
# HostContext's Apple Silicon backend is implemented in a small assembly
|
||||
# companion. It must be part of the product runtime as well as the
|
||||
# standalone context test; otherwise the final executable is missing
|
||||
# mkw_co_init/mkw_co_switch at link time.
|
||||
enable_language(ASM)
|
||||
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
|
||||
else()
|
||||
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
|
||||
endif()
|
||||
set(MKW_PLATFORM_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/platform/host_platform.cpp")
|
||||
set(MKW_BASE_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/base_product.cpp")
|
||||
set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro_rewind_product.cpp")
|
||||
# The host ISA guard is the one translation unit that must not receive the
|
||||
@@ -235,25 +260,134 @@ set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro
|
||||
# mkw_runtime_common. See cmake/PublicProducts.cmake and the file's own header.
|
||||
set(MKW_CPU_BASELINE_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/host_cpu_baseline.cpp")
|
||||
list(REMOVE_ITEM SOURCES ${MKW_BASE_PRODUCT_SOURCE} ${MKW_RETRO_REWIND_PRODUCT_SOURCE}
|
||||
${MKW_CPU_BASELINE_SOURCE})
|
||||
${MKW_CPU_BASELINE_SOURCE} ${MKW_PLATFORM_SOURCE})
|
||||
|
||||
# This deliberately small library contains host services that are safe to
|
||||
# validate before guest memory and fiber work makes a full runtime build viable.
|
||||
add_library(mkw_platform STATIC "${MKW_PLATFORM_SOURCE}")
|
||||
target_include_directories(mkw_platform PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_platform PUBLIC cxx_std_17)
|
||||
set_target_properties(mkw_platform PROPERTIES UNITY_BUILD OFF)
|
||||
|
||||
# Keep these independent from Aurora's BUILD_TESTING option: they validate the
|
||||
# project's host-platform contracts, not Aurora's third-party test suite.
|
||||
enable_testing()
|
||||
add_executable(mkw_platform_paths_tests "${CMAKE_CURRENT_LIST_DIR}/tests/platform_paths_tests.cpp")
|
||||
target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
|
||||
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
|
||||
|
||||
add_executable(mkw_nand_settings_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_settings_tests.cpp")
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(mkw_nand_settings_tests PRIVATE Threads::Threads)
|
||||
target_include_directories(mkw_nand_settings_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
|
||||
|
||||
add_executable(mkw_nand_save_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_save_tests.cpp")
|
||||
target_include_directories(mkw_nand_save_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_nand_save_tests COMMAND mkw_nand_save_tests)
|
||||
|
||||
# The input expression engine is self-contained, so it can be exercised without
|
||||
# linking the runtime or SDL.
|
||||
add_executable(mkw_input_expr_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/test_expr.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/input_expr.cpp")
|
||||
target_include_directories(mkw_input_expr_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
|
||||
|
||||
# HostContext deliberately keeps the platform-specific context primitive out
|
||||
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
|
||||
# refactors cannot silently remove its headers, implementation, or link edge.
|
||||
if(MKW_PLATFORM_LINUX)
|
||||
add_executable(mkw_linux_host_context_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp")
|
||||
target_include_directories(mkw_linux_host_context_tests PRIVATE
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/third_party/libco")
|
||||
target_compile_features(mkw_linux_host_context_tests PRIVATE cxx_std_17)
|
||||
target_link_libraries(mkw_linux_host_context_tests PRIVATE mkw::libco)
|
||||
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
|
||||
endif()
|
||||
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
# Exercise the Apple Silicon context ABI and the public host-memory
|
||||
# contracts separately from translated products.
|
||||
enable_language(ASM)
|
||||
add_executable(mkw_macos_context_abi_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
|
||||
target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
|
||||
|
||||
add_executable(mkw_macos_host_context_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
|
||||
target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
|
||||
|
||||
add_executable(mkw_macos_guest_flat_memory_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_guest_flat_memory_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
|
||||
target_include_directories(mkw_macos_guest_flat_memory_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_macos_guest_flat_memory_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests)
|
||||
endif()
|
||||
|
||||
# The translator emits the complete, content-addressed source graph. Consuming
|
||||
# this one manifest keeps configure independent of the 28k generated function
|
||||
# files and of optional Retro Rewind artifacts such as code.map.
|
||||
set(MKW_TRANSLATED_SHARD_MANIFEST
|
||||
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
|
||||
CACHE FILEPATH "Translator-owned aggregate shard manifest")
|
||||
if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
message(FATAL_ERROR
|
||||
"Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. "
|
||||
"Run Translator.Cli emit-build-shards first; see translator/README.md.")
|
||||
endif()
|
||||
include("${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS})
|
||||
message(STATUS
|
||||
"Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; "
|
||||
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; "
|
||||
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions")
|
||||
if(MKW_BUILD_PRODUCTS)
|
||||
set(MKW_TRANSLATED_SHARD_MANIFEST
|
||||
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
|
||||
CACHE FILEPATH "Translator-owned aggregate shard manifest")
|
||||
# The prebuilt export only needs the aurora/third-party closure configured above, so a
|
||||
# packaging machine without a translation stops here instead of failing.
|
||||
if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
message(STATUS "No translator shard manifest; configuring the native prebuilt export only")
|
||||
return()
|
||||
endif()
|
||||
if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
message(FATAL_ERROR
|
||||
"Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. "
|
||||
"Run Translator.Cli emit-build-shards first; see translator/README.md.")
|
||||
endif()
|
||||
include("${MKW_TRANSLATED_SHARD_MANIFEST}")
|
||||
set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS})
|
||||
message(STATUS
|
||||
"Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; "
|
||||
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; "
|
||||
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions")
|
||||
|
||||
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake")
|
||||
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake")
|
||||
else()
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
# Compile-only audit of native runtime sources. It deliberately avoids
|
||||
# translated products until their host dependencies are portable.
|
||||
# These sources depend on generated/RuntimeConfig.h, which is emitted for
|
||||
# a particular game by the translator and is intentionally unavailable
|
||||
# in this platform-only configuration.
|
||||
set(MKW_MACOS_NATIVE_AUDIT_SOURCES ${SOURCES})
|
||||
list(REMOVE_ITEM MKW_MACOS_NATIVE_AUDIT_SOURCES
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/abi_bridge.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/hle/os/os_alarm.cpp")
|
||||
add_library(mkw_macos_native_compile OBJECT ${MKW_MACOS_NATIVE_AUDIT_SOURCES})
|
||||
target_include_directories(mkw_macos_native_compile PRIVATE
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include" "${CMAKE_CURRENT_LIST_DIR}/src"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/.." "${CMAKE_CURRENT_LIST_DIR}/../aurora-main/include")
|
||||
target_compile_features(mkw_macos_native_compile PRIVATE cxx_std_20)
|
||||
target_compile_definitions(mkw_macos_native_compile PRIVATE SDL_MAIN_HANDLED TARGET_PC)
|
||||
target_link_libraries(mkw_macos_native_compile PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
|
||||
mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
|
||||
endif()
|
||||
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
|
||||
message(STATUS "Translated product targets disabled (MKW_BUILD_PRODUCTS=OFF)")
|
||||
endif()
|
||||
|
||||
@@ -51,6 +51,16 @@ foreach(_dir IN LISTS _mkw_np_includes)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Dawn's own packaged config (DawnTargets.cmake) links dawn::webgpu_dawn against
|
||||
# Threads::Threads directly. A from-source aurora build resolves that as a side effect of
|
||||
# add_subdirectory(aurora-main) pulling in Dawn's own CMakeLists.txt; this mode never runs that
|
||||
# subdirectory at all, so nothing else would ever define it (verified directly: configuring without
|
||||
# this fails with "The link interface of target dawn::webgpu_dawn contains: Threads::Threads but
|
||||
# the target was not found"). find_package(Threads) is one of CMake's most basic finder modules and
|
||||
# is a no-op on Windows (its threading support is already part of the CRT), so this is safe on
|
||||
# every platform this package format targets, not just the one that first hit the failure.
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
# Dawn is a prebuilt package on both sides; resolve the same install tree the
|
||||
# package was built against so its imported target (and therefore
|
||||
# webgpu_dawn.dll / dxcompiler.dll / dxil.dll) is available exactly as a
|
||||
|
||||
@@ -25,6 +25,11 @@ if(EXISTS "${DATA_INIT_BLOB_ASM}")
|
||||
endif()
|
||||
list(REMOVE_DUPLICATES SOURCES)
|
||||
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
find_library(MKW_IOKIT_FRAMEWORK IOKit REQUIRED)
|
||||
find_library(MKW_COREFOUNDATION_FRAMEWORK CoreFoundation REQUIRED)
|
||||
endif()
|
||||
|
||||
function(mkw_apply_common_compile_options target)
|
||||
target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe)
|
||||
endfunction()
|
||||
@@ -76,11 +81,13 @@ target_compile_definitions(mkw_runtime_common PRIVATE
|
||||
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
if(WIN32)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
|
||||
else()
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco)
|
||||
elseif(MKW_PLATFORM_LINUX)
|
||||
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
|
||||
# media monitoring). Empty string on glibc >= 2.34 where dl* is in libc.
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
if(MKW_CPPWINRT_INCLUDE_DIR)
|
||||
if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
|
||||
@@ -125,16 +132,16 @@ set_target_properties(mkw_runtime_common PROPERTIES UNITY_BUILD ON UNITY_BUILD_M
|
||||
target_precompile_headers(mkw_runtime_common PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
|
||||
mkw_apply_common_compile_options(mkw_runtime_common)
|
||||
|
||||
# Host ISA guard. Everything in MKW_ALL_BUILD_TARGETS below is compiled with
|
||||
# -march=x86-64-v3; this object library deliberately is not, which
|
||||
# is the whole point of keeping it out of mkw_runtime_common. It runs a CPUID
|
||||
# check from a C initializer so an unsupported machine gets a readable error
|
||||
# instead of an illegal-instruction crash. Excluded from the unity build and the
|
||||
# precompiled header because both are produced with the owning target's flags.
|
||||
add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}")
|
||||
target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17)
|
||||
set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF)
|
||||
target_compile_options(mkw_cpu_baseline PRIVATE -w)
|
||||
# Host ISA guard. Windows and Linux x86_64 product targets use x86-64-v3, so
|
||||
# this object deliberately keeps the plain baseline ISA and checks the CPU
|
||||
# before any AVX2/FMA code can execute. AArch64 has no equivalent optional ISA
|
||||
# floor to probe: NEON/FMA are architectural requirements.
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}")
|
||||
target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17)
|
||||
set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF)
|
||||
target_compile_options(mkw_cpu_baseline PRIVATE -w)
|
||||
endif()
|
||||
|
||||
if(NOT MKW_BASE_COMMON_SHARDS)
|
||||
message(FATAL_ERROR "Translator build graph contains no shared base shards")
|
||||
@@ -174,7 +181,9 @@ function(mkw_configure_product target)
|
||||
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_runtime_common>)
|
||||
# Startup CPU check. Must stay a separate object library so it keeps the
|
||||
# plain baseline ISA while everything around it is built for x86-64-v3.
|
||||
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_cpu_baseline>)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_cpu_baseline>)
|
||||
endif()
|
||||
target_include_directories(${target} PRIVATE
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/include"
|
||||
"${MKW_RUNTIME_SOURCE_DIR}/src"
|
||||
@@ -190,10 +199,14 @@ function(mkw_configure_product target)
|
||||
# include the same fat translated headers; bound them by the same pool.
|
||||
mkw_bound_translated_compiles(${target})
|
||||
target_link_libraries(${target} PRIVATE
|
||||
mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
mkw_platform mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
|
||||
target_link_libraries(${target} PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
target_link_libraries(${target} PRIVATE
|
||||
"${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}")
|
||||
endif()
|
||||
if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
|
||||
include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
|
||||
aurora_copy_runtime_dlls(${target})
|
||||
@@ -208,21 +221,23 @@ function(mkw_configure_product target)
|
||||
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_link_libraries(${target} PRIVATE
|
||||
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
|
||||
|
||||
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
else()
|
||||
elseif(MKW_PLATFORM_LINUX)
|
||||
# mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o
|
||||
# files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own
|
||||
# target_link_libraries (object libraries don't carry usage requirements to a consumer
|
||||
# that isn't itself linked against as a target). fiber_manager.cpp's co_* calls live in
|
||||
# those objects, so the actual executable link needs mkw::libco directly, same as it
|
||||
# needs it independently of that first `if(WIN32)` branch above.
|
||||
target_link_libraries(${target} PRIVATE mkw::libco)
|
||||
# needs it independently of that first `if(WIN32)` branch above. ${CMAKE_DL_LIBS} is
|
||||
# here for the same reason: music_attenuation.cpp's dlopen(libdbus-1) lives in those
|
||||
# objects (empty string on glibc >= 2.34, where dl* is in libc).
|
||||
target_link_libraries(${target} PRIVATE mkw::libco ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
if(WIN32)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
foreach(runtime_dll libc++.dll libunwind.dll)
|
||||
execute_process(
|
||||
COMMAND "${CMAKE_CXX_COMPILER}" "--print-file-name=${runtime_dll}"
|
||||
@@ -298,11 +313,23 @@ else()
|
||||
message(STATUS "RetroRewind target disabled (run translate-mod and emit-build-shards)")
|
||||
endif()
|
||||
|
||||
# Windows and Linux x86_64 share the x86-64-v3 floor that the CPU baseline
|
||||
# object above checks. AArch64 builds are compiled locally for the host that
|
||||
# will run them, so both Linux and Apple Silicon use the compiler's native CPU
|
||||
# tuning rather than leaving target-specific performance on the table.
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3)
|
||||
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
|
||||
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
|
||||
else()
|
||||
set(MKW_BASELINE_ARCH_FLAG "")
|
||||
endif()
|
||||
|
||||
set(MKW_ALL_BUILD_TARGETS
|
||||
mkw_runtime_common mkw_base_shared mkw_base_sensitive mkw_retro_sensitive
|
||||
mkw_retro_rewind_functions WiiCompiled RetroRewind)
|
||||
foreach(target IN LISTS MKW_ALL_BUILD_TARGETS)
|
||||
if(TARGET ${target})
|
||||
target_compile_options(${target} PRIVATE -march=x86-64-v3)
|
||||
if(TARGET ${target} AND MKW_BASELINE_ARCH_FLAG)
|
||||
target_compile_options(${target} PRIVATE ${MKW_BASELINE_ARCH_FLAG})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
@@ -1,38 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "runtime_config.h"
|
||||
#include "nand_path.h"
|
||||
#include "nand_settings.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace RuntimeConsoleIdentity {
|
||||
|
||||
struct Identity {
|
||||
std::string serial;
|
||||
std::string productCode;
|
||||
std::string area;
|
||||
std::string gameRegion;
|
||||
std::array<uint8_t, 6> mac;
|
||||
};
|
||||
|
||||
inline bool IsValidSerial(const std::string& serial) {
|
||||
return serial.size() == 9 &&
|
||||
serial != "000000000" &&
|
||||
std::all_of(serial.begin(), serial.end(),
|
||||
[](unsigned char value) { return std::isdigit(value) != 0; });
|
||||
}
|
||||
|
||||
inline Identity FromSerial(std::string serial) {
|
||||
// Keep Nintendo's Wii OUI. The suffix is derived from the persisted serial
|
||||
// Keep Nintendo's Wii OUI. The suffix is derived from the NAND serial
|
||||
// so every API exposes one coherent, stable virtual-console identity.
|
||||
uint32_t hash = 2166136261u;
|
||||
for (const unsigned char value : serial) {
|
||||
@@ -46,6 +36,7 @@ inline Identity FromSerial(std::string serial) {
|
||||
|
||||
return {
|
||||
std::move(serial),
|
||||
{}, {}, {},
|
||||
{
|
||||
0x00,
|
||||
0x09,
|
||||
@@ -57,83 +48,23 @@ inline Identity FromSerial(std::string serial) {
|
||||
};
|
||||
}
|
||||
|
||||
inline std::optional<std::string> ReadSerial(const std::filesystem::path& path) {
|
||||
std::ifstream input(path);
|
||||
std::string line;
|
||||
if (!input || !std::getline(input, line)) {
|
||||
return std::nullopt;
|
||||
inline Identity LoadFromNand() {
|
||||
const auto root = RuntimeNandPath::DiscoverNandRootPath();
|
||||
const auto settings = RuntimeNandSettings::Read(root);
|
||||
if (!settings || !RuntimeNandSettings::HasIdentity(*settings)) {
|
||||
RuntimeNandPath::FailNandRoot(
|
||||
"NAND setting.txt is missing or has invalid console identity fields (SERNO, CODE, AREA, GAME)",
|
||||
root / "title/00000001/00000002/data/setting.txt");
|
||||
}
|
||||
constexpr std::string_view prefix = "serial=";
|
||||
if (line.rfind(prefix, 0) != 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::string serial = line.substr(prefix.size());
|
||||
if (!IsValidSerial(serial)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return serial;
|
||||
}
|
||||
|
||||
inline bool WriteSerial(const std::filesystem::path& path, const std::string& serial) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
if (ec) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::path temporary = path;
|
||||
temporary += ".tmp";
|
||||
{
|
||||
std::ofstream output(temporary, std::ios::trunc);
|
||||
if (!output) {
|
||||
return false;
|
||||
}
|
||||
output << "serial=" << serial << '\n';
|
||||
output.close();
|
||||
if (!output) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::rename(temporary, path, ec);
|
||||
if (!ec) {
|
||||
return true;
|
||||
}
|
||||
std::filesystem::remove(temporary, ec);
|
||||
return false;
|
||||
}
|
||||
|
||||
inline std::string GenerateSerial() {
|
||||
std::random_device entropy;
|
||||
std::seed_seq seed{
|
||||
entropy(),
|
||||
entropy(),
|
||||
entropy(),
|
||||
entropy(),
|
||||
};
|
||||
std::mt19937 generator(seed);
|
||||
std::uniform_int_distribution<uint32_t> distribution(100000000u, 999999999u);
|
||||
return std::to_string(distribution(generator));
|
||||
}
|
||||
|
||||
inline Identity LoadOrCreate(const std::filesystem::path& path) {
|
||||
if (const auto serial = ReadSerial(path)) {
|
||||
return FromSerial(*serial);
|
||||
}
|
||||
|
||||
const std::string generated = GenerateSerial();
|
||||
if (WriteSerial(path, generated)) {
|
||||
return FromSerial(generated);
|
||||
}
|
||||
|
||||
// Remain operational in a read-only environment. This fallback matches
|
||||
// Dolphin's deterministic serial while keeping the same valid identity shape.
|
||||
return FromSerial("123456789");
|
||||
Identity identity = FromSerial(settings->at("SERNO"));
|
||||
identity.productCode = settings->at("CODE");
|
||||
identity.area = settings->at("AREA");
|
||||
identity.gameRegion = settings->at("GAME");
|
||||
return identity;
|
||||
}
|
||||
|
||||
inline const Identity& Current() {
|
||||
static const Identity identity =
|
||||
LoadOrCreate(RuntimeConfigFile::ApplicationDataDirectory() / "ConsoleIdentity.txt");
|
||||
static const Identity identity = LoadFromNand();
|
||||
return identity;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#pragma once
|
||||
|
||||
// The single vocabulary shared by everything that has to turn a Config.toml
|
||||
// controller name into a real button: the F10 settings bar, the macro engine,
|
||||
// and the startup mapping pass. Keeping one table here means a name that the
|
||||
// settings bar offers is always a name the config parser accepts, and vice
|
||||
// versa; the two used to drift because each side carried its own copy.
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <dolphin/pad.h>
|
||||
|
||||
namespace ControllerNames {
|
||||
|
||||
// A GameCube button as the game sees it, with the Config.toml key that selects
|
||||
// it. Order matches RuntimeConfigFile::kControllerButtonKeys.
|
||||
struct GameCubeButtonItem {
|
||||
const char* configKey;
|
||||
const char* label;
|
||||
PADButton padButton;
|
||||
};
|
||||
|
||||
inline constexpr std::array<GameCubeButtonItem, PAD_BUTTON_COUNT> kGameCubeButtons = {{
|
||||
{"a", "A", PAD_BUTTON_A},
|
||||
{"b", "B", PAD_BUTTON_B},
|
||||
{"x", "X", PAD_BUTTON_X},
|
||||
{"y", "Y", PAD_BUTTON_Y},
|
||||
{"start", "Start", PAD_BUTTON_START},
|
||||
{"z", "Z", PAD_TRIGGER_Z},
|
||||
{"l", "L", PAD_TRIGGER_L},
|
||||
{"r", "R", PAD_TRIGGER_R},
|
||||
{"up", "D-pad Up", PAD_BUTTON_UP},
|
||||
{"down", "D-pad Down", PAD_BUTTON_DOWN},
|
||||
{"left", "D-pad Left", PAD_BUTTON_LEFT},
|
||||
{"right", "D-pad Right", PAD_BUTTON_RIGHT},
|
||||
}};
|
||||
|
||||
// A physical button on the host pad. Names are positional (south/east/...)
|
||||
// rather than Xbox-labelled so one config reads the same on any hardware.
|
||||
struct NativeButtonItem {
|
||||
const char* configName;
|
||||
const char* label;
|
||||
uint32_t nativeButton;
|
||||
};
|
||||
|
||||
inline constexpr std::array<NativeButtonItem, SDL_GAMEPAD_BUTTON_COUNT + 1> kNativeButtons = {{
|
||||
{"unmapped", "Unmapped / analog trigger", PAD_NATIVE_BUTTON_INVALID},
|
||||
{"south", "South (A / Cross)", SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
{"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST},
|
||||
{"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST},
|
||||
{"north", "North (Y / Triangle)", SDL_GAMEPAD_BUTTON_NORTH},
|
||||
{"back", "Back / Select / Create", SDL_GAMEPAD_BUTTON_BACK},
|
||||
{"guide", "Guide / Home / PS", SDL_GAMEPAD_BUTTON_GUIDE},
|
||||
{"start", "Start / Options", SDL_GAMEPAD_BUTTON_START},
|
||||
{"left_stick", "Left stick click (L3)", SDL_GAMEPAD_BUTTON_LEFT_STICK},
|
||||
{"right_stick", "Right stick click (R3)", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
|
||||
{"left_shoulder", "Left bumper (LB / L1)", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
|
||||
{"right_shoulder", "Right bumper (RB / R1)", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
|
||||
{"dpad_up", "D-pad Up", SDL_GAMEPAD_BUTTON_DPAD_UP},
|
||||
{"dpad_down", "D-pad Down", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
|
||||
{"dpad_left", "D-pad Left", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
|
||||
{"dpad_right", "D-pad Right", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
||||
{"misc1", "Misc 1 / Share / Mic", SDL_GAMEPAD_BUTTON_MISC1},
|
||||
{"right_paddle1", "Right paddle 1", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
|
||||
{"left_paddle1", "Left paddle 1", SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
|
||||
{"right_paddle2", "Right paddle 2", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
|
||||
{"left_paddle2", "Left paddle 2", SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
|
||||
{"touchpad", "Touchpad click", SDL_GAMEPAD_BUTTON_TOUCHPAD},
|
||||
{"misc2", "Misc 2", SDL_GAMEPAD_BUTTON_MISC2},
|
||||
{"misc3", "Misc 3 / GC L click", SDL_GAMEPAD_BUTTON_MISC3},
|
||||
{"misc4", "Misc 4 / GC R click", SDL_GAMEPAD_BUTTON_MISC4},
|
||||
{"misc5", "Misc 5", SDL_GAMEPAD_BUTTON_MISC5},
|
||||
{"misc6", "Misc 6", SDL_GAMEPAD_BUTTON_MISC6},
|
||||
}};
|
||||
|
||||
inline std::string TrimToken(std::string_view token) {
|
||||
const size_t begin = token.find_first_not_of(" \t");
|
||||
if (begin == std::string_view::npos) {
|
||||
return {};
|
||||
}
|
||||
const size_t end = token.find_last_not_of(" \t");
|
||||
return std::string(token.substr(begin, end - begin + 1));
|
||||
}
|
||||
|
||||
inline const NativeButtonItem* FindNativeButton(std::string_view configName) {
|
||||
const std::string name = TrimToken(configName);
|
||||
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
|
||||
[&](const NativeButtonItem& item) { return name == item.configName; });
|
||||
return it == kNativeButtons.end() ? nullptr : &*it;
|
||||
}
|
||||
|
||||
// Falls back to the "unmapped" entry so callers always have a label to draw.
|
||||
inline const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) {
|
||||
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
|
||||
[&](const NativeButtonItem& item) { return nativeButton == item.nativeButton; });
|
||||
return it == kNativeButtons.end() ? kNativeButtons.front() : *it;
|
||||
}
|
||||
|
||||
inline const GameCubeButtonItem* FindGameCubeButton(std::string_view configKey) {
|
||||
const std::string key = TrimToken(configKey);
|
||||
const auto it = std::find_if(kGameCubeButtons.begin(), kGameCubeButtons.end(),
|
||||
[&](const GameCubeButtonItem& item) { return key == item.configKey; });
|
||||
return it == kGameCubeButtons.end() ? nullptr : &*it;
|
||||
}
|
||||
|
||||
// "up" or "up,a" -> the OR of those GC button bits. Unknown names are skipped so
|
||||
// a typo costs one button instead of the whole macro.
|
||||
inline uint16_t GameCubeMaskFromKeys(std::string_view keys) {
|
||||
uint16_t mask = 0;
|
||||
size_t begin = 0;
|
||||
while (begin <= keys.size()) {
|
||||
const size_t comma = keys.find(',', begin);
|
||||
const std::string_view token =
|
||||
keys.substr(begin, comma == std::string_view::npos ? std::string_view::npos : comma - begin);
|
||||
if (const GameCubeButtonItem* item = FindGameCubeButton(token)) {
|
||||
mask |= static_cast<uint16_t>(item->padButton);
|
||||
}
|
||||
if (comma == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
begin = comma + 1;
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
inline std::string GameCubeKeysFromMask(uint16_t mask) {
|
||||
std::string keys;
|
||||
for (const auto& item : kGameCubeButtons) {
|
||||
if ((mask & static_cast<uint16_t>(item.padButton)) == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!keys.empty()) {
|
||||
keys += ',';
|
||||
}
|
||||
keys += item.configKey;
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
inline std::string GameCubeLabelsFromMask(uint16_t mask) {
|
||||
std::string labels;
|
||||
for (const auto& item : kGameCubeButtons) {
|
||||
if ((mask & static_cast<uint16_t>(item.padButton)) == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!labels.empty()) {
|
||||
labels += " + ";
|
||||
}
|
||||
labels += item.label;
|
||||
}
|
||||
return labels.empty() ? std::string("None") : labels;
|
||||
}
|
||||
|
||||
} // namespace ControllerNames
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
// Host implementation of Dolphin's /dev/dolphin Discord contract. The guest
|
||||
// sends only strings and big-endian integer fields; the IPC wire protocol is
|
||||
// owned here so translated game code never needs host SDK headers.
|
||||
namespace DiscordPresence {
|
||||
|
||||
struct Activity {
|
||||
std::string details;
|
||||
std::string state;
|
||||
std::string largeImageKey;
|
||||
std::string largeImageText;
|
||||
std::string smallImageKey;
|
||||
std::string smallImageText;
|
||||
int64_t startTimestamp = 0;
|
||||
int64_t endTimestamp = 0;
|
||||
uint32_t partySize = 0;
|
||||
uint32_t partyMax = 0;
|
||||
};
|
||||
|
||||
void Initialize(const std::string& basicClientId, const std::string& basicTitle);
|
||||
void SetClient(const std::string& clientId);
|
||||
void SetActivity(Activity activity);
|
||||
void Reset();
|
||||
void Shutdown();
|
||||
|
||||
} // namespace DiscordPresence
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#ifndef NOMINMAX
|
||||
@@ -22,7 +23,7 @@
|
||||
// Forward declarations
|
||||
struct CpuContext;
|
||||
|
||||
// GuestFiberManager: each guest OSThread maps to a Windows Fiber. A scheduler fiber picks
|
||||
// GuestFiberManager: each guest OSThread maps to a host context. A scheduler context picks
|
||||
// which guest fiber runs; a real timer thread queues VI retraces at the VI cadence. Guest
|
||||
// threads only switch at explicit yield points (OSSleepThread, OSYieldThread, ...), matching
|
||||
// Wii cooperative semantics exactly.
|
||||
@@ -39,7 +40,7 @@ enum class ThreadState : uint32_t {
|
||||
|
||||
// Information about a guest fiber
|
||||
struct GuestFiber {
|
||||
void* fiber = nullptr; // Windows fiber handle
|
||||
void* fiber = nullptr; // Host context handle
|
||||
uint32_t entryPoint = 0; // Thread entry function
|
||||
uint32_t entryArg = 0; // Argument to entry function
|
||||
CpuContext cpuContext{}; // Saved CPU context for this fiber
|
||||
@@ -102,10 +103,6 @@ private:
|
||||
static void CALLBACK FiberProc(void* param);
|
||||
#else
|
||||
static void FiberProc(void* param);
|
||||
// libco's co_create() entry points take no argument (unlike CreateFiber's FiberProc(void*)),
|
||||
// so this trampoline reads the guest thread address staged by CreateGuestFiber() and forwards
|
||||
// into the (platform-neutral-bodied) FiberProc above. See fiber_manager.cpp.
|
||||
static void FiberProcTrampoline();
|
||||
#endif
|
||||
// Switch from whichever fiber is currently active straight to the scheduler fiber, without
|
||||
// the SwitchToThread bookkeeping (CPU context save/restore, s_currentGuestThread). Used for
|
||||
@@ -117,9 +114,8 @@ private:
|
||||
static std::mutex s_mutex;
|
||||
static std::unordered_map<uint32_t, GuestFiber> s_fibers;
|
||||
static std::vector<void*> s_fibersPendingDelete;
|
||||
// The scheduler's own "fiber": a Windows HFIBER, or (non-Windows) libco's cothread_t for
|
||||
// whichever native call stack first called GuestFiberManager::Initialize() - both are
|
||||
// plain void* handles, so one field serves both platforms.
|
||||
// The scheduler's own host context. Its opaque handle is supplied by the
|
||||
// active HostContext backend, so one field serves every supported host.
|
||||
static void* s_schedulerFiber;
|
||||
static uint32_t s_currentGuestThread;
|
||||
static bool s_initialized;
|
||||
@@ -135,4 +131,3 @@ private:
|
||||
extern std::atomic<uint32_t> g_viRetracePendingCount;
|
||||
|
||||
} // namespace Fiber
|
||||
|
||||
|
||||
@@ -12,10 +12,33 @@
|
||||
namespace GuestFlat {
|
||||
|
||||
// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load
|
||||
// of a global. 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the
|
||||
// usual image/heap placement.
|
||||
// of a global.
|
||||
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
|
||||
inline constexpr size_t kGuestPageSize = 0x1000;
|
||||
#if defined(__x86_64__)
|
||||
// 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap
|
||||
// placement.
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'1000'0000'0000ull;
|
||||
#elif defined(__aarch64__) && defined(__APPLE__)
|
||||
// Keep this well above the low address ranges that Darwin's ASLR may use for
|
||||
// a PIE executable and its shared cache. Apple Silicon's user VA is wider
|
||||
// than Linux's 39-bit minimum, so this 512 GiB region is available while the
|
||||
// Linux AArch64 target retains its 64 GiB placement below.
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0080'0000'0000ull;
|
||||
#elif defined(__aarch64__)
|
||||
// 16 TiB (this arch's x86_64 sibling value) is unreachable on any AArch64
|
||||
// kernel configured for 39-bit virtual addresses (512 GiB ceiling) - common on
|
||||
// older/embedded targets (e.g. this project's own tested Jetson/L4T board, kernel
|
||||
// 4.9). There mmap() silently ignores a hint above the ceiling and hands back an
|
||||
// address near the top of the real range instead, which this module's caller
|
||||
// then rejects as "already occupied". 64 GiB is reachable on every AArch64 VA
|
||||
// width in real use (39-bit minimum and up) and was confirmed via direct mmap
|
||||
// probing to sit far below where the PIE image, heap, shared libraries and
|
||||
// stack actually land (all clustered above ~340 GiB on a 39-bit/512 GiB system).
|
||||
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0010'0000'0000ull;
|
||||
#else
|
||||
#error "guest_flat_memory.h has no fixed flat guest base chosen for this architecture"
|
||||
#endif
|
||||
|
||||
#define MKW_FLAT_GUEST_BASE (reinterpret_cast<uint8_t*>(GuestFlat::kFixedFlatGuestBase))
|
||||
|
||||
@@ -42,6 +65,25 @@ struct FaultCounters {
|
||||
// True once the reservation exists and translated code may use the flat path.
|
||||
bool IsActive();
|
||||
|
||||
// True when a host VM page covers more than one 4 KiB Wii page. In that
|
||||
// configuration, guest-view page protection cannot safely represent per-Wii-
|
||||
// page MMIO, deferred-read, or executable-write state, so general translated
|
||||
// accesses must use the checked Memory::* path.
|
||||
// Windows user mode and x86-64 always use a 4 KiB base page, so those builds
|
||||
// fold this to a compile-time false: it appears in every flat access and must
|
||||
// not become a hot-path load. Only AArch64, where the page size is a kernel
|
||||
// configuration (4/16/64 KiB), has to probe it at runtime.
|
||||
#if defined(_WIN32) || defined(__x86_64__)
|
||||
#define MKW_GUEST_FLAT_FIXED_PAGE_SIZE 1
|
||||
#endif
|
||||
|
||||
#if defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
|
||||
inline constexpr bool RequiresCheckedAccess() noexcept { return false; }
|
||||
#else
|
||||
extern bool g_requiresCheckedAccess;
|
||||
inline bool RequiresCheckedAccess() noexcept { return g_requiresCheckedAccess; }
|
||||
#endif
|
||||
|
||||
// Reserves the 4 GiB space (once per process) and maps every requested region
|
||||
// into both views. Throws std::runtime_error with a precise diagnosis when the
|
||||
// reservation, the section objects or a view cannot be created - a silent
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
// HostContext is the deliberately small boundary between the guest scheduler
|
||||
// and the host's cooperative-context facility. Windows uses native Fibers and
|
||||
// Linux uses libco; macOS AArch64 uses the local assembly backend because it
|
||||
// must preserve Darwin's platform-reserved x18 register, which libco's AArch64
|
||||
// backend does not save. Its handles are only valid on the thread that
|
||||
// initialized the scheduler.
|
||||
namespace HostContext {
|
||||
|
||||
using Handle = void*;
|
||||
using Entry = void (*)(void*);
|
||||
|
||||
bool InitializeScheduler(Handle* scheduler);
|
||||
void ShutdownScheduler(Handle scheduler);
|
||||
|
||||
Handle Create(std::size_t stackSize, Entry entry, void* argument);
|
||||
void Destroy(Handle context);
|
||||
bool IsCurrent(Handle context);
|
||||
void Switch(Handle target);
|
||||
|
||||
} // namespace HostContext
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
// Per-port expression bindings for the GameCube controls, plus import of a
|
||||
// Dolphin GCPadNew.ini.
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include <dolphin/pad.h>
|
||||
|
||||
namespace InputBindings {
|
||||
|
||||
// The controls an expression can drive, in Dolphin's own naming so an
|
||||
// imported config maps across without translation.
|
||||
struct ControlInfo {
|
||||
const char* dolphinName;
|
||||
const char* label;
|
||||
uint16_t padButton; // 0 for the analog-only controls below
|
||||
int analog; // 0 none, 1 trigger L, 2 trigger R
|
||||
};
|
||||
|
||||
inline constexpr std::array<ControlInfo, 14> kControls = {{
|
||||
{"Buttons/A", "A", PAD_BUTTON_A, 0},
|
||||
{"Buttons/B", "B", PAD_BUTTON_B, 0},
|
||||
{"Buttons/X", "X", PAD_BUTTON_X, 0},
|
||||
{"Buttons/Y", "Y", PAD_BUTTON_Y, 0},
|
||||
{"Buttons/Z", "Z", PAD_TRIGGER_Z, 0},
|
||||
{"Buttons/Start", "Start", PAD_BUTTON_START, 0},
|
||||
{"D-Pad/Up", "D-pad Up", PAD_BUTTON_UP, 0},
|
||||
{"D-Pad/Down", "D-pad Down", PAD_BUTTON_DOWN, 0},
|
||||
{"D-Pad/Left", "D-pad Left", PAD_BUTTON_LEFT, 0},
|
||||
{"D-Pad/Right", "D-pad Right", PAD_BUTTON_RIGHT, 0},
|
||||
{"Triggers/L", "L", PAD_TRIGGER_L, 1},
|
||||
{"Triggers/R", "R", PAD_TRIGGER_R, 2},
|
||||
{"Triggers/L-Analog", "L analog", 0, 1},
|
||||
{"Triggers/R-Analog", "R analog", 0, 2},
|
||||
}};
|
||||
|
||||
void Reload() noexcept;
|
||||
|
||||
// The pad library has PADBlockInput but no matching query, so the settings
|
||||
// overlay reports its own state here.
|
||||
void SetInputBlocked(bool blocked) noexcept;
|
||||
bool InputBlocked() noexcept;
|
||||
|
||||
// Mix expression output into a freshly read status set. Call once per guest
|
||||
// PADRead, after every other input source has been merged.
|
||||
void Apply(PADStatus* statuses) noexcept;
|
||||
|
||||
std::string GetExpression(uint32_t port, size_t control) noexcept;
|
||||
// Returns false and fills error if the text does not parse; the binding is
|
||||
// left unchanged in that case.
|
||||
bool SetExpression(uint32_t port, size_t control, const std::string& text, std::string& error) noexcept;
|
||||
|
||||
// True while the control's expression is above the press threshold.
|
||||
bool IsActive(uint32_t port, size_t control) noexcept;
|
||||
|
||||
// The default Dolphin config location on Windows, then next to the executable.
|
||||
std::string DefaultDolphinConfigPath() noexcept;
|
||||
|
||||
// Imports [GCPad<padIndex>] into the given port. Returns the number of controls
|
||||
// imported, or -1 on failure with error filled.
|
||||
int ImportDolphinConfig(const std::string& path, int padIndex, uint32_t port,
|
||||
std::string& summary, std::string& error) noexcept;
|
||||
|
||||
} // namespace InputBindings
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
// Dolphin-compatible input expressions.
|
||||
//
|
||||
// Values are doubles in Dolphin's ControlState style; a control counts as
|
||||
// pressed above kConditionThreshold. Timing matches Dolphin: wall-clock
|
||||
// seconds on a steady clock, so an expression copied from GCPadNew.ini
|
||||
// behaves the same here as it does there.
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace InputExpr {
|
||||
|
||||
inline constexpr double kConditionThreshold = 0.5;
|
||||
|
||||
// Resolves a backtick-quoted input name to its current value.
|
||||
using InputSource = std::function<double(const std::string&)>;
|
||||
|
||||
struct Node;
|
||||
|
||||
class Expression {
|
||||
public:
|
||||
Expression();
|
||||
~Expression();
|
||||
Expression(Expression&&) noexcept;
|
||||
Expression& operator=(Expression&&) noexcept;
|
||||
|
||||
// Returns false and fills error on a syntax problem.
|
||||
static bool Parse(const std::string& text, Expression& out, std::string& error);
|
||||
|
||||
bool Empty() const { return m_root == nullptr; }
|
||||
double Evaluate(const InputSource& source) const;
|
||||
|
||||
// Input names the expression references, for diagnostics.
|
||||
std::vector<std::string> ReferencedInputs() const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Node> m_root;
|
||||
};
|
||||
|
||||
// Parses a Dolphin GCPadNew.ini and returns the expression text for each
|
||||
// control of the requested pad, keyed by Dolphin's own control names
|
||||
// ("Buttons/A", "D-Pad/Up", "Triggers/L", ...). Returns false if the file
|
||||
// cannot be read or the section is missing.
|
||||
bool ReadDolphinConfig(const std::filesystem::path& path, int padIndex,
|
||||
std::vector<std::pair<std::string, std::string>>& controls,
|
||||
std::string& deviceName, std::string& error);
|
||||
|
||||
} // namespace InputExpr
|
||||
@@ -4,7 +4,13 @@
|
||||
#include <cstdint>
|
||||
|
||||
#define MKW_RESTRICT __restrict
|
||||
#if defined(__x86_64__)
|
||||
#include <immintrin.h>
|
||||
#elif defined(__aarch64__)
|
||||
#include <arm_neon.h>
|
||||
#else
|
||||
#error "ppc_isa_config.h has no SIMD intrinsics header for this architecture"
|
||||
#endif
|
||||
|
||||
inline constexpr bool MkwStateFreeAbiEnabled(uint32_t) noexcept
|
||||
{
|
||||
|
||||
@@ -62,7 +62,7 @@ public:
|
||||
{
|
||||
g_currentCpuContext = ctx;
|
||||
|
||||
savedMxcsr_ = _mm_getcsr();
|
||||
savedMxcsr_ = MkwGetHostFpControl();
|
||||
if (ctx != nullptr)
|
||||
MkwApplyHostNiMode(ctx->fpscr);
|
||||
}
|
||||
|
||||
@@ -78,37 +78,71 @@ inline void PpcSetPairedFprInline(PPC_FPR& fpr, double packed)
|
||||
fpr.d = packed;
|
||||
}
|
||||
|
||||
// Must stay inside the XMM register domain. Bitcasting through a 64-bit GPR added a movq
|
||||
#if defined(__x86_64__)
|
||||
using PpcPairVec = __m128;
|
||||
#elif defined(__aarch64__)
|
||||
// Only 2 lanes are ever meaningful (a PPC paired-single register), so a 2-lane float32x2_t
|
||||
// (one 64-bit D register) is a more natural fit than mirroring x86's 128-bit register usage.
|
||||
using PpcPairVec = float32x2_t;
|
||||
#endif
|
||||
|
||||
// Must stay inside the vector register domain. Bitcasting through a 64-bit GPR added a movq
|
||||
// domain crossing on every paired-single op (630 in the THP IDCT region alone); a double
|
||||
// local already lives in an XMM register, so these casts compile to nothing.
|
||||
inline __m128 PpcPsToM128Inline(double value)
|
||||
// local already lives in a vector register, so these casts compile to nothing.
|
||||
inline PpcPairVec PpcPsToM128Inline(double value)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
return _mm_castpd_ps(_mm_set_sd(value));
|
||||
#elif defined(__aarch64__)
|
||||
return vreinterpret_f32_f64(vdup_n_f64(value));
|
||||
#endif
|
||||
}
|
||||
|
||||
inline double PpcM128ToPsInline(__m128 value)
|
||||
inline double PpcM128ToPsInline(PpcPairVec value)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
return _mm_cvtsd_f64(_mm_castps_pd(value));
|
||||
#elif defined(__aarch64__)
|
||||
return vget_lane_f64(vreinterpret_f64_f32(value), 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
inline __m128 PpcBroadcastPs0Inline(double value)
|
||||
inline PpcPairVec PpcBroadcastPs0Inline(double value)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
const __m128 lanes = PpcPsToM128Inline(value);
|
||||
return _mm_shuffle_ps(lanes, lanes, _MM_SHUFFLE(1, 1, 1, 1));
|
||||
#elif defined(__aarch64__)
|
||||
// ps0 lives in lane 1 (see the lane-accessor comment below).
|
||||
return vdup_lane_f32(PpcPsToM128Inline(value), 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
inline __m128 PpcBroadcastPs1Inline(double value)
|
||||
inline PpcPairVec PpcBroadcastPs1Inline(double value)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
const __m128 lanes = PpcPsToM128Inline(value);
|
||||
return _mm_shuffle_ps(lanes, lanes, _MM_SHUFFLE(0, 0, 0, 0));
|
||||
#elif defined(__aarch64__)
|
||||
// ps1 is already lane 0.
|
||||
return vdup_lane_f32(PpcPsToM128Inline(value), 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
inline __m128 PpcNegateNonNanLanesInline(__m128 value)
|
||||
inline PpcPairVec PpcNegateNonNanLanesInline(PpcPairVec value)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
const __m128 signMask = _mm_castsi128_ps(_mm_set1_epi32(static_cast<int>(0x80000000u)));
|
||||
const __m128 negated = _mm_xor_ps(value, signMask);
|
||||
const __m128 ordered = _mm_cmpord_ps(value, value);
|
||||
return _mm_or_ps(_mm_and_ps(ordered, negated), _mm_andnot_ps(ordered, value));
|
||||
#elif defined(__aarch64__)
|
||||
const uint32x2_t signMask = vdup_n_u32(0x80000000u);
|
||||
const float32x2_t negated = vreinterpret_f32_u32(veor_u32(vreinterpret_u32_f32(value), signMask));
|
||||
// NEON has no direct "ordered compare"; a value compares equal to itself iff it's not NaN.
|
||||
const uint32x2_t ordered = vceq_f32(value, value);
|
||||
return vbsl_f32(ordered, negated, value);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Paired-single lane accessors. The packed double's LOW 32 bits hold ps1 and HIGH 32 bits
|
||||
@@ -119,20 +153,34 @@ inline __m128 PpcNegateNonNanLanesInline(__m128 value)
|
||||
inline float PpcGetPs0Inline(double value)
|
||||
{
|
||||
// ps0 lives in lane 1; PpcBroadcastPs0Inline already splats it.
|
||||
#if defined(__x86_64__)
|
||||
return _mm_cvtss_f32(PpcBroadcastPs0Inline(value));
|
||||
#elif defined(__aarch64__)
|
||||
return vget_lane_f32(PpcBroadcastPs0Inline(value), 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
inline float PpcGetPs1Inline(double value)
|
||||
{
|
||||
// ps1 is already lane 0 of the packed representation.
|
||||
#if defined(__x86_64__)
|
||||
return _mm_cvtss_f32(PpcPsToM128Inline(value));
|
||||
#elif defined(__aarch64__)
|
||||
return vget_lane_f32(PpcPsToM128Inline(value), 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
inline double PpcPackPairedInline(float ps0, float ps1)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
// _mm_unpacklo_ps(x, y) -> { x[0], y[0], x[1], y[1] }, so lane 0 becomes
|
||||
// ps1 and lane 1 becomes ps0, matching the union layout bit for bit.
|
||||
return PpcM128ToPsInline(_mm_unpacklo_ps(_mm_set_ss(ps1), _mm_set_ss(ps0)));
|
||||
#elif defined(__aarch64__)
|
||||
// Lane 0 = ps1, lane 1 = ps0, matching the union layout bit for bit.
|
||||
const float32x2_t lane0 = vdup_n_f32(ps1);
|
||||
return PpcM128ToPsInline(vset_lane_f32(ps0, lane0, 1));
|
||||
#endif
|
||||
}
|
||||
|
||||
// FPSCR[NI] is modeled by MXCSR FTZ/DAZ, so arithmetic output flushing compiles to nothing.
|
||||
@@ -148,14 +196,24 @@ inline float PpcForceSingleValueInline(double value)
|
||||
// FPSCR[NI] flushes an exact pre-round single-subnormal even when rounding would promote it
|
||||
// to the smallest normal. g_mkwNiFlushThreshold (2^-126 active, 0.0 inactive) turns the
|
||||
// flush into a branchless mask: a set compare lane keeps just the sign bit, a clear lane
|
||||
// passes the value to CVTSD2SS. DAZ (set exactly when NI is) makes the compare itself treat
|
||||
// a subnormal as zero, matching the mask's answer.
|
||||
// passes the value to the double->float conversion. DAZ/FZ (set exactly when NI is) makes
|
||||
// the compare itself treat a subnormal as zero, matching the mask's answer.
|
||||
#if defined(__x86_64__)
|
||||
const __m128d v = _mm_set_sd(value);
|
||||
const __m128d signMask = _mm_set_sd(-0.0);
|
||||
const __m128d magnitude = _mm_andnot_pd(signMask, v);
|
||||
const __m128d flush = _mm_cmplt_sd(magnitude, _mm_set_sd(g_mkwNiFlushThreshold));
|
||||
const __m128d kept = _mm_andnot_pd(_mm_andnot_pd(signMask, flush), v);
|
||||
return static_cast<float>(_mm_cvtsd_f64(kept));
|
||||
#elif defined(__aarch64__)
|
||||
const float64x1_t v = vdup_n_f64(value);
|
||||
const uint64x1_t signMask = vdup_n_u64(0x8000000000000000ULL);
|
||||
const float64x1_t magnitude = vreinterpret_f64_u64(vbic_u64(vreinterpret_u64_f64(v), signMask));
|
||||
const uint64x1_t flush = vclt_f64(magnitude, vdup_n_f64(g_mkwNiFlushThreshold));
|
||||
const uint64x1_t signOnly = vand_u64(vreinterpret_u64_f64(v), signMask);
|
||||
const uint64x1_t kept = vbsl_u64(flush, signOnly, vreinterpret_u64_f64(v));
|
||||
return static_cast<float>(vget_lane_f64(vreinterpret_f64_u64(kept), 0));
|
||||
#endif
|
||||
}
|
||||
|
||||
inline float PpcFlushSingleForNiInline(float value)
|
||||
@@ -468,15 +526,72 @@ inline double PpcFnmsubsInline(double a, double c, double b)
|
||||
return static_cast<double>(std::isnan(result) ? result : -result);
|
||||
}
|
||||
|
||||
#if defined(__aarch64__)
|
||||
// x86 resolves a single NaN source operand (SNaN or QNaN alike) to that operand quieted with
|
||||
// sign and payload kept, and an invalid op with no NaN input to 0xFFC00000; NEON prefers SNaNs,
|
||||
// checks the accumulator first, and yields the positive default NaN 0x7FC00000. NaN result
|
||||
// lanes (rare: one branch on the pair) are therefore rewritten to the x86 answer, keeping the
|
||||
// two hosts bit-identical for every case where x86 itself is deterministic. With two or more
|
||||
// NaN operands even x86 is not: the winning operand depends on which FMA form / commuted
|
||||
// operand order clang picked per call site, so this resolver's fixed first-operand priority is
|
||||
// one of the answers a real x86 build can give, not a guaranteed match.
|
||||
inline uint64_t PpcPairNanLaneBitsInline(PpcPairVec value)
|
||||
{
|
||||
return vget_lane_u64(vreinterpret_u64_u32(vmvn_u32(vceq_f32(value, value))), 0);
|
||||
}
|
||||
|
||||
inline PpcPairVec PpcQuietPairInline(PpcPairVec value)
|
||||
{
|
||||
return vreinterpret_f32_u32(vorr_u32(vreinterpret_u32_f32(value), vdup_n_u32(0x00400000u)));
|
||||
}
|
||||
|
||||
inline PpcPairVec PpcResolveNanLanesInline(PpcPairVec result, PpcPairVec op1, PpcPairVec op2)
|
||||
{
|
||||
const uint32x2_t op1Nan = vmvn_u32(vceq_f32(op1, op1));
|
||||
const uint32x2_t op2Nan = vmvn_u32(vceq_f32(op2, op2));
|
||||
PpcPairVec replacement = vreinterpret_f32_u32(vdup_n_u32(0xFFC00000u));
|
||||
replacement = vbsl_f32(op2Nan, PpcQuietPairInline(op2), replacement);
|
||||
replacement = vbsl_f32(op1Nan, PpcQuietPairInline(op1), replacement);
|
||||
const uint32x2_t resultNan = vmvn_u32(vceq_f32(result, result));
|
||||
return vbsl_f32(resultNan, replacement, result);
|
||||
}
|
||||
|
||||
inline PpcPairVec PpcResolveNanLanes3Inline(
|
||||
PpcPairVec result, PpcPairVec op1, PpcPairVec op2, PpcPairVec op3)
|
||||
{
|
||||
const uint32x2_t op3Nan = vmvn_u32(vceq_f32(op3, op3));
|
||||
PpcPairVec replacement = vreinterpret_f32_u32(vdup_n_u32(0xFFC00000u));
|
||||
replacement = vbsl_f32(op3Nan, PpcQuietPairInline(op3), replacement);
|
||||
const uint32x2_t op2Nan = vmvn_u32(vceq_f32(op2, op2));
|
||||
replacement = vbsl_f32(op2Nan, PpcQuietPairInline(op2), replacement);
|
||||
const uint32x2_t op1Nan = vmvn_u32(vceq_f32(op1, op1));
|
||||
replacement = vbsl_f32(op1Nan, PpcQuietPairInline(op1), replacement);
|
||||
const uint32x2_t resultNan = vmvn_u32(vceq_f32(result, result));
|
||||
return vbsl_f32(resultNan, replacement, result);
|
||||
}
|
||||
#endif // defined(__aarch64__)
|
||||
|
||||
inline PpcPairVec PpcMulPairInline(PpcPairVec lhs, PpcPairVec rhs)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
return _mm_mul_ps(lhs, rhs);
|
||||
#elif defined(__aarch64__)
|
||||
const PpcPairVec result = vmul_f32(lhs, rhs);
|
||||
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
|
||||
return PpcResolveNanLanesInline(result, lhs, rhs);
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline double PPC_PsMulInline(double lhs, double rhs)
|
||||
{
|
||||
return PpcFlushPairedForNiInline(
|
||||
PpcM128ToPsInline(_mm_mul_ps(PpcPsToM128Inline(lhs), PpcPsToM128Inline(rhs))));
|
||||
PpcM128ToPsInline(PpcMulPairInline(PpcPsToM128Inline(lhs), PpcPsToM128Inline(rhs))));
|
||||
}
|
||||
|
||||
inline double PPC_PsMulNoNiInline(double lhs, double rhs)
|
||||
{
|
||||
return PpcM128ToPsInline(_mm_mul_ps(PpcPsToM128Inline(lhs), PpcPsToM128Inline(rhs)));
|
||||
return PpcM128ToPsInline(PpcMulPairInline(PpcPsToM128Inline(lhs), PpcPsToM128Inline(rhs)));
|
||||
}
|
||||
|
||||
// The paired madd family lowers to one hardware FMA. Semantics match the scalar lanes exactly: a
|
||||
@@ -485,9 +600,37 @@ inline double PPC_PsMulNoNiInline(double lhs, double rhs)
|
||||
// PpcNegateNonNanLanesInline. NI flushing is handled by MXCSR (see
|
||||
// MkwApplyHostNiMode), so the NI and NoNi entry points are identical here.
|
||||
|
||||
// Fused multiply-add/subtract on a pair. NEON's vfma_f32(acc, a, b) = acc + a*b has an
|
||||
// accumulator-first operand order, unlike x86's _mm_fmadd_ps(a, b, c) = a*b + c - msub is
|
||||
// therefore an fma against a negated accumulator. That vneg would flip a sole-NaN subtractor's
|
||||
// sign, which x86's vfmsub does not do, so the NaN resolver receives the original subtractor.
|
||||
inline PpcPairVec PpcFmaddPairInline(PpcPairVec multiplicand, PpcPairVec multiplier, PpcPairVec addend)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
return _mm_fmadd_ps(multiplicand, multiplier, addend);
|
||||
#elif defined(__aarch64__)
|
||||
const PpcPairVec result = vfma_f32(addend, multiplicand, multiplier);
|
||||
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
|
||||
return PpcResolveNanLanes3Inline(result, multiplicand, multiplier, addend);
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline PpcPairVec PpcFmsubPairInline(PpcPairVec multiplicand, PpcPairVec multiplier, PpcPairVec subtractor)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
return _mm_fmsub_ps(multiplicand, multiplier, subtractor);
|
||||
#elif defined(__aarch64__)
|
||||
const PpcPairVec result = vfma_f32(vneg_f32(subtractor), multiplicand, multiplier);
|
||||
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
|
||||
return PpcResolveNanLanes3Inline(result, multiplicand, multiplier, subtractor);
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline double PPC_PsMsubInline(double multiplicand, double multiplier, double subtractor)
|
||||
{
|
||||
return PpcM128ToPsInline(_mm_fmsub_ps(
|
||||
return PpcM128ToPsInline(PpcFmsubPairInline(
|
||||
PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(subtractor)));
|
||||
}
|
||||
|
||||
@@ -498,7 +641,7 @@ inline double PPC_PsMsubNoNiInline(double multiplicand, double multiplier, doubl
|
||||
|
||||
inline double PPC_PsMaddInline(double multiplicand, double multiplier, double addend)
|
||||
{
|
||||
return PpcM128ToPsInline(_mm_fmadd_ps(
|
||||
return PpcM128ToPsInline(PpcFmaddPairInline(
|
||||
PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(addend)));
|
||||
}
|
||||
|
||||
@@ -509,19 +652,19 @@ inline double PPC_PsMaddNoNiInline(double multiplicand, double multiplier, doubl
|
||||
|
||||
inline double PPC_PsMadds0Inline(double multiplicand, double multiplier, double addend)
|
||||
{
|
||||
return PpcM128ToPsInline(_mm_fmadd_ps(
|
||||
return PpcM128ToPsInline(PpcFmaddPairInline(
|
||||
PpcPsToM128Inline(multiplicand), PpcBroadcastPs0Inline(multiplier), PpcPsToM128Inline(addend)));
|
||||
}
|
||||
|
||||
inline double PPC_PsMadds1Inline(double multiplicand, double multiplier, double addend)
|
||||
{
|
||||
return PpcM128ToPsInline(_mm_fmadd_ps(
|
||||
return PpcM128ToPsInline(PpcFmaddPairInline(
|
||||
PpcPsToM128Inline(multiplicand), PpcBroadcastPs1Inline(multiplier), PpcPsToM128Inline(addend)));
|
||||
}
|
||||
|
||||
inline double PPC_PsNmsubInline(double multiplicand, double multiplier, double subtractor)
|
||||
{
|
||||
return PpcM128ToPsInline(PpcNegateNonNanLanesInline(_mm_fmsub_ps(
|
||||
return PpcM128ToPsInline(PpcNegateNonNanLanesInline(PpcFmsubPairInline(
|
||||
PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(subtractor))));
|
||||
}
|
||||
|
||||
@@ -532,20 +675,20 @@ inline double PPC_PsNmsubNoNiInline(double multiplicand, double multiplier, doub
|
||||
|
||||
inline double PPC_PsNmaddInline(double multiplicand, double multiplier, double addend)
|
||||
{
|
||||
return PpcM128ToPsInline(PpcNegateNonNanLanesInline(_mm_fmadd_ps(
|
||||
return PpcM128ToPsInline(PpcNegateNonNanLanesInline(PpcFmaddPairInline(
|
||||
PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(addend))));
|
||||
}
|
||||
|
||||
inline double PPC_PsMuls0Inline(double aValue, double cValue)
|
||||
{
|
||||
return PpcFlushPairedForNiInline(PpcM128ToPsInline(
|
||||
_mm_mul_ps(PpcPsToM128Inline(aValue), PpcBroadcastPs0Inline(cValue))));
|
||||
PpcMulPairInline(PpcPsToM128Inline(aValue), PpcBroadcastPs0Inline(cValue))));
|
||||
}
|
||||
|
||||
inline double PPC_PsMuls1Inline(double aValue, double cValue)
|
||||
{
|
||||
return PpcFlushPairedForNiInline(PpcM128ToPsInline(
|
||||
_mm_mul_ps(PpcPsToM128Inline(aValue), PpcBroadcastPs1Inline(cValue))));
|
||||
PpcMulPairInline(PpcPsToM128Inline(aValue), PpcBroadcastPs1Inline(cValue))));
|
||||
}
|
||||
|
||||
inline PPC_FPR PpcMakePairedResultInline(float ps0, float ps1);
|
||||
@@ -571,50 +714,105 @@ inline double PPC_PsToScalarInline(double value)
|
||||
return static_cast<double>(PpcGetPs0Inline(value));
|
||||
}
|
||||
|
||||
// ps_merge* are pure lane selections (result.ps0 from frA, result.ps1 from frB); with lane 0
|
||||
// == ps1 and lane 1 == ps0, two shuffles build the result bit-exact instead of round-tripping
|
||||
// through the pack helper.
|
||||
// ps_merge* are pure lane selections (result.ps0 from frA, result.ps1 from frB). On x86 two
|
||||
// shuffles build the result bit-exact without round-tripping through the pack helper; NEON has
|
||||
// no equally cheap 2-lane general shuffle, so its branch expresses the exact same selection
|
||||
// (verified against the x86 comments below, lane for lane) directly in terms of the portable
|
||||
// Get/Pack accessors instead.
|
||||
inline double PPC_PsMerge00Inline(double aValue, double bValue)
|
||||
{
|
||||
// lane0 = b.ps0 (b lane 1), lane1 = a.ps0 (a lane 1)
|
||||
// result.ps0 = a.ps0, result.ps1 = b.ps0 (lane0 = b.ps0/b lane1, lane1 = a.ps0/a lane1)
|
||||
#if defined(__x86_64__)
|
||||
const __m128 gathered = _mm_shuffle_ps(
|
||||
PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(1, 1, 1, 1));
|
||||
return PpcM128ToPsInline(_mm_shuffle_ps(gathered, gathered, _MM_SHUFFLE(0, 0, 2, 0)));
|
||||
#elif defined(__aarch64__)
|
||||
return PpcPackPairedInline(PpcGetPs0Inline(aValue), PpcGetPs0Inline(bValue));
|
||||
#endif
|
||||
}
|
||||
|
||||
inline double PPC_PsMerge01Inline(double aValue, double bValue)
|
||||
{
|
||||
// lane0 = b.ps1 (b lane 0), lane1 = a.ps0 (a lane 1)
|
||||
// result.ps0 = a.ps0, result.ps1 = b.ps1 (lane0 = b.ps1/b lane0, lane1 = a.ps0/a lane1)
|
||||
#if defined(__x86_64__)
|
||||
const __m128 gathered = _mm_shuffle_ps(
|
||||
PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(1, 1, 0, 0));
|
||||
return PpcM128ToPsInline(_mm_shuffle_ps(gathered, gathered, _MM_SHUFFLE(0, 0, 2, 0)));
|
||||
#elif defined(__aarch64__)
|
||||
return PpcPackPairedInline(PpcGetPs0Inline(aValue), PpcGetPs1Inline(bValue));
|
||||
#endif
|
||||
}
|
||||
|
||||
inline double PPC_PsMerge10Inline(double aValue, double bValue)
|
||||
{
|
||||
// lane0 = b.ps0 (b lane 1), lane1 = a.ps1 (a lane 0)
|
||||
// result.ps0 = a.ps1, result.ps1 = b.ps0 (lane0 = b.ps0/b lane1, lane1 = a.ps1/a lane0)
|
||||
#if defined(__x86_64__)
|
||||
const __m128 gathered = _mm_shuffle_ps(
|
||||
PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(0, 0, 1, 1));
|
||||
return PpcM128ToPsInline(_mm_shuffle_ps(gathered, gathered, _MM_SHUFFLE(0, 0, 2, 0)));
|
||||
#elif defined(__aarch64__)
|
||||
return PpcPackPairedInline(PpcGetPs1Inline(aValue), PpcGetPs0Inline(bValue));
|
||||
#endif
|
||||
}
|
||||
|
||||
inline double PPC_PsMerge11Inline(double aValue, double bValue)
|
||||
{
|
||||
// lane0 = b.ps1 (b lane 0), lane1 = a.ps1 (a lane 0): plain unpcklps.
|
||||
// result.ps0 = a.ps1, result.ps1 = b.ps1 (lane0 = b.ps1/b lane0, lane1 = a.ps1/a lane0):
|
||||
// plain unpcklps on x86.
|
||||
#if defined(__x86_64__)
|
||||
return PpcM128ToPsInline(
|
||||
_mm_unpacklo_ps(PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue)));
|
||||
#elif defined(__aarch64__)
|
||||
return PpcPackPairedInline(PpcGetPs1Inline(aValue), PpcGetPs1Inline(bValue));
|
||||
#endif
|
||||
}
|
||||
|
||||
inline PpcPairVec PpcAddPairInline(PpcPairVec lhs, PpcPairVec rhs)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
return _mm_add_ps(lhs, rhs);
|
||||
#elif defined(__aarch64__)
|
||||
const PpcPairVec result = vadd_f32(lhs, rhs);
|
||||
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
|
||||
return PpcResolveNanLanesInline(result, lhs, rhs);
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline PpcPairVec PpcSubPairInline(PpcPairVec lhs, PpcPairVec rhs)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
return _mm_sub_ps(lhs, rhs);
|
||||
#elif defined(__aarch64__)
|
||||
const PpcPairVec result = vsub_f32(lhs, rhs);
|
||||
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
|
||||
return PpcResolveNanLanesInline(result, lhs, rhs);
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline PpcPairVec PpcDivPairInline(PpcPairVec lhs, PpcPairVec rhs)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
return _mm_div_ps(lhs, rhs);
|
||||
#elif defined(__aarch64__)
|
||||
const PpcPairVec result = vdiv_f32(lhs, rhs);
|
||||
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
|
||||
return PpcResolveNanLanesInline(result, lhs, rhs);
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline double PPC_PsAddInline(double aValue, double bValue)
|
||||
{
|
||||
return PpcFlushPairedForNiInline(
|
||||
PpcM128ToPsInline(_mm_add_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
|
||||
PpcM128ToPsInline(PpcAddPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
|
||||
}
|
||||
|
||||
inline double PPC_PsAddNoNiInline(double aValue, double bValue)
|
||||
{
|
||||
return PpcM128ToPsInline(
|
||||
_mm_add_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)));
|
||||
PpcAddPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)));
|
||||
}
|
||||
|
||||
inline double PPC_PsSelInline(double lhsValue, double controlValue, double rhsValue)
|
||||
@@ -633,19 +831,19 @@ inline double PPC_PsSelInline(double lhsValue, double controlValue, double rhsVa
|
||||
inline double PPC_PsSubInline(double aValue, double bValue)
|
||||
{
|
||||
return PpcFlushPairedForNiInline(
|
||||
PpcM128ToPsInline(_mm_sub_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
|
||||
PpcM128ToPsInline(PpcSubPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
|
||||
}
|
||||
|
||||
inline double PPC_PsSubNoNiInline(double aValue, double bValue)
|
||||
{
|
||||
return PpcM128ToPsInline(
|
||||
_mm_sub_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)));
|
||||
PpcSubPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)));
|
||||
}
|
||||
|
||||
inline double PPC_PsDivInline(double aValue, double bValue)
|
||||
{
|
||||
return PpcFlushPairedForNiInline(
|
||||
PpcM128ToPsInline(_mm_div_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
|
||||
PpcM128ToPsInline(PpcDivPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
|
||||
}
|
||||
|
||||
inline double PPC_PsNegInline(double value)
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
#pragma once
|
||||
// FPSCR[NI] (non-IEEE flush-to-zero) modeled on the host FP environment, plus
|
||||
// the thread-local mirror of that state the hot paths read instead of MXCSR.
|
||||
// the thread-local mirror of that state the hot paths read instead of MXCSR/FPCR.
|
||||
|
||||
#include "ppc_isa_config.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Software-flushing Gekko's single-precision denormals per op roughly doubled the THP IDCT
|
||||
// kernel's cycle count, so instead the runtime mirrors guest FPSCR[NI] into host MXCSR FTZ+DAZ
|
||||
// wherever FPSCR can change (PPC_Mtfs*, fiber context switches, CpuContextScope), making per-op
|
||||
// flushes free. Accepted deviations (same trade Dolphin makes): FTZ also flushes double
|
||||
// denormals unlike real NI, and a pre-round-flush edge near FLT_MIN rounds via cvtsd2ss instead.
|
||||
inline constexpr uint32_t kMkwMxcsrFlushToZeroBits = (1u << 15) | (1u << 6); // FTZ | DAZ
|
||||
|
||||
// kernel's cycle count, so instead the runtime mirrors guest FPSCR[NI] into the host FP control
|
||||
// register's flush-to-zero bit(s) wherever FPSCR can change (PPC_Mtfs*, fiber context switches,
|
||||
// CpuContextScope), making per-op flushes free. Accepted deviations (same trade Dolphin makes):
|
||||
// flush-to-zero also flushes double denormals unlike real NI, and a pre-round-flush edge near
|
||||
// FLT_MIN rounds via cvtsd2ss (or its AArch64 equivalent) instead.
|
||||
#if defined(__x86_64__)
|
||||
// MXCSR: FTZ (bit 15, flushes arithmetic *output* denormals) and DAZ (bit 6, treats denormal
|
||||
// *inputs* as zero) are two separate bits on x86; both are set/cleared together here.
|
||||
inline constexpr uint32_t kMkwFpControlFlushToZeroBits = (1u << 15) | (1u << 6); // FTZ | DAZ
|
||||
#elif defined(__aarch64__)
|
||||
// FPCR: a single FZ bit (bit 24) flushes both single- and double-precision denormals, both as
|
||||
// inputs and outputs - actually a cleaner match to the "flush everything" trade above than x86's
|
||||
// two-bit combo, not an extra deviation.
|
||||
inline constexpr uint32_t kMkwFpControlFlushToZeroBits = (1u << 24); // FZ
|
||||
#else
|
||||
#error "ppc_isa_fpenv.h has no host FP control register mapping for this architecture"
|
||||
#endif
|
||||
|
||||
inline thread_local bool g_mkwHostNiActive = false;
|
||||
|
||||
@@ -22,32 +33,60 @@ inline thread_local bool g_mkwHostNiActive = false;
|
||||
inline constexpr double kMkwNiFlushThreshold = 0x1p-126; // 0x3810000000000000
|
||||
inline thread_local double g_mkwNiFlushThreshold = 0.0;
|
||||
|
||||
// Host FP control register access (MXCSR on x86_64, FPCR on AArch64), abstracted so
|
||||
// MkwApplyHostNiMode/MkwRestoreHostMxcsr and CpuContextScope (ppc_isa_context.h) don't each need
|
||||
// their own per-arch branch.
|
||||
inline uint32_t MkwGetHostFpControl() noexcept
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
return _mm_getcsr();
|
||||
#elif defined(__aarch64__)
|
||||
uint64_t fpcr = 0;
|
||||
__asm__ __volatile__("mrs %0, fpcr" : "=r"(fpcr));
|
||||
return static_cast<uint32_t>(fpcr);
|
||||
#endif
|
||||
}
|
||||
|
||||
inline void MkwSetHostFpControl(uint32_t value) noexcept
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
_mm_setcsr(value);
|
||||
#elif defined(__aarch64__)
|
||||
// Read-modify-write the full 64-bit FPCR: only the low 32 bits are architecturally defined
|
||||
// today, but a bare 32-bit write would zero the (reserved) upper half of the real register.
|
||||
uint64_t fpcr = 0;
|
||||
__asm__ __volatile__("mrs %0, fpcr" : "=r"(fpcr));
|
||||
fpcr = (fpcr & ~static_cast<uint64_t>(0xFFFFFFFFu)) | value;
|
||||
__asm__ __volatile__("msr fpcr, %0" :: "r"(fpcr));
|
||||
#endif
|
||||
}
|
||||
|
||||
inline void MkwApplyHostNiMode(uint32_t fpscr) noexcept
|
||||
{
|
||||
const uint32_t csr = _mm_getcsr();
|
||||
const uint32_t csr = MkwGetHostFpControl();
|
||||
const bool wantNi = (fpscr & 0x4u) != 0;
|
||||
const uint32_t want = wantNi
|
||||
? (csr | kMkwMxcsrFlushToZeroBits)
|
||||
: (csr & ~kMkwMxcsrFlushToZeroBits);
|
||||
? (csr | kMkwFpControlFlushToZeroBits)
|
||||
: (csr & ~kMkwFpControlFlushToZeroBits);
|
||||
if (want != csr)
|
||||
_mm_setcsr(want);
|
||||
// `want` has both bits set or both clear, so this is exactly
|
||||
// `(_mm_getcsr() & kMkwMxcsrFlushToZeroBits) != 0` after the write - the
|
||||
// mirror cannot disagree with the register even if the incoming CSR held
|
||||
// only one of the two bits.
|
||||
MkwSetHostFpControl(want);
|
||||
// `want` has every flush-to-zero bit set or every one clear, so this is exactly
|
||||
// `(MkwGetHostFpControl() & kMkwFpControlFlushToZeroBits) != 0` after the write - the
|
||||
// mirror cannot disagree with the register even if the incoming value held only some of
|
||||
// those bits.
|
||||
g_mkwHostNiActive = wantNi;
|
||||
g_mkwNiFlushThreshold = wantNi ? kMkwNiFlushThreshold : 0.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores a previously captured MXCSR value and re-derives the mirror from
|
||||
/// it. Every raw restore has to go through here; a bare _mm_setcsr would leave
|
||||
/// the mirror describing the FP environment that was just replaced.
|
||||
/// Restores a previously captured MXCSR/FPCR value and re-derives the mirror from it. Every raw
|
||||
/// restore has to go through here; a bare MkwSetHostFpControl would leave the mirror describing
|
||||
/// the FP environment that was just replaced.
|
||||
/// </summary>
|
||||
inline void MkwRestoreHostMxcsr(uint32_t csr) noexcept
|
||||
{
|
||||
_mm_setcsr(csr);
|
||||
const bool niActive = (csr & kMkwMxcsrFlushToZeroBits) != 0;
|
||||
MkwSetHostFpControl(csr);
|
||||
const bool niActive = (csr & kMkwFpControlFlushToZeroBits) != 0;
|
||||
g_mkwHostNiActive = niActive;
|
||||
g_mkwNiFlushThreshold = niActive ? kMkwNiFlushThreshold : 0.0;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
@@ -38,16 +39,26 @@ inline uint32_t PpcStorePsqFloatBitsInline(uint32_t value)
|
||||
|
||||
inline uint64_t PpcLoadPairPsqFloatBitsPackedInline(uint64_t value)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
const __m128i lanes = _mm_cvtsi64_si128(static_cast<long long>(value));
|
||||
const __m128i magnitude = _mm_and_si128(lanes, _mm_set1_epi32(0x7FFFFFFF));
|
||||
const __m128i nanMask = _mm_cmpgt_epi32(magnitude, _mm_set1_epi32(0x7F800000));
|
||||
const __m128i result = _mm_or_si128(
|
||||
lanes, _mm_and_si128(nanMask, _mm_set1_epi32(0x00400000)));
|
||||
return static_cast<uint64_t>(_mm_cvtsi128_si64(result));
|
||||
#elif defined(__aarch64__)
|
||||
// Equivalent to applying PpcLoadPsqFloatBitsInline to each 32-bit lane: the
|
||||
// x86 body above only ever acts on these same two lanes (the upper 64 bits
|
||||
// _mm_cvtsi64_si128 zero-fills never survive the final truncating extract).
|
||||
const uint32_t lo = PpcLoadPsqFloatBitsInline(static_cast<uint32_t>(value));
|
||||
const uint32_t hi = PpcLoadPsqFloatBitsInline(static_cast<uint32_t>(value >> 32));
|
||||
return (static_cast<uint64_t>(hi) << 32) | lo;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline uint64_t PpcStorePairPsqFloatBitsPackedInline(uint64_t value)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
const __m128i lanes = _mm_cvtsi64_si128(static_cast<long long>(value));
|
||||
const __m128i magnitude = _mm_and_si128(lanes, _mm_set1_epi32(0x7FFFFFFF));
|
||||
const __m128i subnormalMask = _mm_cmplt_epi32(magnitude, _mm_set1_epi32(0x00800000));
|
||||
@@ -60,8 +71,16 @@ inline uint64_t PpcStorePairPsqFloatBitsPackedInline(uint64_t value)
|
||||
_mm_and_si128(subnormalMask, signedZero),
|
||||
_mm_andnot_si128(subnormalMask, quieted));
|
||||
return static_cast<uint64_t>(_mm_cvtsi128_si64(result));
|
||||
#elif defined(__aarch64__)
|
||||
// Equivalent to applying PpcStorePsqFloatBitsInline to each 32-bit lane;
|
||||
// same reasoning as the load-side port above.
|
||||
const uint32_t lo = PpcStorePsqFloatBitsInline(static_cast<uint32_t>(value));
|
||||
const uint32_t hi = PpcStorePsqFloatBitsInline(static_cast<uint32_t>(value >> 32));
|
||||
return (static_cast<uint64_t>(hi) << 32) | lo;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(__x86_64__)
|
||||
inline __m128i PpcPsqSwapPairBytesInline(__m128i lanes)
|
||||
{
|
||||
const __m128i order = _mm_setr_epi8(
|
||||
@@ -95,21 +114,42 @@ inline __m128i PpcStorePairPsqFloatBitsLanesInline(__m128i lanes)
|
||||
_mm_and_si128(subnormalMask, signedZero),
|
||||
_mm_andnot_si128(subnormalMask, quieted));
|
||||
}
|
||||
#endif // defined(__x86_64__)
|
||||
|
||||
// host -> packed FPR double, guard already proven by the caller.
|
||||
inline double PpcLoadPairPsqFloatFromHostInline(const uint8_t* host)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
const __m128i raw = _mm_loadl_epi64(reinterpret_cast<const __m128i*>(host));
|
||||
return PpcM128ToPsInline(_mm_castsi128_ps(
|
||||
PpcLoadPairPsqFloatBitsLanesInline(PpcPsqSwapPairBytesInline(raw))));
|
||||
#elif defined(__aarch64__)
|
||||
// The x86 path's full 8-byte pshufb reversal plus a same-endian load is,
|
||||
// taken together, exactly a 64-bit byteswap of a plain little-endian load:
|
||||
// it turns the on-disk [ps0 big-endian][ps1 big-endian] byte layout into a
|
||||
// native uint64 with low 32 bits = ps1, high 32 bits = ps0 (this file's
|
||||
// documented packed-double lane convention).
|
||||
uint64_t raw = 0;
|
||||
std::memcpy(&raw, host, sizeof(raw));
|
||||
const uint64_t swapped = __builtin_bswap64(raw);
|
||||
return PpcBitCastToDoubleInline(PpcLoadPairPsqFloatBitsPackedInline(swapped));
|
||||
#endif
|
||||
}
|
||||
|
||||
// packed FPR double -> host, guard already proven by the caller.
|
||||
inline void PpcStorePairPsqFloatToHostInline(uint8_t* host, double value)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
const __m128i lanes = PpcStorePairPsqFloatBitsLanesInline(
|
||||
_mm_castps_si128(PpcPsToM128Inline(value)));
|
||||
_mm_storel_epi64(reinterpret_cast<__m128i*>(host), PpcPsqSwapPairBytesInline(lanes));
|
||||
#elif defined(__aarch64__)
|
||||
// Inverse of the load path above: bswap64 is its own inverse, so applying
|
||||
// it to the quieted packed value reproduces the on-disk big-endian bytes.
|
||||
const uint64_t quieted = PpcStorePairPsqFloatBitsPackedInline(PpcBitCastToU64Inline(value));
|
||||
const uint64_t swapped = __builtin_bswap64(quieted);
|
||||
std::memcpy(host, &swapped, sizeof(swapped));
|
||||
#endif
|
||||
}
|
||||
|
||||
template <typename SignedType>
|
||||
@@ -224,6 +264,8 @@ inline void PpcWritePairPsqInline(uint32_t addr, T first, T second)
|
||||
// reading stale bytes, and unmapped pages commit on demand, same as MemoryInline::Flat* loads.
|
||||
MKW_PPC_FORCE_INLINE const uint8_t* PpcTryGetPsqReadableHostInline(uint32_t addr)
|
||||
{
|
||||
if (GuestFlat::RequiresCheckedAccess()) [[unlikely]]
|
||||
return nullptr;
|
||||
return MKW_FLAT_GUEST_BASE + addr;
|
||||
}
|
||||
|
||||
@@ -234,6 +276,8 @@ MKW_PPC_FORCE_INLINE const uint8_t* PpcTryGetPsqReadableHostInline(uint32_t addr
|
||||
// executable, and unmapped pages still trap.
|
||||
MKW_PPC_FORCE_INLINE uint8_t* PpcTryGetPsqWritableHostInline(uint32_t addr)
|
||||
{
|
||||
if (GuestFlat::RequiresCheckedAccess()) [[unlikely]]
|
||||
return nullptr;
|
||||
if (addr > UINT32_MAX - 7u) [[unlikely]]
|
||||
return nullptr;
|
||||
if (MemoryInline::FlatWriteNeedsPolicy(addr) ||
|
||||
@@ -329,6 +373,7 @@ inline uint8_t PpcQuantizePsqU8Scale61Inline(float value)
|
||||
// matching PpcQuantizePsqU8Scale61Inline's !(scaled > 0) rule.
|
||||
inline uint16_t PpcQuantizePairPsqU8Scale61PackedInline(double value)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
const __m128 scaled = _mm_mul_ps(PpcPsToM128Inline(value), _mm_set1_ps(0.125f));
|
||||
const __m128 nonNegative = _mm_max_ps(scaled, _mm_setzero_ps());
|
||||
const __m128 clamped = _mm_min_ps(nonNegative, _mm_set1_ps(255.0f));
|
||||
@@ -338,6 +383,17 @@ inline uint16_t PpcQuantizePairPsqU8Scale61PackedInline(double value)
|
||||
// Native lane 0 is ps1 and lane 1 is ps0. Packing to the low uint16_t
|
||||
// therefore produces the guest-order numeric value (ps0 << 8) | ps1.
|
||||
return static_cast<uint16_t>(_mm_cvtsi128_si32(lanes8));
|
||||
#elif defined(__aarch64__)
|
||||
// Equivalent to two calls of the already-portable scalar quantizer above
|
||||
// (its own !(scaled > 0) rule maps NaN to 0, matching what MAXPS-with-zero
|
||||
// does on the x86 path per the comment there), packed the same way the
|
||||
// fallback path just below already does for the non-SIMD case.
|
||||
PPC_FPR fpr{};
|
||||
fpr.d = value;
|
||||
const uint8_t q0 = PpcQuantizePsqU8Scale61Inline(fpr.paired.ps0);
|
||||
const uint8_t q1 = PpcQuantizePsqU8Scale61Inline(fpr.paired.ps1);
|
||||
return static_cast<uint16_t>((static_cast<uint16_t>(q0) << 8) | static_cast<uint16_t>(q1));
|
||||
#endif
|
||||
}
|
||||
|
||||
// Preserve the complete memory/MMIO/executable-write behavior off the leaf
|
||||
|
||||
@@ -231,6 +231,13 @@ MKW_MEMORY_FORCE_INLINE uint8_t* ResolveRangeHost(uint32_t base, int32_t minOffs
|
||||
(void)needsRead;
|
||||
const uint32_t guestStart = base + static_cast<uint32_t>(minOffset);
|
||||
if (length == 0 || length > kPageSize || guestStart > UINT32_MAX - (length - 1)) return nullptr;
|
||||
if (GuestFlat::RequiresCheckedAccess()) {
|
||||
// A host page can cover multiple independently-special Wii pages.
|
||||
// Returning null keeps resolved accesses on the checked Memory::*
|
||||
// path, which materializes deferred reads and applies write policy.
|
||||
(void)needsWrite;
|
||||
return nullptr;
|
||||
}
|
||||
if (needsWrite &&
|
||||
(FlatWriteNeedsPolicy(guestStart) || FlatWriteNeedsPolicy(guestStart + (length - 1))))
|
||||
[[unlikely]] return nullptr;
|
||||
@@ -522,6 +529,13 @@ MKW_MEMORY_FORCE_INLINE void WriteResolvedFloat64(uint8_t* r, uint32_t o, uint32
|
||||
// around `*(T*)(base + addr)`, no page-table load or limit check (interception model documented
|
||||
// in guest_flat_memory.h). The one exception kept inline is the MMIO write policy, since the
|
||||
// written value can't be recovered from a fault record.
|
||||
//
|
||||
// When a host VM page is larger than a 4 KiB Wii page, guest-view protections
|
||||
// cannot distinguish adjacent special Wii pages. The general FlatRead*/
|
||||
// FlatWrite* helpers then use the checked page-table path, which materializes
|
||||
// deferred reads and applies executable-write/MMIO policy before touching RAM.
|
||||
// FlatWriteRam* remains direct because the translator emits it only for
|
||||
// addresses it has proven are ordinary RAM.
|
||||
|
||||
template <typename T>
|
||||
MKW_MEMORY_FORCE_INLINE T FlatLoad(uint32_t address) {
|
||||
@@ -536,47 +550,62 @@ MKW_MEMORY_FORCE_INLINE void FlatStore(uint32_t address, T value) {
|
||||
std::memcpy(MKW_FLAT_GUEST_BASE + address, &swapped, sizeof(T));
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) { return FlatLoad<uint8_t>(address); }
|
||||
MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) { return FlatLoad<uint16_t>(address); }
|
||||
MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) { return FlatLoad<uint32_t>(address); }
|
||||
MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read8(address);
|
||||
return FlatLoad<uint8_t>(address);
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read16(address);
|
||||
return FlatLoad<uint16_t>(address);
|
||||
}
|
||||
MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read32(address);
|
||||
return FlatLoad<uint32_t>(address);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE float FlatReadFloat32(uint32_t address) {
|
||||
const uint32_t bits = FlatLoad<uint32_t>(address);
|
||||
const uint32_t bits = FlatRead32(address);
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE double FlatReadFloat64(uint32_t address) {
|
||||
const uint64_t bits = FlatLoad<uint64_t>(address);
|
||||
const uint64_t bits = GuestFlat::RequiresCheckedAccess()
|
||||
? Memory::Read64(address) : FlatLoad<uint64_t>(address);
|
||||
double value = 0.0;
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite8(uint32_t address, uint8_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write8(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write8Slow(address, value); return; }
|
||||
FlatStore<uint8_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite16(uint32_t address, uint16_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write16(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write16Slow(address, value); return; }
|
||||
FlatStore<uint16_t>(address, value);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWrite32(uint32_t address, uint32_t value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write32(address, value); return; }
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write32Slow(address, value); return; }
|
||||
FlatStore<uint32_t>(address, value);
|
||||
}
|
||||
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat32(uint32_t address, double value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat32(address, value); return; }
|
||||
const uint32_t bits = ConvertPpcDoubleToSingleBits(value);
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat32Slow(address, value); return; }
|
||||
FlatStore<uint32_t>(address, bits);
|
||||
}
|
||||
|
||||
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat64(uint32_t address, double value) {
|
||||
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat64(address, value); return; }
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat64Slow(address, value); return; }
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
|
||||
namespace MusicAttenuation {
|
||||
|
||||
// Enables the optional Windows media-session integration. The monitor is
|
||||
// started lazily the first time this is enabled.
|
||||
// Enables the optional external-media integration (Windows media sessions,
|
||||
// or MPRIS over D-Bus on Linux). The monitor is started lazily the first time
|
||||
// this is enabled.
|
||||
void SetEnabled(bool enabled) noexcept;
|
||||
void SetMusicVolume(float volume) noexcept;
|
||||
void SetSoundEffectsVolume(float volume) noexcept;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "runtime_config.h"
|
||||
#include "nand_settings.h"
|
||||
#include "runtime_log.h"
|
||||
#include "system_bridge.h"
|
||||
|
||||
@@ -163,7 +164,7 @@ inline std::filesystem::path CreateManagedNandRoot() {
|
||||
return root;
|
||||
}
|
||||
|
||||
inline std::filesystem::path DiscoverNandRootPath() {
|
||||
inline std::filesystem::path ResolveNandRootPath() {
|
||||
const std::string configPath = RuntimeConfigFile::NandRoot();
|
||||
if (!configPath.empty()) {
|
||||
const auto path = ResolveConfiguredPath(configPath);
|
||||
@@ -179,4 +180,16 @@ inline std::filesystem::path DiscoverNandRootPath() {
|
||||
return CreateManagedNandRoot();
|
||||
}
|
||||
|
||||
inline std::filesystem::path DiscoverNandRootPath() {
|
||||
static const auto root = [] {
|
||||
const auto resolved = ResolveNandRootPath();
|
||||
std::string error;
|
||||
if (!RuntimeNandSettings::Ensure(resolved, error)) {
|
||||
FailNandRoot(error.c_str(), RuntimeNandSettings::FilePath(resolved));
|
||||
}
|
||||
return resolved;
|
||||
}();
|
||||
return root;
|
||||
}
|
||||
|
||||
} // namespace RuntimeNandPath
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <istream>
|
||||
|
||||
namespace RuntimeNandSave {
|
||||
|
||||
enum class Contents { Missing, Blank, Nonzero, Error };
|
||||
enum class ReadAction { Proceed, Missing, Error, RecoveryNeeded };
|
||||
|
||||
// A failed read is not evidence that a save is blank. Check badbit before EOF:
|
||||
// an I/O failure may set both, whereas a successful short final read sets EOF.
|
||||
inline Contents InspectStream(std::istream& input) {
|
||||
if (!input) return Contents::Error;
|
||||
char block[4096];
|
||||
for (;;) {
|
||||
input.read(block, sizeof(block));
|
||||
if (input.bad() || (input.fail() && !input.eof())) return Contents::Error;
|
||||
for (std::streamsize i = 0; i < input.gcount(); ++i) {
|
||||
if (block[i] != 0) return Contents::Nonzero;
|
||||
}
|
||||
if (input.eof()) return Contents::Blank;
|
||||
}
|
||||
}
|
||||
|
||||
inline Contents InspectFile(const std::filesystem::path& path) {
|
||||
std::error_code ec;
|
||||
const auto status = std::filesystem::symlink_status(path, ec);
|
||||
if (ec && ec != std::errc::no_such_file_or_directory) return Contents::Error;
|
||||
if (!std::filesystem::exists(status)) return Contents::Missing;
|
||||
if (!std::filesystem::is_regular_file(path, ec) || ec) return Contents::Error;
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
return InspectStream(input);
|
||||
}
|
||||
|
||||
// Probe only read-only opens of the actual save and its exact write shadow.
|
||||
// No probe writes, removes, or repairs data, and backups are not save aliases.
|
||||
inline ReadAction CheckRead(const std::filesystem::path& path, int mode) {
|
||||
const auto name = path.filename();
|
||||
const bool isMain = name == "rksys.dat";
|
||||
if (mode != 1 || (!isMain && name != "rksys.dat.nandsafe.tmp")) return ReadAction::Proceed;
|
||||
const auto contents = InspectFile(path);
|
||||
if (contents == Contents::Error) return ReadAction::Error;
|
||||
if (contents == Contents::Nonzero) return ReadAction::Proceed;
|
||||
if (isMain) {
|
||||
auto shadow = path;
|
||||
shadow += ".nandsafe.tmp";
|
||||
const auto shadowContents = InspectFile(shadow);
|
||||
if (shadowContents == Contents::Error) return ReadAction::Error;
|
||||
// The next write normally discards an old shadow. Preserve a possible
|
||||
// recovery source when there is no usable original, without promoting
|
||||
// an uncommitted (and potentially incomplete) shadow to the real save.
|
||||
if (shadowContents == Contents::Nonzero) return ReadAction::RecoveryNeeded;
|
||||
}
|
||||
return contents == Contents::Blank ? ReadAction::Missing : ReadAction::Proceed;
|
||||
}
|
||||
|
||||
} // namespace RuntimeNandSave
|
||||
@@ -0,0 +1,199 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace RuntimeNandSettings {
|
||||
|
||||
using Settings = std::map<std::string, std::string>;
|
||||
|
||||
inline std::filesystem::path FilePath(const std::filesystem::path& root) {
|
||||
return root / "title/00000001/00000002/data/setting.txt";
|
||||
}
|
||||
|
||||
// Wii setting.txt is a 256-byte buffer encrypted with a rotating XOR key.
|
||||
inline std::optional<Settings> Read(const std::filesystem::path& nandRoot) {
|
||||
std::ifstream input(FilePath(nandRoot), std::ios::binary);
|
||||
std::array<uint8_t, 256> bytes{};
|
||||
if (!input.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
uint32_t key = 0x73B5DBFAu;
|
||||
std::string decoded;
|
||||
for (const uint8_t byte : bytes) {
|
||||
const char value = static_cast<char>(byte ^ static_cast<uint8_t>(key));
|
||||
key = (key << 1) | (key >> 31);
|
||||
if (value == '\0') {
|
||||
break;
|
||||
}
|
||||
if (value != '\r') {
|
||||
decoded += value;
|
||||
}
|
||||
}
|
||||
Settings settings;
|
||||
for (size_t start = 0; start < decoded.size();) {
|
||||
const size_t end = decoded.find('\n', start);
|
||||
const std::string line = decoded.substr(start, end - start);
|
||||
const size_t equals = line.find('=');
|
||||
if (equals != std::string::npos && equals != 0) {
|
||||
settings.emplace(line.substr(0, equals), line.substr(equals + 1));
|
||||
}
|
||||
if (end == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
start = end + 1;
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
inline bool HasIdentity(const Settings& settings) {
|
||||
const auto serial = settings.find("SERNO");
|
||||
if (serial == settings.end() || serial->second.empty() || serial->second.size() > 9 ||
|
||||
serial->second.find_first_not_of("0123456789") != std::string::npos ||
|
||||
serial->second.find_first_not_of('0') == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& field : {std::pair{"CODE", 5u}, {"AREA", 3u}, {"GAME", 2u}}) {
|
||||
const auto value = settings.find(field.first);
|
||||
if (value == settings.end() || value->second.empty() ||
|
||||
value->second.size() > field.second) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Dolphin's normal (non-deterministic) first-boot algorithm. It is independent
|
||||
// of the ES device ID. Matching another NAND requires that NAND's saved serial.
|
||||
inline std::string GenerateSerial(std::time_t now) {
|
||||
if (now < 0) {
|
||||
return {};
|
||||
}
|
||||
const auto digits = std::to_string(now % 1000000000);
|
||||
return std::string(9 - digits.size(), '0') + digits;
|
||||
}
|
||||
|
||||
// This recompilation targets the European disc. These are Dolphin's PAL boot
|
||||
// defaults; an existing setting.txt always takes precedence, in every region.
|
||||
inline std::optional<std::array<uint8_t, 256>> EncodeNew(const std::string& serial) {
|
||||
const Settings identity{{"SERNO", serial}, {"CODE", "LEH"}, {"AREA", "EUR"}, {"GAME", "EU"}};
|
||||
if (!HasIdentity(identity)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::array<uint8_t, 256> bytes{};
|
||||
size_t position = 0;
|
||||
uint32_t key = 0x73B5DBFAu;
|
||||
const auto writeByte = [&](char value) {
|
||||
bytes[position++] = static_cast<uint8_t>(value) ^ static_cast<uint8_t>(key);
|
||||
key = (key << 1) | (key >> 31);
|
||||
};
|
||||
for (const std::string& line : {std::string("AREA=EUR\r\n"), std::string("MODEL=RVL-001(EUR)\r\n"),
|
||||
std::string("DVD=0\r\n"), std::string("MPCH=0x7FFE\r\n"), std::string("CODE=LEH\r\n"),
|
||||
"SERNO=" + serial + "\r\n", std::string("VIDEO=PAL\r\n"), std::string("GAME=EU\r\n")}) {
|
||||
for (;;) {
|
||||
if (position + line.size() > bytes.size()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto start = position;
|
||||
const auto savedKey = key;
|
||||
bool hasNull = false;
|
||||
for (const char value : line) {
|
||||
writeByte(value);
|
||||
hasNull |= bytes[position - 1] == 0;
|
||||
}
|
||||
if (!hasNull) {
|
||||
break;
|
||||
}
|
||||
// Nintendo stops at an encoded NUL. Dolphin inserts an extra LF
|
||||
// before this line and retries with the shifted encryption key.
|
||||
position = start;
|
||||
key = savedKey;
|
||||
writeByte('\n');
|
||||
}
|
||||
}
|
||||
return bytes; // The unused tail stays raw zero, as in Dolphin.
|
||||
}
|
||||
|
||||
// Never replace an existing file, including an unreadable or damaged one.
|
||||
// Publish a complete file atomically so simultaneous launches use one identity.
|
||||
inline bool Ensure(const std::filesystem::path& root, std::string& error,
|
||||
std::time_t now = std::time(nullptr)) {
|
||||
const auto path = FilePath(root);
|
||||
std::error_code ec;
|
||||
const auto status = std::filesystem::symlink_status(path, ec);
|
||||
if (ec && ec != std::errc::no_such_file_or_directory) {
|
||||
error = "Cannot inspect NAND setting.txt: " + ec.message();
|
||||
return false;
|
||||
}
|
||||
if (std::filesystem::exists(status)) {
|
||||
const auto existing = Read(root);
|
||||
if (existing && HasIdentity(*existing)) {
|
||||
return true;
|
||||
}
|
||||
error = "Existing NAND setting.txt is unreadable or invalid; restore it from this console's backup";
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto bytes = EncodeNew(GenerateSerial(now));
|
||||
if (!bytes) {
|
||||
error = "Cannot initialize NAND settings: invalid system clock";
|
||||
return false;
|
||||
}
|
||||
ec.clear();
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
if (ec) {
|
||||
error = "Cannot create NAND settings directory: " + ec.message();
|
||||
return false;
|
||||
}
|
||||
static std::atomic<unsigned> sequence{0};
|
||||
const auto scratch = path.parent_path() / (".setting-init-" + std::to_string(
|
||||
std::chrono::steady_clock::now().time_since_epoch().count()) + "-" + std::to_string(sequence++));
|
||||
if (!std::filesystem::create_directory(scratch, ec)) {
|
||||
error = "Cannot create temporary NAND settings directory: " + ec.message();
|
||||
return false;
|
||||
}
|
||||
const auto temporary = scratch / "setting.txt";
|
||||
bool written = false;
|
||||
{
|
||||
std::ofstream output(temporary, std::ios::binary);
|
||||
output.write(reinterpret_cast<const char*>(bytes->data()), bytes->size());
|
||||
output.close();
|
||||
written = static_cast<bool>(output);
|
||||
}
|
||||
bool published = false;
|
||||
if (written) {
|
||||
#ifdef _WIN32
|
||||
published = MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_WRITE_THROUGH) != 0;
|
||||
#else
|
||||
published = ::link(temporary.c_str(), path.c_str()) == 0;
|
||||
#endif
|
||||
}
|
||||
std::filesystem::remove(temporary, ec);
|
||||
std::filesystem::remove(scratch, ec);
|
||||
// A competing launcher may have published its settings first. Always read
|
||||
// the winner from NAND rather than using our unpersisted candidate serial.
|
||||
const auto persisted = Read(root);
|
||||
if (persisted && HasIdentity(*persisted)) {
|
||||
return true;
|
||||
}
|
||||
error = published ? "Cannot read newly initialized NAND setting.txt" :
|
||||
"Cannot persist NAND setting.txt; check NAND directory permissions";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace RuntimeNandSettings
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
// Small host-services boundary for functionality that must not leak Win32
|
||||
// assumptions into runtime or game code. Guest execution, virtual memory, and
|
||||
// cooperative contexts remain outside this layer until dedicated macOS
|
||||
// prototypes establish a safe abstraction.
|
||||
namespace RuntimePlatform {
|
||||
|
||||
std::optional<std::filesystem::path> ExecutableDirectory() noexcept;
|
||||
|
||||
// Returns the platform's conventional per-user application-data directory.
|
||||
// It does not create the directory, leaving that policy to the caller.
|
||||
std::filesystem::path ApplicationDataDirectory(std::string_view applicationName);
|
||||
|
||||
// The root for per-run diagnostics. Keeping this here ensures log placement
|
||||
// follows the same host convention as configuration and other user data.
|
||||
std::filesystem::path LogDirectory(std::string_view applicationName);
|
||||
|
||||
uint64_t CurrentProcessId() noexcept;
|
||||
|
||||
} // namespace RuntimePlatform
|
||||
@@ -8,8 +8,10 @@
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -17,6 +19,7 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <toml.hpp>
|
||||
#include "platform/host_platform.h"
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
@@ -52,7 +55,31 @@ struct RuntimeUserConfig {
|
||||
std::optional<bool> audioMuted;
|
||||
std::optional<bool> audioMixWorker;
|
||||
std::optional<bool> attenuateMusicWhenMediaPlays;
|
||||
// Real Wii Remotes (with or without Nunchuk / Classic Controller) and Wii U Pro
|
||||
// Controllers paired over Bluetooth, driven by SDL's HIDAPI Wii driver. The driver
|
||||
// is opt-in on SDL's side, so this decides whether the runtime turns it on.
|
||||
std::optional<bool> wiiRemotes;
|
||||
// Keep re-enumerating Bluetooth HID devices while no Wii controller is connected
|
||||
// (Dolphin's "continuous scanning"), so a remote that dropped or was switched on
|
||||
// after launch shows up without restarting.
|
||||
std::optional<bool> wiiContinuousScan;
|
||||
// Accelerometer zero-point correction for the Bluetooth Wii Remote, in g and in
|
||||
// SDL's sensor frame (x right, y out of the button face, z towards the user).
|
||||
// SDL's Wii driver falls back to a nominal zero point when its read of the
|
||||
// remote's calibration block times out (common over Bluetooth), so this is
|
||||
// measured in the overlay with the remote at rest.
|
||||
std::optional<double> wiiAccelOffsetX;
|
||||
std::optional<double> wiiAccelOffsetY;
|
||||
std::optional<double> wiiAccelOffsetZ;
|
||||
// Debugging aid: append every KPAD sample of the Bluetooth remote (raw and
|
||||
// corrected accelerometer, buttons) to wii_accel_trace.csv next to Config.toml.
|
||||
std::optional<bool> wiiAccelTrace;
|
||||
std::optional<bool> networkEnabled;
|
||||
std::optional<bool> discordPresenceEnabled;
|
||||
// The application ID of the WiiCompiled Discord application. This is only
|
||||
// used by the base product; Retro Rewind supplies its own ID through the
|
||||
// standard Dolphin /dev/dolphin interface.
|
||||
std::optional<std::string> discordClientId;
|
||||
std::optional<std::string> nandRoot;
|
||||
std::optional<std::string> dvdRoot;
|
||||
// The one canonical Retro Rewind installation, owned and updated by the frontend. Setup records
|
||||
@@ -63,6 +90,8 @@ struct RuntimeUserConfig {
|
||||
// comma-separated SDL-style physical button names ("south", or
|
||||
// "dpad_up,left_shoulder") as values; pressing either bound button counts.
|
||||
std::array<std::optional<std::string>, 12> controllerButtons;
|
||||
std::optional<bool> rumbleEnabled;
|
||||
std::map<std::string, std::string> controllerExpressions;
|
||||
};
|
||||
|
||||
namespace RuntimeConfigFile {
|
||||
@@ -136,7 +165,14 @@ inline bool IsSupportedResolutionMultiplier(float value) {
|
||||
// Must stay in step with the backend table in main.cpp, which is what actually
|
||||
// maps these to AuroraBackend.
|
||||
inline bool IsSupportedGraphicsApi(std::string_view value) {
|
||||
#if defined(__APPLE__)
|
||||
static constexpr std::array<std::string_view, 2> values{"auto", "metal"};
|
||||
// only vulkan for linux
|
||||
#elif defined(__linux__)
|
||||
static constexpr std::array<std::string_view, 2> values{"auto", "vulkan"};
|
||||
#elif defined(_WIN32)
|
||||
static constexpr std::array<std::string_view, 3> values{"auto", "d3d12", "vulkan"};
|
||||
#endif
|
||||
return std::find(values.begin(), values.end(), value) != values.end();
|
||||
}
|
||||
|
||||
@@ -167,6 +203,8 @@ inline std::optional<std::filesystem::path> ExecutableDirectory() {
|
||||
}
|
||||
buffer.resize(buffer.size() * 2);
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
return RuntimePlatform::ExecutableDirectory();
|
||||
#else
|
||||
// /proc/self/exe is a Linux-specific magic symlink to the running executable; readlink()
|
||||
// does not NUL-terminate and silently truncates if the buffer is too small, so this grows
|
||||
@@ -224,6 +262,8 @@ inline std::filesystem::path ApplicationDataDirectory() {
|
||||
CoTaskMemFree(rawPath);
|
||||
return directory;
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
return RuntimePlatform::ApplicationDataDirectory(kApplicationDirectoryName);
|
||||
#else
|
||||
// XDG Base Directory spec equivalent of FOLDERID_LocalAppData: $XDG_DATA_HOME if set and
|
||||
// non-empty, otherwise its default of $HOME/.local/share.
|
||||
@@ -286,6 +326,12 @@ inline void EnsureConfigFile() {
|
||||
"mix_worker = true\n\n"
|
||||
"[network]\n"
|
||||
"enabled = true\n\n"
|
||||
"[discord]\n"
|
||||
"# Rich Presence talks only to a locally-running Discord client.\n"
|
||||
"# Retro Rewind supplies its official app ID automatically. Set this\n"
|
||||
"# to WiiCompiled's Discord application ID for basic base-game presence.\n"
|
||||
"enabled = true\n"
|
||||
"# client_id = \"123456789012345678\"\n\n"
|
||||
"[paths]\n"
|
||||
"# dvd_root = \"D:\\\\MarioKartWii\\\\DATA\"\n"
|
||||
"# nand_root = \"D:\\\\WiiNand\"\n"
|
||||
@@ -352,6 +398,7 @@ inline void AppendOverlayRoots(RuntimeUserConfig& config, const std::string& roo
|
||||
}
|
||||
}
|
||||
|
||||
// Reads every supported setting out of a parsed Config.toml document.
|
||||
inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
RuntimeUserConfig config;
|
||||
|
||||
@@ -363,6 +410,17 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
FindConfigValue<std::string>(document, "controller", buttonKeys[index]);
|
||||
}
|
||||
|
||||
config.rumbleEnabled = FindConfigValue<bool>(document, "controller", "rumble");
|
||||
|
||||
if (const auto* section = document.contains("controller") ? &document.at("controller") : nullptr;
|
||||
section != nullptr && section->is_table()) {
|
||||
for (const auto& [key, value] : section->as_table()) {
|
||||
if (key.rfind("expr_", 0) == 0 && value.is_string()) {
|
||||
config.controllerExpressions[key] = value.as_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
|
||||
config.windowPosX = FindConfigInt(document, "video", "window_x");
|
||||
config.windowPosY = FindConfigInt(document, "video", "window_y");
|
||||
@@ -417,7 +475,15 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
config.audioMixWorker = FindConfigValue<bool>(document, "audio", "mix_worker");
|
||||
config.attenuateMusicWhenMediaPlays =
|
||||
FindConfigValue<bool>(document, "audio", "attenuate_music_when_media_plays");
|
||||
config.wiiRemotes = FindConfigValue<bool>(document, "controller", "wii_remotes");
|
||||
config.wiiContinuousScan = FindConfigValue<bool>(document, "controller", "wii_continuous_scan");
|
||||
config.wiiAccelOffsetX = FindConfigValue<double>(document, "controller", "wii_accel_offset_x");
|
||||
config.wiiAccelOffsetY = FindConfigValue<double>(document, "controller", "wii_accel_offset_y");
|
||||
config.wiiAccelOffsetZ = FindConfigValue<double>(document, "controller", "wii_accel_offset_z");
|
||||
config.wiiAccelTrace = FindConfigValue<bool>(document, "controller", "wii_accel_trace");
|
||||
config.networkEnabled = FindConfigValue<bool>(document, "network", "enabled");
|
||||
config.discordPresenceEnabled = FindConfigValue<bool>(document, "discord", "enabled");
|
||||
config.discordClientId = FindConfigValue<std::string>(document, "discord", "client_id");
|
||||
|
||||
config.nandRoot = FindConfigValue<std::string>(document, "paths", "nand_root");
|
||||
config.dvdRoot = FindConfigValue<std::string>(document, "paths", "dvd_root");
|
||||
@@ -625,6 +691,25 @@ inline bool SetControllerButton(size_t index, std::string value) {
|
||||
return WriteSetting("controller", kControllerButtonKeys[index], FormatString(value));
|
||||
}
|
||||
|
||||
inline std::string ControllerExpression(const std::string& key) {
|
||||
const auto it = Get().controllerExpressions.find(key);
|
||||
return it == Get().controllerExpressions.end() ? std::string() : it->second;
|
||||
}
|
||||
|
||||
inline bool SetControllerExpression(const std::string& key, const std::string& value) {
|
||||
Mutable().controllerExpressions[key] = value;
|
||||
return WriteSetting("controller", key, FormatString(value));
|
||||
}
|
||||
|
||||
inline bool RumbleEnabled(bool fallback = true) {
|
||||
return Get().rumbleEnabled.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool SetRumbleEnabled(bool value) {
|
||||
Mutable().rumbleEnabled = value;
|
||||
return WriteSetting("controller", "rumble", value ? "true" : "false");
|
||||
}
|
||||
|
||||
inline bool SetAudioVolume(float value) {
|
||||
value = std::clamp(value, 0.0f, 1.0f);
|
||||
Mutable().audioVolume = value;
|
||||
@@ -734,14 +819,75 @@ inline bool AudioMixWorkerEnabled(bool fallback = true) {
|
||||
return Get().audioMixWorker.value_or(fallback);
|
||||
}
|
||||
|
||||
// Whether background music should duck automatically for other media playback.
|
||||
inline bool AttenuateMusicWhenMediaPlays(bool fallback = false) {
|
||||
return Get().attenuateMusicWhenMediaPlays.value_or(fallback);
|
||||
}
|
||||
|
||||
// Bluetooth Wii Remotes / Wii U Pro Controllers. Read once before SDL's joystick
|
||||
// subsystem comes up, so a change only takes effect on the next launch.
|
||||
inline bool WiiRemotesEnabled(bool fallback = true) {
|
||||
return Get().wiiRemotes.value_or(fallback);
|
||||
}
|
||||
|
||||
// Persists the Bluetooth Wii Remote driver switch.
|
||||
inline bool SetWiiRemotesEnabled(bool value) {
|
||||
Mutable().wiiRemotes = value;
|
||||
return WriteSetting("controller", "wii_remotes", value ? "true" : "false");
|
||||
}
|
||||
|
||||
// Whether to keep rescanning Bluetooth while no Wii controller is connected.
|
||||
inline bool WiiContinuousScanEnabled(bool fallback = false) {
|
||||
return Get().wiiContinuousScan.value_or(fallback);
|
||||
}
|
||||
|
||||
// Persists the continuous scanning switch.
|
||||
inline bool SetWiiContinuousScanEnabled(bool value) {
|
||||
Mutable().wiiContinuousScan = value;
|
||||
return WriteSetting("controller", "wii_continuous_scan", value ? "true" : "false");
|
||||
}
|
||||
|
||||
// Wii Remote accelerometer zero-point correction (g, SDL sensor frame); all zero
|
||||
// when the remote has not been calibrated.
|
||||
inline std::array<double, 3> WiiAccelOffset() {
|
||||
const RuntimeUserConfig& config = Get();
|
||||
return {config.wiiAccelOffsetX.value_or(0.0), config.wiiAccelOffsetY.value_or(0.0),
|
||||
config.wiiAccelOffsetZ.value_or(0.0)};
|
||||
}
|
||||
|
||||
// Whether to write the per-frame accelerometer trace (off unless asked for).
|
||||
inline bool WiiAccelTraceEnabled(bool fallback = false) {
|
||||
return Get().wiiAccelTrace.value_or(fallback);
|
||||
}
|
||||
|
||||
// True while a non-zero correction is stored ("Clear calibration" writes zeros).
|
||||
inline bool HasWiiAccelOffset() {
|
||||
const std::array<double, 3> offset = WiiAccelOffset();
|
||||
return offset[0] != 0.0 || offset[1] != 0.0 || offset[2] != 0.0;
|
||||
}
|
||||
|
||||
// Persists the accelerometer correction measured by the overlay's calibration.
|
||||
inline bool SetWiiAccelOffset(const std::array<double, 3>& offset) {
|
||||
Mutable().wiiAccelOffsetX = offset[0];
|
||||
Mutable().wiiAccelOffsetY = offset[1];
|
||||
Mutable().wiiAccelOffsetZ = offset[2];
|
||||
bool ok = true;
|
||||
const char* keys[3] = {"wii_accel_offset_x", "wii_accel_offset_y", "wii_accel_offset_z"};
|
||||
for (size_t i = 0; i < 3; ++i) {
|
||||
// Always a float literal, so a whole-number offset does not come back as a TOML integer.
|
||||
std::ostringstream formatted;
|
||||
formatted << std::fixed << std::setprecision(4) << offset[i];
|
||||
ok = WriteSetting("controller", keys[i], formatted.str()) && ok;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Target frame rate for frame interpolation, or 0 to disable it.
|
||||
inline uint32_t FrameInterpolationFps(uint32_t fallback = 0) {
|
||||
return Get().frameInterpolationFps.value_or(fallback);
|
||||
}
|
||||
|
||||
// Whether to skip draws whose graphics pipeline has not finished compiling yet.
|
||||
inline bool SkipUnreadyPipelines(bool fallback = true) {
|
||||
return Get().skipUnreadyPipelines.value_or(fallback);
|
||||
}
|
||||
@@ -815,6 +961,14 @@ inline std::string RetroRewindRoot(std::string fallback = "") {
|
||||
return Get().retroRewindRoot.value_or(std::move(fallback));
|
||||
}
|
||||
|
||||
inline bool DiscordPresenceEnabled(bool fallback = true) {
|
||||
return Get().discordPresenceEnabled.value_or(fallback);
|
||||
}
|
||||
|
||||
inline std::string DiscordClientId(std::string fallback = "1543984562369990706") {
|
||||
return Get().discordClientId.value_or(std::move(fallback));
|
||||
}
|
||||
|
||||
inline const std::vector<std::string>& OverlayRoots() {
|
||||
return Get().overlayRoots;
|
||||
}
|
||||
@@ -871,6 +1025,9 @@ inline void LogLoadedConfig() {
|
||||
if (config.networkEnabled) {
|
||||
std::cout << " network_enabled=" << (*config.networkEnabled ? "true" : "false");
|
||||
}
|
||||
if (config.discordPresenceEnabled) {
|
||||
std::cout << " discord_enabled=" << (*config.discordPresenceEnabled ? "true" : "false");
|
||||
}
|
||||
if (config.nandRoot) {
|
||||
std::cout << " nand_root=" << *config.nandRoot;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ extern thread_local uint32_t g_sehLastAccessType;
|
||||
|
||||
void WriteFatalLog(std::string_view reason);
|
||||
void SetRuntimeExitCode(int code);
|
||||
void MarkFatalErrorReported();
|
||||
|
||||
// Centralized crash reporting (defined in main.cpp). Every fatal path funnels
|
||||
// through these so the per-run log folder always receives the same artifact
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#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 looking for a remote (no Wii controller connected).
|
||||
bool IsScanning();
|
||||
// True where looking means periodic rescans; elsewhere Poll() waits for hotplug.
|
||||
bool PeriodicRescanEnabled();
|
||||
// Rescans issued since a Wii controller was last seen.
|
||||
uint32_t ScanCount();
|
||||
|
||||
} // namespace WiiRemoteInput
|
||||
@@ -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
|
||||
+29
-140
@@ -2,6 +2,7 @@
|
||||
#include "memory.h"
|
||||
#include "abi_bridge.h"
|
||||
#include "hle_stubs.h"
|
||||
#include "host_context.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
// Defined in hle/os/os_sleep.cpp; the sleep-timer table is file-local there.
|
||||
@@ -13,24 +14,8 @@
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#include "libco.h"
|
||||
#endif
|
||||
|
||||
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::unordered_map<uint32_t, GuestFiber> GuestFiberManager::s_fibers;
|
||||
std::vector<void*> GuestFiberManager::s_fibersPendingDelete;
|
||||
@@ -45,21 +30,11 @@ void GuestFiberManager::PurgePendingFibers() {
|
||||
std::lock_guard<std::mutex> lock(s_mutex);
|
||||
toDelete.swap(s_fibersPendingDelete);
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
const void* current = GetCurrentFiber();
|
||||
for (void* f : toDelete) {
|
||||
if (f && f != current) {
|
||||
DeleteFiber(f);
|
||||
if (f && !HostContext::IsCurrent(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
|
||||
@@ -212,27 +187,13 @@ void GuestFiberManager::Initialize() {
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
// Convert the main thread to a fiber (the scheduler fiber)
|
||||
s_schedulerFiber = ConvertThreadToFiber(nullptr);
|
||||
if (!s_schedulerFiber) {
|
||||
// May already be a fiber
|
||||
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();
|
||||
}
|
||||
if (!HostContext::InitializeScheduler(&s_schedulerFiber)) {
|
||||
RT_LOG(RT_TAG_OS) << "FATAL: Failed to initialize scheduler context!" << std::endl;
|
||||
ShowRuntimeFatalPopup("guest scheduler initialization failed",
|
||||
"The host could not create the scheduler context 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_initialized = true;
|
||||
}
|
||||
@@ -240,32 +201,18 @@ void GuestFiberManager::Initialize() {
|
||||
void GuestFiberManager::Shutdown() {
|
||||
std::lock_guard<std::mutex> lock(s_mutex);
|
||||
|
||||
#if defined(_WIN32)
|
||||
for (auto& [addr, fiber] : s_fibers) {
|
||||
if (fiber.fiber && !fiber.isSchedulerFiber) {
|
||||
DeleteFiber(fiber.fiber);
|
||||
HostContext::Destroy(fiber.fiber);
|
||||
fiber.fiber = nullptr;
|
||||
}
|
||||
}
|
||||
s_fibers.clear();
|
||||
|
||||
// Convert scheduler fiber back to thread
|
||||
if (s_schedulerFiber) {
|
||||
ConvertFiberToThread();
|
||||
HostContext::ShutdownScheduler(s_schedulerFiber);
|
||||
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;
|
||||
}
|
||||
@@ -288,11 +235,7 @@ bool GuestFiberManager::CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entr
|
||||
if (existingIt != s_fibers.end()) {
|
||||
// Delete the old fiber if it exists and is not the scheduler fiber
|
||||
if (existingIt->second.fiber && !existingIt->second.isSchedulerFiber) {
|
||||
#if defined(_WIN32)
|
||||
DeleteFiber(existingIt->second.fiber);
|
||||
#else
|
||||
co_delete(static_cast<cothread_t>(existingIt->second.fiber));
|
||||
#endif
|
||||
HostContext::Destroy(existingIt->second.fiber);
|
||||
}
|
||||
s_fibers.erase(existingIt);
|
||||
}
|
||||
@@ -312,31 +255,21 @@ bool GuestFiberManager::CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entr
|
||||
gf.cpuContext.pc = entryPoint;
|
||||
gf.cpuContext.srr0 = entryPoint;
|
||||
|
||||
#if defined(_WIN32)
|
||||
// Create Windows fiber with reasonable stack size
|
||||
// Use host stack size (64KB should be plenty for translated code)
|
||||
constexpr size_t kHostStackSize = 64 * 1024;
|
||||
gf.fiber = CreateFiber(kHostStackSize, FiberProc, reinterpret_cast<void*>(static_cast<uintptr_t>(guestThreadAddr)));
|
||||
// The host stack models only translated host calls; the guest stack starts
|
||||
// at stackBase in the CPU context above. 64 KiB is too small for deep
|
||||
// translated/HLE call chains (notably NW4R's sound worker), and on macOS
|
||||
// it can exhaust the guarded coroutine stack as unrelated host work (such
|
||||
// as a window resize) adds a little more nesting. Keep enough headroom for
|
||||
// those chains while the guest stack remains separately bounded.
|
||||
constexpr size_t kHostStackSize = 1024 * 1024;
|
||||
gf.fiber = HostContext::Create(kHostStackSize, FiberProc,
|
||||
reinterpret_cast<void*>(static_cast<uintptr_t>(guestThreadAddr)));
|
||||
|
||||
if (!gf.fiber) {
|
||||
DWORD err = GetLastError();
|
||||
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"
|
||||
RT_LOG(RT_TAG_OS) << "Failed to create host context for thread 0x"
|
||||
<< std::hex << guestThreadAddr << std::dec << std::endl;
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
s_fibers[guestThreadAddr] = gf;
|
||||
|
||||
@@ -390,24 +323,11 @@ void GuestFiberManager::ExitGuestThread(uint32_t guestThreadAddr, ThreadState fi
|
||||
}
|
||||
|
||||
if (it->second.fiber && !it->second.isSchedulerFiber) {
|
||||
#if defined(_WIN32)
|
||||
const void* current = GetCurrentFiber();
|
||||
if (it->second.fiber == current) {
|
||||
if (HostContext::IsCurrent(it->second.fiber)) {
|
||||
s_fibersPendingDelete.push_back(it->second.fiber);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
@@ -474,12 +394,7 @@ void GuestFiberManager::SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu
|
||||
|
||||
// Check if we're already on the target fiber (e.g., switching to main thread
|
||||
// when we're already on the scheduler fiber)
|
||||
#if defined(_WIN32)
|
||||
void* currentFiber = GetCurrentFiber();
|
||||
#else
|
||||
void* currentFiber = co_active();
|
||||
#endif
|
||||
if (currentFiber == fiberHandle) {
|
||||
if (HostContext::IsCurrent(fiberHandle)) {
|
||||
// Already executing on the target host fiber. This is common for the
|
||||
// default guest thread, which also owns the scheduler fiber. Keep the
|
||||
// live CPU context instead of restoring a possibly stale saved copy
|
||||
@@ -496,15 +411,7 @@ void GuestFiberManager::SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu
|
||||
}
|
||||
|
||||
// Switch to the target fiber (the target fiber will load its own context)
|
||||
#if defined(_WIN32)
|
||||
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
|
||||
HostContext::Switch(fiberHandle);
|
||||
|
||||
// When we return here, the fiber that issued SwitchToThread has resumed.
|
||||
// That does not automatically mean the previous guest thread became runnable
|
||||
@@ -618,14 +525,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)
|
||||
void CALLBACK GuestFiberManager::FiberProc(void* param)
|
||||
#else
|
||||
@@ -634,6 +533,7 @@ void GuestFiberManager::FiberProc(void* param)
|
||||
{
|
||||
uint32_t guestThreadAddr = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(param));
|
||||
|
||||
|
||||
// Get our fiber info
|
||||
GuestFiber* fiber = nullptr;
|
||||
uint32_t entryPoint = 0;
|
||||
@@ -644,7 +544,7 @@ void GuestFiberManager::FiberProc(void* param)
|
||||
auto it = s_fibers.find(guestThreadAddr);
|
||||
if (it == s_fibers.end()) {
|
||||
RT_LOG(RT_TAG_OS) << "FiberProc: fiber not found!" << std::endl;
|
||||
SwitchToScheduler();
|
||||
HostContext::Switch(s_schedulerFiber);
|
||||
return;
|
||||
}
|
||||
fiber = &it->second;
|
||||
@@ -707,7 +607,7 @@ void GuestFiberManager::FiberProc(void* param)
|
||||
<< ", fn=0x" << startFn << ") after retries; continuing anyway." << std::dec << std::endl;
|
||||
break;
|
||||
}
|
||||
SwitchToScheduler();
|
||||
HostContext::Switch(s_schedulerFiber);
|
||||
}
|
||||
|
||||
// The deferral loop above yields to the scheduler and therefore can resume
|
||||
@@ -764,18 +664,7 @@ void GuestFiberManager::FiberProc(void* param)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
#endif
|
||||
|
||||
namespace GuestFlat {
|
||||
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
|
||||
bool g_requiresCheckedAccess = false;
|
||||
#endif
|
||||
namespace {
|
||||
|
||||
#if defined(_WIN32)
|
||||
@@ -48,6 +51,16 @@ constexpr DWORD kMemPreservePlaceholder = 0x00000002;
|
||||
constexpr size_t kAllocationGranularity = 0x10000; // 64 KiB
|
||||
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
|
||||
// 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
|
||||
@@ -273,8 +286,12 @@ void EnsureReservation() {
|
||||
std::ostringstream oss;
|
||||
oss << "Unable to reserve the 4 GiB flat guest address space at 0x" << std::hex
|
||||
<< reinterpret_cast<uintptr_t>(requested) << std::dec
|
||||
<< ". Something else in this process already occupies part of the 16 TiB region - "
|
||||
"an injected library, an overlay or a debugging tool is the usual cause.";
|
||||
<< ". Either something else in this process already occupies that address (an "
|
||||
"injected library, an overlay or a debugging tool is the usual cause), or this "
|
||||
"kernel's virtual address space does not reach that high (common on some 32-bit-"
|
||||
"userspace-compatible or older AArch64 configurations, e.g. a kernel built for "
|
||||
"39-bit virtual addresses) - in the latter case mmap() silently substitutes an "
|
||||
"address near the top of the space it does have instead of honoring the request.";
|
||||
throw std::runtime_error(oss.str());
|
||||
}
|
||||
#endif
|
||||
@@ -345,7 +362,7 @@ bool IsMmio(uint32_t address) { return MemoryInline::IsMmioAddress(address); }
|
||||
bool IsGpuFifo(uint32_t address) { return MemoryInline::IsGpuFifoAddress(address); }
|
||||
|
||||
void ApplyExecutableProtectionLocked() {
|
||||
if (g_base == nullptr) return;
|
||||
if (g_base == nullptr || RequiresCheckedAccess()) return;
|
||||
auto& protectedPages = ExecutableProtectedPages();
|
||||
for (const auto& range : ExecutableRanges()) {
|
||||
// Only pages fully inside the range are protected: edge pages often share a page with data
|
||||
@@ -493,6 +510,10 @@ bool IsActive() {
|
||||
void Initialize(const std::vector<RegionRequest>& regions) {
|
||||
std::lock_guard<std::mutex> lock(StateMutex());
|
||||
|
||||
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
|
||||
g_requiresCheckedAccess = HostPageSize() > kGuestPageSize;
|
||||
#endif
|
||||
|
||||
if (g_initialized) {
|
||||
if (!SameLayout(g_activeRegions, regions)) {
|
||||
throw std::runtime_error(
|
||||
@@ -611,7 +632,7 @@ uint8_t* HostPointer(uint32_t guestAddress) {
|
||||
}
|
||||
|
||||
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;
|
||||
if (end > kGuestSpaceSize) return;
|
||||
std::lock_guard<std::mutex> lock(StateMutex());
|
||||
@@ -626,7 +647,7 @@ void ProtectDeferredRange(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());
|
||||
auto& ranges = DeferredRanges();
|
||||
const uint64_t end = static_cast<uint64_t>(address) + length;
|
||||
@@ -641,7 +662,7 @@ void UnprotectDeferredRange(uint32_t address, size_t length) {
|
||||
}
|
||||
|
||||
void RegisterExecutableRange(uint32_t start, uint32_t end) {
|
||||
if (end <= start) return;
|
||||
if (RequiresCheckedAccess() || end <= start) return;
|
||||
std::lock_guard<std::mutex> lock(StateMutex());
|
||||
auto& ranges = ExecutableRanges();
|
||||
if (std::any_of(ranges.begin(), ranges.end(), [&](const GuardedRange& range) {
|
||||
|
||||
@@ -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
|
||||
@@ -394,3 +394,4 @@ extern std::mutex g_tlutObjMutex;
|
||||
// that happens on another key: unordered_map keeps references valid across a
|
||||
// rehash, an open-addressed table would not.
|
||||
extern std::unordered_map<uint32_t, TexObjSlot> g_TexObjMeta;
|
||||
extern std::map<uint32_t, TlutObjMeta> g_TlutObjMeta;
|
||||
|
||||
@@ -33,6 +33,15 @@ void WriteGuestFloat(uint32_t addr, float value, const char* label) {
|
||||
|
||||
void* GuestToHostPtr(uint32_t addr, size_t len) {
|
||||
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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,249 @@
|
||||
#include "hle_stubs.h"
|
||||
#include "memory.h"
|
||||
#include "wii_remote_input.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#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)
|
||||
{
|
||||
(void)chan;
|
||||
(void)statusPtr;
|
||||
(void)count;
|
||||
return 0;
|
||||
if (chan >= g_channels.size() || statusPtr == 0 || count == 0) {
|
||||
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),
|
||||
(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)
|
||||
{
|
||||
(void)chan;
|
||||
(void)statusPtr;
|
||||
(void)count;
|
||||
return 0;
|
||||
constexpr uint32_t kMaxEntries = 16; // KPAD_MAX_READ_BUFS
|
||||
if (chan >= g_channels.size() || statusPtr == 0 || count == 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,
|
||||
(uint32_t chan, uint32_t statusPtr, uint32_t count), (chan, statusPtr, count));
|
||||
|
||||
@@ -1,17 +1,75 @@
|
||||
#include "hle_stubs.h"
|
||||
#include "memory.h"
|
||||
#include "hle/controller_status_contract.h"
|
||||
#include "input_bindings.h"
|
||||
#include "wii_remote_input.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <dolphin/pad.h>
|
||||
|
||||
namespace {
|
||||
|
||||
std::atomic<bool> g_rumbleEnabled{true};
|
||||
|
||||
bool NativeButtonHeld(SDL_Gamepad* gamepad, uint32_t nativeButton) {
|
||||
if (gamepad == nullptr || nativeButton == PAD_NATIVE_BUTTON_INVALID ||
|
||||
nativeButton >= SDL_GAMEPAD_BUTTON_COUNT) {
|
||||
return false;
|
||||
}
|
||||
return SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(nativeButton));
|
||||
}
|
||||
|
||||
// A digital button bound to L or R has no analog travel of its own. On real
|
||||
// hardware the click only engages at full depression, so report a full pull.
|
||||
void FillTriggersHeldByButtons(PADStatus* statuses) {
|
||||
if (InputBindings::InputBlocked()) {
|
||||
return;
|
||||
}
|
||||
for (uint32_t port = 0; port < PAD_CHANMAX; ++port) {
|
||||
if (statuses[port].err != PAD_ERR_NONE) {
|
||||
continue;
|
||||
}
|
||||
const s32 index = PADGetIndexForPort(port);
|
||||
if (index < 0) {
|
||||
continue;
|
||||
}
|
||||
SDL_Gamepad* gamepad = PADGetSDLGamepadForIndex(static_cast<u32>(index));
|
||||
if (gamepad == nullptr) {
|
||||
continue;
|
||||
}
|
||||
const auto scan = [&](PADButtonMapping* mappings, u32 count) {
|
||||
if (mappings == nullptr) {
|
||||
return;
|
||||
}
|
||||
for (u32 i = 0; i < count; ++i) {
|
||||
const PADButtonMapping& mapping = mappings[i];
|
||||
if (mapping.padButton != PAD_TRIGGER_L && mapping.padButton != PAD_TRIGGER_R) {
|
||||
continue;
|
||||
}
|
||||
if (!NativeButtonHeld(gamepad, mapping.nativeButton)) {
|
||||
continue;
|
||||
}
|
||||
if (mapping.padButton == PAD_TRIGGER_L) {
|
||||
statuses[port].triggerLeft = 255;
|
||||
} else {
|
||||
statuses[port].triggerRight = 255;
|
||||
}
|
||||
}
|
||||
};
|
||||
u32 count = 0;
|
||||
scan(PADGetButtonMappings(port, &count), count);
|
||||
count = 0;
|
||||
scan(PADGetAltButtonMappings(port, &count), count);
|
||||
}
|
||||
}
|
||||
|
||||
void WritePadStatus(uint32_t base, const PADStatus& status) {
|
||||
const auto guestStatus = PadStatusContract::Encode({
|
||||
status.button,
|
||||
@@ -31,12 +89,18 @@ void WritePadStatus(uint32_t base, const PADStatus& status) {
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" void PAD_HLE_SetRumbleEnabled(bool enabled)
|
||||
{
|
||||
g_rumbleEnabled.store(enabled, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
extern "C" uint32_t PAD__Init_HLE()
|
||||
{
|
||||
return PADInit() ? 1u : 0u;
|
||||
}
|
||||
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)
|
||||
{
|
||||
if (statusPtr == 0) {
|
||||
@@ -44,7 +108,16 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
|
||||
}
|
||||
|
||||
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);
|
||||
// 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);
|
||||
|
||||
FillTriggersHeldByButtons(statuses);
|
||||
InputBindings::Apply(statuses);
|
||||
|
||||
try {
|
||||
for (uint32_t i = 0; i < PAD_CHANMAX; ++i) {
|
||||
@@ -73,6 +146,9 @@ PPC_NATIVE_OVERRIDE(801AF1E4, PAD__Recalibrate_HLE, uint32_t, (uint32_t mask), (
|
||||
|
||||
extern "C" void PAD__ControlMotor_HLE(int32_t chan, uint32_t command)
|
||||
{
|
||||
if (command == PAD_MOTOR_RUMBLE && !g_rumbleEnabled.load(std::memory_order_relaxed)) {
|
||||
command = PAD_MOTOR_STOP;
|
||||
}
|
||||
PADControlMotor(chan, command);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801AF908, PAD__ControlMotor_HLE, (int32_t chan, uint32_t command), (chan, command));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "hle_stubs.h"
|
||||
#include "memory.h"
|
||||
#include "hle/controller_status_contract.h"
|
||||
#include "wii_remote_input.h"
|
||||
|
||||
#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));
|
||||
|
||||
// WPADSetDataFormat: records the per-channel data format the game asked for.
|
||||
extern "C" int32_t WPADSetDataFormat_HLE(uint32_t chan, int32_t format)
|
||||
{
|
||||
return g_state.contract.SetDataFormat(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)
|
||||
{
|
||||
if (chan >= WpadContract::kChannelCount) {
|
||||
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) {
|
||||
Memory::Write32(typePtr, WpadContract::kExtensionCore);
|
||||
}
|
||||
|
||||
@@ -499,7 +499,7 @@ int32_t HandleIpTopIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const s
|
||||
// Nonblocking sockets get -SO_EAGAIN immediately (Dolphin's retry predicate
|
||||
// short-circuits on nonBlock/forceNonBlock, IOS/Network/Socket.cpp:715-718);
|
||||
// waiting here anyway stalled the whole emulation thread on every empty read.
|
||||
constexpr int kStreamRecvWaitMs = 250;
|
||||
constexpr int kStreamRecvWaitMs = 1000;
|
||||
const int streamWaitMs = (forceNonBlock || s->nonblocking) ? 0 : kStreamRecvWaitMs;
|
||||
const bool waited = ret < 0 && !fromPtr && s->type == SOCK_STREAM &&
|
||||
IsWouldBlockError(nativeErr) && WaitForReadable(s->native, streamWaitMs);
|
||||
|
||||
@@ -270,7 +270,13 @@ static int32_t EnsureSslCredentials(SslSession& ssl) {
|
||||
|
||||
SCHANNEL_CRED cred{};
|
||||
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{};
|
||||
const SECURITY_STATUS status = AcquireCredentialsHandleA(
|
||||
@@ -289,6 +295,13 @@ static int32_t SslHandshakeImpl(SslSession& ssl) {
|
||||
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);
|
||||
if (credRet != SSL_OK) {
|
||||
return credRet;
|
||||
@@ -331,7 +344,7 @@ static int32_t SslHandshakeImpl(SslSession& ssl) {
|
||||
|
||||
const SECURITY_STATUS status = InitializeSecurityContextA(
|
||||
&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);
|
||||
if (status != SEC_E_INVALID_HANDLE) {
|
||||
ssl.haveContext = true;
|
||||
@@ -567,10 +580,9 @@ int32_t HandleSslIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const std
|
||||
|
||||
switch (cmd) {
|
||||
case IOCTLV_NET_SSL_NEW: {
|
||||
// out[0] carries the verify option. Its value is not consulted - every
|
||||
// certificate check is delegated to Schannel with
|
||||
// SCH_CRED_MANUAL_CRED_VALIDATION - but it is still read so that a
|
||||
// request pointing outside guest memory faults here as it always has.
|
||||
// out[0] carries the guest verify option. Host TLS verification is
|
||||
// always enforced by Schannel, but still read this value so a request
|
||||
// pointing outside guest memory faults here as it always has.
|
||||
if (!out.empty() && out[0].address && out[0].size >= 4) {
|
||||
(void)Memory::Read32(out[0].address);
|
||||
}
|
||||
|
||||
+29
-15
@@ -12,7 +12,25 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kPalProductRegion = 2;
|
||||
// Use the SDK's own value tables, including its unknown-region result.
|
||||
uint32_t LookupProductRegion(uint32_t table, uint32_t stride, uint32_t count,
|
||||
const std::string& value) {
|
||||
for (uint32_t index = 0; index < count; ++index) {
|
||||
const uint32_t entry = table + index * stride;
|
||||
if (!Memory::Contains(entry, stride)) {
|
||||
break;
|
||||
}
|
||||
const auto* bytes = static_cast<const uint8_t*>(Memory::GetPointer(entry, stride));
|
||||
if (bytes[0] == 0xFF) {
|
||||
break;
|
||||
}
|
||||
if (value.size() < stride - 1 &&
|
||||
std::memcmp(bytes + 1, value.c_str(), value.size() + 1) == 0) {
|
||||
return bytes[0];
|
||||
}
|
||||
}
|
||||
return 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -50,16 +68,12 @@ extern "C" uint32_t SCGetEuRgb60Mode_HLE()
|
||||
|
||||
PPC_NATIVE_OVERRIDE(801B1CAC, SCGetEuRgb60Mode_HLE, uint32_t, (), ());
|
||||
|
||||
// The managed NAND intentionally starts without a console-owned setting.txt.
|
||||
// DWC nevertheless requires the Wii product code and serial number so it can
|
||||
// include csnum in NAS authentication. Expose one stable virtual-console
|
||||
// identity without requiring or mutating a user's real NAND.
|
||||
// Expose the selected emulated NAND identity through the SDK SC APIs.
|
||||
|
||||
extern "C" uint32_t SCGetProductArea_HLE()
|
||||
{
|
||||
// The PAL setting.txt AREA value is "EUR". The SDK's lookup table at
|
||||
// 0x8029CEB0 maps JPN=0, USA=1, EUR=2.
|
||||
return kPalProductRegion;
|
||||
return LookupProductRegion(0x8029CEB0u, 5, 13,
|
||||
RuntimeConsoleIdentity::Current().area);
|
||||
}
|
||||
|
||||
PPC_NATIVE_OVERRIDE(801B23A0, SCGetProductArea_HLE, uint32_t, (), ());
|
||||
@@ -68,12 +82,13 @@ extern "C" uint32_t SCGetProductCode_HLE()
|
||||
{
|
||||
// Original PAL SC storage for the six-byte CODE value.
|
||||
constexpr uint32_t kProductCodeAddress = 0x803869E0u;
|
||||
static constexpr char kProductCode[] = "LEH";
|
||||
if (!Memory::Contains(kProductCodeAddress, sizeof(kProductCode))) {
|
||||
const std::string& productCode = RuntimeConsoleIdentity::Current().productCode;
|
||||
const size_t size = productCode.size() + 1;
|
||||
if (!Memory::Contains(kProductCodeAddress, size)) {
|
||||
return 0;
|
||||
}
|
||||
std::memcpy(Memory::GetPointer(kProductCodeAddress, sizeof(kProductCode)),
|
||||
kProductCode, sizeof(kProductCode));
|
||||
std::memcpy(Memory::GetPointer(kProductCodeAddress, size),
|
||||
productCode.c_str(), size);
|
||||
return kProductCodeAddress;
|
||||
}
|
||||
|
||||
@@ -94,9 +109,8 @@ PPC_NATIVE_OVERRIDE(801B2460, SCGetProductSN_HLE, uint32_t, (uint32_t serialAddr
|
||||
|
||||
extern "C" uint32_t SCGetProductGameRegion_HLE()
|
||||
{
|
||||
// The PAL setting.txt GAME value is "EU". The SDK's own lookup table at
|
||||
// 0x8029CEF8 maps JP=0, US=1, EU=2.
|
||||
return kPalProductRegion;
|
||||
return LookupProductRegion(0x8029CEF8u, 4, 4,
|
||||
RuntimeConsoleIdentity::Current().gameRegion);
|
||||
}
|
||||
|
||||
PPC_NATIVE_OVERRIDE(801B24C8, SCGetProductGameRegion_HLE, uint32_t, (), ());
|
||||
|
||||
@@ -93,6 +93,9 @@ extern "C" int32_t NANDOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint32_t
|
||||
|
||||
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||
|
||||
if (const auto result = NandCheckSystemSaveRead("NANDOpen", hostPath, mode))
|
||||
return *result;
|
||||
|
||||
// Existing-file write opens go through a shadow copy seeded from the original, so a
|
||||
// crash between NANDWrite and NANDClose cannot leave a torn file (the game patches
|
||||
// sub-ranges, e.g. ghost saves at a non-zero offset). New files still create in place.
|
||||
|
||||
@@ -411,6 +411,8 @@ extern "C" int32_t NANDSafeOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint
|
||||
if (mode == 1) {
|
||||
// Read-only safe open reads the original in place; the library builds no scratch
|
||||
// copy for this case.
|
||||
if (const auto result = NandCheckSystemSaveRead("NANDSafeOpen", hostPath, mode))
|
||||
return *result;
|
||||
FILE* file = NandFopen(hostPath, "rb");
|
||||
if (!file && IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) {
|
||||
file = NandFopen(hostPath, "rb");
|
||||
|
||||
@@ -411,6 +411,26 @@ bool IsFaceLibResourcePath(const char* path) {
|
||||
return std::strcmp(path, "/shared2/menu/FaceLib/RFL_Res.dat") == 0;
|
||||
}
|
||||
|
||||
std::optional<int32_t> NandCheckSystemSaveRead(const char* who,
|
||||
const std::filesystem::path& hostPath, int mode, bool ios) {
|
||||
const auto action = RuntimeNandSave::CheckRead(hostPath, mode);
|
||||
if (action == RuntimeNandSave::ReadAction::Proceed) return std::nullopt;
|
||||
if (action == RuntimeNandSave::ReadAction::Missing) {
|
||||
LogNandWarning(who, "treating empty or zero-filled system save '%s' as missing",
|
||||
HostPathText(hostPath).c_str());
|
||||
return ios ? ISFS_ENOENT : NAND_RESULT_NOEXISTS;
|
||||
}
|
||||
if (action == RuntimeNandSave::ReadAction::RecoveryNeeded) {
|
||||
LogNandError(who, "system save '%s' is missing or blank but its .nandsafe.tmp contains data; "
|
||||
"back up both files before attempting recovery",
|
||||
HostPathText(hostPath).c_str());
|
||||
} else {
|
||||
LogNandError(who, "could not inspect system save '%s' or its write shadow; leaving data untouched",
|
||||
HostPathText(hostPath).c_str());
|
||||
}
|
||||
return ios ? ISFS_EIO : NAND_RESULT_UNKNOWN;
|
||||
}
|
||||
|
||||
// Create directories recursively
|
||||
bool CreateDirectoryPath(const std::filesystem::path& path) {
|
||||
if (path.empty()) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "hle/runtime_parse_helpers.h"
|
||||
#include "memory.h"
|
||||
#include "nand_path.h"
|
||||
#include "nand_save_probe.h"
|
||||
#include "hle/net/network.h"
|
||||
#include "recomp_mod_loader.h"
|
||||
#include "runtime_config.h"
|
||||
@@ -26,6 +27,7 @@
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
@@ -56,6 +58,11 @@ constexpr uint32_t kNandTitleIdLo = 0x524D4350; // "RMCP" fallback
|
||||
void LogNandError(const char* func, const char* fmt, ...);
|
||||
void LogNandWarning(const char* func, const char* fmt, ...);
|
||||
|
||||
// An empty optional means continue opening normally; otherwise return the
|
||||
// supplied NAND/IOS error without exposing a failed scan as a missing save.
|
||||
std::optional<int32_t> NandCheckSystemSaveRead(const char* who,
|
||||
const std::filesystem::path& hostPath, int mode, bool ios = false);
|
||||
|
||||
// ============================================================================
|
||||
// File Descriptor Management
|
||||
// ============================================================================
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "nand_internal.h"
|
||||
|
||||
#include "discord_presence.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
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_DISCORD_SET_CLIENT:
|
||||
case DOLPHIN_IOCTL_DISCORD_SET_PRESENCE:
|
||||
return ISFS_OK;
|
||||
|
||||
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:
|
||||
if (numIn != 0 || numOut != 0) {
|
||||
return ISFS_EINVAL;
|
||||
}
|
||||
if (RuntimeConfigFile::DiscordPresenceEnabled()) {
|
||||
DiscordPresence::Reset();
|
||||
}
|
||||
return ISFS_OK;
|
||||
|
||||
case DOLPHIN_IOCTL_GET_SYSTEM_TIME: {
|
||||
@@ -330,6 +391,9 @@ extern "C" int32_t NAND_IOS_Open_HLE(uint32_t pathPtr, uint32_t mode) {
|
||||
|
||||
// It's a NAND file path
|
||||
const std::filesystem::path hostPath = TranslateNandPath(path);
|
||||
|
||||
if (const auto result = NandCheckSystemSaveRead("IOS_Open", hostPath, mode, true))
|
||||
return *result;
|
||||
|
||||
// Seed FaceLib resources before the existence check so every open mode can
|
||||
// still find them on a fresh managed NAND.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "aurora_events.h"
|
||||
#include "settings_overlay.h"
|
||||
#include "fiber_manager.h"
|
||||
#include "platform/host_platform.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
#include <dolphin/vi.h>
|
||||
@@ -20,18 +21,15 @@
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include <aurora/aurora.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
#include <aurora/aurora.h>
|
||||
|
||||
// Forward declaration for OSWakeupThread - used to wake threads on VI retrace queue
|
||||
extern "C" void OSWakeupThread_HLE_801aaaa4(CpuContext* ctx);
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -4,6 +4,13 @@
|
||||
// build/PCH) and runs from a priority-101 C initializer, ahead of every C++ dynamic initializer
|
||||
// and thus the first AVX2 code that could execute. Keep it free of anything that could pull in
|
||||
// vectorized code: no iostreams, no std::string, no runtime-wide headers.
|
||||
//
|
||||
// x86-64-v3 is an x86-specific optional-feature baseline (AVX2/BMI2/FMA and friends are not
|
||||
// guaranteed present on every x86_64 chip); nothing here applies on AArch64, where ASIMD/NEON is
|
||||
// mandatory in the base architecture and PublicProducts.cmake never applies an -march=x86-64-v3
|
||||
// equivalent flag to begin with. That branch below is a no-op stub, not a port of this check.
|
||||
|
||||
#if defined(__x86_64__)
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -214,3 +221,11 @@ extern "C" int MkwHostCpuBaselineInit() {
|
||||
__attribute__((constructor(101))) static void MkwHostCpuBaselineCtor() {
|
||||
MkwHostCpuBaselineInit();
|
||||
}
|
||||
|
||||
#else // !defined(__x86_64__)
|
||||
|
||||
extern "C" int MkwHostCpuBaselineInit() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif // defined(__x86_64__)
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
#include "input_bindings.h"
|
||||
|
||||
#include "controller_button_names.h"
|
||||
#include "input_expr.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
|
||||
namespace InputBindings {
|
||||
namespace {
|
||||
|
||||
struct Binding {
|
||||
std::string text;
|
||||
InputExpr::Expression expr;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
std::mutex g_mutex;
|
||||
std::array<std::array<Binding, kControls.size()>, PAD_CHANMAX> g_bindings;
|
||||
bool g_anyBound = false;
|
||||
bool g_inputBlocked = false;
|
||||
|
||||
// Dolphin input names, mapped onto SDL. XInput-style names are exact; DInput
|
||||
// "Button <n>" indices follow the common PlayStation layout, which is what
|
||||
// DInput reports for a DualShock/DualSense. Other pads may number differently.
|
||||
const std::unordered_map<std::string, SDL_GamepadButton>& ButtonNames() {
|
||||
static const std::unordered_map<std::string, SDL_GamepadButton> table = {
|
||||
{"Button A", SDL_GAMEPAD_BUTTON_SOUTH}, {"Button B", SDL_GAMEPAD_BUTTON_EAST},
|
||||
{"Button X", SDL_GAMEPAD_BUTTON_WEST}, {"Button Y", SDL_GAMEPAD_BUTTON_NORTH},
|
||||
{"Shoulder L", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
|
||||
{"Shoulder R", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
|
||||
{"Thumb L", SDL_GAMEPAD_BUTTON_LEFT_STICK}, {"Thumb R", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
|
||||
{"Start", SDL_GAMEPAD_BUTTON_START}, {"Back", SDL_GAMEPAD_BUTTON_BACK},
|
||||
{"Guide", SDL_GAMEPAD_BUTTON_GUIDE},
|
||||
{"Pad N", SDL_GAMEPAD_BUTTON_DPAD_UP}, {"Pad S", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
|
||||
{"Pad W", SDL_GAMEPAD_BUTTON_DPAD_LEFT}, {"Pad E", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
||||
{"Hat 0 N", SDL_GAMEPAD_BUTTON_DPAD_UP}, {"Hat 0 S", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
|
||||
{"Hat 0 W", SDL_GAMEPAD_BUTTON_DPAD_LEFT}, {"Hat 0 E", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
||||
{"Button 0", SDL_GAMEPAD_BUTTON_WEST}, {"Button 1", SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
{"Button 2", SDL_GAMEPAD_BUTTON_EAST}, {"Button 3", SDL_GAMEPAD_BUTTON_NORTH},
|
||||
{"Button 4", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
|
||||
{"Button 5", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
|
||||
{"Button 8", SDL_GAMEPAD_BUTTON_BACK}, {"Button 9", SDL_GAMEPAD_BUTTON_START},
|
||||
{"Button 10", SDL_GAMEPAD_BUTTON_LEFT_STICK},
|
||||
{"Button 11", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
|
||||
{"Button 12", SDL_GAMEPAD_BUTTON_GUIDE}, {"Button 13", SDL_GAMEPAD_BUTTON_TOUCHPAD},
|
||||
};
|
||||
return table;
|
||||
}
|
||||
|
||||
// Signed axis names: SDL axis plus the direction that counts as positive.
|
||||
struct AxisRef {
|
||||
SDL_GamepadAxis axis;
|
||||
int sign;
|
||||
};
|
||||
|
||||
const std::unordered_map<std::string, AxisRef>& AxisNames() {
|
||||
static const std::unordered_map<std::string, AxisRef> table = {
|
||||
{"Axis X-", {SDL_GAMEPAD_AXIS_LEFTX, -1}}, {"Axis X+", {SDL_GAMEPAD_AXIS_LEFTX, 1}},
|
||||
{"Axis Y-", {SDL_GAMEPAD_AXIS_LEFTY, -1}}, {"Axis Y+", {SDL_GAMEPAD_AXIS_LEFTY, 1}},
|
||||
{"Axis Z-", {SDL_GAMEPAD_AXIS_RIGHTX, -1}}, {"Axis Z+", {SDL_GAMEPAD_AXIS_RIGHTX, 1}},
|
||||
{"Axis Zr-", {SDL_GAMEPAD_AXIS_RIGHTY, -1}},{"Axis Zr+", {SDL_GAMEPAD_AXIS_RIGHTY, 1}},
|
||||
{"Left X-", {SDL_GAMEPAD_AXIS_LEFTX, -1}}, {"Left X+", {SDL_GAMEPAD_AXIS_LEFTX, 1}},
|
||||
{"Left Y-", {SDL_GAMEPAD_AXIS_LEFTY, 1}}, {"Left Y+", {SDL_GAMEPAD_AXIS_LEFTY, -1}},
|
||||
{"Right X-", {SDL_GAMEPAD_AXIS_RIGHTX, -1}},{"Right X+", {SDL_GAMEPAD_AXIS_RIGHTX, 1}},
|
||||
{"Right Y-", {SDL_GAMEPAD_AXIS_RIGHTY, 1}}, {"Right Y+", {SDL_GAMEPAD_AXIS_RIGHTY, -1}},
|
||||
{"Full Axis Xr+", {SDL_GAMEPAD_AXIS_LEFT_TRIGGER, 1}},
|
||||
{"Full Axis Yr+", {SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, 1}},
|
||||
{"Trigger L", {SDL_GAMEPAD_AXIS_LEFT_TRIGGER, 1}},
|
||||
{"Trigger R", {SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, 1}},
|
||||
};
|
||||
return table;
|
||||
}
|
||||
|
||||
double ReadInput(SDL_Gamepad* gamepad, const std::string& name) {
|
||||
if (gamepad == nullptr) {
|
||||
return 0.0;
|
||||
}
|
||||
if (const auto it = ButtonNames().find(name); it != ButtonNames().end()) {
|
||||
return SDL_GetGamepadButton(gamepad, it->second) ? 1.0 : 0.0;
|
||||
}
|
||||
if (const auto it = AxisNames().find(name); it != AxisNames().end()) {
|
||||
const double raw = SDL_GetGamepadAxis(gamepad, it->second.axis) / 32767.0;
|
||||
return std::clamp(raw * it->second.sign, 0.0, 1.0);
|
||||
}
|
||||
// Fall back to this project's own positional names, so a binding written
|
||||
// here does not have to use Dolphin vocabulary.
|
||||
if (const auto* native = ControllerNames::FindNativeButton(name)) {
|
||||
if (native->nativeButton != PAD_NATIVE_BUTTON_INVALID) {
|
||||
return SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(native->nativeButton)) ? 1.0
|
||||
: 0.0;
|
||||
}
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
SDL_Gamepad* GamepadForPort(uint32_t port) {
|
||||
const s32 index = PADGetIndexForPort(port);
|
||||
return index < 0 ? nullptr : PADGetSDLGamepadForIndex(static_cast<u32>(index));
|
||||
}
|
||||
|
||||
size_t ControlIndexForDolphinName(const std::string& name) {
|
||||
for (size_t i = 0; i < kControls.size(); ++i) {
|
||||
if (name == kControls[i].dolphinName) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return kControls.size();
|
||||
}
|
||||
|
||||
void RecomputeAnyBoundLocked() {
|
||||
g_anyBound = false;
|
||||
for (const auto& port : g_bindings) {
|
||||
for (const auto& binding : port) {
|
||||
if (!binding.expr.Empty()) {
|
||||
g_anyBound = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string ConfigKey(uint32_t port, size_t control) {
|
||||
std::string key = "expr_" + std::to_string(port + 1) + "_";
|
||||
for (const char* c = kControls[control].dolphinName; *c != '\0'; ++c) {
|
||||
key += (*c == '/' || *c == '-') ? '_' : static_cast<char>(std::tolower(*c));
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SetInputBlocked(bool blocked) noexcept {
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
g_inputBlocked = blocked;
|
||||
}
|
||||
|
||||
bool InputBlocked() noexcept {
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
return g_inputBlocked;
|
||||
}
|
||||
|
||||
void Reload() noexcept {
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
for (uint32_t port = 0; port < PAD_CHANMAX; ++port) {
|
||||
for (size_t control = 0; control < kControls.size(); ++control) {
|
||||
Binding& binding = g_bindings[port][control];
|
||||
binding = Binding{};
|
||||
binding.text = RuntimeConfigFile::ControllerExpression(ConfigKey(port, control));
|
||||
std::string error;
|
||||
if (!binding.text.empty() &&
|
||||
!InputExpr::Expression::Parse(binding.text, binding.expr, error)) {
|
||||
RT_LOG(RT_TAG_CONFIG) << "expression for port " << (port + 1) << " "
|
||||
<< kControls[control].dolphinName << ": " << error << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
RecomputeAnyBoundLocked();
|
||||
}
|
||||
|
||||
void Apply(PADStatus* statuses) noexcept {
|
||||
if (statuses == nullptr) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
if (!g_anyBound) {
|
||||
return;
|
||||
}
|
||||
const bool blocked = g_inputBlocked;
|
||||
for (uint32_t port = 0; port < PAD_CHANMAX; ++port) {
|
||||
if (statuses[port].err != PAD_ERR_NONE) {
|
||||
continue;
|
||||
}
|
||||
SDL_Gamepad* gamepad = GamepadForPort(port);
|
||||
const InputExpr::InputSource source = [gamepad](const std::string& name) {
|
||||
return ReadInput(gamepad, name);
|
||||
};
|
||||
for (size_t control = 0; control < kControls.size(); ++control) {
|
||||
Binding& binding = g_bindings[port][control];
|
||||
if (binding.expr.Empty()) {
|
||||
continue;
|
||||
}
|
||||
if (blocked) {
|
||||
binding.active = false;
|
||||
continue;
|
||||
}
|
||||
const double value = binding.expr.Evaluate(source);
|
||||
binding.active = value > InputExpr::kConditionThreshold;
|
||||
const ControlInfo& info = kControls[control];
|
||||
if (info.padButton != 0 && binding.active) {
|
||||
statuses[port].button |= info.padButton;
|
||||
}
|
||||
if (info.analog != 0) {
|
||||
const double safe = std::isfinite(value) ? std::clamp(value, 0.0, 1.0) : 0.0;
|
||||
const auto scaled = static_cast<uint8_t>(safe * 255.0);
|
||||
uint8_t& target =
|
||||
info.analog == 1 ? statuses[port].triggerLeft : statuses[port].triggerRight;
|
||||
target = std::max(target, scaled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string GetExpression(uint32_t port, size_t control) noexcept {
|
||||
if (port >= PAD_CHANMAX || control >= kControls.size()) {
|
||||
return {};
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
return g_bindings[port][control].text;
|
||||
}
|
||||
|
||||
bool SetExpression(uint32_t port, size_t control, const std::string& text, std::string& error) noexcept {
|
||||
if (port >= PAD_CHANMAX || control >= kControls.size()) {
|
||||
error = "invalid control";
|
||||
return false;
|
||||
}
|
||||
InputExpr::Expression parsed;
|
||||
if (!InputExpr::Expression::Parse(text, parsed, error)) {
|
||||
return false;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
Binding& binding = g_bindings[port][control];
|
||||
binding.text = text;
|
||||
binding.expr = std::move(parsed);
|
||||
binding.active = false;
|
||||
RecomputeAnyBoundLocked();
|
||||
}
|
||||
RuntimeConfigFile::SetControllerExpression(ConfigKey(port, control), text);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsActive(uint32_t port, size_t control) noexcept {
|
||||
if (port >= PAD_CHANMAX || control >= kControls.size()) {
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
return g_bindings[port][control].active;
|
||||
}
|
||||
|
||||
std::string DefaultDolphinConfigPath() noexcept {
|
||||
std::error_code ec;
|
||||
if (const char* appdata = std::getenv("APPDATA"); appdata != nullptr) {
|
||||
const std::filesystem::path roaming =
|
||||
std::filesystem::path(appdata) / "Dolphin Emulator" / "Config" / "GCPadNew.ini";
|
||||
if (std::filesystem::exists(roaming, ec)) {
|
||||
return RuntimeConfigFile::PathToUtf8(roaming);
|
||||
}
|
||||
}
|
||||
const auto executableDirectory = RuntimeConfigFile::ExecutableDirectory();
|
||||
return RuntimeConfigFile::PathToUtf8(executableDirectory ? *executableDirectory / "GCPadNew.ini"
|
||||
: std::filesystem::path("GCPadNew.ini"));
|
||||
}
|
||||
|
||||
int ImportDolphinConfig(const std::string& path, int padIndex, uint32_t port, std::string& summary,
|
||||
std::string& error) noexcept {
|
||||
std::vector<std::pair<std::string, std::string>> controls;
|
||||
std::string device;
|
||||
if (!InputExpr::ReadDolphinConfig(RuntimeConfigFile::PathFromUtf8(path), padIndex, controls, device,
|
||||
error)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int imported = 0;
|
||||
std::vector<std::string> skipped;
|
||||
for (const auto& [name, text] : controls) {
|
||||
const size_t control = ControlIndexForDolphinName(name);
|
||||
if (control == kControls.size()) {
|
||||
if (name.rfind("Main Stick/", 0) == 0 || name.rfind("C-Stick/", 0) == 0) {
|
||||
skipped.push_back(name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
std::string parseError;
|
||||
if (!SetExpression(port, control, text, parseError)) {
|
||||
skipped.push_back(name);
|
||||
RT_LOG(RT_TAG_CONFIG) << "import " << name << ": " << parseError << std::endl;
|
||||
continue;
|
||||
}
|
||||
++imported;
|
||||
}
|
||||
|
||||
summary = "Imported " + std::to_string(imported) + " controls";
|
||||
if (!device.empty()) {
|
||||
summary += " from " + device;
|
||||
}
|
||||
if (!skipped.empty()) {
|
||||
summary += "; skipped " + std::to_string(skipped.size()) +
|
||||
" (stick axes and unsupported inputs keep their existing mapping)";
|
||||
}
|
||||
return imported;
|
||||
}
|
||||
|
||||
} // namespace InputBindings
|
||||
@@ -0,0 +1,631 @@
|
||||
#include "input_expr.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace InputExpr {
|
||||
namespace {
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using FSec = std::chrono::duration<double>;
|
||||
|
||||
enum class Kind {
|
||||
Literal, Input, Not, Add, Sub, Mul, Div, And, Or, Xor,
|
||||
Greater, Less, Equal,
|
||||
FnIf, FnMin, FnMax, FnClamp, FnAbs, FnSqrt, FnPow, FnSin, FnCos, FnTan,
|
||||
FnDeadzone, FnTimer, FnToggle, FnHold, FnTap, FnPulse, FnSmooth, FnNot,
|
||||
};
|
||||
|
||||
struct FnInfo {
|
||||
Kind kind;
|
||||
int minArgs;
|
||||
int maxArgs;
|
||||
};
|
||||
|
||||
const std::unordered_map<std::string, FnInfo>& FunctionTable() {
|
||||
static const std::unordered_map<std::string, FnInfo> table = {
|
||||
{"not", {Kind::FnNot, 1, 1}}, {"if", {Kind::FnIf, 3, 3}},
|
||||
{"min", {Kind::FnMin, 2, 2}}, {"max", {Kind::FnMax, 2, 2}},
|
||||
{"clamp", {Kind::FnClamp, 3, 3}}, {"abs", {Kind::FnAbs, 1, 1}},
|
||||
{"sqrt", {Kind::FnSqrt, 1, 1}}, {"pow", {Kind::FnPow, 2, 2}},
|
||||
{"sin", {Kind::FnSin, 1, 1}}, {"cos", {Kind::FnCos, 1, 1}},
|
||||
{"tan", {Kind::FnTan, 1, 1}}, {"deadzone", {Kind::FnDeadzone, 2, 2}},
|
||||
{"timer", {Kind::FnTimer, 1, 1}}, {"toggle", {Kind::FnToggle, 1, 2}},
|
||||
{"hold", {Kind::FnHold, 2, 2}}, {"tap", {Kind::FnTap, 2, 3}},
|
||||
{"pulse", {Kind::FnPulse, 2, 2}}, {"smooth", {Kind::FnSmooth, 2, 3}},
|
||||
};
|
||||
return table;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct Node {
|
||||
Kind kind;
|
||||
double literal = 0.0;
|
||||
std::string input;
|
||||
std::vector<std::unique_ptr<Node>> args;
|
||||
|
||||
// Per-instance state for the stateful functions. Mutable because Evaluate
|
||||
// is logically a read of current input state.
|
||||
mutable bool released = false;
|
||||
mutable bool state = false;
|
||||
mutable unsigned taps = 0;
|
||||
mutable double value = 0.0;
|
||||
mutable Clock::time_point mark = Clock::now();
|
||||
mutable bool marked = false;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
// ---- tokenizer ----------------------------------------------------------
|
||||
|
||||
struct Token {
|
||||
enum Type { End, Input, Number, Ident, Op, LParen, RParen, Comma } type = End;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
class Lexer {
|
||||
public:
|
||||
explicit Lexer(const std::string& text) : m_text(text) {}
|
||||
|
||||
bool Next(Token& tok, std::string& error) {
|
||||
while (m_pos < m_text.size() && std::isspace(static_cast<unsigned char>(m_text[m_pos]))) {
|
||||
++m_pos;
|
||||
}
|
||||
if (m_pos >= m_text.size()) {
|
||||
tok = Token{};
|
||||
return true;
|
||||
}
|
||||
const char c = m_text[m_pos];
|
||||
if (c == '`') {
|
||||
const size_t close = m_text.find('`', m_pos + 1);
|
||||
if (close == std::string::npos) {
|
||||
error = "unterminated ` in expression";
|
||||
return false;
|
||||
}
|
||||
tok.type = Token::Input;
|
||||
tok.text = m_text.substr(m_pos + 1, close - m_pos - 1);
|
||||
m_pos = close + 1;
|
||||
return true;
|
||||
}
|
||||
if (std::isdigit(static_cast<unsigned char>(c)) || c == '.') {
|
||||
size_t end = m_pos;
|
||||
while (end < m_text.size() &&
|
||||
(std::isdigit(static_cast<unsigned char>(m_text[end])) || m_text[end] == '.')) {
|
||||
++end;
|
||||
}
|
||||
tok.type = Token::Number;
|
||||
tok.text = m_text.substr(m_pos, end - m_pos);
|
||||
m_pos = end;
|
||||
return true;
|
||||
}
|
||||
if (std::isalpha(static_cast<unsigned char>(c)) || c == '_') {
|
||||
size_t end = m_pos;
|
||||
while (end < m_text.size() &&
|
||||
(std::isalnum(static_cast<unsigned char>(m_text[end])) || m_text[end] == '_' ||
|
||||
m_text[end] == ' ')) {
|
||||
++end;
|
||||
}
|
||||
// Trailing spaces belong to the separator, not the identifier.
|
||||
while (end > m_pos && m_text[end - 1] == ' ') {
|
||||
--end;
|
||||
}
|
||||
tok.type = Token::Ident;
|
||||
tok.text = m_text.substr(m_pos, end - m_pos);
|
||||
m_pos = end;
|
||||
return true;
|
||||
}
|
||||
if (c == '(') { tok.type = Token::LParen; ++m_pos; return true; }
|
||||
if (c == ')') { tok.type = Token::RParen; ++m_pos; return true; }
|
||||
if (c == ',') { tok.type = Token::Comma; ++m_pos; return true; }
|
||||
if (std::string("!&|^+-*/><=").find(c) != std::string::npos) {
|
||||
tok.type = Token::Op;
|
||||
tok.text = std::string(1, c);
|
||||
++m_pos;
|
||||
return true;
|
||||
}
|
||||
error = std::string("unexpected character '") + c + "' in expression";
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t Position() const { return m_pos; }
|
||||
|
||||
private:
|
||||
const std::string& m_text;
|
||||
size_t m_pos = 0;
|
||||
};
|
||||
|
||||
// ---- parser -------------------------------------------------------------
|
||||
|
||||
using NodePtr = std::unique_ptr<Node>;
|
||||
|
||||
class Parser {
|
||||
public:
|
||||
explicit Parser(const std::string& text) : m_lexer(text) { Advance(); }
|
||||
|
||||
NodePtr ParseExpression(std::string& error) {
|
||||
NodePtr node = ParseBinary(0, error);
|
||||
if (!node) {
|
||||
return nullptr;
|
||||
}
|
||||
if (m_failed) {
|
||||
error = m_lexError;
|
||||
return nullptr;
|
||||
}
|
||||
if (m_tok.type != Token::End) {
|
||||
error = "unexpected trailing input in expression";
|
||||
return nullptr;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
private:
|
||||
void Advance() {
|
||||
if (!m_lexer.Next(m_tok, m_lexError)) {
|
||||
m_tok = Token{};
|
||||
m_failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
static int Precedence(const std::string& op) {
|
||||
if (op == "|") return 1;
|
||||
if (op == "^") return 2;
|
||||
if (op == "&") return 3;
|
||||
if (op == ">" || op == "<" || op == "=") return 4;
|
||||
if (op == "+" || op == "-") return 5;
|
||||
if (op == "*" || op == "/") return 6;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static Kind BinaryKind(const std::string& op) {
|
||||
if (op == "|") return Kind::Or;
|
||||
if (op == "^") return Kind::Xor;
|
||||
if (op == "&") return Kind::And;
|
||||
if (op == ">") return Kind::Greater;
|
||||
if (op == "<") return Kind::Less;
|
||||
if (op == "=") return Kind::Equal;
|
||||
if (op == "+") return Kind::Add;
|
||||
if (op == "-") return Kind::Sub;
|
||||
if (op == "*") return Kind::Mul;
|
||||
return Kind::Div;
|
||||
}
|
||||
|
||||
NodePtr ParseBinary(int minPrec, std::string& error) {
|
||||
NodePtr lhs = ParseUnary(error);
|
||||
if (!lhs) {
|
||||
return nullptr;
|
||||
}
|
||||
while (m_tok.type == Token::Op) {
|
||||
const int prec = Precedence(m_tok.text);
|
||||
if (prec < 0 || prec < minPrec) {
|
||||
break;
|
||||
}
|
||||
const std::string op = m_tok.text;
|
||||
Advance();
|
||||
NodePtr rhs = ParseBinary(prec + 1, error);
|
||||
if (!rhs) {
|
||||
return nullptr;
|
||||
}
|
||||
auto node = std::make_unique<Node>();
|
||||
node->kind = BinaryKind(op);
|
||||
node->args.push_back(std::move(lhs));
|
||||
node->args.push_back(std::move(rhs));
|
||||
lhs = std::move(node);
|
||||
}
|
||||
return lhs;
|
||||
}
|
||||
|
||||
NodePtr ParseUnary(std::string& error) {
|
||||
if (m_failed) {
|
||||
error = m_lexError;
|
||||
return nullptr;
|
||||
}
|
||||
if (m_tok.type == Token::Op && (m_tok.text == "!" || m_tok.text == "-" || m_tok.text == "+")) {
|
||||
const std::string op = m_tok.text;
|
||||
Advance();
|
||||
NodePtr inner = ParseUnary(error);
|
||||
if (!inner) {
|
||||
return nullptr;
|
||||
}
|
||||
if (op == "+") {
|
||||
return inner;
|
||||
}
|
||||
auto node = std::make_unique<Node>();
|
||||
if (op == "!") {
|
||||
node->kind = Kind::Not;
|
||||
node->args.push_back(std::move(inner));
|
||||
} else {
|
||||
node->kind = Kind::Sub;
|
||||
auto zero = std::make_unique<Node>();
|
||||
zero->kind = Kind::Literal;
|
||||
node->args.push_back(std::move(zero));
|
||||
node->args.push_back(std::move(inner));
|
||||
}
|
||||
return node;
|
||||
}
|
||||
return ParsePrimary(error);
|
||||
}
|
||||
|
||||
NodePtr ParsePrimary(std::string& error) {
|
||||
if (m_failed) {
|
||||
error = m_lexError;
|
||||
return nullptr;
|
||||
}
|
||||
switch (m_tok.type) {
|
||||
case Token::Input: {
|
||||
auto node = std::make_unique<Node>();
|
||||
node->kind = Kind::Input;
|
||||
node->input = m_tok.text;
|
||||
Advance();
|
||||
return node;
|
||||
}
|
||||
case Token::Number: {
|
||||
auto node = std::make_unique<Node>();
|
||||
node->kind = Kind::Literal;
|
||||
node->literal = std::strtod(m_tok.text.c_str(), nullptr);
|
||||
Advance();
|
||||
return node;
|
||||
}
|
||||
case Token::LParen: {
|
||||
Advance();
|
||||
NodePtr inner = ParseBinary(0, error);
|
||||
if (!inner) {
|
||||
return nullptr;
|
||||
}
|
||||
if (m_tok.type != Token::RParen) {
|
||||
error = "expected closing paren";
|
||||
return nullptr;
|
||||
}
|
||||
Advance();
|
||||
return inner;
|
||||
}
|
||||
case Token::Ident: {
|
||||
const std::string name = m_tok.text;
|
||||
Advance();
|
||||
if (m_tok.type != Token::LParen) {
|
||||
// A bare identifier is an input name, as Dolphin allows for
|
||||
// simple cases such as "Start" or "LSHIFT".
|
||||
auto node = std::make_unique<Node>();
|
||||
node->kind = Kind::Input;
|
||||
node->input = name;
|
||||
return node;
|
||||
}
|
||||
const auto it = FunctionTable().find(name);
|
||||
if (it == FunctionTable().end()) {
|
||||
error = "unknown function '" + name + "'";
|
||||
return nullptr;
|
||||
}
|
||||
Advance();
|
||||
auto node = std::make_unique<Node>();
|
||||
node->kind = it->second.kind;
|
||||
if (m_tok.type != Token::RParen) {
|
||||
while (true) {
|
||||
NodePtr arg = ParseBinary(0, error);
|
||||
if (!arg) {
|
||||
return nullptr;
|
||||
}
|
||||
node->args.push_back(std::move(arg));
|
||||
if (m_tok.type != Token::Comma) {
|
||||
break;
|
||||
}
|
||||
Advance();
|
||||
}
|
||||
}
|
||||
if (m_tok.type != Token::RParen) {
|
||||
error = "expected closing paren after " + name + " arguments";
|
||||
return nullptr;
|
||||
}
|
||||
Advance();
|
||||
const int count = static_cast<int>(node->args.size());
|
||||
if (count < it->second.minArgs || count > it->second.maxArgs) {
|
||||
error = name + " takes " + std::to_string(it->second.minArgs) + " to " +
|
||||
std::to_string(it->second.maxArgs) + " arguments";
|
||||
return nullptr;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
default:
|
||||
error = "expected start of expression";
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Lexer m_lexer;
|
||||
Token m_tok;
|
||||
std::string m_lexError;
|
||||
bool m_failed = false;
|
||||
};
|
||||
|
||||
// ---- evaluator ----------------------------------------------------------
|
||||
|
||||
double Eval(const Node& node, const InputSource& source);
|
||||
|
||||
double Arg(const Node& node, size_t index, const InputSource& source) {
|
||||
return Eval(*node.args[index], source);
|
||||
}
|
||||
|
||||
double Eval(const Node& node, const InputSource& source) {
|
||||
switch (node.kind) {
|
||||
case Kind::Literal: return node.literal;
|
||||
case Kind::Input: return source ? source(node.input) : 0.0;
|
||||
case Kind::Not:
|
||||
case Kind::FnNot: return 1.0 - Arg(node, 0, source);
|
||||
case Kind::Add: return Arg(node, 0, source) + Arg(node, 1, source);
|
||||
case Kind::Sub: return Arg(node, 0, source) - Arg(node, 1, source);
|
||||
case Kind::Mul: return Arg(node, 0, source) * Arg(node, 1, source);
|
||||
case Kind::Div: {
|
||||
// Both sides are evaluated even when the divisor is zero: the left
|
||||
// subtree may hold stateful functions that need their frame update.
|
||||
const double lhs = Arg(node, 0, source);
|
||||
const double rhs = Arg(node, 1, source);
|
||||
return rhs == 0.0 ? 0.0 : lhs / rhs;
|
||||
}
|
||||
case Kind::And: return std::min(Arg(node, 0, source), Arg(node, 1, source));
|
||||
case Kind::Or: return std::max(Arg(node, 0, source), Arg(node, 1, source));
|
||||
case Kind::Xor: {
|
||||
const double a = Arg(node, 0, source);
|
||||
const double b = Arg(node, 1, source);
|
||||
return std::max(std::min(a, 1.0 - b), std::min(b, 1.0 - a));
|
||||
}
|
||||
case Kind::Greater: return Arg(node, 0, source) > Arg(node, 1, source) ? 1.0 : 0.0;
|
||||
case Kind::Less: return Arg(node, 0, source) < Arg(node, 1, source) ? 1.0 : 0.0;
|
||||
case Kind::Equal: return Arg(node, 0, source) == Arg(node, 1, source) ? 1.0 : 0.0;
|
||||
case Kind::FnIf:
|
||||
return Arg(node, 0, source) > kConditionThreshold ? Arg(node, 1, source) : Arg(node, 2, source);
|
||||
case Kind::FnMin: return std::min(Arg(node, 0, source), Arg(node, 1, source));
|
||||
case Kind::FnMax: return std::max(Arg(node, 0, source), Arg(node, 1, source));
|
||||
case Kind::FnClamp: {
|
||||
const double v = Arg(node, 0, source);
|
||||
double lo = Arg(node, 1, source);
|
||||
double hi = Arg(node, 2, source);
|
||||
if (lo > hi) {
|
||||
std::swap(lo, hi);
|
||||
}
|
||||
return std::clamp(v, lo, hi);
|
||||
}
|
||||
case Kind::FnAbs: return std::abs(Arg(node, 0, source));
|
||||
case Kind::FnSqrt: return std::sqrt(Arg(node, 0, source));
|
||||
case Kind::FnPow: return std::pow(Arg(node, 0, source), Arg(node, 1, source));
|
||||
case Kind::FnSin: return std::sin(Arg(node, 0, source));
|
||||
case Kind::FnCos: return std::cos(Arg(node, 0, source));
|
||||
case Kind::FnTan: return std::tan(Arg(node, 0, source));
|
||||
case Kind::FnDeadzone: {
|
||||
const double v = Arg(node, 0, source);
|
||||
const double dz = std::clamp(Arg(node, 1, source), 0.0, 0.999);
|
||||
return std::copysign(std::max(0.0, std::abs(v) - dz) / (1.0 - dz), v);
|
||||
}
|
||||
case Kind::FnTimer: {
|
||||
const auto now = Clock::now();
|
||||
if (!node.marked) {
|
||||
node.mark = now;
|
||||
node.marked = true;
|
||||
}
|
||||
const double period = Arg(node, 0, source);
|
||||
double progress = std::chrono::duration_cast<FSec>(now - node.mark).count() / period;
|
||||
if (!std::isfinite(progress) || progress < 0.0) {
|
||||
progress = 0.0;
|
||||
node.mark = now;
|
||||
} else if (progress >= 1.0) {
|
||||
const double resets = std::floor(progress);
|
||||
node.mark += std::chrono::duration_cast<Clock::duration>(FSec(period * resets));
|
||||
progress -= resets;
|
||||
}
|
||||
return progress;
|
||||
}
|
||||
case Kind::FnToggle: {
|
||||
const double inner = Arg(node, 0, source);
|
||||
if (inner < kConditionThreshold) {
|
||||
node.released = true;
|
||||
} else if (node.released) {
|
||||
node.released = false;
|
||||
node.state = !node.state;
|
||||
}
|
||||
if (node.args.size() == 2 && Arg(node, 1, source) > kConditionThreshold) {
|
||||
node.state = false;
|
||||
}
|
||||
return node.state ? 1.0 : 0.0;
|
||||
}
|
||||
case Kind::FnHold: {
|
||||
const auto now = Clock::now();
|
||||
if (!node.marked) {
|
||||
node.mark = now;
|
||||
node.marked = true;
|
||||
}
|
||||
const double input = Arg(node, 0, source);
|
||||
if (input < kConditionThreshold) {
|
||||
node.state = false;
|
||||
node.mark = now;
|
||||
} else if (!node.state) {
|
||||
if (std::chrono::duration_cast<FSec>(now - node.mark).count() >= Arg(node, 1, source)) {
|
||||
node.state = true;
|
||||
}
|
||||
}
|
||||
return node.state ? 1.0 : 0.0;
|
||||
}
|
||||
case Kind::FnTap: {
|
||||
const auto now = Clock::now();
|
||||
if (!node.marked) {
|
||||
node.mark = now;
|
||||
node.marked = true;
|
||||
}
|
||||
const double elapsed = std::chrono::duration_cast<FSec>(now - node.mark).count();
|
||||
const double input = Arg(node, 0, source);
|
||||
const bool timeUp = elapsed > Arg(node, 1, source);
|
||||
// The count is user authored, so a negative or huge value must not
|
||||
// reach the unsigned conversion.
|
||||
double requested = node.args.size() == 3 ? Arg(node, 2, source) : 2.0;
|
||||
if (!std::isfinite(requested)) {
|
||||
requested = 2.0;
|
||||
}
|
||||
const auto desired = static_cast<unsigned>(std::clamp(requested + 0.5, 1.0, 64.0));
|
||||
if (input < kConditionThreshold) {
|
||||
node.released = true;
|
||||
if (node.taps > 0 && timeUp) {
|
||||
node.taps = 0;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
if (node.released) {
|
||||
if (node.taps == 0) {
|
||||
node.mark = now;
|
||||
}
|
||||
++node.taps;
|
||||
node.released = false;
|
||||
}
|
||||
return desired == node.taps ? 1.0 : 0.0;
|
||||
}
|
||||
case Kind::FnPulse: {
|
||||
const auto now = Clock::now();
|
||||
const double input = Arg(node, 0, source);
|
||||
if (input < kConditionThreshold) {
|
||||
node.released = true;
|
||||
} else if (node.released) {
|
||||
node.released = false;
|
||||
const double requested = Arg(node, 1, source);
|
||||
const double safe = std::isfinite(requested) ? std::clamp(requested, 0.0, 3600.0) : 0.0;
|
||||
const auto seconds = std::chrono::duration_cast<Clock::duration>(FSec(safe));
|
||||
if (node.state) {
|
||||
node.mark += seconds;
|
||||
} else {
|
||||
node.state = true;
|
||||
node.mark = now + seconds;
|
||||
}
|
||||
}
|
||||
if (node.state && now >= node.mark) {
|
||||
node.state = false;
|
||||
}
|
||||
return node.state ? 1.0 : 0.0;
|
||||
}
|
||||
case Kind::FnSmooth: {
|
||||
const auto now = Clock::now();
|
||||
if (!node.marked) {
|
||||
node.mark = now;
|
||||
node.marked = true;
|
||||
}
|
||||
const double elapsed = std::chrono::duration_cast<FSec>(now - node.mark).count();
|
||||
node.mark = now;
|
||||
const double desired = Arg(node, 0, source);
|
||||
const double up = Arg(node, 1, source);
|
||||
const double down = node.args.size() == 3 ? Arg(node, 2, source) : up;
|
||||
const double rate = (desired < node.value) ? down : up;
|
||||
const double maxMove = elapsed / rate;
|
||||
if (!std::isfinite(maxMove)) {
|
||||
node.value = desired;
|
||||
} else {
|
||||
const double diff = desired - node.value;
|
||||
node.value += std::copysign(std::min(maxMove, std::abs(diff)), diff);
|
||||
}
|
||||
return node.value;
|
||||
}
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
void Collect(const Node& node, std::vector<std::string>& out) {
|
||||
if (node.kind == Kind::Input) {
|
||||
if (std::find(out.begin(), out.end(), node.input) == out.end()) {
|
||||
out.push_back(node.input);
|
||||
}
|
||||
}
|
||||
for (const auto& arg : node.args) {
|
||||
Collect(*arg, out);
|
||||
}
|
||||
}
|
||||
|
||||
std::string Trim(const std::string& text) {
|
||||
const size_t begin = text.find_first_not_of(" \t\r\n");
|
||||
if (begin == std::string::npos) {
|
||||
return {};
|
||||
}
|
||||
return text.substr(begin, text.find_last_not_of(" \t\r\n") - begin + 1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Expression::Expression() = default;
|
||||
Expression::~Expression() = default;
|
||||
Expression::Expression(Expression&&) noexcept = default;
|
||||
Expression& Expression::operator=(Expression&&) noexcept = default;
|
||||
|
||||
bool Expression::Parse(const std::string& text, Expression& out, std::string& error) {
|
||||
out.m_root.reset();
|
||||
if (Trim(text).empty()) {
|
||||
return true;
|
||||
}
|
||||
Parser parser(text);
|
||||
NodePtr root = parser.ParseExpression(error);
|
||||
if (!root) {
|
||||
return false;
|
||||
}
|
||||
out.m_root = std::move(root);
|
||||
return true;
|
||||
}
|
||||
|
||||
double Expression::Evaluate(const InputSource& source) const {
|
||||
if (m_root == nullptr) {
|
||||
return 0.0;
|
||||
}
|
||||
const double value = Eval(*m_root, source);
|
||||
return std::isfinite(value) ? value : 0.0;
|
||||
}
|
||||
|
||||
std::vector<std::string> Expression::ReferencedInputs() const {
|
||||
std::vector<std::string> out;
|
||||
if (m_root) {
|
||||
Collect(*m_root, out);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool ReadDolphinConfig(const std::filesystem::path& path, int padIndex,
|
||||
std::vector<std::pair<std::string, std::string>>& controls,
|
||||
std::string& deviceName, std::string& error) {
|
||||
std::ifstream file(path);
|
||||
if (!file) {
|
||||
error = "could not open " + path.string();
|
||||
return false;
|
||||
}
|
||||
const std::string wanted = "[GCPad" + std::to_string(padIndex) + "]";
|
||||
bool inSection = false;
|
||||
bool found = false;
|
||||
std::string line;
|
||||
controls.clear();
|
||||
deviceName.clear();
|
||||
while (std::getline(file, line)) {
|
||||
const std::string trimmed = Trim(line);
|
||||
if (trimmed.empty() || trimmed[0] == '#' || trimmed[0] == ';') {
|
||||
continue;
|
||||
}
|
||||
if (trimmed.front() == '[') {
|
||||
inSection = trimmed == wanted;
|
||||
found = found || inSection;
|
||||
continue;
|
||||
}
|
||||
if (!inSection) {
|
||||
continue;
|
||||
}
|
||||
const size_t eq = trimmed.find('=');
|
||||
if (eq == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
const std::string key = Trim(trimmed.substr(0, eq));
|
||||
const std::string value = Trim(trimmed.substr(eq + 1));
|
||||
if (key == "Device") {
|
||||
deviceName = value;
|
||||
} else if (!value.empty()) {
|
||||
controls.emplace_back(key, value);
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
error = wanted + " not found in " + path.string();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace InputExpr
|
||||
+60
-2
@@ -23,6 +23,10 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32)
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
@@ -38,7 +42,12 @@
|
||||
#include <dbghelp.h>
|
||||
#else
|
||||
#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>
|
||||
#endif
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
@@ -49,6 +58,8 @@
|
||||
#include "system_bridge.h"
|
||||
#include "ppc_runtime.h"
|
||||
#include "aurora_events.h"
|
||||
#include "wii_remote_input.h"
|
||||
#include "discord_presence.h"
|
||||
#include "fiber_manager.h"
|
||||
#include "hle_stubs.h"
|
||||
#include "runtime_config.h"
|
||||
@@ -921,6 +932,7 @@ constexpr DWORD kCppExceptionCodeMsvc = 0xE06D7363;
|
||||
// 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.
|
||||
constexpr DWORD kAsanFatalAppExit = 0x40000015; // STATUS_FATAL_APP_EXIT
|
||||
LONG ReportFatalSehAndExit(EXCEPTION_POINTERS* info);
|
||||
|
||||
void ReportStructuredException(EXCEPTION_POINTERS* info) {
|
||||
if (!info || !info->ExceptionRecord) {
|
||||
@@ -1022,13 +1034,34 @@ LONG CALLBACK SehLogger(EXCEPTION_POINTERS* info) {
|
||||
info->ExceptionRecord->ExceptionCode == kAsanFatalAppExit) { // ASan reporting - let it print first
|
||||
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
|
||||
static std::atomic_flag s_inCrashHandler = ATOMIC_FLAG_INIT;
|
||||
if (s_inCrashHandler.test_and_set()) {
|
||||
std::_Exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
|
||||
// Report the structured exception with detailed information
|
||||
ReportStructuredException(info);
|
||||
const auto* record = info->ExceptionRecord;
|
||||
@@ -1063,6 +1096,7 @@ LONG CALLBACK SehLogger(EXCEPTION_POINTERS* info) {
|
||||
void InstallSehLogger() {
|
||||
if (!g_vectoredSehHandle) {
|
||||
g_vectoredSehHandle = AddVectoredExceptionHandler(1, SehLogger);
|
||||
SetUnhandledExceptionFilter(UnhandledSehFilter);
|
||||
}
|
||||
}
|
||||
#else
|
||||
@@ -1268,6 +1302,7 @@ static void TerminateHandler() {
|
||||
std::_Exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// Runtime entry point: loads the configuration, brings up aurora and runs the game.
|
||||
int RuntimeMain(int argc, char** argv) {
|
||||
// Must run before the transcript duplicates stdout/stderr: it decides what
|
||||
// those descriptors are mirrored to now that the products are GUI-subsystem.
|
||||
@@ -1292,6 +1327,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.");
|
||||
}
|
||||
RuntimeConfigFile::LogLoadedConfig();
|
||||
if (RuntimeConfigFile::DiscordPresenceEnabled()) {
|
||||
DiscordPresence::Initialize(RuntimeConfigFile::DiscordClientId(), "Mario Kart Wii");
|
||||
}
|
||||
SystemBridge::Initialize();
|
||||
TranslatedFunctionRegistry::Finalize();
|
||||
|
||||
@@ -1343,9 +1381,21 @@ int RuntimeMain(int argc, char** argv) {
|
||||
const char* configName;
|
||||
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{{
|
||||
{"auto", BACKEND_AUTO}, {"d3d12", BACKEND_D3D12}, {"vulkan", BACKEND_VULKAN},
|
||||
}};
|
||||
|
||||
#endif
|
||||
const auto backendDisplayName = [](AuroraBackend value) -> const char* {
|
||||
for (const auto& entry : kGraphicsBackends) {
|
||||
if (entry.backend == value) {
|
||||
@@ -1364,6 +1414,11 @@ int RuntimeMain(int argc, char** argv) {
|
||||
}
|
||||
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);
|
||||
if (requestedBackend != BACKEND_AUTO && auroraInfo.backend != requestedBackend) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "graphics_api=\"" << backend
|
||||
@@ -1418,6 +1473,7 @@ int RuntimeMain(int argc, char** argv) {
|
||||
Fiber::GuestFiberManager::Shutdown();
|
||||
WindowPlacementPersistence::Flush(true);
|
||||
aurora_shutdown();
|
||||
DiscordPresence::Shutdown();
|
||||
SetRuntimeExitCodeImpl(0);
|
||||
ShutdownProcessTranscript();
|
||||
return 0;
|
||||
@@ -1435,6 +1491,7 @@ int RuntimeMain(int argc, char** argv) {
|
||||
Fiber::GuestFiberManager::Shutdown();
|
||||
WindowPlacementPersistence::Flush(true);
|
||||
aurora_shutdown();
|
||||
DiscordPresence::Shutdown();
|
||||
ShutdownProcessTranscript();
|
||||
return 1;
|
||||
} catch (const std::exception& ex) {
|
||||
@@ -1446,6 +1503,7 @@ int RuntimeMain(int argc, char** argv) {
|
||||
Fiber::GuestFiberManager::Shutdown();
|
||||
WindowPlacementPersistence::Flush(true);
|
||||
aurora_shutdown();
|
||||
DiscordPresence::Shutdown();
|
||||
ShutdownProcessTranscript();
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
#include <winrt/Windows.Media.Control.h>
|
||||
#elif defined(__linux__)
|
||||
#include <dlfcn.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#endif
|
||||
|
||||
namespace MusicAttenuation {
|
||||
@@ -191,6 +196,256 @@ void MonitorWindowsMediaSessions() noexcept {
|
||||
<< error.message().c_str() << std::endl;
|
||||
}
|
||||
}
|
||||
#elif defined(__linux__)
|
||||
|
||||
// For linux - watches MPRIS players on the D-Bus session bus and drives the
|
||||
// same g_externalMediaPlaying / g_mediaControl* atomics.
|
||||
//
|
||||
// libdbus-1 is loaded with dlopen rather than linked so a machine with no
|
||||
// session bus (headless, D-Bus absent, some containers) can still run the game,
|
||||
// here, the monitor reports "unavailable" like the Windows does
|
||||
// when the media-session API is missing.
|
||||
|
||||
extern "C" {
|
||||
|
||||
struct MprisDBusConnection;
|
||||
struct MprisDBusMessage;
|
||||
|
||||
// Mirrors DBusError from dbus-errors.h (frozen public layout).
|
||||
struct MprisDBusError {
|
||||
const char* name;
|
||||
const char* message;
|
||||
unsigned int dummy1 : 1;
|
||||
unsigned int dummy2 : 1;
|
||||
unsigned int dummy3 : 1;
|
||||
unsigned int dummy4 : 1;
|
||||
unsigned int dummy5 : 1;
|
||||
void* padding1;
|
||||
};
|
||||
|
||||
// Mirrors DBusMessageIter from dbus-message.h
|
||||
struct MprisDBusIter {
|
||||
void* dummy1;
|
||||
void* dummy2;
|
||||
uint32_t dummy3;
|
||||
int dummy4;
|
||||
int dummy5;
|
||||
int dummy6;
|
||||
int dummy7;
|
||||
int dummy8;
|
||||
int dummy9;
|
||||
int dummy10;
|
||||
int dummy11;
|
||||
int pad1;
|
||||
int pad2;
|
||||
void* pad3;
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
|
||||
constexpr int kMprisBusSession = 0; // DBUS_BUS_SESSION
|
||||
constexpr int kMprisTypeInvalid = 0; // DBUS_TYPE_INVALID
|
||||
constexpr int kMprisTypeString = 's'; // DBUS_TYPE_STRING
|
||||
constexpr int kMprisTypeArray = 'a'; // DBUS_TYPE_ARRAY
|
||||
constexpr int kMprisTypeVariant = 'v';// DBUS_TYPE_VARIANT
|
||||
// Short blocking timeout so a wedged player can never stall the poll thread for
|
||||
// the full libdbus default (25s).
|
||||
constexpr int kMprisSendTimeoutMs = 500;
|
||||
constexpr char kMprisNamePrefix[] = "org.mpris.MediaPlayer2.";
|
||||
|
||||
struct MprisDBus {
|
||||
void* handle = nullptr;
|
||||
|
||||
MprisDBusConnection* (*bus_get_private)(int, MprisDBusError*) = nullptr;
|
||||
void (*connection_close)(MprisDBusConnection*) = nullptr;
|
||||
void (*connection_unref)(MprisDBusConnection*) = nullptr;
|
||||
void (*connection_set_exit_on_disconnect)(MprisDBusConnection*, unsigned int) = nullptr;
|
||||
void (*error_init)(MprisDBusError*) = nullptr;
|
||||
void (*error_free)(MprisDBusError*) = nullptr;
|
||||
MprisDBusMessage* (*message_new_method_call)(const char*, const char*, const char*,
|
||||
const char*) = nullptr;
|
||||
unsigned int (*message_append_args)(MprisDBusMessage*, int, ...) = nullptr;
|
||||
MprisDBusMessage* (*send_with_reply_and_block)(MprisDBusConnection*, MprisDBusMessage*, int,
|
||||
MprisDBusError*) = nullptr;
|
||||
void (*message_unref)(MprisDBusMessage*) = nullptr;
|
||||
unsigned int (*message_iter_init)(MprisDBusMessage*, MprisDBusIter*) = nullptr;
|
||||
unsigned int (*message_iter_next)(MprisDBusIter*) = nullptr;
|
||||
void (*message_iter_recurse)(MprisDBusIter*, MprisDBusIter*) = nullptr;
|
||||
int (*message_iter_get_arg_type)(MprisDBusIter*) = nullptr;
|
||||
void (*message_iter_get_basic)(MprisDBusIter*, void*) = nullptr;
|
||||
|
||||
template <typename Fn>
|
||||
bool Bind(Fn& fn, const char* symbol) noexcept {
|
||||
fn = reinterpret_cast<Fn>(dlsym(handle, symbol));
|
||||
return fn != nullptr;
|
||||
}
|
||||
|
||||
bool Load() noexcept {
|
||||
handle = dlopen("libdbus-1.so.3", RTLD_NOW | RTLD_LOCAL);
|
||||
if (handle == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const bool ok =
|
||||
Bind(bus_get_private, "dbus_bus_get_private") &&
|
||||
Bind(connection_close, "dbus_connection_close") &&
|
||||
Bind(connection_unref, "dbus_connection_unref") &&
|
||||
Bind(connection_set_exit_on_disconnect, "dbus_connection_set_exit_on_disconnect") &&
|
||||
Bind(error_init, "dbus_error_init") &&
|
||||
Bind(error_free, "dbus_error_free") &&
|
||||
Bind(message_new_method_call, "dbus_message_new_method_call") &&
|
||||
Bind(message_append_args, "dbus_message_append_args") &&
|
||||
Bind(send_with_reply_and_block, "dbus_connection_send_with_reply_and_block") &&
|
||||
Bind(message_unref, "dbus_message_unref") &&
|
||||
Bind(message_iter_init, "dbus_message_iter_init") &&
|
||||
Bind(message_iter_next, "dbus_message_iter_next") &&
|
||||
Bind(message_iter_recurse, "dbus_message_iter_recurse") &&
|
||||
Bind(message_iter_get_arg_type, "dbus_message_iter_get_arg_type") &&
|
||||
Bind(message_iter_get_basic, "dbus_message_iter_get_basic");
|
||||
if (!ok) {
|
||||
dlclose(handle);
|
||||
handle = nullptr;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// org.freedesktop.DBus.ListNames
|
||||
bool ListNames(MprisDBusConnection* conn, std::vector<std::string>& out) noexcept {
|
||||
MprisDBusMessage* msg = message_new_method_call(
|
||||
"org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames");
|
||||
if (msg == nullptr) {
|
||||
return false;
|
||||
}
|
||||
MprisDBusError err;
|
||||
error_init(&err);
|
||||
MprisDBusMessage* reply = send_with_reply_and_block(conn, msg, kMprisSendTimeoutMs, &err);
|
||||
message_unref(msg);
|
||||
if (reply == nullptr) {
|
||||
error_free(&err);
|
||||
return false;
|
||||
}
|
||||
error_free(&err);
|
||||
|
||||
MprisDBusIter iter;
|
||||
MprisDBusIter sub;
|
||||
if (message_iter_init(reply, &iter) &&
|
||||
message_iter_get_arg_type(&iter) == kMprisTypeArray) {
|
||||
message_iter_recurse(&iter, &sub);
|
||||
while (message_iter_get_arg_type(&sub) == kMprisTypeString) {
|
||||
const char* name = nullptr;
|
||||
message_iter_get_basic(&sub, &name);
|
||||
if (name != nullptr) {
|
||||
out.emplace_back(name);
|
||||
}
|
||||
message_iter_next(&sub);
|
||||
}
|
||||
}
|
||||
message_unref(reply);
|
||||
return true;
|
||||
}
|
||||
|
||||
// org.freedesktop.DBus.Properties.Get(Player, "PlaybackStatus") for one
|
||||
// MPRIS player - the reply is a variant wrapping a string (eg: "Playing").
|
||||
bool PlaybackStatus(MprisDBusConnection* conn, const char* busName, std::string& out) noexcept {
|
||||
MprisDBusMessage* msg = message_new_method_call(
|
||||
busName, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get");
|
||||
if (msg == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const char* iface = "org.mpris.MediaPlayer2.Player";
|
||||
const char* prop = "PlaybackStatus";
|
||||
if (!message_append_args(msg, kMprisTypeString, &iface, kMprisTypeString, &prop,
|
||||
kMprisTypeInvalid)) {
|
||||
message_unref(msg);
|
||||
return false;
|
||||
}
|
||||
MprisDBusError err;
|
||||
error_init(&err);
|
||||
MprisDBusMessage* reply = send_with_reply_and_block(conn, msg, kMprisSendTimeoutMs, &err);
|
||||
message_unref(msg);
|
||||
if (reply == nullptr) {
|
||||
error_free(&err);
|
||||
return false;
|
||||
}
|
||||
error_free(&err);
|
||||
|
||||
bool haveValue = false;
|
||||
MprisDBusIter iter;
|
||||
MprisDBusIter variant;
|
||||
if (message_iter_init(reply, &iter) &&
|
||||
message_iter_get_arg_type(&iter) == kMprisTypeVariant) {
|
||||
message_iter_recurse(&iter, &variant);
|
||||
if (message_iter_get_arg_type(&variant) == kMprisTypeString) {
|
||||
const char* value = nullptr;
|
||||
message_iter_get_basic(&variant, &value);
|
||||
if (value != nullptr) {
|
||||
out.assign(value);
|
||||
haveValue = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
message_unref(reply);
|
||||
return haveValue;
|
||||
}
|
||||
};
|
||||
|
||||
void MonitorLinuxMprisSessions() noexcept {
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
MprisDBus dbus;
|
||||
if (!dbus.Load()) {
|
||||
g_mediaControlAvailable.store(false, std::memory_order_release);
|
||||
g_externalMediaPlaying.store(false, std::memory_order_release);
|
||||
g_mediaControlInitializationComplete.store(true, std::memory_order_release);
|
||||
std::cerr << "[audio] MPRIS media monitoring unavailable: libdbus-1 could not be loaded"
|
||||
<< std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
MprisDBusError err;
|
||||
dbus.error_init(&err);
|
||||
MprisDBusConnection* conn = dbus.bus_get_private(kMprisBusSession, &err);
|
||||
if (conn == nullptr) {
|
||||
g_mediaControlAvailable.store(false, std::memory_order_release);
|
||||
g_externalMediaPlaying.store(false, std::memory_order_release);
|
||||
g_mediaControlInitializationComplete.store(true, std::memory_order_release);
|
||||
std::cerr << "[audio] MPRIS media monitoring unavailable: "
|
||||
<< (err.message != nullptr ? err.message : "no D-Bus session bus") << std::endl;
|
||||
dbus.error_free(&err);
|
||||
return;
|
||||
}
|
||||
dbus.error_free(&err);
|
||||
// A private bus connection defaults to terminating the process when the bus drops.
|
||||
dbus.connection_set_exit_on_disconnect(conn, 0u);
|
||||
|
||||
for (bool firstPoll = true;; firstPoll = false) {
|
||||
bool playing = false;
|
||||
std::vector<std::string> names;
|
||||
const bool queried = dbus.ListNames(conn, names);
|
||||
if (queried) {
|
||||
for (const auto& name : names) {
|
||||
if (name.compare(0, sizeof(kMprisNamePrefix) - 1, kMprisNamePrefix) != 0) {
|
||||
continue;
|
||||
}
|
||||
std::string status;
|
||||
if (dbus.PlaybackStatus(conn, name.c_str(), status) && status == "Playing") {
|
||||
playing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
g_externalMediaPlaying.store(playing, std::memory_order_release);
|
||||
g_mediaControlAvailable.store(queried, std::memory_order_release);
|
||||
if (firstPoll) {
|
||||
g_mediaControlInitializationComplete.store(true, std::memory_order_release);
|
||||
if (!queried) {
|
||||
std::cerr << "[audio] MPRIS media monitoring unavailable: session bus query failed"
|
||||
<< std::endl;
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(250ms);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void StartMonitor() noexcept {
|
||||
@@ -198,6 +453,9 @@ void StartMonitor() noexcept {
|
||||
// The process owns this monitor for its remaining lifetime. Keeping it
|
||||
// detached avoids shutdown ordering between WinRT and static audio state.
|
||||
std::thread(MonitorWindowsMediaSessions).detach();
|
||||
#elif defined(__linux__)
|
||||
// Detached so there is no shutdown ordering to manage against static audio state.
|
||||
std::thread(MonitorLinuxMprisSessions).detach();
|
||||
#else
|
||||
g_mediaControlAvailable.store(false, std::memory_order_release);
|
||||
g_mediaControlInitializationComplete.store(true, std::memory_order_release);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,10 +1,13 @@
|
||||
#include "settings_overlay.h"
|
||||
#include "audio_backend.h"
|
||||
#include "controller_button_names.h"
|
||||
#include "controller_mapping_wizard.h"
|
||||
#include "input_bindings.h"
|
||||
#include "game_graphics_options.h"
|
||||
#include "music_attenuation.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
#include "wii_remote_input.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <SDL3/SDL_events.h>
|
||||
@@ -33,6 +36,8 @@
|
||||
#endif
|
||||
|
||||
#include <dolphin/pad.h>
|
||||
|
||||
extern "C" void PAD_HLE_SetRumbleEnabled(bool enabled);
|
||||
#include <dolphin/vi.h>
|
||||
#include <aurora/aurora.h>
|
||||
#include <aurora/gfx.h>
|
||||
@@ -64,6 +69,7 @@ const char* GraphicsApiDisplayName() {
|
||||
}
|
||||
|
||||
bool g_topBarVisible = false;
|
||||
bool g_rumbleEnabled = RuntimeConfigFile::RumbleEnabled(true);
|
||||
int g_controllerPort = 0;
|
||||
float g_resolutionScale = RuntimeConfigFile::ResolutionMultiplier(1.0f);
|
||||
int g_audioVolumePercent = static_cast<int>(std::lround(RuntimeConfigFile::AudioVolume(1.0f) * 100.0f));
|
||||
@@ -105,62 +111,9 @@ std::array<int32_t, PAD_MAX_CONTROLLERS> g_configuredControllerIndices = [] {
|
||||
return indices;
|
||||
}();
|
||||
|
||||
struct ControllerButtonItem {
|
||||
const char* configKey;
|
||||
const char* label;
|
||||
PADButton padButton;
|
||||
};
|
||||
|
||||
constexpr std::array<ControllerButtonItem, PAD_BUTTON_COUNT> kControllerButtons = {{
|
||||
{"a", "A", PAD_BUTTON_A},
|
||||
{"b", "B", PAD_BUTTON_B},
|
||||
{"x", "X", PAD_BUTTON_X},
|
||||
{"y", "Y", PAD_BUTTON_Y},
|
||||
{"start", "Start", PAD_BUTTON_START},
|
||||
{"z", "Z", PAD_TRIGGER_Z},
|
||||
{"l", "L", PAD_TRIGGER_L},
|
||||
{"r", "R", PAD_TRIGGER_R},
|
||||
{"up", "D-pad Up", PAD_BUTTON_UP},
|
||||
{"down", "D-pad Down", PAD_BUTTON_DOWN},
|
||||
{"left", "D-pad Left", PAD_BUTTON_LEFT},
|
||||
{"right", "D-pad Right", PAD_BUTTON_RIGHT},
|
||||
}};
|
||||
|
||||
struct NativeButtonItem {
|
||||
const char* configName;
|
||||
const char* label;
|
||||
uint32_t nativeButton;
|
||||
};
|
||||
|
||||
constexpr std::array<NativeButtonItem, SDL_GAMEPAD_BUTTON_COUNT + 1> kNativeButtons = {{
|
||||
{"unmapped", "Unmapped / analog trigger", PAD_NATIVE_BUTTON_INVALID},
|
||||
{"south", "South (A / Cross)", SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
{"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST},
|
||||
{"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST},
|
||||
{"north", "North (Y / Triangle)", SDL_GAMEPAD_BUTTON_NORTH},
|
||||
{"back", "Back / Select", SDL_GAMEPAD_BUTTON_BACK},
|
||||
{"guide", "Guide / Home", SDL_GAMEPAD_BUTTON_GUIDE},
|
||||
{"start", "Start / Options", SDL_GAMEPAD_BUTTON_START},
|
||||
{"left_stick", "Left stick click", SDL_GAMEPAD_BUTTON_LEFT_STICK},
|
||||
{"right_stick", "Right stick click", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
|
||||
{"left_shoulder", "Left shoulder", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
|
||||
{"right_shoulder", "Right shoulder", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
|
||||
{"dpad_up", "D-pad Up", SDL_GAMEPAD_BUTTON_DPAD_UP},
|
||||
{"dpad_down", "D-pad Down", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
|
||||
{"dpad_left", "D-pad Left", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
|
||||
{"dpad_right", "D-pad Right", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
||||
{"misc1", "Misc 1 / Share", SDL_GAMEPAD_BUTTON_MISC1},
|
||||
{"right_paddle1", "Right paddle 1", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
|
||||
{"left_paddle1", "Left paddle 1", SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
|
||||
{"right_paddle2", "Right paddle 2", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
|
||||
{"left_paddle2", "Left paddle 2", SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
|
||||
{"touchpad", "Touchpad", SDL_GAMEPAD_BUTTON_TOUCHPAD},
|
||||
{"misc2", "Misc 2", SDL_GAMEPAD_BUTTON_MISC2},
|
||||
{"misc3", "Misc 3 / GC L click", SDL_GAMEPAD_BUTTON_MISC3},
|
||||
{"misc4", "Misc 4 / GC R click", SDL_GAMEPAD_BUTTON_MISC4},
|
||||
{"misc5", "Misc 5", SDL_GAMEPAD_BUTTON_MISC5},
|
||||
{"misc6", "Misc 6", SDL_GAMEPAD_BUTTON_MISC6},
|
||||
}};
|
||||
using ControllerNames::kNativeButtons;
|
||||
using ControllerNames::NativeButtonItem;
|
||||
constexpr const auto& kControllerButtons = ControllerNames::kGameCubeButtons;
|
||||
|
||||
// Classic Controller Pro layout, indexed like kControllerButtons: the SNES-style
|
||||
// diamond (A right, B bottom, X top, Y left) with digital bumpers driving the GC
|
||||
@@ -177,6 +130,13 @@ constexpr std::array<const char*, PAD_BUTTON_COUNT> kClassicProPreset = {
|
||||
"dpad_up", "dpad_down", "dpad_left", "dpad_right",
|
||||
};
|
||||
|
||||
// PlayStation layout: bumpers drive the GC triggers, Z moves to Create/Share.
|
||||
constexpr std::array<const char*, PAD_BUTTON_COUNT> kPlayStationPreset = {
|
||||
"south", "east", "west", "north", "start", "back",
|
||||
"left_shoulder", "right_shoulder",
|
||||
"dpad_up", "dpad_down", "dpad_left", "dpad_right",
|
||||
};
|
||||
|
||||
struct ResolutionItem {
|
||||
const char* label;
|
||||
float scale;
|
||||
@@ -224,43 +184,25 @@ void LimitResolutionForFrameRate() {
|
||||
}
|
||||
}
|
||||
|
||||
const NativeButtonItem* FindNativeButton(std::string value) {
|
||||
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(), [&](const NativeButtonItem& item) {
|
||||
return value == item.configName;
|
||||
});
|
||||
return it == kNativeButtons.end() ? nullptr : &*it;
|
||||
}
|
||||
using ControllerNames::FindNativeButton;
|
||||
|
||||
struct ControllerBindingPair {
|
||||
std::string primary;
|
||||
std::string secondary;
|
||||
};
|
||||
|
||||
std::string TrimBindingToken(const std::string& token) {
|
||||
const size_t begin = token.find_first_not_of(" \t");
|
||||
if (begin == std::string::npos) {
|
||||
return {};
|
||||
}
|
||||
const size_t end = token.find_last_not_of(" \t");
|
||||
return token.substr(begin, end - begin + 1);
|
||||
}
|
||||
|
||||
// Config values hold up to two comma-separated button names ("dpad_up" or
|
||||
// "dpad_up,left_shoulder"); pressing either one counts as the GC button.
|
||||
ControllerBindingPair SplitControllerBinding(const std::string& value) {
|
||||
const size_t comma = value.find(',');
|
||||
if (comma == std::string::npos) {
|
||||
return {TrimBindingToken(value), {}};
|
||||
return {ControllerNames::TrimToken(value), {}};
|
||||
}
|
||||
return {TrimBindingToken(value.substr(0, comma)), TrimBindingToken(value.substr(comma + 1))};
|
||||
return {ControllerNames::TrimToken(value.substr(0, comma)), ControllerNames::TrimToken(value.substr(comma + 1))};
|
||||
}
|
||||
|
||||
const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) {
|
||||
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(), [&](const NativeButtonItem& item) {
|
||||
return nativeButton == item.nativeButton;
|
||||
});
|
||||
return it == kNativeButtons.end() ? kNativeButtons.front() : *it;
|
||||
}
|
||||
using ControllerNames::NativeButtonForValue;
|
||||
|
||||
void SetTopBarVisible(bool visible) {
|
||||
if (g_topBarVisible == visible) {
|
||||
@@ -279,6 +221,15 @@ void ApplyConfiguredMappings() {
|
||||
if (controllerIndex < 0) {
|
||||
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;
|
||||
if (PADGetButtonMappings(port, &count) == nullptr || count != PAD_BUTTON_COUNT) {
|
||||
@@ -310,6 +261,235 @@ void ApplyConfiguredMappings() {
|
||||
}
|
||||
}
|
||||
|
||||
bool g_wiiRemotesEnabled = RuntimeConfigFile::WiiRemotesEnabled(true);
|
||||
bool g_wiiContinuousScan = RuntimeConfigFile::WiiContinuousScanEnabled(false);
|
||||
|
||||
// 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.
|
||||
int ExpressionResizeCallback(ImGuiInputTextCallbackData* data) {
|
||||
if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
|
||||
auto* text = static_cast<std::string*>(data->UserData);
|
||||
text->resize(static_cast<size_t>(data->BufTextLen));
|
||||
data->Buf = text->data();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void DrawExpressionSettings() {
|
||||
ImGui::SeparatorText("Expressions (Dolphin syntax)");
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 440.0f);
|
||||
ImGui::TextDisabled(
|
||||
"Optional. An expression overrides nothing: its result is combined with the "
|
||||
"button mapping above. Operators ! & | ^ and functions if, min, max, clamp, "
|
||||
"timer, toggle, hold, tap, pulse, smooth, deadzone behave as they do in Dolphin.");
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
static std::array<std::string, InputBindings::kControls.size()> errors;
|
||||
static std::array<std::string, InputBindings::kControls.size()> buffers;
|
||||
static std::string importStatus;
|
||||
static int loadedPort = -1;
|
||||
static bool reloadBuffers = true;
|
||||
const auto port = static_cast<uint32_t>(g_controllerPort);
|
||||
|
||||
if (loadedPort != g_controllerPort || reloadBuffers) {
|
||||
for (size_t i = 0; i < buffers.size(); ++i) {
|
||||
buffers[i] = InputBindings::GetExpression(port, i);
|
||||
}
|
||||
errors.fill(std::string());
|
||||
loadedPort = g_controllerPort;
|
||||
reloadBuffers = false;
|
||||
}
|
||||
|
||||
if (ImGui::Button("Import from Dolphin")) {
|
||||
const std::string path = InputBindings::DefaultDolphinConfigPath();
|
||||
std::string summary;
|
||||
std::string error;
|
||||
if (InputBindings::ImportDolphinConfig(path, g_controllerPort + 1, port, summary, error) < 0) {
|
||||
importStatus = error;
|
||||
} else {
|
||||
importStatus = summary;
|
||||
errors.fill(std::string());
|
||||
reloadBuffers = true;
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Reads [GCPad%d] from %%APPDATA%%\\Dolphin Emulator\\Config\\GCPadNew.ini,\n"
|
||||
"or GCPadNew.ini next to the executable.", g_controllerPort + 1);
|
||||
}
|
||||
if (!importStatus.empty()) {
|
||||
ImGui::TextDisabled("%s", importStatus.c_str());
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < InputBindings::kControls.size(); ++i) {
|
||||
ImGui::PushID(static_cast<int>(i) + 2000);
|
||||
std::string& text = buffers[i];
|
||||
ImGui::SetNextItemWidth(300.0f);
|
||||
if (ImGui::InputText(InputBindings::kControls[i].label, text.data(), text.capacity() + 1,
|
||||
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackResize,
|
||||
ExpressionResizeCallback, &text)) {
|
||||
std::string error;
|
||||
errors[i] = InputBindings::SetExpression(port, i, text, error) ? std::string() : error;
|
||||
}
|
||||
if (InputBindings::IsActive(port, i)) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f), "active");
|
||||
}
|
||||
if (!errors[i].empty()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.3f, 1.0f), "%s", errors[i].c_str());
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
void DrawRumbleSettings() {
|
||||
ImGui::SeparatorText("Vibration");
|
||||
if (ImGui::Checkbox("Controller vibration", &g_rumbleEnabled)) {
|
||||
PAD_HLE_SetRumbleEnabled(g_rumbleEnabled);
|
||||
RuntimeConfigFile::SetRumbleEnabled(g_rumbleEnabled);
|
||||
if (!g_rumbleEnabled) {
|
||||
// Stop whatever is already running: the game will not send another
|
||||
// motor command until its own state machine decides to.
|
||||
constexpr std::array<uint32_t, PAD_MAX_CONTROLLERS> stopAll{
|
||||
PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD,
|
||||
};
|
||||
PADControlAllMotors(stopAll.data());
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Applies to every port.");
|
||||
}
|
||||
}
|
||||
|
||||
void DrawControllerSettings() {
|
||||
for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
||||
const std::string label = "Port " + std::to_string(port + 1);
|
||||
@@ -329,6 +509,7 @@ void DrawControllerSettings() {
|
||||
}
|
||||
ImGui::Separator();
|
||||
controller_mapping_wizard::DrawSetupList();
|
||||
DrawWiiRemoteSettings(selectedGamePort);
|
||||
const uint32_t controllerCount = PADCount();
|
||||
if (controllerCount == 0) {
|
||||
ImGui::TextDisabled("No controller connected");
|
||||
@@ -397,20 +578,28 @@ void DrawControllerSettings() {
|
||||
PADSerializeMappings();
|
||||
mappings = PADGetButtonMappings(port, &mappingCount);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Classic Controller Pro")) {
|
||||
const auto applyPreset = [&](const std::array<const char*, PAD_BUTTON_COUNT>& preset) {
|
||||
const uint32_t port = static_cast<uint32_t>(g_controllerPort);
|
||||
for (size_t i = 0; i < kControllerButtons.size(); ++i) {
|
||||
if (const NativeButtonItem* native = FindNativeButton(kClassicProPreset[i])) {
|
||||
if (const NativeButtonItem* native = FindNativeButton(preset[i])) {
|
||||
PADSetButtonMapping(port, PADButtonMapping{native->nativeButton, kControllerButtons[i].padButton});
|
||||
PADSetAltButtonMapping(port,
|
||||
PADButtonMapping{PAD_NATIVE_BUTTON_INVALID, kControllerButtons[i].padButton});
|
||||
RuntimeConfigFile::SetControllerButton(i, kClassicProPreset[i]);
|
||||
RuntimeConfigFile::SetControllerButton(i, preset[i]);
|
||||
}
|
||||
}
|
||||
altRowExpanded.fill(false);
|
||||
PADSerializeMappings();
|
||||
mappings = PADGetButtonMappings(port, &mappingCount);
|
||||
};
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Classic Controller Pro")) {
|
||||
applyPreset(kClassicProPreset);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("PlayStation")) {
|
||||
applyPreset(kPlayStationPreset);
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Button mapping");
|
||||
@@ -492,6 +681,8 @@ void DrawControllerSettings() {
|
||||
ImGui::TextUnformatted(kControllerButtons[i].label);
|
||||
ImGui::PopID();
|
||||
}
|
||||
DrawExpressionSettings();
|
||||
DrawRumbleSettings();
|
||||
}
|
||||
|
||||
void DrawAudioSettings() {
|
||||
@@ -547,9 +738,9 @@ void DrawAudioSettings() {
|
||||
if (MusicAttenuation::IsExternalMediaPlaying()) {
|
||||
ImGui::TextDisabled("External media is playing; game music is muted.");
|
||||
} else if (!MusicAttenuation::IsMediaControlInitializationComplete()) {
|
||||
ImGui::TextDisabled("Waiting for Windows Media Control...");
|
||||
ImGui::TextDisabled("Waiting for media controls...");
|
||||
} else if (!MusicAttenuation::IsMediaControlAvailable()) {
|
||||
ImGui::TextDisabled("Windows Media Control is unavailable.");
|
||||
ImGui::TextDisabled("Media controls are unavailable.");
|
||||
} else {
|
||||
ImGui::TextDisabled("No external media is currently playing.");
|
||||
}
|
||||
@@ -846,6 +1037,8 @@ void PersistDisplayModeIfChanged() {
|
||||
} // namespace
|
||||
|
||||
void InitializeRuntimeSettings() noexcept {
|
||||
PAD_HLE_SetRumbleEnabled(g_rumbleEnabled);
|
||||
InputBindings::Reload();
|
||||
controller_mapping_wizard::LoadPersistedMappings();
|
||||
ApplyConfiguredMappings();
|
||||
AudioBackend::Instance().SetMasterVolume(static_cast<float>(g_audioVolumePercent) / 100.0f);
|
||||
@@ -866,6 +1059,7 @@ void InitializeRuntimeSettings() noexcept {
|
||||
g_strapInputAccepted.store(false, std::memory_order_relaxed);
|
||||
g_startupDismissFrame.store(UINT64_MAX, std::memory_order_relaxed);
|
||||
PADBlockInput(false);
|
||||
InputBindings::SetInputBlocked(false);
|
||||
}
|
||||
|
||||
void HandleEvents(const AuroraEvent* events) noexcept {
|
||||
@@ -893,6 +1087,11 @@ void Draw() noexcept {
|
||||
// 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.
|
||||
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();
|
||||
PersistDisplayModeIfChanged();
|
||||
UpdateCursorAutoHide();
|
||||
@@ -903,7 +1102,9 @@ void Draw() noexcept {
|
||||
DrawTopBar();
|
||||
controller_mapping_wizard::Draw();
|
||||
// The wizard captures raw presses; keep them out of the game.
|
||||
PADBlockInput(controller_mapping_wizard::IsActive());
|
||||
const bool inputBlocked = controller_mapping_wizard::IsActive();
|
||||
PADBlockInput(inputBlocked);
|
||||
InputBindings::SetInputBlocked(inputBlocked);
|
||||
DrawStartupScreen();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,784 @@
|
||||
#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>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
|
||||
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.
|
||||
// A sub-warning message is written once: SDL repeats the same line on every
|
||||
// enumeration (one "couldn't open /dev/hidraw7: Permission denied" per HID
|
||||
// device per pass), and console.log is unbuffered, so the repeats were a
|
||||
// per-pass burst of writes on the main thread for no new information.
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
if (priority < SDL_LOG_PRIORITY_WARN) {
|
||||
// Device paths in these messages keep changing (/dev/hidrawN climbs with
|
||||
// hotplug churn), so cap the set instead of holding one string per line
|
||||
// for the whole session.
|
||||
static std::unordered_set<std::string> s_seen;
|
||||
if (s_seen.size() >= 256) {
|
||||
s_seen.clear();
|
||||
}
|
||||
if (!s_seen.insert(message).second) {
|
||||
return;
|
||||
}
|
||||
RT_LOG("sdl") << message << " (further identical messages suppressed)" << std::endl;
|
||||
return;
|
||||
}
|
||||
RT_LOG("sdl") << message << std::endl;
|
||||
}
|
||||
|
||||
// Whether Poll() drives its own periodic re-enumeration. The 1->0->1 hint
|
||||
// flip below makes SDL close and re-open every HIDAPI device on the main
|
||||
// thread, and on Linux that means an open() attempt on every /dev/hidraw node
|
||||
// (each failing with EACCES until a udev rule grants access), which showed up
|
||||
// as a frame hitch every scan interval even on an empty menu. It exists for
|
||||
// Windows Bluetooth stacks, where a remote that drops or is switched on after
|
||||
// launch is not seen again until the driver re-enumerates. Linux and macOS
|
||||
// already get hotplug from udev / IOKit: SDL re-enumerates when a device
|
||||
// appears, so nothing periodic is needed there. The overlay's "Rescan now"
|
||||
// still works everywhere.
|
||||
#if defined(_WIN32)
|
||||
constexpr bool kPeriodicRescan = true;
|
||||
#else
|
||||
constexpr bool kPeriodicRescan = false;
|
||||
#endif
|
||||
|
||||
// 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(false)) {
|
||||
g_scanning = false;
|
||||
return;
|
||||
}
|
||||
const uint64_t now = SDL_GetTicks();
|
||||
if (!g_scanning) {
|
||||
RT_LOG(RT_TAG_CONFIG) << "No Wii Remote connected; "
|
||||
<< (kPeriodicRescan ? "scanning for one" : "waiting for one to be paired")
|
||||
<< " (press 1+2 on the remote)" << std::endl;
|
||||
g_scanning = true;
|
||||
g_lostAtMs = now;
|
||||
}
|
||||
if (!kPeriodicRescan || 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;
|
||||
}
|
||||
|
||||
// Whether looking for a remote means periodic rescans or waiting for hotplug.
|
||||
bool PeriodicRescanEnabled() {
|
||||
return kPeriodicRescan;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
#include "nand_save_probe.h"
|
||||
#include "nand_settings.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
using RuntimeNandSave::ReadAction;
|
||||
using RuntimeNandSave::Contents;
|
||||
|
||||
static void Require(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
static void Write(const fs::path& path, const std::string& bytes) {
|
||||
fs::create_directories(path.parent_path());
|
||||
std::ofstream output(path, std::ios::binary);
|
||||
output.write(bytes.data(), bytes.size());
|
||||
output.close();
|
||||
Require(static_cast<bool>(output), "Fixture write failed");
|
||||
}
|
||||
|
||||
static std::string Read(const fs::path& path) {
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
Require(static_cast<bool>(input), "Fixture read failed");
|
||||
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
|
||||
}
|
||||
|
||||
// A disk error after zero-filled blocks must not look like a blank file's EOF.
|
||||
class FailingDisk : public std::streambuf {
|
||||
int blocks;
|
||||
public:
|
||||
explicit FailingDisk(int zeroBlocks) : blocks(zeroBlocks) {}
|
||||
std::streamsize xsgetn(char* buffer, std::streamsize length) override {
|
||||
if (blocks-- <= 0) throw std::runtime_error("injected read failure");
|
||||
std::fill(buffer, buffer + length, '\0');
|
||||
return length;
|
||||
}
|
||||
};
|
||||
|
||||
int main() {
|
||||
const auto root = fs::temp_directory_path() / ("wiicomp-save-scenarios-" +
|
||||
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
try {
|
||||
const auto save = root / "title/00010004/524d4350/data/rksys.dat";
|
||||
const auto shadow = fs::path(save.native() + fs::path(".nandsafe.tmp").native());
|
||||
std::string error;
|
||||
Require(RuntimeNandSettings::Ensure(root, error, 1800000123), "New profile settings bootstrap");
|
||||
const auto identity = Read(RuntimeNandSettings::FilePath(root));
|
||||
Require(RuntimeNandSave::CheckRead(save, 1) == ReadAction::Proceed, "Fresh profile follows normal missing-file handling");
|
||||
Require(!fs::exists(save), "Probing fresh profile must not create a save");
|
||||
|
||||
// First launch interrupted before save initialization, including block
|
||||
// boundaries and a full-sized synthetic zero-filled allocation.
|
||||
for (const size_t size : {size_t(0), size_t(1), size_t(4095), size_t(4096), size_t(4097), size_t(3 * 1024 * 1024)}) {
|
||||
const std::string bytes(size, '\0');
|
||||
Write(save, bytes);
|
||||
Require(RuntimeNandSave::CheckRead(save, 1) == ReadAction::Missing, "Blank save should be offered first-save recovery");
|
||||
Require(Read(save) == bytes, "Blank-save detection must not modify the file");
|
||||
for (int mode : {2, 3}) {
|
||||
Require(RuntimeNandSave::CheckRead(save, mode) == ReadAction::Proceed, "Write opens must remain available for initialization");
|
||||
}
|
||||
}
|
||||
|
||||
// Existing saves, imported saves, partial/corrupt saves, and a zero
|
||||
// prefix with data only in the final byte are all left to the game.
|
||||
std::string existing(3 * 1024 * 1024, '\0');
|
||||
existing.replace(0, 8, "RKSD0006");
|
||||
existing[10000] = 42;
|
||||
for (const std::string& bytes : {existing, std::string("RKSD"), std::string("damaged-header"),
|
||||
std::string(8192, '\0') + "x", std::string(8191, '\0') + "x"}) {
|
||||
Write(save, bytes);
|
||||
Require(RuntimeNandSave::CheckRead(save, 1) == ReadAction::Proceed, "Never hide a save containing any data");
|
||||
Require(Read(save) == bytes, "Existing/partial save must be byte-identical after inspection");
|
||||
}
|
||||
|
||||
// Interrupted replacement: retain a committed original regardless of
|
||||
// whether the shadow is blank, partial, or contains a complete header.
|
||||
Write(save, existing);
|
||||
for (const std::string& bytes : {std::string(), std::string(4096, '\0'), std::string("RKSD"), existing}) {
|
||||
Write(shadow, bytes);
|
||||
Require(RuntimeNandSave::CheckRead(save, 1) == ReadAction::Proceed, "Committed original takes precedence over write shadow");
|
||||
Require(Read(save) == existing && Read(shadow) == bytes, "Probe must preserve both sides of an interrupted write");
|
||||
}
|
||||
// No usable original: do not let missing-save recovery discard the
|
||||
// only possible recovery source, and do not auto-promote that shadow.
|
||||
for (const bool mainExists : {false, true}) {
|
||||
fs::remove(save);
|
||||
if (mainExists) Write(save, std::string(4096, '\0'));
|
||||
Write(shadow, existing);
|
||||
Require(RuntimeNandSave::CheckRead(save, 1) == ReadAction::RecoveryNeeded, "Preserve recovery candidate when original is missing or blank");
|
||||
Require(Read(shadow) == existing, "Recovery candidate must remain unchanged");
|
||||
Require(fs::exists(save) == mainExists, "Do not promote shadow automatically");
|
||||
}
|
||||
Write(shadow, std::string(4096, '\0'));
|
||||
Require(RuntimeNandSave::CheckRead(save, 1) == ReadAction::Missing, "Two blank files may use first-save recovery");
|
||||
fs::remove(shadow);
|
||||
|
||||
for (const char* name : {"rksys.dat.bak", "rksys.dat.backup", "rksys.dat2", "banner.bin", "setting.txt"}) {
|
||||
const auto unrelated = save.parent_path() / name;
|
||||
Write(unrelated, std::string(4096, '\0'));
|
||||
Require(RuntimeNandSave::CheckRead(unrelated, 1) == ReadAction::Proceed, "Do not classify backups or unrelated files as missing saves");
|
||||
}
|
||||
for (int blocks : {0, 1, 2}) {
|
||||
FailingDisk disk(blocks);
|
||||
std::istream input(&disk);
|
||||
Require(RuntimeNandSave::InspectStream(input) == Contents::Error, "Read failure must remain an error, including after zero-filled blocks");
|
||||
}
|
||||
std::istringstream badEof;
|
||||
badEof.setstate(std::ios::badbit | std::ios::eofbit);
|
||||
Require(RuntimeNandSave::InspectStream(badEof) == Contents::Error, "Badbit plus EOF must not imply a blank save");
|
||||
|
||||
#ifdef _WIN32
|
||||
Write(save, existing);
|
||||
const HANDLE locked = CreateFileW(save.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
Require(locked != INVALID_HANDLE_VALUE, "Could not lock fixture");
|
||||
const auto lockedResult = RuntimeNandSave::CheckRead(save, 1);
|
||||
CloseHandle(locked);
|
||||
Require(lockedResult == ReadAction::Error, "Sharing/access failure must not report a missing save");
|
||||
Require(Read(save) == existing, "Locked save must survive inspection unchanged");
|
||||
Require(SetFileAttributesW(save.c_str(), FILE_ATTRIBUTE_READONLY) != 0, "Set fixture read-only");
|
||||
const auto readOnlyResult = RuntimeNandSave::CheckRead(save, 1);
|
||||
SetFileAttributesW(save.c_str(), FILE_ATTRIBUTE_NORMAL);
|
||||
Require(readOnlyResult == ReadAction::Proceed && Read(save) == existing, "Readable read-only save remains available");
|
||||
#endif
|
||||
Require(RuntimeNandSettings::Ensure(root, error, 1900000123), "Existing profile settings bootstrap");
|
||||
Require(Read(RuntimeNandSettings::FilePath(root)) == identity, "Save recovery must not change console identity");
|
||||
fs::remove_all(root);
|
||||
std::cout << "NAND save startup, preservation, interrupted-write and I/O failure scenarios passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << " (fixtures retained at " << root << ")\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
#include "nand_settings.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
static void Require(bool condition, const char* message = "NAND settings check failed") {
|
||||
if (!condition) {
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
}
|
||||
|
||||
static std::string ReadBytes(const std::filesystem::path& path) {
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
|
||||
}
|
||||
|
||||
int main() {
|
||||
const auto root = std::filesystem::temp_directory_path() /
|
||||
("wiicomp-nand-settings-" + std::to_string(
|
||||
std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
const auto path = root / "title/00000001/00000002/data/setting.txt";
|
||||
try {
|
||||
using namespace RuntimeNandSettings;
|
||||
Require(GenerateSerial(1800000123) == "800000123", "Dolphin timestamp modulo");
|
||||
Require(GenerateSerial(1000000001) == "000000001", "Dolphin leading zero padding");
|
||||
Require(GenerateSerial(-1).empty(), "Invalid clock must not supply an identity");
|
||||
// Golden bytes generated by Dolphin's unmodified SettingsHandler.cpp
|
||||
// (upstream 2026-09-06), PAL boot fields and synthetic serial 000000001.
|
||||
// Everything after this prefix is raw zero padding to 256 bytes.
|
||||
const std::string goldenHex =
|
||||
"bba6ac929a0bc96b7eed83d27f33a1e7e73d9b836d8b47c59ee23df6b275baab"
|
||||
"bec9d9dead03cc7a3bdafee50c30ab9fb86194e119fe4ba19eff62d5ec3aacb3"
|
||||
"b5c9d9e3977eac0943d7ff903120a49ef024eafe1cf77be79cf6229a823aabf0f0";
|
||||
std::array<uint8_t, 256> golden{};
|
||||
for (size_t i = 0; i < goldenHex.size() / 2; ++i) {
|
||||
golden[i] = static_cast<uint8_t>(std::stoul(goldenHex.substr(i * 2, 2), nullptr, 16));
|
||||
}
|
||||
Require(EncodeNew("000000001") == golden, "Exact Dolphin writer golden fixture");
|
||||
std::string error;
|
||||
Require(!RuntimeNandSettings::Read(root));
|
||||
Require(!std::filesystem::exists(root));
|
||||
std::filesystem::create_directories(path.parent_path());
|
||||
const std::string plain = "AREA=USA\r\n\nCODE=LU\r\nSERNO=987654321\r\nGAME=US\r\n";
|
||||
std::array<uint8_t, 256> fixture{};
|
||||
for (size_t i = 0; i < fixture.size(); ++i) {
|
||||
const unsigned shift = i % 32;
|
||||
const uint32_t key = shift == 0 ? 0x73B5DBFAu :
|
||||
(0x73B5DBFAu << shift) | (0x73B5DBFAu >> (32 - shift));
|
||||
fixture[i] = static_cast<uint8_t>(key) ^ (i < plain.size() ? plain[i] : 0);
|
||||
}
|
||||
{
|
||||
std::ofstream output(path, std::ios::binary);
|
||||
output.write(reinterpret_cast<const char*>(fixture.data()), fixture.size());
|
||||
}
|
||||
auto settings = RuntimeNandSettings::Read(root);
|
||||
Require(settings && RuntimeNandSettings::HasIdentity(*settings));
|
||||
Require(settings->at("SERNO") == "987654321" && settings->at("CODE") == "LU");
|
||||
Require(settings->at("AREA") == "USA" && settings->at("GAME") == "US");
|
||||
Require(Ensure(root, error, 1800000123), "Existing imported NAND must work");
|
||||
std::array<uint8_t, 256> after{};
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
input.read(reinterpret_cast<char*>(after.data()), after.size());
|
||||
}
|
||||
Require(after == fixture);
|
||||
for (const auto serial : {"", "000000000", "1234567890", "123ABC789"}) {
|
||||
(*settings)["SERNO"] = serial;
|
||||
Require(!RuntimeNandSettings::HasIdentity(*settings));
|
||||
}
|
||||
(*settings)["SERNO"] = "012345678";
|
||||
Require(RuntimeNandSettings::HasIdentity(*settings));
|
||||
(*settings)["CODE"] = "TOOLONG";
|
||||
Require(!RuntimeNandSettings::HasIdentity(*settings));
|
||||
(*settings)["CODE"] = "LEH";
|
||||
settings->erase("GAME");
|
||||
Require(!RuntimeNandSettings::HasIdentity(*settings));
|
||||
std::filesystem::resize_file(path, 128);
|
||||
Require(!RuntimeNandSettings::Read(root));
|
||||
const auto damaged = ReadBytes(path);
|
||||
Require(!Ensure(root, error, 1800000123), "Do not replace a truncated identity");
|
||||
Require(ReadBytes(path) == damaged, "Damaged file must remain untouched");
|
||||
|
||||
const auto fresh = root / "fresh";
|
||||
Require(Ensure(fresh, error, 1800000123), "Missing setting.txt must initialize");
|
||||
const auto generated = Read(fresh);
|
||||
Require(generated && HasIdentity(*generated), "Generated file must be readable");
|
||||
Require(generated->at("SERNO") == "800000123", "Persist Dolphin-generated serial");
|
||||
Require(generated->at("CODE") == "LEH" && generated->at("AREA") == "EUR" &&
|
||||
generated->at("GAME") == "EU", "PAL first-boot fields");
|
||||
Require(generated->at("MODEL") == "RVL-001(EUR)" && generated->at("VIDEO") == "PAL" &&
|
||||
generated->at("DVD") == "0" && generated->at("MPCH") == "0x7FFE",
|
||||
"Complete Dolphin boot settings");
|
||||
const auto firstBoot = ReadBytes(FilePath(fresh));
|
||||
Require(firstBoot.size() == 256 && firstBoot.back() == 0, "Dolphin buffer size and raw zero padding");
|
||||
Require(Ensure(fresh, error, 1900000999), "Second boot");
|
||||
Require(ReadBytes(FilePath(fresh)) == firstBoot, "Second boot must not change any bytes");
|
||||
|
||||
const auto blocked = root / "blocked";
|
||||
{ std::ofstream output(blocked); output << "file obstructing NAND directory"; }
|
||||
Require(!Ensure(blocked, error, 1800000123), "Write failure must not return an ephemeral identity");
|
||||
Require(!Ensure(root / "bad-clock", error, -1), "Clock failure must not initialize");
|
||||
|
||||
const auto concurrent = root / "concurrent";
|
||||
std::array<bool, 16> results{};
|
||||
std::vector<std::thread> workers;
|
||||
for (size_t i = 0; i < results.size(); ++i) {
|
||||
workers.emplace_back([&, i] {
|
||||
std::string detail;
|
||||
results[i] = Ensure(concurrent, detail, 1800000001 + i);
|
||||
});
|
||||
}
|
||||
for (auto& worker : workers) worker.join();
|
||||
for (const bool result : results) Require(result, "Concurrent boot must read the persisted winner");
|
||||
const auto winner = ReadBytes(FilePath(concurrent));
|
||||
Require(Read(concurrent) && HasIdentity(*Read(concurrent)), "Concurrent boot must persist valid settings");
|
||||
Require(Ensure(concurrent, error, 1900000999), "Boot after concurrent initialization");
|
||||
Require(ReadBytes(FilePath(concurrent)) == winner, "Concurrent winner must remain stable");
|
||||
|
||||
// Independently decode as Nintendo does: stop at the first encoded NUL.
|
||||
// Exercise serials that force Dolphin's extra-LF escaping, not only
|
||||
// values that happen to work with a plain rotating-XOR encoder.
|
||||
bool sawExtraLf = false;
|
||||
for (int serial = 1; serial <= 10000; ++serial) {
|
||||
const auto number = GenerateSerial(1000000000 + serial);
|
||||
const auto encoded = EncodeNew(number);
|
||||
Require(encoded.has_value(), "Serial encoding must fit");
|
||||
std::string decoded;
|
||||
for (size_t i = 0; i < encoded->size() && (*encoded)[i] != 0; ++i) {
|
||||
const unsigned shift = i % 32;
|
||||
const uint32_t key = shift == 0 ? 0x73B5DBFAu :
|
||||
(0x73B5DBFAu << shift) | (0x73B5DBFAu >> (32 - shift));
|
||||
decoded += static_cast<char>((*encoded)[i] ^ static_cast<uint8_t>(key));
|
||||
}
|
||||
Require(decoded.find("SERNO=" + number + "\r\n") != std::string::npos &&
|
||||
decoded.find("GAME=EU\r\n") != std::string::npos,
|
||||
"Encoded NUL must not truncate settings");
|
||||
sawExtraLf |= decoded.find("\r\n\n") != std::string::npos;
|
||||
}
|
||||
Require(sawExtraLf, "Exercise Dolphin LF escape path");
|
||||
std::filesystem::remove_all(root);
|
||||
std::cout << "NAND settings checks passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::filesystem::remove_all(root);
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
// Verifies the expression engine against Dolphin's documented semantics,
|
||||
// including the exact line from the user's GCPadNew.ini.
|
||||
|
||||
#include "input_expr.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
static int g_failures = 0;
|
||||
static std::map<std::string, double> g_inputs;
|
||||
|
||||
static InputExpr::InputSource Source() {
|
||||
return [](const std::string& name) {
|
||||
const auto it = g_inputs.find(name);
|
||||
return it == g_inputs.end() ? 0.0 : it->second;
|
||||
};
|
||||
}
|
||||
|
||||
static void Check(bool ok, const std::string& what) {
|
||||
if (!ok) {
|
||||
std::printf(" FAIL: %s\n", what.c_str());
|
||||
++g_failures;
|
||||
}
|
||||
}
|
||||
|
||||
static InputExpr::Expression Compile(const std::string& text) {
|
||||
InputExpr::Expression expr;
|
||||
std::string error;
|
||||
if (!InputExpr::Expression::Parse(text, expr, error)) {
|
||||
std::printf(" FAIL: parse '%s': %s\n", text.c_str(), error.c_str());
|
||||
++g_failures;
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
|
||||
static bool Pressed(const InputExpr::Expression& e) {
|
||||
return e.Evaluate(Source()) > InputExpr::kConditionThreshold;
|
||||
}
|
||||
|
||||
static void Sleep(int ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); }
|
||||
|
||||
int main() {
|
||||
std::printf("Dolphin expression engine\n");
|
||||
|
||||
// Operators: & is min, | is max, ! is 1-x, matching Dolphin.
|
||||
g_inputs["A"] = 1.0;
|
||||
g_inputs["B"] = 0.0;
|
||||
Check(Pressed(Compile("`A`")), "bare input");
|
||||
Check(!Pressed(Compile("!`A`")), "not");
|
||||
Check(!Pressed(Compile("`A` & `B`")), "and is min");
|
||||
Check(Pressed(Compile("`A` | `B`")), "or is max");
|
||||
Check(Pressed(Compile("`A` ^ `B`")), "xor");
|
||||
Check(!Pressed(Compile("`A` ^ `A`")), "xor of equal inputs is false");
|
||||
|
||||
// Precedence: & binds tighter than |, so this is A | (B & A).
|
||||
g_inputs["B"] = 0.0;
|
||||
Check(Pressed(Compile("`A` | `B` & `A`")), "& binds tighter than |");
|
||||
|
||||
// Parens and numeric literals.
|
||||
Check(Pressed(Compile("(`B` | 1)")), "literal");
|
||||
Check(Pressed(Compile("min(1, `A`)")), "min");
|
||||
Check(!Pressed(Compile("min(0, `A`)")), "min with zero");
|
||||
Check(Pressed(Compile("if(`A`, 1, 0)")), "if");
|
||||
Check(Pressed(Compile("clamp(5, 0, 1)")), "clamp");
|
||||
|
||||
// toggle flips on each rising edge and holds between them.
|
||||
auto toggle = Compile("toggle(`T`)");
|
||||
g_inputs["T"] = 0.0;
|
||||
toggle.Evaluate(Source());
|
||||
g_inputs["T"] = 1.0;
|
||||
Check(Pressed(toggle), "toggle on after first press");
|
||||
g_inputs["T"] = 0.0;
|
||||
Check(Pressed(toggle), "toggle stays on after release");
|
||||
g_inputs["T"] = 1.0;
|
||||
Check(!Pressed(toggle), "toggle off on second press");
|
||||
|
||||
// hold requires the input to be down for the full duration.
|
||||
auto hold = Compile("hold(`H`, 0.05)");
|
||||
g_inputs["H"] = 1.0;
|
||||
Check(!Pressed(hold), "hold not satisfied immediately");
|
||||
Sleep(70);
|
||||
Check(Pressed(hold), "hold satisfied after the interval");
|
||||
g_inputs["H"] = 0.0;
|
||||
Check(!Pressed(hold), "hold clears on release");
|
||||
|
||||
// pulse fires for the given duration after a rising edge.
|
||||
auto pulse = Compile("pulse(`P`, 0.05)");
|
||||
g_inputs["P"] = 0.0;
|
||||
pulse.Evaluate(Source());
|
||||
g_inputs["P"] = 1.0;
|
||||
Check(Pressed(pulse), "pulse fires on rising edge");
|
||||
Sleep(80);
|
||||
Check(!Pressed(pulse), "pulse expires");
|
||||
|
||||
// The timing-window idiom seen in shared Dolphin configs.
|
||||
auto window = Compile("!pulse(`W`, 0.05) & pulse(`W`, 0.15)");
|
||||
g_inputs["W"] = 0.0;
|
||||
window.Evaluate(Source());
|
||||
g_inputs["W"] = 1.0;
|
||||
Check(!Pressed(window), "window closed before its start");
|
||||
Sleep(90);
|
||||
Check(Pressed(window), "window open between the two pulses");
|
||||
Sleep(90);
|
||||
Check(!Pressed(window), "window closed after its end");
|
||||
|
||||
// timer ramps 0..1 and wraps, so a threshold turns it into a square wave.
|
||||
auto timer = Compile("`X` & timer(0.1)");
|
||||
g_inputs["X"] = 1.0;
|
||||
int high = 0;
|
||||
int low = 0;
|
||||
for (int i = 0; i < 40; ++i) {
|
||||
(Pressed(timer) ? high : low)++;
|
||||
Sleep(5);
|
||||
}
|
||||
Check(high > 5 && low > 5, "timer alternates high and low");
|
||||
|
||||
// The exact D-Pad/Up line from the user's GCPadNew.ini.
|
||||
auto dolphinLine = Compile("`Hat 0 N` | `Button 4` & timer(0.01)");
|
||||
g_inputs["Hat 0 N"] = 0.0;
|
||||
g_inputs["Button 4"] = 0.0;
|
||||
Check(!Pressed(dolphinLine), "idle with nothing held");
|
||||
g_inputs["Hat 0 N"] = 1.0;
|
||||
Check(Pressed(dolphinLine), "hat alone presses");
|
||||
g_inputs["Hat 0 N"] = 0.0;
|
||||
g_inputs["Button 4"] = 1.0;
|
||||
high = low = 0;
|
||||
for (int i = 0; i < 60; ++i) {
|
||||
(Pressed(dolphinLine) ? high : low)++;
|
||||
Sleep(2);
|
||||
}
|
||||
Check(high > 5 && low > 5, "LB alternates via timer(0.01)");
|
||||
|
||||
// Regression tests for the CodeRabbit findings on PR #89.
|
||||
g_inputs["A"] = 1.0;
|
||||
// clamp with reversed bounds: std::clamp is UB when lo > hi.
|
||||
Check(Compile("clamp(0.5, 1, 0)").Evaluate(Source()) == 0.5, "clamp tolerates reversed bounds");
|
||||
// deadzone(v, 1) would divide by zero.
|
||||
{
|
||||
const double v = Compile("deadzone(`A`, 1)").Evaluate(Source());
|
||||
Check(std::isfinite(v), "deadzone with dz=1 stays finite");
|
||||
}
|
||||
// timer with a zero or negative period would produce inf or NaN.
|
||||
for (const char* text : {"timer(0)", "timer(-1)"}) {
|
||||
const double v = Compile(text).Evaluate(Source());
|
||||
Check(std::isfinite(v), std::string(text) + " stays finite");
|
||||
}
|
||||
// Any non-finite result is squashed before it can reach the uint8_t cast.
|
||||
for (const char* text : {"sqrt(0 - 1)", "pow(10, 10000)", "tan(1.5707963267948966)"}) {
|
||||
const double v = Compile(text).Evaluate(Source());
|
||||
Check(std::isfinite(v), std::string(text) + " is sanitised at the boundary");
|
||||
}
|
||||
|
||||
// tap count is user authored; negative, huge and non-finite must not reach
|
||||
// the unsigned conversion.
|
||||
for (const char* text : {"tap(`A`, 0.2, -1)", "tap(`A`, 0.2, 999999999)", "tap(`A`, 0.2, 0)"}) {
|
||||
InputExpr::Expression e;
|
||||
std::string err;
|
||||
Check(InputExpr::Expression::Parse(text, e, err), std::string("parse ") + text);
|
||||
const double v = e.Evaluate(Source());
|
||||
Check(std::isfinite(v), std::string(text) + " evaluates without UB");
|
||||
}
|
||||
|
||||
// Exponent notation is not part of the number syntax, matching Dolphin's
|
||||
// lexer; it is rejected rather than silently misparsed.
|
||||
{
|
||||
InputExpr::Expression e;
|
||||
std::string err;
|
||||
Check(!InputExpr::Expression::Parse("tap(`A`, 0.2, 1e30)", e, err), "exponent notation rejected");
|
||||
}
|
||||
|
||||
// A zero divisor must not skip the left subtree: stateful functions there
|
||||
// still need their per-frame update.
|
||||
{
|
||||
auto divToggle = Compile("toggle(`D`) / `Z`");
|
||||
g_inputs["Z"] = 0.0;
|
||||
g_inputs["D"] = 0.0;
|
||||
divToggle.Evaluate(Source());
|
||||
g_inputs["D"] = 1.0;
|
||||
divToggle.Evaluate(Source()); // rising edge seen even though rhs is 0
|
||||
g_inputs["D"] = 0.0;
|
||||
g_inputs["Z"] = 1.0;
|
||||
Check(divToggle.Evaluate(Source()) > InputExpr::kConditionThreshold,
|
||||
"toggle still latched while the divisor was zero");
|
||||
}
|
||||
|
||||
// smooth with a zero rate divides 0 by 0; NaN must not stick in the node.
|
||||
{
|
||||
auto sm = Compile("smooth(`A`, 0)");
|
||||
g_inputs["A"] = 1.0;
|
||||
sm.Evaluate(Source());
|
||||
Sleep(5);
|
||||
Check(std::isfinite(sm.Evaluate(Source())), "smooth with a zero rate stays finite");
|
||||
}
|
||||
|
||||
// Referenced inputs, used for diagnostics in the UI.
|
||||
const auto refs = dolphinLine.ReferencedInputs();
|
||||
Check(refs.size() == 2, "two referenced inputs");
|
||||
|
||||
// Errors are reported, not silently swallowed.
|
||||
InputExpr::Expression bad;
|
||||
std::string error;
|
||||
Check(!InputExpr::Expression::Parse("`A` & ", bad, error), "trailing operator rejected");
|
||||
Check(!InputExpr::Expression::Parse("nope(1)", bad, error), "unknown function rejected");
|
||||
Check(!InputExpr::Expression::Parse("(`A`", bad, error), "missing paren rejected");
|
||||
Check(!InputExpr::Expression::Parse("`A", bad, error), "unterminated backtick rejected");
|
||||
Check(InputExpr::Expression::Parse("", bad, error) && bad.Empty(), "empty parses to empty");
|
||||
Check(!InputExpr::Expression::Parse("hold(`A`)", bad, error), "wrong arg count rejected");
|
||||
|
||||
if (g_failures == 0) {
|
||||
std::printf("all checks passed\n");
|
||||
return 0;
|
||||
}
|
||||
std::printf("%d check(s) failed\n", g_failures);
|
||||
return 1;
|
||||
}
|
||||
@@ -166,6 +166,7 @@ return command switch
|
||||
"emit-build-shards" => RunEmitBuildShards(tail),
|
||||
"emit-base-manifest" => RunEmitBaseManifest(tail),
|
||||
"check-base-mod-awareness" => RunCheckBaseModAwareness(tail),
|
||||
"validate-retro-wfc-payload" => RunValidateRetroWfcPayload(tail),
|
||||
_ => ShowHelp(command)
|
||||
};
|
||||
|
||||
@@ -354,6 +355,33 @@ int RunInfo()
|
||||
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)
|
||||
{
|
||||
if (argsTail.Length == 0 || argsTail[0].StartsWith("--", StringComparison.Ordinal))
|
||||
@@ -3639,7 +3667,8 @@ static string[] KnownCommands() => new[]
|
||||
"translate-mod",
|
||||
"emit-base-manifest",
|
||||
"emit-build-shards",
|
||||
"check-base-mod-awareness"
|
||||
"check-base-mod-awareness",
|
||||
"validate-retro-wfc-payload"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -3682,6 +3711,10 @@ static (string? Positional, CommandOption[] Options)? CommandSpec(string command
|
||||
new("--translation-output-metadata", "path"),
|
||||
new("--code-pul", "path")
|
||||
}),
|
||||
"validate-retro-wfc-payload" => (null, new CommandOption[]
|
||||
{
|
||||
new("--directory", "directory", Required: true)
|
||||
}),
|
||||
"emit-base-manifest" => (null, new CommandOption[]
|
||||
{
|
||||
new("--out", "path"),
|
||||
@@ -4061,4 +4094,3 @@ sealed record ResolvedDispatchEntry(
|
||||
bool MustRemainDynamicallyDispatchable,
|
||||
string SourceFile);
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Translator.Core\Translator.Core.csproj" />
|
||||
<ProjectReference Include="..\..\..\Launcher\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -22,14 +22,15 @@ internal static class AssemblyBlobWriter
|
||||
var expectedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var assembly = new StringBuilder();
|
||||
foreach (var header in headerLines) assembly.AppendLine(header);
|
||||
// PE/COFF (Windows) and ELF (Linux) spell a read-only data section differently in GNU-as
|
||||
// syntax - COFF section flags ("dr" = data, read-only) versus an ELF section needing an
|
||||
// allocatable-only flag plus an explicit @progbits type. The build always targets
|
||||
// PE/COFF (Windows), Mach-O (macOS), and ELF (Linux) spell a read-only data section
|
||||
// differently in GNU-as syntax. The build always targets
|
||||
// whichever platform the translator itself runs on (there is no cross-compilation
|
||||
// support), so that's what this picks the section syntax from.
|
||||
assembly.AppendLine(OperatingSystem.IsWindows()
|
||||
? ".section .rdata,\"dr\""
|
||||
: ".section .rodata,\"a\",@progbits");
|
||||
: OperatingSystem.IsMacOS()
|
||||
? ".section __TEXT,__const"
|
||||
: ".section .rodata,\"a\",@progbits");
|
||||
assembly.AppendLine();
|
||||
|
||||
foreach (var blob in blobs)
|
||||
@@ -40,8 +41,12 @@ internal static class AssemblyBlobWriter
|
||||
var hash = ChecksumUtilities.Sha256Hex(blob.Data.Span);
|
||||
assembly.AppendLine($"// {blob.Comment}; sha256={hash}");
|
||||
assembly.AppendLine(".p2align 4");
|
||||
assembly.AppendLine($".globl {blob.Symbol}");
|
||||
assembly.AppendLine($"{blob.Symbol}:");
|
||||
// C/C++ external symbols carry a leading underscore in Mach-O,
|
||||
// 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);
|
||||
assembly.AppendLine($".incbin \"{SanitizeAssemblyPath(referencePath)}\"");
|
||||
assembly.AppendLine();
|
||||
@@ -51,7 +56,7 @@ internal static class AssemblyBlobWriter
|
||||
{
|
||||
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
|
||||
// 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 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 int HeaderSize = 0x20;
|
||||
|
||||
@@ -58,7 +64,7 @@ public sealed class KamekChunk
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -74,7 +80,7 @@ public sealed class KamekChunk
|
||||
|
||||
var magic0 = 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}.");
|
||||
}
|
||||
@@ -83,9 +89,15 @@ public sealed class KamekChunk
|
||||
var codeSize = reader.ReadUInt32();
|
||||
var ctorStart = reader.ReadUInt32();
|
||||
var ctorEnd = reader.ReadUInt32();
|
||||
var chunkSize = reader.ReadUInt32();
|
||||
var declaredChunkSize = reader.ReadUInt32();
|
||||
_ = 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)
|
||||
{
|
||||
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,
|
||||
_ => 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))
|
||||
{
|
||||
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);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user