30 Commits

Author SHA1 Message Date
patchzyy 352c4ca02e Update settings_overlay.cpp 2026-09-10 17:35:29 +02:00
Cristian Boehm cc286f3060 Merge branch 'patchzyy:main' into main 2026-09-09 13:17:50 -04:00
Michael G 8769cf6dea docs: add macOS source build guide for WiiCompiled and Retro Rewind (#177)
* docs: add macOS source build guide for WiiCompiled and Retro Rewind

* review fixes

* Change Retro-WFC payload download URL

Updated the URL for downloading Retro-WFC payload.
2026-09-09 18:23:15 +02:00
theofficialgman 452b478bb3 Resolve z fighting (#134)
* Fix already downloaded toolchain re-use

the following mv command would move $work into $toolchain_dir if the $toolchain_dir folder already existed.

* resolve z-fighting
2026-09-09 14:47:14 +02:00
patchzyy 407f8a7190 https payload (#198) 2026-09-09 14:41:58 +02:00
Jordan Blake c2289e4ba4 os_sleep: process due sleep timers one at a time to stop stranding parked threads (#195)
ProcessSleepTimers popped every due timer into a private vector and then
resumed the sleepers in a loop. OSResumeThread re-enters SelectThread, which
can switch fibers away mid-loop, so the timers still in that vector were
gone from gSleepTimers while their threads stayed parked (Ready, suspended,
no timer). The reconciler healed them 100ms later and the stale-timer drop
fired when the original fiber eventually resumed.

Pop one due timer at a time straight from the shared table instead, so any
timer not yet processed stays visible to every other pump while this call
is switched away.

Co-authored-by: jordanblakepp <slamuelrose2002@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 08:30:16 +02:00
Cristian Boehm b034f7c4cc Merge branch 'patchzyy:main' into main 2026-09-07 14:45:55 -04:00
Wubbzee a135beb201 Reduce CI time using caching (#180)
* caching a little bit

* provide CMake with the explicit path to sccache.exe

* map ACTIONS_RESULTS_URL to ACTIONS_CACHE_URL so sccache can upload the
files...

* i removed the parallel oops

* small change

* doing a little bit of flag editing

* update sccache and cache nuget stuff
2026-09-07 09:18:53 +02:00
GalaxisBeast a51fc25daf Merge branch 'main' of https://github.com/GalaxisBeast/Wiicompiled into input-remapping-restored 2026-09-06 23:39:15 -04:00
GalaxisBeast f41d1f1393 add dimming when in settings and add clear mapping button 2026-09-06 23:39:11 -04:00
Cristian Boehm 6242926af9 Merge branch 'patchzyy:main' into main 2026-09-06 23:27:11 -04:00
patchzyy 88b990b060 update 2026-09-06 15:38:14 +02:00
patchzyy e0e362bd99 csnum fix 2026-09-06 15:28:56 +02:00
patchzyy 5654d8f21b Merge branch 'main' of https://github.com/patchzyy/Wiicompiled 2026-09-06 11:23:13 +02:00
patchzyy 730e3122d5 version 0.2.30 2026-09-06 11:22:57 +02:00
Wubbzee f424536d3b Treat empty MKW save as missing rather than corrupt (#168)
* treat empty mkw save as missing

first-run format zero-fills rksys.dat before any real save; a quit before
the first save left an all-zero file that read back as corrupt and trapped
the user in a delete/recreate loop. read opens now treat an all-zero
rksys.dat as absent (a real save always begins with the RKSD0006 header),
so the game recreates it from scratch. also ignore native build output.

* shorten

* I dont really want to change this to be honest.

* extra safety

---------

Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
2026-09-06 11:20:51 +02:00
patchzyy 2d9dc4e0f2 import setting.txt (#169)
* import setting.txt

* coderabbit ugh
2026-09-06 11:14:54 +02:00
patchzyy 8e57cc162f version 2026-09-06 00:48:41 +02:00
patchzyy 1c0a3edee9 Add synthetic recompilation CI workflow (#161) 2026-09-05 23:00:47 +02:00
patchzyy d1d80613cc Load console identity from NAND setting.txt (#164) 2026-09-05 23:00:23 +02:00
GalaxisBeast 88643d6884 implemented code rabbits suggestions
- Preserved NSO GameCube analog triggers.
  - Made modal closure and Escape cancel every rebind kind.
  - Deduced the native button array size; <array> already existed.
  - Centralized axis/sign decoding.
  - Kept threshold updates live, saving only when editing ends.
2026-09-05 13:59:25 -04:00
GalaxisBeast 5381c4ecf2 Update README.md
readme typo
2026-09-05 13:44:56 -04:00
GalaxisBeast e5258af5ec Update README.md 2026-09-05 13:44:06 -04:00
GalaxisBeast c57ed5f69b add keyboard support, analog triggers to digital input, and rebinding overhaul 2026-09-05 13:42:29 -04:00
patchzyy 5d67b229f6 Update settings_overlay.cpp 2026-09-05 16:54:24 +02:00
patchzyy 56db6ba641 v0.2.28 2026-09-05 16:09:34 +02:00
Michael G a67069afd3 fix(macos): increase guest fiber stack size (#154) 2026-09-05 15:45:29 +02:00
patchzyy 009697fb97 Update network_socket.cpp 2026-09-05 14:31:53 +02:00
Cristian Boehm 3f7fed48c9 Fix GC Pocket+ rumble stop handling (#148) 2026-09-05 10:20:15 +02:00
theofficialgman 6eba523d70 Switch back to LLVM 22 so build can succeed on at least some systems (#141)
two upstream LLVM bugs currently prevent building on some of the newest distros. There is no current LLVM release that works on them so we are pending fixes from LLVM
https://github.com/patchzyy/Wiicompiled/issues/136
2026-09-05 10:17:21 +02:00
48 changed files with 1135 additions and 723 deletions
+1
View File
@@ -3,3 +3,4 @@
# Patch files must stay LF: git apply matches context bytes against LF upstream sources # Patch files must stay LF: git apply matches context bytes against LF upstream sources
*.patch -text *.patch -text
translator/tests/Translator.Tests/TestAssets/**/*.bin binary
+8 -35
View File
@@ -30,6 +30,14 @@ jobs:
with: with:
dotnet-version: '8.0.x' dotnet-version: '8.0.x'
- name: Cache NuGet packages
uses: actions/cache@v5
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('translator/Translator.sln', '**/*.csproj', '**/*.props', '**/*.targets', '**/packages.lock.json', 'global.json', 'NuGet.config', 'nuget.config') }}
restore-keys: |
${{ runner.os }}-nuget-
- name: Restore - name: Restore
run: dotnet restore translator/Translator.sln run: dotnet restore translator/Translator.sln
@@ -38,38 +46,3 @@ jobs:
- name: Test - name: Test
run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal
macos_substrate:
name: macOS arm64 (configure + substrate tests)
runs-on: macos-14
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Configure native runtime
shell: bash
run: |
test "$(uname -m)" = arm64
cmake -S runtime -B build-macos -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DMKW_BUILD_PRODUCTS=OFF
grep -qx 'CMAKE_OSX_DEPLOYMENT_TARGET:STRING=12.0' \
build-macos/CMakeCache.txt
- name: Build macOS portability targets
shell: bash
run: |
cmake --build build-macos --target \
mkw_platform_paths_tests \
mkw_nand_save_tests \
mkw_nand_settings_tests \
mkw_sc_serial_tests \
mkw_input_expr_tests \
mkw_macos_native_compile \
mkw_macos_context_abi_tests \
mkw_macos_host_context_tests \
mkw_macos_guest_flat_memory_tests
- name: Test execution substrate
shell: bash
run: ctest --test-dir build-macos --output-on-failure
+6 -127
View File
@@ -1,8 +1,8 @@
name: Package installers name: Package installers
# Builds the per-platform installer/setup tool (WiiCompiled-Setup.exe / # Builds the per-platform installer/setup tool (WiiCompiled-Setup.exe /
# WiiCompiled-Setup-x86_64.AppImage / WiiCompiled-Setup.pkg) via the platform packaging scripts - # WiiCompiled-Setup-x86_64.AppImage) via Launcher/Build-Installer.ps1 and
# the same scripts a maintainer runs by hand today to # 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: # 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), # 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. # which is proprietary and not present in this repository or in CI.
@@ -11,11 +11,6 @@ on:
tags: tags:
- '*' - '*'
workflow_dispatch: workflow_dispatch:
inputs:
version:
description: Package version
required: true
type: string
permissions: permissions:
contents: read contents: read
@@ -95,130 +90,14 @@ jobs:
if-no-files-found: error if-no-files-found: error
archive: false archive: false
macos-setup-package:
name: macOS (universal Setup.pkg)
runs-on: macos-14
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '8.0.x'
- name: Verify Apple Silicon runner
shell: bash
run: |
test "$(uname -m)" = arm64
xcode-select -p
- name: Download pinned Nod tools
shell: bash
run: |
mkdir -p Launcher/artifacts/macos
nodtool_version=v2.0.0-alpha.10
nodtool_arm64_asset=nodtool-macos-arm64
nodtool_arm64_sha256=e23ca466999b720c55e6d29c9683fce8cc74451ba64ead2e543d50129f24528a
nodtool_x86_64_asset=nodtool-macos-x86_64
nodtool_x86_64_sha256=f68f504dc2b72694b468ca78b6a24142c7aa5c8800f77564297f4143682e6575
curl -fsSL --retry 3 \
"https://github.com/encounter/nod/releases/download/${nodtool_version}/${nodtool_arm64_asset}" \
-o Launcher/artifacts/macos/nodtool-arm64
curl -fsSL --retry 3 \
"https://github.com/encounter/nod/releases/download/${nodtool_version}/${nodtool_x86_64_asset}" \
-o Launcher/artifacts/macos/nodtool-x86_64
printf '%s %s\n' "$nodtool_arm64_sha256" Launcher/artifacts/macos/nodtool-arm64 | shasum -a 256 -c -
printf '%s %s\n' "$nodtool_x86_64_sha256" Launcher/artifacts/macos/nodtool-x86_64 | shasum -a 256 -c -
chmod +x Launcher/artifacts/macos/nodtool-arm64 Launcher/artifacts/macos/nodtool-x86_64
- name: Publish self-contained Translator tools
shell: bash
run: |
dotnet publish translator/src/Translator.Cli/Translator.Cli.csproj \
-c Release -r osx-arm64 --self-contained true \
-p:PublishSingleFile=true \
-o Launcher/artifacts/macos/translator-arm64
dotnet publish translator/src/Translator.Cli/Translator.Cli.csproj \
-c Release -r osx-x64 --self-contained true \
-p:PublishSingleFile=true \
-o Launcher/artifacts/macos/translator-x86_64
- name: Download pinned universal Ninja
shell: bash
run: |
ninja_version=1.13.2
ninja_sha256=c99048673aa765960a99cf10c6ddb9f1fad506099ff0a0e137ad8960a88f321b
curl -fsSL --retry 3 "https://github.com/ninja-build/ninja/releases/download/v${ninja_version}/ninja-mac.zip" -o ninja-mac.zip
printf '%s %s\n' "$ninja_sha256" ninja-mac.zip | shasum -a 256 -c -
unzip -q ninja-mac.zip -d Launcher/artifacts/macos/ninja
chmod +x Launcher/artifacts/macos/ninja/ninja
- name: Download pinned portable CMake
shell: bash
run: |
cmake_version=4.4.3
archive="cmake-${cmake_version}-macos-universal.tar.gz"
base_url="https://github.com/Kitware/CMake/releases/download/v${cmake_version}"
expected_sha256=0c5d65251c14cc884bfa16bdbed3c263ce5bffe2e21c0d0d00962cb0610464fa
curl -fsSL --retry 3 "$base_url/$archive" -o "$archive"
printf '%s %s\n' "$expected_sha256" "$archive" | shasum -a 256 -c -
tar -xzf "$archive"
mv "cmake-${cmake_version}-macos-universal/CMake.app/Contents" Launcher/artifacts/macos/cmake
- name: Build Setup.pkg
env:
PACKAGE_VERSION: ${{ inputs.version }}
TAG_VERSION: ${{ github.ref_name }}
shell: bash
run: |
package_version="$PACKAGE_VERSION"
if [[ -z "$package_version" ]]; then package_version="${TAG_VERSION#v}"; fi
mkdir -p Launcher/dist
Launcher/macos/build-setup-pkg.command \
--nodtool-arm64 Launcher/artifacts/macos/nodtool-arm64 \
--nodtool-x86_64 Launcher/artifacts/macos/nodtool-x86_64 \
--translator-arm64 Launcher/artifacts/macos/translator-arm64/Translator.Cli \
--translator-x86_64 Launcher/artifacts/macos/translator-x86_64/Translator.Cli \
--cmake-root Launcher/artifacts/macos/cmake \
--ninja-arm64 Launcher/artifacts/macos/ninja/ninja \
--ninja-x86_64 Launcher/artifacts/macos/ninja/ninja \
--output Launcher/dist/WiiCompiled-Setup.pkg \
--version "$package_version"
- name: Verify package layout and architecture-specific tools
shell: bash
run: |
pkgutil --check-signature Launcher/dist/WiiCompiled-Setup.pkg
! pkgutil --payload-files Launcher/dist/WiiCompiled-Setup.pkg | \
grep -E '/(Assets|generated|PulsarPacks|WiiCompiled.app|RetroRewind.app)(/|$)'
expanded="$RUNNER_TEMP/wiicompiled-setup-expanded"
pkgutil --expand-full Launcher/dist/WiiCompiled-Setup.pkg "$expanded"
resources="$expanded/Payload/Applications/WiiCompiled Setup.app/Contents/Resources"
for arch in arm64 x86_64; do
for tool in nodtool Translator.Cli ninja; do
lipo "$resources/tools/$arch/$tool" -verify_arch "$arch"
done
done
lipo "$resources/tools/cmake/bin/cmake" -verify_arch arm64 x86_64
bash "$resources/setup.command" --help
/usr/bin/arch -x86_64 /bin/bash "$resources/setup.command" --help
- uses: actions/upload-artifact@v7
with:
name: WiiCompiled-Setup-macos-universal
path: Launcher/dist/WiiCompiled-Setup.pkg
if-no-files-found: error
archive: false
# Publishes the packaged installers as a GitHub Release whenever a v* tag is pushed. Wheel Wizard # 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 # discovers updates from these releases, so the contract it relies on is enforced here: a full
# (non-prerelease) release whose tag is v<semver>, carrying the expected platform assets, # (non-prerelease) release whose tag is v<semver>, carrying an asset named exactly
# produced by setup hosts that report that same version. # WiiCompiled-Setup.exe, produced by a setup host that reports that same version.
release: release:
name: Publish GitHub Release name: Publish GitHub Release
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
needs: [linux-appimage, windows-installer, macos-setup-package, recompilation] needs: [linux-appimage, windows-installer, recompilation]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
@@ -264,7 +143,7 @@ jobs:
set -euo pipefail set -euo pipefail
ls -lR artifacts ls -lR artifacts
assets=() assets=()
for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage WiiCompiled-Setup.pkg; do for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage; do
found="$(find artifacts -type f -name "$name" | head -n 1)" found="$(find artifacts -type f -name "$name" | head -n 1)"
[ -n "$found" ] && [ -s "$found" ] || { echo "::error::missing release asset $name"; exit 1; } [ -n "$found" ] && [ -s "$found" ] || { echo "::error::missing release asset $name"; exit 1; }
assets+=("$found") assets+=("$found")
+21 -1
View File
@@ -28,6 +28,19 @@ jobs:
path: Launcher/artifacts/downloads path: Launcher/artifacts/downloads
key: windows-recomp-downloads-${{ hashFiles('Launcher/Prepare-PortableTools.ps1', 'Launcher/Prepare-Dependencies.ps1') }} key: windows-recomp-downloads-${{ hashFiles('Launcher/Prepare-PortableTools.ps1', 'Launcher/Prepare-Dependencies.ps1') }}
# Install sccache.
- name: Run sccache-action
uses: mozilla/sccache-action@v0.0.11
# Tell CMake to use sccache and use GitHub's API.
- name: Configure sccache environment
shell: pwsh
run: |
"SCCACHE_GHA_ENABLED=true" | Add-Content -Path $env:GITHUB_ENV
"ACTIONS_CACHE_SERVICE_V2=on" | Add-Content -Path $env:GITHUB_ENV
"CMAKE_C_COMPILER_LAUNCHER=$env:SCCACHE_PATH" | Add-Content -Path $env:GITHUB_ENV
"CMAKE_CXX_COMPILER_LAUNCHER=$env:SCCACHE_PATH" | Add-Content -Path $env:GITHUB_ENV
- name: Prepare the shipped Windows toolchain - name: Prepare the shipped Windows toolchain
shell: pwsh shell: pwsh
run: ./Launcher/Prepare-PortableTools.ps1 run: ./Launcher/Prepare-PortableTools.ps1
@@ -36,6 +49,13 @@ jobs:
shell: pwsh shell: pwsh
run: ./Launcher/Prepare-Dependencies.ps1 run: ./Launcher/Prepare-Dependencies.ps1
# Expose cache token context to the build script.
- name: Translate, compile the full runtime, and link - name: Translate, compile the full runtime, and link
shell: pwsh shell: pwsh
run: ./Launcher/Test-Recompilation.ps1 -Parallel 3 run: ./Launcher/Test-Recompilation.ps1 -Parallel 4
# Print cache results (even if the build fails)
- name: Show sccache stats
if: always()
shell: pwsh
run: sccache --show-stats
+2
View File
@@ -72,3 +72,5 @@ project.lock.json
*.log *.log
output.txt output.txt
# Operating System
.DS_Store
+11 -5
View File
@@ -117,12 +117,13 @@ function Get-MkwProjectPins([string]$ProjectFile) {
} }
function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Description, function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Description,
[string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '') { [string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '', [bool]$WaitForProcessTree = $true) {
<# <#
Runs a build tool and turns a non-zero exit code into a described failure. Start-Process -Wait Runs a build tool and turns a non-zero exit code into a described failure. By default,
is deliberate: it waits for the whole process tree, since a .NET single-file bundle host may Start-Process -Wait waits for the whole process tree, since a .NET single-file bundle host may
hand off to an extracted child that PowerShell's call operator would not wait for. Start-Process hand off to an extracted child that PowerShell's call operator would not wait for. Callers that
doesn't publish $LASTEXITCODE, so this sets it manually for callers that check it. need to avoid waiting on unrelated descendants can opt into the call-operator path.
Start-Process doesn't publish $LASTEXITCODE, so this sets it manually for callers that check it.
-StepId emits the machine-readable form the installer's progress bar consumes (BuildStepIds in -StepId emits the machine-readable form the installer's progress bar consumes (BuildStepIds in
WiiCompiled.Setup/InstallProgress.cs); the human sentence stays on the same log line. WiiCompiled.Setup/InstallProgress.cs); the human sentence stays on the same log line.
#> #>
@@ -132,9 +133,14 @@ function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Descri
if ($_.Contains('"')) { throw "A native build argument contains an unsupported quote: $_" } if ($_.Contains('"')) { throw "A native build argument contains an unsupported quote: $_" }
'"' + $_ + '"' '"' + $_ + '"'
}) })
if ($WaitForProcessTree) {
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments ` $process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
-NoNewWindow -Wait -PassThru -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode $exitCode = $process.ExitCode
} else {
& $FilePath @Arguments
$exitCode = $LASTEXITCODE
}
$global:LASTEXITCODE = $exitCode $global:LASTEXITCODE = $exitCode
if ($exitCode -ne 0) { throw "$Description failed with exit code $exitCode." } if ($exitCode -ne 0) { throw "$Description failed with exit code $exitCode." }
} }
+7 -1
View File
@@ -1,6 +1,6 @@
# Fails the release build when a fact duplicated across the repo stops agreeing with the copy # Fails the release build when a fact duplicated across the repo stops agreeing with the copy
# that owns it (recomp.yml). Scripts read pinned facts through Get-MkwProjectPins, but three # that owns it (recomp.yml). Scripts read pinned facts through Get-MkwProjectPins, but three
# consumers can't read YAML (the C++ runtime header, the C# constants, hand-written lists on # consumers can't read YAML (the C++ runtime header, the C# constants, shell scripts, and hand-written lists on
# both sides of the C#/PowerShell boundary), so those are checked here instead. # both sides of the C#/PowerShell boundary), so those are checked here instead.
[CmdletBinding()] [CmdletBinding()]
param([string]$RepositoryRoot) param([string]$RepositoryRoot)
@@ -58,6 +58,12 @@ $hostUri = Get-CapturedValue $retroWfcPayload 'CurrentRetroWfcPayloadUri\s*=\s*"
if ($hostUri -cne $pins.RetroWfcPayloadUri) { if ($hostUri -cne $pins.RetroWfcPayloadUri) {
Add-Failure "InputValidation.CurrentRetroWfcPayloadUri is '$hostUri' but recomp.yml pins '$($pins.RetroWfcPayloadUri)'." Add-Failure "InputValidation.CurrentRetroWfcPayloadUri is '$hostUri' but recomp.yml pins '$($pins.RetroWfcPayloadUri)'."
} }
$macosSetup = Read-SourceFile (Join-Path $launcher 'macos\setup.command') 'macOS setup.command'
$macosUri = Get-CapturedValue $macosSetup "'([^']*/api/wfc/payload\?g=RMCPD00)'" `
'The macOS Retro-WFC endpoint'
if ($macosUri -cne $pins.RetroWfcPayloadUri) {
Add-Failure "macOS setup.command downloads '$macosUri' but recomp.yml pins '$($pins.RetroWfcPayloadUri)'."
}
# --- The game identity: the manifest carries it, but the host also compiles a fallback for a # --- The game identity: the manifest carries it, but the host also compiles a fallback for a
# --- manifest that predates the field, and that fallback decides which disc is accepted. # --- manifest that predates the field, and that fallback decides which disc is accepted.
+4 -2
View File
@@ -136,9 +136,11 @@ try {
-CxxCompiler (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') ` -ResourceCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-windres.exe') `
-DependenciesDirectory $dependencies -AdditionalArguments @('-DMKW_BUILD_PRODUCTS=ON') -DependenciesDirectory $dependencies -AdditionalArguments @('-DMKW_BUILD_PRODUCTS=ON')
Invoke-Checked $cmake $configure 'Configuring the production Windows runtime' Invoke-Checked $cmake $configure 'Configuring the production Windows runtime' `
-WaitForProcessTree $false
Invoke-Checked $cmake @('--build', $nativeBuild, '--target', 'WiiCompiled', '--parallel', "$Parallel") ` Invoke-Checked $cmake @('--build', $nativeBuild, '--target', 'WiiCompiled', '--parallel', "$Parallel") `
'Compiling and linking the synthetic product with the full runtime' 'Compiling and linking the synthetic product with the full runtime' `
-WaitForProcessTree $false
Assert-File (Join-Path $nativeBuild 'WiiCompiled.exe') 'Linked synthetic product' Assert-File (Join-Path $nativeBuild 'WiiCompiled.exe') 'Linked synthetic product'
} finally { } finally {
$env:PATH = $oldPath $env:PATH = $oldPath
@@ -24,7 +24,7 @@ public static class RetroWfcPayload
private static readonly TimeSpan RetroWfcDownloadTimeout = TimeSpan.FromSeconds(30); private static readonly TimeSpan RetroWfcDownloadTimeout = TimeSpan.FromSeconds(30);
private static readonly TimeSpan RetroWfcRetryDelay = TimeSpan.FromSeconds(1); private static readonly TimeSpan RetroWfcRetryDelay = TimeSpan.FromSeconds(1);
public const string CurrentRetroWfcPayloadUri = "http://nas.play.rwfc.net/payload?g=RMCPD00"; public const string CurrentRetroWfcPayloadUri = "https://rwfc.net/api/wfc/payload?g=RMCPD00";
private static readonly string RetroWfcOfflinePayloadFile = private static readonly string RetroWfcOfflinePayloadFile =
Path.Combine("binary", "payload.RMCPD00.bin"); Path.Combine("binary", "payload.RMCPD00.bin");
+6 -9
View File
@@ -22,7 +22,7 @@ Usage: local-build-macos.command --output-dir DIR [options]
--retro-rewind-package-dir DIR RetroRewind6 directory (required for Retro Rewind) --retro-rewind-package-dir DIR RetroRewind6 directory (required for Retro Rewind)
--retro-wfc-offline-dir DIR Directory containing binary/payload.RMCPD00.bin --retro-wfc-offline-dir DIR Directory containing binary/payload.RMCPD00.bin
--skip-retro-wfc-payload Build Retro Rewind without the shared Retro-WFC payload --skip-retro-wfc-payload Build Retro Rewind without the shared Retro-WFC payload
--force-clean-build Delete local generated and current-architecture native build caches --force-clean-build Delete local generated and native-build-macos caches
--parallel N Pin translation and build parallelism --parallel N Pin translation and build parallelism
--cmake PATH --ninja PATH Override build tools --cmake PATH --ninja PATH Override build tools
--dotnet PATH Override dotnet --dotnet PATH Override dotnet
@@ -56,9 +56,7 @@ while (($#)); do
done done
[[ $(uname -s) == Darwin ]] || fail 'this build script is for macOS only' [[ $(uname -s) == Darwin ]] || fail 'this build script is for macOS only'
macos_arch=$(uname -m) [[ $(uname -m) == arm64 ]] || fail 'the current macOS product target is Apple Silicon only'
case "$macos_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $macos_arch" ;; esac
macos_deployment_target=12.0
workspace=$(cd "$workspace" && pwd) workspace=$(cd "$workspace" && pwd)
[[ -n "$output_dir" ]] || fail '--output-dir is required' [[ -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 case "$profile" in base|retro-rewind|both) ;; *) fail '--profile must be base, retro-rewind, or both' ;; esac
@@ -75,8 +73,7 @@ for tool in "$cmake_bin" "$ninja_bin" clang clang++ shasum; do command -v "$tool
project="$workspace/projects/mkwii/recomp.yml"; assets="$workspace/Assets"; generated="$workspace/generated" 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" 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" manifest="$manifest_dir/mkwii_base_manifest.json"; shards="$generated/build_shards"; native_build="$workspace/native-build-macos"
native_build="$workspace/native-build-macos-$macos_arch"
assert_file "$project" 'translation project' assert_file "$project" 'translation project'
if [[ -n "$game" ]]; then "$script_dir/macos/extract-disc.command" --game "$game" --assets-dir "$assets" --nodtool "$nodtool"; fi 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' assert_file "$assets/main.dol" 'extracted main.dol'; assert_file "$assets/StaticR.rel" 'extracted StaticR.rel'
@@ -134,9 +131,9 @@ if (( builds_retro )); then args+=(--resolved-profile "$mod_out/resolved_dispatc
step emit-build-shards 'Preparing native build shards'; translator "${args[@]}" step emit-build-shards 'Preparing native build shards'; translator "${args[@]}"
step configure-native 'Configuring the native toolchain' 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" -DCMAKE_OSX_ARCHITECTURES="$macos_arch" -DCMAKE_OSX_DEPLOYMENT_TARGET="$macos_deployment_target" -DAURORA_SDL3_PROVIDER=vendor -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs" "$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) 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" step compile "Compiling ${targets[*]} locally"; "$cmake_bin" --build "$native_build" --target "${targets[@]}" --parallel "$global_jobs"
if [[ "$profile" != retro-rewind ]]; then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product WiiCompiled --output-dir "${base_output_dir:-$output_dir}" --architecture "$macos_arch" --minimum-system-version "$macos_deployment_target"; fi if [[ "$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" --architecture "$macos_arch" --minimum-system-version "$macos_deployment_target"; fi if (( builds_retro )); then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product RetroRewind --output-dir "$output_dir"; fi
printf 'MKWCBUILD:OUTPUT=%s\n' "$output_dir" printf 'MKWCBUILD:OUTPUT=%s\n' "$output_dir"
+18 -66
View File
@@ -9,15 +9,12 @@ fail() { printf 'build-setup-pkg.command: error: %s\n' "$*" >&2; exit 1; }
copy_clean() { DITTONORSRC=1 ditto --norsrc --noqtn "$@"; } copy_clean() { DITTONORSRC=1 ditto --norsrc --noqtn "$@"; }
usage() { usage() {
cat <<'EOF' cat <<'EOF'
Usage: build-setup-pkg.command --nodtool-arm64 PATH --nodtool-x86_64 PATH --translator-arm64 PATH --translator-x86_64 PATH --cmake-root DIR --ninja-arm64 PATH --ninja-x86_64 PATH --output PKG [options] Usage: build-setup-pkg.command --nodtool PATH --translator PATH --cmake-root DIR --ninja PATH --output PKG [options]
Creates a game-code-free WiiCompiled Setup.pkg. The supplied tools must be Creates a game-code-free WiiCompiled Setup.pkg. The supplied tools must be
maintainer-verified, redistributable macOS artifacts for both arm64 and maintainer-verified, redistributable macOS arm64 artifacts. The resulting pkg
x86_64. The setup package selects native tools for its host while the game is is unsigned unless --installer-identity is supplied; releases should sign and
compiled locally for that host architecture. CMake must be universal2. The notarize it with a Developer ID Installer certificate.
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) --workspace DIR Repository root (default: script's grandparent)
--version VERSION Bundle/package version (default: 0.1.0) --version VERSION Bundle/package version (default: 0.1.0)
@@ -26,17 +23,14 @@ EOF
} }
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace=$(cd "$script_dir/../.." && pwd); nodtool_arm64=""; nodtool_x86_64=""; translator_arm64=""; translator_x86_64=""; cmake_root=""; ninja_arm64=""; ninja_x86_64=""; output=""; version=0.1.0; identity="" workspace=$(cd "$script_dir/../.." && pwd); nodtool=""; translator=""; cmake_root=""; ninja=""; output=""; version=0.1.0; identity=""
while (($#)); do while (($#)); do
case "$1" in case "$1" in
--workspace) workspace=${2:-}; shift 2 ;; --workspace) workspace=${2:-}; shift 2 ;;
--nodtool-arm64) nodtool_arm64=${2:-}; shift 2 ;; --nodtool) nodtool=${2:-}; shift 2 ;;
--nodtool-x86_64) nodtool_x86_64=${2:-}; shift 2 ;; --translator) translator=${2:-}; shift 2 ;;
--translator-arm64) translator_arm64=${2:-}; shift 2 ;;
--translator-x86_64) translator_x86_64=${2:-}; shift 2 ;;
--cmake-root) cmake_root=${2:-}; shift 2 ;; --cmake-root) cmake_root=${2:-}; shift 2 ;;
--ninja-arm64) ninja_arm64=${2:-}; shift 2 ;; --ninja) ninja=${2:-}; shift 2 ;;
--ninja-x86_64) ninja_x86_64=${2:-}; shift 2 ;;
--output) output=${2:-}; shift 2 ;; --output) output=${2:-}; shift 2 ;;
--version) version=${2:-}; shift 2 ;; --version) version=${2:-}; shift 2 ;;
--installer-identity) identity=${2:-}; shift 2 ;; --installer-identity) identity=${2:-}; shift 2 ;;
@@ -48,48 +42,12 @@ version=${version#v}
[[ "$version" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]] || fail '--version must contain one to three period-separated integers' [[ "$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" IFS=. read -r version_major version_minor version_patch <<< "$version"
short_version="$version_major.${version_minor:-0}.${version_patch:-0}" short_version="$version_major.${version_minor:-0}.${version_patch:-0}"
for tool in pkgbuild productbuild ditto codesign lipo; do command -v "$tool" >/dev/null || fail "required macOS tool unavailable: $tool"; done for tool in pkgbuild productbuild ditto codesign; do command -v "$tool" >/dev/null || fail "required macOS tool unavailable: $tool"; done
for tool_path in "$nodtool_arm64" "$nodtool_x86_64" "$translator_arm64" "$translator_x86_64" "$ninja_arm64" "$ninja_x86_64"; do [[ -x "$tool_path" ]] || fail 'each architecture-specific tool must name an executable'; done [[ -x "$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 "$cmake_root/bin/cmake" ]] || fail '--cmake-root must contain bin/cmake'
require_arch() { [[ -x "$ninja" ]] || fail '--ninja must name an executable'
local artifact=$1 arch=$2 label=$3 "$nodtool" --version >/dev/null || fail '--nodtool did not run successfully'
lipo "$artifact" -verify_arch "$arch" >/dev/null 2>&1 || fail "$label must contain a $arch slice: $artifact"
}
require_arch "$nodtool_arm64" arm64 '--nodtool-arm64'; require_arch "$nodtool_x86_64" x86_64 '--nodtool-x86_64'
require_arch "$translator_arm64" arm64 '--translator-arm64'; require_arch "$translator_x86_64" x86_64 '--translator-x86_64'
require_arch "$ninja_arm64" arm64 '--ninja-arm64'; require_arch "$ninja_x86_64" x86_64 '--ninja-x86_64'
lipo "$cmake_root/bin/cmake" -verify_arch arm64 x86_64 >/dev/null 2>&1 || fail '--cmake-root/bin/cmake must be universal2'
# Slice checks above prevent accidental cross-architecture packaging. Exercise
# each supplied executable as well: an incorrectly bundled runtime can have a
# valid Mach-O header but still fail before the setup app can use it. Apple
# Silicon maintainers validate Intel tools through Rosetta when it is present.
host_arch=$(uname -m)
run_for_arch() {
local arch=$1 label=$2
shift 2
if [[ "$arch" == "$host_arch" ]]; then
"$@" >/dev/null || fail "$label did not run successfully"
elif [[ "$host_arch" == arm64 && "$arch" == x86_64 ]] && /usr/bin/arch -x86_64 /usr/bin/true >/dev/null 2>&1; then
/usr/bin/arch -x86_64 "$@" >/dev/null || fail "$label did not run successfully under Rosetta"
else
# Intel hosts cannot execute arm64 binaries. The slice remains checked
# above; CI or an Apple Silicon maintainer must execute that tool set.
printf 'build-setup-pkg.command: warning: unable to execute %s on %s; architecture slice was verified, but run it in %s CI before release\n' \
"$label" "$host_arch" "$arch" >&2
fi
}
for arch in arm64 x86_64; do
if [[ "$arch" == arm64 ]]; then
nodtool=$nodtool_arm64; translator=$translator_arm64; ninja=$ninja_arm64
else
nodtool=$nodtool_x86_64; translator=$translator_x86_64; ninja=$ninja_x86_64
fi
run_for_arch "$arch" "--nodtool-$arch" "$nodtool" --version
run_for_arch "$arch" "--translator-$arch" "$translator" --help
run_for_arch "$arch" "--ninja-$arch" "$ninja" --version
done
run_for_arch "$host_arch" '--cmake-root/bin/cmake' "$cmake_root/bin/cmake" --version
workspace=$(cd "$workspace" && pwd); output=$(cd "$(dirname "$output")" && pwd)/$(basename "$output") workspace=$(cd "$workspace" && pwd); output=$(cd "$(dirname "$output")" && pwd)/$(basename "$output")
stage=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-pkg.XXXXXX") stage=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-pkg.XXXXXX")
trap 'rm -rf "$stage"' EXIT trap 'rm -rf "$stage"' EXIT
@@ -106,7 +64,7 @@ cat > "$app/Contents/Info.plist" <<EOF
<key>CFBundlePackageType</key><string>APPL</string> <key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>$short_version</string> <key>CFBundleShortVersionString</key><string>$short_version</string>
<key>CFBundleVersion</key><string>$version</string> <key>CFBundleVersion</key><string>$version</string>
<key>LSMinimumSystemVersion</key><string>12.0</string> <key>LSMinimumSystemVersion</key><string>14.0</string>
</dict></plist> </dict></plist>
EOF EOF
cat > "$app/Contents/MacOS/WiiCompiledSetup" <<'EOF' cat > "$app/Contents/MacOS/WiiCompiledSetup" <<'EOF'
@@ -145,17 +103,11 @@ copy_clean "$workspace/Launcher/local-build-macos.command" "$resources/workspace
copy_clean "$workspace/Launcher/macos/extract-disc.command" "$resources/workspace/Launcher/macos/extract-disc.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" 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 chmod +x "$resources/workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/macos/"*.command
# setup.command uses this marker to refresh source inputs in an existing user mkdir -p "$resources/tools/cmake"
# workspace without replacing extracted game assets or Retro Rewind files. copy_clean "$nodtool" "$resources/tools/nodtool"; chmod +x "$resources/tools/nodtool"
printf '%s\n' "$version" > "$resources/workspace/.bundle-version" copy_clean "$translator" "$resources/tools/Translator.Cli"; chmod +x "$resources/tools/Translator.Cli"
mkdir -p "$resources/tools/cmake" "$resources/tools/arm64" "$resources/tools/x86_64"
copy_clean "$nodtool_arm64" "$resources/tools/arm64/nodtool"; chmod +x "$resources/tools/arm64/nodtool"
copy_clean "$nodtool_x86_64" "$resources/tools/x86_64/nodtool"; chmod +x "$resources/tools/x86_64/nodtool"
copy_clean "$translator_arm64" "$resources/tools/arm64/Translator.Cli"; chmod +x "$resources/tools/arm64/Translator.Cli"
copy_clean "$translator_x86_64" "$resources/tools/x86_64/Translator.Cli"; chmod +x "$resources/tools/x86_64/Translator.Cli"
copy_clean "$cmake_root" "$resources/tools/cmake" copy_clean "$cmake_root" "$resources/tools/cmake"
copy_clean "$ninja_arm64" "$resources/tools/arm64/ninja"; chmod +x "$resources/tools/arm64/ninja" copy_clean "$ninja" "$resources/tools/ninja"; chmod +x "$resources/tools/ninja"
copy_clean "$ninja_x86_64" "$resources/tools/x86_64/ninja"; chmod +x "$resources/tools/x86_64/ninja"
copy_clean "$workspace/LICENSE" "$resources/LICENSE" copy_clean "$workspace/LICENSE" "$resources/LICENSE"
copy_clean "$workspace/THIRD-PARTY-NOTICES.md" "$resources/THIRD-PARTY-NOTICES.md" copy_clean "$workspace/THIRD-PARTY-NOTICES.md" "$resources/THIRD-PARTY-NOTICES.md"
codesign --force --deep --sign - "$app" codesign --force --deep --sign - "$app"
@@ -1,11 +0,0 @@
# Cross-compile a thin x86_64 macOS build from an Apple Silicon Mac.
# Pass this file on the first configure with:
# -DCMAKE_TOOLCHAIN_FILE=/absolute/path/to/macos-x86_64-toolchain.cmake
#
# CMAKE_SYSTEM_PROCESSOR is deliberately declared here rather than inferred
# from CMAKE_OSX_ARCHITECTURES, so target-aware CMake dependencies select their
# x86_64 artifacts.
set(CMAKE_SYSTEM_NAME Darwin)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_OSX_ARCHITECTURES x86_64 CACHE STRING
"Target macOS architectures" FORCE)
+10 -55
View File
@@ -5,36 +5,28 @@ set -euo pipefail
fail() { printf 'publish-app.command: error: %s\n' "$*" >&2; exit 1; } fail() { printf 'publish-app.command: error: %s\n' "$*" >&2; exit 1; }
usage() { usage() {
cat <<'EOF' cat <<'EOF'
Usage: publish-app.command --build-dir DIR --product {WiiCompiled|RetroRewind} --output-dir DIR [options] Usage: publish-app.command --build-dir DIR --product {WiiCompiled|RetroRewind} --output-dir DIR
Copies a locally built product and its runtime assets into OUTPUT-DIR/<product>.app. 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 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 result. This is suitable for local use; a release must replace ad-hoc signing
with the project's Developer ID signing and notarization process. with the project's Developer ID signing and notarization process.
--architecture {arm64|x86_64} Required architecture of the compiled product (default: host)
--minimum-system-version VERSION App bundle minimum macOS version (default: 12.0)
EOF EOF
} }
build_dir=""; product=""; output_dir=""; architecture=$(uname -m); minimum_system_version=12.0 build_dir=""; product=""; output_dir=""
while (($#)); do while (($#)); do
case "$1" in case "$1" in
--build-dir) build_dir=${2:-}; shift 2 ;; --build-dir) build_dir=${2:-}; shift 2 ;;
--product) product=${2:-}; shift 2 ;; --product) product=${2:-}; shift 2 ;;
--output-dir) output_dir=${2:-}; shift 2 ;; --output-dir) output_dir=${2:-}; shift 2 ;;
--architecture) architecture=${2:-}; shift 2 ;;
--minimum-system-version) minimum_system_version=${2:-}; shift 2 ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
*) fail "unknown option: $1" ;; *) fail "unknown option: $1" ;;
esac esac
done done
[[ "$product" == WiiCompiled || "$product" == RetroRewind ]] || fail '--product must be WiiCompiled or RetroRewind' [[ "$product" == WiiCompiled || "$product" == RetroRewind ]] || fail '--product must be WiiCompiled or RetroRewind'
[[ "$architecture" == arm64 || "$architecture" == x86_64 ]] || fail '--architecture must be arm64 or x86_64' for tool in codesign ditto install_name_tool otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
[[ "$minimum_system_version" =~ ^[0-9]+(\.[0-9]+){1,2}$ ]] || fail '--minimum-system-version must contain two or three period-separated integers'
for tool in codesign ditto install_name_tool lipo otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
[[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product" [[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product"
lipo "$build_dir/$product" -verify_arch "$architecture" || fail "compiled product is not $architecture: $build_dir/$product"
for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done 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" app="$output_dir/$product.app"
@@ -55,7 +47,7 @@ cat > "$app/Contents/Info.plist" <<EOF
<key>CFBundlePackageType</key><string>APPL</string> <key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>0.1.0</string> <key>CFBundleShortVersionString</key><string>0.1.0</string>
<key>CFBundleVersion</key><string>1</string> <key>CFBundleVersion</key><string>1</string>
<key>LSMinimumSystemVersion</key><string>$minimum_system_version</string> <key>LSMinimumSystemVersion</key><string>14.0</string>
<key>NSHighResolutionCapable</key><true/> <key>NSHighResolutionCapable</key><true/>
</dict></plist> </dict></plist>
EOF EOF
@@ -65,62 +57,25 @@ for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do
ln -s "../Resources/$asset" "$macos/$asset" ln -s "../Resources/$asset" "$macos/$asset"
done done
# Resolve a non-system dependency from the build product's rpaths. This covers # Build a closure of Homebrew dylibs. System libraries remain system references.
# both traditional Homebrew dylibs and the vendored dylibs CMake emits under
# the local build directory for a cross-architecture build.
dependency_path() {
local current=$1 dependency=$2 name rpath candidate
case "$dependency" in
/opt/homebrew/*|/usr/local/*)
[[ -f "$dependency" ]] && { printf '%s\n' "$dependency"; return 0; }
;;
@rpath/*)
name=${dependency##*/}
while IFS= read -r rpath; do
case "$rpath" in
@loader_path/*) rpath="$(dirname "$current")/${rpath#@loader_path/}" ;;
@executable_path/*) rpath="$macos/${rpath#@executable_path/}" ;;
esac
candidate="$rpath/$name"
[[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; }
done < <(otool -l "$current" | awk '
/LC_RPATH/ { rpath = 1; next }
rpath && /^[[:space:]]*path / {
sub(/^[[:space:]]*path /, "")
sub(/ \(offset [0-9]+\)$/, "")
print
rpath = 0
}
')
;;
@loader_path/*)
candidate="$(dirname "$current")/${dependency#@loader_path/}"
[[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; }
;;
esac
return 1
}
# Build a closure of non-system dylibs. System libraries remain system
# references, while every resolved dependency is copied beside the executable.
queue=("$macos/$product") queue=("$macos/$product")
while ((${#queue[@]})); do while ((${#queue[@]})); do
current=${queue[0]} current=${queue[0]}
queue=("${queue[@]:1}") queue=("${queue[@]:1}")
while IFS= read -r dependency; do while IFS= read -r dependency; do
dependency_path=$(dependency_path "$current" "$dependency") || continue [[ "$dependency" == /opt/homebrew/* || "$dependency" == /usr/local/* ]] || continue
[[ -f "$dependency" ]] || continue
name=$(basename "$dependency") name=$(basename "$dependency")
if [[ ! -f "$frameworks/$name" ]]; then if [[ ! -f "$frameworks/$name" ]]; then
ditto "$dependency_path" "$frameworks/$name" ditto "$dependency" "$frameworks/$name"
install_name_tool -id "@rpath/$name" "$frameworks/$name" install_name_tool -id "@rpath/$name" "$frameworks/$name"
# Resolve transitive @loader_path dependencies relative to the queue+=("$frameworks/$name")
# original dylib, not its copied Frameworks location.
queue+=("$dependency_path")
fi fi
done < <(otool -L "$current" | tail -n +2 | awk '{print $1}') done < <(otool -L "$current" | tail -n +2 | awk '{print $1}')
done done
while IFS= read -r binary; do while IFS= read -r binary; do
while IFS= read -r old; do while IFS= read -r old; do
[[ "$old" == /opt/homebrew/* || "$old" == /usr/local/* ]] || continue
name=$(basename "$old") name=$(basename "$old")
[[ -f "$frameworks/$name" ]] || continue [[ -f "$frameworks/$name" ]] || continue
if [[ "$binary" == "$macos/$product" ]]; then if [[ "$binary" == "$macos/$product" ]]; then
+6 -34
View File
@@ -5,7 +5,10 @@ set -euo pipefail
resources=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) resources=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace_source="$resources/workspace" workspace_source="$resources/workspace"
nodtool="$resources/tools/nodtool"
translator="$resources/tools/Translator.Cli"
cmake_bin="$resources/tools/cmake/bin/cmake" cmake_bin="$resources/tools/cmake/bin/cmake"
ninja_bin="$resources/tools/ninja"
support_root="$HOME/Library/Application Support/WiiCompiled" support_root="$HOME/Library/Application Support/WiiCompiled"
workspace="$support_root/BuildWorkspace" workspace="$support_root/BuildWorkspace"
products="$support_root/Products" products="$support_root/Products"
@@ -33,13 +36,6 @@ while (($#)); do
done done
[[ "$install_location" == user || "$install_location" == applications ]] || fail '--install-location must be user or applications' [[ "$install_location" == user || "$install_location" == applications ]] || fail '--install-location must be user or applications'
host_arch=$(uname -m)
case "$host_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $host_arch" ;; esac
host_tools="$resources/tools/$host_arch"
nodtool="$host_tools/nodtool"
translator="$host_tools/Translator.Cli"
ninja_bin="$host_tools/ninja"
if [[ -z "$game" ]]; then if [[ -z "$game" ]]; then
game=$(/usr/bin/osascript <<'APPLESCRIPT' game=$(/usr/bin/osascript <<'APPLESCRIPT'
set selectedFile to choose file with prompt "Choose your clean Mario Kart Wii PAL (RMCP01) disc image" set selectedFile to choose file with prompt "Choose your clean Mario Kart Wii PAL (RMCP01) disc image"
@@ -64,36 +60,12 @@ if ! /usr/bin/xcode-select -p >/dev/null 2>&1; then
/usr/bin/xcode-select --install || true /usr/bin/xcode-select --install || true
exit 1 exit 1
fi fi
for tool in "$nodtool" "$translator" "$cmake_bin" "$ninja_bin"; do
/usr/bin/lipo "$tool" -verify_arch "$host_arch" >/dev/null 2>&1 || \
fail "the packaged $(basename "$tool") does not support $host_arch"
done
mkdir -p "$support_root" "$products" mkdir -p "$support_root" "$products"
source_bundle_version="$workspace_source/.bundle-version" if [[ ! -d "$workspace/.git" && ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then
workspace_bundle_version="$workspace/.bundle-version"
needs_workspace_refresh=0
if [[ ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then
needs_workspace_refresh=1
elif [[ -f "$source_bundle_version" ]] && [[ ! -f "$workspace_bundle_version" || "$(<"$source_bundle_version")" != "$(<"$workspace_bundle_version")" ]]; then
needs_workspace_refresh=1
fi
if (( needs_workspace_refresh )); then
printf 'Preparing the local build workspace...\n' printf 'Preparing the local build workspace...\n'
if [[ ! -d "$workspace" ]]; then rm -rf "$workspace"
/usr/bin/ditto "$workspace_source" "$workspace" /usr/bin/ditto "$workspace_source" "$workspace"
else
# Refresh only packaged source inputs. Assets and the staged Retro
# Rewind package belong to the user and stay in place.
for source in aurora-main projects runtime translator Launcher; do
/usr/bin/ditto "$workspace_source/$source" "$workspace/$source"
done
/usr/bin/ditto "$source_bundle_version" "$workspace_bundle_version"
# A dependency provider can be cached in this directory, so make the
# refreshed sources configure from a clean native build tree.
rm -rf "$workspace/native-build-macos-arm64" "$workspace/native-build-macos-x86_64"
fi
fi fi
profile=base profile=base
@@ -118,7 +90,7 @@ if [[ -n "$retro_dir" ]]; then
trap 'rm -rf "$payload_stage"' EXIT trap 'rm -rf "$payload_stage"' EXIT
/usr/bin/curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \ /usr/bin/curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \
--retry 1 --output "$temporary_payload" \ --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' 'https://rwfc.net/api/wfc/payload?g=RMCPD00' || fail 'could not download the Retro-WFC payload needed for online play'
"$translator" validate-retro-wfc-payload --directory "$payload_stage" || \ "$translator" validate-retro-wfc-payload --directory "$payload_stage" || \
fail 'downloaded Retro-WFC payload failed signature validation' fail 'downloaded Retro-WFC payload failed signature validation'
mkdir -p "$retro_wfc_dir/binary" mkdir -p "$retro_wfc_dir/binary"
+1
View File
@@ -233,5 +233,6 @@ EOF
rm -rf "$test_dir" rm -rf "$test_dir"
trap - EXIT trap - EXIT
rm -rf "$toolchain_dir"
mv "$work" "$toolchain_dir" mv "$work" "$toolchain_dir"
echo "prepare-portable-tools.sh: toolchain ready at $toolchain_dir ($(du -sh "$toolchain_dir" | cut -f1))" echo "prepare-portable-tools.sh: toolchain ready at $toolchain_dir ($(du -sh "$toolchain_dir" | cut -f1))"
+19 -18
View File
@@ -54,10 +54,24 @@ Press **F10** while the game window has focus:
Everything you change is saved to `Config.toml` on the spot and restored next launch. Everything you change is saved to `Config.toml` on the spot and restored next launch.
**Real controller support.** **Real controller support.**
Controllers are fed to the game as a GameCube controller. Controllers are fed to the game as a GameCube controller.
Mappings are positional (`south`, `east`, `west`, `north`) rather than Xbox-labelled, so the Mappings are positional (`south`, `east`, `west`, `north`) rather than Xbox-labelled, so the
same config makes sense on Xbox, PlayStation, Nintendo and generic SDL pads alike, and extra same config makes sense on Xbox, PlayStation, Nintendo and generic SDL pads alike, and extra
inputs like paddles, touchpads and share buttons show up when the hardware reports them. inputs like paddles, touchpads and share buttons show up when the hardware reports them.
Both button-binding slots also accept SDL triggers and stick directions. Selecting an analog
input shows a threshold slider beneath it (1100%, default 50%); reaching that amount of travel
holds the chosen digital button. Each binding's threshold is saved independently in `Config.toml`
(for example, `a = "right_trigger@35,south"`).
**Keyboard and Mouse support.**
Keyboard and mouse are also available through **F10 > Controller settings > Keyboard and mouse**
for each port. Enabling this replaces that port's gamepad input. The default preset uses WASD
for the main stick, left mouse for A (accelerate), Space for B (brake), right mouse for R
(drift), middle mouse for Z (item), arrow keys for the D-pad (tricks), and Enter for Start.
Keys and mouse buttons can be remapped, including both sticks and triggers; mouse movement
is not used. These settings are saved in `keyboard_bindings.dat` and restored next launch.
**Dolphin-compatible input expressions.** **Dolphin-compatible input expressions.**
Each GameCube control can carry an expression in Dolphin's input syntax, with the same operators Each GameCube control can carry an expression in Dolphin's input syntax, with the same operators
@@ -89,8 +103,8 @@ Known limitations of the Wii Remote path:
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher - GPU: GTX 1650 / RX 6400 / Arc A310 or higher
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher - CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
- About 20 GB of free disk space during installation (Final game size ~5 GB) - About 20 GB of free disk space during installation (Final game size ~5 GB)
- macOS 12 (Monterey) or later on Apple Silicon (`arm64`) or Intel (`x86_64-v3`); pre-Haswell Intel CPUs are unsupported - macOS 14 (Sonoma) or later on Apple Silicon
- On macOS, a Metal-capable GPU and Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing) - On macOS, Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM, - A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
GCZ, CISO, WBFS, WIA and RVZ are accepted. GCZ, CISO, WBFS, WIA and RVZ are accepted.
@@ -111,21 +125,6 @@ image under Settings, turn on **WiiCompiled (beta)**, and hit install from the H
Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it. launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
### macOS
Download `WiiCompiled-Setup.pkg` from this repository's Releases page and open it. It requires an
Apple Silicon Mac because its bundled nodtool and Translator.Cli executables are arm64. It installs
**WiiCompiled Setup** in Applications; open that app, choose your clean PAL `RMCP01` disc image,
and select either the base game or Retro Rewind. For Retro Rewind, choose the `RetroRewind6` folder
or its parent folder.
Setup verifies and extracts the image locally, then translates and compiles the native app on your
Mac. On a first run it may ask macOS to install Xcode Command Line Tools; complete Apple's installer,
then open Setup again. When the build completes, Setup asks for administrator approval once to install
`WiiCompiled.app` (and, if selected, `RetroRewind.app`) in `/Applications`.
Setup opens Terminal while it works, so the extraction and build progress—and any error that needs
reporting—remain visible.
> [!CAUTION] > [!CAUTION]
> Only take builds from this repository's > Only take builds from this repository's
@@ -165,7 +164,9 @@ The default test suite needs no binaries and no host C++ compiler, so you can ha
translator without any game data around. translator without any game data around.
For everything beyond that, feeding in your own `main.dol`/`StaticR.rel`, running the For everything beyond that, feeding in your own `main.dol`/`StaticR.rel`, running the
translation, generating the manifest and build graph, and compiling. see [`translator/README.md`](translator/README.md). translation, generating the manifest and build graph, and compiling, see [`translator/README.md`](translator/README.md).
For a step-by-step guide on compiling both WiiCompiled and Retro Rewind from source on macOS (Apple Silicon), see the [macOS Build Guide](docs/building-macos.md).
## FAQ ## FAQ
+2 -5
View File
@@ -41,10 +41,7 @@ if (_aurora_sdl3_provider STREQUAL "auto")
set(_aurora_sdl3_provider "package") set(_aurora_sdl3_provider "package")
else () else ()
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON) set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
# Aurora uses APIs from the SDL version pinned by AURORA_SDL3_VERSION. find_package(SDL3 QUIET)
# Do not silently select an older system package and fail later while
# compiling its headers.
find_package(SDL3 ${AURORA_SDL3_VERSION} QUIET)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF) set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
if (SDL3_FOUND) if (SDL3_FOUND)
set(_aurora_sdl3_provider "system") set(_aurora_sdl3_provider "system")
@@ -61,7 +58,7 @@ if (_aurora_sdl3_provider STREQUAL "system")
message(STATUS "aurora: Using system SDL3 (provider=system)") message(STATUS "aurora: Using system SDL3 (provider=system)")
if (NOT SDL3_FOUND) if (NOT SDL3_FOUND)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON) set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
find_package(SDL3 ${AURORA_SDL3_VERSION} REQUIRED) find_package(SDL3 REQUIRED)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF) set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
endif () endif ()
_aurora_sdl3_select_target() _aurora_sdl3_select_target()
+1 -3
View File
@@ -41,9 +41,7 @@ if (AURORA_ENABLE_GX)
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_METAL) target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_METAL)
target_sources(aurora_core PRIVATE lib/dawn/MetalBinding.mm) target_sources(aurora_core PRIVATE lib/dawn/MetalBinding.mm)
set_source_files_properties(lib/dawn/MetalBinding.mm PROPERTIES COMPILE_FLAGS -fobjc-arc) set_source_files_properties(lib/dawn/MetalBinding.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
target_link_options(aurora_core PUBLIC target_link_options(aurora_core PUBLIC "LINKER:-weak_framework,Metal")
"LINKER:-weak_framework,Metal"
"LINKER:-U,_OBJC_CLASS_$_MTLLogStateDescriptor")
endif () endif ()
if (DAWN_ENABLE_D3D11) if (DAWN_ENABLE_D3D11)
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_D3D11) target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_D3D11)
-50
View File
@@ -16,60 +16,10 @@ function(aurora_find_package_global)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ${_PREV_FIND_PACKAGE_TARGETS_GLOBAL}) set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ${_PREV_FIND_PACKAGE_TARGETS_GLOBAL})
endfunction() endfunction()
# A macOS build for another architecture must not discover Homebrew packages
# built for its physical host. CMAKE_CROSSCOMPILING is not sufficient here:
# Apple Clang can target another architecture through CMAKE_OSX_ARCHITECTURES
# without CMake considering the configure a cross-build.
set(_AURORA_EXCLUDE_HOST_HOMEBREW FALSE)
if (APPLE AND CMAKE_OSX_ARCHITECTURES)
list(LENGTH CMAKE_OSX_ARCHITECTURES _AURORA_OSX_ARCH_COUNT)
if (_AURORA_OSX_ARCH_COUNT EQUAL 1)
list(GET CMAKE_OSX_ARCHITECTURES 0 _AURORA_TARGET_ARCH)
string(TOLOWER "${_AURORA_TARGET_ARCH}" _AURORA_TARGET_ARCH)
# hw.optional.arm64 identifies Apple Silicon even when CMake itself runs
# through Rosetta, where CMAKE_HOST_SYSTEM_PROCESSOR reports x86_64.
execute_process(
COMMAND /usr/sbin/sysctl -n hw.optional.arm64
RESULT_VARIABLE _AURORA_ARM64_PROBE_RESULT
OUTPUT_VARIABLE _AURORA_ARM64_PROBE
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE)
if (_AURORA_ARM64_PROBE_RESULT EQUAL 0 AND _AURORA_ARM64_PROBE STREQUAL "1")
set(_AURORA_HOST_ARCH arm64)
else ()
execute_process(
COMMAND /usr/bin/uname -m
OUTPUT_VARIABLE _AURORA_HOST_ARCH
OUTPUT_STRIP_TRAILING_WHITESPACE)
string(TOLOWER "${_AURORA_HOST_ARCH}" _AURORA_HOST_ARCH)
endif ()
if (_AURORA_TARGET_ARCH STREQUAL "x86_64" AND _AURORA_HOST_ARCH MATCHES "^(arm64|aarch64)$")
list(APPEND CMAKE_IGNORE_PREFIX_PATH "/opt/homebrew")
set(_AURORA_EXCLUDE_HOST_HOMEBREW TRUE)
elseif (_AURORA_TARGET_ARCH MATCHES "^(arm64|aarch64)$" AND _AURORA_HOST_ARCH MATCHES "^(x86_64|amd64)$")
list(APPEND CMAKE_IGNORE_PREFIX_PATH "/usr/local")
set(_AURORA_EXCLUDE_HOST_HOMEBREW TRUE)
endif ()
endif ()
endif ()
if (_AURORA_EXCLUDE_HOST_HOMEBREW)
list(REMOVE_DUPLICATES CMAKE_IGNORE_PREFIX_PATH)
message(STATUS "aurora: cross-architecture macOS build; ignoring host Homebrew prefixes")
endif ()
if (AURORA_ENABLE_GX) if (AURORA_ENABLE_GX)
include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/AuroraDawnProvider.cmake) include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/AuroraDawnProvider.cmake)
endif () endif ()
# SDL's pkg-config probe can bypass CMake's prefix exclusion. macOS does not
# need libusb for the supported SDL input paths, so keep that host-only library
# out of a cross-architecture configure.
if (_AURORA_EXCLUDE_HOST_HOMEBREW)
set(SDL_HIDAPI_LIBUSB OFF CACHE BOOL "" FORCE)
set(SDL_HIDAPI_LIBUSB_SHARED OFF CACHE BOOL "" FORCE)
endif ()
# Abseil is needed for core libraries. It normally comes via Dawn's vendor build. # Abseil is needed for core libraries. It normally comes via Dawn's vendor build.
# Otherwise prefer a system package and only fetch it as a last resort. # Otherwise prefer a system package and only fetch it as a last resort.
if (NOT TARGET absl::flat_hash_map OR NOT TARGET absl::btree) if (NOT TARGET absl::flat_hash_map OR NOT TARGET absl::btree)
+16
View File
@@ -171,6 +171,22 @@ typedef struct PADButtonMapping {
PADButton padButton; PADButton padButton;
} PADButtonMapping; } PADButtonMapping;
// Explicitly disabled, unlike INVALID which permits default L/R trigger input.
#define PAD_NATIVE_BUTTON_DISABLED 0xfffffffeu
// Axis-to-button bindings share the persisted nativeButton field without
// changing the binary layout of existing controller mapping files.
constexpr u32 PADEncodeAxisButton(u32 axis, bool negative, u32 threshold = 50) {
return 0x10000u | axis | (negative ? 0x80u : 0u) | (threshold << 8);
}
constexpr bool PADIsAxisButton(u32 binding) { return (binding & 0xffff0000u) == 0x10000u; }
constexpr u32 PADAxisButtonThreshold(u32 binding) { return (binding >> 8) & 0xffu; }
constexpr u32 PADAxisButtonAxis(u32 binding) { return binding & 0x7fu; }
constexpr bool PADAxisButtonNegative(u32 binding) { return (binding & 0x80u) != 0; }
constexpr u32 PADAxisButtonIdentity(u32 binding) {
return PADIsAxisButton(binding) ? (binding & ~0xff00u) : binding;
}
typedef struct PADAxisMapping { typedef struct PADAxisMapping {
PADSignedNativeAxis nativeAxis; PADSignedNativeAxis nativeAxis;
s32 nativeButton; s32 nativeButton;
+58 -35
View File
@@ -6,10 +6,10 @@
#include <SDL3/SDL_mouse.h> #include <SDL3/SDL_mouse.h>
#include <SDL3/SDL_joystick.h> #include <SDL3/SDL_joystick.h>
#include <algorithm>
#include <array> #include <array>
#include <atomic> #include <atomic>
#include <sys/stat.h> #include <sys/stat.h>
#include <ranges>
namespace { namespace {
constexpr int32_t k_mappingsFileVersion = 3; constexpr int32_t k_mappingsFileVersion = 3;
@@ -319,6 +319,18 @@ std::array<bool, PAD_CHANMAX> g_suppressLeftTrigger{};
std::array<bool, PAD_CHANMAX> g_suppressRightTrigger{}; std::array<bool, PAD_CHANMAX> g_suppressRightTrigger{};
bool is_mouse_scancode(const s32 scancode) { return scancode < PAD_KEY_INVALID; } bool is_mouse_scancode(const s32 scancode) { return scancode < PAD_KEY_INVALID; }
bool is_native_binding_pressed(SDL_Gamepad* gamepad, u32 binding) {
if (PADIsAxisButton(binding)) {
const u32 axis = PADAxisButtonAxis(binding);
const u32 threshold = PADAxisButtonThreshold(binding);
if (axis >= SDL_GAMEPAD_AXIS_COUNT || threshold < 1 || threshold > 100) return false;
int value = SDL_GetGamepadAxis(gamepad, static_cast<SDL_GamepadAxis>(axis));
if (PADAxisButtonNegative(binding)) value = -value;
return value > 0 && value * 100 >= static_cast<int>(threshold) * 32767;
}
return binding < SDL_GAMEPAD_BUTTON_COUNT &&
SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(binding));
}
bool is_mouse_button_pressed(const s32 scancode) { bool is_mouse_button_pressed(const s32 scancode) {
const int32_t buttonNum = -(scancode + 1); const int32_t buttonNum = -(scancode + 1);
if (buttonNum < 1 || buttonNum > 5) { if (buttonNum < 1 || buttonNum > 5) {
@@ -342,7 +354,7 @@ BOOL PADInit() {
} }
g_initialized = true; g_initialized = true;
std::for_each(g_keyboardBindings.begin(), g_keyboardBindings.end(), [](auto& state) { std::ranges::for_each(g_keyboardBindings, [](auto& state) {
state.m_buttonMapping = g_defaultKeys; state.m_buttonMapping = g_defaultKeys;
state.m_axisMapping = g_defaultKeyAxis; state.m_axisMapping = g_defaultKeyAxis;
}); });
@@ -634,8 +646,8 @@ static void EnsureMappingLoaded(aurora::input::GameController* controller) {
static Sint16 _get_axis_value(const aurora::input::GameController* controller, // NOLINT(*-reserved-identifier) static Sint16 _get_axis_value(const aurora::input::GameController* controller, // NOLINT(*-reserved-identifier)
PADAxis axis) { PADAxis axis) {
const auto iter = std::find_if(controller->m_axisMapping.begin(), controller->m_axisMapping.end(), const auto iter =
[axis](const auto& pair) { return pair.padAxis == axis; }); std::ranges::find_if(controller->m_axisMapping, [axis](const auto& pair) { return pair.padAxis == axis; });
if (iter == controller->m_axisMapping.end()) { if (iter == controller->m_axisMapping.end()) {
return 0; return 0;
} }
@@ -724,10 +736,10 @@ u32 PADRead(PADStatus* status) {
} }
status[i].err = PAD_ERR_NONE; status[i].err = PAD_ERR_NONE;
if (g_keyboardBindings[i].m_mappingsSet) { if (g_keyboardBindings[i].m_mappingsSet && SDL_GetKeyboardFocus() != nullptr) {
std::for_each(g_keyboardBindings[i].m_buttonMapping.begin(), g_keyboardBindings[i].m_buttonMapping.end(), std::ranges::for_each(
[&kbState, &i, &status](const PADKeyButtonBinding& mapping) { g_keyboardBindings[i].m_buttonMapping, [&kbState, &numKeys, &i, &status](const PADKeyButtonBinding& mapping) {
if (mapping.scancode > PAD_KEY_INVALID && kbState[mapping.scancode]) { if (mapping.scancode > PAD_KEY_INVALID && mapping.scancode < numKeys && kbState[mapping.scancode]) {
status[i].button |= mapping.padButton; status[i].button |= mapping.padButton;
} else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) { } else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) {
status[i].button |= mapping.padButton; status[i].button |= mapping.padButton;
@@ -788,7 +800,7 @@ u32 PADRead(PADStatus* status) {
status[i].triggerRight = static_cast<u8>(std::min(static_cast<int>(status[i].triggerRight) + tr, 255)); status[i].triggerRight = static_cast<u8>(std::min(static_cast<int>(status[i].triggerRight) + tr, 255));
} }
if (controller) { if (controller && !g_keyboardBindings[i].m_mappingsSet) {
EnsureMappingLoaded(controller); EnsureMappingLoaded(controller);
// Wii U Pro Controller raw D-pad fallback. SDL's HIDAPI Wii driver posts // Wii U Pro Controller raw D-pad fallback. SDL's HIDAPI Wii driver posts
@@ -833,9 +845,9 @@ u32 PADRead(PADStatus* status) {
bool leftTriggerSet = false; bool leftTriggerSet = false;
bool rightTriggerSet = false; bool rightTriggerSet = false;
std::for_each(controller->m_buttonMapping.begin(), controller->m_buttonMapping.end(), std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet,
[&controller, &i, &status, &leftTriggerSet, &rightTriggerSet](const auto& mapping) { &rightTriggerSet](const auto& mapping) {
if (SDL_GetGamepadButton(controller->m_controller, static_cast<SDL_GamepadButton>(mapping.nativeButton))) { if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) {
status[i].button |= mapping.padButton; status[i].button |= mapping.padButton;
} }
@@ -847,12 +859,12 @@ u32 PADRead(PADStatus* status) {
} }
}); });
std::for_each(controller->m_altButtonMapping.begin(), controller->m_altButtonMapping.end(), std::ranges::for_each(controller->m_altButtonMapping, [&controller, &i, &status, &leftTriggerSet,
[&controller, &i, &status, &leftTriggerSet, &rightTriggerSet](const auto& mapping) { &rightTriggerSet](const auto& mapping) {
if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) { if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) {
return; return;
} }
if (SDL_GetGamepadButton(controller->m_controller, static_cast<SDL_GamepadButton>(mapping.nativeButton))) { if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) {
status[i].button |= mapping.padButton; status[i].button |= mapping.padButton;
} }
@@ -946,6 +958,17 @@ u32 PADRead(PADStatus* status) {
Sint16 tl = std::max(static_cast<Sint16>(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_L)); Sint16 tl = std::max(static_cast<Sint16>(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_L));
Sint16 tr = std::max(static_cast<Sint16>(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_R)); Sint16 tr = std::max(static_cast<Sint16>(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_R));
// Games can read either the digital L/R bits or their analog pressure.
// An explicit button binding must drive both, otherwise the original
// L2/R2 axis still activates L/R even when it was rebound to L1/R1.
// Real GC pads retain independent analog travel and end-stop clicks.
if (!(controller->m_isGameCube ||
(SDL_GetGamepadType(controller->m_controller) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO &&
controller->m_pid == 0x2073))) {
if (leftTriggerSet) tl = (status[i].button & PAD_TRIGGER_L) != 0 ? 32767 : 0;
if (rightTriggerSet) tr = (status[i].button & PAD_TRIGGER_R) != 0 ? 32767 : 0;
}
if (controller->m_deadZones.emulateTriggers) { if (controller->m_deadZones.emulateTriggers) {
if (!leftTriggerSet && tl > controller->m_deadZones.leftTriggerActivationZone) { if (!leftTriggerSet && tl > controller->m_deadZones.leftTriggerActivationZone) {
status[i].button |= PAD_TRIGGER_L; status[i].button |= PAD_TRIGGER_L;
@@ -990,12 +1013,13 @@ void PADControlMotor(const u32 chan, const u32 cmd) {
} }
if (controller->m_isGameCube) { if (controller->m_isGameCube) {
if (cmd == PAD_MOTOR_STOP) { if (cmd == PAD_MOTOR_STOP || cmd == PAD_MOTOR_STOP_HARD) {
aurora::input::controller_rumble(instance, 0, 1, 0); // Use an unambiguous motor-off request. The (0, 1) coast encoding
// requires SDL's GameCube brake mode; other backends or an overridden
// hint interpret it as rumble and can leave the controller vibrating.
aurora::input::controller_rumble(instance, 0, 0, 0);
} else if (cmd == PAD_MOTOR_RUMBLE) { } else if (cmd == PAD_MOTOR_RUMBLE) {
aurora::input::controller_rumble(instance, 1, 1, 0); aurora::input::controller_rumble(instance, 1, 1, 0);
} else if (cmd == PAD_MOTOR_STOP_HARD) {
aurora::input::controller_rumble(instance, 0, 0, 0);
} }
} else { } else {
if (cmd == PAD_MOTOR_STOP) { if (cmd == PAD_MOTOR_STOP) {
@@ -1165,7 +1189,7 @@ void PADSetButtonMapping(const u32 port, const PADButtonMapping mapping) {
return; return;
} }
const auto iter = std::find_if(controller->m_buttonMapping.begin(), controller->m_buttonMapping.end(), const auto iter = std::ranges::find_if(controller->m_buttonMapping,
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; }); [mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
if (iter == controller->m_buttonMapping.end()) { if (iter == controller->m_buttonMapping.end()) {
return; return;
@@ -1199,7 +1223,7 @@ void PADSetAltButtonMapping(const u32 port, const PADButtonMapping mapping) {
return; return;
} }
const auto iter = std::find_if(controller->m_altButtonMapping.begin(), controller->m_altButtonMapping.end(), const auto iter = std::ranges::find_if(controller->m_altButtonMapping,
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; }); [mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
if (iter == controller->m_altButtonMapping.end()) { if (iter == controller->m_altButtonMapping.end()) {
return; return;
@@ -1226,7 +1250,7 @@ void PADSetAxisMapping(const u32 port, const PADAxisMapping mapping) {
return; return;
} }
const auto iter = std::find_if(controller->m_axisMapping.begin(), controller->m_axisMapping.end(), const auto iter = std::ranges::find_if(controller->m_axisMapping,
[mapping](const auto& pair) { return mapping.padAxis == pair.padAxis; }); [mapping](const auto& pair) { return mapping.padAxis == pair.padAxis; });
if (iter == controller->m_axisMapping.end()) { if (iter == controller->m_axisMapping.end()) {
return; return;
@@ -1278,6 +1302,11 @@ BOOL PADSetKeyButtonBindings(const u32 port, PADKeyButtonBinding bindings[PAD_BU
} }
PADKeyButtonBinding* PADGetKeyButtonBindings(const u32 port, u32* buttonCount) { PADKeyButtonBinding* PADGetKeyButtonBindings(const u32 port, u32* buttonCount) {
PADInit();
if (!g_keyboardBindingsLoaded) {
g_keyboardBindingsLoaded = true;
load_keyboard_bindings();
}
if (port >= PAD_MAX_CONTROLLERS || !g_keyboardBindings[port].m_mappingsSet) { if (port >= PAD_MAX_CONTROLLERS || !g_keyboardBindings[port].m_mappingsSet) {
return nullptr; return nullptr;
} }
@@ -1396,10 +1425,9 @@ static void load_keyboard_bindings() {
if (mappingsSet) { if (mappingsSet) {
const bool anyBound = const bool anyBound =
std::any_of(buttonMapping.begin(), buttonMapping.end(), std::ranges::any_of(buttonMapping,
[](const PADKeyButtonBinding& b) { return b.scancode != PAD_KEY_INVALID; }) || [](const PADKeyButtonBinding& b) { return b.scancode != PAD_KEY_INVALID; }) ||
std::any_of(axisMapping.begin(), axisMapping.end(), std::ranges::any_of(axisMapping, [](const PADKeyAxisBinding& b) { return b.scancode != PAD_KEY_INVALID; });
[](const PADKeyAxisBinding& b) { return b.scancode != PAD_KEY_INVALID; });
if (!anyBound) { if (!anyBound) {
mappingsSet = false; mappingsSet = false;
} }
@@ -1439,10 +1467,7 @@ void __PADWriteDeadZones(SDL_IOStream* file, // NOLINT(*-reserved-identifier)
void PADSerializeMappings() { void PADSerializeMappings() {
const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath); const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath);
// Avoid std::views::values here: older Apple libc++ releases implement the for (auto& controller : aurora::input::g_GameControllers | std::views::values) {
// C++20 ranges algorithms we use but not this adaptor.
for (auto& entry : aurora::input::g_GameControllers) {
auto& controller = entry.second;
EnsureMappingLoaded(&controller); EnsureMappingLoaded(&controller);
const auto filePath = const auto filePath =
basePath / fmt::format("{}_{:04X}_{:04X}.controller", aurora::input::controller_name(controller.m_index), basePath / fmt::format("{}_{:04X}_{:04X}.controller", aurora::input::controller_name(controller.m_index),
@@ -1544,8 +1569,8 @@ static constexpr std::array<std::pair<PADButton, std::string_view>, PAD_AXIS_COU
const char* PADGetButtonName(const PADButton button) { const char* PADGetButtonName(const PADButton button) {
if (const auto iter = std::find_if(skButtonNames.begin(), skButtonNames.end(), if (const auto iter =
[&button](const auto& pair) { return button == pair.first; }); std::ranges::find_if(skButtonNames, [&button](const auto& pair) { return button == pair.first; });
iter != skButtonNames.end()) { iter != skButtonNames.end()) {
return iter->second.data(); return iter->second.data();
} }
@@ -1558,8 +1583,7 @@ const char* PADGetNativeButtonName(u32 button) {
} }
const char* PADGetAxisName(const PADAxis axis) { const char* PADGetAxisName(const PADAxis axis) {
if (const auto it = std::find_if(skAxisNames.begin(), skAxisNames.end(), if (const auto it = std::ranges::find_if(skAxisNames, [&axis](const auto& pair) { return axis == pair.first; });
[&axis](const auto& pair) { return axis == pair.first; });
it != skAxisNames.end()) { it != skAxisNames.end()) {
return it->second.data(); return it->second.data();
} }
@@ -1568,8 +1592,7 @@ const char* PADGetAxisName(const PADAxis axis) {
} }
const char* PADGetAxisDirectionLabel(const PADAxis axis) { const char* PADGetAxisDirectionLabel(const PADAxis axis) {
if (const auto it = std::find_if(skAxisDirLabels.begin(), skAxisDirLabels.end(), if (const auto it = std::ranges::find_if(skAxisDirLabels, [&axis](const auto& pair) { return axis == pair.first; });
[&axis](const auto& pair) { return axis == pair.first; });
it != skAxisDirLabels.end()) { it != skAxisDirLabels.end()) {
return it->second.data(); return it->second.data();
} }
+25 -8
View File
@@ -22,6 +22,7 @@
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <optional> #include <optional>
#include <ranges>
#include <absl/container/flat_hash_map.h> #include <absl/container/flat_hash_map.h>
#include <magic_enum.hpp> #include <magic_enum.hpp>
@@ -167,7 +168,12 @@ struct RenderPass {
Range resolveUniformRange; Range resolveUniformRange;
std::array<u32, 3> resolveCopyFilterCoefficients{0, 64, 0}; std::array<u32, 3> resolveCopyFilterCoefficients{0, 64, 0};
Vec4<float> clearColorValue{0.f, 0.f, 0.f, 0.f}; Vec4<float> clearColorValue{0.f, 0.f, 0.f, 0.f};
float clearDepthValue = 1.f; // 1.f is the forward-Z "farthest" clear value; under UseReversedZ farthest is 0.f instead (see
// gx::clear_depth_value(), which the main render pass explicitly overrides this default with -
// any OTHER pass that keeps this default, e.g. an offscreen render-to-texture pass composited
// later, needs the same reversed-Z-aware value or its depth buffer starts "already nearest",
// failing every subsequent depth test and making whatever's drawn into it vanish).
float clearDepthValue = gx::UseReversedZ ? 0.f : 1.f;
CommandList commands; CommandList commands;
bool clearColor = true; bool clearColor = true;
bool clearDepth = true; bool clearDepth = true;
@@ -756,7 +762,9 @@ void begin_offscreen(uint32_t width, uint32_t height) {
.targetSize = {width, height, 1}, .targetSize = {width, height, 1},
.msaaSamples = 1, .msaaSamples = 1,
.clearColorValue = {0.f, 0.f, 0.f, 0.f}, .clearColorValue = {0.f, 0.f, 0.f, 0.f},
.clearDepthValue = 1.f, // See the RenderPass::clearDepthValue default's comment: this offscreen pass gets its own
// depth buffer, and the farthest clear value is 0.f, not 1.f, under UseReversedZ.
.clearDepthValue = gx::UseReversedZ ? 0.f : 1.f,
.clearColor = true, .clearColor = true,
.clearDepth = true, .clearDepth = true,
}; };
@@ -1327,8 +1335,8 @@ static void render_impl(std::vector<RenderPass>& renderPasses, wgpu::CommandEnco
#if defined(AURORA_GFX_DEBUG_GROUPS) #if defined(AURORA_GFX_DEBUG_GROUPS)
if (finalize && !g_debugGroupStack.empty()) { if (finalize && !g_debugGroupStack.empty()) {
for (auto it = g_debugGroupStack.rbegin(); it != g_debugGroupStack.rend(); ++it) { for (auto& it : std::ranges::reverse_view(g_debugGroupStack)) {
Log.warn("Debug group was not popped at end of frame: {}", *it); Log.warn("Debug group was not popped at end of frame: {}", it);
} }
g_debugGroupStack.clear(); g_debugGroupStack.clear();
} }
@@ -1409,10 +1417,19 @@ static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vec
switch (cmd.type) { switch (cmd.type) {
case CommandType::SetViewport: { case CommandType::SetViewport: {
const auto& vp = cmd.data.setViewport; const auto& vp = cmd.data.setViewport;
// WebGPU requires 0 <= minDepth <= maxDepth <= 1, and the guest's (near, far) order is already // WebGPU requires 0 <= minDepth <= maxDepth <= 1. vp.znear/vp.zfar are in GX's own distance
// reproduced in clip space. Passing the raw swapped pair diverged per backend in release builds. // terms (0 = near); under UseReversedZ the host depth-buffer storage direction is flipped
const float minDepth = std::clamp(std::min(vp.znear, vp.zfar), 0.0f, 1.0f); // (near = 1, far = 0), so this range has to be remapped through 1-x the same way the
const float maxDepth = std::clamp(std::max(vp.znear, vp.zfar), 0.0f, 1.0f); // projection matrix, depth compare function, and clear value all are - a plain min/max clamp
// (the previous code here) maps a *restricted* range (e.g. a viewport deliberately narrowed
// to force something to draw "in front of everything") to the wrong end of the buffer: what
// should land near the near-storage-extreme (1.0) instead lands near the far-storage-extreme
// (0.0), so anything else drawn afterward at its true depth wins the compare test and the
// "in front" geometry silently vanishes. A full [0,1] viewport is unaffected either way,
// which is why this only broke specific elements, not the whole scene. Matches upstream
// aurora's apply_viewport (lib/gfx/encoding.cpp) exactly.
const float minDepth = gx::UseReversedZ ? 1.0f - vp.zfar : vp.znear;
const float maxDepth = gx::UseReversedZ ? 1.0f - vp.znear : vp.zfar;
pass.SetViewport(vp.left, vp.top, vp.width, vp.height, minDepth, maxDepth); pass.SetViewport(vp.left, vp.top, vp.width, vp.height, minDepth, maxDepth);
} break; } break;
case CommandType::SetScissor: { case CommandType::SetScissor: {
+1 -1
View File
@@ -92,7 +92,7 @@ struct Params {
constexpr std::string_view ReversedZBody = R"( constexpr std::string_view ReversedZBody = R"(
fn gx_z24(depth: f32) -> u32 { fn gx_z24(depth: f32) -> u32 {
return min(u32(clamp(depth, 0.0, 1.0) * 16777216.0), 0x00ffffffu); return min(u32(clamp(1.0 - depth, 0.0, 1.0) * 16777215.0 + 0.5), 0x00ffffffu);
} }
)"sv; )"sv;
+1 -1
View File
@@ -137,7 +137,7 @@ fn gx_z24_at_coord(unclamped_coord: vec2i) -> u32 {
let tex_size = vec2i(textureDimensions(src)); let tex_size = vec2i(textureDimensions(src));
let coord = clamp(unclamped_coord, vec2i(0), tex_size - vec2i(1)); let coord = clamp(unclamped_coord, vec2i(0), tex_size - vec2i(1));
let depth = textureLoad(src, coord, 0); let depth = textureLoad(src, coord, 0);
return min(u32(clamp(depth, 0.0, 1.0) * 16777216.0), 0x00ffffffu); return min(u32(clamp(1.0 - depth, 0.0, 1.0) * 16777215.0 + 0.5), 0x00ffffffu);
} }
)"s )"s
: R"( : R"(
+1 -1
View File
@@ -553,7 +553,7 @@ void build_index() noexcept {
continue; continue;
} }
s_replacementIndex.try_emplace(*parsed, ReplacementIndexEntry{path}); s_replacementIndex.try_emplace(*parsed, path);
} }
Log.info("Indexed {} texture replacements", s_replacementIndex.size()); Log.info("Indexed {} texture replacements", s_replacementIndex.size());
+13 -4
View File
@@ -1416,23 +1416,32 @@ static inline GXBlendFactor remove_dst_alpha_usage(GXBlendFactor fac) {
} }
} }
// GX_LEQUAL etc. describe "pass if this pixel is closer than/equal to what's stored" in GX's own
// distance terms, independent of how that distance is encoded as a host depth value. Under
// UseReversedZ the encoding is flipped (near=1, far=0), so "closer" now corresponds to a *larger*
// stored value, not a smaller one - the ordered compare functions (LESS/LEQUAL/GREATER/GEQUAL)
// must invert to match, or the depth test silently runs backwards (verified directly: this was
// the actual cause of a bug report after the projection/shader half of the reverse-Z fix
// eliminated the double-negation that used to accidentally keep the unreversed comparisons
// correct - LEQUAL now needs GreaterEqual, not LessEqual, once the encoding it's testing against
// is genuinely reversed). Matches upstream aurora's to_compare_function exactly.
static inline wgpu::CompareFunction to_compare_function(GXCompare func) { static inline wgpu::CompareFunction to_compare_function(GXCompare func) {
switch (func) { switch (func) {
DEFAULT_FATAL("invalid depth fn {}", underlying(func)); DEFAULT_FATAL("invalid depth fn {}", underlying(func));
case GX_NEVER: case GX_NEVER:
return wgpu::CompareFunction::Never; return wgpu::CompareFunction::Never;
case GX_LESS: case GX_LESS:
return wgpu::CompareFunction::Less; return UseReversedZ ? wgpu::CompareFunction::Greater : wgpu::CompareFunction::Less;
case GX_EQUAL: case GX_EQUAL:
return wgpu::CompareFunction::Equal; return wgpu::CompareFunction::Equal;
case GX_LEQUAL: case GX_LEQUAL:
return wgpu::CompareFunction::LessEqual; return UseReversedZ ? wgpu::CompareFunction::GreaterEqual : wgpu::CompareFunction::LessEqual;
case GX_GREATER: case GX_GREATER:
return wgpu::CompareFunction::Greater; return UseReversedZ ? wgpu::CompareFunction::Less : wgpu::CompareFunction::Greater;
case GX_NEQUAL: case GX_NEQUAL:
return wgpu::CompareFunction::NotEqual; return wgpu::CompareFunction::NotEqual;
case GX_GEQUAL: case GX_GEQUAL:
return wgpu::CompareFunction::GreaterEqual; return UseReversedZ ? wgpu::CompareFunction::LessEqual : wgpu::CompareFunction::GreaterEqual;
case GX_ALWAYS: case GX_ALWAYS:
return wgpu::CompareFunction::Always; return wgpu::CompareFunction::Always;
} }
+8 -1
View File
@@ -485,7 +485,14 @@ const gfx::TextureBind& get_texture(GXTexMapID id) noexcept;
void resolve_sampled_textures(const ShaderInfo& info) noexcept; void resolve_sampled_textures(const ShaderInfo& info) noexcept;
inline float clear_depth_value() { inline float clear_depth_value() {
return std::min(static_cast<float>(g_gxState.clearDepth) / 16777216.f, 16777215.f / 16777216.f); // g_gxState.clearDepth is in GX's own distance terms (0 = near, larger = farther), independent of
// how UseReversedZ encodes that as a host depth value - it must be re-mapped the same way the
// projection matrix and depth compare function are, or the buffer clears to the wrong extreme
// (verified directly: matches upstream aurora's clear_depth_value, which does this same inversion
// and was the second missing piece alongside to_compare_function's compare-op inversion).
const float normalizedDepth =
std::min(static_cast<float>(g_gxState.clearDepth) / 16777216.f, 16777215.f / 16777216.f);
return UseReversedZ ? (1.f - normalizedDepth) : normalizedDepth;
} }
inline bool render_target_has_alpha(GXPixelFmt pixelFmt) noexcept { return pixelFmt == GX_PF_RGBA6_Z24; } inline bool render_target_has_alpha(GXPixelFmt pixelFmt) noexcept { return pixelFmt == GX_PF_RGBA6_Z24; }
+23 -8
View File
@@ -993,11 +993,13 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
"\n let clip_base = select(clip_a, clip_b, use_b);" "\n let clip_base = select(clip_a, clip_b, use_b);"
"\n out.pos = vec4f(clip_base.xy + offset_ndc * clip_base.w, clip_base.zw);"; "\n out.pos = vec4f(clip_base.xy + offset_ndc * clip_base.w, clip_base.zw);";
} }
if constexpr (UseReversedZ) { // The near/far depth correction used to be applied here per-vertex (out.pos.z = -out.pos.z for
vtxXfrAttrsPre += "\n out.pos.z = -out.pos.z;"; // reversed, or += out.pos.w for forward), redundantly on top of the same correction already
} else { // folded into ubuf.proj by effective_projection() (shader_info.cpp) - applying it twice canceled
vtxXfrAttrsPre += "\n out.pos.z += out.pos.w;"; // out for the common case (any draw where effective_projection() decides to flip), silently
} // making "reversed" Z behave identically to forward Z. It is now applied exactly once, in the
// projection matrix alone (matching upstream aurora commit 1dde08fa: "Move depth correction to
// projection matrix"), so nothing needs to happen to out.pos.z here.
// GX rasterizes at a 7/12 pixel center when antialiasing is disabled, while WebGPU rasterizes at 1/2. // GX rasterizes at a 7/12 pixel center when antialiasing is disabled, while WebGPU rasterizes at 1/2.
vtxXfrAttrsPre += vtxXfrAttrsPre +=
"\n let gx_pixel_center_correction = " "\n let gx_pixel_center_correction = "
@@ -1465,7 +1467,14 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
textureDependency.texMapId, uvIn); textureDependency.texMapId, uvIn);
} }
std::string fogDepthExpr = UseReversedZ ? "in.pos.z" : "(1.0 - in.pos.z)"; // in.pos.z is the host NDC z (forward: 0=near/1=far; reversed: 1=near/0=far post-fix), but this
// expression needs to produce GX's own native distance term (always 0=near/1=far, matching how
// g_gxState.clearDepth/clear_depth_value() are interpreted before their own UseReversedZ
// inversion) - forward already matches directly; reversed needs the same 1-x flip everything
// else reversed-Z-aware uses. This was backwards (verified directly against upstream aurora's
// identical expression in build_shader_source), which fed both fog density and the GX_ZT_ADD
// z-texture path the wrong distance value.
std::string fogDepthExpr = UseReversedZ ? "(1.0 - in.pos.z)" : "in.pos.z";
std::string fogZCoordExpr = std::string fogZCoordExpr =
fmt::format("u32(round(clamp({}, 0.0, 1.0) * 16777216.0))", fogDepthExpr); fmt::format("u32(round(clamp({}, 0.0, 1.0) * 16777216.0))", fogDepthExpr);
if (usesZTextureDepth) { if (usesZTextureDepth) {
@@ -1498,7 +1507,7 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
fragmentFn += fmt::format( fragmentFn += fmt::format(
"\n let oldZ = u32(round(clamp({0}, 0.0, 1.0) * 16777216.0));" "\n let oldZ = u32(round(clamp({0}, 0.0, 1.0) * 16777216.0));"
"\n ztexCoord = (ztexCoord + oldZ) & 0x00ffffffu;", "\n ztexCoord = (ztexCoord + oldZ) & 0x00ffffffu;",
UseReversedZ ? "in.pos.z" : "(1.0 - in.pos.z)"); UseReversedZ ? "(1.0 - in.pos.z)" : "in.pos.z");
} }
fragmentFn += "\n let ztexDepth = f32(ztexCoord) / 16777216.0;"; fragmentFn += "\n let ztexDepth = f32(ztexCoord) / 16777216.0;";
fogZCoordExpr = "ztexCoord"; fogZCoordExpr = "ztexCoord";
@@ -1639,7 +1648,13 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
" @builtin(frag_depth) depth: f32,\n" " @builtin(frag_depth) depth: f32,\n"
"};"; "};";
fragmentFn += fmt::format("\n let fragDepth = {}ztexDepth;", UseReversedZ ? "" : "1.0 - "); // ztexDepth is in GX's native distance terms (0=near/1=far, see fogDepthExpr's comment above),
// but frag_depth must be written in the same host NDC-z convention in.pos.z itself uses -
// forward matches directly (no change), reversed needs the same 1-x flip. This was backwards
// the same way fogDepthExpr was (verified by the same derivation, since aurora upstream has no
// directly equivalent line here to cross-check against - this z-texture-depth-output path
// appears to be specific to this fork).
fragmentFn += fmt::format("\n let fragDepth = {}ztexDepth;", UseReversedZ ? "1.0 - " : "");
fragmentReturnType = "FragmentOutput"; fragmentReturnType = "FragmentOutput";
fragmentReturn = fragmentReturn =
" var out: FragmentOutput;\n" " var out: FragmentOutput;\n"
+11 -3
View File
@@ -548,14 +548,22 @@ constexpr size_t kStagedUniformBytes =
96 + sizeof(Mat4x4<float>) + sizeof(Mat3x4<float>) * (MaxPostexMtx + MaxPnMtx); 96 + sizeof(Mat4x4<float>) + sizeof(Mat3x4<float>) * (MaxPostexMtx + MaxPnMtx);
// The host viewport always receives the normalized GX depth window (render_pass_impl clamps to minDepth <= maxDepth). // The host viewport always receives the normalized GX depth window (render_pass_impl clamps to minDepth <= maxDepth).
//
// Folds the near/far depth correction the vertex shader used to apply per-vertex directly into the
// projection matrix instead (matching upstream aurora commit 1dde08fa, "Move depth correction to
// projection matrix") - valid because the correction is a linear combination of the z/w rows, so
// applying it once here to the row is equivalent to applying it once per-vertex to the dot product,
// and it must be applied exactly once: doing it here AND in the shader (the previous bug) canceled
// the negation out for `flip`, silently making "reversed" Z behave identically to forward Z.
// `flip` decides which of the two single-application forms this draw needs: true bakes in the
// reversed-Z inversion (z' = -z), false bakes in the forward-Z near/far combination (z' = z + w) -
// exactly one always applies, never both, and never neither.
static Mat4x4<float> effective_projection() noexcept { static Mat4x4<float> effective_projection() noexcept {
const auto& vp = g_gxState.renderViewport; const auto& vp = g_gxState.renderViewport;
const bool flip = (vp.znear <= vp.zfar) == UseReversedZ; const bool flip = (vp.znear <= vp.zfar) == UseReversedZ;
Mat4x4<float> proj = g_gxState.proj; Mat4x4<float> proj = g_gxState.proj;
if (flip) {
for (size_t i = 0; i < 4; ++i) { for (size_t i = 0; i < 4; ++i) {
proj.m2.m[i] = -(proj.m2.m[i] + proj.m3.m[i]); proj.m2.m[i] = flip ? -proj.m2.m[i] : (proj.m2.m[i] + proj.m3.m[i]);
}
} }
return proj; return proj;
} }
+1 -1
View File
@@ -235,7 +235,7 @@ std::string GetOSVersion() {
constexpr auto name = "iOS"; constexpr auto name = "iOS";
#elif TARGET_OS_TV #elif TARGET_OS_TV
constexpr auto name = "tvOS"; constexpr auto name = "tvOS";
#else #elif
constexpr auto name = Unknown; constexpr auto name = Unknown;
#endif #endif
+349
View File
@@ -0,0 +1,349 @@
# Building WiiCompiled and Retro Rewind on macOS
This guide covers building **WiiCompiled** (base game) and **Retro Rewind** from source on macOS for Apple Silicon (`arm64`). Follow these instructions to compile the native executables directly.
> [!NOTE]
> If you only want to build the base game (**WiiCompiled**), look for sections marked **`(Skip if only building WiiCompiled)`** to bypass Retro Rewind and online payload steps.
---
## 1. Prerequisites
### System Requirements
- **Hardware**: Apple Silicon Mac (M1/M2/M3/M4)
- **Operating System**: macOS 14 (Sonoma) or later
- **Xcode Command Line Tools**:
```bash
xcode-select --install
```
### Toolchain Dependencies
Install the required tools using [Homebrew](https://brew.sh):
```bash
brew install cmake ninja
brew install --cask dotnet-sdk@8
```
Verify that Clang, CMake, Ninja, and the .NET 8 runtime are available:
```bash
clang --version
cmake --version
ninja --version
dotnet --list-runtimes # Must list Microsoft.NETCore.App 8.x
```
---
## 2. Required Game and Mod Assets
Due to legal requirements, no proprietary Nintendo assets or code are included in this repository. You must provide your own legally dumped game files.
1. **Mario Kart Wii PAL (`RMCP01`) Disc Image** *(Required)*:
- Supported formats: `.iso`, `.wbfs`, `.ciso`, `.rvz`, `.gcm`, `.gcz`.
2. **nodtool** *(Required for disc extraction)*:
- Download the macOS Apple Silicon binary of [nodtool](https://github.com/encounter/nod/releases):
```bash
curl -fsSL "https://github.com/encounter/nod/releases/download/v2.0.0-alpha.10/nodtool-macos-arm64" -o nodtool
chmod +x nodtool
```
3. **Retro Rewind Distribution** *(Skip if only building WiiCompiled)*:
- Download the [Retro Rewind](https://wiki.tockdom.com/wiki/Retro_Rewind) release package. You will need the `RetroRewind6` folder (which contains `Binaries/Code.pul`).
4. **Retro-WFC Payload** *(Skip if only building WiiCompiled or building offline)*:
- Required for online multiplayer on Retro Rewind. Downloaded during setup from `http://nas.play.rwfc.net/payload?g=RMCPD00`.
---
## 3. Step 1: Extract Disc Assets
Extract your clean PAL `RMCP01` disc into the `Assets/` directory of the repository:
```bash
# Using nodtool directly into a temporary scratch directory
mkdir -p /tmp/mkw-extract
./nodtool extract /path/to/RMCP01.iso /tmp/mkw-extract
# Copy extracted assets into the repository Assets directory
rm -rf Assets/DATA/files Assets/DATA/sys
mkdir -p Assets/DATA
cp /tmp/mkw-extract/*/sys/main.dol Assets/main.dol
cp /tmp/mkw-extract/*/files/rel/StaticR.rel Assets/StaticR.rel
cp -R /tmp/mkw-extract/*/files Assets/DATA/files
cp -R /tmp/mkw-extract/*/sys Assets/DATA/sys
# Clean up temporary files
rm -rf /tmp/mkw-extract
```
> [!TIP]
> Alternatively, you can use the repository's helper script:
> ```bash
> Launcher/macos/extract-disc.command --game /path/to/RMCP01.iso --assets-dir Assets --nodtool ./nodtool
> ```
### Verify Extracted Asset Hashes
Confirm that the extracted files match the expected clean PAL revision:
```bash
shasum -a 256 Assets/main.dol Assets/StaticR.rel
```
- `Assets/main.dol`: `80d18895b39c63bd80f457398bfcbb91b7d16ac116a41a88967e954080155b05`
- `Assets/StaticR.rel`: `16d9d146112541fefea701ecb5bc1a496f9d50e4a752fbb5b6778e7c6399f67d`
---
## 4. Step 2: Build the Translator CLI
Compile the static recompiler CLI:
```bash
dotnet build translator/src/Translator.Cli/Translator.Cli.csproj -c Release
```
Define a shell function to invoke the translator (ensuring paths with spaces are handled safely):
```bash
translator() {
dotnet "$(pwd)/translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll" "$@"
}
```
---
## 5. Step 3: Translation
### A. Translate Base Game Functions
```bash
mkdir -p generated/functions build/base
translator translate-recursive 0x8000629c \
--project projects/mkwii/recomp.yml \
--outdir generated/functions \
--output-metadata generated/base_translation_output.json \
--production-source-bundle generated/base_translation_sources.bin \
--no-function-files \
--prune-stale \
--threads $(sysctl -n hw.ncpu)
```
### B. Emit Base Manifest
```bash
translator emit-base-manifest \
--project projects/mkwii/recomp.yml \
--out build/base \
--functions-dir generated/functions \
--translation-output-metadata generated/base_translation_output.json \
--region P
```
---
### C. Stage and Translate Retro Rewind *(Skip this step if you only want to build WiiCompiled)*
1. Stage `Code.pul`:
```bash
RETRO_DIR="/path/to/RetroRewind6"
mkdir -p PulsarPacks/completed/RetroRewind/RetroRewind6/Binaries
cp "$RETRO_DIR/Binaries/Code.pul" PulsarPacks/completed/RetroRewind/RetroRewind6/Binaries/Code.pul
```
2. **Retro-WFC Payload Setup (for Online Multiplayer)**:
Online play in Retro Rewind requires the shared Retro-WFC payload. Download and validate it:
```bash
mkdir -p build/retro-wfc/binary
curl -fsSL --retry 3 "https://nas.play.rwfc.net/payload?g=RMCPD00" \
-o build/retro-wfc/binary/payload.RMCPD00.bin
# Validate payload signature and integrity
translator validate-retro-wfc-payload --directory build/retro-wfc
```
3. Run Retro Rewind translation:
```bash
mkdir -p build/mods/retro_rewind_full_cpp
translator translate-mod \
--project projects/mkwii/recomp.yml \
--profile retro-rewind \
--base-manifest build/base/mkwii_base_manifest.json \
--base-translation-output-metadata generated/base_translation_output.json \
--code-pul "$RETRO_DIR/Binaries/Code.pul" \
--mod-root "$RETRO_DIR" \
--mod-name "Retro Rewind" \
--region P \
--out build/mods/retro_rewind_full_cpp \
--prefer-cached-inputs \
--emit-cpp \
--threads $(sysctl -n hw.ncpu) \
--retro-wfc-payload build/retro-wfc/binary/payload.RMCPD00.bin
```
> [!TIP]
> If you do not want online play or do not have an internet connection, replace `--retro-wfc-payload ...` with `--skip-retro-wfc`.
---
### D. Generate Data Initialization and Build Shards
First, generate the embedded game data initializer:
```bash
translator generate-data-init --project projects/mkwii/recomp.yml
```
Next, generate the CMake build shards using **one** of the following options:
#### Option 1: Base Game Only (WiiCompiled)
```bash
mkdir -p generated/build_shards
translator emit-build-shards \
--project projects/mkwii/recomp.yml \
--base-metadata generated/base_translation_output.json \
--base-functions-dir generated/functions \
--native-source-dir runtime/src \
--out generated/build_shards
```
#### Option 2: Base Game + Retro Rewind
```bash
mkdir -p generated/build_shards
translator emit-build-shards \
--project projects/mkwii/recomp.yml \
--base-metadata generated/base_translation_output.json \
--base-functions-dir generated/functions \
--native-source-dir runtime/src \
--out generated/build_shards \
--resolved-profile build/mods/retro_rewind_full_cpp/resolved_dispatch_profile.json \
--retro-cpp-dir build/mods/retro_rewind_full_cpp/cpp
```
---
## 6. Step 4: Configure and Compile with CMake & Ninja
Configure the native C++ build targeting Apple Silicon:
```bash
cmake -S runtime -B build-macos -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DAURORA_SDL3_PROVIDER=vendor
```
Compile the desired target:
```bash
# To build WiiCompiled only:
cmake --build build-macos --target WiiCompiled --parallel $(sysctl -n hw.ncpu)
# OR to build both WiiCompiled and Retro Rewind:
cmake --build build-macos --target WiiCompiled RetroRewind --parallel $(sysctl -n hw.ncpu)
```
Once compilation completes, the executables are ready in your build directory:
- `build-macos/WiiCompiled`
- `build-macos/RetroRewind` (if built)
During the build, CMake automatically copies the required runtime assets into `build-macos/`:
- `build-macos/dsp_coef.bin`
- `build-macos/initial_pipeline_cache.db`
- `build-macos/wii_bootstrap/`
---
## 7. Step 5: Running Executables from the Build Folder
### Configure `Config.toml`
The runtime reads configuration from `~/Library/Application Support/WiiCompiled/Config.toml`.
Create the directory and configuration file:
```bash
mkdir -p "$HOME/Library/Application Support/WiiCompiled"
```
#### For Base Game Only (WiiCompiled):
```toml
# ~/Library/Application Support/WiiCompiled/Config.toml
[video]
widescreen = true
resolution_multiplier = 1.0
graphics_api = "metal"
[paths]
dvd_root = "/absolute/path/to/Wiicompiled/Assets/DATA"
```
#### For Base Game and Retro Rewind:
```toml
# ~/Library/Application Support/WiiCompiled/Config.toml
[video]
widescreen = true
resolution_multiplier = 1.0
graphics_api = "metal"
[paths]
dvd_root = "/absolute/path/to/Wiicompiled/Assets/DATA"
retro_rewind_root = "/path/to/RetroRewind6"
```
> [!NOTE]
> Ensure `dvd_root` points to the directory containing `files` and `sys/fst.bin`.
### Launching the Game
Run the compiled binaries directly from your terminal or by double clicking:
```bash
# Run base WiiCompiled
./build-macos/WiiCompiled
# Run Retro Rewind
./build-macos/RetroRewind
```
Press **F10** in-game at any time to open the configuration bar (controls, resolution, display settings, audio).
---
## Quick Reference: Automated Helper Script
The repository provides a script (`Launcher/local-build-macos.command`) that handles extraction, translation, and compilation in a single command.
### Building Base Game Only:
```bash
Launcher/local-build-macos.command \
--profile base \
--output-dir build-macos/Products \
--game /path/to/RMCP01.iso \
--nodtool ./nodtool
```
### Building Both (with Online Retro-WFC Payload):
```bash
# 1. Download Retro-WFC payload into a staging directory:
mkdir -p build/retro-wfc/binary
curl -fsSL --retry 3 "http://nas.play.rwfc.net/payload?g=RMCPD00" \
-o build/retro-wfc/binary/payload.RMCPD00.bin
# 2. Run the automated build with the payload directory:
Launcher/local-build-macos.command \
--profile both \
--output-dir build-macos/Products \
--base-output-dir build-macos/Products \
--game /path/to/RMCP01.iso \
--nodtool ./nodtool \
--retro-rewind-package-dir /path/to/RetroRewind6 \
--retro-wfc-offline-dir build/retro-wfc
```
### Building Both (Offline, Skipping Payload):
```bash
Launcher/local-build-macos.command \
--profile both \
--output-dir build-macos/Products \
--base-output-dir build-macos/Products \
--game /path/to/RMCP01.iso \
--nodtool ./nodtool \
--retro-rewind-package-dir /path/to/RetroRewind6 \
--skip-retro-wfc-payload
```
When finished, the compiled executables reside in `native-build-macos/` and the bundled `.app` packages are placed in `build-macos/Products/`.
+1 -1
View File
@@ -58,7 +58,7 @@ profiles:
module_link_base: 0x803992E0 module_link_base: 0x803992E0
output: build/mods/retro_rewind_full_cpp output: build/mods/retro_rewind_full_cpp
enable_retro_wfc: true enable_retro_wfc: true
retro_wfc_payload: http://nas.play.rwfc.net/payload?g=RMCPD00 retro_wfc_payload: https://rwfc.net/api/wfc/payload?g=RMCPD00
retro_wfc_legacy_bootstrap_hook: 0x800ED6E8 retro_wfc_legacy_bootstrap_hook: 0x800ED6E8
riivolution: riivolution:
xml: xml/RetroRewind6.xml xml: xml/RetroRewind6.xml
+10 -53
View File
@@ -1,44 +1,22 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
# Dawn's pinned macOS artifacts target 12.0. Set the same floor before project()
# initializes the Apple toolchain so direct developer CMake invocations cannot
# accidentally inherit the running SDK's deployment version. This cache entry
# is harmless on non-Apple platforms and remains overridable by a caller.
if(NOT CMAKE_OSX_DEPLOYMENT_TARGET)
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING
"Minimum macOS version supported by WiiCompiled" FORCE)
endif()
project(mkw_recompiled) project(mkw_recompiled)
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8) 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") message(FATAL_ERROR "WiiCompiled requires a 64-bit Clang toolchain")
endif() endif()
if(APPLE AND CMAKE_OSX_ARCHITECTURES)
list(LENGTH CMAKE_OSX_ARCHITECTURES MKW_OSX_ARCHITECTURE_COUNT)
if(MKW_OSX_ARCHITECTURE_COUNT GREATER 1)
message(FATAL_ERROR
"WiiCompiled supports one macOS architecture per build directory; "
"configure separate arm64 and x86_64 build directories")
endif()
endif()
if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$") if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_PLATFORM_WINDOWS TRUE) set(MKW_PLATFORM_WINDOWS TRUE)
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|arm64|ARM64)$") elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64)$")
# The first native macOS target is Apple Silicon. Intel and universal
# binaries remain future compatibility work; do not silently claim them.
set(MKW_PLATFORM_MACOS TRUE) set(MKW_PLATFORM_MACOS TRUE)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_PLATFORM_MACOS_X86_64 TRUE)
else()
set(MKW_PLATFORM_MACOS_ARM64 TRUE)
endif()
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$") 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) set(MKW_PLATFORM_LINUX TRUE)
else() else()
message(FATAL_ERROR message(FATAL_ERROR
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS x86_64/arm64") "WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS arm64")
endif() endif()
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release") if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
message(FATAL_ERROR "WiiCompiled only supports Release builds") message(FATAL_ERROR "WiiCompiled only supports Release builds")
endif() endif()
@@ -81,15 +59,14 @@ target_include_directories(mkw_pugixml PUBLIC third_party/pugixml)
target_compile_features(mkw_pugixml PUBLIC cxx_std_17) target_compile_features(mkw_pugixml PUBLIC cxx_std_17)
set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF) set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF)
# POSIX x86-64 guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric # Linux guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric
# stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers # stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike # directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
# asymmetric resume/yield coroutine libraries, which would need every call site restructured). # asymmetric resume/yield coroutine libraries, which would need every call site restructured).
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is # Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers; # separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers
# Apple Silicon uses the project's x18-safe AArch64 assembly backend, while Intel macOS uses # and macOS uses the project's x18-safe AArch64 assembly backend, so this target is Linux-only.
# libco's existing System V AMD64 backend. if(MKW_PLATFORM_LINUX)
if(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
add_library(mkw_libco STATIC third_party/libco/libco.c) add_library(mkw_libco STATIC third_party/libco/libco.c)
add_library(mkw::libco ALIAS mkw_libco) add_library(mkw::libco ALIAS mkw_libco)
target_include_directories(mkw_libco PUBLIC third_party/libco) target_include_directories(mkw_libco PUBLIC third_party/libco)
@@ -266,14 +243,12 @@ endif()
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp") file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
if(MKW_PLATFORM_MACOS) if(MKW_PLATFORM_MACOS)
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory.cpp") list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory.cpp")
if(MKW_PLATFORM_MACOS_ARM64)
# HostContext's Apple Silicon backend is implemented in a small assembly # HostContext's Apple Silicon backend is implemented in a small assembly
# companion. It must be part of the product runtime as well as the # companion. It must be part of the product runtime as well as the
# standalone context test; otherwise the final executable is missing # standalone context test; otherwise the final executable is missing
# mkw_co_init/mkw_co_switch at link time. # mkw_co_init/mkw_co_switch at link time.
enable_language(ASM) enable_language(ASM)
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S") list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
endif()
else() else()
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp") list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
endif() endif()
@@ -344,9 +319,8 @@ if(MKW_PLATFORM_LINUX)
endif() endif()
if(MKW_PLATFORM_MACOS) if(MKW_PLATFORM_MACOS)
# Exercise the public host-memory contracts separately from translated products. # Exercise the Apple Silicon context ABI and the public host-memory
if(MKW_PLATFORM_MACOS_ARM64) # contracts separately from translated products.
# Apple Silicon's context ABI is implemented by the local assembly backend.
enable_language(ASM) enable_language(ASM)
add_executable(mkw_macos_context_abi_tests add_executable(mkw_macos_context_abi_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp" "${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
@@ -359,16 +333,6 @@ if(MKW_PLATFORM_MACOS)
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp" "${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S") "${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_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
else()
# Intel macOS follows the same System V AMD64 libco path as Linux.
add_executable(mkw_macos_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp")
target_include_directories(mkw_macos_host_context_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include"
"${CMAKE_CURRENT_LIST_DIR}/third_party/libco")
target_link_libraries(mkw_macos_host_context_tests PRIVATE mkw::libco)
endif()
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17) 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_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
@@ -427,13 +391,6 @@ else()
target_link_libraries(mkw_macos_native_compile PRIVATE target_link_libraries(mkw_macos_native_compile PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
mkw::pugixml mkw::toml11 mkw::cryptopp) mkw::pugixml mkw::toml11 mkw::cryptopp)
if(MKW_PLATFORM_MACOS_X86_64)
target_link_libraries(mkw_macos_native_compile PRIVATE mkw::libco)
# Keep this compile-only audit on the same Haswell-era x86-64-v3
# baseline as the translated product. PPC paired FMA helpers use
# FMA intrinsics and intentionally cannot compile for plain x86-64.
target_compile_options(mkw_macos_native_compile PRIVATE -march=x86-64-v3)
endif()
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF) set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
endif() endif()
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform) add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
+4 -6
View File
@@ -1,4 +1,4 @@
# Public WiiCompiled product graph. # Public WiiCompiled product graph.
# #
# The translator owns the translated build graph. Mario Kart's profile-neutral # The translator owns the translated build graph. Mario Kart's profile-neutral
# functions are compiled once into mkw_base_shared; only callers whose direct # functions are compiled once into mkw_base_shared; only callers whose direct
@@ -84,9 +84,9 @@ target_link_libraries(mkw_runtime_common PRIVATE
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp) target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
if(MKW_PLATFORM_WINDOWS) if(MKW_PLATFORM_WINDOWS)
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp) target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64) elseif(MKW_PLATFORM_LINUX)
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS # ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
# media monitoring). Empty on platforms where dl* is already in libc/libSystem. # media monitoring). Empty string on glibc >= 2.34 where dl* is in libc.
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS}) target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS})
endif() endif()
if(MKW_CPPWINRT_INCLUDE_DIR) if(MKW_CPPWINRT_INCLUDE_DIR)
@@ -206,8 +206,6 @@ function(mkw_configure_product target)
if(MKW_PLATFORM_MACOS) if(MKW_PLATFORM_MACOS)
target_link_libraries(${target} PRIVATE target_link_libraries(${target} PRIVATE
"${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}") "${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}")
target_link_options(${target} PRIVATE
"LINKER:-U,_OBJC_CLASS_$_MTLLogStateDescriptor")
endif() endif()
if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake") if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake") include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
@@ -228,7 +226,7 @@ function(mkw_configure_product target)
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp) dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE) set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64) elseif(MKW_PLATFORM_LINUX)
# mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o # mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o
# files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own # files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own
# target_link_libraries (object libraries don't carry usage requirements to a consumer # target_link_libraries (object libraries don't carry usage requirements to a consumer
+17 -6
View File
@@ -48,8 +48,19 @@ struct NativeButtonItem {
uint32_t nativeButton; uint32_t nativeButton;
}; };
inline constexpr std::array<NativeButtonItem, SDL_GAMEPAD_BUTTON_COUNT + 1> kNativeButtons = {{ inline constexpr auto kNativeButtons = std::to_array<NativeButtonItem>({
{"unmapped", "Unmapped / analog trigger", PAD_NATIVE_BUTTON_INVALID}, {"disabled", "Unmapped", PAD_NATIVE_BUTTON_DISABLED},
{"left_trigger", "Left trigger (LT / L2)", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFT_TRIGGER, false)},
{"right_trigger", "Right trigger (RT / R2)", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, false)},
{"left_stick_left", "Left stick left", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTX, true)},
{"left_stick_right", "Left stick right", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTX, false)},
{"left_stick_up", "Left stick up", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTY, true)},
{"left_stick_down", "Left stick down", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTY, false)},
{"right_stick_left", "Right stick left", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTX, true)},
{"right_stick_right", "Right stick right", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTX, false)},
{"right_stick_up", "Right stick up", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTY, true)},
{"right_stick_down", "Right stick down", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTY, false)},
{"unmapped", "Default", PAD_NATIVE_BUTTON_INVALID},
{"south", "South (A / Cross)", SDL_GAMEPAD_BUTTON_SOUTH}, {"south", "South (A / Cross)", SDL_GAMEPAD_BUTTON_SOUTH},
{"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST}, {"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST},
{"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST}, {"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST},
@@ -76,7 +87,7 @@ inline constexpr std::array<NativeButtonItem, SDL_GAMEPAD_BUTTON_COUNT + 1> kNat
{"misc4", "Misc 4 / GC R click", SDL_GAMEPAD_BUTTON_MISC4}, {"misc4", "Misc 4 / GC R click", SDL_GAMEPAD_BUTTON_MISC4},
{"misc5", "Misc 5", SDL_GAMEPAD_BUTTON_MISC5}, {"misc5", "Misc 5", SDL_GAMEPAD_BUTTON_MISC5},
{"misc6", "Misc 6", SDL_GAMEPAD_BUTTON_MISC6}, {"misc6", "Misc 6", SDL_GAMEPAD_BUTTON_MISC6},
}}; });
inline std::string TrimToken(std::string_view token) { inline std::string TrimToken(std::string_view token) {
const size_t begin = token.find_first_not_of(" \t"); const size_t begin = token.find_first_not_of(" \t");
@@ -88,7 +99,7 @@ inline std::string TrimToken(std::string_view token) {
} }
inline const NativeButtonItem* FindNativeButton(std::string_view configName) { inline const NativeButtonItem* FindNativeButton(std::string_view configName) {
const std::string name = TrimToken(configName); const std::string name = TrimToken(configName.substr(0, configName.find('@')));
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(), const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
[&](const NativeButtonItem& item) { return name == item.configName; }); [&](const NativeButtonItem& item) { return name == item.configName; });
return it == kNativeButtons.end() ? nullptr : &*it; return it == kNativeButtons.end() ? nullptr : &*it;
@@ -97,8 +108,8 @@ inline const NativeButtonItem* FindNativeButton(std::string_view configName) {
// Falls back to the "unmapped" entry so callers always have a label to draw. // Falls back to the "unmapped" entry so callers always have a label to draw.
inline const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) { inline const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) {
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(), const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
[&](const NativeButtonItem& item) { return nativeButton == item.nativeButton; }); [&](const NativeButtonItem& item) { return PADAxisButtonIdentity(nativeButton) == PADAxisButtonIdentity(item.nativeButton); });
return it == kNativeButtons.end() ? kNativeButtons.front() : *it; return it == kNativeButtons.end() ? *FindNativeButton("unmapped") : *it;
} }
inline const GameCubeButtonItem* FindGameCubeButton(std::string_view configKey) { inline const GameCubeButtonItem* FindGameCubeButton(std::string_view configKey) {
+5 -5
View File
@@ -3,11 +3,11 @@
#include <cstddef> #include <cstddef>
// HostContext is the deliberately small boundary between the guest scheduler // HostContext is the deliberately small boundary between the guest scheduler
// and the host's cooperative-context facility. Windows uses native Fibers; // and the host's cooperative-context facility. Windows uses native Fibers and
// Linux and Intel macOS use libco's System V x86-64 backend. macOS AArch64 uses // Linux uses libco; macOS AArch64 uses the local assembly backend because it
// the local assembly backend because it must preserve Darwin's platform-reserved // must preserve Darwin's platform-reserved x18 register, which libco's AArch64
// x18 register, which libco's AArch64 backend does not save. Its handles are // backend does not save. Its handles are only valid on the thread that
// only valid on the thread that initialized the scheduler. // initialized the scheduler.
namespace HostContext { namespace HostContext {
using Handle = void*; using Handle = void*;
+27 -11
View File
@@ -78,22 +78,38 @@ bool ProcessSleepTimers(CpuContext* cpu)
{ {
using Clock = std::chrono::steady_clock; using Clock = std::chrono::steady_clock;
std::vector<SleepTimerEntry> dueTimers; // Pop and process ONE due timer at a time, straight from the shared table. Resuming a
// sleeper re-enters the scheduler (OSResumeThread -> SelectThread) and can switch fibers
// away from this call. Timers that had already been popped into a private list would then
// sit on the suspended fiber's stack with their threads parked and no entry in the table:
// exactly the "park-shaped with no pending wake timer" strand the reconciler below heals
// 100ms late, followed by a "sleep-timer stale" drop when this fiber finally resumes.
// Leaving unprocessed timers in the table keeps them visible to every other pump (idle
// loop, other threads' SelectThread) while this one is switched away.
bool processedAny = false;
constexpr size_t kMaxTimersPerCall = 64;
size_t processedCount = 0;
const auto now = Clock::now(); const auto now = Clock::now();
while (processedCount < kMaxTimersPerCall) {
SleepTimerEntry timer{0, {}};
bool found = false;
{ {
std::lock_guard<std::mutex> lock(gSleepTimerMutex); std::lock_guard<std::mutex> lock(gSleepTimerMutex);
auto it = gSleepTimers.begin(); for (auto it = gSleepTimers.begin(); it != gSleepTimers.end(); ++it) {
while (it != gSleepTimers.end()) { if (it->deadline <= now) {
if (it->deadline > now) { timer = *it;
++it; gSleepTimers.erase(it);
continue; found = true;
} break;
dueTimers.push_back(*it);
it = gSleepTimers.erase(it);
} }
} }
}
if (!found) {
break;
}
++processedCount;
processedAny = true;
for (const SleepTimerEntry& timer : dueTimers) {
const uint32_t threadPtr = timer.threadPtr; const uint32_t threadPtr = timer.threadPtr;
if (threadPtr == 0 || if (threadPtr == 0 ||
!Memory::Contains(threadPtr + kThreadSuspendOffset, sizeof(uint32_t))) { !Memory::Contains(threadPtr + kThreadSuspendOffset, sizeof(uint32_t))) {
@@ -219,7 +235,7 @@ bool ProcessSleepTimers(CpuContext* cpu)
} }
} }
return !dueTimers.empty(); return processedAny;
} }
} // namespace OsHleInternal } // namespace OsHleInternal
+2 -2
View File
@@ -14,7 +14,7 @@
extern "C" void mkw_co_switch(void** targetSp, void** sourceSp); extern "C" void mkw_co_switch(void** targetSp, void** sourceSp);
extern "C" void* mkw_co_init(void* stackTop, void (*entry)(void*), void* argument); extern "C" void* mkw_co_init(void* stackTop, void (*entry)(void*), void* argument);
#elif defined(__linux__) || (defined(__APPLE__) && defined(__x86_64__)) #elif defined(__linux__)
#include <libco.h> #include <libco.h>
#include <cstdlib> #include <cstdlib>
@@ -158,7 +158,7 @@ void Switch(Handle target)
g_current = source; g_current = source;
} }
#elif defined(__linux__) || (defined(__APPLE__) && defined(__x86_64__)) #elif defined(__linux__)
namespace { namespace {
struct Context { struct Context {
+6 -1
View File
@@ -94,7 +94,12 @@ double ReadInput(SDL_Gamepad* gamepad, const std::string& name) {
// Fall back to this project's own positional names, so a binding written // Fall back to this project's own positional names, so a binding written
// here does not have to use Dolphin vocabulary. // here does not have to use Dolphin vocabulary.
if (const auto* native = ControllerNames::FindNativeButton(name)) { if (const auto* native = ControllerNames::FindNativeButton(name)) {
if (native->nativeButton != PAD_NATIVE_BUTTON_INVALID) { if (PADIsAxisButton(native->nativeButton)) {
const auto axis = static_cast<SDL_GamepadAxis>(PADAxisButtonAxis(native->nativeButton));
const double sign = PADAxisButtonNegative(native->nativeButton) ? -1.0 : 1.0;
return std::clamp(SDL_GetGamepadAxis(gamepad, axis) / 32767.0 * sign, 0.0, 1.0);
}
if (native->nativeButton < SDL_GAMEPAD_BUTTON_COUNT) {
return SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(native->nativeButton)) ? 1.0 return SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(native->nativeButton)) ? 1.0
: 0.0; : 0.0;
} }
+2 -11
View File
@@ -44,15 +44,10 @@
#include <signal.h> #include <signal.h>
#if defined(__x86_64__) #if defined(__x86_64__)
// Only the x86 POSIX fault path inspects ucontext_t to recover the page-fault // Only the x86 POSIX fault path inspects ucontext_t to recover the page-fault
// write bit. macOS exposes the signal-handler context through sys/ucontext.h; // write bit. macOS deprecates ucontext and requires _XOPEN_SOURCE just to
// avoid ucontext.h itself because its deprecated user-context APIs require // include the header, while the arm64 handler does not use it at all.
// _XOPEN_SOURCE. The arm64 handler does not inspect a host context at all.
#if defined(__APPLE__)
#include <sys/ucontext.h>
#else
#include <ucontext.h> #include <ucontext.h>
#endif #endif
#endif
#include <unistd.h> #include <unistd.h>
#endif #endif
@@ -1133,11 +1128,7 @@ void PosixMemoryFaultHandler(int sig, siginfo_t* info, void* ucontextVoid) {
// error code x86 pushes on a page fault records whether it was a write. // error code x86 pushes on a page fault records whether it was a write.
if (ucontextVoid != nullptr) { if (ucontextVoid != nullptr) {
auto* uc = static_cast<ucontext_t*>(ucontextVoid); auto* uc = static_cast<ucontext_t*>(ucontextVoid);
#if defined(__APPLE__)
isWrite = uc->uc_mcontext != nullptr && (uc->uc_mcontext->__es.__err & 0x2) != 0;
#else
isWrite = (uc->uc_mcontext.gregs[REG_ERR] & 0x2) != 0; isWrite = (uc->uc_mcontext.gregs[REG_ERR] & 0x2) != 0;
#endif
} }
#endif #endif
+4 -18
View File
@@ -4,9 +4,9 @@
#include <array> #include <array>
#include <atomic> #include <atomic>
#include <bit>
#include <chrono> #include <chrono>
#include <cstdint> #include <cstdint>
#include <cstring>
#include <iostream> #include <iostream>
#include <mutex> #include <mutex>
#include <thread> #include <thread>
@@ -52,20 +52,6 @@ std::array<float, kSoundPlayerCount> g_requestedSoundPlayerVolumes{};
std::array<float, kSoundPlayerCount> g_lastAppliedSoundPlayerVolumes{}; std::array<float, kSoundPlayerCount> g_lastAppliedSoundPlayerVolumes{};
std::array<bool, kSoundPlayerCount> g_haveSoundPlayerVolumes{}; std::array<bool, kSoundPlayerCount> g_haveSoundPlayerVolumes{};
uint32_t FloatBits(float value) noexcept {
static_assert(sizeof(float) == sizeof(uint32_t));
uint32_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
return bits;
}
float BitsFloat(uint32_t bits) noexcept {
static_assert(sizeof(float) == sizeof(uint32_t));
float value = 0.0f;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
float ClampSoundPlayerVolume(float volume) noexcept { float ClampSoundPlayerVolume(float volume) noexcept {
// Match nw4r::snd::SoundPlayer::SetVolume at 0x800A35E0 exactly, // Match nw4r::snd::SoundPlayer::SetVolume at 0x800A35E0 exactly,
// including its NaN behavior (unordered compares select the upper bound). // including its NaN behavior (unordered compares select the upper bound).
@@ -77,7 +63,7 @@ float ClampSoundPlayerVolume(float volume) noexcept {
bool WriteGuestFloat(uint32_t address, float value) noexcept { bool WriteGuestFloat(uint32_t address, float value) noexcept {
try { try {
Memory::Write32(address, FloatBits(value)); Memory::Write32(address, std::bit_cast<uint32_t>(value));
return true; return true;
} catch (const Memory::AccessViolation&) { } catch (const Memory::AccessViolation&) {
return false; return false;
@@ -89,7 +75,7 @@ bool ReadGuestFloat(uint32_t address, float& value) noexcept {
if (!Memory::TryRead32(address, bits)) { if (!Memory::TryRead32(address, bits)) {
return false; return false;
} }
value = BitsFloat(bits); value = std::bit_cast<float>(bits);
return true; return true;
} }
@@ -572,7 +558,7 @@ void SetSoundPlayerVolume(uint32_t soundPlayer, float requestedVolume) {
} }
// Preserve the original function's access semantics. An invalid player is // Preserve the original function's access semantics. An invalid player is
// a guest bug and must not be converted into a silent successful call. // a guest bug and must not be converted into a silent successful call.
Memory::Write32(soundPlayer + kSoundPlayerVolumeOffset, FloatBits(applied)); Memory::Write32(soundPlayer + kSoundPlayerVolumeOffset, std::bit_cast<uint32_t>(applied));
} }
} // namespace MusicAttenuation } // namespace MusicAttenuation
+305 -40
View File
@@ -20,6 +20,7 @@
#include <algorithm> #include <algorithm>
#include <atomic> #include <atomic>
#include <cctype> #include <cctype>
#include <charconv>
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
@@ -186,6 +187,18 @@ void LimitResolutionForFrameRate() {
using ControllerNames::FindNativeButton; using ControllerNames::FindNativeButton;
uint32_t ConfiguredNativeButton(const NativeButtonItem& item, const std::string& token) {
if (!PADIsAxisButton(item.nativeButton)) return item.nativeButton;
const size_t separator = token.find('@');
if (separator == std::string::npos) return item.nativeButton;
uint32_t threshold = 0;
const char* end = token.data() + token.size();
const auto parsed = std::from_chars(token.data() + separator + 1, end, threshold);
if (parsed.ec != std::errc{} || parsed.ptr != end || threshold < 1 || threshold > 100)
return item.nativeButton;
return PADAxisButtonIdentity(item.nativeButton) | (threshold << 8);
}
struct ControllerBindingPair { struct ControllerBindingPair {
std::string primary; std::string primary;
std::string secondary; std::string secondary;
@@ -204,6 +217,13 @@ ControllerBindingPair SplitControllerBinding(const std::string& value) {
using ControllerNames::NativeButtonForValue; using ControllerNames::NativeButtonForValue;
std::string NativeBindingConfig(uint32_t binding) {
std::string value = NativeButtonForValue(binding).configName;
if (PADIsAxisButton(binding)) value += '@' + std::to_string(PADAxisButtonThreshold(binding));
return value;
}
void SetTopBarVisible(bool visible) { void SetTopBarVisible(bool visible) {
if (g_topBarVisible == visible) { if (g_topBarVisible == visible) {
return; return;
@@ -242,7 +262,7 @@ void ApplyConfiguredMappings() {
} }
const ControllerBindingPair binding = SplitControllerBinding(*configured); const ControllerBindingPair binding = SplitControllerBinding(*configured);
if (const NativeButtonItem* native = FindNativeButton(binding.primary)) { if (const NativeButtonItem* native = FindNativeButton(binding.primary)) {
PADSetButtonMapping(port, PADButtonMapping{native->nativeButton, kControllerButtons[i].padButton}); PADSetButtonMapping(port, PADButtonMapping{ConfiguredNativeButton(*native, binding.primary), kControllerButtons[i].padButton});
} else { } else {
RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey
<< " button '" << binding.primary << "'" << std::endl; << " button '" << binding.primary << "'" << std::endl;
@@ -250,7 +270,7 @@ void ApplyConfiguredMappings() {
uint32_t altNative = PAD_NATIVE_BUTTON_INVALID; uint32_t altNative = PAD_NATIVE_BUTTON_INVALID;
if (!binding.secondary.empty()) { if (!binding.secondary.empty()) {
if (const NativeButtonItem* native = FindNativeButton(binding.secondary)) { if (const NativeButtonItem* native = FindNativeButton(binding.secondary)) {
altNative = native->nativeButton; altNative = ConfiguredNativeButton(*native, binding.secondary);
} else { } else {
RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey
<< " secondary button '" << binding.secondary << "'" << std::endl; << " secondary button '" << binding.secondary << "'" << std::endl;
@@ -395,6 +415,219 @@ void DrawWiiRemoteSettings(uint32_t selectedGamePort) {
ImGui::EndMenu(); ImGui::EndMenu();
} }
const char* KeyBindingName(int scancode) {
switch (scancode) {
case PAD_KEY_MOUSE_LEFT: return "Mouse left";
case PAD_KEY_MOUSE_RIGHT: return "Mouse right";
case PAD_KEY_MOUSE_MIDDLE: return "Mouse middle";
case PAD_KEY_MOUSE_X1: return "Mouse side 1";
case PAD_KEY_MOUSE_X2: return "Mouse side 2";
case PAD_KEY_INVALID: return "Unmapped";
default:
return scancode >= 0 && scancode < SDL_SCANCODE_COUNT
? SDL_GetScancodeName(static_cast<SDL_Scancode>(scancode)) : "Unknown";
}
}
enum class RebindKind { KeyboardButton, KeyboardAxis, Controller };
struct RebindState {
bool active = false;
bool openPopup = false;
RebindKind kind{};
uint32_t port = 0;
uint16_t target = 0;
bool secondary = false;
SDL_JoystickID instance = 0;
Clock::time_point deadline{};
std::string label;
std::array<bool, SDL_SCANCODE_COUNT> keys{};
uint32_t mouse = 0;
std::array<bool, SDL_GAMEPAD_BUTTON_COUNT> buttons{};
std::array<bool, SDL_GAMEPAD_AXIS_COUNT> axesReady{};
} g_rebind;
void BeginRebind(RebindKind kind, uint16_t target, const char* label, bool secondary = false) {
g_rebind = {};
g_rebind.active = true;
g_rebind.openPopup = true;
g_rebind.kind = kind;
g_rebind.port = static_cast<uint32_t>(g_controllerPort);
g_rebind.target = target;
g_rebind.secondary = secondary;
g_rebind.label = label;
g_rebind.deadline = Clock::now() + std::chrono::seconds(10);
int count = 0;
const bool* keys = SDL_GetKeyboardState(&count);
std::copy_n(keys, std::min(count, static_cast<int>(g_rebind.keys.size())), g_rebind.keys.begin());
g_rebind.mouse = SDL_GetMouseState(nullptr, nullptr);
const int index = PADGetIndexForPort(g_rebind.port);
if (kind == RebindKind::Controller && index >= 0) {
if (auto* pad = PADGetSDLGamepadForIndex(index)) {
g_rebind.instance = SDL_GetGamepadID(pad);
for (int i = 0; i < SDL_GAMEPAD_BUTTON_COUNT; ++i)
g_rebind.buttons[i] = SDL_GetGamepadButton(pad, static_cast<SDL_GamepadButton>(i));
for (int i = 0; i < SDL_GAMEPAD_AXIS_COUNT; ++i)
g_rebind.axesReady[i] = std::abs(static_cast<int>(SDL_GetGamepadAxis(pad, static_cast<SDL_GamepadAxis>(i)))) < 8000;
}
}
}
void CompleteRebind(uint32_t value) {
const auto& capture = g_rebind;
if (capture.kind == RebindKind::Controller) {
const int index = PADGetIndexForPort(capture.port);
auto* pad = index >= 0 ? PADGetSDLGamepadForIndex(index) : nullptr;
if (pad == nullptr || SDL_GetGamepadID(pad) != capture.instance) {
g_rebind.active = false;
return;
}
if (capture.secondary) PADSetAltButtonMapping(capture.port, {value, capture.target});
else PADSetButtonMapping(capture.port, {value, capture.target});
uint32_t count = 0, altCount = 0;
auto* primary = PADGetButtonMappings(capture.port, &count);
auto* alternate = PADGetAltButtonMappings(capture.port, &altCount);
uint32_t primaryValue = PAD_NATIVE_BUTTON_INVALID, alternateValue = PAD_NATIVE_BUTTON_INVALID;
for (uint32_t i = 0; i < count; ++i)
if (primary[i].padButton == capture.target) primaryValue = primary[i].nativeButton;
for (uint32_t i = 0; i < altCount; ++i)
if (alternate[i].padButton == capture.target) alternateValue = alternate[i].nativeButton;
std::string config = NativeBindingConfig(primaryValue);
if (alternateValue != PAD_NATIVE_BUTTON_INVALID) config += ',' + NativeBindingConfig(alternateValue);
for (size_t i = 0; i < kControllerButtons.size(); ++i)
if (kControllerButtons[i].padButton == capture.target) RuntimeConfigFile::SetControllerButton(i, config);
} else if (capture.kind == RebindKind::KeyboardButton) {
PADSetKeyButtonBinding(capture.port, {static_cast<int32_t>(value), capture.target});
} else {
PADSetKeyAxisBinding(capture.port, {static_cast<int32_t>(value), capture.target, 1});
}
PADSerializeMappings();
g_rebind.active = false;
}
void DrawRebindPrompt() {
if (g_rebind.openPopup) {
ImGui::OpenPopup("Rebind input");
g_rebind.openPopup = false;
}
if (!ImGui::BeginPopupModal("Rebind input", &g_rebind.active, ImGuiWindowFlags_AlwaysAutoResize)) {
g_rebind.active = false;
return;
}
if (g_rebind.active) {
ImGui::Text("Rebind: %s", g_rebind.label.c_str());
ImGui::TextUnformatted(g_rebind.kind == RebindKind::Controller
? "Press a controller button, pull a trigger, or move a stick."
: "Press a keyboard key or click a mouse button.");
ImGui::TextUnformatted("Release any held input first. Backspace or Delete clears the mapping.");
ImGui::TextUnformatted("Escape can be bound. F10 is reserved for settings.");
const float remaining = std::chrono::duration<float>(g_rebind.deadline - Clock::now()).count();
ImGui::Text("Unmapped in %d seconds", std::max(0, static_cast<int>(std::ceil(remaining))));
const bool clear = ImGui::Button("Clear mapping");
ImGui::SameLine();
if (ImGui::Button("Cancel")) g_rebind.active = false;
// UI clicks must not become mouse bindings (buttons activate on release).
const bool overControl = ImGui::IsAnyItemHovered();
if (g_rebind.active && (clear || remaining <= 0.0f)) {
CompleteRebind(g_rebind.kind == RebindKind::Controller ? PAD_NATIVE_BUTTON_DISABLED
: static_cast<uint32_t>(PAD_KEY_INVALID));
} else if (g_rebind.active && SDL_GetKeyboardFocus() != nullptr && g_rebind.kind != RebindKind::Controller) {
int count = 0;
const bool* keys = SDL_GetKeyboardState(&count);
for (int i = 1; i < std::min(count, static_cast<int>(SDL_SCANCODE_COUNT)) && g_rebind.active; ++i) {
if (keys[i] && !g_rebind.keys[i] && i != SDL_SCANCODE_F10) CompleteRebind(i);
g_rebind.keys[i] = keys[i];
}
const uint32_t mouse = SDL_GetMouseState(nullptr, nullptr);
for (int i = 1; i <= 5 && g_rebind.active; ++i)
if (!overControl && (mouse & ~g_rebind.mouse & (1u << (i - 1))) != 0) CompleteRebind(static_cast<uint32_t>(-i - 1));
g_rebind.mouse = mouse;
} else if (g_rebind.active && SDL_GetKeyboardFocus() != nullptr && g_rebind.kind == RebindKind::Controller) {
auto* pad = SDL_GetGamepadFromID(g_rebind.instance);
if (pad != nullptr) {
for (int i = 0; i < SDL_GAMEPAD_BUTTON_COUNT && g_rebind.active; ++i) {
const bool pressed = SDL_GetGamepadButton(pad, static_cast<SDL_GamepadButton>(i));
if (pressed && !g_rebind.buttons[i]) CompleteRebind(i);
g_rebind.buttons[i] = pressed;
}
for (int i = 0; i < SDL_GAMEPAD_AXIS_COUNT && g_rebind.active; ++i) {
const int value = SDL_GetGamepadAxis(pad, static_cast<SDL_GamepadAxis>(i));
if (std::abs(value) < 8000) g_rebind.axesReady[i] = true;
if (g_rebind.axesReady[i] && std::abs(value) >= 16384)
CompleteRebind(PADEncodeAxisButton(i, value < 0));
}
}
}
}
if (!g_rebind.active) ImGui::CloseCurrentPopup();
ImGui::EndPopup();
}
void DrawKeyBinding(const char* label, int scancode, RebindKind kind, uint16_t target) {
const std::string caption = std::string(KeyBindingName(scancode)) + "##binding";
if (ImGui::Button(caption.c_str(), ImVec2(220.0f, 0.0f))) BeginRebind(kind, target, label);
ImGui::SameLine();
ImGui::TextUnformatted(label);
}
bool DrawKeyboardSettings(uint32_t port) {
uint32_t count = 0;
auto* buttons = PADGetKeyButtonBindings(port, &count);
bool enabled = buttons != nullptr;
bool usePreset = false;
if (ImGui::Checkbox("Keyboard and mouse", &enabled)) {
PADSetKeyboardActive(port, enabled);
PADSerializeMappings();
buttons = PADGetKeyButtonBindings(port, &count);
usePreset = enabled && std::all_of(buttons, buttons + count, [](const auto& binding) {
return binding.scancode == PAD_KEY_INVALID;
});
}
if (!enabled) return false;
ImGui::TextDisabled("Replaces the gamepad on this port. F10 opens settings.");
if (ImGui::Button("Use WASD + mouse preset") || usePreset) {
const std::array<int, PAD_BUTTON_COUNT> keys = {
PAD_KEY_MOUSE_LEFT, SDL_SCANCODE_SPACE, SDL_SCANCODE_E, SDL_SCANCODE_Q,
SDL_SCANCODE_RETURN, PAD_KEY_MOUSE_MIDDLE, SDL_SCANCODE_LSHIFT, PAD_KEY_MOUSE_RIGHT,
SDL_SCANCODE_UP, SDL_SCANCODE_DOWN, SDL_SCANCODE_LEFT, SDL_SCANCODE_RIGHT,
};
for (size_t i = 0; i < keys.size(); ++i)
PADSetKeyButtonBinding(port, {keys[i], kControllerButtons[i].padButton});
const std::array<int, PAD_AXIS_COUNT> axes = {
SDL_SCANCODE_D, SDL_SCANCODE_A, SDL_SCANCODE_W, SDL_SCANCODE_S,
SDL_SCANCODE_L, SDL_SCANCODE_J, SDL_SCANCODE_I, SDL_SCANCODE_K,
SDL_SCANCODE_LSHIFT, PAD_KEY_MOUSE_RIGHT,
};
uint32_t axisCount = 0;
auto* mappings = PADGetKeyAxisBindings(port, &axisCount);
for (uint32_t i = 0; i < axisCount; ++i)
PADSetKeyAxisBinding(port, {axes[i], mappings[i].padAxis, 1});
PADSerializeMappings();
}
ImGui::SeparatorText("Button mapping");
for (uint32_t i = 0; i < count; ++i) {
int key = buttons[i].scancode;
ImGui::PushID(static_cast<int>(i));
ImGui::SetNextItemWidth(220.0f);
DrawKeyBinding(PADGetButtonName(buttons[i].padButton), key, RebindKind::KeyboardButton, buttons[i].padButton);
ImGui::PopID();
}
ImGui::SeparatorText("Stick and trigger mapping");
uint32_t axisCount = 0;
auto* axes = PADGetKeyAxisBindings(port, &axisCount);
for (uint32_t i = 0; i < axisCount; ++i) {
int key = axes[i].scancode;
ImGui::PushID(static_cast<int>(count + i));
const char* direction = PADGetAxisDirectionLabel(axes[i].padAxis);
const std::string label = std::string(PADGetAxisName(axes[i].padAxis)) + " " +
(direction != nullptr ? direction : "");
ImGui::SetNextItemWidth(220.0f);
DrawKeyBinding(label.c_str(), key, RebindKind::KeyboardAxis, axes[i].padAxis);
ImGui::PopID();
}
return true;
}
// Controller settings menu: port selection, controller assignment and button mapping. // Controller settings menu: port selection, controller assignment and button mapping.
int ExpressionResizeCallback(ImGuiInputTextCallbackData* data) { int ExpressionResizeCallback(ImGuiInputTextCallbackData* data) {
if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) { if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
@@ -501,6 +734,10 @@ void DrawControllerSettings() {
ImGui::Separator(); ImGui::Separator();
const uint32_t selectedGamePort = static_cast<uint32_t>(g_controllerPort); const uint32_t selectedGamePort = static_cast<uint32_t>(g_controllerPort);
if (DrawKeyboardSettings(selectedGamePort)) {
return;
}
ImGui::Separator();
const char* currentName = PADGetName(selectedGamePort); const char* currentName = PADGetName(selectedGamePort);
ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None"); ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None");
if (ImGui::MenuItem("Unassign controller")) { if (ImGui::MenuItem("Unassign controller")) {
@@ -542,10 +779,10 @@ void DrawControllerSettings() {
PADGetAltButtonMappings(static_cast<uint32_t>(g_controllerPort), &altMappingCount); PADGetAltButtonMappings(static_cast<uint32_t>(g_controllerPort), &altMappingCount);
const auto writeBinding = [](size_t index, uint32_t primaryNative, uint32_t altNative) { const auto writeBinding = [](size_t index, uint32_t primaryNative, uint32_t altNative) {
std::string value = NativeButtonForValue(primaryNative).configName; std::string value = NativeBindingConfig(primaryNative);
if (altNative != PAD_NATIVE_BUTTON_INVALID) { if (altNative != PAD_NATIVE_BUTTON_INVALID) {
value += ','; value += ',';
value += NativeButtonForValue(altNative).configName; value += NativeBindingConfig(altNative);
} }
RuntimeConfigFile::SetControllerButton(index, value); RuntimeConfigFile::SetControllerButton(index, value);
}; };
@@ -603,6 +840,11 @@ void DrawControllerSettings() {
} }
ImGui::SeparatorText("Button mapping"); ImGui::SeparatorText("Button mapping");
ImGui::TextDisabled("LT / L2 = left trigger. RT / R2 = right trigger.");
ImGui::TextDisabled("LB / L1 = left shoulder. RB / R1 = right shoulder.");
ImGui::TextDisabled("Click a binding, then press an input. No input for 10 seconds clears it.");
const float bindingWidth = ImGui::CalcTextSize("Right shoulder (RB / R1)").x +
ImGui::GetFrameHeight() + ImGui::GetStyle().FramePadding.x * 2.0f;
for (size_t i = 0; i < kControllerButtons.size(); ++i) { for (size_t i = 0; i < kControllerButtons.size(); ++i) {
auto mappingIt = std::find_if(mappings, mappings + mappingCount, [&](const PADButtonMapping& mapping) { auto mappingIt = std::find_if(mappings, mappings + mappingCount, [&](const PADButtonMapping& mapping) {
return mapping.padButton == kControllerButtons[i].padButton; return mapping.padButton == kControllerButtons[i].padButton;
@@ -622,30 +864,40 @@ void DrawControllerSettings() {
const NativeButtonItem& current = NativeButtonForValue(mappingIt->nativeButton); const NativeButtonItem& current = NativeButtonForValue(mappingIt->nativeButton);
ImGui::PushID(static_cast<int>(i)); ImGui::PushID(static_cast<int>(i));
ImGui::SetNextItemWidth(190.0f); const auto drawThreshold = [&](PADButtonMapping* mapping, bool secondary) {
if (ImGui::BeginCombo("##primary", current.label)) { if (!PADIsAxisButton(mapping->nativeButton)) return;
for (const auto& candidate : kNativeButtons) { int threshold = static_cast<int>(PADAxisButtonThreshold(mapping->nativeButton));
const bool selected = candidate.nativeButton == mappingIt->nativeButton; ImGui::SetNextItemWidth(bindingWidth);
if (ImGui::Selectable(candidate.label, selected)) { if (ImGui::SliderInt(secondary ? "##altThreshold" : "##primaryThreshold", &threshold,
const uint32_t port = static_cast<uint32_t>(g_controllerPort); 1, 100, "Threshold: %d%%", ImGuiSliderFlags_AlwaysClamp)) {
PADSetButtonMapping(port, PADButtonMapping{candidate.nativeButton, kControllerButtons[i].padButton}); const PADButtonMapping updated = {
writeBinding(i, candidate.nativeButton, PADAxisButtonIdentity(mapping->nativeButton) | (static_cast<uint32_t>(threshold) << 8),
mapping->padButton,
};
if (secondary) PADSetAltButtonMapping(selectedGamePort, updated);
else PADSetButtonMapping(selectedGamePort, updated);
}
if (ImGui::IsItemDeactivatedAfterEdit()) {
writeBinding(i, mappingIt->nativeButton,
altIt != nullptr ? altIt->nativeButton : PAD_NATIVE_BUTTON_INVALID); altIt != nullptr ? altIt->nativeButton : PAD_NATIVE_BUTTON_INVALID);
PADSerializeMappings(); PADSerializeMappings();
mappings = PADGetButtonMappings(port, &mappingCount);
} }
if (selected) { };
ImGui::SetItemDefaultFocus(); ImGui::BeginGroup();
} ImGui::SetNextItemWidth(bindingWidth);
} const std::string primaryCaption = std::string(current.label) + "##primary";
ImGui::EndCombo(); if (ImGui::Button(primaryCaption.c_str(), ImVec2(bindingWidth, 0.0f))) {
BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label);
} }
drawThreshold(mappingIt, false);
ImGui::EndGroup();
if (altIt != nullptr) { if (altIt != nullptr) {
const bool altBound = altIt->nativeButton != PAD_NATIVE_BUTTON_INVALID; const bool altBound = altIt->nativeButton != PAD_NATIVE_BUTTON_INVALID;
if (!altBound && !altRowExpanded[i]) { if (!altBound && !altRowExpanded[i]) {
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::SmallButton("+")) { if (ImGui::SmallButton("+")) {
altRowExpanded[i] = true; altRowExpanded[i] = true;
BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label, true);
} }
if (ImGui::IsItemHovered()) { if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Add a second binding; pressing either one works"); ImGui::SetTooltip("Add a second binding; pressing either one works");
@@ -654,27 +906,15 @@ void DrawControllerSettings() {
ImGui::SameLine(); ImGui::SameLine();
ImGui::TextUnformatted("or"); ImGui::TextUnformatted("or");
ImGui::SameLine(); ImGui::SameLine();
ImGui::BeginGroup();
const char* altLabel = altBound ? NativeButtonForValue(altIt->nativeButton).label : "None"; const char* altLabel = altBound ? NativeButtonForValue(altIt->nativeButton).label : "None";
ImGui::SetNextItemWidth(190.0f); ImGui::SetNextItemWidth(bindingWidth);
if (ImGui::BeginCombo("##alt", altLabel)) { const std::string altCaption = std::string(altLabel) + "##alt";
for (const auto& candidate : kNativeButtons) { if (ImGui::Button(altCaption.c_str(), ImVec2(bindingWidth, 0.0f))) {
const bool isNone = candidate.nativeButton == PAD_NATIVE_BUTTON_INVALID; BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label, true);
const bool selected = candidate.nativeButton == altIt->nativeButton;
if (ImGui::Selectable(isNone ? "None" : candidate.label, selected)) {
const uint32_t port = static_cast<uint32_t>(g_controllerPort);
PADSetAltButtonMapping(
port, PADButtonMapping{candidate.nativeButton, kControllerButtons[i].padButton});
writeBinding(i, mappingIt->nativeButton, candidate.nativeButton);
if (isNone) {
altRowExpanded[i] = false;
}
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
} }
drawThreshold(altIt, true);
ImGui::EndGroup();
} }
} }
ImGui::SameLine(); ImGui::SameLine();
@@ -936,10 +1176,27 @@ void DrawStartupScreen() {
} }
void DrawTopBar() { void DrawTopBar() {
if (!g_topBarVisible || !ImGui::BeginMainMenuBar()) { if (!g_topBarVisible) {
return; return;
} }
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::GetBackgroundDrawList()->AddRectFilled(viewport->Pos,
ImVec2(viewport->Pos.x + viewport->Size.x, viewport->Pos.y + viewport->Size.y),
IM_COL32(0, 0, 0, 70));
ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x + viewport->Size.x * 0.5f,
viewport->Pos.y + viewport->Size.y - 24.0f),
ImGuiCond_Always, ImVec2(0.5f, 1.0f));
ImGui::SetNextWindowBgAlpha(0.85f);
if (ImGui::Begin("Settings input hint", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoFocusOnAppearing)) {
ImGui::TextUnformatted("Settings open - game controls disabled. Press F10 to return to the game.");
}
ImGui::End();
if (!ImGui::BeginMainMenuBar()) return;
ImGui::TextUnformatted("WiiCompiled"); ImGui::TextUnformatted("WiiCompiled");
ImGui::Separator(); ImGui::Separator();
const auto resolutionIt = std::find_if(kResolutions.begin(), kResolutions.end(), [](const ResolutionItem& item) { const auto resolutionIt = std::find_if(kResolutions.begin(), kResolutions.end(), [](const ResolutionItem& item) {
@@ -968,6 +1225,9 @@ void DrawTopBar() {
if (ImGui::BeginMenu("Controller settings")) { if (ImGui::BeginMenu("Controller settings")) {
DrawControllerSettings(); DrawControllerSettings();
// Nest capture under this menu so opening/closing the modal preserves
// the settings popup and its current port and scroll position.
DrawRebindPrompt();
ImGui::EndMenu(); ImGui::EndMenu();
} }
@@ -1074,7 +1334,12 @@ void HandleEvents(const AuroraEvent* events) noexcept {
continue; continue;
} }
controller_mapping_wizard::HandleSdlEvent(ev->sdl); controller_mapping_wizard::HandleSdlEvent(ev->sdl);
if (IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) { if (g_rebind.active && (IsToggleKey(ev->sdl, SDL_SCANCODE_BACKSPACE) ||
IsToggleKey(ev->sdl, SDL_SCANCODE_DELETE))) {
CompleteRebind(g_rebind.kind == RebindKind::Controller ? PAD_NATIVE_BUTTON_DISABLED
: static_cast<uint32_t>(PAD_KEY_INVALID));
}
if (!g_rebind.active && IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) {
SetTopBarVisible(!g_topBarVisible); SetTopBarVisible(!g_topBarVisible);
} }
if (IsMouseActivity(ev->sdl)) { if (IsMouseActivity(ev->sdl)) {
@@ -1102,7 +1367,7 @@ void Draw() noexcept {
DrawTopBar(); DrawTopBar();
controller_mapping_wizard::Draw(); controller_mapping_wizard::Draw();
// The wizard captures raw presses; keep them out of the game. // The wizard captures raw presses; keep them out of the game.
const bool inputBlocked = controller_mapping_wizard::IsActive(); const bool inputBlocked = controller_mapping_wizard::IsActive() || g_rebind.active;
PADBlockInput(inputBlocked); PADBlockInput(inputBlocked);
InputBindings::SetInputBlocked(inputBlocked); InputBindings::SetInputBlocked(inputBlocked);
DrawStartupScreen(); DrawStartupScreen();
+5 -19
View File
@@ -97,29 +97,15 @@ int main() {
Check(!Pressed(pulse), "pulse expires"); Check(!Pressed(pulse), "pulse expires");
// The timing-window idiom seen in shared Dolphin configs. // The timing-window idiom seen in shared Dolphin configs.
auto window = Compile("!pulse(`W`, 0.05) & pulse(`W`, 0.35)"); auto window = Compile("!pulse(`W`, 0.05) & pulse(`W`, 0.15)");
g_inputs["W"] = 0.0; g_inputs["W"] = 0.0;
window.Evaluate(Source()); window.Evaluate(Source());
g_inputs["W"] = 1.0; g_inputs["W"] = 1.0;
Check(!Pressed(window), "window closed before its start"); Check(!Pressed(window), "window closed before its start");
bool windowOpened = false; Sleep(90);
for (int i = 0; i < 40; ++i) { Check(Pressed(window), "window open between the two pulses");
Sleep(10); Sleep(90);
if (Pressed(window)) { Check(!Pressed(window), "window closed after its end");
windowOpened = true;
break;
}
}
Check(windowOpened, "window open between the two pulses");
bool windowClosed = false;
for (int i = 0; i < 50; ++i) {
Sleep(10);
if (!Pressed(window)) {
windowClosed = true;
break;
}
}
Check(windowClosed, "window closed after its end");
// timer ramps 0..1 and wraps, so a threshold turns it into a square wave. // timer ramps 0..1 and wraps, so a threshold turns it into a square wave.
auto timer = Compile("`X` & timer(0.1)"); auto timer = Compile("`X` & timer(0.1)");
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@@ -378,7 +378,7 @@ public sealed partial class CxxLinearCodeGenerator
instructionContinuationLabels.TryGetValue(trace.Address, out var continuationLabel)) instructionContinuationLabels.TryGetValue(trace.Address, out var continuationLabel))
{ {
RecordEmittedLocalLabel(continuationLabel); RecordEmittedLocalLabel(continuationLabel);
body.AppendLine($"{continuationLabel}: ;"); body.AppendLine($"{continuationLabel}:");
} }
var localFallthroughLr = TryGetLocalFallthroughLr(block.Instructions, i, nonReturningCallTargets, lrContinuationCallTargets); var localFallthroughLr = TryGetLocalFallthroughLr(block.Instructions, i, nonReturningCallTargets, lrContinuationCallTargets);
// State-free bodies are cloned after register caching and lose their // State-free bodies are cloned after register caching and lose their
@@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using Translator.Core.Analysis.Representation; using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis.Ssa; using Translator.Core.Analysis.Ssa;
using Translator.Core.CodeGen; using Translator.Core.CodeGen;
@@ -153,35 +153,4 @@ public class EmittedOutputShapeTests
Assert.Contains("f3.d = MemoryInline::FlatReadFloat32((r4 + 16));", code, StringComparison.Ordinal); Assert.Contains("f3.d = MemoryInline::FlatReadFloat32((r4 + 16));", code, StringComparison.Ordinal);
Assert.Contains("f4.d = MemoryInline::FlatReadFloat64((r4 + 24));", code, StringComparison.Ordinal); Assert.Contains("f4.d = MemoryInline::FlatReadFloat64((r4 + 24));", code, StringComparison.Ordinal);
} }
[Fact]
public void ContinuationLabelAtBlockEndEmitsValidCxx17Statement()
{
var function = new IrFunction("continuation_at_block_end", "0x800E7798", new[]
{
new IrBasicBlock("0x800E7798", new IrInstruction[]
{
new IrCall(string.Empty, "0x8179B000", System.Array.Empty<IrValue>()),
new IrTracePpc(0x800E77A0u, "nop", "0x60000000"),
new IrJump("0x800E77A4")
}),
new IrBasicBlock("0x800E77A4", new IrInstruction[]
{
new IrReturn(null)
})
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>());
var code = new CxxLinearCodeGenerator().Emit(
0x800E7798,
new SsaTransformer().Convert(function),
new FunctionAbiClassification(function.Name, ValueRepresentation.Void),
types,
lrContinuationCallTargets: new HashSet<uint> { 0x8179B000u });
// In C++17, a label before a closing brace is invalid without an intervening statement.
Assert.Contains("loc_800E77A0: ;", code, StringComparison.Ordinal);
Assert.DoesNotContain("loc_800E77A0:\n}", code.Replace("\r\n", "\n"), StringComparison.Ordinal);
} }
}
@@ -35,6 +35,39 @@ public class RetroWfcPayloadLoweringTests
Assert.Equal("moduleFunction", pointer.TargetKind); Assert.Equal("moduleFunction", pointer.TargetKind);
} }
[Fact]
public void ProductionPayloadValidatesAndTranslatesEverySupportedPatch()
{
var payloadRoot = Path.Combine(
AppContext.BaseDirectory,
"TestAssets",
"RetroWfcPayload");
WiiCompiled.Setup.Common.RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(payloadRoot);
var payloadPath = Path.Combine(
payloadRoot,
"binary",
"payload.RMCPD00.bin");
var payload = File.ReadAllBytes(payloadPath);
var result = RetroWfcPayload.Parse(
payload,
ProductionPayloadManifest(),
0x81800000u,
0x00200000u,
"TestAssets/RetroWfcPayload/binary/payload.RMCPD00.bin");
Assert.Equal("RMCPD00", result.Summary.Game);
Assert.Equal(payload.Length, result.Summary.PayloadImageSize);
Assert.True(result.LoweringPlan.IsPlannable);
Assert.Empty(result.LoweringPlan.Issues);
Assert.NotEmpty(result.LoweringPlan.StaticBytePatches);
Assert.NotEmpty(result.LoweringPlan.ExecutableHooks);
Assert.NotEmpty(result.LoweringPlan.StaticPointers);
Assert.All(result.LoweringPlan.ExecutableHooks, hook => Assert.NotNull(hook.TargetAddress));
Assert.All(result.LoweringPlan.StaticPointers, pointer => Assert.NotNull(pointer.TargetAddress));
Assert.NotEmpty(result.Summary.InitializationCallbacks);
}
private static BaseManifest TestManifest() => private static BaseManifest TestManifest() =>
new( new(
"test", "test",
@@ -52,6 +85,51 @@ public class RetroWfcPayloadLoweringTests
], ],
"ranges.json"); "ranges.json");
// The payload parser needs the base image's address classes and containing
// function ranges to prove every patch can be lowered. A single synthetic
// executable and writable ranges are sufficient here: the assertions above
// test the real production payload without checking proprietary game bytes
// into CI. The split also proves pointer patches lower as data writes.
private static BaseManifest ProductionPayloadManifest() =>
new(
"test",
1,
"RMCP01",
"P",
"",
0,
[
new BaseSectionMetadata(
".synthetic-text",
"synthetic.dol",
0x80000000u,
0x80800000u,
true,
false,
"synthetic_text.bin",
0),
new BaseSectionMetadata(
".synthetic-data",
"synthetic.dol",
0x80800000u,
0x81000000u,
false,
true,
"synthetic_data.bin",
0)
],
[
new BaseFunctionRangeMetadata(
0x80000000u,
0x80800000u,
"synthetic_base",
".synthetic-text",
0,
"test",
["Executable"])
],
"ranges.json");
private static byte[] BuildSharedPayloadFixture() private static byte[] BuildSharedPayloadFixture()
{ {
var payload = new byte[0x240]; var payload = new byte[0x240];
@@ -26,6 +26,11 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Translator.Core\Translator.Core.csproj" /> <ProjectReference Include="..\..\src\Translator.Core\Translator.Core.csproj" />
<ProjectReference Include="..\..\src\Translator.Cli\Translator.Cli.csproj" /> <ProjectReference Include="..\..\src\Translator.Cli\Translator.Cli.csproj" />
<ProjectReference Include="..\..\..\Launcher\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="TestAssets\RetroWfcPayload\binary\payload.RMCPD00.bin" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup> </ItemGroup>
<!-- <!--