32 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
patchzyy 8c6c177857 Simplify README by removing redundant input details 2026-09-05 10:11:48 +02:00
Nicholas Bly be153e0fa0 Add Dolphin-compatible input expressions and GCPadNew.ini import, DualSense L/R remapping, vibration toggle (#89)
* Add Dolphin-compatible input expressions and GCPadNew.ini import

Rebased onto current main; addresses both CodeRabbit reviews on #89.

- Expression engine matching Dolphin's semantics: doubles rather than
  booleans, 0.5 press threshold, & as min, | as max, and the functions if,
  min, max, clamp, abs, sqrt, pow, sin, cos, tan, deadzone, timer, toggle,
  hold, tap, pulse and smooth. Timing uses a steady clock in seconds, as
  Dolphin does, so a copied expression behaves identically.

- Expressions bind to the GameCube buttons and triggers, combined with the
  existing button mapping rather than replacing it, and are skipped while
  the settings overlay holds input.

- Import reads [GCPadN] from the Dolphin config directory or from
  GCPadNew.ini beside the executable. Stick axes are not expression driven
  and keep their normal mapping.

- Fixes #74: a digital button bound to L or R now reports a fully pulled
  analog trigger, plus a PlayStation preset and a vibration toggle.

Review fixes: config paths round-trip through RuntimeConfigFile::PathToUtf8
and PathFromUtf8 so non-ASCII paths open correctly on Windows, and the
duplicated exists branch is gone; the tap count is clamped before the
unsigned conversion; the expression editor uses resizable storage via
ImGuiInputTextFlags_CallbackResize so a long expression cannot be saved
truncated; clamp bounds are ordered before std::clamp; <cstdlib> is included
for std::strtod; non-finite values are rejected at the evaluator boundary as
well as at the deadzone and timer divisions; and InputBindings::Reload() runs
from InitializeRuntimeSettings rather than the vibration handler.

runtime/tests/test_expr.cpp covers operator precedence, each stateful
function and every case raised in review.

Third review round: smooth() guards NaN as well as infinity so a zero rate
cannot latch a non-finite value in node state; division evaluates both operands
so stateful functions in the left subtree still update when the divisor is zero;
the expression editor clears stale errors when the port changes; and
runtime/tests/test_expr.cpp is registered with CTest as mkw_input_expr_tests,
following the existing test targets.

* Update runtime/src/input_expr.cpp

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update runtime/src/input_expr.cpp

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update runtime/src/input_expr.cpp

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-09-05 10:07:05 +02:00
96 changed files with 3759 additions and 1572 deletions
+1
View File
@@ -3,3 +3,4 @@
# Patch files must stay LF: git apply matches context bytes against LF upstream sources
*.patch -text
translator/tests/Translator.Tests/TestAssets/**/*.bin binary
+12 -43
View File
@@ -14,6 +14,10 @@ concurrency:
cancel-in-progress: true
jobs:
recompilation:
name: Recompilation test
uses: ./.github/workflows/recomp-test.yml
translator:
name: Translator (build + test)
runs-on: windows-latest
@@ -26,6 +30,14 @@ jobs:
with:
dotnet-version: '8.0.x'
- name: Cache NuGet packages
uses: actions/cache@v5
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('translator/Translator.sln', '**/*.csproj', '**/*.props', '**/*.targets', '**/packages.lock.json', 'global.json', 'NuGet.config', 'nuget.config') }}
restore-keys: |
${{ runner.os }}-nuget-
- name: Restore
run: dotnet restore translator/Translator.sln
@@ -34,46 +46,3 @@ jobs:
- name: Test
run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal
ios-crosscompile:
name: iOS arm64 (cross-compile, no Xcode)
runs-on: ubuntu-24.04
env:
IOS_SDK_DIR: ${{ github.workspace }}/iOS-SDKs/iPhoneOS26.5.sdk
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
# The runner images carry clang up to 18; the iOS toolchain file is tested
# against upstream 19, so take it from apt.llvm.org rather than the archive.
- name: Install clang 19, lld and ninja
run: |
wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh
chmod +x /tmp/llvm.sh
sudo /tmp/llvm.sh 19
sudo apt-get install -y lld-19 ninja-build
# The only Apple piece in the build. Sparse-cloned so the checkout is one
# SDK (~50 MB) rather than every SDK the repository carries.
- name: Fetch the iPhoneOS SDK
run: |
git clone --depth 1 --filter=blob:none --sparse \
https://github.com/xybp888/iOS-SDKs.git "${{ github.workspace }}/iOS-SDKs"
git -C "${{ github.workspace }}/iOS-SDKs" sparse-checkout set --no-cone iPhoneOS26.5.sdk
# Compile-only: linking a product needs the translated shards, which come
# from the user's own Mario Kart Wii dump and are not in this repository.
# This proves the runtime's own sources still build for iOS arm64.
- name: Configure
run: |
cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}/runtime/cmake/ios-linux-toolchain.cmake" \
-DIOS_SDK="$IOS_SDK_DIR" \
-DMKW_IOS_LLVM_BIN=/usr/lib/llvm-19/bin \
-DMKW_BUILD_PRODUCTS=OFF \
-DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE \
-DAURORA_DAWN_PROVIDER=package
- name: Compile the runtime sources for iOS
run: cmake --build build-ios --target mkw_macos_native_compile
+5 -1
View File
@@ -20,6 +20,10 @@ concurrency:
cancel-in-progress: true
jobs:
recompilation:
name: Recompilation test
uses: ./.github/workflows/recomp-test.yml
linux-appimage:
name: Linux (AppImage, ${{ matrix.arch }})
strategy:
@@ -93,7 +97,7 @@ jobs:
release:
name: Publish GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
needs: [linux-appimage, windows-installer]
needs: [linux-appimage, windows-installer, recompilation]
runs-on: ubuntu-latest
permissions:
contents: write
+61
View File
@@ -0,0 +1,61 @@
name: Synthetic recompilation
on:
workflow_call:
workflow_dispatch:
permissions:
contents: read
jobs:
windows:
name: Windows runtime (synthetic DOL)
runs-on: windows-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '8.0.x'
# Cache downloads only. Preparation still validates pins, and every run
# compiles current Aurora, runtime, and generated sources from scratch.
- uses: actions/cache@v5
with:
path: Launcher/artifacts/downloads
key: windows-recomp-downloads-${{ hashFiles('Launcher/Prepare-PortableTools.ps1', 'Launcher/Prepare-Dependencies.ps1') }}
# Install sccache.
- name: Run sccache-action
uses: mozilla/sccache-action@v0.0.11
# Tell CMake to use sccache and use GitHub's API.
- name: Configure sccache environment
shell: pwsh
run: |
"SCCACHE_GHA_ENABLED=true" | Add-Content -Path $env:GITHUB_ENV
"ACTIONS_CACHE_SERVICE_V2=on" | Add-Content -Path $env:GITHUB_ENV
"CMAKE_C_COMPILER_LAUNCHER=$env:SCCACHE_PATH" | Add-Content -Path $env:GITHUB_ENV
"CMAKE_CXX_COMPILER_LAUNCHER=$env:SCCACHE_PATH" | Add-Content -Path $env:GITHUB_ENV
- name: Prepare the shipped Windows toolchain
shell: pwsh
run: ./Launcher/Prepare-PortableTools.ps1
- name: Prepare pinned native dependencies
shell: pwsh
run: ./Launcher/Prepare-Dependencies.ps1
# Expose cache token context to the build script.
- name: Translate, compile the full runtime, and link
shell: pwsh
run: ./Launcher/Test-Recompilation.ps1 -Parallel 4
# Print cache results (even if the build fails)
- name: Show sccache stats
if: always()
shell: pwsh
run: sccache --show-stats
+4
View File
@@ -26,6 +26,8 @@ Code.pul
/build/
/build-*/
/native-build/
/native-build-macos/
/local-products/
/dist/
/out/
[Bb]in/
@@ -70,3 +72,5 @@ project.lock.json
*.log
output.txt
# Operating System
.DS_Store
+1 -1
View File
@@ -281,7 +281,7 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
$manifest = [ordered]@{
SchemaVersion = 2
ProductVersion = '0.2.27'
ProductVersion = '0.2.31'
ExpectedGameId = $pins.GameId
ExpectedDolSha256 = $pins.DolSha256
ExpectedRelSha256 = $pins.RelSha256
+14 -8
View File
@@ -117,12 +117,13 @@ function Get-MkwProjectPins([string]$ProjectFile) {
}
function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Description,
[string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '') {
[string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '', [bool]$WaitForProcessTree = $true) {
<#
Runs a build tool and turns a non-zero exit code into a described failure. Start-Process -Wait
is deliberate: it waits for the whole process tree, since a .NET single-file bundle host may
hand off to an extracted child that PowerShell's call operator would not wait for. Start-Process
doesn't publish $LASTEXITCODE, so this sets it manually for callers that check it.
Runs a build tool and turns a non-zero exit code into a described failure. By default,
Start-Process -Wait waits for the whole process tree, since a .NET single-file bundle host may
hand off to an extracted child that PowerShell's call operator would not wait for. Callers that
need to avoid waiting on unrelated descendants can opt into the call-operator path.
Start-Process doesn't publish $LASTEXITCODE, so this sets it manually for callers that check it.
-StepId emits the machine-readable form the installer's progress bar consumes (BuildStepIds in
WiiCompiled.Setup/InstallProgress.cs); the human sentence stays on the same log line.
#>
@@ -132,9 +133,14 @@ function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Descri
if ($_.Contains('"')) { throw "A native build argument contains an unsupported quote: $_" }
'"' + $_ + '"'
})
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
-NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
if ($WaitForProcessTree) {
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
-NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
} else {
& $FilePath @Arguments
$exitCode = $LASTEXITCODE
}
$global:LASTEXITCODE = $exitCode
if ($exitCode -ne 0) { throw "$Description failed with exit code $exitCode." }
}
+1 -1
View File
@@ -88,7 +88,7 @@ assert_file "$ninja_bin" "Portable Ninja"
assert_file "$cc" "Portable C compiler"
assert_file "$cxx" "Portable C++ compiler"
assert_dir "$aurora_source" "aurora-main source tree"
clang_binary=$(normalize "$toolchain_dir/bin/clang-23")
clang_binary=$(normalize "$toolchain_dir/bin/clang-22")
assert_file "$clang_binary" "Portable clang driver binary"
(( parallel > 0 )) || parallel=$(nproc)
+7 -1
View File
@@ -1,6 +1,6 @@
# Fails the release build when a fact duplicated across the repo stops agreeing with the copy
# that owns it (recomp.yml). Scripts read pinned facts through Get-MkwProjectPins, but three
# consumers can't read YAML (the C++ runtime header, the C# constants, hand-written lists on
# consumers can't read YAML (the C++ runtime header, the C# constants, shell scripts, and hand-written lists on
# both sides of the C#/PowerShell boundary), so those are checked here instead.
[CmdletBinding()]
param([string]$RepositoryRoot)
@@ -58,6 +58,12 @@ $hostUri = Get-CapturedValue $retroWfcPayload 'CurrentRetroWfcPayloadUri\s*=\s*"
if ($hostUri -cne $pins.RetroWfcPayloadUri) {
Add-Failure "InputValidation.CurrentRetroWfcPayloadUri is '$hostUri' but recomp.yml pins '$($pins.RetroWfcPayloadUri)'."
}
$macosSetup = Read-SourceFile (Join-Path $launcher 'macos\setup.command') 'macOS setup.command'
$macosUri = Get-CapturedValue $macosSetup "'([^']*/api/wfc/payload\?g=RMCPD00)'" `
'The macOS Retro-WFC endpoint'
if ($macosUri -cne $pins.RetroWfcPayloadUri) {
Add-Failure "macOS setup.command downloads '$macosUri' but recomp.yml pins '$($pins.RetroWfcPayloadUri)'."
}
# --- The game identity: the manifest carries it, but the host also compiles a fallback for a
# --- manifest that predates the field, and that fallback decides which disc is accepted.
+148
View File
@@ -0,0 +1,148 @@
# Build the real Windows runtime with translated, entirely synthetic PowerPC code.
# No game dump, game symbol map, REL, mod download, or existing generated/ output is used.
[CmdletBinding()]
param(
[string]$PortableToolsDirectory = 'Launcher/artifacts/portable-tools',
[string]$DependencySourceDirectory = 'Launcher/artifacts/dependencies',
[string]$StageDirectory = 'build/recomp-test',
[ValidateRange(1, 64)] [int]$Parallel = 3
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 3.0
. (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1')
$repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
function Full([string]$Path) {
if ([IO.Path]::IsPathRooted($Path)) { return [IO.Path]::GetFullPath($Path) }
return [IO.Path]::GetFullPath((Join-Path $repoRoot $Path))
}
$portableTools = Full $PortableToolsDirectory
$dependencies = Full $DependencySourceDirectory
$stage = Full $StageDirectory
$dotnet = (Get-Command dotnet -CommandType Application).Source
$cmake = Join-Path $portableTools 'CMake/bin/cmake.exe'
$ninja = Join-Path $portableTools 'Ninja/ninja.exe'
$compilerBin = Join-Path $portableTools 'llvm-mingw/bin'
Assert-File $cmake 'Pinned CMake (run Prepare-PortableTools.ps1 first)'
Assert-File $ninja 'Pinned Ninja'
Assert-File (Join-Path $dependencies 'cppwinrt/winrt/base.h') 'Pinned dependencies (run Prepare-Dependencies.ps1 first)'
# Refuse reuse so a developer's game translation or an earlier build cannot make
# the test pass. Keep the staging tree after the run for diagnostics.
if (Test-Path -LiteralPath $stage) { throw "Test stage already exists; choose a fresh -StageDirectory: $stage" }
[IO.Directory]::CreateDirectory($stage) | Out-Null
Write-Host "Synthetic recompilation workspace: $stage"
# Copy current sources, including uncommitted edits, but no ignored build output.
# An isolated workspace preserves the developer's real generated/ directory.
$sourceFiles = & git -C $repoRoot -c core.quotepath=false ls-files --cached --others --exclude-standard -- runtime aurora-main
if ($LASTEXITCODE -ne 0) { throw 'Could not enumerate runtime and Aurora sources.' }
foreach ($relative in $sourceFiles | Sort-Object -Unique) {
$destination = Join-Path $stage $relative
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination)) | Out-Null
Copy-Item -LiteralPath (Join-Path $repoRoot $relative) -Destination $destination
}
# One synthetic text section: li r3,40; addi r3,r3,2; nop; blr.
# Native HLE wrappers also call these eight guest symbols directly. Give each
# its own generated blr function so the real product can link without game code.
# Keep this list explicit: a new unresolved guest dependency must fail the test.
[uint32]$entry = 0x80001000L
[uint32[]]$guestCallbacks = @(
0x8012B830L, 0x801A0620L, 0x801A1ED8L, 0x801A961CL,
0x801AADE0L, 0x801D8D30L, 0x801D9E94L, 0x8055531CL
)
$textSize = [int]($guestCallbacks[-1] - $entry + 4)
$dataOffset = 0x100 + $textSize
$dol = [byte[]]::new($dataOffset + 4)
function Write-BigEndian32([int]$Offset, [uint32]$Value) {
$dol[$Offset] = [byte](($Value -shr 24) -band 255)
$dol[$Offset + 1] = [byte](($Value -shr 16) -band 255)
$dol[$Offset + 2] = [byte](($Value -shr 8) -band 255)
$dol[$Offset + 3] = [byte]($Value -band 255)
}
Write-BigEndian32 0x00 0x100 # text[0] file offset
Write-BigEndian32 0x48 $entry # text[0] guest address
Write-BigEndian32 0x90 $textSize # text[0] length (unreachable gaps are zero)
Write-BigEndian32 0x1C $dataOffset # data[0] file offset
Write-BigEndian32 0x64 0x80600000L # data[0] guest address
Write-BigEndian32 0xAC 4 # data[0] length
Write-BigEndian32 0xD8 0x80601000L # BSS address
Write-BigEndian32 0xDC 32 # BSS length
Write-BigEndian32 0xE0 $entry # entry point
Write-BigEndian32 0x100 0x38600028 # li r3,40
Write-BigEndian32 0x104 0x38630002 # addi r3,r3,2
Write-BigEndian32 0x108 0x60000000 # nop
Write-BigEndian32 0x10C 0x4E800020 # blr
foreach ($address in $guestCallbacks) {
Write-BigEndian32 ([int](0x100 + $address - $entry)) 0x4E800020
}
Write-BigEndian32 $dataOffset 0x12345678
[IO.File]::WriteAllBytes((Join-Path $stage 'synthetic.dol'), $dol)
$entryPoints = (@($entry) + $guestCallbacks | ForEach-Object { '0x{0:X8}' -f $_ }) -join ', '
$functionMap = (@($entry) + $guestCallbacks | ForEach-Object { '{0:X8} func_{0:X8}' -f $_ }) -join "`n"
[IO.File]::WriteAllText((Join-Path $stage 'synthetic-functions.txt'), $functionMap)
$manifest = Join-Path $stage 'recomp.yml'
[IO.File]::WriteAllText($manifest, @"
schema_version: 1
workspace_root: .
project:
id: ci-synthetic-dol
display_name: CI Synthetic DOL
memory:
base: 0x80000000
size: 0x01800000
sda_base: 0x80600000
sda2_base: 0x80600000
inputs:
dol:
path: synthetic.dol
translation:
entry_points: [$entryPoints]
function_map:
path: synthetic-functions.txt
allow_unsupported_instructions: false
runtime:
native_abi_directories: []
native_registration_root: runtime/src
output:
root: generated
"@)
$translatorProject = Join-Path $repoRoot 'translator/src/Translator.Cli/Translator.Cli.csproj'
Invoke-Checked $dotnet @('build', $translatorProject, '-c', 'Release', '--disable-build-servers') 'Building the translator'
$translator = Join-Path $repoRoot 'translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll'
$metadata = Join-Path $stage 'generated/base_translation_output.json'
Invoke-Checked $dotnet @($translator, 'translate-recursive', '0x80001000', '--project', $manifest,
'--output-metadata', $metadata, '--threads', "$Parallel") `
'Translating the synthetic DOL'
# Function-map seeds can be skipped by discovery; do not accept a partial fixture.
$translated = Get-Content -LiteralPath $metadata -Raw | ConvertFrom-Json
foreach ($address in @($entry) + $guestCallbacks) {
if ($address -notin $translated.functions.entryPoint) {
throw ('Synthetic function 0x{0:X8} was not translated.' -f $address)
}
}
Invoke-Checked $dotnet @($translator, 'generate-data-init', '--project', $manifest) 'Generating synthetic data and runtime configuration'
Invoke-Checked $dotnet @($translator, 'emit-build-shards', '--project', $manifest) 'Emitting the production build graph'
$nativeBuild = Join-Path $stage 'native-build'
$oldPath = $env:PATH
try {
$env:PATH = Get-MkwToolchainPath $portableTools
$configure = Get-MkwNativeConfigureArguments -SourceDirectory (Join-Path $stage 'runtime') -BuildDirectory $nativeBuild `
-Ninja $ninja -CCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang.exe') `
-CxxCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang++.exe') `
-ResourceCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-windres.exe') `
-DependenciesDirectory $dependencies -AdditionalArguments @('-DMKW_BUILD_PRODUCTS=ON')
Invoke-Checked $cmake $configure 'Configuring the production Windows runtime' `
-WaitForProcessTree $false
Invoke-Checked $cmake @('--build', $nativeBuild, '--target', 'WiiCompiled', '--parallel', "$Parallel") `
'Compiling and linking the synthetic product with the full runtime' `
-WaitForProcessTree $false
Assert-File (Join-Path $nativeBuild 'WiiCompiled.exe') 'Linked synthetic product'
} finally {
$env:PATH = $oldPath
}
Write-Host 'Synthetic recompilation passed (translation, data generation, runtime compilation, and product link).'
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
<Version>0.2.22</Version>
<Version>0.2.31</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description>
@@ -24,7 +24,7 @@ public static class RetroWfcPayload
private static readonly TimeSpan RetroWfcDownloadTimeout = TimeSpan.FromSeconds(30);
private static readonly TimeSpan RetroWfcRetryDelay = TimeSpan.FromSeconds(1);
public const string CurrentRetroWfcPayloadUri = "http://nas.play.rwfc.net/payload?g=RMCPD00";
public const string CurrentRetroWfcPayloadUri = "https://rwfc.net/api/wfc/payload?g=RMCPD00";
private static readonly string RetroWfcOfflinePayloadFile =
Path.Combine("binary", "payload.RMCPD00.bin");
@@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
<Version>0.2.22</Version>
<Version>0.2.31</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description>
+1 -1
View File
@@ -3,7 +3,7 @@ namespace WiiCompiled.Setup.Linux;
internal static class ProductInfo
{
public const string Name = "WiiCompiled";
public const string Version = "0.2.27";
public const string Version = "0.2.31";
}
/// <summary>One installed product's record inside install-state.json.</summary>
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<AssemblyName>WiiCompiled.Setup.Linux</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Linux</RootNamespace>
<Version>0.2.22</Version>
<Version>0.2.31</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled on Linux</Description>
@@ -121,7 +121,7 @@ internal static class PlatformChecks
internal static class ProductInfo
{
public const string Name = "WiiCompiled";
public const string Version = "0.2.27";
public const string Version = "0.2.31";
/// <summary>
/// The setup executable is copied into the installation under this name. It is the launcher and
@@ -7,7 +7,7 @@
<AssemblyName>WiiCompiled.Setup</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.2.27</Version>
<Version>0.2.31</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled</Description>
+1 -1
View File
@@ -90,7 +90,7 @@ if [[ -n "$retro_dir" ]]; then
trap 'rm -rf "$payload_stage"' EXIT
/usr/bin/curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \
--retry 1 --output "$temporary_payload" \
'http://nas.play.rwfc.net/payload?g=RMCPD00' || fail 'could not download the Retro-WFC payload needed for online play'
'https://rwfc.net/api/wfc/payload?g=RMCPD00' || fail 'could not download the Retro-WFC payload needed for online play'
"$translator" validate-retro-wfc-payload --directory "$payload_stage" || \
fail 'downloaded Retro-WFC payload failed signature validation'
mkdir -p "$retro_wfc_dir/binary"
+20 -19
View File
@@ -25,7 +25,7 @@ set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace=$(cd "$script_dir/.." && pwd)
llvm_version=23.1.0
llvm_version=22.1.8
cmake_version=4.3.3
ninja_version=1.13.2
destination="$script_dir/artifacts/portable-tools"
@@ -52,13 +52,13 @@ done
case "$arch" in
x86_64) llvm_release_arch=X64; target_triple=x86_64-unknown-linux-gnu
llvm_release_sha256=18da30f77f475688a18f7704d23f9f155ae007ed9922dbed6850a9419d9fec8c
llvm_release_sha256=df0e1ecf16caf3489a272a5eea4eec9b0d82878f6477fa309504f918a0006384
cmake_release_arch=x86_64
cmake_sha256=927b2368a946c37269c3a66225ab00544e756459cdd0b5d0da438694fb9ff802
ninja_asset=ninja-linux.zip
ninja_sha256=5749cbc4e668273514150a80e387a957f933c6ed3f5f11e03fb30955e2bbead6 ;;
aarch64) llvm_release_arch=ARM64; target_triple=aarch64-unknown-linux-gnu
llvm_release_sha256=cfb31bfc713ef453248bf5bd026312f838ad6c52c25623e987cb6a340f3050d4
llvm_release_sha256=805efad2bb91cb4967fa569e0881d10c0f69c04461cf671cccbae19f547acc34
cmake_release_arch=aarch64
cmake_sha256=9ea38356dbd3e32e51029a3e09a0f2f8e117ef4fbcaad7a21ffb36409bbd5cb4
ninja_asset=ninja-linux-aarch64.zip
@@ -120,9 +120,9 @@ echo "prepare-portable-tools.sh: pruning to the minimal compile+link toolchain..
# clang: the real driver executable plus the clang/clang++ symlinks CMake/local-build.sh invoke.
# Stripped: debug symbols are dead weight for a bundled compiler nobody will debug.
cp -a "$src/bin/clang-23" "$work/bin/"
strip "$work/bin/clang-23"
ln -s clang-23 "$work/bin/clang"
cp -a "$src/bin/clang-22" "$work/bin/"
strip "$work/bin/clang-22"
ln -s clang-22 "$work/bin/clang"
ln -s clang "$work/bin/clang++"
# lld: linked via -fuse-ld=lld, which clang resolves by looking for ld.lld next to itself first -
@@ -201,10 +201,10 @@ Ninja $ninja_version
Apache License 2.0
EOF
echo "prepare-portable-tools.sh: smoke-testing the toolchain..."
smoke_dir=$(mktemp -d)
trap 'rm -rf "$smoke_dir"' EXIT
cat > "$smoke_dir/t.cpp" <<'EOF'
echo "prepare-portable-tools.sh: testing the toolchain..."
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
cat > "$test_dir/t.cpp" <<'EOF'
#include <vector>
#include <cstdio>
int main() {
@@ -214,24 +214,25 @@ int main() {
return sum == 6 ? 0 : 1;
}
EOF
"$work/bin/clang++" -std=c++20 -fuse-ld=lld "$smoke_dir/t.cpp" -o "$smoke_dir/t"
"$smoke_dir/t"
"$work/bin/clang++" -std=c++20 -fuse-ld=lld "$test_dir/t.cpp" -o "$test_dir/t"
"$test_dir/t"
# Also exercised together through CMake+Ninja, exactly how local-build.sh drives them - a plain
# clang++ invocation above would not catch a broken CMAKE_ROOT (Modules/Templates) or a Ninja that
# can't find the compiler.
cat > "$smoke_dir/CMakeLists.txt" <<'EOF'
cat > "$test_dir/CMakeLists.txt" <<'EOF'
cmake_minimum_required(VERSION 3.16)
project(smoke CXX)
add_executable(smoke t.cpp)
project(test CXX)
add_executable(test t.cpp)
EOF
"$work/bin/cmake" -S "$smoke_dir" -B "$smoke_dir/build" -G Ninja \
"$work/bin/cmake" -S "$test_dir" -B "$test_dir/build" -G Ninja \
-DCMAKE_MAKE_PROGRAM="$work/bin/ninja" -DCMAKE_CXX_COMPILER="$work/bin/clang++" >/dev/null
"$work/bin/cmake" --build "$smoke_dir/build" >/dev/null
"$smoke_dir/build/smoke"
"$work/bin/cmake" --build "$test_dir/build" >/dev/null
"$test_dir/build/test"
rm -rf "$smoke_dir"
rm -rf "$test_dir"
trap - EXIT
rm -rf "$toolchain_dir"
mv "$work" "$toolchain_dir"
echo "prepare-portable-tools.sh: toolchain ready at $toolchain_dir ($(du -sh "$toolchain_dir" | cut -f1))"
+29 -102
View File
@@ -46,35 +46,46 @@ Press **F10** while the game window has focus:
- Internal resolution
- FPS counter
- Controller assignment for all four ports
- Full per-controller button mapping
- Full per-controller button mapping, including the bumpers
- Dolphin-syntax input expressions and GCPadNew.ini import
- Controller vibration on/off
- Volume, instant mute, and the music ducking toggle
Everything you change is saved to `Config.toml` on the spot and restored next launch.
**Real controller support.**
Controllers are fed to the game as a GameCube controller.
Mappings are positional (`south`, `east`, `west`, `north`) rather than Xbox-labelled, so the
same config makes sense on Xbox, PlayStation, Nintendo and generic SDL pads alike, and extra
inputs like paddles, touchpads and share buttons show up when the hardware reports them.
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.**
Each GameCube control can carry an expression in Dolphin's input syntax, with the same operators
and the same functions.
A Dolphin `GCPadNew.ini` can be imported directly from the F10 bar.
**Vibration toggle.**
Force feedback can be turned off for every port at once.
The official Wii U / Switch GameCube adapter (WUP-028) works too; as with Dolphin, on Windows the
adapter must be switched to the WinUSB driver once (Zadig).
**Real Wii Remotes over Bluetooth.**
Pair a Wii Remote with Windows (Settings > Bluetooth > Add device, press 1+2 or SYNC, leave the
PIN empty) and the game reads it as an actual Wii Remote through KPAD: Wii Remote icons and
prompts, Wii Wheel tilt steering, wheelies and tricks all come from the game's own motion code.
Nunchuk and Classic Controller are real Wii extensions too: the game gets the Nunchuk's stick,
C/Z and accelerometer, and the Classic Controller through `KPADGetUnifiedWpadStatus` with its own
layout and icons, so its buttons do what the game says they do and no mapping is involved. Plug an
extension in or pull it out mid-game and the game switches control scheme like on the console
(the runtime patches SDL's Wii driver, which otherwise loses the remote for good on an extension
change). Only the Wii U Pro Controller, which has no Wii-era equivalent, is fed to the game as a
GameCube pad with Nintendo's layout. If a remote drops out or was switched on after launch, the
runtime keeps rescanning Bluetooth until it comes back (F10 > Controller settings > Wii Remotes). SDL's read of
the remote's factory accelerometer calibration often times out over Bluetooth (`console.log`
then says "Using fallback accelerometer calibration") and it falls back to a nominal zero point,
so the same menu has a one-button calibration (remote flat, buttons up) that removes the small
tilt offset some remotes show.
PIN empty)
Known limitations of the Wii Remote path:
- No IR pointer yet: menus are navigated with the D-pad and A (the game treats the remote as
@@ -115,92 +126,6 @@ Wheel Wizard downloads the setup tool from this repo and walks you through insta
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
### iOS
Requires macOS with Xcode. Build the `WiiCompiled` target for iOS arm64; the build writes
`WiiCompiled-unsigned.ipa` next to the app bundle.
```sh
cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_SYSTEM_NAME=iOS -DCMAKE_SYSTEM_PROCESSOR=arm64 \
-DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_SYSROOT=iphoneos \
-DCMAKE_OSX_DEPLOYMENT_TARGET=17.0 \
-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH \
-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY \
-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY \
-DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE \
-DAURORA_DAWN_PROVIDER=package
cmake --build build-ios --target WiiCompiled
```
#### iOS from Linux
The same .ipa builds on a Linux host with upstream clang and lld; no Theos, xtool or Xcode. Tested
on Debian 13 with `clang-19 lld-19 llvm-19 cmake ninja-build git`. The only Apple piece is the
iPhoneOS SDK, and [xybp888/iOS-SDKs](https://github.com/xybp888/iOS-SDKs) carries current ones:
```sh
git clone --depth 1 --filter=blob:none --sparse https://github.com/xybp888/iOS-SDKs.git /opt/iOS-SDKs
git -C /opt/iOS-SDKs sparse-checkout set --no-cone iPhoneOS26.5.sdk
```
Pass that directory as `IOS_SDK` (or copy `iPhoneOS.sdk` out of a Mac's Xcode to `/opt/iPhoneOS.sdk`,
the default). The translated shard manifest under `generated/` records absolute paths from the
machine that ran the translator, so either translate on the Linux host or symlink that path to your
checkout.
```sh
cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE=cmake/ios-linux-toolchain.cmake -DIOS_SDK=/opt/iOS-SDKs/iPhoneOS26.5.sdk \
-DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE -DAURORA_DAWN_PROVIDER=package
cmake --build build-ios --target WiiCompiled
```
#### iOS from Windows
The same toolchain file works on Windows with [llvm-mingw](https://github.com/mstorsjo/llvm-mingw),
which is what the Windows build already uses. Three things differ from a Linux host:
- llvm-mingw does not ship `ld64.lld.exe` or `llvm-install-name-tool.exe`; LLVM tools dispatch on
their file name, so copy `ld.lld.exe` to `ld64.lld.exe` and `llvm-objcopy.exe` to
`llvm-install-name-tool.exe` in its `bin` directory.
- Git for Windows checks the SDK repo's symlinks out as small text files unless `core.symlinks`
is on (Developer Mode). Either enable that before cloning, or replace each placeholder with a
copy of its target; `libSystem.tbd` is one of them and the link fails without it.
- Pass the toolchain file as an absolute path, and `MKW_IOS_LLVM_BIN` as the llvm-mingw `bin`
directory with forward slashes.
```powershell
cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release `
-DCMAKE_TOOLCHAIN_FILE=C:/src/Wiicompiled/runtime/cmake/ios-linux-toolchain.cmake `
-DIOS_SDK=C:/iOS-SDKs/iPhoneOS26.5.sdk -DMKW_IOS_LLVM_BIN=C:/llvm-mingw/bin `
-DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE -DAURORA_DAWN_PROVIDER=package
cmake --build build-ios --target WiiCompiled
```
Install it with AltStore or SideStore, which sign with your own Apple ID. The app needs the
`com.apple.developer.kernel.increased-memory-limit` entitlement or it exits at startup;
[GetMoreRam](https://github.com/hugeBlack/GetMoreRam) grants it with a free Apple ID.
GetMoreRam grants `com.apple.developer.kernel.extended-virtual-addressing` at the same time, which
matters on devices with less than 4 GB of RAM: the runtime reserves a 4 GiB guest address space at
startup and that is right on the limit iOS allows without it. It makes no difference on the 8 GB
devices this was developed on.
`runtime/cmake/ios/WiiCompiled.entitlements` is a template for signing by hand, with placeholders to
replace; the sideloaders build their own and ignore it. It carries both entitlements, so the
provisioning profile you sign with needs both capabilities enabled.
Copy `Config.toml` and an extracted `DATA` directory into the app's Documents folder, using Files on
the device or the Finder with it connected. Keep `dvd_root` relative:
```toml
[paths]
dvd_root = "DATA"
```
Touch controls appear when no gamepad is attached; tap the FPS readout for settings.
> [!CAUTION]
> Only take builds from this repository's
> [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an
@@ -239,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.
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
+16 -1
View File
@@ -171,6 +171,22 @@ typedef struct PADButtonMapping {
PADButton padButton;
} PADButtonMapping;
// Explicitly disabled, unlike INVALID which permits default L/R trigger input.
#define PAD_NATIVE_BUTTON_DISABLED 0xfffffffeu
// Axis-to-button bindings share the persisted nativeButton field without
// changing the binary layout of existing controller mapping files.
constexpr u32 PADEncodeAxisButton(u32 axis, bool negative, u32 threshold = 50) {
return 0x10000u | axis | (negative ? 0x80u : 0u) | (threshold << 8);
}
constexpr bool PADIsAxisButton(u32 binding) { return (binding & 0xffff0000u) == 0x10000u; }
constexpr u32 PADAxisButtonThreshold(u32 binding) { return (binding >> 8) & 0xffu; }
constexpr u32 PADAxisButtonAxis(u32 binding) { return binding & 0x7fu; }
constexpr bool PADAxisButtonNegative(u32 binding) { return (binding & 0x80u) != 0; }
constexpr u32 PADAxisButtonIdentity(u32 binding) {
return PADIsAxisButton(binding) ? (binding & ~0xff00u) : binding;
}
typedef struct PADAxisMapping {
PADSignedNativeAxis nativeAxis;
s32 nativeButton;
@@ -229,7 +245,6 @@ s32 PADGetNativeButtonPressed(u32 port);
PADSignedNativeAxis PADGetNativeAxisPulled(u32 port);
void PADRestoreDefaultMapping(u32 port);
void PADBlockInput(bool block);
bool PADIsInputBlocked(void);
/**
* Set the default controller mapping used.
+39 -12
View File
@@ -319,6 +319,18 @@ std::array<bool, PAD_CHANMAX> g_suppressLeftTrigger{};
std::array<bool, PAD_CHANMAX> g_suppressRightTrigger{};
bool is_mouse_scancode(const s32 scancode) { return scancode < PAD_KEY_INVALID; }
bool is_native_binding_pressed(SDL_Gamepad* gamepad, u32 binding) {
if (PADIsAxisButton(binding)) {
const u32 axis = PADAxisButtonAxis(binding);
const u32 threshold = PADAxisButtonThreshold(binding);
if (axis >= SDL_GAMEPAD_AXIS_COUNT || threshold < 1 || threshold > 100) return false;
int value = SDL_GetGamepadAxis(gamepad, static_cast<SDL_GamepadAxis>(axis));
if (PADAxisButtonNegative(binding)) value = -value;
return value > 0 && value * 100 >= static_cast<int>(threshold) * 32767;
}
return binding < SDL_GAMEPAD_BUTTON_COUNT &&
SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(binding));
}
bool is_mouse_button_pressed(const s32 scancode) {
const int32_t buttonNum = -(scancode + 1);
if (buttonNum < 1 || buttonNum > 5) {
@@ -724,10 +736,10 @@ u32 PADRead(PADStatus* status) {
}
status[i].err = PAD_ERR_NONE;
if (g_keyboardBindings[i].m_mappingsSet) {
if (g_keyboardBindings[i].m_mappingsSet && SDL_GetKeyboardFocus() != nullptr) {
std::ranges::for_each(
g_keyboardBindings[i].m_buttonMapping, [&kbState, &i, &status](const PADKeyButtonBinding& mapping) {
if (mapping.scancode > PAD_KEY_INVALID && kbState[mapping.scancode]) {
g_keyboardBindings[i].m_buttonMapping, [&kbState, &numKeys, &i, &status](const PADKeyButtonBinding& mapping) {
if (mapping.scancode > PAD_KEY_INVALID && mapping.scancode < numKeys && kbState[mapping.scancode]) {
status[i].button |= mapping.padButton;
} else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) {
status[i].button |= mapping.padButton;
@@ -788,7 +800,7 @@ u32 PADRead(PADStatus* status) {
status[i].triggerRight = static_cast<u8>(std::min(static_cast<int>(status[i].triggerRight) + tr, 255));
}
if (controller) {
if (controller && !g_keyboardBindings[i].m_mappingsSet) {
EnsureMappingLoaded(controller);
// Wii U Pro Controller raw D-pad fallback. SDL's HIDAPI Wii driver posts
@@ -835,7 +847,7 @@ u32 PADRead(PADStatus* status) {
bool rightTriggerSet = false;
std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet,
&rightTriggerSet](const auto& mapping) {
if (SDL_GetGamepadButton(controller->m_controller, static_cast<SDL_GamepadButton>(mapping.nativeButton))) {
if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) {
status[i].button |= mapping.padButton;
}
@@ -852,7 +864,7 @@ u32 PADRead(PADStatus* status) {
if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) {
return;
}
if (SDL_GetGamepadButton(controller->m_controller, static_cast<SDL_GamepadButton>(mapping.nativeButton))) {
if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) {
status[i].button |= mapping.padButton;
}
@@ -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 tr = std::max(static_cast<Sint16>(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_R));
// Games can read either the digital L/R bits or their analog pressure.
// An explicit button binding must drive both, otherwise the original
// L2/R2 axis still activates L/R even when it was rebound to L1/R1.
// Real GC pads retain independent analog travel and end-stop clicks.
if (!(controller->m_isGameCube ||
(SDL_GetGamepadType(controller->m_controller) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO &&
controller->m_pid == 0x2073))) {
if (leftTriggerSet) tl = (status[i].button & PAD_TRIGGER_L) != 0 ? 32767 : 0;
if (rightTriggerSet) tr = (status[i].button & PAD_TRIGGER_R) != 0 ? 32767 : 0;
}
if (controller->m_deadZones.emulateTriggers) {
if (!leftTriggerSet && tl > controller->m_deadZones.leftTriggerActivationZone) {
status[i].button |= PAD_TRIGGER_L;
@@ -990,12 +1013,13 @@ void PADControlMotor(const u32 chan, const u32 cmd) {
}
if (controller->m_isGameCube) {
if (cmd == PAD_MOTOR_STOP) {
aurora::input::controller_rumble(instance, 0, 1, 0);
if (cmd == PAD_MOTOR_STOP || cmd == PAD_MOTOR_STOP_HARD) {
// Use an unambiguous motor-off request. The (0, 1) coast encoding
// requires SDL's GameCube brake mode; other backends or an overridden
// hint interpret it as rumble and can leave the controller vibrating.
aurora::input::controller_rumble(instance, 0, 0, 0);
} else if (cmd == PAD_MOTOR_RUMBLE) {
aurora::input::controller_rumble(instance, 1, 1, 0);
} else if (cmd == PAD_MOTOR_STOP_HARD) {
aurora::input::controller_rumble(instance, 0, 0, 0);
}
} else {
if (cmd == PAD_MOTOR_STOP) {
@@ -1278,6 +1302,11 @@ BOOL PADSetKeyButtonBindings(const u32 port, PADKeyButtonBinding bindings[PAD_BU
}
PADKeyButtonBinding* PADGetKeyButtonBindings(const u32 port, u32* buttonCount) {
PADInit();
if (!g_keyboardBindingsLoaded) {
g_keyboardBindingsLoaded = true;
load_keyboard_bindings();
}
if (port >= PAD_MAX_CONTROLLERS || !g_keyboardBindings[port].m_mappingsSet) {
return nullptr;
}
@@ -1627,8 +1656,6 @@ void PADBlockInput(const bool block) {
}
}
bool PADIsInputBlocked() { return g_blockPAD.load(std::memory_order_acquire); }
SDL_Gamepad* PADGetSDLGamepadForIndex(const u32 index) {
const auto* ctrl = __PADGetControllerForIndex(index);
+22 -6
View File
@@ -168,7 +168,12 @@ struct RenderPass {
Range resolveUniformRange;
std::array<u32, 3> resolveCopyFilterCoefficients{0, 64, 0};
Vec4<float> clearColorValue{0.f, 0.f, 0.f, 0.f};
float clearDepthValue = 1.f;
// 1.f is the forward-Z "farthest" clear value; under UseReversedZ farthest is 0.f instead (see
// gx::clear_depth_value(), which the main render pass explicitly overrides this default with -
// any OTHER pass that keeps this default, e.g. an offscreen render-to-texture pass composited
// later, needs the same reversed-Z-aware value or its depth buffer starts "already nearest",
// failing every subsequent depth test and making whatever's drawn into it vanish).
float clearDepthValue = gx::UseReversedZ ? 0.f : 1.f;
CommandList commands;
bool clearColor = true;
bool clearDepth = true;
@@ -757,7 +762,9 @@ void begin_offscreen(uint32_t width, uint32_t height) {
.targetSize = {width, height, 1},
.msaaSamples = 1,
.clearColorValue = {0.f, 0.f, 0.f, 0.f},
.clearDepthValue = 1.f,
// See the RenderPass::clearDepthValue default's comment: this offscreen pass gets its own
// depth buffer, and the farthest clear value is 0.f, not 1.f, under UseReversedZ.
.clearDepthValue = gx::UseReversedZ ? 0.f : 1.f,
.clearColor = true,
.clearDepth = true,
};
@@ -1410,10 +1417,19 @@ static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vec
switch (cmd.type) {
case CommandType::SetViewport: {
const auto& vp = cmd.data.setViewport;
// WebGPU requires 0 <= minDepth <= maxDepth <= 1, and the guest's (near, far) order is already
// reproduced in clip space. Passing the raw swapped pair diverged per backend in release builds.
const float minDepth = std::clamp(std::min(vp.znear, vp.zfar), 0.0f, 1.0f);
const float maxDepth = std::clamp(std::max(vp.znear, vp.zfar), 0.0f, 1.0f);
// WebGPU requires 0 <= minDepth <= maxDepth <= 1. vp.znear/vp.zfar are in GX's own distance
// terms (0 = near); under UseReversedZ the host depth-buffer storage direction is flipped
// (near = 1, far = 0), so this range has to be remapped through 1-x the same way the
// projection matrix, depth compare function, and clear value all are - a plain min/max clamp
// (the previous code here) maps a *restricted* range (e.g. a viewport deliberately narrowed
// to force something to draw "in front of everything") to the wrong end of the buffer: what
// should land near the near-storage-extreme (1.0) instead lands near the far-storage-extreme
// (0.0), so anything else drawn afterward at its true depth wins the compare test and the
// "in front" geometry silently vanishes. A full [0,1] viewport is unaffected either way,
// which is why this only broke specific elements, not the whole scene. Matches upstream
// aurora's apply_viewport (lib/gfx/encoding.cpp) exactly.
const float minDepth = gx::UseReversedZ ? 1.0f - vp.zfar : vp.znear;
const float maxDepth = gx::UseReversedZ ? 1.0f - vp.znear : vp.zfar;
pass.SetViewport(vp.left, vp.top, vp.width, vp.height, minDepth, maxDepth);
} break;
case CommandType::SetScissor: {
+1 -1
View File
@@ -92,7 +92,7 @@ struct Params {
constexpr std::string_view ReversedZBody = R"(
fn gx_z24(depth: f32) -> u32 {
return min(u32(clamp(depth, 0.0, 1.0) * 16777216.0), 0x00ffffffu);
return min(u32(clamp(1.0 - depth, 0.0, 1.0) * 16777215.0 + 0.5), 0x00ffffffu);
}
)"sv;
+1 -1
View File
@@ -137,7 +137,7 @@ fn gx_z24_at_coord(unclamped_coord: vec2i) -> u32 {
let tex_size = vec2i(textureDimensions(src));
let coord = clamp(unclamped_coord, vec2i(0), tex_size - vec2i(1));
let depth = textureLoad(src, coord, 0);
return min(u32(clamp(depth, 0.0, 1.0) * 16777216.0), 0x00ffffffu);
return min(u32(clamp(1.0 - depth, 0.0, 1.0) * 16777215.0 + 0.5), 0x00ffffffu);
}
)"s
: R"(
+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) {
switch (func) {
DEFAULT_FATAL("invalid depth fn {}", underlying(func));
case GX_NEVER:
return wgpu::CompareFunction::Never;
case GX_LESS:
return wgpu::CompareFunction::Less;
return UseReversedZ ? wgpu::CompareFunction::Greater : wgpu::CompareFunction::Less;
case GX_EQUAL:
return wgpu::CompareFunction::Equal;
case GX_LEQUAL:
return wgpu::CompareFunction::LessEqual;
return UseReversedZ ? wgpu::CompareFunction::GreaterEqual : wgpu::CompareFunction::LessEqual;
case GX_GREATER:
return wgpu::CompareFunction::Greater;
return UseReversedZ ? wgpu::CompareFunction::Less : wgpu::CompareFunction::Greater;
case GX_NEQUAL:
return wgpu::CompareFunction::NotEqual;
case GX_GEQUAL:
return wgpu::CompareFunction::GreaterEqual;
return UseReversedZ ? wgpu::CompareFunction::LessEqual : wgpu::CompareFunction::GreaterEqual;
case GX_ALWAYS:
return wgpu::CompareFunction::Always;
}
+8 -1
View File
@@ -485,7 +485,14 @@ const gfx::TextureBind& get_texture(GXTexMapID id) noexcept;
void resolve_sampled_textures(const ShaderInfo& info) noexcept;
inline float clear_depth_value() {
return std::min(static_cast<float>(g_gxState.clearDepth) / 16777216.f, 16777215.f / 16777216.f);
// g_gxState.clearDepth is in GX's own distance terms (0 = near, larger = farther), independent of
// how UseReversedZ encodes that as a host depth value - it must be re-mapped the same way the
// projection matrix and depth compare function are, or the buffer clears to the wrong extreme
// (verified directly: matches upstream aurora's clear_depth_value, which does this same inversion
// and was the second missing piece alongside to_compare_function's compare-op inversion).
const float normalizedDepth =
std::min(static_cast<float>(g_gxState.clearDepth) / 16777216.f, 16777215.f / 16777216.f);
return UseReversedZ ? (1.f - normalizedDepth) : normalizedDepth;
}
inline bool render_target_has_alpha(GXPixelFmt pixelFmt) noexcept { return pixelFmt == GX_PF_RGBA6_Z24; }
+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 out.pos = vec4f(clip_base.xy + offset_ndc * clip_base.w, clip_base.zw);";
}
if constexpr (UseReversedZ) {
vtxXfrAttrsPre += "\n out.pos.z = -out.pos.z;";
} else {
vtxXfrAttrsPre += "\n out.pos.z += out.pos.w;";
}
// The near/far depth correction used to be applied here per-vertex (out.pos.z = -out.pos.z for
// reversed, or += out.pos.w for forward), redundantly on top of the same correction already
// folded into ubuf.proj by effective_projection() (shader_info.cpp) - applying it twice canceled
// out for the common case (any draw where effective_projection() decides to flip), silently
// making "reversed" Z behave identically to forward Z. It is now applied exactly once, in the
// projection matrix alone (matching upstream aurora commit 1dde08fa: "Move depth correction to
// projection matrix"), so nothing needs to happen to out.pos.z here.
// GX rasterizes at a 7/12 pixel center when antialiasing is disabled, while WebGPU rasterizes at 1/2.
vtxXfrAttrsPre +=
"\n let gx_pixel_center_correction = "
@@ -1465,7 +1467,14 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
textureDependency.texMapId, uvIn);
}
std::string fogDepthExpr = UseReversedZ ? "in.pos.z" : "(1.0 - in.pos.z)";
// in.pos.z is the host NDC z (forward: 0=near/1=far; reversed: 1=near/0=far post-fix), but this
// expression needs to produce GX's own native distance term (always 0=near/1=far, matching how
// g_gxState.clearDepth/clear_depth_value() are interpreted before their own UseReversedZ
// inversion) - forward already matches directly; reversed needs the same 1-x flip everything
// else reversed-Z-aware uses. This was backwards (verified directly against upstream aurora's
// identical expression in build_shader_source), which fed both fog density and the GX_ZT_ADD
// z-texture path the wrong distance value.
std::string fogDepthExpr = UseReversedZ ? "(1.0 - in.pos.z)" : "in.pos.z";
std::string fogZCoordExpr =
fmt::format("u32(round(clamp({}, 0.0, 1.0) * 16777216.0))", fogDepthExpr);
if (usesZTextureDepth) {
@@ -1498,7 +1507,7 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
fragmentFn += fmt::format(
"\n let oldZ = u32(round(clamp({0}, 0.0, 1.0) * 16777216.0));"
"\n ztexCoord = (ztexCoord + oldZ) & 0x00ffffffu;",
UseReversedZ ? "in.pos.z" : "(1.0 - in.pos.z)");
UseReversedZ ? "(1.0 - in.pos.z)" : "in.pos.z");
}
fragmentFn += "\n let ztexDepth = f32(ztexCoord) / 16777216.0;";
fogZCoordExpr = "ztexCoord";
@@ -1639,7 +1648,13 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
" @builtin(frag_depth) depth: f32,\n"
"};";
fragmentFn += fmt::format("\n let fragDepth = {}ztexDepth;", UseReversedZ ? "" : "1.0 - ");
// ztexDepth is in GX's native distance terms (0=near/1=far, see fogDepthExpr's comment above),
// but frag_depth must be written in the same host NDC-z convention in.pos.z itself uses -
// forward matches directly (no change), reversed needs the same 1-x flip. This was backwards
// the same way fogDepthExpr was (verified by the same derivation, since aurora upstream has no
// directly equivalent line here to cross-check against - this z-texture-depth-output path
// appears to be specific to this fork).
fragmentFn += fmt::format("\n let fragDepth = {}ztexDepth;", UseReversedZ ? "1.0 - " : "");
fragmentReturnType = "FragmentOutput";
fragmentReturn =
" var out: FragmentOutput;\n"
+12 -4
View File
@@ -548,14 +548,22 @@ constexpr size_t kStagedUniformBytes =
96 + sizeof(Mat4x4<float>) + sizeof(Mat3x4<float>) * (MaxPostexMtx + MaxPnMtx);
// The host viewport always receives the normalized GX depth window (render_pass_impl clamps to minDepth <= maxDepth).
//
// Folds the near/far depth correction the vertex shader used to apply per-vertex directly into the
// projection matrix instead (matching upstream aurora commit 1dde08fa, "Move depth correction to
// projection matrix") - valid because the correction is a linear combination of the z/w rows, so
// applying it once here to the row is equivalent to applying it once per-vertex to the dot product,
// and it must be applied exactly once: doing it here AND in the shader (the previous bug) canceled
// the negation out for `flip`, silently making "reversed" Z behave identically to forward Z.
// `flip` decides which of the two single-application forms this draw needs: true bakes in the
// reversed-Z inversion (z' = -z), false bakes in the forward-Z near/far combination (z' = z + w) -
// exactly one always applies, never both, and never neither.
static Mat4x4<float> effective_projection() noexcept {
const auto& vp = g_gxState.renderViewport;
const bool flip = (vp.znear <= vp.zfar) == UseReversedZ;
Mat4x4<float> proj = g_gxState.proj;
if (flip) {
for (size_t i = 0; i < 4; ++i) {
proj.m2.m[i] = -(proj.m2.m[i] + proj.m3.m[i]);
}
for (size_t i = 0; i < 4; ++i) {
proj.m2.m[i] = flip ? -proj.m2.m[i] : (proj.m2.m[i] + proj.m3.m[i]);
}
return proj;
}
+9
View File
@@ -401,6 +401,8 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
return -1;
}
controller.m_isGameCube = controller.m_vid == 0x057E && controller.m_pid == 0x0337;
const char* serial = SDL_GetGamepadSerial(ctrl);
controller.m_gameCubeUseOrdinaryStop = controller.m_isGameCube && serial && "GCP+"sv == serial;
if (controller.m_isGameCube ||
(SDL_GetGamepadType(ctrl) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO && controller.m_pid == 0x2073)) {
controller.m_deadZones.emulateTriggers = false;
@@ -481,6 +483,13 @@ bool controller_has_rumble(Uint32 instance) noexcept {
void controller_rumble(uint32_t instance, uint16_t low_freq_intensity, uint16_t high_freq_intensity,
uint16_t duration_ms) noexcept {
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
// GC Pocket+ has been observed continuing to vibrate after a hard stop;
// an ordinary stop cleared it. With GAMECUBE_RUMBLE_BRAKE enabled, SDL
// encodes (0, 1) as adapter command 0, whereas (0, 0) sends command 2.
// Apply the workaround here so shutdown uses the same stop as PAD calls.
if (it->second.m_gameCubeUseOrdinaryStop && low_freq_intensity == 0 && high_freq_intensity == 0) {
high_freq_intensity = 1;
}
SDL_RumbleGamepad(it->second.m_controller, low_freq_intensity, high_freq_intensity, duration_ms);
}
}
+1
View File
@@ -17,6 +17,7 @@ extern Module Log;
struct GameController {
SDL_Gamepad* m_controller = nullptr;
bool m_isGameCube = false;
bool m_gameCubeUseOrdinaryStop = false;
Sint32 m_index = -1;
Sint32 m_playerIndex = -1;
bool m_hasRumble = false;
+2 -18
View File
@@ -11,9 +11,6 @@
#include <aurora/event.h>
#include <aurora/gfx.h>
#include <aurora/render_size_limits.hpp>
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
#include <SDL3/SDL_error.h>
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_keyboard.h>
@@ -411,21 +408,8 @@ bool create_window(AuroraBackend backend) {
height = 480;
}
Sint32 posX = g_config.hasWindowPosition ? g_config.windowPosX : SDL_WINDOWPOS_CENTERED;
Sint32 posY = g_config.hasWindowPosition ? g_config.windowPosY : SDL_WINDOWPOS_CENTERED;
#if defined(__APPLE__) && TARGET_OS_IPHONE
// Without this the window falls back to the 1280x960 default on any device
// with no saved size.
if (const SDL_DisplayID primary = SDL_GetPrimaryDisplay(); primary != 0) {
SDL_Rect bounds{};
if (SDL_GetDisplayBounds(primary, &bounds) && bounds.w > 0 && bounds.h > 0) {
width = bounds.w;
height = bounds.h;
}
posX = SDL_WINDOWPOS_CENTERED_DISPLAY(primary);
posY = SDL_WINDOWPOS_CENTERED_DISPLAY(primary);
}
#endif
const Sint32 posX = g_config.hasWindowPosition ? g_config.windowPosX : SDL_WINDOWPOS_CENTERED;
const Sint32 posY = g_config.hasWindowPosition ? g_config.windowPosY : SDL_WINDOWPOS_CENTERED;
const auto props = SDL_CreateProperties();
TRY(SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, g_config.appName), "Failed to set {}: {}",
+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
output: build/mods/retro_rewind_full_cpp
enable_retro_wfc: true
retro_wfc_payload: http://nas.play.rwfc.net/payload?g=RMCPD00
retro_wfc_payload: https://rwfc.net/api/wfc/payload?g=RMCPD00
retro_wfc_legacy_bootstrap_hook: 0x800ED6E8
riivolution:
xml: xml/RetroRewind6.xml
+30 -41
View File
@@ -7,13 +7,6 @@ endif()
if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_PLATFORM_WINDOWS TRUE)
elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS")
# Checked before macOS: APPLE is true for iOS too, so the macOS branch would
# otherwise claim an iOS build and select a backend that cannot compile
# there (<mach/mach_vm.h> is absent from the iOS SDK) or run there (/tmp is
# outside the sandbox).
set(MKW_PLATFORM_IOS TRUE)
set(MKW_PLATFORM_MACOS TRUE) # shares the Apple arm64 substrate
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.
@@ -22,16 +15,13 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(
set(MKW_PLATFORM_LINUX TRUE)
else()
message(FATAL_ERROR
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux "
"x86_64/aarch64, or Apple Clang on macOS arm64 or iOS arm64")
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS arm64")
endif()
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
message(FATAL_ERROR "WiiCompiled only supports Release builds")
endif()
option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON)
set(MKW_IOS_BUNDLE_ID_PREFIX "it.tobyfox" CACHE STRING
"Reverse-DNS prefix for the iOS bundle identifier")
# Preprocessor definitions that belong to this project's own code (the runtime,
# the translated shards and the product glue) and to nothing else. They are
@@ -40,11 +30,6 @@ set(MKW_IOS_BUNDLE_ID_PREFIX "it.tobyfox" CACHE STRING
# libraries and the products in cmake/PublicProducts.cmake) inherits them while
# aurora-main and its third-party tree stay unaffected.
set(MKW_PROJECT_COMPILE_DEFINITIONS NOMINMAX)
if(MKW_PLATFORM_IOS)
# One project-wide answer to "is this iOS", so sources do not each pull in
# TargetConditionals.h and spell out the __APPLE__ && TARGET_OS_IPHONE dance.
list(APPEND MKW_PROJECT_COMPILE_DEFINITIONS MKW_PLATFORM_IOS=1)
endif()
set(CMAKE_CXX_STANDARD 17)
@@ -151,16 +136,6 @@ else()
if(NOT EXISTS ${MKW_AURORA_DIR}/CMakeLists.txt)
message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing")
endif()
# aurora picks shared third-party libraries whenever BUILD_SHARED_LIBS is
# merely undefined, and its extern/ tree calls unset(BUILD_SHARED_LIBS CACHE)
# while working around xxhash, which deletes any -D the user passed. Defining
# it here, before aurora is added, is what actually sticks across
# reconfigures. Apple builds ship a self-contained bundle, so a dylib
# resolved from the build tree or from Homebrew cannot be allowed to leak
# into the link.
if(APPLE)
set(BUILD_SHARED_LIBS OFF)
endif()
set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE)
if(MKW_PLATFORM_WINDOWS)
set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE)
@@ -274,18 +249,8 @@ if(MKW_PLATFORM_MACOS)
# mkw_co_init/mkw_co_switch at link time.
enable_language(ASM)
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
set_source_files_properties("${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S"
PROPERTIES LANGUAGE ASM SKIP_UNITY_BUILD_INCLUSION ON SKIP_PRECOMPILE_HEADERS ON)
# iOS cannot use the macOS backend: <mach/mach_vm.h> is absent from that SDK
# and /tmp is outside the sandbox.
if(MKW_PLATFORM_IOS)
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
else()
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_ios.cpp")
endif()
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_ios.cpp")
endif()
set(MKW_PLATFORM_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/platform/host_platform.cpp")
set(MKW_BASE_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/base_product.cpp")
@@ -312,6 +277,32 @@ target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
add_executable(mkw_nand_save_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_save_tests.cpp")
target_include_directories(mkw_nand_save_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_save_tests COMMAND mkw_nand_save_tests)
add_executable(mkw_nand_settings_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_settings_tests.cpp")
find_package(Threads REQUIRED)
target_link_libraries(mkw_nand_settings_tests PRIVATE Threads::Threads)
target_include_directories(mkw_nand_settings_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
add_executable(mkw_sc_serial_tests "${CMAKE_CURRENT_LIST_DIR}/tests/sc_serial_tests.cpp")
target_include_directories(mkw_sc_serial_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_sc_serial_tests PRIVATE cxx_std_17)
add_test(NAME mkw_sc_serial_tests COMMAND mkw_sc_serial_tests)
# The input expression engine is self-contained, so it can be exercised without
# linking the runtime or SDL.
add_executable(mkw_input_expr_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/test_expr.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/input_expr.cpp")
target_include_directories(mkw_input_expr_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
# HostContext deliberately keeps the platform-specific context primitive out
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
# refactors cannot silently remove its headers, implementation, or link edge.
@@ -327,11 +318,9 @@ if(MKW_PLATFORM_LINUX)
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
endif()
if(MKW_PLATFORM_MACOS AND NOT MKW_PLATFORM_IOS)
if(MKW_PLATFORM_MACOS)
# Exercise the Apple Silicon context ABI and the public host-memory
# contracts separately from translated products. Host-only: the flat memory
# test pulls in guest_flat_memory_macos.cpp, whose <mach/mach_vm.h> does not
# exist in the iOS SDK, and none of these can run on a device anyway.
# contracts separately from translated products.
enable_language(ASM)
add_executable(mkw_macos_context_abi_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
@@ -401,7 +390,7 @@ else()
target_compile_definitions(mkw_macos_native_compile PRIVATE SDL_MAIN_HANDLED TARGET_PC)
target_link_libraries(mkw_macos_native_compile PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
PNG::PNG mkw::pugixml mkw::toml11 mkw::cryptopp)
mkw::pugixml mkw::toml11 mkw::cryptopp)
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
endif()
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
-8
View File
@@ -1,8 +0,0 @@
GameCube Button Icons and Controls - Zacksly
Licensed under CC BY 3.0 - https://zacksly.itch.io
The PNG files in this directory are taken unmodified from the "Buttons Outline /
White" set of that pack, at 256px. They are drawn tinted and partly transparent
at runtime, which is a rendering choice and not an edit to the artwork.
Full licence: http://creativecommons.org/licenses/by/3.0/
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

+2 -69
View File
@@ -77,17 +77,10 @@ add_library(mkw_runtime_common OBJECT ${SOURCES})
mkw_configure_object_target(mkw_runtime_common)
target_compile_features(mkw_runtime_common PRIVATE cxx_std_20)
target_compile_definitions(mkw_runtime_common PRIVATE
SDL_MAIN_HANDLED
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
if(NOT MKW_PLATFORM_IOS)
# iOS is the one target where SDL must own main(): its entry point is what
# drives UIApplicationMain. Defining this makes <SDL3/SDL_main.h> a no-op,
# so the app links a bare main() that UIKit never calls.
target_compile_definitions(mkw_runtime_common PRIVATE SDL_MAIN_HANDLED)
endif()
target_link_libraries(mkw_runtime_common PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
# The touch overlay decodes its button artwork at startup.
target_link_libraries(mkw_runtime_common PRIVATE PNG::PNG)
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
if(MKW_PLATFORM_WINDOWS)
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
@@ -199,10 +192,7 @@ function(mkw_configure_product target)
"${MKW_RUNTIME_SOURCE_DIR}/.."
"${MKW_RUNTIME_SOURCE_DIR}/../aurora-main/include")
target_compile_definitions(${target} PRIVATE
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION TARGET_PC)
if(NOT MKW_PLATFORM_IOS)
target_compile_definitions(${target} PRIVATE SDL_MAIN_HANDLED)
endif()
SDL_MAIN_HANDLED _DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION TARGET_PC)
target_compile_features(${target} PRIVATE cxx_std_20)
mkw_apply_common_compile_options(${target})
# The dispatch-table and registration shards compile inside the product target itself and
@@ -231,26 +221,6 @@ function(mkw_configure_product target)
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
endif()
if(MKW_PLATFORM_IOS AND NOT CMAKE_HOST_APPLE)
set(shim "${MKW_RUNTIME_SOURCE_DIR}/src/platform/ios/compiler_rt_shim.c")
target_sources(${target} PRIVATE "${shim}")
set_source_files_properties("${shim}" PROPERTIES SKIP_PRECOMPILE_HEADERS ON SKIP_UNITY_BUILD_INCLUSION ON)
endif()
if(MKW_PLATFORM_IOS)
# CMake bundles iOS targets with its own default Info.plist, whose
# CFBundleIdentifier is empty and which carries none of the iOS keys, so
# iOS refuses to install the result. Supply a real one.
set_target_properties(${target} PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_LIST_DIR}/ios/Info.plist.in"
MACOSX_BUNDLE_EXECUTABLE_NAME "${target}"
MACOSX_BUNDLE_BUNDLE_NAME "${target}"
MACOSX_BUNDLE_GUI_IDENTIFIER "${MKW_IOS_BUNDLE_ID_PREFIX}.wiicompiled"
MACOSX_BUNDLE_BUNDLE_VERSION "1"
MACOSX_BUNDLE_SHORT_VERSION_STRING "1.0")
endif()
if(MKW_PLATFORM_WINDOWS)
target_link_libraries(${target} PRIVATE
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
@@ -293,19 +263,6 @@ function(mkw_configure_product target)
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory
"${MKW_WII_BOOTSTRAP_SOURCE_DIR}" "$<TARGET_FILE_DIR:${target}>/wii_bootstrap")
# Touch control artwork. Only the touch build reads these, but they are copied
# everywhere the other assets are so the bundle layout stays uniform.
set(MKW_TOUCH_ASSET_DIR "${MKW_RUNTIME_SOURCE_DIR}/assets/touch")
if(NOT EXISTS "${MKW_TOUCH_ASSET_DIR}/a.png")
message(FATAL_ERROR "Missing touch control artwork: ${MKW_TOUCH_ASSET_DIR}")
endif()
# Cleared first: copy_directory merges, so a removed icon would otherwise
# linger in the bundle and get shipped.
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E rm -rf "$<TARGET_FILE_DIR:${target}>/touch"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${MKW_TOUCH_ASSET_DIR}" "$<TARGET_FILE_DIR:${target}>/touch")
set(MKW_DSP_COEFFICIENT_ROM "${MKW_RUNTIME_SOURCE_DIR}/assets/dsp/dsp_coef.bin")
if(NOT EXISTS "${MKW_DSP_COEFFICIENT_ROM}")
message(FATAL_ERROR "Missing Wii DSP coefficient ROM: ${MKW_DSP_COEFFICIENT_ROM}")
@@ -329,25 +286,6 @@ function(mkw_configure_product target)
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${MKW_INITIAL_PIPELINE_CACHE}"
"$<TARGET_FILE_DIR:${target}>/initial_pipeline_cache.db")
if(MKW_PLATFORM_IOS)
# An .ipa is a zip with the bundle under Payload/. Left unsigned: the
# sideloaders people actually install with (AltStore, SideStore) sign on
# the device with the user's own Apple ID, and a signature applied here
# would only be replaced. WiiCompiled.entitlements beside this file names
# the one entitlement the runtime needs, for whatever does the signing.
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E rm -rf "$<TARGET_FILE_DIR:${target}>/../ipa"
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:${target}>/../ipa/Payload"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"$<TARGET_BUNDLE_DIR:${target}>"
"$<TARGET_FILE_DIR:${target}>/../ipa/Payload/${target}.app"
COMMAND ${CMAKE_COMMAND} -E chdir "$<TARGET_FILE_DIR:${target}>/../ipa"
${CMAKE_COMMAND} -E tar cf "${CMAKE_BINARY_DIR}/${target}-unsigned.ipa" --format=zip Payload
COMMAND ${CMAKE_COMMAND} -E rm -rf "$<TARGET_FILE_DIR:${target}>/../ipa"
COMMENT "Packaging ${target}-unsigned.ipa")
endif()
endfunction()
add_executable(WiiCompiled "${MKW_BASE_PRODUCT_SOURCE}" ${MKW_BASE_REGISTRATION_SOURCES})
@@ -381,11 +319,6 @@ endif()
# tuning rather than leaving target-specific performance on the table.
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3)
elseif(MKW_PLATFORM_IOS)
# iOS 17 runs on the A12 and later, and the build host is never the device;
# -mcpu=native there would tune for whatever Mac (or Linux box) did the
# compile, and upstream clang rejects it when cross-compiling.
set(MKW_BASELINE_ARCH_FLAG -mcpu=apple-a12)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
else()
-71
View File
@@ -1,71 +0,0 @@
# Cross-compile the iOS arm64 products from a Linux host.
#
# Needs upstream clang and lld (Debian 13: clang-19 lld-19 llvm-19) and an
# iPhoneOS SDK, either copied out of Xcode or sparse-cloned from
# github.com/xybp888/iOS-SDKs. Nothing from Theos or xtool. See README.md,
# "iOS from Linux".
#
# cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release \
# -DCMAKE_TOOLCHAIN_FILE=cmake/ios-linux-toolchain.cmake \
# -DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE -DAURORA_DAWN_PROVIDER=package
set(IOS_SDK "/opt/iPhoneOS.sdk" CACHE PATH "iPhoneOS SDK copied from Xcode")
set(MKW_IOS_LLVM_BIN "/usr/lib/llvm-19/bin" CACHE PATH "Directory holding clang, ld64.lld and llvm-ar")
set(MKW_IOS_CLANG_RT "" CACHE FILEPATH
"Optional: Xcode's libclang_rt.ios.a. Left empty, src/platform/ios/compiler_rt_shim.c covers what the runtime needs")
list(APPEND CMAKE_TRY_COMPILE_PLATFORM_VARIABLES IOS_SDK MKW_IOS_LLVM_BIN MKW_IOS_CLANG_RT)
set(CMAKE_SYSTEM_NAME iOS)
set(CMAKE_SYSTEM_PROCESSOR arm64)
set(CMAKE_OSX_ARCHITECTURES arm64 CACHE STRING "")
set(CMAKE_OSX_DEPLOYMENT_TARGET 17.0 CACHE STRING "")
set(CMAKE_OSX_SYSROOT "${IOS_SDK}" CACHE PATH "")
set(CMAKE_SYSROOT "${IOS_SDK}")
set(x "${CMAKE_HOST_EXECUTABLE_SUFFIX}")
set(CMAKE_C_COMPILER "${MKW_IOS_LLVM_BIN}/clang${x}")
set(CMAKE_CXX_COMPILER "${MKW_IOS_LLVM_BIN}/clang++${x}")
set(CMAKE_OBJC_COMPILER "${MKW_IOS_LLVM_BIN}/clang${x}")
set(CMAKE_OBJCXX_COMPILER "${MKW_IOS_LLVM_BIN}/clang++${x}")
set(CMAKE_ASM_COMPILER "${MKW_IOS_LLVM_BIN}/clang${x}")
set(CMAKE_AR "${MKW_IOS_LLVM_BIN}/llvm-ar${x}" CACHE FILEPATH "")
set(CMAKE_RANLIB "${MKW_IOS_LLVM_BIN}/llvm-ranlib${x}" CACHE FILEPATH "")
set(CMAKE_STRIP "${MKW_IOS_LLVM_BIN}/llvm-strip${x}" CACHE FILEPATH "")
# Not every LLVM distribution ships these (llvm-mingw does not); nothing in
# this build needs them, but CMake's Darwin support likes to know where they are.
foreach(tool INSTALL_NAME_TOOL:install-name-tool LIPO:lipo OTOOL:otool)
string(REPLACE ":" ";" tool "${tool}")
list(GET tool 0 var)
list(GET tool 1 exe)
if(EXISTS "${MKW_IOS_LLVM_BIN}/llvm-${exe}${x}")
set(CMAKE_${var} "${MKW_IOS_LLVM_BIN}/llvm-${exe}${x}" CACHE FILEPATH "")
endif()
endforeach()
set(CMAKE_LINKER "${MKW_IOS_LLVM_BIN}/ld64.lld${x}" CACHE FILEPATH "")
foreach(lang C CXX OBJC OBJCXX ASM)
set(CMAKE_${lang}_COMPILER_TARGET arm64-apple-ios17.0)
endforeach()
# Apple's clang searches the SDK's SubFrameworks implicitly and UIKit's own
# headers import from there.
foreach(lang C CXX OBJC OBJCXX)
set(CMAKE_${lang}_FLAGS_INIT "-iframework ${IOS_SDK}/System/Library/SubFrameworks")
endforeach()
# The SDK carries its own libc++ headers and Apple's clang uses those. Upstream
# clang prefers a libc++ shipped beside itself when there is one (llvm-mingw
# does), which pairs the wrong C++ library with Darwin's C headers.
foreach(lang CXX OBJCXX)
string(APPEND CMAKE_${lang}_FLAGS_INIT " -nostdinc++ -isystem ${IOS_SDK}/usr/include/c++/v1")
endforeach()
set(CMAKE_EXE_LINKER_FLAGS_INIT "-fuse-ld=lld ${MKW_IOS_CLANG_RT}")
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-fuse-ld=lld ${MKW_IOS_CLANG_RT}")
set(CMAKE_MODULE_LINKER_FLAGS_INIT "-fuse-ld=lld ${MKW_IOS_CLANG_RT}")
string(STRIP "${CMAKE_EXE_LINKER_FLAGS_INIT}" CMAKE_EXE_LINKER_FLAGS_INIT)
set(CMAKE_FIND_ROOT_PATH "${IOS_SDK}")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
-26
View File
@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key><string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key><string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
<key>CFBundleName</key><string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundleDisplayName</key><string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundleVersion</key><string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
<key>CFBundleShortVersionString</key><string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>LSRequiresIPhoneOS</key><true/>
<key>MinimumOSVersion</key><string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
<key>UIDeviceFamily</key><array><integer>1</integer><integer>2</integer></array>
<key>UILaunchScreen</key><dict/>
<key>UIRequiresFullScreen</key><true/>
<key>CADisableMinimumFrameDuration</key><true/>
<key>UIFileSharingEnabled</key><true/>
<key>LSSupportsOpeningDocumentsInPlace</key><true/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>application-identifier</key><string>CHANGEME.com.example.wiicompiled</string>
<key>com.apple.developer.team-identifier</key><string>CHANGEME</string>
<key>get-task-allow</key><true/>
<key>com.apple.developer.kernel.increased-memory-limit</key><true/>
<key>com.apple.developer.kernel.extended-virtual-addressing</key><true/>
</dict>
</plist>
+20 -89
View File
@@ -1,38 +1,28 @@
#pragma once
#include "runtime_config.h"
#include "nand_path.h"
#include "nand_settings.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <optional>
#include <random>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
namespace RuntimeConsoleIdentity {
struct Identity {
std::string serial;
std::string productCode;
std::string area;
std::string gameRegion;
std::array<uint8_t, 6> mac;
};
inline bool IsValidSerial(const std::string& serial) {
return serial.size() == 9 &&
serial != "000000000" &&
std::all_of(serial.begin(), serial.end(),
[](unsigned char value) { return std::isdigit(value) != 0; });
}
inline Identity FromSerial(std::string serial) {
// Keep Nintendo's Wii OUI. The suffix is derived from the persisted serial
// Keep Nintendo's Wii OUI. The suffix is derived from the NAND serial
// so every API exposes one coherent, stable virtual-console identity.
uint32_t hash = 2166136261u;
for (const unsigned char value : serial) {
@@ -46,6 +36,7 @@ inline Identity FromSerial(std::string serial) {
return {
std::move(serial),
{}, {}, {},
{
0x00,
0x09,
@@ -57,83 +48,23 @@ inline Identity FromSerial(std::string serial) {
};
}
inline std::optional<std::string> ReadSerial(const std::filesystem::path& path) {
std::ifstream input(path);
std::string line;
if (!input || !std::getline(input, line)) {
return std::nullopt;
inline Identity LoadFromNand() {
const auto root = RuntimeNandPath::DiscoverNandRootPath();
const auto settings = RuntimeNandSettings::Read(root);
if (!settings || !RuntimeNandSettings::HasIdentity(*settings)) {
RuntimeNandPath::FailNandRoot(
"NAND setting.txt is missing or has invalid console identity fields (SERNO, CODE, AREA, GAME)",
root / "title/00000001/00000002/data/setting.txt");
}
constexpr std::string_view prefix = "serial=";
if (line.rfind(prefix, 0) != 0) {
return std::nullopt;
}
std::string serial = line.substr(prefix.size());
if (!IsValidSerial(serial)) {
return std::nullopt;
}
return serial;
}
inline bool WriteSerial(const std::filesystem::path& path, const std::string& serial) {
std::error_code ec;
std::filesystem::create_directories(path.parent_path(), ec);
if (ec) {
return false;
}
std::filesystem::path temporary = path;
temporary += ".tmp";
{
std::ofstream output(temporary, std::ios::trunc);
if (!output) {
return false;
}
output << "serial=" << serial << '\n';
output.close();
if (!output) {
return false;
}
}
std::filesystem::rename(temporary, path, ec);
if (!ec) {
return true;
}
std::filesystem::remove(temporary, ec);
return false;
}
inline std::string GenerateSerial() {
std::random_device entropy;
std::seed_seq seed{
entropy(),
entropy(),
entropy(),
entropy(),
};
std::mt19937 generator(seed);
std::uniform_int_distribution<uint32_t> distribution(100000000u, 999999999u);
return std::to_string(distribution(generator));
}
inline Identity LoadOrCreate(const std::filesystem::path& path) {
if (const auto serial = ReadSerial(path)) {
return FromSerial(*serial);
}
const std::string generated = GenerateSerial();
if (WriteSerial(path, generated)) {
return FromSerial(generated);
}
// Remain operational in a read-only environment. This fallback matches
// Dolphin's deterministic serial while keeping the same valid identity shape.
return FromSerial("123456789");
Identity identity = FromSerial(settings->at("SERNO"));
identity.productCode = settings->at("CODE");
identity.area = settings->at("AREA");
identity.gameRegion = settings->at("GAME");
return identity;
}
inline const Identity& Current() {
static const Identity identity =
LoadOrCreate(RuntimeConfigFile::ApplicationDataDirectory() / "ConsoleIdentity.txt");
static const Identity identity = LoadFromNand();
return identity;
}
+170
View File
@@ -0,0 +1,170 @@
#pragma once
// The single vocabulary shared by everything that has to turn a Config.toml
// controller name into a real button: the F10 settings bar, the macro engine,
// and the startup mapping pass. Keeping one table here means a name that the
// settings bar offers is always a name the config parser accepts, and vice
// versa; the two used to drift because each side carried its own copy.
#include <algorithm>
#include <array>
#include <cstdint>
#include <string>
#include <string_view>
#include <SDL3/SDL_gamepad.h>
#include <dolphin/pad.h>
namespace ControllerNames {
// A GameCube button as the game sees it, with the Config.toml key that selects
// it. Order matches RuntimeConfigFile::kControllerButtonKeys.
struct GameCubeButtonItem {
const char* configKey;
const char* label;
PADButton padButton;
};
inline constexpr std::array<GameCubeButtonItem, PAD_BUTTON_COUNT> kGameCubeButtons = {{
{"a", "A", PAD_BUTTON_A},
{"b", "B", PAD_BUTTON_B},
{"x", "X", PAD_BUTTON_X},
{"y", "Y", PAD_BUTTON_Y},
{"start", "Start", PAD_BUTTON_START},
{"z", "Z", PAD_TRIGGER_Z},
{"l", "L", PAD_TRIGGER_L},
{"r", "R", PAD_TRIGGER_R},
{"up", "D-pad Up", PAD_BUTTON_UP},
{"down", "D-pad Down", PAD_BUTTON_DOWN},
{"left", "D-pad Left", PAD_BUTTON_LEFT},
{"right", "D-pad Right", PAD_BUTTON_RIGHT},
}};
// A physical button on the host pad. Names are positional (south/east/...)
// rather than Xbox-labelled so one config reads the same on any hardware.
struct NativeButtonItem {
const char* configName;
const char* label;
uint32_t nativeButton;
};
inline constexpr auto kNativeButtons = std::to_array<NativeButtonItem>({
{"disabled", "Unmapped", PAD_NATIVE_BUTTON_DISABLED},
{"left_trigger", "Left trigger (LT / L2)", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFT_TRIGGER, false)},
{"right_trigger", "Right trigger (RT / R2)", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, false)},
{"left_stick_left", "Left stick left", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTX, true)},
{"left_stick_right", "Left stick right", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTX, false)},
{"left_stick_up", "Left stick up", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTY, true)},
{"left_stick_down", "Left stick down", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTY, false)},
{"right_stick_left", "Right stick left", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTX, true)},
{"right_stick_right", "Right stick right", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTX, false)},
{"right_stick_up", "Right stick up", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTY, true)},
{"right_stick_down", "Right stick down", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTY, false)},
{"unmapped", "Default", PAD_NATIVE_BUTTON_INVALID},
{"south", "South (A / Cross)", SDL_GAMEPAD_BUTTON_SOUTH},
{"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST},
{"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST},
{"north", "North (Y / Triangle)", SDL_GAMEPAD_BUTTON_NORTH},
{"back", "Back / Select / Create", SDL_GAMEPAD_BUTTON_BACK},
{"guide", "Guide / Home / PS", SDL_GAMEPAD_BUTTON_GUIDE},
{"start", "Start / Options", SDL_GAMEPAD_BUTTON_START},
{"left_stick", "Left stick click (L3)", SDL_GAMEPAD_BUTTON_LEFT_STICK},
{"right_stick", "Right stick click (R3)", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
{"left_shoulder", "Left bumper (LB / L1)", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
{"right_shoulder", "Right bumper (RB / R1)", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
{"dpad_up", "D-pad Up", SDL_GAMEPAD_BUTTON_DPAD_UP},
{"dpad_down", "D-pad Down", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
{"dpad_left", "D-pad Left", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
{"dpad_right", "D-pad Right", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
{"misc1", "Misc 1 / Share / Mic", SDL_GAMEPAD_BUTTON_MISC1},
{"right_paddle1", "Right paddle 1", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
{"left_paddle1", "Left paddle 1", SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
{"right_paddle2", "Right paddle 2", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
{"left_paddle2", "Left paddle 2", SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
{"touchpad", "Touchpad click", SDL_GAMEPAD_BUTTON_TOUCHPAD},
{"misc2", "Misc 2", SDL_GAMEPAD_BUTTON_MISC2},
{"misc3", "Misc 3 / GC L click", SDL_GAMEPAD_BUTTON_MISC3},
{"misc4", "Misc 4 / GC R click", SDL_GAMEPAD_BUTTON_MISC4},
{"misc5", "Misc 5", SDL_GAMEPAD_BUTTON_MISC5},
{"misc6", "Misc 6", SDL_GAMEPAD_BUTTON_MISC6},
});
inline std::string TrimToken(std::string_view token) {
const size_t begin = token.find_first_not_of(" \t");
if (begin == std::string_view::npos) {
return {};
}
const size_t end = token.find_last_not_of(" \t");
return std::string(token.substr(begin, end - begin + 1));
}
inline const NativeButtonItem* FindNativeButton(std::string_view configName) {
const std::string name = TrimToken(configName.substr(0, configName.find('@')));
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
[&](const NativeButtonItem& item) { return name == item.configName; });
return it == kNativeButtons.end() ? nullptr : &*it;
}
// Falls back to the "unmapped" entry so callers always have a label to draw.
inline const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) {
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
[&](const NativeButtonItem& item) { return PADAxisButtonIdentity(nativeButton) == PADAxisButtonIdentity(item.nativeButton); });
return it == kNativeButtons.end() ? *FindNativeButton("unmapped") : *it;
}
inline const GameCubeButtonItem* FindGameCubeButton(std::string_view configKey) {
const std::string key = TrimToken(configKey);
const auto it = std::find_if(kGameCubeButtons.begin(), kGameCubeButtons.end(),
[&](const GameCubeButtonItem& item) { return key == item.configKey; });
return it == kGameCubeButtons.end() ? nullptr : &*it;
}
// "up" or "up,a" -> the OR of those GC button bits. Unknown names are skipped so
// a typo costs one button instead of the whole macro.
inline uint16_t GameCubeMaskFromKeys(std::string_view keys) {
uint16_t mask = 0;
size_t begin = 0;
while (begin <= keys.size()) {
const size_t comma = keys.find(',', begin);
const std::string_view token =
keys.substr(begin, comma == std::string_view::npos ? std::string_view::npos : comma - begin);
if (const GameCubeButtonItem* item = FindGameCubeButton(token)) {
mask |= static_cast<uint16_t>(item->padButton);
}
if (comma == std::string_view::npos) {
break;
}
begin = comma + 1;
}
return mask;
}
inline std::string GameCubeKeysFromMask(uint16_t mask) {
std::string keys;
for (const auto& item : kGameCubeButtons) {
if ((mask & static_cast<uint16_t>(item.padButton)) == 0) {
continue;
}
if (!keys.empty()) {
keys += ',';
}
keys += item.configKey;
}
return keys;
}
inline std::string GameCubeLabelsFromMask(uint16_t mask) {
std::string labels;
for (const auto& item : kGameCubeButtons) {
if ((mask & static_cast<uint16_t>(item.padButton)) == 0) {
continue;
}
if (!labels.empty()) {
labels += " + ";
}
labels += item.label;
}
return labels.empty() ? std::string("None") : labels;
}
} // namespace ControllerNames
-14
View File
@@ -15,17 +15,6 @@ namespace GuestFlat {
// of a global.
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
inline constexpr size_t kGuestPageSize = 0x1000;
#ifdef MKW_PLATFORM_IOS
// No fixed base works on every device: probing an iPhone 17 Pro and an iPad
// Pro M5 gave disjoint sets of free 4 GiB windows (448 GiB only, versus 12-48
// GiB), and the extended-virtual-addressing entitlement changes neither. So the
// base is whatever the kernel hands out at Initialize().
//
// Note that memory_access.h routes every flat access to the checked path on
// Apple, so this has no reader yet. It is groundwork, not a hot path.
extern uint8_t* g_flatGuestBase;
#define MKW_FLAT_GUEST_BASE (GuestFlat::g_flatGuestBase)
#else
#if defined(__x86_64__)
// 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap
// placement.
@@ -51,10 +40,7 @@ inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0010'0000'0000ull;
#error "guest_flat_memory.h has no fixed flat guest base chosen for this architecture"
#endif
// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load
// of a global.
#define MKW_FLAT_GUEST_BASE (reinterpret_cast<uint8_t*>(GuestFlat::kFixedFlatGuestBase))
#endif
enum class Backing {
Owned,
+67
View File
@@ -0,0 +1,67 @@
#pragma once
// Per-port expression bindings for the GameCube controls, plus import of a
// Dolphin GCPadNew.ini.
#include <array>
#include <cstdint>
#include <string>
#include <dolphin/pad.h>
namespace InputBindings {
// The controls an expression can drive, in Dolphin's own naming so an
// imported config maps across without translation.
struct ControlInfo {
const char* dolphinName;
const char* label;
uint16_t padButton; // 0 for the analog-only controls below
int analog; // 0 none, 1 trigger L, 2 trigger R
};
inline constexpr std::array<ControlInfo, 14> kControls = {{
{"Buttons/A", "A", PAD_BUTTON_A, 0},
{"Buttons/B", "B", PAD_BUTTON_B, 0},
{"Buttons/X", "X", PAD_BUTTON_X, 0},
{"Buttons/Y", "Y", PAD_BUTTON_Y, 0},
{"Buttons/Z", "Z", PAD_TRIGGER_Z, 0},
{"Buttons/Start", "Start", PAD_BUTTON_START, 0},
{"D-Pad/Up", "D-pad Up", PAD_BUTTON_UP, 0},
{"D-Pad/Down", "D-pad Down", PAD_BUTTON_DOWN, 0},
{"D-Pad/Left", "D-pad Left", PAD_BUTTON_LEFT, 0},
{"D-Pad/Right", "D-pad Right", PAD_BUTTON_RIGHT, 0},
{"Triggers/L", "L", PAD_TRIGGER_L, 1},
{"Triggers/R", "R", PAD_TRIGGER_R, 2},
{"Triggers/L-Analog", "L analog", 0, 1},
{"Triggers/R-Analog", "R analog", 0, 2},
}};
void Reload() noexcept;
// The pad library has PADBlockInput but no matching query, so the settings
// overlay reports its own state here.
void SetInputBlocked(bool blocked) noexcept;
bool InputBlocked() noexcept;
// Mix expression output into a freshly read status set. Call once per guest
// PADRead, after every other input source has been merged.
void Apply(PADStatus* statuses) noexcept;
std::string GetExpression(uint32_t port, size_t control) noexcept;
// Returns false and fills error if the text does not parse; the binding is
// left unchanged in that case.
bool SetExpression(uint32_t port, size_t control, const std::string& text, std::string& error) noexcept;
// True while the control's expression is above the press threshold.
bool IsActive(uint32_t port, size_t control) noexcept;
// The default Dolphin config location on Windows, then next to the executable.
std::string DefaultDolphinConfigPath() noexcept;
// Imports [GCPad<padIndex>] into the given port. Returns the number of controls
// imported, or -1 on failure with error filled.
int ImportDolphinConfig(const std::string& path, int padIndex, uint32_t port,
std::string& summary, std::string& error) noexcept;
} // namespace InputBindings
+53
View File
@@ -0,0 +1,53 @@
#pragma once
// Dolphin-compatible input expressions.
//
// Values are doubles in Dolphin's ControlState style; a control counts as
// pressed above kConditionThreshold. Timing matches Dolphin: wall-clock
// seconds on a steady clock, so an expression copied from GCPadNew.ini
// behaves the same here as it does there.
#include <filesystem>
#include <functional>
#include <memory>
#include <string>
#include <vector>
namespace InputExpr {
inline constexpr double kConditionThreshold = 0.5;
// Resolves a backtick-quoted input name to its current value.
using InputSource = std::function<double(const std::string&)>;
struct Node;
class Expression {
public:
Expression();
~Expression();
Expression(Expression&&) noexcept;
Expression& operator=(Expression&&) noexcept;
// Returns false and fills error on a syntax problem.
static bool Parse(const std::string& text, Expression& out, std::string& error);
bool Empty() const { return m_root == nullptr; }
double Evaluate(const InputSource& source) const;
// Input names the expression references, for diagnostics.
std::vector<std::string> ReferencedInputs() const;
private:
std::unique_ptr<Node> m_root;
};
// Parses a Dolphin GCPadNew.ini and returns the expression text for each
// control of the requested pad, keyed by Dolphin's own control names
// ("Buttons/A", "D-Pad/Up", "Triggers/L", ...). Returns false if the file
// cannot be read or the section is missing.
bool ReadDolphinConfig(const std::filesystem::path& path, int padIndex,
std::vector<std::pair<std::string, std::string>>& controls,
std::string& deviceName, std::string& error);
} // namespace InputExpr
+14 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#include "runtime_config.h"
#include "nand_settings.h"
#include "runtime_log.h"
#include "system_bridge.h"
@@ -163,7 +164,7 @@ inline std::filesystem::path CreateManagedNandRoot() {
return root;
}
inline std::filesystem::path DiscoverNandRootPath() {
inline std::filesystem::path ResolveNandRootPath() {
const std::string configPath = RuntimeConfigFile::NandRoot();
if (!configPath.empty()) {
const auto path = ResolveConfiguredPath(configPath);
@@ -179,4 +180,16 @@ inline std::filesystem::path DiscoverNandRootPath() {
return CreateManagedNandRoot();
}
inline std::filesystem::path DiscoverNandRootPath() {
static const auto root = [] {
const auto resolved = ResolveNandRootPath();
std::string error;
if (!RuntimeNandSettings::Ensure(resolved, error)) {
FailNandRoot(error.c_str(), RuntimeNandSettings::FilePath(resolved));
}
return resolved;
}();
return root;
}
} // namespace RuntimeNandPath
+59
View File
@@ -0,0 +1,59 @@
#pragma once
#include <filesystem>
#include <fstream>
#include <istream>
namespace RuntimeNandSave {
enum class Contents { Missing, Blank, Nonzero, Error };
enum class ReadAction { Proceed, Missing, Error, RecoveryNeeded };
// A failed read is not evidence that a save is blank. Check badbit before EOF:
// an I/O failure may set both, whereas a successful short final read sets EOF.
inline Contents InspectStream(std::istream& input) {
if (!input) return Contents::Error;
char block[4096];
for (;;) {
input.read(block, sizeof(block));
if (input.bad() || (input.fail() && !input.eof())) return Contents::Error;
for (std::streamsize i = 0; i < input.gcount(); ++i) {
if (block[i] != 0) return Contents::Nonzero;
}
if (input.eof()) return Contents::Blank;
}
}
inline Contents InspectFile(const std::filesystem::path& path) {
std::error_code ec;
const auto status = std::filesystem::symlink_status(path, ec);
if (ec && ec != std::errc::no_such_file_or_directory) return Contents::Error;
if (!std::filesystem::exists(status)) return Contents::Missing;
if (!std::filesystem::is_regular_file(path, ec) || ec) return Contents::Error;
std::ifstream input(path, std::ios::binary);
return InspectStream(input);
}
// Probe only read-only opens of the actual save and its exact write shadow.
// No probe writes, removes, or repairs data, and backups are not save aliases.
inline ReadAction CheckRead(const std::filesystem::path& path, int mode) {
const auto name = path.filename();
const bool isMain = name == "rksys.dat";
if (mode != 1 || (!isMain && name != "rksys.dat.nandsafe.tmp")) return ReadAction::Proceed;
const auto contents = InspectFile(path);
if (contents == Contents::Error) return ReadAction::Error;
if (contents == Contents::Nonzero) return ReadAction::Proceed;
if (isMain) {
auto shadow = path;
shadow += ".nandsafe.tmp";
const auto shadowContents = InspectFile(shadow);
if (shadowContents == Contents::Error) return ReadAction::Error;
// The next write normally discards an old shadow. Preserve a possible
// recovery source when there is no usable original, without promoting
// an uncommitted (and potentially incomplete) shadow to the real save.
if (shadowContents == Contents::Nonzero) return ReadAction::RecoveryNeeded;
}
return contents == Contents::Blank ? ReadAction::Missing : ReadAction::Proceed;
}
} // namespace RuntimeNandSave
+220
View File
@@ -0,0 +1,220 @@
#pragma once
#include <array>
#include <atomic>
#include <chrono>
#include <ctime>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <map>
#include <optional>
#include <string>
#include <utility>
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif
namespace RuntimeNandSettings {
using Settings = std::map<std::string, std::string>;
inline std::filesystem::path FilePath(const std::filesystem::path& root) {
return root / "title/00000001/00000002/data/setting.txt";
}
// Wii setting.txt is a 256-byte buffer encrypted with a rotating XOR key.
inline std::optional<Settings> Read(const std::filesystem::path& nandRoot) {
std::ifstream input(FilePath(nandRoot), std::ios::binary);
std::array<uint8_t, 256> bytes{};
if (!input.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) {
return std::nullopt;
}
uint32_t key = 0x73B5DBFAu;
std::string decoded;
for (const uint8_t byte : bytes) {
const char value = static_cast<char>(byte ^ static_cast<uint8_t>(key));
key = (key << 1) | (key >> 31);
if (value == '\0') {
break;
}
if (value != '\r') {
decoded += value;
}
}
Settings settings;
for (size_t start = 0; start < decoded.size();) {
const size_t end = decoded.find('\n', start);
const std::string line = decoded.substr(start, end - start);
const size_t equals = line.find('=');
if (equals != std::string::npos && equals != 0) {
settings.emplace(line.substr(0, equals), line.substr(equals + 1));
}
if (end == std::string::npos) {
break;
}
start = end + 1;
}
return settings;
}
inline bool HasIdentity(const Settings& settings) {
const auto serial = settings.find("SERNO");
if (serial == settings.end() || serial->second.empty() || serial->second.size() > 9 ||
serial->second.find_first_not_of("0123456789") != std::string::npos ||
serial->second.find_first_not_of('0') == std::string::npos) {
return false;
}
for (const auto& field : {std::pair{"CODE", 5u}, {"AREA", 3u}, {"GAME", 2u}}) {
const auto value = settings.find(field.first);
if (value == settings.end() || value->second.empty() ||
value->second.size() > field.second) {
return false;
}
}
return true;
}
// Dolphin's normal (non-deterministic) first-boot algorithm. It is independent
// of the ES device ID. Matching another NAND requires that NAND's saved serial.
inline std::string GenerateSerial(std::time_t now) {
if (now < 0) {
return {};
}
const auto digits = std::to_string(now % 1000000000);
return std::string(9 - digits.size(), '0') + digits;
}
// This recompilation targets the European disc. These are Dolphin's PAL boot
// defaults; an existing setting.txt always takes precedence, in every region.
inline std::optional<std::array<uint8_t, 256>> EncodeNew(const std::string& serial) {
const Settings identity{{"SERNO", serial}, {"CODE", "LEH"}, {"AREA", "EUR"}, {"GAME", "EU"}};
if (!HasIdentity(identity)) {
return std::nullopt;
}
std::array<uint8_t, 256> bytes{};
size_t position = 0;
uint32_t key = 0x73B5DBFAu;
const auto writeByte = [&](char value) {
bytes[position++] = static_cast<uint8_t>(value) ^ static_cast<uint8_t>(key);
key = (key << 1) | (key >> 31);
};
for (const std::string& line : {std::string("AREA=EUR\r\n"), std::string("MODEL=RVL-001(EUR)\r\n"),
std::string("DVD=0\r\n"), std::string("MPCH=0x7FFE\r\n"), std::string("CODE=LEH\r\n"),
"SERNO=" + serial + "\r\n", std::string("VIDEO=PAL\r\n"), std::string("GAME=EU\r\n")}) {
for (;;) {
if (position + line.size() > bytes.size()) {
return std::nullopt;
}
const auto start = position;
const auto savedKey = key;
bool hasNull = false;
for (const char value : line) {
writeByte(value);
hasNull |= bytes[position - 1] == 0;
}
if (!hasNull) {
break;
}
// Nintendo stops at an encoded NUL. Dolphin inserts an extra LF
// before this line and retries with the shifted encryption key.
position = start;
key = savedKey;
writeByte('\n');
}
}
return bytes; // The unused tail stays raw zero, as in Dolphin.
}
// Atomically claim our own scratch directory. A collision belongs to another
// launch (or a previous crashed launch); leave it untouched and try another name.
inline std::optional<std::filesystem::path> CreateScratchDirectory(
const std::filesystem::path& parent, const std::string& token, std::error_code& ec) {
for (unsigned attempt = 0; attempt < 128; ++attempt) {
const auto candidate = parent / (".setting-init-" + token + "-" + std::to_string(attempt));
ec.clear();
if (std::filesystem::create_directory(candidate, ec)) return candidate;
if (ec && ec != std::errc::file_exists) return std::nullopt;
}
ec = std::make_error_code(std::errc::file_exists);
return std::nullopt;
}
// Never replace an existing file, including an unreadable or damaged one.
// Publish a complete file atomically so simultaneous launches use one identity.
inline bool Ensure(const std::filesystem::path& root, std::string& error,
std::time_t now = std::time(nullptr)) {
const auto path = FilePath(root);
std::error_code ec;
const auto status = std::filesystem::symlink_status(path, ec);
if (ec && ec != std::errc::no_such_file_or_directory) {
error = "Cannot inspect NAND setting.txt: " + ec.message();
return false;
}
if (std::filesystem::exists(status)) {
const auto existing = Read(root);
if (existing && HasIdentity(*existing)) {
return true;
}
error = "Existing NAND setting.txt is unreadable or invalid; restore it from this console's backup";
return false;
}
const auto bytes = EncodeNew(GenerateSerial(now));
if (!bytes) {
error = "Cannot initialize NAND settings: invalid system clock";
return false;
}
ec.clear();
std::filesystem::create_directories(path.parent_path(), ec);
if (ec) {
error = "Cannot create NAND settings directory: " + ec.message();
return false;
}
static std::atomic<unsigned> sequence{0};
#ifdef _WIN32
const auto processId = GetCurrentProcessId();
#else
const auto processId = getpid();
#endif
const auto scratch = CreateScratchDirectory(path.parent_path(),
std::to_string(processId) + "-" + std::to_string(
std::chrono::steady_clock::now().time_since_epoch().count()) + "-" +
std::to_string(sequence++), ec);
if (!scratch) {
error = "Cannot create temporary NAND settings directory: " + ec.message();
return false;
}
const auto temporary = *scratch / "setting.txt";
bool written = false;
{
std::ofstream output(temporary, std::ios::binary);
output.write(reinterpret_cast<const char*>(bytes->data()), bytes->size());
output.close();
written = static_cast<bool>(output);
}
bool published = false;
if (written) {
#ifdef _WIN32
published = MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_WRITE_THROUGH) != 0;
#else
published = ::link(temporary.c_str(), path.c_str()) == 0;
#endif
}
std::filesystem::remove(temporary, ec);
std::filesystem::remove(*scratch, ec);
// A competing launcher may have published its settings first. Always read
// the winner from NAND rather than using our unpersisted candidate serial.
const auto persisted = Read(root);
if (persisted && HasIdentity(*persisted)) {
return true;
}
error = published ? "Cannot read newly initialized NAND setting.txt" :
"Cannot persist NAND setting.txt; check NAND directory permissions";
return false;
}
} // namespace RuntimeNandSettings
+33
View File
@@ -11,6 +11,7 @@
#include <iomanip>
#include <iostream>
#include <limits>
#include <map>
#include <optional>
#include <sstream>
#include <string>
@@ -89,6 +90,8 @@ struct RuntimeUserConfig {
// comma-separated SDL-style physical button names ("south", or
// "dpad_up,left_shoulder") as values; pressing either bound button counts.
std::array<std::optional<std::string>, 12> controllerButtons;
std::optional<bool> rumbleEnabled;
std::map<std::string, std::string> controllerExpressions;
};
namespace RuntimeConfigFile {
@@ -407,6 +410,17 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
FindConfigValue<std::string>(document, "controller", buttonKeys[index]);
}
config.rumbleEnabled = FindConfigValue<bool>(document, "controller", "rumble");
if (const auto* section = document.contains("controller") ? &document.at("controller") : nullptr;
section != nullptr && section->is_table()) {
for (const auto& [key, value] : section->as_table()) {
if (key.rfind("expr_", 0) == 0 && value.is_string()) {
config.controllerExpressions[key] = value.as_string();
}
}
}
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
config.windowPosX = FindConfigInt(document, "video", "window_x");
config.windowPosY = FindConfigInt(document, "video", "window_y");
@@ -677,6 +691,25 @@ inline bool SetControllerButton(size_t index, std::string value) {
return WriteSetting("controller", kControllerButtonKeys[index], FormatString(value));
}
inline std::string ControllerExpression(const std::string& key) {
const auto it = Get().controllerExpressions.find(key);
return it == Get().controllerExpressions.end() ? std::string() : it->second;
}
inline bool SetControllerExpression(const std::string& key, const std::string& value) {
Mutable().controllerExpressions[key] = value;
return WriteSetting("controller", key, FormatString(value));
}
inline bool RumbleEnabled(bool fallback = true) {
return Get().rumbleEnabled.value_or(fallback);
}
inline bool SetRumbleEnabled(bool value) {
Mutable().rumbleEnabled = value;
return WriteSetting("controller", "rumble", value ? "true" : "false");
}
inline bool SetAudioVolume(float value) {
value = std::clamp(value, 0.0f, 1.0f);
Mutable().audioVolume = value;
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <charconv>
#include <cstddef>
#include <cstdint>
#include <string_view>
#include <system_error>
namespace RuntimeScSerial {
// SCGetProductSN's output is a u32, not a character buffer. DWC loads
// that word and formats it with the product code to construct csnum.
template <typename RangeValidator, typename WordWriter>
uint32_t Write(std::string_view serial, uint32_t address,
RangeValidator&& contains, WordWriter&& write32) {
if (serial.empty() || serial.size() > 9 ||
serial.find_first_not_of("0123456789") != std::string_view::npos) return 0;
uint32_t number = 0;
const auto parsed = std::from_chars(serial.data(), serial.data() + serial.size(), number);
if (parsed.ec != std::errc{} || parsed.ptr != serial.data() + serial.size() ||
!address || !contains(address, sizeof(uint32_t))) return 0;
write32(address, number);
return 1;
}
} // namespace RuntimeScSerial
-3
View File
@@ -9,9 +9,6 @@ void InitializeRuntimeSettings() noexcept;
// Draw the F10 settings bar before each Aurora present.
void HandleEvents(const AuroraEvent* events) noexcept;
void Draw() noexcept;
void ToggleTopBar() noexcept;
bool TopBarVisible() noexcept;
bool FpsOverlayBounds(float* minX, float* minY, float* maxX, float* maxY) noexcept;
bool StartupScreenVisible() noexcept;
void NotifyStrapInputAccepted() noexcept;
void AdvancePresentedFrame() noexcept;
-18
View File
@@ -1,18 +0,0 @@
#pragma once
#include <filesystem>
#include <string>
#include <imgui.h>
// Icons are CC BY 3.0 by Zacksly - see runtime/assets/touch/ATTRIBUTION.txt.
namespace TouchArt {
// By file stem, e.g. "a" or "d-pad". Null when the file is missing; callers
// fall back to drawing the shape.
ImTextureID Get(const char* name);
// Call once, after aurora is up.
void Preload();
} // namespace TouchArt
-27
View File
@@ -1,27 +0,0 @@
#pragma once
#include <array>
#include <cstdint>
struct PADStatus;
// On-screen touch controls. Read() fills a status array the caller merges over
// aurora's PADRead result.
//
// Polls SDL's finger list directly rather than using ImGui widgets: steering,
// accelerating and firing happen at once, and ImGui's SDL backend collapses
// touch to a single emulated mouse.
namespace TouchPad {
// True when the host has a touch screen and no physical pad is driving port 0.
bool IsActive();
// Port 0 only. Returns false when touch is not driving input, leaving the
// caller's existing statuses untouched.
bool Read(std::array<PADStatus, 4>& statuses);
// Draws the control overlay. Must be called inside a live ImGui frame.
void Draw();
} // namespace TouchPad
-17
View File
@@ -1,20 +1,5 @@
#include "discord_presence.h"
#ifdef MKW_PLATFORM_IOS
// iOS has no Discord client for the IPC socket to reach, so the whole thing is
// stubbed out rather than retrying a connection that cannot succeed.
namespace DiscordPresence {
void Initialize(const std::string&, const std::string&) {}
void SetClient(const std::string&) {}
void SetActivity(Activity) {}
void Reset() {}
void Shutdown() {}
} // namespace DiscordPresence
#else
#include "runtime_log.h"
#include <algorithm>
@@ -479,5 +464,3 @@ void Shutdown() {
}
} // namespace DiscordPresence
#endif
+6 -2
View File
@@ -256,8 +256,12 @@ bool GuestFiberManager::CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entr
gf.cpuContext.srr0 = entryPoint;
// The host stack models only translated host calls; the guest stack starts
// at stackBase in the CPU context above.
constexpr size_t kHostStackSize = 64 * 1024;
// at stackBase in the CPU context above. 64 KiB is too small for deep
// translated/HLE call chains (notably NW4R's sound worker), and on macOS
// it can exhaust the guarded coroutine stack as unrelated host work (such
// as a window resize) adds a little more nesting. Keep enough headroom for
// those chains while the guest stack remains separately bounded.
constexpr size_t kHostStackSize = 1024 * 1024;
gf.fiber = HostContext::Create(kHostStackSize, FiberProc,
reinterpret_cast<void*>(static_cast<uintptr_t>(guestThreadAddr)));
-154
View File
@@ -1,154 +0,0 @@
#include "guest_flat_memory.h"
// iOS variant of the macOS backend. <mach/mach_vm.h> is not in the iOS SDK, so
// the vm_* calls from <mach/mach.h> are used instead, and the backing store is
// an anonymous mapping rather than a file in /tmp, which the sandbox denies.
#include <mach/mach.h>
#include <mach/mach_error.h>
#include <mach/vm_map.h>
#include <sys/mman.h>
#include <unistd.h>
#include <algorithm>
#include <cerrno>
#include <cstring>
#include <string>
#include <mutex>
#include <stdexcept>
#include <vector>
namespace GuestFlat {
uint8_t* g_flatGuestBase = nullptr;
bool g_requiresCheckedAccess = false;
namespace {
struct Mapping { uint32_t base; uint64_t size; uint8_t* host; };
std::mutex g_mutex;
std::vector<Mapping> g_mappings;
std::vector<RegionRequest> g_layout;
uint8_t* g_base = nullptr;
bool g_active = false;
uint64_t Offset(const RegionRequest& r) {
if (r.backing == Backing::Mem1) return r.base & 0x1fffffffu;
if (r.backing == Backing::Mem2) return (r.base & 0x1fffffffu) - 0x10000000u;
return 0;
}
bool Same(const std::vector<RegionRequest>& a, const std::vector<RegionRequest>& b) {
return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(),
[](const auto& x, const auto& y) { return x.base == y.base && x.size == y.size && x.backing == y.backing; });
}
} // namespace
bool IsActive() { return g_active; }
void Initialize(const std::vector<RegionRequest>& regions) {
std::lock_guard lock(g_mutex);
g_requiresCheckedAccess = static_cast<size_t>(getpagesize()) > kGuestPageSize;
if (g_active) {
if (!Same(g_layout, regions)) throw std::runtime_error("flat guest layout cannot be remapped");
return;
}
// The free window differs per device, so let the kernel place it.
vm_address_t address = 0;
if (vm_allocate(mach_task_self(), &address, static_cast<vm_size_t>(kGuestSpaceSize),
VM_FLAGS_ANYWHERE) != KERN_SUCCESS) {
throw std::runtime_error("unable to reserve a 4 GiB iOS guest address space; a device with\n"
"less than 4 GB of RAM may need the extended-virtual-addressing entitlement");
}
g_base = reinterpret_cast<uint8_t*>(address);
struct Store { Backing kind; uint32_t owned; uint64_t size; uint8_t* mem; };
std::vector<Store> stores;
// Initialize() is called inside a try that reports a dialog rather than
// aborting, so a throw here has to give the reservation back.
struct Rollback {
vm_address_t reservation;
std::vector<Store>* stores;
bool armed = true;
~Rollback() {
if (!armed) return;
if (stores != nullptr) {
for (const auto& s : *stores) {
if (s.mem != nullptr) munmap(s.mem, static_cast<size_t>(s.size));
}
}
if (reservation != 0) {
vm_deallocate(mach_task_self(), reservation,
static_cast<vm_size_t>(kGuestSpaceSize));
}
}
} rollback{address, &stores};
for (const auto& r : regions) {
if (!r.size) continue;
const uint32_t owned = r.backing == Backing::Owned ? r.base : 0;
auto it = std::find_if(stores.begin(), stores.end(),
[&](const Store& s) { return s.kind == r.backing && s.owned == owned; });
const uint64_t need = Offset(r) + r.size;
if (it == stores.end()) stores.push_back({r.backing, owned, need, nullptr});
else it->size = std::max(it->size, need);
}
for (auto& s : stores) {
void* mem = mmap(nullptr, static_cast<size_t>(s.size), PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANON, -1, 0);
if (mem == MAP_FAILED) throw std::runtime_error("unable to create iOS guest backing store");
s.mem = static_cast<uint8_t*>(mem);
}
for (const auto& r : regions) {
if (!r.size) continue;
const uint32_t owned = r.backing == Backing::Owned ? r.base : 0;
const auto& s = *std::find_if(stores.begin(), stores.end(),
[&](const Store& x) { return x.kind == r.backing && x.owned == owned; });
uint8_t* host = s.mem + Offset(r);
// Alias the same physical pages into the reservation. VM_FLAGS_OVERWRITE
// replaces the placeholder in place; copy=false is what makes this a
// shared alias rather than a copy-on-write duplicate.
vm_address_t target = reinterpret_cast<vm_address_t>(g_base + r.base);
vm_prot_t cur = VM_PROT_NONE, max = VM_PROT_NONE;
const kern_return_t kr = vm_remap(
mach_task_self(), &target, static_cast<vm_size_t>(r.size), 0,
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE, mach_task_self(),
reinterpret_cast<vm_address_t>(host), FALSE, &cur, &max, VM_INHERIT_SHARE);
if (kr != KERN_SUCCESS) {
throw std::runtime_error(std::string("vm_remap failed for the iOS guest alias: ") +
mach_error_string(kr));
}
if (target != reinterpret_cast<vm_address_t>(g_base + r.base)) {
throw std::runtime_error("vm_remap placed the iOS guest alias at the wrong address");
}
if (mprotect(g_base + r.base, static_cast<size_t>(r.size), PROT_READ | PROT_WRITE) != 0) {
throw std::runtime_error(std::string("mprotect failed on the iOS guest alias: ") +
std::strerror(errno));
}
g_mappings.push_back({r.base, r.size, host});
}
g_layout = regions;
// Publish the base only now. Until the aliases are mapped the reservation is
// an unbacked placeholder, and the header advertises this pointer as the base
// for every translated access - so a caller that trusted it early would fault.
rollback.armed = false;
g_flatGuestBase = g_base;
g_active = true;
}
uint8_t* HostPointer(uint32_t a) {
for (const auto& m : g_mappings)
if (a >= m.base && uint64_t(a - m.base) < m.size) return m.host + (a - m.base);
return nullptr;
}
void ProtectDeferredRange(uint32_t, size_t) {}
void UnprotectDeferredRange(uint32_t, size_t) {}
void RegisterExecutableRange(uint32_t, uint32_t) {}
FaultCounters Counters() { return {}; }
void LogFaultSummary() noexcept {}
bool HandleAccessViolation(void*, bool) noexcept { return false; }
} // namespace GuestFlat
+67 -12
View File
@@ -1,21 +1,75 @@
#include "hle_stubs.h"
#include "touch_pad.h"
#include <array>
#include "memory.h"
#include "hle/controller_status_contract.h"
#include "input_bindings.h"
#include "wii_remote_input.h"
#include <algorithm>
#include <atomic>
#include <cstdio>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <SDL3/SDL_gamepad.h>
#include <dolphin/pad.h>
namespace {
std::atomic<bool> g_rumbleEnabled{true};
bool NativeButtonHeld(SDL_Gamepad* gamepad, uint32_t nativeButton) {
if (gamepad == nullptr || nativeButton == PAD_NATIVE_BUTTON_INVALID ||
nativeButton >= SDL_GAMEPAD_BUTTON_COUNT) {
return false;
}
return SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(nativeButton));
}
// A digital button bound to L or R has no analog travel of its own. On real
// hardware the click only engages at full depression, so report a full pull.
void FillTriggersHeldByButtons(PADStatus* statuses) {
if (InputBindings::InputBlocked()) {
return;
}
for (uint32_t port = 0; port < PAD_CHANMAX; ++port) {
if (statuses[port].err != PAD_ERR_NONE) {
continue;
}
const s32 index = PADGetIndexForPort(port);
if (index < 0) {
continue;
}
SDL_Gamepad* gamepad = PADGetSDLGamepadForIndex(static_cast<u32>(index));
if (gamepad == nullptr) {
continue;
}
const auto scan = [&](PADButtonMapping* mappings, u32 count) {
if (mappings == nullptr) {
return;
}
for (u32 i = 0; i < count; ++i) {
const PADButtonMapping& mapping = mappings[i];
if (mapping.padButton != PAD_TRIGGER_L && mapping.padButton != PAD_TRIGGER_R) {
continue;
}
if (!NativeButtonHeld(gamepad, mapping.nativeButton)) {
continue;
}
if (mapping.padButton == PAD_TRIGGER_L) {
statuses[port].triggerLeft = 255;
} else {
statuses[port].triggerRight = 255;
}
}
};
u32 count = 0;
scan(PADGetButtonMappings(port, &count), count);
count = 0;
scan(PADGetAltButtonMappings(port, &count), count);
}
}
void WritePadStatus(uint32_t base, const PADStatus& status) {
const auto guestStatus = PadStatusContract::Encode({
status.button,
@@ -35,6 +89,11 @@ void WritePadStatus(uint32_t base, const PADStatus& status) {
} // namespace
extern "C" void PAD_HLE_SetRumbleEnabled(bool enabled)
{
g_rumbleEnabled.store(enabled, std::memory_order_relaxed);
}
extern "C" uint32_t PAD__Init_HLE()
{
return PADInit() ? 1u : 0u;
@@ -57,15 +116,8 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
// between "connected" and "no controller" every time the overlay toggles.
WiiRemoteInput::HideRemotesFromPad(statuses, PAD_CHANMAX);
// On-screen controls drive port 0 unless a physical pad is connected.
// TouchPad::Read makes that call itself; the err field is no proxy for it,
// since keyboard bindings report PAD_ERR_NONE with no controller attached.
std::array<PADStatus, PAD_CHANMAX> touchStatuses{};
#ifdef MKW_PLATFORM_IOS
if (!PADIsInputBlocked() && TouchPad::Read(touchStatuses)) {
statuses[0] = touchStatuses[0];
}
#endif
FillTriggersHeldByButtons(statuses);
InputBindings::Apply(statuses);
try {
for (uint32_t i = 0; i < PAD_CHANMAX; ++i) {
@@ -94,6 +146,9 @@ PPC_NATIVE_OVERRIDE(801AF1E4, PAD__Recalibrate_HLE, uint32_t, (uint32_t mask), (
extern "C" void PAD__ControlMotor_HLE(int32_t chan, uint32_t command)
{
if (command == PAD_MOTOR_RUMBLE && !g_rumbleEnabled.load(std::memory_order_relaxed)) {
command = PAD_MOTOR_STOP;
}
PADControlMotor(chan, command);
}
PPC_NATIVE_OVERRIDE_VOID(801AF908, PAD__ControlMotor_HLE, (int32_t chan, uint32_t command), (chan, command));
-108
View File
@@ -1,108 +0,0 @@
#include "touch_art.h"
#include <aurora/imgui.h>
#include <png.h>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <unordered_map>
#include <vector>
#include "runtime_config.h"
// Icons are CC BY 3.0 by Zacksly; see runtime/assets/touch/ATTRIBUTION.txt.
// Textures are owned by aurora once handed over and live for the process.
namespace TouchArt {
namespace {
std::unordered_map<std::string, ImTextureID> g_cache;
// libpng's error path longjmps, so everything it can touch must be released
// through this struct rather than by falling off the end of the function.
struct PngReader {
std::FILE* file = nullptr;
png_structp png = nullptr;
png_infop info = nullptr;
std::vector<png_bytep> rows;
~PngReader() {
if (png != nullptr) png_destroy_read_struct(&png, info != nullptr ? &info : nullptr, nullptr);
if (file != nullptr) std::fclose(file);
}
};
bool DecodeRgba8(const std::filesystem::path& path, std::vector<uint8_t>& out, uint32_t& width,
uint32_t& height) {
PngReader r;
r.file = std::fopen(path.string().c_str(), "rb");
if (r.file == nullptr) {
return false;
}
r.png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (r.png == nullptr) return false;
r.info = png_create_info_struct(r.png);
if (r.info == nullptr) return false;
if (setjmp(png_jmpbuf(r.png))) return false;
png_init_io(r.png, r.file);
png_read_info(r.png, r.info);
png_set_expand(r.png);
png_set_strip_16(r.png);
png_set_gray_to_rgb(r.png);
png_set_filler(r.png, 0xFF, PNG_FILLER_AFTER);
png_read_update_info(r.png, r.info);
width = png_get_image_width(r.png, r.info);
height = png_get_image_height(r.png, r.info);
if (width == 0 || height == 0) return false;
out.assign(static_cast<size_t>(width) * height * 4u, 0);
r.rows.resize(height);
for (uint32_t y = 0; y < height; ++y) {
r.rows[y] = out.data() + static_cast<size_t>(y) * width * 4u;
}
png_read_image(r.png, r.rows.data());
return true;
}
} // namespace
ImTextureID Get(const char* name) {
if (name == nullptr || *name == '\0') {
return ImTextureID{};
}
if (const auto it = g_cache.find(name); it != g_cache.end()) {
return it->second;
}
ImTextureID id{};
if (const auto dir = RuntimeConfigFile::ExecutableDirectory()) {
const std::filesystem::path path = *dir / "touch" / (std::string(name) + ".png");
std::vector<uint8_t> rgba;
uint32_t w = 0;
uint32_t h = 0;
if (DecodeRgba8(path, rgba, w, h)) {
id = aurora_imgui_add_texture(w, h, rgba.data());
} else {
// Cached null so this reports once rather than every frame.
std::cout << "[touch] missing button art: " << path << std::endl;
}
}
g_cache.emplace(name, id);
return id;
}
void Preload() {
// Must match the names used in touch_pad.cpp; a miss costs a hitch, not a
// failure.
static constexpr const char* kNames[] = {
"a", "b", "right_bumper", "l_analog", "r_analog", "start_pause", "d-pad", "control_stick",
};
for (const char* name : kNames) {
Get(name);
Get((std::string(name) + "_fill").c_str());
}
}
} // namespace TouchArt
-408
View File
@@ -1,408 +0,0 @@
#include "touch_pad.h"
#include "touch_art.h"
#include "hle/controller_status_contract.h"
#include "settings_overlay.h"
#include <dolphin/pad.h>
#include <imgui.h>
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_touch.h>
#include <algorithm>
#include <atomic>
#include <cmath>
#include <string>
namespace TouchPad {
namespace {
struct Circle {
float x, y, r;
bool Contains(float px, float py, float aspect) const {
const float dx = (px - x) * aspect;
const float dy = py - y;
return (dx * dx + dy * dy) <= (r * r);
}
};
// Positions are held as a distance from one screen edge in units of screen
// height. A fraction of width would land somewhere different on a 1.45:1 iPad
// and a 2.17:1 iPhone.
struct Anchor {
enum Edge { Left, Right, Centre };
Edge edge;
float dx; // distance from that edge, in units of screen height
float y; // fraction of screen height
float r; // radius, in units of screen height
Circle Resolve(float aspect) const {
const float offset = dx / aspect;
switch (edge) {
case Left: return Circle{offset, y, r};
case Right: return Circle{1.0f - offset, y, r};
case Centre: return Circle{0.5f + offset, y, r};
}
return Circle{0.5f, y, r};
}
};
constexpr Anchor kStickA{Anchor::Left, 0.215f, 0.400f, 0.130f};
constexpr Anchor kShoulderLA{Anchor::Left, 0.130f, 0.090f, 0.070f};
constexpr Anchor kShoulderRA{Anchor::Right, 0.130f, 0.090f, 0.070f};
constexpr Anchor kButtonAA{Anchor::Right, 0.210f, 0.640f, 0.105f};
constexpr Anchor kButtonBA{Anchor::Right, 0.430f, 0.790f, 0.072f};
constexpr Anchor kButtonItemA{Anchor::Right, 0.165f, 0.310f, 0.070f};
constexpr Anchor kButtonStartA{Anchor::Centre, 0.100f, 0.070f, 0.042f};
// There is no F10 on a touch device, so this is the only way into the settings
// bar. It is drawn whenever a touch screen exists - including when a pad has
// hidden the game controls - or connecting a pad would lock the user out.
constexpr Anchor kButtonMenuA{Anchor::Centre, -0.100f, 0.070f, 0.040f};
// r is the half-extent of the whole cross, not of one arm.
constexpr Anchor kDpadA{Anchor::Left, 0.215f, 0.760f, 0.125f};
constexpr float kDpadDeadzone = 0.30f;
struct Layout {
Circle stick, shoulderL, shoulderR, a, b, item, start, menu;
Circle dpad; // the whole cross; direction is derived, not four zones
};
Layout LayoutFor(float aspect) {
Layout l{};
l.stick = kStickA.Resolve(aspect);
l.shoulderL = kShoulderLA.Resolve(aspect);
l.shoulderR = kShoulderRA.Resolve(aspect);
l.a = kButtonAA.Resolve(aspect);
l.b = kButtonBA.Resolve(aspect);
l.item = kButtonItemA.Resolve(aspect);
l.start = kButtonStartA.Resolve(aspect);
l.menu = kButtonMenuA.Resolve(aspect);
l.dpad = kDpadA.Resolve(aspect);
return l;
}
struct Frame {
bool stickHeld = false;
float stickDx = 0.0f; // -1..1 after deadzone and clamping
float stickDy = 0.0f;
bool a = false, b = false, item = false, start = false;
bool l = false, r = false;
bool up = false, down = false, left = false, right = false;
};
// A finger inside the stick circle steers; its offset from the circle centre is
// the analog deflection. Every other finger is hit-tested against the buttons,
// so any number of them can be held at once.
// Owning finger, or 0. Single-threaded: guest fibers and the event pump share
// the host main thread.
SDL_FingerID g_stickFinger = 0;
Frame SampleFingers(float aspect) {
const Layout L = LayoutFor(aspect);
Frame frame;
bool stickFingerSeen = false;
int deviceCount = 0;
SDL_TouchID* devices = SDL_GetTouchDevices(&deviceCount);
if (devices == nullptr) {
return frame;
}
for (int d = 0; d < deviceCount; ++d) {
int fingerCount = 0;
SDL_Finger** fingers = SDL_GetTouchFingers(devices[d], &fingerCount);
if (fingers == nullptr) {
continue;
}
for (int f = 0; f < fingerCount; ++f) {
const float px = fingers[f]->x;
const float py = fingers[f]->y;
// Holding the grab lets the thumb slide past the gate without
// dropping steering.
const bool ownsStick = (g_stickFinger != 0 && fingers[f]->id == g_stickFinger);
if (ownsStick || (!frame.stickHeld && g_stickFinger == 0 &&
L.stick.Contains(px, py, aspect))) {
g_stickFinger = fingers[f]->id;
stickFingerSeen = true;
frame.stickHeld = true;
const float dx = (px - L.stick.x) * aspect / L.stick.r;
const float dy = (py - L.stick.y) / L.stick.r;
const float len = std::sqrt(dx * dx + dy * dy);
// Clamp to the circle so a finger dragged outside still reads as
// full deflection rather than overshooting the guest's range.
const float scale = (len > 1.0f) ? (1.0f / len) : 1.0f;
frame.stickDx = dx * scale;
frame.stickDy = dy * scale;
continue;
}
if (L.a.Contains(px, py, aspect)) frame.a = true;
if (L.b.Contains(px, py, aspect)) frame.b = true;
if (L.item.Contains(px, py, aspect)) frame.item = true;
if (L.start.Contains(px, py, aspect)) frame.start = true;
if (L.shoulderL.Contains(px, py, aspect)) frame.l = true;
if (L.shoulderR.Contains(px, py, aspect)) frame.r = true;
{
const float ddx = (px - L.dpad.x) * aspect / L.dpad.r;
const float ddy = (py - L.dpad.y) / L.dpad.r;
if (std::fabs(ddx) <= 1.0f && std::fabs(ddy) <= 1.0f &&
(ddx * ddx + ddy * ddy) > (kDpadDeadzone * kDpadDeadzone)) {
if (std::fabs(ddx) > std::fabs(ddy)) {
if (ddx < 0.0f) frame.left = true; else frame.right = true;
} else {
if (ddy < 0.0f) frame.up = true; else frame.down = true;
}
}
}
}
SDL_free(fingers);
}
SDL_free(devices);
if (!stickFingerSeen) {
g_stickFinger = 0;
}
return frame;
}
float CurrentAspect() {
const ImGuiIO& io = ImGui::GetIO();
if (io.DisplaySize.y <= 0.0f) {
return 1.0f;
}
return io.DisplaySize.x / io.DisplaySize.y;
}
// GameCube's own button colours. White-on-white is unreadable over bright track
// surfaces, so every control also gets a dark halo and a dark label shadow,
// which keeps it legible on light and dark backgrounds alike.
constexpr ImU32 kColA = IM_COL32(67, 176, 42, 255); // green
constexpr ImU32 kColB = IM_COL32(228, 0, 43, 255); // red
constexpr ImU32 kColZ = IM_COL32(104, 82, 214, 255); // purple
constexpr ImU32 kColGrey = IM_COL32(196, 198, 206, 255);
constexpr ImU32 kColStick = IM_COL32(174, 176, 184, 255);
ImU32 WithAlpha(ImU32 colour, int alpha) {
return (colour & 0x00FFFFFFu) | (static_cast<ImU32>(alpha) << IM_COL32_A_SHIFT);
}
void DrawCircle(ImDrawList* list, const Circle& c, const ImVec2& size, const char* label,
bool pressed, ImU32 colour, const char* art) {
const ImVec2 centre{c.x * size.x, c.y * size.y};
const float radius = c.r * size.y;
const ImVec2 lo{centre.x - radius, centre.y - radius};
const ImVec2 hi{centre.x + radius, centre.y + radius};
// The solid silhouette tinted black darkens the glyph's own shape; a plain
// circle behind it would put a disc around the cross and the triggers.
if (art != nullptr) {
const ImTextureID fill = TouchArt::Get((std::string(art) + "_fill").c_str());
if (fill != ImTextureID{}) {
list->AddImage(fill, lo, hi, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f),
IM_COL32(0, 0, 0, pressed ? 165 : 120));
}
const ImTextureID line = TouchArt::Get(art);
if (line != ImTextureID{}) {
list->AddImage(line, lo, hi, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f),
IM_COL32(255, 255, 255, pressed ? 250 : 175));
return;
}
}
// Vector fallback, used when the artwork is missing.
list->AddCircleFilled(centre, radius + 3.0f, IM_COL32(0, 0, 0, pressed ? 150 : 110), 48);
list->AddCircleFilled(centre, radius, WithAlpha(colour, pressed ? 235 : 130), 48);
list->AddCircle(centre, radius, WithAlpha(IM_COL32(255, 255, 255, 255), pressed ? 255 : 170),
48, 2.5f);
if (label != nullptr && *label != '\0') {
const ImVec2 text = ImGui::CalcTextSize(label);
const ImVec2 at{centre.x - text.x * 0.5f, centre.y - text.y * 0.5f};
list->AddText(ImVec2{at.x + 1.0f, at.y + 1.0f}, IM_COL32(0, 0, 0, 190), label);
list->AddText(at, IM_COL32(255, 255, 255, 240), label);
}
}
} // namespace
bool HasTouchScreen() {
int deviceCount = 0;
SDL_TouchID* devices = SDL_GetTouchDevices(&deviceCount);
if (devices != nullptr) {
SDL_free(devices);
}
return deviceCount > 0;
}
// Edge-triggered: the settings bar must toggle once per tap, not once per frame
// for as long as a finger rests on the button.
void PollMenuButton() {
static bool wasDown = false;
bool down = false;
const float aspect = CurrentAspect();
const Layout L = LayoutFor(aspect);
int deviceCount = 0;
SDL_TouchID* devices = SDL_GetTouchDevices(&deviceCount);
if (devices != nullptr) {
for (int d = 0; d < deviceCount && !down; ++d) {
int fingerCount = 0;
SDL_Finger** fingers = SDL_GetTouchFingers(devices[d], &fingerCount);
if (fingers == nullptr) {
continue;
}
for (int f = 0; f < fingerCount; ++f) {
if (L.menu.Contains(fingers[f]->x, fingers[f]->y, aspect)) {
down = true;
break;
}
}
SDL_free(fingers);
}
SDL_free(devices);
}
if (down && !wasDown) {
settings_overlay::ToggleTopBar();
}
wasDown = down;
}
// Tapping the FPS readout opens the settings bar. Preferred over a dedicated
// button because it reuses something already on screen.
bool PollFpsTap(const ImVec2& size) {
float minX = 0.0f, minY = 0.0f, maxX = 0.0f, maxY = 0.0f;
if (!settings_overlay::FpsOverlayBounds(&minX, &minY, &maxX, &maxY)) {
return false; // readout hidden - caller falls back to its own button
}
static bool wasDown = false;
bool down = false;
int deviceCount = 0;
SDL_TouchID* devices = SDL_GetTouchDevices(&deviceCount);
if (devices != nullptr) {
for (int d = 0; d < deviceCount && !down; ++d) {
int fingerCount = 0;
SDL_Finger** fingers = SDL_GetTouchFingers(devices[d], &fingerCount);
if (fingers == nullptr) {
continue;
}
for (int f = 0; f < fingerCount; ++f) {
const float px = fingers[f]->x * size.x;
const float py = fingers[f]->y * size.y;
if (px >= minX && px <= maxX && py >= minY && py <= maxY) {
down = true;
break;
}
}
SDL_free(fingers);
}
SDL_free(devices);
}
if (down && !wasDown) {
settings_overlay::ToggleTopBar();
}
wasDown = down;
return true;
}
bool IsActive() {
// A connected pad wins: PAD__Read_HLE already prefers it for input, and the
// overlay should get out of the way visually too rather than sit on top of
// the game doing nothing.
int gamepadCount = 0;
SDL_JoystickID* gamepads = SDL_GetGamepads(&gamepadCount);
if (gamepads != nullptr) {
SDL_free(gamepads);
}
if (gamepadCount > 0) {
return false;
}
int deviceCount = 0;
SDL_TouchID* devices = SDL_GetTouchDevices(&deviceCount);
if (devices != nullptr) {
SDL_free(devices);
}
return deviceCount > 0;
}
bool Read(std::array<PADStatus, 4>& statuses) {
if (!IsActive()) {
return false;
}
const Frame frame = SampleFingers(CurrentAspect());
PADStatus& pad = statuses[0];
pad = PADStatus{};
pad.err = PAD_ERR_NONE;
uint16_t buttons = 0;
if (frame.a) buttons |= PAD_BUTTON_A;
if (frame.b) buttons |= PAD_BUTTON_B;
if (frame.item) buttons |= PAD_TRIGGER_Z;
if (frame.start) buttons |= PAD_BUTTON_START;
if (frame.l) buttons |= PAD_TRIGGER_L;
if (frame.r) buttons |= PAD_TRIGGER_R;
if (frame.up) buttons |= PAD_BUTTON_UP;
if (frame.down) buttons |= PAD_BUTTON_DOWN;
if (frame.left) buttons |= PAD_BUTTON_LEFT;
if (frame.right) buttons |= PAD_BUTTON_RIGHT;
pad.button = buttons;
// Screen y grows downward, the guest stick grows upward.
pad.stickX = static_cast<int8_t>(std::clamp(frame.stickDx * 127.0f, -127.0f, 127.0f));
pad.stickY = static_cast<int8_t>(std::clamp(-frame.stickDy * 127.0f, -127.0f, 127.0f));
pad.triggerLeft = frame.l ? 255 : 0;
pad.triggerRight = frame.r ? 255 : 0;
return true;
}
void Draw() {
const ImGuiIO& io = ImGui::GetIO();
const ImVec2 size = io.DisplaySize;
if (size.x <= 0.0f || size.y <= 0.0f || !HasTouchScreen()) {
return;
}
const Layout L = LayoutFor(size.x / size.y);
// The way into settings exists whenever there is a screen to tap, even when
// a pad has hidden everything else.
ImDrawList* menuList = ImGui::GetBackgroundDrawList();
if (!PollFpsTap(size)) {
// No FPS readout to tap, so fall back to an explicit button - otherwise a
// touch host with show_fps off has no way into the settings bar at all.
PollMenuButton();
DrawCircle(menuList, L.menu, size, settings_overlay::TopBarVisible() ? "X" : "=",
settings_overlay::TopBarVisible(), kColGrey, nullptr);
}
if (!IsActive()) {
return;
}
const Frame frame = SampleFingers(size.x / size.y);
ImDrawList* list = ImGui::GetBackgroundDrawList();
// Stick: outer ring plus a thumb dot showing current deflection.
// The glyph is the knob; nothing is drawn behind it.
const ImVec2 stickCentre{L.stick.x * size.x, L.stick.y * size.y};
const float stickRadius = L.stick.r * size.y;
const float knobRadius = stickRadius * 0.80f;
const float travel = stickRadius * 0.38f;
const Circle knob{(stickCentre.x + frame.stickDx * travel) / size.x,
(stickCentre.y + frame.stickDy * travel) / size.y,
knobRadius / size.y};
DrawCircle(list, knob, size, "", frame.stickHeld, kColStick, "control_stick");
DrawCircle(list, L.a, size, "A", frame.a, kColA, "a");
DrawCircle(list, L.b, size, "B", frame.b, kColB, "b");
DrawCircle(list, L.item, size, "Z", frame.item, kColZ, "right_bumper");
DrawCircle(list, L.start, size, "START", frame.start, kColGrey, "start_pause");
DrawCircle(list, L.shoulderL, size, "L", frame.l, kColGrey, "l_analog");
DrawCircle(list, L.shoulderR, size, "R", frame.r, kColGrey, "r_analog");
const bool dpadHeld = frame.up || frame.down || frame.left || frame.right;
DrawCircle(list, L.dpad, size, "", dpadHeld, kColGrey, "d-pad");
}
} // namespace TouchPad
+1 -1
View File
@@ -499,7 +499,7 @@ int32_t HandleIpTopIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const s
// Nonblocking sockets get -SO_EAGAIN immediately (Dolphin's retry predicate
// short-circuits on nonBlock/forceNonBlock, IOS/Network/Socket.cpp:715-718);
// waiting here anyway stalled the whole emulation thread on every empty read.
constexpr int kStreamRecvWaitMs = 250;
constexpr int kStreamRecvWaitMs = 1000;
const int streamWaitMs = (forceNonBlock || s->nonblocking) ? 0 : kStreamRecvWaitMs;
const bool waited = ret < 0 && !fromPtr && s->type == SOCK_STREAM &&
IsWouldBlockError(nativeErr) && WaitForReadable(s->native, streamWaitMs);
+29 -13
View File
@@ -78,22 +78,38 @@ bool ProcessSleepTimers(CpuContext* cpu)
{
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();
{
std::lock_guard<std::mutex> lock(gSleepTimerMutex);
auto it = gSleepTimers.begin();
while (it != gSleepTimers.end()) {
if (it->deadline > now) {
++it;
continue;
while (processedCount < kMaxTimersPerCall) {
SleepTimerEntry timer{0, {}};
bool found = false;
{
std::lock_guard<std::mutex> lock(gSleepTimerMutex);
for (auto it = gSleepTimers.begin(); it != gSleepTimers.end(); ++it) {
if (it->deadline <= now) {
timer = *it;
gSleepTimers.erase(it);
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;
if (threadPtr == 0 ||
!Memory::Contains(threadPtr + kThreadSuspendOffset, sizeof(uint32_t))) {
@@ -219,7 +235,7 @@ bool ProcessSleepTimers(CpuContext* cpu)
}
}
return !dueTimers.empty();
return processedAny;
}
} // namespace OsHleInternal
+33 -21
View File
@@ -1,6 +1,7 @@
#include "hle_stubs.h"
#include "console_identity.h"
#include "sc_serial_contract.h"
#include <cstdlib>
#include <cstddef>
#include <cstdint>
@@ -12,7 +13,25 @@
namespace {
constexpr uint32_t kPalProductRegion = 2;
// Use the SDK's own value tables, including its unknown-region result.
uint32_t LookupProductRegion(uint32_t table, uint32_t stride, uint32_t count,
const std::string& value) {
for (uint32_t index = 0; index < count; ++index) {
const uint32_t entry = table + index * stride;
if (!Memory::Contains(entry, stride)) {
break;
}
const auto* bytes = static_cast<const uint8_t*>(Memory::GetPointer(entry, stride));
if (bytes[0] == 0xFF) {
break;
}
if (value.size() < stride - 1 &&
std::memcmp(bytes + 1, value.c_str(), value.size() + 1) == 0) {
return bytes[0];
}
}
return 0xFFFFFFFFu;
}
} // namespace
@@ -50,16 +69,12 @@ extern "C" uint32_t SCGetEuRgb60Mode_HLE()
PPC_NATIVE_OVERRIDE(801B1CAC, SCGetEuRgb60Mode_HLE, uint32_t, (), ());
// The managed NAND intentionally starts without a console-owned setting.txt.
// DWC nevertheless requires the Wii product code and serial number so it can
// include csnum in NAS authentication. Expose one stable virtual-console
// identity without requiring or mutating a user's real NAND.
// Expose the selected emulated NAND identity through the SDK SC APIs.
extern "C" uint32_t SCGetProductArea_HLE()
{
// The PAL setting.txt AREA value is "EUR". The SDK's lookup table at
// 0x8029CEB0 maps JPN=0, USA=1, EUR=2.
return kPalProductRegion;
return LookupProductRegion(0x8029CEB0u, 5, 13,
RuntimeConsoleIdentity::Current().area);
}
PPC_NATIVE_OVERRIDE(801B23A0, SCGetProductArea_HLE, uint32_t, (), ());
@@ -68,12 +83,13 @@ extern "C" uint32_t SCGetProductCode_HLE()
{
// Original PAL SC storage for the six-byte CODE value.
constexpr uint32_t kProductCodeAddress = 0x803869E0u;
static constexpr char kProductCode[] = "LEH";
if (!Memory::Contains(kProductCodeAddress, sizeof(kProductCode))) {
const std::string& productCode = RuntimeConsoleIdentity::Current().productCode;
const size_t size = productCode.size() + 1;
if (!Memory::Contains(kProductCodeAddress, size)) {
return 0;
}
std::memcpy(Memory::GetPointer(kProductCodeAddress, sizeof(kProductCode)),
kProductCode, sizeof(kProductCode));
std::memcpy(Memory::GetPointer(kProductCodeAddress, size),
productCode.c_str(), size);
return kProductCodeAddress;
}
@@ -82,21 +98,17 @@ PPC_NATIVE_OVERRIDE(801B2424, SCGetProductCode_HLE, uint32_t, (), ());
extern "C" uint32_t SCGetProductSN_HLE(uint32_t serialAddress)
{
const std::string& serial = RuntimeConsoleIdentity::Current().serial;
if (!serialAddress || !Memory::Contains(serialAddress, serial.size() + 1)) {
return 0;
}
std::memcpy(Memory::GetPointer(serialAddress, serial.size() + 1),
serial.c_str(), serial.size() + 1);
return 1;
return RuntimeScSerial::Write(serial, serialAddress,
[](uint32_t address, size_t size) { return Memory::Contains(address, size); },
[](uint32_t address, uint32_t value) { Memory::Write32(address, value); });
}
PPC_NATIVE_OVERRIDE(801B2460, SCGetProductSN_HLE, uint32_t, (uint32_t serialAddress), (serialAddress));
extern "C" uint32_t SCGetProductGameRegion_HLE()
{
// The PAL setting.txt GAME value is "EU". The SDK's own lookup table at
// 0x8029CEF8 maps JP=0, US=1, EU=2.
return kPalProductRegion;
return LookupProductRegion(0x8029CEF8u, 4, 4,
RuntimeConsoleIdentity::Current().gameRegion);
}
PPC_NATIVE_OVERRIDE(801B24C8, SCGetProductGameRegion_HLE, uint32_t, (), ());
+3
View File
@@ -93,6 +93,9 @@ extern "C" int32_t NANDOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint32_t
const std::filesystem::path hostPath = TranslateNandPath(path);
if (const auto result = NandCheckSystemSaveRead("NANDOpen", hostPath, mode))
return *result;
// Existing-file write opens go through a shadow copy seeded from the original, so a
// crash between NANDWrite and NANDClose cannot leave a torn file (the game patches
// sub-ranges, e.g. ghost saves at a non-zero offset). New files still create in place.
+2
View File
@@ -411,6 +411,8 @@ extern "C" int32_t NANDSafeOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint
if (mode == 1) {
// Read-only safe open reads the original in place; the library builds no scratch
// copy for this case.
if (const auto result = NandCheckSystemSaveRead("NANDSafeOpen", hostPath, mode))
return *result;
FILE* file = NandFopen(hostPath, "rb");
if (!file && IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) {
file = NandFopen(hostPath, "rb");
+20
View File
@@ -411,6 +411,26 @@ bool IsFaceLibResourcePath(const char* path) {
return std::strcmp(path, "/shared2/menu/FaceLib/RFL_Res.dat") == 0;
}
std::optional<int32_t> NandCheckSystemSaveRead(const char* who,
const std::filesystem::path& hostPath, int mode, bool ios) {
const auto action = RuntimeNandSave::CheckRead(hostPath, mode);
if (action == RuntimeNandSave::ReadAction::Proceed) return std::nullopt;
if (action == RuntimeNandSave::ReadAction::Missing) {
LogNandWarning(who, "treating empty or zero-filled system save '%s' as missing",
HostPathText(hostPath).c_str());
return ios ? ISFS_ENOENT : NAND_RESULT_NOEXISTS;
}
if (action == RuntimeNandSave::ReadAction::RecoveryNeeded) {
LogNandError(who, "system save '%s' is missing or blank but its .nandsafe.tmp contains data; "
"back up both files before attempting recovery",
HostPathText(hostPath).c_str());
} else {
LogNandError(who, "could not inspect system save '%s' or its write shadow; leaving data untouched",
HostPathText(hostPath).c_str());
}
return ios ? ISFS_EIO : NAND_RESULT_UNKNOWN;
}
// Create directories recursively
bool CreateDirectoryPath(const std::filesystem::path& path) {
if (path.empty()) {
+7
View File
@@ -9,6 +9,7 @@
#include "hle/runtime_parse_helpers.h"
#include "memory.h"
#include "nand_path.h"
#include "nand_save_probe.h"
#include "hle/net/network.h"
#include "recomp_mod_loader.h"
#include "runtime_config.h"
@@ -26,6 +27,7 @@
#include <deque>
#include <map>
#include <mutex>
#include <optional>
#include <vector>
#include <filesystem>
#include <string>
@@ -56,6 +58,11 @@ constexpr uint32_t kNandTitleIdLo = 0x524D4350; // "RMCP" fallback
void LogNandError(const char* func, const char* fmt, ...);
void LogNandWarning(const char* func, const char* fmt, ...);
// An empty optional means continue opening normally; otherwise return the
// supplied NAND/IOS error without exposing a failed scan as a missing save.
std::optional<int32_t> NandCheckSystemSaveRead(const char* who,
const std::filesystem::path& hostPath, int mode, bool ios = false);
// ============================================================================
// File Descriptor Management
// ============================================================================
+3
View File
@@ -391,6 +391,9 @@ extern "C" int32_t NAND_IOS_Open_HLE(uint32_t pathPtr, uint32_t mode) {
// It's a NAND file path
const std::filesystem::path hostPath = TranslateNandPath(path);
if (const auto result = NandCheckSystemSaveRead("IOS_Open", hostPath, mode, true))
return *result;
// Seed FaceLib resources before the existence check so every open mode can
// still find them on a fresh managed NAND.
+307
View File
@@ -0,0 +1,307 @@
#include "input_bindings.h"
#include "controller_button_names.h"
#include "input_expr.h"
#include "runtime_config.h"
#include "runtime_log.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <filesystem>
#include <mutex>
#include <unordered_map>
#include <SDL3/SDL_gamepad.h>
namespace InputBindings {
namespace {
struct Binding {
std::string text;
InputExpr::Expression expr;
bool active = false;
};
std::mutex g_mutex;
std::array<std::array<Binding, kControls.size()>, PAD_CHANMAX> g_bindings;
bool g_anyBound = false;
bool g_inputBlocked = false;
// Dolphin input names, mapped onto SDL. XInput-style names are exact; DInput
// "Button <n>" indices follow the common PlayStation layout, which is what
// DInput reports for a DualShock/DualSense. Other pads may number differently.
const std::unordered_map<std::string, SDL_GamepadButton>& ButtonNames() {
static const std::unordered_map<std::string, SDL_GamepadButton> table = {
{"Button A", SDL_GAMEPAD_BUTTON_SOUTH}, {"Button B", SDL_GAMEPAD_BUTTON_EAST},
{"Button X", SDL_GAMEPAD_BUTTON_WEST}, {"Button Y", SDL_GAMEPAD_BUTTON_NORTH},
{"Shoulder L", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
{"Shoulder R", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
{"Thumb L", SDL_GAMEPAD_BUTTON_LEFT_STICK}, {"Thumb R", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
{"Start", SDL_GAMEPAD_BUTTON_START}, {"Back", SDL_GAMEPAD_BUTTON_BACK},
{"Guide", SDL_GAMEPAD_BUTTON_GUIDE},
{"Pad N", SDL_GAMEPAD_BUTTON_DPAD_UP}, {"Pad S", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
{"Pad W", SDL_GAMEPAD_BUTTON_DPAD_LEFT}, {"Pad E", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
{"Hat 0 N", SDL_GAMEPAD_BUTTON_DPAD_UP}, {"Hat 0 S", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
{"Hat 0 W", SDL_GAMEPAD_BUTTON_DPAD_LEFT}, {"Hat 0 E", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
{"Button 0", SDL_GAMEPAD_BUTTON_WEST}, {"Button 1", SDL_GAMEPAD_BUTTON_SOUTH},
{"Button 2", SDL_GAMEPAD_BUTTON_EAST}, {"Button 3", SDL_GAMEPAD_BUTTON_NORTH},
{"Button 4", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
{"Button 5", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
{"Button 8", SDL_GAMEPAD_BUTTON_BACK}, {"Button 9", SDL_GAMEPAD_BUTTON_START},
{"Button 10", SDL_GAMEPAD_BUTTON_LEFT_STICK},
{"Button 11", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
{"Button 12", SDL_GAMEPAD_BUTTON_GUIDE}, {"Button 13", SDL_GAMEPAD_BUTTON_TOUCHPAD},
};
return table;
}
// Signed axis names: SDL axis plus the direction that counts as positive.
struct AxisRef {
SDL_GamepadAxis axis;
int sign;
};
const std::unordered_map<std::string, AxisRef>& AxisNames() {
static const std::unordered_map<std::string, AxisRef> table = {
{"Axis X-", {SDL_GAMEPAD_AXIS_LEFTX, -1}}, {"Axis X+", {SDL_GAMEPAD_AXIS_LEFTX, 1}},
{"Axis Y-", {SDL_GAMEPAD_AXIS_LEFTY, -1}}, {"Axis Y+", {SDL_GAMEPAD_AXIS_LEFTY, 1}},
{"Axis Z-", {SDL_GAMEPAD_AXIS_RIGHTX, -1}}, {"Axis Z+", {SDL_GAMEPAD_AXIS_RIGHTX, 1}},
{"Axis Zr-", {SDL_GAMEPAD_AXIS_RIGHTY, -1}},{"Axis Zr+", {SDL_GAMEPAD_AXIS_RIGHTY, 1}},
{"Left X-", {SDL_GAMEPAD_AXIS_LEFTX, -1}}, {"Left X+", {SDL_GAMEPAD_AXIS_LEFTX, 1}},
{"Left Y-", {SDL_GAMEPAD_AXIS_LEFTY, 1}}, {"Left Y+", {SDL_GAMEPAD_AXIS_LEFTY, -1}},
{"Right X-", {SDL_GAMEPAD_AXIS_RIGHTX, -1}},{"Right X+", {SDL_GAMEPAD_AXIS_RIGHTX, 1}},
{"Right Y-", {SDL_GAMEPAD_AXIS_RIGHTY, 1}}, {"Right Y+", {SDL_GAMEPAD_AXIS_RIGHTY, -1}},
{"Full Axis Xr+", {SDL_GAMEPAD_AXIS_LEFT_TRIGGER, 1}},
{"Full Axis Yr+", {SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, 1}},
{"Trigger L", {SDL_GAMEPAD_AXIS_LEFT_TRIGGER, 1}},
{"Trigger R", {SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, 1}},
};
return table;
}
double ReadInput(SDL_Gamepad* gamepad, const std::string& name) {
if (gamepad == nullptr) {
return 0.0;
}
if (const auto it = ButtonNames().find(name); it != ButtonNames().end()) {
return SDL_GetGamepadButton(gamepad, it->second) ? 1.0 : 0.0;
}
if (const auto it = AxisNames().find(name); it != AxisNames().end()) {
const double raw = SDL_GetGamepadAxis(gamepad, it->second.axis) / 32767.0;
return std::clamp(raw * it->second.sign, 0.0, 1.0);
}
// Fall back to this project's own positional names, so a binding written
// here does not have to use Dolphin vocabulary.
if (const auto* native = ControllerNames::FindNativeButton(name)) {
if (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
: 0.0;
}
}
return 0.0;
}
SDL_Gamepad* GamepadForPort(uint32_t port) {
const s32 index = PADGetIndexForPort(port);
return index < 0 ? nullptr : PADGetSDLGamepadForIndex(static_cast<u32>(index));
}
size_t ControlIndexForDolphinName(const std::string& name) {
for (size_t i = 0; i < kControls.size(); ++i) {
if (name == kControls[i].dolphinName) {
return i;
}
}
return kControls.size();
}
void RecomputeAnyBoundLocked() {
g_anyBound = false;
for (const auto& port : g_bindings) {
for (const auto& binding : port) {
if (!binding.expr.Empty()) {
g_anyBound = true;
return;
}
}
}
}
std::string ConfigKey(uint32_t port, size_t control) {
std::string key = "expr_" + std::to_string(port + 1) + "_";
for (const char* c = kControls[control].dolphinName; *c != '\0'; ++c) {
key += (*c == '/' || *c == '-') ? '_' : static_cast<char>(std::tolower(*c));
}
return key;
}
} // namespace
void SetInputBlocked(bool blocked) noexcept {
std::lock_guard<std::mutex> lock(g_mutex);
g_inputBlocked = blocked;
}
bool InputBlocked() noexcept {
std::lock_guard<std::mutex> lock(g_mutex);
return g_inputBlocked;
}
void Reload() noexcept {
std::lock_guard<std::mutex> lock(g_mutex);
for (uint32_t port = 0; port < PAD_CHANMAX; ++port) {
for (size_t control = 0; control < kControls.size(); ++control) {
Binding& binding = g_bindings[port][control];
binding = Binding{};
binding.text = RuntimeConfigFile::ControllerExpression(ConfigKey(port, control));
std::string error;
if (!binding.text.empty() &&
!InputExpr::Expression::Parse(binding.text, binding.expr, error)) {
RT_LOG(RT_TAG_CONFIG) << "expression for port " << (port + 1) << " "
<< kControls[control].dolphinName << ": " << error << std::endl;
}
}
}
RecomputeAnyBoundLocked();
}
void Apply(PADStatus* statuses) noexcept {
if (statuses == nullptr) {
return;
}
std::lock_guard<std::mutex> lock(g_mutex);
if (!g_anyBound) {
return;
}
const bool blocked = g_inputBlocked;
for (uint32_t port = 0; port < PAD_CHANMAX; ++port) {
if (statuses[port].err != PAD_ERR_NONE) {
continue;
}
SDL_Gamepad* gamepad = GamepadForPort(port);
const InputExpr::InputSource source = [gamepad](const std::string& name) {
return ReadInput(gamepad, name);
};
for (size_t control = 0; control < kControls.size(); ++control) {
Binding& binding = g_bindings[port][control];
if (binding.expr.Empty()) {
continue;
}
if (blocked) {
binding.active = false;
continue;
}
const double value = binding.expr.Evaluate(source);
binding.active = value > InputExpr::kConditionThreshold;
const ControlInfo& info = kControls[control];
if (info.padButton != 0 && binding.active) {
statuses[port].button |= info.padButton;
}
if (info.analog != 0) {
const double safe = std::isfinite(value) ? std::clamp(value, 0.0, 1.0) : 0.0;
const auto scaled = static_cast<uint8_t>(safe * 255.0);
uint8_t& target =
info.analog == 1 ? statuses[port].triggerLeft : statuses[port].triggerRight;
target = std::max(target, scaled);
}
}
}
}
std::string GetExpression(uint32_t port, size_t control) noexcept {
if (port >= PAD_CHANMAX || control >= kControls.size()) {
return {};
}
std::lock_guard<std::mutex> lock(g_mutex);
return g_bindings[port][control].text;
}
bool SetExpression(uint32_t port, size_t control, const std::string& text, std::string& error) noexcept {
if (port >= PAD_CHANMAX || control >= kControls.size()) {
error = "invalid control";
return false;
}
InputExpr::Expression parsed;
if (!InputExpr::Expression::Parse(text, parsed, error)) {
return false;
}
{
std::lock_guard<std::mutex> lock(g_mutex);
Binding& binding = g_bindings[port][control];
binding.text = text;
binding.expr = std::move(parsed);
binding.active = false;
RecomputeAnyBoundLocked();
}
RuntimeConfigFile::SetControllerExpression(ConfigKey(port, control), text);
return true;
}
bool IsActive(uint32_t port, size_t control) noexcept {
if (port >= PAD_CHANMAX || control >= kControls.size()) {
return false;
}
std::lock_guard<std::mutex> lock(g_mutex);
return g_bindings[port][control].active;
}
std::string DefaultDolphinConfigPath() noexcept {
std::error_code ec;
if (const char* appdata = std::getenv("APPDATA"); appdata != nullptr) {
const std::filesystem::path roaming =
std::filesystem::path(appdata) / "Dolphin Emulator" / "Config" / "GCPadNew.ini";
if (std::filesystem::exists(roaming, ec)) {
return RuntimeConfigFile::PathToUtf8(roaming);
}
}
const auto executableDirectory = RuntimeConfigFile::ExecutableDirectory();
return RuntimeConfigFile::PathToUtf8(executableDirectory ? *executableDirectory / "GCPadNew.ini"
: std::filesystem::path("GCPadNew.ini"));
}
int ImportDolphinConfig(const std::string& path, int padIndex, uint32_t port, std::string& summary,
std::string& error) noexcept {
std::vector<std::pair<std::string, std::string>> controls;
std::string device;
if (!InputExpr::ReadDolphinConfig(RuntimeConfigFile::PathFromUtf8(path), padIndex, controls, device,
error)) {
return -1;
}
int imported = 0;
std::vector<std::string> skipped;
for (const auto& [name, text] : controls) {
const size_t control = ControlIndexForDolphinName(name);
if (control == kControls.size()) {
if (name.rfind("Main Stick/", 0) == 0 || name.rfind("C-Stick/", 0) == 0) {
skipped.push_back(name);
}
continue;
}
std::string parseError;
if (!SetExpression(port, control, text, parseError)) {
skipped.push_back(name);
RT_LOG(RT_TAG_CONFIG) << "import " << name << ": " << parseError << std::endl;
continue;
}
++imported;
}
summary = "Imported " + std::to_string(imported) + " controls";
if (!device.empty()) {
summary += " from " + device;
}
if (!skipped.empty()) {
summary += "; skipped " + std::to_string(skipped.size()) +
" (stick axes and unsupported inputs keep their existing mapping)";
}
return imported;
}
} // namespace InputBindings
+631
View File
@@ -0,0 +1,631 @@
#include "input_expr.h"
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <fstream>
#include <unordered_map>
namespace InputExpr {
namespace {
using Clock = std::chrono::steady_clock;
using FSec = std::chrono::duration<double>;
enum class Kind {
Literal, Input, Not, Add, Sub, Mul, Div, And, Or, Xor,
Greater, Less, Equal,
FnIf, FnMin, FnMax, FnClamp, FnAbs, FnSqrt, FnPow, FnSin, FnCos, FnTan,
FnDeadzone, FnTimer, FnToggle, FnHold, FnTap, FnPulse, FnSmooth, FnNot,
};
struct FnInfo {
Kind kind;
int minArgs;
int maxArgs;
};
const std::unordered_map<std::string, FnInfo>& FunctionTable() {
static const std::unordered_map<std::string, FnInfo> table = {
{"not", {Kind::FnNot, 1, 1}}, {"if", {Kind::FnIf, 3, 3}},
{"min", {Kind::FnMin, 2, 2}}, {"max", {Kind::FnMax, 2, 2}},
{"clamp", {Kind::FnClamp, 3, 3}}, {"abs", {Kind::FnAbs, 1, 1}},
{"sqrt", {Kind::FnSqrt, 1, 1}}, {"pow", {Kind::FnPow, 2, 2}},
{"sin", {Kind::FnSin, 1, 1}}, {"cos", {Kind::FnCos, 1, 1}},
{"tan", {Kind::FnTan, 1, 1}}, {"deadzone", {Kind::FnDeadzone, 2, 2}},
{"timer", {Kind::FnTimer, 1, 1}}, {"toggle", {Kind::FnToggle, 1, 2}},
{"hold", {Kind::FnHold, 2, 2}}, {"tap", {Kind::FnTap, 2, 3}},
{"pulse", {Kind::FnPulse, 2, 2}}, {"smooth", {Kind::FnSmooth, 2, 3}},
};
return table;
}
} // namespace
struct Node {
Kind kind;
double literal = 0.0;
std::string input;
std::vector<std::unique_ptr<Node>> args;
// Per-instance state for the stateful functions. Mutable because Evaluate
// is logically a read of current input state.
mutable bool released = false;
mutable bool state = false;
mutable unsigned taps = 0;
mutable double value = 0.0;
mutable Clock::time_point mark = Clock::now();
mutable bool marked = false;
};
namespace {
// ---- tokenizer ----------------------------------------------------------
struct Token {
enum Type { End, Input, Number, Ident, Op, LParen, RParen, Comma } type = End;
std::string text;
};
class Lexer {
public:
explicit Lexer(const std::string& text) : m_text(text) {}
bool Next(Token& tok, std::string& error) {
while (m_pos < m_text.size() && std::isspace(static_cast<unsigned char>(m_text[m_pos]))) {
++m_pos;
}
if (m_pos >= m_text.size()) {
tok = Token{};
return true;
}
const char c = m_text[m_pos];
if (c == '`') {
const size_t close = m_text.find('`', m_pos + 1);
if (close == std::string::npos) {
error = "unterminated ` in expression";
return false;
}
tok.type = Token::Input;
tok.text = m_text.substr(m_pos + 1, close - m_pos - 1);
m_pos = close + 1;
return true;
}
if (std::isdigit(static_cast<unsigned char>(c)) || c == '.') {
size_t end = m_pos;
while (end < m_text.size() &&
(std::isdigit(static_cast<unsigned char>(m_text[end])) || m_text[end] == '.')) {
++end;
}
tok.type = Token::Number;
tok.text = m_text.substr(m_pos, end - m_pos);
m_pos = end;
return true;
}
if (std::isalpha(static_cast<unsigned char>(c)) || c == '_') {
size_t end = m_pos;
while (end < m_text.size() &&
(std::isalnum(static_cast<unsigned char>(m_text[end])) || m_text[end] == '_' ||
m_text[end] == ' ')) {
++end;
}
// Trailing spaces belong to the separator, not the identifier.
while (end > m_pos && m_text[end - 1] == ' ') {
--end;
}
tok.type = Token::Ident;
tok.text = m_text.substr(m_pos, end - m_pos);
m_pos = end;
return true;
}
if (c == '(') { tok.type = Token::LParen; ++m_pos; return true; }
if (c == ')') { tok.type = Token::RParen; ++m_pos; return true; }
if (c == ',') { tok.type = Token::Comma; ++m_pos; return true; }
if (std::string("!&|^+-*/><=").find(c) != std::string::npos) {
tok.type = Token::Op;
tok.text = std::string(1, c);
++m_pos;
return true;
}
error = std::string("unexpected character '") + c + "' in expression";
return false;
}
size_t Position() const { return m_pos; }
private:
const std::string& m_text;
size_t m_pos = 0;
};
// ---- parser -------------------------------------------------------------
using NodePtr = std::unique_ptr<Node>;
class Parser {
public:
explicit Parser(const std::string& text) : m_lexer(text) { Advance(); }
NodePtr ParseExpression(std::string& error) {
NodePtr node = ParseBinary(0, error);
if (!node) {
return nullptr;
}
if (m_failed) {
error = m_lexError;
return nullptr;
}
if (m_tok.type != Token::End) {
error = "unexpected trailing input in expression";
return nullptr;
}
return node;
}
private:
void Advance() {
if (!m_lexer.Next(m_tok, m_lexError)) {
m_tok = Token{};
m_failed = true;
}
}
static int Precedence(const std::string& op) {
if (op == "|") return 1;
if (op == "^") return 2;
if (op == "&") return 3;
if (op == ">" || op == "<" || op == "=") return 4;
if (op == "+" || op == "-") return 5;
if (op == "*" || op == "/") return 6;
return -1;
}
static Kind BinaryKind(const std::string& op) {
if (op == "|") return Kind::Or;
if (op == "^") return Kind::Xor;
if (op == "&") return Kind::And;
if (op == ">") return Kind::Greater;
if (op == "<") return Kind::Less;
if (op == "=") return Kind::Equal;
if (op == "+") return Kind::Add;
if (op == "-") return Kind::Sub;
if (op == "*") return Kind::Mul;
return Kind::Div;
}
NodePtr ParseBinary(int minPrec, std::string& error) {
NodePtr lhs = ParseUnary(error);
if (!lhs) {
return nullptr;
}
while (m_tok.type == Token::Op) {
const int prec = Precedence(m_tok.text);
if (prec < 0 || prec < minPrec) {
break;
}
const std::string op = m_tok.text;
Advance();
NodePtr rhs = ParseBinary(prec + 1, error);
if (!rhs) {
return nullptr;
}
auto node = std::make_unique<Node>();
node->kind = BinaryKind(op);
node->args.push_back(std::move(lhs));
node->args.push_back(std::move(rhs));
lhs = std::move(node);
}
return lhs;
}
NodePtr ParseUnary(std::string& error) {
if (m_failed) {
error = m_lexError;
return nullptr;
}
if (m_tok.type == Token::Op && (m_tok.text == "!" || m_tok.text == "-" || m_tok.text == "+")) {
const std::string op = m_tok.text;
Advance();
NodePtr inner = ParseUnary(error);
if (!inner) {
return nullptr;
}
if (op == "+") {
return inner;
}
auto node = std::make_unique<Node>();
if (op == "!") {
node->kind = Kind::Not;
node->args.push_back(std::move(inner));
} else {
node->kind = Kind::Sub;
auto zero = std::make_unique<Node>();
zero->kind = Kind::Literal;
node->args.push_back(std::move(zero));
node->args.push_back(std::move(inner));
}
return node;
}
return ParsePrimary(error);
}
NodePtr ParsePrimary(std::string& error) {
if (m_failed) {
error = m_lexError;
return nullptr;
}
switch (m_tok.type) {
case Token::Input: {
auto node = std::make_unique<Node>();
node->kind = Kind::Input;
node->input = m_tok.text;
Advance();
return node;
}
case Token::Number: {
auto node = std::make_unique<Node>();
node->kind = Kind::Literal;
node->literal = std::strtod(m_tok.text.c_str(), nullptr);
Advance();
return node;
}
case Token::LParen: {
Advance();
NodePtr inner = ParseBinary(0, error);
if (!inner) {
return nullptr;
}
if (m_tok.type != Token::RParen) {
error = "expected closing paren";
return nullptr;
}
Advance();
return inner;
}
case Token::Ident: {
const std::string name = m_tok.text;
Advance();
if (m_tok.type != Token::LParen) {
// A bare identifier is an input name, as Dolphin allows for
// simple cases such as "Start" or "LSHIFT".
auto node = std::make_unique<Node>();
node->kind = Kind::Input;
node->input = name;
return node;
}
const auto it = FunctionTable().find(name);
if (it == FunctionTable().end()) {
error = "unknown function '" + name + "'";
return nullptr;
}
Advance();
auto node = std::make_unique<Node>();
node->kind = it->second.kind;
if (m_tok.type != Token::RParen) {
while (true) {
NodePtr arg = ParseBinary(0, error);
if (!arg) {
return nullptr;
}
node->args.push_back(std::move(arg));
if (m_tok.type != Token::Comma) {
break;
}
Advance();
}
}
if (m_tok.type != Token::RParen) {
error = "expected closing paren after " + name + " arguments";
return nullptr;
}
Advance();
const int count = static_cast<int>(node->args.size());
if (count < it->second.minArgs || count > it->second.maxArgs) {
error = name + " takes " + std::to_string(it->second.minArgs) + " to " +
std::to_string(it->second.maxArgs) + " arguments";
return nullptr;
}
return node;
}
default:
error = "expected start of expression";
return nullptr;
}
}
Lexer m_lexer;
Token m_tok;
std::string m_lexError;
bool m_failed = false;
};
// ---- evaluator ----------------------------------------------------------
double Eval(const Node& node, const InputSource& source);
double Arg(const Node& node, size_t index, const InputSource& source) {
return Eval(*node.args[index], source);
}
double Eval(const Node& node, const InputSource& source) {
switch (node.kind) {
case Kind::Literal: return node.literal;
case Kind::Input: return source ? source(node.input) : 0.0;
case Kind::Not:
case Kind::FnNot: return 1.0 - Arg(node, 0, source);
case Kind::Add: return Arg(node, 0, source) + Arg(node, 1, source);
case Kind::Sub: return Arg(node, 0, source) - Arg(node, 1, source);
case Kind::Mul: return Arg(node, 0, source) * Arg(node, 1, source);
case Kind::Div: {
// Both sides are evaluated even when the divisor is zero: the left
// subtree may hold stateful functions that need their frame update.
const double lhs = Arg(node, 0, source);
const double rhs = Arg(node, 1, source);
return rhs == 0.0 ? 0.0 : lhs / rhs;
}
case Kind::And: return std::min(Arg(node, 0, source), Arg(node, 1, source));
case Kind::Or: return std::max(Arg(node, 0, source), Arg(node, 1, source));
case Kind::Xor: {
const double a = Arg(node, 0, source);
const double b = Arg(node, 1, source);
return std::max(std::min(a, 1.0 - b), std::min(b, 1.0 - a));
}
case Kind::Greater: return Arg(node, 0, source) > Arg(node, 1, source) ? 1.0 : 0.0;
case Kind::Less: return Arg(node, 0, source) < Arg(node, 1, source) ? 1.0 : 0.0;
case Kind::Equal: return Arg(node, 0, source) == Arg(node, 1, source) ? 1.0 : 0.0;
case Kind::FnIf:
return Arg(node, 0, source) > kConditionThreshold ? Arg(node, 1, source) : Arg(node, 2, source);
case Kind::FnMin: return std::min(Arg(node, 0, source), Arg(node, 1, source));
case Kind::FnMax: return std::max(Arg(node, 0, source), Arg(node, 1, source));
case Kind::FnClamp: {
const double v = Arg(node, 0, source);
double lo = Arg(node, 1, source);
double hi = Arg(node, 2, source);
if (lo > hi) {
std::swap(lo, hi);
}
return std::clamp(v, lo, hi);
}
case Kind::FnAbs: return std::abs(Arg(node, 0, source));
case Kind::FnSqrt: return std::sqrt(Arg(node, 0, source));
case Kind::FnPow: return std::pow(Arg(node, 0, source), Arg(node, 1, source));
case Kind::FnSin: return std::sin(Arg(node, 0, source));
case Kind::FnCos: return std::cos(Arg(node, 0, source));
case Kind::FnTan: return std::tan(Arg(node, 0, source));
case Kind::FnDeadzone: {
const double v = Arg(node, 0, source);
const double dz = std::clamp(Arg(node, 1, source), 0.0, 0.999);
return std::copysign(std::max(0.0, std::abs(v) - dz) / (1.0 - dz), v);
}
case Kind::FnTimer: {
const auto now = Clock::now();
if (!node.marked) {
node.mark = now;
node.marked = true;
}
const double period = Arg(node, 0, source);
double progress = std::chrono::duration_cast<FSec>(now - node.mark).count() / period;
if (!std::isfinite(progress) || progress < 0.0) {
progress = 0.0;
node.mark = now;
} else if (progress >= 1.0) {
const double resets = std::floor(progress);
node.mark += std::chrono::duration_cast<Clock::duration>(FSec(period * resets));
progress -= resets;
}
return progress;
}
case Kind::FnToggle: {
const double inner = Arg(node, 0, source);
if (inner < kConditionThreshold) {
node.released = true;
} else if (node.released) {
node.released = false;
node.state = !node.state;
}
if (node.args.size() == 2 && Arg(node, 1, source) > kConditionThreshold) {
node.state = false;
}
return node.state ? 1.0 : 0.0;
}
case Kind::FnHold: {
const auto now = Clock::now();
if (!node.marked) {
node.mark = now;
node.marked = true;
}
const double input = Arg(node, 0, source);
if (input < kConditionThreshold) {
node.state = false;
node.mark = now;
} else if (!node.state) {
if (std::chrono::duration_cast<FSec>(now - node.mark).count() >= Arg(node, 1, source)) {
node.state = true;
}
}
return node.state ? 1.0 : 0.0;
}
case Kind::FnTap: {
const auto now = Clock::now();
if (!node.marked) {
node.mark = now;
node.marked = true;
}
const double elapsed = std::chrono::duration_cast<FSec>(now - node.mark).count();
const double input = Arg(node, 0, source);
const bool timeUp = elapsed > Arg(node, 1, source);
// The count is user authored, so a negative or huge value must not
// reach the unsigned conversion.
double requested = node.args.size() == 3 ? Arg(node, 2, source) : 2.0;
if (!std::isfinite(requested)) {
requested = 2.0;
}
const auto desired = static_cast<unsigned>(std::clamp(requested + 0.5, 1.0, 64.0));
if (input < kConditionThreshold) {
node.released = true;
if (node.taps > 0 && timeUp) {
node.taps = 0;
}
return 0.0;
}
if (node.released) {
if (node.taps == 0) {
node.mark = now;
}
++node.taps;
node.released = false;
}
return desired == node.taps ? 1.0 : 0.0;
}
case Kind::FnPulse: {
const auto now = Clock::now();
const double input = Arg(node, 0, source);
if (input < kConditionThreshold) {
node.released = true;
} else if (node.released) {
node.released = false;
const double requested = Arg(node, 1, source);
const double safe = std::isfinite(requested) ? std::clamp(requested, 0.0, 3600.0) : 0.0;
const auto seconds = std::chrono::duration_cast<Clock::duration>(FSec(safe));
if (node.state) {
node.mark += seconds;
} else {
node.state = true;
node.mark = now + seconds;
}
}
if (node.state && now >= node.mark) {
node.state = false;
}
return node.state ? 1.0 : 0.0;
}
case Kind::FnSmooth: {
const auto now = Clock::now();
if (!node.marked) {
node.mark = now;
node.marked = true;
}
const double elapsed = std::chrono::duration_cast<FSec>(now - node.mark).count();
node.mark = now;
const double desired = Arg(node, 0, source);
const double up = Arg(node, 1, source);
const double down = node.args.size() == 3 ? Arg(node, 2, source) : up;
const double rate = (desired < node.value) ? down : up;
const double maxMove = elapsed / rate;
if (!std::isfinite(maxMove)) {
node.value = desired;
} else {
const double diff = desired - node.value;
node.value += std::copysign(std::min(maxMove, std::abs(diff)), diff);
}
return node.value;
}
}
return 0.0;
}
void Collect(const Node& node, std::vector<std::string>& out) {
if (node.kind == Kind::Input) {
if (std::find(out.begin(), out.end(), node.input) == out.end()) {
out.push_back(node.input);
}
}
for (const auto& arg : node.args) {
Collect(*arg, out);
}
}
std::string Trim(const std::string& text) {
const size_t begin = text.find_first_not_of(" \t\r\n");
if (begin == std::string::npos) {
return {};
}
return text.substr(begin, text.find_last_not_of(" \t\r\n") - begin + 1);
}
} // namespace
Expression::Expression() = default;
Expression::~Expression() = default;
Expression::Expression(Expression&&) noexcept = default;
Expression& Expression::operator=(Expression&&) noexcept = default;
bool Expression::Parse(const std::string& text, Expression& out, std::string& error) {
out.m_root.reset();
if (Trim(text).empty()) {
return true;
}
Parser parser(text);
NodePtr root = parser.ParseExpression(error);
if (!root) {
return false;
}
out.m_root = std::move(root);
return true;
}
double Expression::Evaluate(const InputSource& source) const {
if (m_root == nullptr) {
return 0.0;
}
const double value = Eval(*m_root, source);
return std::isfinite(value) ? value : 0.0;
}
std::vector<std::string> Expression::ReferencedInputs() const {
std::vector<std::string> out;
if (m_root) {
Collect(*m_root, out);
}
return out;
}
bool ReadDolphinConfig(const std::filesystem::path& path, int padIndex,
std::vector<std::pair<std::string, std::string>>& controls,
std::string& deviceName, std::string& error) {
std::ifstream file(path);
if (!file) {
error = "could not open " + path.string();
return false;
}
const std::string wanted = "[GCPad" + std::to_string(padIndex) + "]";
bool inSection = false;
bool found = false;
std::string line;
controls.clear();
deviceName.clear();
while (std::getline(file, line)) {
const std::string trimmed = Trim(line);
if (trimmed.empty() || trimmed[0] == '#' || trimmed[0] == ';') {
continue;
}
if (trimmed.front() == '[') {
inSection = trimmed == wanted;
found = found || inSection;
continue;
}
if (!inSection) {
continue;
}
const size_t eq = trimmed.find('=');
if (eq == std::string::npos) {
continue;
}
const std::string key = Trim(trimmed.substr(0, eq));
const std::string value = Trim(trimmed.substr(eq + 1));
if (key == "Device") {
deviceName = value;
} else if (!value.empty()) {
controls.emplace_back(key, value);
}
}
if (!found) {
error = wanted + " not found in " + path.string();
return false;
}
return true;
}
} // namespace InputExpr
-26
View File
@@ -60,7 +60,6 @@
#include "aurora_events.h"
#include "wii_remote_input.h"
#include "discord_presence.h"
#include "touch_art.h"
#include "fiber_manager.h"
#include "hle_stubs.h"
#include "runtime_config.h"
@@ -1432,10 +1431,6 @@ int RuntimeMain(int argc, char** argv) {
}
aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait);
GxGuestWrite::InstallAuroraHooks();
#ifdef MKW_PLATFORM_IOS
// Up front, or the frame the controls appear on pays for every decode.
TouchArt::Preload();
#endif
UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width,
auroraInfo.windowSize.native_fb_height);
settings_overlay::InitializeRuntimeSettings();
@@ -1514,28 +1509,7 @@ int RuntimeMain(int argc, char** argv) {
}
}
// SDL must own main() on iOS: it supplies the entry point that starts
// UIApplicationMain and hands control back here once the app is running.
#ifdef MKW_PLATFORM_IOS
#include <SDL3/SDL_main.h>
#endif
int main(int argc, char** argv) {
#ifdef MKW_PLATFORM_IOS
// SDL's iOS backend calls SDL_main from scene:willConnectToSession:, once
// for every scene that connects (SDL_uikitappdelegate.m, postFinishLaunch).
// Attaching an external display makes iOS create a second scene, so this
// runs again while the first call is still inside the game loop, pumping
// events. Re-entering would boot the guest a second time in a live process:
// the observed result was data sections reloaded, 0 static constructors on
// the second pass, and aurora dying on "Only one window allowed per
// display". Declaring UIApplicationSupportsMultipleScenes=false does not
// prevent the extra scene, so refuse the re-entry here instead.
static std::atomic_flag alreadyRunning = ATOMIC_FLAG_INIT;
if (alreadyRunning.test_and_set(std::memory_order_acq_rel)) {
return 0;
}
#endif
return RuntimeMain(argc, argv);
}
extern "C" bool g_dynamicAspectRatioEnabled = false;
-12
View File
@@ -59,18 +59,6 @@ std::filesystem::path ApplicationDataDirectory(std::string_view applicationName)
CoTaskMemFree(rawPath);
return directory;
}
#elif defined(MKW_PLATFORM_IOS)
// The whole container is already private to this app, so user state lives
// in Documents rather than a name-scoped Application Support subdirectory.
// That puts Config.toml beside the game data the user copied in, which is
// what makes a relative dvd_root resolve, and UIFileSharingEnabled exposes
// the directory in Files.app so the user can manage it.
if (const char* home = std::getenv("HOME"); home && *home) {
return std::filesystem::path(home) / "Documents";
}
if (const passwd* user = getpwuid(getuid()); user && user->pw_dir && *user->pw_dir) {
return std::filesystem::path(user->pw_dir) / "Documents";
}
#elif defined(__APPLE__)
if (const char* home = std::getenv("HOME"); home && *home) {
return std::filesystem::path(home) / "Library" / "Application Support" / applicationName;
@@ -1,21 +0,0 @@
// Apple's clang links libclang_rt.ios.a implicitly; upstream clang on a Linux
// host has no iOS compiler-rt, and the only builtins this program needs from
// it are the two @available() checks. They are thin wrappers over dyld, so
// provide them here rather than asking people to extract a file from Xcode.
#include <stdint.h>
typedef struct {
uint32_t platform;
uint32_t version;
} dyld_build_version_t;
extern int _availability_version_check(uint32_t count, dyld_build_version_t versions[]);
int32_t __isPlatformVersionAtLeast(uint32_t platform, uint32_t major, uint32_t minor, uint32_t subminor) {
dyld_build_version_t wanted = {platform, (major << 16) | (minor << 8) | subminor};
return _availability_version_check(1, &wanted);
}
int32_t __isOSVersionAtLeast(int32_t major, int32_t minor, int32_t subminor) {
return __isPlatformVersionAtLeast(2 /* PLATFORM_IOS */, (uint32_t)major, (uint32_t)minor, (uint32_t)subminor);
}
+440 -158
View File
@@ -1,7 +1,8 @@
#include "settings_overlay.h"
#include "touch_pad.h"
#include "audio_backend.h"
#include "controller_button_names.h"
#include "controller_mapping_wizard.h"
#include "input_bindings.h"
#include "game_graphics_options.h"
#include "music_attenuation.h"
#include "runtime_config.h"
@@ -19,6 +20,7 @@
#include <algorithm>
#include <atomic>
#include <cctype>
#include <charconv>
#include <chrono>
#include <cmath>
#include <cstdint>
@@ -35,6 +37,8 @@
#endif
#include <dolphin/pad.h>
extern "C" void PAD_HLE_SetRumbleEnabled(bool enabled);
#include <dolphin/vi.h>
#include <aurora/aurora.h>
#include <aurora/gfx.h>
@@ -66,8 +70,7 @@ const char* GraphicsApiDisplayName() {
}
bool g_topBarVisible = false;
float g_fpsBounds[4] = {0.0f, 0.0f, 0.0f, 0.0f};
bool g_fpsBoundsValid = false;
bool g_rumbleEnabled = RuntimeConfigFile::RumbleEnabled(true);
int g_controllerPort = 0;
float g_resolutionScale = RuntimeConfigFile::ResolutionMultiplier(1.0f);
int g_audioVolumePercent = static_cast<int>(std::lround(RuntimeConfigFile::AudioVolume(1.0f) * 100.0f));
@@ -109,62 +112,9 @@ std::array<int32_t, PAD_MAX_CONTROLLERS> g_configuredControllerIndices = [] {
return indices;
}();
struct ControllerButtonItem {
const char* configKey;
const char* label;
PADButton padButton;
};
constexpr std::array<ControllerButtonItem, PAD_BUTTON_COUNT> kControllerButtons = {{
{"a", "A", PAD_BUTTON_A},
{"b", "B", PAD_BUTTON_B},
{"x", "X", PAD_BUTTON_X},
{"y", "Y", PAD_BUTTON_Y},
{"start", "Start", PAD_BUTTON_START},
{"z", "Z", PAD_TRIGGER_Z},
{"l", "L", PAD_TRIGGER_L},
{"r", "R", PAD_TRIGGER_R},
{"up", "D-pad Up", PAD_BUTTON_UP},
{"down", "D-pad Down", PAD_BUTTON_DOWN},
{"left", "D-pad Left", PAD_BUTTON_LEFT},
{"right", "D-pad Right", PAD_BUTTON_RIGHT},
}};
struct NativeButtonItem {
const char* configName;
const char* label;
uint32_t nativeButton;
};
constexpr std::array<NativeButtonItem, SDL_GAMEPAD_BUTTON_COUNT + 1> kNativeButtons = {{
{"unmapped", "Unmapped / analog trigger", PAD_NATIVE_BUTTON_INVALID},
{"south", "South (A / Cross)", SDL_GAMEPAD_BUTTON_SOUTH},
{"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST},
{"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST},
{"north", "North (Y / Triangle)", SDL_GAMEPAD_BUTTON_NORTH},
{"back", "Back / Select", SDL_GAMEPAD_BUTTON_BACK},
{"guide", "Guide / Home", SDL_GAMEPAD_BUTTON_GUIDE},
{"start", "Start / Options", SDL_GAMEPAD_BUTTON_START},
{"left_stick", "Left stick click", SDL_GAMEPAD_BUTTON_LEFT_STICK},
{"right_stick", "Right stick click", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
{"left_shoulder", "Left shoulder", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
{"right_shoulder", "Right shoulder", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
{"dpad_up", "D-pad Up", SDL_GAMEPAD_BUTTON_DPAD_UP},
{"dpad_down", "D-pad Down", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
{"dpad_left", "D-pad Left", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
{"dpad_right", "D-pad Right", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
{"misc1", "Misc 1 / Share", SDL_GAMEPAD_BUTTON_MISC1},
{"right_paddle1", "Right paddle 1", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
{"left_paddle1", "Left paddle 1", SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
{"right_paddle2", "Right paddle 2", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
{"left_paddle2", "Left paddle 2", SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
{"touchpad", "Touchpad", SDL_GAMEPAD_BUTTON_TOUCHPAD},
{"misc2", "Misc 2", SDL_GAMEPAD_BUTTON_MISC2},
{"misc3", "Misc 3 / GC L click", SDL_GAMEPAD_BUTTON_MISC3},
{"misc4", "Misc 4 / GC R click", SDL_GAMEPAD_BUTTON_MISC4},
{"misc5", "Misc 5", SDL_GAMEPAD_BUTTON_MISC5},
{"misc6", "Misc 6", SDL_GAMEPAD_BUTTON_MISC6},
}};
using ControllerNames::kNativeButtons;
using ControllerNames::NativeButtonItem;
constexpr const auto& kControllerButtons = ControllerNames::kGameCubeButtons;
// Classic Controller Pro layout, indexed like kControllerButtons: the SNES-style
// diamond (A right, B bottom, X top, Y left) with digital bumpers driving the GC
@@ -181,6 +131,13 @@ constexpr std::array<const char*, PAD_BUTTON_COUNT> kClassicProPreset = {
"dpad_up", "dpad_down", "dpad_left", "dpad_right",
};
// PlayStation layout: bumpers drive the GC triggers, Z moves to Create/Share.
constexpr std::array<const char*, PAD_BUTTON_COUNT> kPlayStationPreset = {
"south", "east", "west", "north", "start", "back",
"left_shoulder", "right_shoulder",
"dpad_up", "dpad_down", "dpad_left", "dpad_right",
};
struct ResolutionItem {
const char* label;
float scale;
@@ -199,12 +156,7 @@ constexpr std::array<std::string_view, 3> kDisplayModeConfigNames = {
uint64_t g_presentedFrame = 0;
std::atomic_bool g_strapInputAccepted = false;
std::atomic_uint64_t g_startupDismissFrame = UINT64_MAX;
// The strap screen is accepted the instant it becomes eligible, so the frames
// it would have spent waiting for A are still rendered underneath this cover.
// One second was not enough on an Apple TV 4K or an iPhone loading from the
// app container: the scene change landed after the cover lifted and the strap
// warning flashed through. Three seconds covers the slowest device measured.
constexpr uint64_t kStrapTransitionCoverFrames = 180;
constexpr uint64_t kStrapTransitionCoverFrames = 60;
constexpr std::array<ResolutionItem, 8> kResolutions = {{
{"Auto (window size)", 0.0f}, {"Native (1x)", 1.0f}, {"1.5x", 1.5f}, {"2x", 2.0f},
@@ -233,11 +185,18 @@ void LimitResolutionForFrameRate() {
}
}
const NativeButtonItem* FindNativeButton(std::string value) {
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(), [&](const NativeButtonItem& item) {
return value == item.configName;
});
return it == kNativeButtons.end() ? nullptr : &*it;
using ControllerNames::FindNativeButton;
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 {
@@ -245,32 +204,26 @@ struct ControllerBindingPair {
std::string secondary;
};
std::string TrimBindingToken(const std::string& token) {
const size_t begin = token.find_first_not_of(" \t");
if (begin == std::string::npos) {
return {};
}
const size_t end = token.find_last_not_of(" \t");
return token.substr(begin, end - begin + 1);
}
// Config values hold up to two comma-separated button names ("dpad_up" or
// "dpad_up,left_shoulder"); pressing either one counts as the GC button.
ControllerBindingPair SplitControllerBinding(const std::string& value) {
const size_t comma = value.find(',');
if (comma == std::string::npos) {
return {TrimBindingToken(value), {}};
return {ControllerNames::TrimToken(value), {}};
}
return {TrimBindingToken(value.substr(0, comma)), TrimBindingToken(value.substr(comma + 1))};
return {ControllerNames::TrimToken(value.substr(0, comma)), ControllerNames::TrimToken(value.substr(comma + 1))};
}
const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) {
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(), [&](const NativeButtonItem& item) {
return nativeButton == item.nativeButton;
});
return it == kNativeButtons.end() ? kNativeButtons.front() : *it;
using ControllerNames::NativeButtonForValue;
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) {
if (g_topBarVisible == visible) {
return;
@@ -309,7 +262,7 @@ void ApplyConfiguredMappings() {
}
const ControllerBindingPair binding = SplitControllerBinding(*configured);
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 {
RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey
<< " button '" << binding.primary << "'" << std::endl;
@@ -317,7 +270,7 @@ void ApplyConfiguredMappings() {
uint32_t altNative = PAD_NATIVE_BUTTON_INVALID;
if (!binding.secondary.empty()) {
if (const NativeButtonItem* native = FindNativeButton(binding.secondary)) {
altNative = native->nativeButton;
altNative = ConfiguredNativeButton(*native, binding.secondary);
} else {
RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey
<< " secondary button '" << binding.secondary << "'" << std::endl;
@@ -401,10 +354,8 @@ void DrawWiiRemoteSettings(uint32_t selectedGamePort) {
}
ImGui::EndDisabled();
ImGui::SameLine();
if (WiiRemoteInput::IsScanning() && WiiRemoteInput::PeriodicRescanEnabled()) {
if (WiiRemoteInput::IsScanning()) {
ImGui::TextDisabled("Scanning... (%u so far) - press 1+2 on the remote", WiiRemoteInput::ScanCount());
} else if (WiiRemoteInput::IsScanning()) {
ImGui::TextDisabled("Waiting for a remote - press 1+2 on the remote");
} else {
ImGui::TextDisabled("Not scanning");
}
@@ -464,7 +415,314 @@ void DrawWiiRemoteSettings(uint32_t selectedGamePort) {
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.
int ExpressionResizeCallback(ImGuiInputTextCallbackData* data) {
if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
auto* text = static_cast<std::string*>(data->UserData);
text->resize(static_cast<size_t>(data->BufTextLen));
data->Buf = text->data();
}
return 0;
}
void DrawExpressionSettings() {
ImGui::SeparatorText("Expressions (Dolphin syntax)");
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 440.0f);
ImGui::TextDisabled(
"Optional. An expression overrides nothing: its result is combined with the "
"button mapping above. Operators ! & | ^ and functions if, min, max, clamp, "
"timer, toggle, hold, tap, pulse, smooth, deadzone behave as they do in Dolphin.");
ImGui::PopTextWrapPos();
static std::array<std::string, InputBindings::kControls.size()> errors;
static std::array<std::string, InputBindings::kControls.size()> buffers;
static std::string importStatus;
static int loadedPort = -1;
static bool reloadBuffers = true;
const auto port = static_cast<uint32_t>(g_controllerPort);
if (loadedPort != g_controllerPort || reloadBuffers) {
for (size_t i = 0; i < buffers.size(); ++i) {
buffers[i] = InputBindings::GetExpression(port, i);
}
errors.fill(std::string());
loadedPort = g_controllerPort;
reloadBuffers = false;
}
if (ImGui::Button("Import from Dolphin")) {
const std::string path = InputBindings::DefaultDolphinConfigPath();
std::string summary;
std::string error;
if (InputBindings::ImportDolphinConfig(path, g_controllerPort + 1, port, summary, error) < 0) {
importStatus = error;
} else {
importStatus = summary;
errors.fill(std::string());
reloadBuffers = true;
}
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Reads [GCPad%d] from %%APPDATA%%\\Dolphin Emulator\\Config\\GCPadNew.ini,\n"
"or GCPadNew.ini next to the executable.", g_controllerPort + 1);
}
if (!importStatus.empty()) {
ImGui::TextDisabled("%s", importStatus.c_str());
}
for (size_t i = 0; i < InputBindings::kControls.size(); ++i) {
ImGui::PushID(static_cast<int>(i) + 2000);
std::string& text = buffers[i];
ImGui::SetNextItemWidth(300.0f);
if (ImGui::InputText(InputBindings::kControls[i].label, text.data(), text.capacity() + 1,
ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackResize,
ExpressionResizeCallback, &text)) {
std::string error;
errors[i] = InputBindings::SetExpression(port, i, text, error) ? std::string() : error;
}
if (InputBindings::IsActive(port, i)) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f), "active");
}
if (!errors[i].empty()) {
ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.3f, 1.0f), "%s", errors[i].c_str());
}
ImGui::PopID();
}
}
void DrawRumbleSettings() {
ImGui::SeparatorText("Vibration");
if (ImGui::Checkbox("Controller vibration", &g_rumbleEnabled)) {
PAD_HLE_SetRumbleEnabled(g_rumbleEnabled);
RuntimeConfigFile::SetRumbleEnabled(g_rumbleEnabled);
if (!g_rumbleEnabled) {
// Stop whatever is already running: the game will not send another
// motor command until its own state machine decides to.
constexpr std::array<uint32_t, PAD_MAX_CONTROLLERS> stopAll{
PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD,
};
PADControlAllMotors(stopAll.data());
}
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Applies to every port.");
}
}
void DrawControllerSettings() {
for (int port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
const std::string label = "Port " + std::to_string(port + 1);
@@ -476,6 +734,10 @@ void DrawControllerSettings() {
ImGui::Separator();
const uint32_t selectedGamePort = static_cast<uint32_t>(g_controllerPort);
if (DrawKeyboardSettings(selectedGamePort)) {
return;
}
ImGui::Separator();
const char* currentName = PADGetName(selectedGamePort);
ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None");
if (ImGui::MenuItem("Unassign controller")) {
@@ -517,10 +779,10 @@ void DrawControllerSettings() {
PADGetAltButtonMappings(static_cast<uint32_t>(g_controllerPort), &altMappingCount);
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) {
value += ',';
value += NativeButtonForValue(altNative).configName;
value += NativeBindingConfig(altNative);
}
RuntimeConfigFile::SetControllerButton(index, value);
};
@@ -553,23 +815,36 @@ void DrawControllerSettings() {
PADSerializeMappings();
mappings = PADGetButtonMappings(port, &mappingCount);
}
ImGui::SameLine();
if (ImGui::Button("Classic Controller Pro")) {
const auto applyPreset = [&](const std::array<const char*, PAD_BUTTON_COUNT>& preset) {
const uint32_t port = static_cast<uint32_t>(g_controllerPort);
for (size_t i = 0; i < kControllerButtons.size(); ++i) {
if (const NativeButtonItem* native = FindNativeButton(kClassicProPreset[i])) {
if (const NativeButtonItem* native = FindNativeButton(preset[i])) {
PADSetButtonMapping(port, PADButtonMapping{native->nativeButton, kControllerButtons[i].padButton});
PADSetAltButtonMapping(port,
PADButtonMapping{PAD_NATIVE_BUTTON_INVALID, kControllerButtons[i].padButton});
RuntimeConfigFile::SetControllerButton(i, kClassicProPreset[i]);
RuntimeConfigFile::SetControllerButton(i, preset[i]);
}
}
altRowExpanded.fill(false);
PADSerializeMappings();
mappings = PADGetButtonMappings(port, &mappingCount);
};
ImGui::SameLine();
if (ImGui::Button("Classic Controller Pro")) {
applyPreset(kClassicProPreset);
}
ImGui::SameLine();
if (ImGui::Button("PlayStation")) {
applyPreset(kPlayStationPreset);
}
ImGui::SeparatorText("Button mapping");
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) {
auto mappingIt = std::find_if(mappings, mappings + mappingCount, [&](const PADButtonMapping& mapping) {
return mapping.padButton == kControllerButtons[i].padButton;
@@ -589,30 +864,40 @@ void DrawControllerSettings() {
const NativeButtonItem& current = NativeButtonForValue(mappingIt->nativeButton);
ImGui::PushID(static_cast<int>(i));
ImGui::SetNextItemWidth(190.0f);
if (ImGui::BeginCombo("##primary", current.label)) {
for (const auto& candidate : kNativeButtons) {
const bool selected = candidate.nativeButton == mappingIt->nativeButton;
if (ImGui::Selectable(candidate.label, selected)) {
const uint32_t port = static_cast<uint32_t>(g_controllerPort);
PADSetButtonMapping(port, PADButtonMapping{candidate.nativeButton, kControllerButtons[i].padButton});
writeBinding(i, candidate.nativeButton,
altIt != nullptr ? altIt->nativeButton : PAD_NATIVE_BUTTON_INVALID);
PADSerializeMappings();
mappings = PADGetButtonMappings(port, &mappingCount);
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
const auto drawThreshold = [&](PADButtonMapping* mapping, bool secondary) {
if (!PADIsAxisButton(mapping->nativeButton)) return;
int threshold = static_cast<int>(PADAxisButtonThreshold(mapping->nativeButton));
ImGui::SetNextItemWidth(bindingWidth);
if (ImGui::SliderInt(secondary ? "##altThreshold" : "##primaryThreshold", &threshold,
1, 100, "Threshold: %d%%", ImGuiSliderFlags_AlwaysClamp)) {
const PADButtonMapping updated = {
PADAxisButtonIdentity(mapping->nativeButton) | (static_cast<uint32_t>(threshold) << 8),
mapping->padButton,
};
if (secondary) PADSetAltButtonMapping(selectedGamePort, updated);
else PADSetButtonMapping(selectedGamePort, updated);
}
ImGui::EndCombo();
if (ImGui::IsItemDeactivatedAfterEdit()) {
writeBinding(i, mappingIt->nativeButton,
altIt != nullptr ? altIt->nativeButton : PAD_NATIVE_BUTTON_INVALID);
PADSerializeMappings();
}
};
ImGui::BeginGroup();
ImGui::SetNextItemWidth(bindingWidth);
const std::string primaryCaption = std::string(current.label) + "##primary";
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) {
const bool altBound = altIt->nativeButton != PAD_NATIVE_BUTTON_INVALID;
if (!altBound && !altRowExpanded[i]) {
ImGui::SameLine();
if (ImGui::SmallButton("+")) {
altRowExpanded[i] = true;
BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label, true);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Add a second binding; pressing either one works");
@@ -621,33 +906,23 @@ void DrawControllerSettings() {
ImGui::SameLine();
ImGui::TextUnformatted("or");
ImGui::SameLine();
ImGui::BeginGroup();
const char* altLabel = altBound ? NativeButtonForValue(altIt->nativeButton).label : "None";
ImGui::SetNextItemWidth(190.0f);
if (ImGui::BeginCombo("##alt", altLabel)) {
for (const auto& candidate : kNativeButtons) {
const bool isNone = candidate.nativeButton == PAD_NATIVE_BUTTON_INVALID;
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();
ImGui::SetNextItemWidth(bindingWidth);
const std::string altCaption = std::string(altLabel) + "##alt";
if (ImGui::Button(altCaption.c_str(), ImVec2(bindingWidth, 0.0f))) {
BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label, true);
}
drawThreshold(altIt, true);
ImGui::EndGroup();
}
}
ImGui::SameLine();
ImGui::TextUnformatted(kControllerButtons[i].label);
ImGui::PopID();
}
DrawExpressionSettings();
DrawRumbleSettings();
}
void DrawAudioSettings() {
@@ -806,9 +1081,6 @@ void DrawGraphicsSettings() {
void DrawFpsOverlay() {
AuroraPresentTiming presentTiming{};
aurora_get_present_timing(&presentTiming);
// Before the early return: a stale rectangle would keep swallowing taps
// meant for the menu button, leaving no way into settings.
g_fpsBoundsValid = false;
if (!g_showFps) {
return;
}
@@ -826,13 +1098,6 @@ void DrawFpsOverlay() {
ImGuiWindowFlags_NoNav |
ImGuiWindowFlags_NoSavedSettings;
if (ImGui::Begin("FPS Overlay", nullptr, kFlags)) {
const ImVec2 wpos = ImGui::GetWindowPos();
const ImVec2 wsize = ImGui::GetWindowSize();
constexpr float kTouchPad = 14.0f;
g_fpsBounds[0] = wpos.x - kTouchPad; g_fpsBounds[1] = wpos.y - kTouchPad;
g_fpsBounds[2] = wpos.x + wsize.x + kTouchPad;
g_fpsBounds[3] = wpos.y + wsize.y + kTouchPad;
g_fpsBoundsValid = true;
if (presentTiming.sampleCount == 0) {
ImGui::TextUnformatted("FPS: --");
} else {
@@ -911,10 +1176,27 @@ void DrawStartupScreen() {
}
void DrawTopBar() {
if (!g_topBarVisible || !ImGui::BeginMainMenuBar()) {
if (!g_topBarVisible) {
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::Separator();
const auto resolutionIt = std::find_if(kResolutions.begin(), kResolutions.end(), [](const ResolutionItem& item) {
@@ -943,6 +1225,9 @@ void DrawTopBar() {
if (ImGui::BeginMenu("Controller settings")) {
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();
}
@@ -1012,6 +1297,8 @@ void PersistDisplayModeIfChanged() {
} // namespace
void InitializeRuntimeSettings() noexcept {
PAD_HLE_SetRumbleEnabled(g_rumbleEnabled);
InputBindings::Reload();
controller_mapping_wizard::LoadPersistedMappings();
ApplyConfiguredMappings();
AudioBackend::Instance().SetMasterVolume(static_cast<float>(g_audioVolumePercent) / 100.0f);
@@ -1032,6 +1319,7 @@ void InitializeRuntimeSettings() noexcept {
g_strapInputAccepted.store(false, std::memory_order_relaxed);
g_startupDismissFrame.store(UINT64_MAX, std::memory_order_relaxed);
PADBlockInput(false);
InputBindings::SetInputBlocked(false);
}
void HandleEvents(const AuroraEvent* events) noexcept {
@@ -1046,7 +1334,12 @@ void HandleEvents(const AuroraEvent* events) noexcept {
continue;
}
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);
}
if (IsMouseActivity(ev->sdl)) {
@@ -1055,14 +1348,6 @@ void HandleEvents(const AuroraEvent* events) noexcept {
}
}
void ToggleTopBar() noexcept { SetTopBarVisible(!g_topBarVisible); }
bool TopBarVisible() noexcept { return g_topBarVisible; }
bool FpsOverlayBounds(float* a, float* b, float* c, float* d) noexcept {
if (!g_fpsBoundsValid) return false;
*a = g_fpsBounds[0]; *b = g_fpsBounds[1]; *c = g_fpsBounds[2]; *d = g_fpsBounds[3];
return true;
}
void Draw() noexcept {
// Wait for the frame worker's DONE phase: it has replayed the previous frame's ImGui draw lists
// and started the next ImGui frame, so all overlay callers can now safely issue ImGui commands.
@@ -1081,13 +1366,10 @@ void Draw() noexcept {
DrawFpsOverlay();
DrawTopBar();
controller_mapping_wizard::Draw();
#ifdef MKW_PLATFORM_IOS
TouchPad::Draw();
#endif
// The wizard captures raw presses; keep them out of the game even when the
// top bar is hidden mid-setup. The top bar matters for touch too: while it
// is open, taps belong to the bar and must not drive the guest.
PADBlockInput(g_topBarVisible || controller_mapping_wizard::IsActive());
// The wizard captures raw presses; keep them out of the game.
const bool inputBlocked = controller_mapping_wizard::IsActive() || g_rebind.active;
PADBlockInput(inputBlocked);
InputBindings::SetInputBlocked(inputBlocked);
DrawStartupScreen();
}
+142
View File
@@ -0,0 +1,142 @@
#include "nand_save_probe.h"
#include <algorithm>
#include <chrono>
#include <iostream>
#include <sstream>
#include <stdexcept>
#ifdef _WIN32
#include <windows.h>
#endif
namespace fs = std::filesystem;
using RuntimeNandSave::ReadAction;
using RuntimeNandSave::Contents;
static void Require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
static void Write(const fs::path& path, const std::string& bytes) {
fs::create_directories(path.parent_path());
std::ofstream output(path, std::ios::binary);
output.write(bytes.data(), bytes.size());
output.close();
Require(static_cast<bool>(output), "Fixture write failed");
}
static std::string Read(const fs::path& path) {
std::ifstream input(path, std::ios::binary);
Require(static_cast<bool>(input), "Fixture read failed");
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
}
// A disk error after zero-filled blocks must not look like a blank file's EOF.
class FailingDisk : public std::streambuf {
int blocks;
public:
explicit FailingDisk(int zeroBlocks) : blocks(zeroBlocks) {}
std::streamsize xsgetn(char* buffer, std::streamsize length) override {
if (blocks-- <= 0) throw std::runtime_error("injected read failure");
std::fill(buffer, buffer + length, '\0');
return length;
}
};
int main() {
const auto root = fs::temp_directory_path() / ("wiicomp-save-scenarios-" +
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
try {
const auto save = root / "title/00010004/524d4350/data/rksys.dat";
const auto shadow = fs::path(save.native() + fs::path(".nandsafe.tmp").native());
// Save inspection must leave unrelated NAND data alone. Settings
// initialization is covered separately by nand_settings_tests.
const auto settingsPath = root / "title/00000001/00000002/data/setting.txt";
const std::string identity(256, '\x5a');
Write(settingsPath, identity);
Require(RuntimeNandSave::CheckRead(save, 1) == ReadAction::Proceed, "Fresh profile follows normal missing-file handling");
Require(!fs::exists(save), "Probing fresh profile must not create a save");
// First launch interrupted before save initialization, including block
// boundaries and a full-sized synthetic zero-filled allocation.
for (const size_t size : {size_t(0), size_t(1), size_t(4095), size_t(4096), size_t(4097), size_t(3 * 1024 * 1024)}) {
const std::string bytes(size, '\0');
Write(save, bytes);
Require(RuntimeNandSave::CheckRead(save, 1) == ReadAction::Missing, "Blank save should be offered first-save recovery");
Require(Read(save) == bytes, "Blank-save detection must not modify the file");
for (int mode : {2, 3}) {
Require(RuntimeNandSave::CheckRead(save, mode) == ReadAction::Proceed, "Write opens must remain available for initialization");
}
}
// Existing saves, imported saves, partial/corrupt saves, and a zero
// prefix with data only in the final byte are all left to the game.
std::string existing(3 * 1024 * 1024, '\0');
existing.replace(0, 8, "RKSD0006");
existing[10000] = 42;
for (const std::string& bytes : {existing, std::string("RKSD"), std::string("damaged-header"),
std::string(8192, '\0') + "x", std::string(8191, '\0') + "x"}) {
Write(save, bytes);
Require(RuntimeNandSave::CheckRead(save, 1) == ReadAction::Proceed, "Never hide a save containing any data");
Require(Read(save) == bytes, "Existing/partial save must be byte-identical after inspection");
}
// Interrupted replacement: retain a committed original regardless of
// whether the shadow is blank, partial, or contains a complete header.
Write(save, existing);
for (const std::string& bytes : {std::string(), std::string(4096, '\0'), std::string("RKSD"), existing}) {
Write(shadow, bytes);
Require(RuntimeNandSave::CheckRead(save, 1) == ReadAction::Proceed, "Committed original takes precedence over write shadow");
Require(Read(save) == existing && Read(shadow) == bytes, "Probe must preserve both sides of an interrupted write");
}
// No usable original: do not let missing-save recovery discard the
// only possible recovery source, and do not auto-promote that shadow.
for (const bool mainExists : {false, true}) {
fs::remove(save);
if (mainExists) Write(save, std::string(4096, '\0'));
Write(shadow, existing);
Require(RuntimeNandSave::CheckRead(save, 1) == ReadAction::RecoveryNeeded, "Preserve recovery candidate when original is missing or blank");
Require(Read(shadow) == existing, "Recovery candidate must remain unchanged");
Require(fs::exists(save) == mainExists, "Do not promote shadow automatically");
}
Write(shadow, std::string(4096, '\0'));
Require(RuntimeNandSave::CheckRead(save, 1) == ReadAction::Missing, "Two blank files may use first-save recovery");
fs::remove(shadow);
for (const char* name : {"rksys.dat.bak", "rksys.dat.backup", "rksys.dat2", "banner.bin", "setting.txt"}) {
const auto unrelated = save.parent_path() / name;
Write(unrelated, std::string(4096, '\0'));
Require(RuntimeNandSave::CheckRead(unrelated, 1) == ReadAction::Proceed, "Do not classify backups or unrelated files as missing saves");
}
for (int blocks : {0, 1, 2}) {
FailingDisk disk(blocks);
std::istream input(&disk);
Require(RuntimeNandSave::InspectStream(input) == Contents::Error, "Read failure must remain an error, including after zero-filled blocks");
}
std::istringstream badEof;
badEof.setstate(std::ios::badbit | std::ios::eofbit);
Require(RuntimeNandSave::InspectStream(badEof) == Contents::Error, "Badbit plus EOF must not imply a blank save");
#ifdef _WIN32
Write(save, existing);
const HANDLE locked = CreateFileW(save.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
Require(locked != INVALID_HANDLE_VALUE, "Could not lock fixture");
const auto lockedResult = RuntimeNandSave::CheckRead(save, 1);
CloseHandle(locked);
Require(lockedResult == ReadAction::Error, "Sharing/access failure must not report a missing save");
Require(Read(save) == existing, "Locked save must survive inspection unchanged");
Require(SetFileAttributesW(save.c_str(), FILE_ATTRIBUTE_READONLY) != 0, "Set fixture read-only");
const auto readOnlyResult = RuntimeNandSave::CheckRead(save, 1);
SetFileAttributesW(save.c_str(), FILE_ATTRIBUTE_NORMAL);
Require(readOnlyResult == ReadAction::Proceed && Read(save) == existing, "Readable read-only save remains available");
#endif
Require(Read(settingsPath) == identity, "Save inspection must not change NAND settings");
fs::remove_all(root);
std::cout << "NAND save startup, preservation, interrupted-write and I/O failure scenarios passed\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << " (fixtures retained at " << root << ")\n";
return 1;
}
}
+183
View File
@@ -0,0 +1,183 @@
#include "nand_settings.h"
#include <chrono>
#include <iostream>
#include <stdexcept>
#include <thread>
#include <vector>
static void Require(bool condition, const char* message = "NAND settings check failed") {
if (!condition) {
throw std::runtime_error(message);
}
}
static std::string ReadBytes(const std::filesystem::path& path) {
std::ifstream input(path, std::ios::binary);
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
}
int main() {
const auto root = std::filesystem::temp_directory_path() /
("wiicomp-nand-settings-" + std::to_string(
std::chrono::steady_clock::now().time_since_epoch().count()));
const auto path = root / "title/00000001/00000002/data/setting.txt";
try {
using namespace RuntimeNandSettings;
Require(GenerateSerial(1800000123) == "800000123", "Dolphin timestamp modulo");
Require(GenerateSerial(1000000001) == "000000001", "Dolphin leading zero padding");
Require(GenerateSerial(-1).empty(), "Invalid clock must not supply an identity");
// Golden bytes generated by Dolphin's unmodified SettingsHandler.cpp
// (upstream 2026-09-06), PAL boot fields and synthetic serial 000000001.
// Everything after this prefix is raw zero padding to 256 bytes.
const std::string goldenHex =
"bba6ac929a0bc96b7eed83d27f33a1e7e73d9b836d8b47c59ee23df6b275baab"
"bec9d9dead03cc7a3bdafee50c30ab9fb86194e119fe4ba19eff62d5ec3aacb3"
"b5c9d9e3977eac0943d7ff903120a49ef024eafe1cf77be79cf6229a823aabf0f0";
std::array<uint8_t, 256> golden{};
for (size_t i = 0; i < goldenHex.size() / 2; ++i) {
golden[i] = static_cast<uint8_t>(std::stoul(goldenHex.substr(i * 2, 2), nullptr, 16));
}
Require(EncodeNew("000000001") == golden, "Exact Dolphin writer golden fixture");
std::string error;
Require(!RuntimeNandSettings::Read(root));
Require(!std::filesystem::exists(root));
std::filesystem::create_directories(path.parent_path());
const auto scratchParent = root / "scratch-collisions";
std::filesystem::create_directories(scratchParent / ".setting-init-fixed-0");
const auto sentinel = scratchParent / ".setting-init-fixed-0" / "setting.txt";
{ std::ofstream output(sentinel); output << "another launch owns this"; }
const auto occupiedFile = scratchParent / ".setting-init-fixed-1";
{ std::ofstream output(occupiedFile); output << "leave this file alone"; }
std::error_code scratchError;
const auto claimed = CreateScratchDirectory(scratchParent, "fixed", scratchError);
Require(claimed && *claimed == scratchParent / ".setting-init-fixed-2" && !scratchError,
"Retry collisions with both existing directories and files");
Require(ReadBytes(sentinel) == "another launch owns this" &&
ReadBytes(occupiedFile) == "leave this file alone", "Never modify another launch's scratch data");
Require(!CreateScratchDirectory(occupiedFile / "not-a-directory", "fixed", scratchError) && scratchError,
"Real filesystem errors must fail rather than retry indefinitely");
// Force all claimants to use the same token; this deterministically
// exercises the collision path even when host clock precision is high.
std::array<std::optional<std::filesystem::path>, 16> claims;
std::vector<std::thread> claimants;
for (size_t i = 0; i < claims.size(); ++i) {
claimants.emplace_back([&, i] {
std::error_code ec;
claims[i] = CreateScratchDirectory(scratchParent, "shared", ec);
});
}
for (auto& claimant : claimants) claimant.join();
for (size_t i = 0; i < claims.size(); ++i) {
Require(claims[i].has_value(), "Every concurrent claimant must acquire a scratch directory");
for (size_t j = 0; j < i; ++j) {
Require(claims[i] != claims[j], "Concurrent claimants must own different scratch directories");
}
}
const std::string plain = "AREA=USA\r\n\nCODE=LU\r\nSERNO=987654321\r\nGAME=US\r\n";
std::array<uint8_t, 256> fixture{};
for (size_t i = 0; i < fixture.size(); ++i) {
const unsigned shift = i % 32;
const uint32_t key = shift == 0 ? 0x73B5DBFAu :
(0x73B5DBFAu << shift) | (0x73B5DBFAu >> (32 - shift));
fixture[i] = static_cast<uint8_t>(key) ^ (i < plain.size() ? plain[i] : 0);
}
{
std::ofstream output(path, std::ios::binary);
output.write(reinterpret_cast<const char*>(fixture.data()), fixture.size());
}
auto settings = RuntimeNandSettings::Read(root);
Require(settings && RuntimeNandSettings::HasIdentity(*settings));
Require(settings->at("SERNO") == "987654321" && settings->at("CODE") == "LU");
Require(settings->at("AREA") == "USA" && settings->at("GAME") == "US");
Require(Ensure(root, error, 1800000123), "Existing imported NAND must work");
std::array<uint8_t, 256> after{};
{
std::ifstream input(path, std::ios::binary);
input.read(reinterpret_cast<char*>(after.data()), after.size());
}
Require(after == fixture);
for (const auto serial : {"", "000000000", "1234567890", "123ABC789"}) {
(*settings)["SERNO"] = serial;
Require(!RuntimeNandSettings::HasIdentity(*settings));
}
(*settings)["SERNO"] = "012345678";
Require(RuntimeNandSettings::HasIdentity(*settings));
(*settings)["CODE"] = "TOOLONG";
Require(!RuntimeNandSettings::HasIdentity(*settings));
(*settings)["CODE"] = "LEH";
settings->erase("GAME");
Require(!RuntimeNandSettings::HasIdentity(*settings));
std::filesystem::resize_file(path, 128);
Require(!RuntimeNandSettings::Read(root));
const auto damaged = ReadBytes(path);
Require(!Ensure(root, error, 1800000123), "Do not replace a truncated identity");
Require(ReadBytes(path) == damaged, "Damaged file must remain untouched");
const auto fresh = root / "fresh";
Require(Ensure(fresh, error, 1800000123), "Missing setting.txt must initialize");
const auto generated = Read(fresh);
Require(generated && HasIdentity(*generated), "Generated file must be readable");
Require(generated->at("SERNO") == "800000123", "Persist Dolphin-generated serial");
Require(generated->at("CODE") == "LEH" && generated->at("AREA") == "EUR" &&
generated->at("GAME") == "EU", "PAL first-boot fields");
Require(generated->at("MODEL") == "RVL-001(EUR)" && generated->at("VIDEO") == "PAL" &&
generated->at("DVD") == "0" && generated->at("MPCH") == "0x7FFE",
"Complete Dolphin boot settings");
const auto firstBoot = ReadBytes(FilePath(fresh));
Require(firstBoot.size() == 256 && firstBoot.back() == 0, "Dolphin buffer size and raw zero padding");
Require(Ensure(fresh, error, 1900000999), "Second boot");
Require(ReadBytes(FilePath(fresh)) == firstBoot, "Second boot must not change any bytes");
const auto blocked = root / "blocked";
{ std::ofstream output(blocked); output << "file obstructing NAND directory"; }
Require(!Ensure(blocked, error, 1800000123), "Write failure must not return an ephemeral identity");
Require(!Ensure(root / "bad-clock", error, -1), "Clock failure must not initialize");
const auto concurrent = root / "concurrent";
std::array<bool, 16> results{};
std::vector<std::thread> workers;
for (size_t i = 0; i < results.size(); ++i) {
workers.emplace_back([&, i] {
std::string detail;
results[i] = Ensure(concurrent, detail, 1800000001 + i);
});
}
for (auto& worker : workers) worker.join();
for (const bool result : results) Require(result, "Concurrent boot must read the persisted winner");
const auto winner = ReadBytes(FilePath(concurrent));
Require(Read(concurrent) && HasIdentity(*Read(concurrent)), "Concurrent boot must persist valid settings");
Require(Ensure(concurrent, error, 1900000999), "Boot after concurrent initialization");
Require(ReadBytes(FilePath(concurrent)) == winner, "Concurrent winner must remain stable");
// Independently decode as Nintendo does: stop at the first encoded NUL.
// Exercise serials that force Dolphin's extra-LF escaping, not only
// values that happen to work with a plain rotating-XOR encoder.
bool sawExtraLf = false;
for (int serial = 1; serial <= 10000; ++serial) {
const auto number = GenerateSerial(1000000000 + serial);
const auto encoded = EncodeNew(number);
Require(encoded.has_value(), "Serial encoding must fit");
std::string decoded;
for (size_t i = 0; i < encoded->size() && (*encoded)[i] != 0; ++i) {
const unsigned shift = i % 32;
const uint32_t key = shift == 0 ? 0x73B5DBFAu :
(0x73B5DBFAu << shift) | (0x73B5DBFAu >> (32 - shift));
decoded += static_cast<char>((*encoded)[i] ^ static_cast<uint8_t>(key));
}
Require(decoded.find("SERNO=" + number + "\r\n") != std::string::npos &&
decoded.find("GAME=EU\r\n") != std::string::npos,
"Encoded NUL must not truncate settings");
sawExtraLf |= decoded.find("\r\n\n") != std::string::npos;
}
Require(sawExtraLf, "Exercise Dolphin LF escape path");
std::filesystem::remove_all(root);
std::cout << "NAND settings checks passed\n";
return 0;
} catch (const std::exception& error) {
std::filesystem::remove_all(root);
std::cerr << error.what() << '\n';
return 1;
}
}
+81
View File
@@ -0,0 +1,81 @@
#include "sc_serial_contract.h"
#include "nand_settings.h"
#include <algorithm>
#include <array>
#include <iostream>
#include <iomanip>
#include <stdexcept>
#include <string>
static void Require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
static uint32_t ReadWord(const unsigned char* bytes) {
return (uint32_t(bytes[0]) << 24) | (uint32_t(bytes[1]) << 16) |
(uint32_t(bytes[2]) << 8) | uint32_t(bytes[3]);
}
int main(int argc, char** argv) {
try {
// Feed real generator + SC ABI outputs to the upstream bot decoder.
// Usage: mkw_sc_serial_tests --timestamp-vectors <unix-seconds> ...
if (argc > 1 && std::string(argv[1]) == "--timestamp-vectors") {
for (int i = 2; i < argc; ++i) {
const auto timestamp = std::stoll(argv[i]);
const auto serial = RuntimeNandSettings::GenerateSerial(static_cast<std::time_t>(timestamp));
std::array<unsigned char, 4> output{};
Require(RuntimeScSerial::Write(serial, 4,
[](uint32_t address, size_t size) { return address == 4 && size == 4; },
[&](uint32_t, uint32_t value) {
for (unsigned j = 0; j < 4; ++j)
output[j] = static_cast<unsigned char>(value >> (24 - 8 * j));
}) == 1, "Generated serial must pass SC ABI");
std::cout << timestamp << '\t' << serial << "\tLEH"
<< std::setfill('0') << std::setw(9) << ReadWord(output.data()) << '\n';
}
return 0;
}
// Reproduce the reported csnums from the old string-writing override.
const unsigned char old7886[] = {'7', '8', '8', '6'};
const unsigned char old7618[] = {'7', '6', '1', '8'};
Require(ReadWord(old7886) == 926431286, "Reproduce shared LEH926431286");
Require(ReadWord(old7618) == 926298424, "Reproduce shared LEH926298424");
std::array<unsigned char, 16> memory;
size_t available = 4;
unsigned writes = 0;
const auto contains = [&](uint32_t address, size_t size) {
return address == 4 && size <= available;
};
const auto write32 = [&](uint32_t address, uint32_t value) {
++writes;
for (unsigned i = 0; i < 4; ++i)
memory[address + i] = static_cast<unsigned char>(value >> (24 - 8 * i));
};
for (const auto& pair : {std::pair{"788600001", 788600001u}, {"788699999", 788699999u},
{"761800001", 761800001u}, {"761899999", 761899999u},
{"012345678", 12345678u}, {"000000001", 1u}, {"999999999", 999999999u}}) {
memory.fill(0xa5);
writes = 0;
Require(RuntimeScSerial::Write(pair.first, 4, contains, write32) == 1, "Accept an exactly four-byte output buffer");
Require(writes == 1 && ReadWord(memory.data() + 4) == pair.second, "Return full numeric serial, including digits after common prefix");
for (size_t i = 0; i < memory.size(); ++i)
if (i < 4 || i >= 8) Require(memory[i] == 0xa5, "Do not overwrite adjacent guest stack data");
}
for (const char* serial : {"", "1234567890", "7886x1234", "-12345678", "+12345678"}) {
writes = 0;
Require(RuntimeScSerial::Write(serial, 4, contains, write32) == 0 && writes == 0, "Reject malformed serial without a write");
}
writes = 0;
Require(RuntimeScSerial::Write("788600001", 0, contains, write32) == 0 && writes == 0, "Reject null output");
available = 3;
Require(RuntimeScSerial::Write("788600001", 4, contains, write32) == 0 && writes == 0, "Reject undersized output");
std::cout << "SC serial collision reproduction, numeric output and memory-boundary tests passed\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
+219
View File
@@ -0,0 +1,219 @@
// Verifies the expression engine against Dolphin's documented semantics,
// including the exact line from the user's GCPadNew.ini.
#include "input_expr.h"
#include <chrono>
#include <cmath>
#include <cstdio>
#include <map>
#include <string>
#include <thread>
static int g_failures = 0;
static std::map<std::string, double> g_inputs;
static InputExpr::InputSource Source() {
return [](const std::string& name) {
const auto it = g_inputs.find(name);
return it == g_inputs.end() ? 0.0 : it->second;
};
}
static void Check(bool ok, const std::string& what) {
if (!ok) {
std::printf(" FAIL: %s\n", what.c_str());
++g_failures;
}
}
static InputExpr::Expression Compile(const std::string& text) {
InputExpr::Expression expr;
std::string error;
if (!InputExpr::Expression::Parse(text, expr, error)) {
std::printf(" FAIL: parse '%s': %s\n", text.c_str(), error.c_str());
++g_failures;
}
return expr;
}
static bool Pressed(const InputExpr::Expression& e) {
return e.Evaluate(Source()) > InputExpr::kConditionThreshold;
}
static void Sleep(int ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); }
int main() {
std::printf("Dolphin expression engine\n");
// Operators: & is min, | is max, ! is 1-x, matching Dolphin.
g_inputs["A"] = 1.0;
g_inputs["B"] = 0.0;
Check(Pressed(Compile("`A`")), "bare input");
Check(!Pressed(Compile("!`A`")), "not");
Check(!Pressed(Compile("`A` & `B`")), "and is min");
Check(Pressed(Compile("`A` | `B`")), "or is max");
Check(Pressed(Compile("`A` ^ `B`")), "xor");
Check(!Pressed(Compile("`A` ^ `A`")), "xor of equal inputs is false");
// Precedence: & binds tighter than |, so this is A | (B & A).
g_inputs["B"] = 0.0;
Check(Pressed(Compile("`A` | `B` & `A`")), "& binds tighter than |");
// Parens and numeric literals.
Check(Pressed(Compile("(`B` | 1)")), "literal");
Check(Pressed(Compile("min(1, `A`)")), "min");
Check(!Pressed(Compile("min(0, `A`)")), "min with zero");
Check(Pressed(Compile("if(`A`, 1, 0)")), "if");
Check(Pressed(Compile("clamp(5, 0, 1)")), "clamp");
// toggle flips on each rising edge and holds between them.
auto toggle = Compile("toggle(`T`)");
g_inputs["T"] = 0.0;
toggle.Evaluate(Source());
g_inputs["T"] = 1.0;
Check(Pressed(toggle), "toggle on after first press");
g_inputs["T"] = 0.0;
Check(Pressed(toggle), "toggle stays on after release");
g_inputs["T"] = 1.0;
Check(!Pressed(toggle), "toggle off on second press");
// hold requires the input to be down for the full duration.
auto hold = Compile("hold(`H`, 0.05)");
g_inputs["H"] = 1.0;
Check(!Pressed(hold), "hold not satisfied immediately");
Sleep(70);
Check(Pressed(hold), "hold satisfied after the interval");
g_inputs["H"] = 0.0;
Check(!Pressed(hold), "hold clears on release");
// pulse fires for the given duration after a rising edge.
auto pulse = Compile("pulse(`P`, 0.05)");
g_inputs["P"] = 0.0;
pulse.Evaluate(Source());
g_inputs["P"] = 1.0;
Check(Pressed(pulse), "pulse fires on rising edge");
Sleep(80);
Check(!Pressed(pulse), "pulse expires");
// The timing-window idiom seen in shared Dolphin configs.
auto window = Compile("!pulse(`W`, 0.05) & pulse(`W`, 0.15)");
g_inputs["W"] = 0.0;
window.Evaluate(Source());
g_inputs["W"] = 1.0;
Check(!Pressed(window), "window closed before its start");
Sleep(90);
Check(Pressed(window), "window open between the two pulses");
Sleep(90);
Check(!Pressed(window), "window closed after its end");
// timer ramps 0..1 and wraps, so a threshold turns it into a square wave.
auto timer = Compile("`X` & timer(0.1)");
g_inputs["X"] = 1.0;
int high = 0;
int low = 0;
for (int i = 0; i < 40; ++i) {
(Pressed(timer) ? high : low)++;
Sleep(5);
}
Check(high > 5 && low > 5, "timer alternates high and low");
// The exact D-Pad/Up line from the user's GCPadNew.ini.
auto dolphinLine = Compile("`Hat 0 N` | `Button 4` & timer(0.01)");
g_inputs["Hat 0 N"] = 0.0;
g_inputs["Button 4"] = 0.0;
Check(!Pressed(dolphinLine), "idle with nothing held");
g_inputs["Hat 0 N"] = 1.0;
Check(Pressed(dolphinLine), "hat alone presses");
g_inputs["Hat 0 N"] = 0.0;
g_inputs["Button 4"] = 1.0;
high = low = 0;
for (int i = 0; i < 60; ++i) {
(Pressed(dolphinLine) ? high : low)++;
Sleep(2);
}
Check(high > 5 && low > 5, "LB alternates via timer(0.01)");
// Regression tests for the CodeRabbit findings on PR #89.
g_inputs["A"] = 1.0;
// clamp with reversed bounds: std::clamp is UB when lo > hi.
Check(Compile("clamp(0.5, 1, 0)").Evaluate(Source()) == 0.5, "clamp tolerates reversed bounds");
// deadzone(v, 1) would divide by zero.
{
const double v = Compile("deadzone(`A`, 1)").Evaluate(Source());
Check(std::isfinite(v), "deadzone with dz=1 stays finite");
}
// timer with a zero or negative period would produce inf or NaN.
for (const char* text : {"timer(0)", "timer(-1)"}) {
const double v = Compile(text).Evaluate(Source());
Check(std::isfinite(v), std::string(text) + " stays finite");
}
// Any non-finite result is squashed before it can reach the uint8_t cast.
for (const char* text : {"sqrt(0 - 1)", "pow(10, 10000)", "tan(1.5707963267948966)"}) {
const double v = Compile(text).Evaluate(Source());
Check(std::isfinite(v), std::string(text) + " is sanitised at the boundary");
}
// tap count is user authored; negative, huge and non-finite must not reach
// the unsigned conversion.
for (const char* text : {"tap(`A`, 0.2, -1)", "tap(`A`, 0.2, 999999999)", "tap(`A`, 0.2, 0)"}) {
InputExpr::Expression e;
std::string err;
Check(InputExpr::Expression::Parse(text, e, err), std::string("parse ") + text);
const double v = e.Evaluate(Source());
Check(std::isfinite(v), std::string(text) + " evaluates without UB");
}
// Exponent notation is not part of the number syntax, matching Dolphin's
// lexer; it is rejected rather than silently misparsed.
{
InputExpr::Expression e;
std::string err;
Check(!InputExpr::Expression::Parse("tap(`A`, 0.2, 1e30)", e, err), "exponent notation rejected");
}
// A zero divisor must not skip the left subtree: stateful functions there
// still need their per-frame update.
{
auto divToggle = Compile("toggle(`D`) / `Z`");
g_inputs["Z"] = 0.0;
g_inputs["D"] = 0.0;
divToggle.Evaluate(Source());
g_inputs["D"] = 1.0;
divToggle.Evaluate(Source()); // rising edge seen even though rhs is 0
g_inputs["D"] = 0.0;
g_inputs["Z"] = 1.0;
Check(divToggle.Evaluate(Source()) > InputExpr::kConditionThreshold,
"toggle still latched while the divisor was zero");
}
// smooth with a zero rate divides 0 by 0; NaN must not stick in the node.
{
auto sm = Compile("smooth(`A`, 0)");
g_inputs["A"] = 1.0;
sm.Evaluate(Source());
Sleep(5);
Check(std::isfinite(sm.Evaluate(Source())), "smooth with a zero rate stays finite");
}
// Referenced inputs, used for diagnostics in the UI.
const auto refs = dolphinLine.ReferencedInputs();
Check(refs.size() == 2, "two referenced inputs");
// Errors are reported, not silently swallowed.
InputExpr::Expression bad;
std::string error;
Check(!InputExpr::Expression::Parse("`A` & ", bad, error), "trailing operator rejected");
Check(!InputExpr::Expression::Parse("nope(1)", bad, error), "unknown function rejected");
Check(!InputExpr::Expression::Parse("(`A`", bad, error), "missing paren rejected");
Check(!InputExpr::Expression::Parse("`A", bad, error), "unterminated backtick rejected");
Check(InputExpr::Expression::Parse("", bad, error) && bad.Empty(), "empty parses to empty");
Check(!InputExpr::Expression::Parse("hold(`A`)", bad, error), "wrong arg count rejected");
if (g_failures == 0) {
std::printf("all checks passed\n");
return 0;
}
std::printf("%d check(s) failed\n", g_failures);
return 1;
}
@@ -35,6 +35,39 @@ public class RetroWfcPayloadLoweringTests
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() =>
new(
"test",
@@ -52,6 +85,51 @@ public class RetroWfcPayloadLoweringTests
],
"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()
{
var payload = new byte[0x240];
@@ -26,6 +26,11 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Translator.Core\Translator.Core.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>
<!--