1 Commits

Author SHA1 Message Date
patchzyy 1ab77952f4 Load console identity from NAND setting.txt 2026-09-05 22:36:56 +02:00
56 changed files with 269 additions and 2991 deletions
-1
View File
@@ -3,4 +3,3 @@
# Patch files must stay LF: git apply matches context bytes against LF upstream sources # Patch files must stay LF: git apply matches context bytes against LF upstream sources
*.patch -text *.patch -text
translator/tests/Translator.Tests/TestAssets/**/*.bin binary
-12
View File
@@ -14,10 +14,6 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
recompilation:
name: Recompilation test
uses: ./.github/workflows/recomp-test.yml
translator: translator:
name: Translator (build + test) name: Translator (build + test)
runs-on: windows-latest runs-on: windows-latest
@@ -30,14 +26,6 @@ jobs:
with: with:
dotnet-version: '8.0.x' dotnet-version: '8.0.x'
- name: Cache NuGet packages
uses: actions/cache@v5
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('translator/Translator.sln', '**/*.csproj', '**/*.props', '**/*.targets', '**/packages.lock.json', 'global.json', 'NuGet.config', 'nuget.config') }}
restore-keys: |
${{ runner.os }}-nuget-
- name: Restore - name: Restore
run: dotnet restore translator/Translator.sln run: dotnet restore translator/Translator.sln
+1 -5
View File
@@ -20,10 +20,6 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
recompilation:
name: Recompilation test
uses: ./.github/workflows/recomp-test.yml
linux-appimage: linux-appimage:
name: Linux (AppImage, ${{ matrix.arch }}) name: Linux (AppImage, ${{ matrix.arch }})
strategy: strategy:
@@ -97,7 +93,7 @@ jobs:
release: release:
name: Publish GitHub Release name: Publish GitHub Release
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
needs: [linux-appimage, windows-installer, recompilation] needs: [linux-appimage, windows-installer]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
-61
View File
@@ -1,61 +0,0 @@
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,8 +26,6 @@ Code.pul
/build/ /build/
/build-*/ /build-*/
/native-build/ /native-build/
/native-build-macos/
/local-products/
/dist/ /dist/
/out/ /out/
[Bb]in/ [Bb]in/
@@ -72,5 +70,3 @@ project.lock.json
*.log *.log
output.txt output.txt
# Operating System
.DS_Store
+1 -1
View File
@@ -281,7 +281,7 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
$manifest = [ordered]@{ $manifest = [ordered]@{
SchemaVersion = 2 SchemaVersion = 2
ProductVersion = '0.2.31' ProductVersion = '0.2.28'
ExpectedGameId = $pins.GameId ExpectedGameId = $pins.GameId
ExpectedDolSha256 = $pins.DolSha256 ExpectedDolSha256 = $pins.DolSha256
ExpectedRelSha256 = $pins.RelSha256 ExpectedRelSha256 = $pins.RelSha256
+8 -14
View File
@@ -117,13 +117,12 @@ function Get-MkwProjectPins([string]$ProjectFile) {
} }
function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Description, function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Description,
[string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '', [bool]$WaitForProcessTree = $true) { [string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '') {
<# <#
Runs a build tool and turns a non-zero exit code into a described failure. By default, Runs a build tool and turns a non-zero exit code into a described failure. Start-Process -Wait
Start-Process -Wait waits for the whole process tree, since a .NET single-file bundle host may 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. Callers that hand off to an extracted child that PowerShell's call operator would not wait for. Start-Process
need to avoid waiting on unrelated descendants can opt into the call-operator path. doesn't publish $LASTEXITCODE, so this sets it manually for callers that check it.
Start-Process doesn't publish $LASTEXITCODE, so this sets it manually for callers that check it.
-StepId emits the machine-readable form the installer's progress bar consumes (BuildStepIds in -StepId emits the machine-readable form the installer's progress bar consumes (BuildStepIds in
WiiCompiled.Setup/InstallProgress.cs); the human sentence stays on the same log line. WiiCompiled.Setup/InstallProgress.cs); the human sentence stays on the same log line.
#> #>
@@ -133,14 +132,9 @@ function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Descri
if ($_.Contains('"')) { throw "A native build argument contains an unsupported quote: $_" } if ($_.Contains('"')) { throw "A native build argument contains an unsupported quote: $_" }
'"' + $_ + '"' '"' + $_ + '"'
}) })
if ($WaitForProcessTree) { $process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments ` -NoNewWindow -Wait -PassThru
-NoNewWindow -Wait -PassThru $exitCode = $process.ExitCode
$exitCode = $process.ExitCode
} else {
& $FilePath @Arguments
$exitCode = $LASTEXITCODE
}
$global:LASTEXITCODE = $exitCode $global:LASTEXITCODE = $exitCode
if ($exitCode -ne 0) { throw "$Description failed with exit code $exitCode." } if ($exitCode -ne 0) { throw "$Description failed with exit code $exitCode." }
} }
+1 -7
View File
@@ -1,6 +1,6 @@
# Fails the release build when a fact duplicated across the repo stops agreeing with the copy # Fails the release build when a fact duplicated across the repo stops agreeing with the copy
# that owns it (recomp.yml). Scripts read pinned facts through Get-MkwProjectPins, but three # that owns it (recomp.yml). Scripts read pinned facts through Get-MkwProjectPins, but three
# consumers can't read YAML (the C++ runtime header, the C# constants, shell scripts, and hand-written lists on # consumers can't read YAML (the C++ runtime header, the C# constants, hand-written lists on
# both sides of the C#/PowerShell boundary), so those are checked here instead. # both sides of the C#/PowerShell boundary), so those are checked here instead.
[CmdletBinding()] [CmdletBinding()]
param([string]$RepositoryRoot) param([string]$RepositoryRoot)
@@ -58,12 +58,6 @@ $hostUri = Get-CapturedValue $retroWfcPayload 'CurrentRetroWfcPayloadUri\s*=\s*"
if ($hostUri -cne $pins.RetroWfcPayloadUri) { if ($hostUri -cne $pins.RetroWfcPayloadUri) {
Add-Failure "InputValidation.CurrentRetroWfcPayloadUri is '$hostUri' but recomp.yml pins '$($pins.RetroWfcPayloadUri)'." Add-Failure "InputValidation.CurrentRetroWfcPayloadUri is '$hostUri' but recomp.yml pins '$($pins.RetroWfcPayloadUri)'."
} }
$macosSetup = Read-SourceFile (Join-Path $launcher 'macos\setup.command') 'macOS setup.command'
$macosUri = Get-CapturedValue $macosSetup "'([^']*/api/wfc/payload\?g=RMCPD00)'" `
'The macOS Retro-WFC endpoint'
if ($macosUri -cne $pins.RetroWfcPayloadUri) {
Add-Failure "macOS setup.command downloads '$macosUri' but recomp.yml pins '$($pins.RetroWfcPayloadUri)'."
}
# --- The game identity: the manifest carries it, but the host also compiles a fallback for a # --- The game identity: the manifest carries it, but the host also compiles a fallback for a
# --- manifest that predates the field, and that fallback decides which disc is accepted. # --- manifest that predates the field, and that fallback decides which disc is accepted.
-148
View File
@@ -1,148 +0,0 @@
# 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> <Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace> <RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName> <AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
<Version>0.2.31</Version> <Version>0.2.28</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description> <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 RetroWfcDownloadTimeout = TimeSpan.FromSeconds(30);
private static readonly TimeSpan RetroWfcRetryDelay = TimeSpan.FromSeconds(1); private static readonly TimeSpan RetroWfcRetryDelay = TimeSpan.FromSeconds(1);
public const string CurrentRetroWfcPayloadUri = "https://rwfc.net/api/wfc/payload?g=RMCPD00"; public const string CurrentRetroWfcPayloadUri = "http://nas.play.rwfc.net/payload?g=RMCPD00";
private static readonly string RetroWfcOfflinePayloadFile = private static readonly string RetroWfcOfflinePayloadFile =
Path.Combine("binary", "payload.RMCPD00.bin"); Path.Combine("binary", "payload.RMCPD00.bin");
@@ -5,7 +5,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace> <RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName> <AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
<Version>0.2.31</Version> <Version>0.2.28</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description> <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 internal static class ProductInfo
{ {
public const string Name = "WiiCompiled"; public const string Name = "WiiCompiled";
public const string Version = "0.2.31"; public const string Version = "0.2.28";
} }
/// <summary>One installed product's record inside install-state.json.</summary> /// <summary>One installed product's record inside install-state.json.</summary>
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyName>WiiCompiled.Setup.Linux</AssemblyName> <AssemblyName>WiiCompiled.Setup.Linux</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Linux</RootNamespace> <RootNamespace>WiiCompiled.Setup.Linux</RootNamespace>
<Version>0.2.31</Version> <Version>0.2.28</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled on Linux</Description> <Description>Command-line installer and launcher for WiiCompiled on Linux</Description>
@@ -121,7 +121,7 @@ internal static class PlatformChecks
internal static class ProductInfo internal static class ProductInfo
{ {
public const string Name = "WiiCompiled"; public const string Name = "WiiCompiled";
public const string Version = "0.2.31"; public const string Version = "0.2.28";
/// <summary> /// <summary>
/// The setup executable is copied into the installation under this name. It is the launcher and /// The setup executable is copied into the installation under this name. It is the launcher and
@@ -7,7 +7,7 @@
<AssemblyName>WiiCompiled.Setup</AssemblyName> <AssemblyName>WiiCompiled.Setup</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace> <RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.2.31</Version> <Version>0.2.28</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled</Description> <Description>Command-line installer and launcher for WiiCompiled</Description>
+1 -1
View File
@@ -90,7 +90,7 @@ if [[ -n "$retro_dir" ]]; then
trap 'rm -rf "$payload_stage"' EXIT trap 'rm -rf "$payload_stage"' EXIT
/usr/bin/curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \ /usr/bin/curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \
--retry 1 --output "$temporary_payload" \ --retry 1 --output "$temporary_payload" \
'https://rwfc.net/api/wfc/payload?g=RMCPD00' || fail 'could not download the Retro-WFC payload needed for online play' 'http://nas.play.rwfc.net/payload?g=RMCPD00' || fail 'could not download the Retro-WFC payload needed for online play'
"$translator" validate-retro-wfc-payload --directory "$payload_stage" || \ "$translator" validate-retro-wfc-payload --directory "$payload_stage" || \
fail 'downloaded Retro-WFC payload failed signature validation' fail 'downloaded Retro-WFC payload failed signature validation'
mkdir -p "$retro_wfc_dir/binary" mkdir -p "$retro_wfc_dir/binary"
+13 -14
View File
@@ -201,10 +201,10 @@ Ninja $ninja_version
Apache License 2.0 Apache License 2.0
EOF EOF
echo "prepare-portable-tools.sh: testing the toolchain..." echo "prepare-portable-tools.sh: smoke-testing the toolchain..."
test_dir=$(mktemp -d) smoke_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT trap 'rm -rf "$smoke_dir"' EXIT
cat > "$test_dir/t.cpp" <<'EOF' cat > "$smoke_dir/t.cpp" <<'EOF'
#include <vector> #include <vector>
#include <cstdio> #include <cstdio>
int main() { int main() {
@@ -214,25 +214,24 @@ int main() {
return sum == 6 ? 0 : 1; return sum == 6 ? 0 : 1;
} }
EOF EOF
"$work/bin/clang++" -std=c++20 -fuse-ld=lld "$test_dir/t.cpp" -o "$test_dir/t" "$work/bin/clang++" -std=c++20 -fuse-ld=lld "$smoke_dir/t.cpp" -o "$smoke_dir/t"
"$test_dir/t" "$smoke_dir/t"
# Also exercised together through CMake+Ninja, exactly how local-build.sh drives them - a plain # 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 # clang++ invocation above would not catch a broken CMAKE_ROOT (Modules/Templates) or a Ninja that
# can't find the compiler. # can't find the compiler.
cat > "$test_dir/CMakeLists.txt" <<'EOF' cat > "$smoke_dir/CMakeLists.txt" <<'EOF'
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(test CXX) project(smoke CXX)
add_executable(test t.cpp) add_executable(smoke t.cpp)
EOF EOF
"$work/bin/cmake" -S "$test_dir" -B "$test_dir/build" -G Ninja \ "$work/bin/cmake" -S "$smoke_dir" -B "$smoke_dir/build" -G Ninja \
-DCMAKE_MAKE_PROGRAM="$work/bin/ninja" -DCMAKE_CXX_COMPILER="$work/bin/clang++" >/dev/null -DCMAKE_MAKE_PROGRAM="$work/bin/ninja" -DCMAKE_CXX_COMPILER="$work/bin/clang++" >/dev/null
"$work/bin/cmake" --build "$test_dir/build" >/dev/null "$work/bin/cmake" --build "$smoke_dir/build" >/dev/null
"$test_dir/build/test" "$smoke_dir/build/smoke"
rm -rf "$test_dir" rm -rf "$smoke_dir"
trap - EXIT trap - EXIT
rm -rf "$toolchain_dir"
mv "$work" "$toolchain_dir" mv "$work" "$toolchain_dir"
echo "prepare-portable-tools.sh: toolchain ready at $toolchain_dir ($(du -sh "$toolchain_dir" | cut -f1))" echo "prepare-portable-tools.sh: toolchain ready at $toolchain_dir ($(du -sh "$toolchain_dir" | cut -f1))"
+2 -31
View File
@@ -1,19 +1,6 @@
<img width="4190" height="1232" alt="wiicomplogofinalfinalfinalev2MADEBY_INKWRECK_plzcredit" src="https://github.com/user-attachments/assets/df7a3f2e-5336-479a-b4c0-968dd578726d" />
# WiiCompiled # WiiCompiled
<p align="center">
<a href="https://github.com/patchzyy/Wiicompiled/releases"><img alt="Windows 10 / 11, x64" src="https://img.shields.io/badge/Windows-10%20%2F%2011%20%C2%B7%20x64-0078D4"></a>
<a href="https://github.com/patchzyy/Wiicompiled/releases"><img alt="Linux, x64 / ARM64" src="https://img.shields.io/badge/Linux-x64%20%2F%20ARM64-FCC624?logo=linux&amp;logoColor=white"></a>
<a href="https://github.com/patchzyy/Wiicompiled/releases"><img alt="macOS 14+, Apple Silicon" src="https://img.shields.io/badge/macOS-14%2B%20%C2%B7%20Apple%20Silicon-0A84FF?logo=apple&amp;logoColor=white"></a>
</p>
<p align="center">
<a href="#building-from-source"><img alt="PowerPC static recompilation" src="https://img.shields.io/badge/PowerPC-static%20recompilation-FF9F0A"></a>
<a href="#retro-rewind"><img alt="Retro Rewind supported" src="https://img.shields.io/badge/Retro%20Rewind-supported-FF375F"></a>
<a href="https://github.com/TeamWheelWizard/WheelWizard/releases"><img alt="Install with Wheel Wizard" src="https://img.shields.io/badge/install%20with-Wheel%20Wizard-8B5CF6"></a>
<a href="LICENSE"><img alt="License: GPLv3" src="https://img.shields.io/badge/license-GPLv3-2EA44F?logo=gnu&amp;logoColor=white"></a>
</p>
A native PC port of Mario Kart Wii, made with static recompilation. A native PC port of Mario Kart Wii, made with static recompilation.
There's no emulator in the loop, no interpreter, no JIT, no PowerPC There's no emulator in the loop, no interpreter, no JIT, no PowerPC
@@ -67,24 +54,10 @@ Press **F10** while the game window has focus:
Everything you change is saved to `Config.toml` on the spot and restored next launch. Everything you change is saved to `Config.toml` on the spot and restored next launch.
**Real controller support.** **Real controller support.**
Controllers are fed to the game as a GameCube controller. Controllers are fed to the game as a GameCube controller.
Mappings are positional (`south`, `east`, `west`, `north`) rather than Xbox-labelled, so the Mappings are positional (`south`, `east`, `west`, `north`) rather than Xbox-labelled, so the
same config makes sense on Xbox, PlayStation, Nintendo and generic SDL pads alike, and extra same config makes sense on Xbox, PlayStation, Nintendo and generic SDL pads alike, and extra
inputs like paddles, touchpads and share buttons show up when the hardware reports them. inputs like paddles, touchpads and share buttons show up when the hardware reports them.
Both button-binding slots also accept SDL triggers and stick directions. Selecting an analog
input shows a threshold slider beneath it (1100%, default 50%); reaching that amount of travel
holds the chosen digital button. Each binding's threshold is saved independently in `Config.toml`
(for example, `a = "right_trigger@35,south"`).
**Keyboard and Mouse support.**
Keyboard and mouse are also available through **F10 > Controller settings > Keyboard and mouse**
for each port. Enabling this replaces that port's gamepad input. The default preset uses WASD
for the main stick, left mouse for A (accelerate), Space for B (brake), right mouse for R
(drift), middle mouse for Z (item), arrow keys for the D-pad (tricks), and Enter for Start.
Keys and mouse buttons can be remapped, including both sticks and triggers; mouse movement
is not used. These settings are saved in `keyboard_bindings.dat` and restored next launch.
**Dolphin-compatible input expressions.** **Dolphin-compatible input expressions.**
Each GameCube control can carry an expression in Dolphin's input syntax, with the same operators Each GameCube control can carry an expression in Dolphin's input syntax, with the same operators
@@ -177,9 +150,7 @@ The default test suite needs no binaries and no host C++ compiler, so you can ha
translator without any game data around. translator without any game data around.
For everything beyond that, feeding in your own `main.dol`/`StaticR.rel`, running the For everything beyond that, feeding in your own `main.dol`/`StaticR.rel`, running the
translation, generating the manifest and build graph, and compiling, see [`translator/README.md`](translator/README.md). translation, generating the manifest and build graph, and compiling. see [`translator/README.md`](translator/README.md).
For a step-by-step guide on compiling both WiiCompiled and Retro Rewind from source on macOS (Apple Silicon), see the [macOS Build Guide](docs/building-macos.md).
## FAQ ## FAQ
@@ -228,7 +199,7 @@ AI coding tools were used during development of this project.
All translated output is verified against real hardware behavior and most importantly, physics accuracy is proven synced across Wii, Dolphin, and WiiCompiled (see FAQ). All translated output is verified against real hardware behavior and most importantly, physics accuracy is proven synced across Wii, Dolphin, and WiiCompiled (see FAQ).
## Credits ## Credits
- **inkwreck** - making the logo
- **[aurora](https://github.com/encounter/aurora)** - the GX rendering/windowing backend this - **[aurora](https://github.com/encounter/aurora)** - the GX rendering/windowing backend this
project's whole graphics layer sits on. MIT licensed. project's whole graphics layer sits on. MIT licensed.
- **[Dawn](https://dawn.googlesource.com/dawn)** - Google's WebGPU implementation, powering - **[Dawn](https://dawn.googlesource.com/dawn)** - Google's WebGPU implementation, powering
-16
View File
@@ -171,22 +171,6 @@ typedef struct PADButtonMapping {
PADButton padButton; PADButton padButton;
} PADButtonMapping; } PADButtonMapping;
// Explicitly disabled, unlike INVALID which permits default L/R trigger input.
#define PAD_NATIVE_BUTTON_DISABLED 0xfffffffeu
// Axis-to-button bindings share the persisted nativeButton field without
// changing the binary layout of existing controller mapping files.
constexpr u32 PADEncodeAxisButton(u32 axis, bool negative, u32 threshold = 50) {
return 0x10000u | axis | (negative ? 0x80u : 0u) | (threshold << 8);
}
constexpr bool PADIsAxisButton(u32 binding) { return (binding & 0xffff0000u) == 0x10000u; }
constexpr u32 PADAxisButtonThreshold(u32 binding) { return (binding >> 8) & 0xffu; }
constexpr u32 PADAxisButtonAxis(u32 binding) { return binding & 0x7fu; }
constexpr bool PADAxisButtonNegative(u32 binding) { return (binding & 0x80u) != 0; }
constexpr u32 PADAxisButtonIdentity(u32 binding) {
return PADIsAxisButton(binding) ? (binding & ~0xff00u) : binding;
}
typedef struct PADAxisMapping { typedef struct PADAxisMapping {
PADSignedNativeAxis nativeAxis; PADSignedNativeAxis nativeAxis;
s32 nativeButton; s32 nativeButton;
+10 -39
View File
@@ -319,18 +319,6 @@ std::array<bool, PAD_CHANMAX> g_suppressLeftTrigger{};
std::array<bool, PAD_CHANMAX> g_suppressRightTrigger{}; std::array<bool, PAD_CHANMAX> g_suppressRightTrigger{};
bool is_mouse_scancode(const s32 scancode) { return scancode < PAD_KEY_INVALID; } bool is_mouse_scancode(const s32 scancode) { return scancode < PAD_KEY_INVALID; }
bool is_native_binding_pressed(SDL_Gamepad* gamepad, u32 binding) {
if (PADIsAxisButton(binding)) {
const u32 axis = PADAxisButtonAxis(binding);
const u32 threshold = PADAxisButtonThreshold(binding);
if (axis >= SDL_GAMEPAD_AXIS_COUNT || threshold < 1 || threshold > 100) return false;
int value = SDL_GetGamepadAxis(gamepad, static_cast<SDL_GamepadAxis>(axis));
if (PADAxisButtonNegative(binding)) value = -value;
return value > 0 && value * 100 >= static_cast<int>(threshold) * 32767;
}
return binding < SDL_GAMEPAD_BUTTON_COUNT &&
SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(binding));
}
bool is_mouse_button_pressed(const s32 scancode) { bool is_mouse_button_pressed(const s32 scancode) {
const int32_t buttonNum = -(scancode + 1); const int32_t buttonNum = -(scancode + 1);
if (buttonNum < 1 || buttonNum > 5) { if (buttonNum < 1 || buttonNum > 5) {
@@ -736,10 +724,10 @@ u32 PADRead(PADStatus* status) {
} }
status[i].err = PAD_ERR_NONE; status[i].err = PAD_ERR_NONE;
if (g_keyboardBindings[i].m_mappingsSet && SDL_GetKeyboardFocus() != nullptr) { if (g_keyboardBindings[i].m_mappingsSet) {
std::ranges::for_each( std::ranges::for_each(
g_keyboardBindings[i].m_buttonMapping, [&kbState, &numKeys, &i, &status](const PADKeyButtonBinding& mapping) { g_keyboardBindings[i].m_buttonMapping, [&kbState, &i, &status](const PADKeyButtonBinding& mapping) {
if (mapping.scancode > PAD_KEY_INVALID && mapping.scancode < numKeys && kbState[mapping.scancode]) { if (mapping.scancode > PAD_KEY_INVALID && kbState[mapping.scancode]) {
status[i].button |= mapping.padButton; status[i].button |= mapping.padButton;
} else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) { } else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) {
status[i].button |= mapping.padButton; status[i].button |= mapping.padButton;
@@ -800,7 +788,7 @@ u32 PADRead(PADStatus* status) {
status[i].triggerRight = static_cast<u8>(std::min(static_cast<int>(status[i].triggerRight) + tr, 255)); status[i].triggerRight = static_cast<u8>(std::min(static_cast<int>(status[i].triggerRight) + tr, 255));
} }
if (controller && !g_keyboardBindings[i].m_mappingsSet) { if (controller) {
EnsureMappingLoaded(controller); EnsureMappingLoaded(controller);
// Wii U Pro Controller raw D-pad fallback. SDL's HIDAPI Wii driver posts // Wii U Pro Controller raw D-pad fallback. SDL's HIDAPI Wii driver posts
@@ -847,7 +835,7 @@ u32 PADRead(PADStatus* status) {
bool rightTriggerSet = false; bool rightTriggerSet = false;
std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet, std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet,
&rightTriggerSet](const auto& mapping) { &rightTriggerSet](const auto& mapping) {
if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) { if (SDL_GetGamepadButton(controller->m_controller, static_cast<SDL_GamepadButton>(mapping.nativeButton))) {
status[i].button |= mapping.padButton; status[i].button |= mapping.padButton;
} }
@@ -864,7 +852,7 @@ u32 PADRead(PADStatus* status) {
if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) { if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) {
return; return;
} }
if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) { if (SDL_GetGamepadButton(controller->m_controller, static_cast<SDL_GamepadButton>(mapping.nativeButton))) {
status[i].button |= mapping.padButton; status[i].button |= mapping.padButton;
} }
@@ -958,17 +946,6 @@ u32 PADRead(PADStatus* status) {
Sint16 tl = std::max(static_cast<Sint16>(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_L)); Sint16 tl = std::max(static_cast<Sint16>(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_L));
Sint16 tr = std::max(static_cast<Sint16>(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_R)); Sint16 tr = std::max(static_cast<Sint16>(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_R));
// Games can read either the digital L/R bits or their analog pressure.
// An explicit button binding must drive both, otherwise the original
// L2/R2 axis still activates L/R even when it was rebound to L1/R1.
// Real GC pads retain independent analog travel and end-stop clicks.
if (!(controller->m_isGameCube ||
(SDL_GetGamepadType(controller->m_controller) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO &&
controller->m_pid == 0x2073))) {
if (leftTriggerSet) tl = (status[i].button & PAD_TRIGGER_L) != 0 ? 32767 : 0;
if (rightTriggerSet) tr = (status[i].button & PAD_TRIGGER_R) != 0 ? 32767 : 0;
}
if (controller->m_deadZones.emulateTriggers) { if (controller->m_deadZones.emulateTriggers) {
if (!leftTriggerSet && tl > controller->m_deadZones.leftTriggerActivationZone) { if (!leftTriggerSet && tl > controller->m_deadZones.leftTriggerActivationZone) {
status[i].button |= PAD_TRIGGER_L; status[i].button |= PAD_TRIGGER_L;
@@ -1013,13 +990,12 @@ void PADControlMotor(const u32 chan, const u32 cmd) {
} }
if (controller->m_isGameCube) { if (controller->m_isGameCube) {
if (cmd == PAD_MOTOR_STOP || cmd == PAD_MOTOR_STOP_HARD) { if (cmd == PAD_MOTOR_STOP) {
// Use an unambiguous motor-off request. The (0, 1) coast encoding aurora::input::controller_rumble(instance, 0, 1, 0);
// requires SDL's GameCube brake mode; other backends or an overridden
// hint interpret it as rumble and can leave the controller vibrating.
aurora::input::controller_rumble(instance, 0, 0, 0);
} else if (cmd == PAD_MOTOR_RUMBLE) { } else if (cmd == PAD_MOTOR_RUMBLE) {
aurora::input::controller_rumble(instance, 1, 1, 0); aurora::input::controller_rumble(instance, 1, 1, 0);
} else if (cmd == PAD_MOTOR_STOP_HARD) {
aurora::input::controller_rumble(instance, 0, 0, 0);
} }
} else { } else {
if (cmd == PAD_MOTOR_STOP) { if (cmd == PAD_MOTOR_STOP) {
@@ -1302,11 +1278,6 @@ BOOL PADSetKeyButtonBindings(const u32 port, PADKeyButtonBinding bindings[PAD_BU
} }
PADKeyButtonBinding* PADGetKeyButtonBindings(const u32 port, u32* buttonCount) { PADKeyButtonBinding* PADGetKeyButtonBindings(const u32 port, u32* buttonCount) {
PADInit();
if (!g_keyboardBindingsLoaded) {
g_keyboardBindingsLoaded = true;
load_keyboard_bindings();
}
if (port >= PAD_MAX_CONTROLLERS || !g_keyboardBindings[port].m_mappingsSet) { if (port >= PAD_MAX_CONTROLLERS || !g_keyboardBindings[port].m_mappingsSet) {
return nullptr; return nullptr;
} }
+6 -22
View File
@@ -168,12 +168,7 @@ struct RenderPass {
Range resolveUniformRange; Range resolveUniformRange;
std::array<u32, 3> resolveCopyFilterCoefficients{0, 64, 0}; std::array<u32, 3> resolveCopyFilterCoefficients{0, 64, 0};
Vec4<float> clearColorValue{0.f, 0.f, 0.f, 0.f}; Vec4<float> clearColorValue{0.f, 0.f, 0.f, 0.f};
// 1.f is the forward-Z "farthest" clear value; under UseReversedZ farthest is 0.f instead (see float clearDepthValue = 1.f;
// gx::clear_depth_value(), which the main render pass explicitly overrides this default with -
// any OTHER pass that keeps this default, e.g. an offscreen render-to-texture pass composited
// later, needs the same reversed-Z-aware value or its depth buffer starts "already nearest",
// failing every subsequent depth test and making whatever's drawn into it vanish).
float clearDepthValue = gx::UseReversedZ ? 0.f : 1.f;
CommandList commands; CommandList commands;
bool clearColor = true; bool clearColor = true;
bool clearDepth = true; bool clearDepth = true;
@@ -762,9 +757,7 @@ void begin_offscreen(uint32_t width, uint32_t height) {
.targetSize = {width, height, 1}, .targetSize = {width, height, 1},
.msaaSamples = 1, .msaaSamples = 1,
.clearColorValue = {0.f, 0.f, 0.f, 0.f}, .clearColorValue = {0.f, 0.f, 0.f, 0.f},
// See the RenderPass::clearDepthValue default's comment: this offscreen pass gets its own .clearDepthValue = 1.f,
// depth buffer, and the farthest clear value is 0.f, not 1.f, under UseReversedZ.
.clearDepthValue = gx::UseReversedZ ? 0.f : 1.f,
.clearColor = true, .clearColor = true,
.clearDepth = true, .clearDepth = true,
}; };
@@ -1417,19 +1410,10 @@ static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vec
switch (cmd.type) { switch (cmd.type) {
case CommandType::SetViewport: { case CommandType::SetViewport: {
const auto& vp = cmd.data.setViewport; const auto& vp = cmd.data.setViewport;
// WebGPU requires 0 <= minDepth <= maxDepth <= 1. vp.znear/vp.zfar are in GX's own distance // WebGPU requires 0 <= minDepth <= maxDepth <= 1, and the guest's (near, far) order is already
// terms (0 = near); under UseReversedZ the host depth-buffer storage direction is flipped // reproduced in clip space. Passing the raw swapped pair diverged per backend in release builds.
// (near = 1, far = 0), so this range has to be remapped through 1-x the same way the const float minDepth = std::clamp(std::min(vp.znear, vp.zfar), 0.0f, 1.0f);
// projection matrix, depth compare function, and clear value all are - a plain min/max clamp const float maxDepth = std::clamp(std::max(vp.znear, vp.zfar), 0.0f, 1.0f);
// (the previous code here) maps a *restricted* range (e.g. a viewport deliberately narrowed
// to force something to draw "in front of everything") to the wrong end of the buffer: what
// should land near the near-storage-extreme (1.0) instead lands near the far-storage-extreme
// (0.0), so anything else drawn afterward at its true depth wins the compare test and the
// "in front" geometry silently vanishes. A full [0,1] viewport is unaffected either way,
// which is why this only broke specific elements, not the whole scene. Matches upstream
// aurora's apply_viewport (lib/gfx/encoding.cpp) exactly.
const float minDepth = gx::UseReversedZ ? 1.0f - vp.zfar : vp.znear;
const float maxDepth = gx::UseReversedZ ? 1.0f - vp.znear : vp.zfar;
pass.SetViewport(vp.left, vp.top, vp.width, vp.height, minDepth, maxDepth); pass.SetViewport(vp.left, vp.top, vp.width, vp.height, minDepth, maxDepth);
} break; } break;
case CommandType::SetScissor: { case CommandType::SetScissor: {
+1 -1
View File
@@ -92,7 +92,7 @@ struct Params {
constexpr std::string_view ReversedZBody = R"( constexpr std::string_view ReversedZBody = R"(
fn gx_z24(depth: f32) -> u32 { fn gx_z24(depth: f32) -> u32 {
return min(u32(clamp(1.0 - depth, 0.0, 1.0) * 16777215.0 + 0.5), 0x00ffffffu); return min(u32(clamp(depth, 0.0, 1.0) * 16777216.0), 0x00ffffffu);
} }
)"sv; )"sv;
+1 -1
View File
@@ -137,7 +137,7 @@ fn gx_z24_at_coord(unclamped_coord: vec2i) -> u32 {
let tex_size = vec2i(textureDimensions(src)); let tex_size = vec2i(textureDimensions(src));
let coord = clamp(unclamped_coord, vec2i(0), tex_size - vec2i(1)); let coord = clamp(unclamped_coord, vec2i(0), tex_size - vec2i(1));
let depth = textureLoad(src, coord, 0); let depth = textureLoad(src, coord, 0);
return min(u32(clamp(1.0 - depth, 0.0, 1.0) * 16777215.0 + 0.5), 0x00ffffffu); return min(u32(clamp(depth, 0.0, 1.0) * 16777216.0), 0x00ffffffu);
} }
)"s )"s
: R"( : R"(
+4 -13
View File
@@ -1416,32 +1416,23 @@ static inline GXBlendFactor remove_dst_alpha_usage(GXBlendFactor fac) {
} }
} }
// GX_LEQUAL etc. describe "pass if this pixel is closer than/equal to what's stored" in GX's own
// distance terms, independent of how that distance is encoded as a host depth value. Under
// UseReversedZ the encoding is flipped (near=1, far=0), so "closer" now corresponds to a *larger*
// stored value, not a smaller one - the ordered compare functions (LESS/LEQUAL/GREATER/GEQUAL)
// must invert to match, or the depth test silently runs backwards (verified directly: this was
// the actual cause of a bug report after the projection/shader half of the reverse-Z fix
// eliminated the double-negation that used to accidentally keep the unreversed comparisons
// correct - LEQUAL now needs GreaterEqual, not LessEqual, once the encoding it's testing against
// is genuinely reversed). Matches upstream aurora's to_compare_function exactly.
static inline wgpu::CompareFunction to_compare_function(GXCompare func) { static inline wgpu::CompareFunction to_compare_function(GXCompare func) {
switch (func) { switch (func) {
DEFAULT_FATAL("invalid depth fn {}", underlying(func)); DEFAULT_FATAL("invalid depth fn {}", underlying(func));
case GX_NEVER: case GX_NEVER:
return wgpu::CompareFunction::Never; return wgpu::CompareFunction::Never;
case GX_LESS: case GX_LESS:
return UseReversedZ ? wgpu::CompareFunction::Greater : wgpu::CompareFunction::Less; return wgpu::CompareFunction::Less;
case GX_EQUAL: case GX_EQUAL:
return wgpu::CompareFunction::Equal; return wgpu::CompareFunction::Equal;
case GX_LEQUAL: case GX_LEQUAL:
return UseReversedZ ? wgpu::CompareFunction::GreaterEqual : wgpu::CompareFunction::LessEqual; return wgpu::CompareFunction::LessEqual;
case GX_GREATER: case GX_GREATER:
return UseReversedZ ? wgpu::CompareFunction::Less : wgpu::CompareFunction::Greater; return wgpu::CompareFunction::Greater;
case GX_NEQUAL: case GX_NEQUAL:
return wgpu::CompareFunction::NotEqual; return wgpu::CompareFunction::NotEqual;
case GX_GEQUAL: case GX_GEQUAL:
return UseReversedZ ? wgpu::CompareFunction::LessEqual : wgpu::CompareFunction::GreaterEqual; return wgpu::CompareFunction::GreaterEqual;
case GX_ALWAYS: case GX_ALWAYS:
return wgpu::CompareFunction::Always; return wgpu::CompareFunction::Always;
} }
+1 -8
View File
@@ -485,14 +485,7 @@ const gfx::TextureBind& get_texture(GXTexMapID id) noexcept;
void resolve_sampled_textures(const ShaderInfo& info) noexcept; void resolve_sampled_textures(const ShaderInfo& info) noexcept;
inline float clear_depth_value() { inline float clear_depth_value() {
// g_gxState.clearDepth is in GX's own distance terms (0 = near, larger = farther), independent of return std::min(static_cast<float>(g_gxState.clearDepth) / 16777216.f, 16777215.f / 16777216.f);
// how UseReversedZ encodes that as a host depth value - it must be re-mapped the same way the
// projection matrix and depth compare function are, or the buffer clears to the wrong extreme
// (verified directly: matches upstream aurora's clear_depth_value, which does this same inversion
// and was the second missing piece alongside to_compare_function's compare-op inversion).
const float normalizedDepth =
std::min(static_cast<float>(g_gxState.clearDepth) / 16777216.f, 16777215.f / 16777216.f);
return UseReversedZ ? (1.f - normalizedDepth) : normalizedDepth;
} }
inline bool render_target_has_alpha(GXPixelFmt pixelFmt) noexcept { return pixelFmt == GX_PF_RGBA6_Z24; } inline bool render_target_has_alpha(GXPixelFmt pixelFmt) noexcept { return pixelFmt == GX_PF_RGBA6_Z24; }
+8 -23
View File
@@ -993,13 +993,11 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
"\n let clip_base = select(clip_a, clip_b, use_b);" "\n let clip_base = select(clip_a, clip_b, use_b);"
"\n out.pos = vec4f(clip_base.xy + offset_ndc * clip_base.w, clip_base.zw);"; "\n out.pos = vec4f(clip_base.xy + offset_ndc * clip_base.w, clip_base.zw);";
} }
// The near/far depth correction used to be applied here per-vertex (out.pos.z = -out.pos.z for if constexpr (UseReversedZ) {
// reversed, or += out.pos.w for forward), redundantly on top of the same correction already vtxXfrAttrsPre += "\n out.pos.z = -out.pos.z;";
// folded into ubuf.proj by effective_projection() (shader_info.cpp) - applying it twice canceled } else {
// out for the common case (any draw where effective_projection() decides to flip), silently vtxXfrAttrsPre += "\n out.pos.z += out.pos.w;";
// making "reversed" Z behave identically to forward Z. It is now applied exactly once, in the }
// projection matrix alone (matching upstream aurora commit 1dde08fa: "Move depth correction to
// projection matrix"), so nothing needs to happen to out.pos.z here.
// GX rasterizes at a 7/12 pixel center when antialiasing is disabled, while WebGPU rasterizes at 1/2. // GX rasterizes at a 7/12 pixel center when antialiasing is disabled, while WebGPU rasterizes at 1/2.
vtxXfrAttrsPre += vtxXfrAttrsPre +=
"\n let gx_pixel_center_correction = " "\n let gx_pixel_center_correction = "
@@ -1467,14 +1465,7 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
textureDependency.texMapId, uvIn); textureDependency.texMapId, uvIn);
} }
// in.pos.z is the host NDC z (forward: 0=near/1=far; reversed: 1=near/0=far post-fix), but this std::string fogDepthExpr = UseReversedZ ? "in.pos.z" : "(1.0 - in.pos.z)";
// expression needs to produce GX's own native distance term (always 0=near/1=far, matching how
// g_gxState.clearDepth/clear_depth_value() are interpreted before their own UseReversedZ
// inversion) - forward already matches directly; reversed needs the same 1-x flip everything
// else reversed-Z-aware uses. This was backwards (verified directly against upstream aurora's
// identical expression in build_shader_source), which fed both fog density and the GX_ZT_ADD
// z-texture path the wrong distance value.
std::string fogDepthExpr = UseReversedZ ? "(1.0 - in.pos.z)" : "in.pos.z";
std::string fogZCoordExpr = std::string fogZCoordExpr =
fmt::format("u32(round(clamp({}, 0.0, 1.0) * 16777216.0))", fogDepthExpr); fmt::format("u32(round(clamp({}, 0.0, 1.0) * 16777216.0))", fogDepthExpr);
if (usesZTextureDepth) { if (usesZTextureDepth) {
@@ -1507,7 +1498,7 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
fragmentFn += fmt::format( fragmentFn += fmt::format(
"\n let oldZ = u32(round(clamp({0}, 0.0, 1.0) * 16777216.0));" "\n let oldZ = u32(round(clamp({0}, 0.0, 1.0) * 16777216.0));"
"\n ztexCoord = (ztexCoord + oldZ) & 0x00ffffffu;", "\n ztexCoord = (ztexCoord + oldZ) & 0x00ffffffu;",
UseReversedZ ? "(1.0 - in.pos.z)" : "in.pos.z"); UseReversedZ ? "in.pos.z" : "(1.0 - in.pos.z)");
} }
fragmentFn += "\n let ztexDepth = f32(ztexCoord) / 16777216.0;"; fragmentFn += "\n let ztexDepth = f32(ztexCoord) / 16777216.0;";
fogZCoordExpr = "ztexCoord"; fogZCoordExpr = "ztexCoord";
@@ -1648,13 +1639,7 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
" @builtin(frag_depth) depth: f32,\n" " @builtin(frag_depth) depth: f32,\n"
"};"; "};";
// ztexDepth is in GX's native distance terms (0=near/1=far, see fogDepthExpr's comment above), fragmentFn += fmt::format("\n let fragDepth = {}ztexDepth;", UseReversedZ ? "" : "1.0 - ");
// but frag_depth must be written in the same host NDC-z convention in.pos.z itself uses -
// forward matches directly (no change), reversed needs the same 1-x flip. This was backwards
// the same way fogDepthExpr was (verified by the same derivation, since aurora upstream has no
// directly equivalent line here to cross-check against - this z-texture-depth-output path
// appears to be specific to this fork).
fragmentFn += fmt::format("\n let fragDepth = {}ztexDepth;", UseReversedZ ? "1.0 - " : "");
fragmentReturnType = "FragmentOutput"; fragmentReturnType = "FragmentOutput";
fragmentReturn = fragmentReturn =
" var out: FragmentOutput;\n" " var out: FragmentOutput;\n"
+4 -12
View File
@@ -548,22 +548,14 @@ constexpr size_t kStagedUniformBytes =
96 + sizeof(Mat4x4<float>) + sizeof(Mat3x4<float>) * (MaxPostexMtx + MaxPnMtx); 96 + sizeof(Mat4x4<float>) + sizeof(Mat3x4<float>) * (MaxPostexMtx + MaxPnMtx);
// The host viewport always receives the normalized GX depth window (render_pass_impl clamps to minDepth <= maxDepth). // The host viewport always receives the normalized GX depth window (render_pass_impl clamps to minDepth <= maxDepth).
//
// Folds the near/far depth correction the vertex shader used to apply per-vertex directly into the
// projection matrix instead (matching upstream aurora commit 1dde08fa, "Move depth correction to
// projection matrix") - valid because the correction is a linear combination of the z/w rows, so
// applying it once here to the row is equivalent to applying it once per-vertex to the dot product,
// and it must be applied exactly once: doing it here AND in the shader (the previous bug) canceled
// the negation out for `flip`, silently making "reversed" Z behave identically to forward Z.
// `flip` decides which of the two single-application forms this draw needs: true bakes in the
// reversed-Z inversion (z' = -z), false bakes in the forward-Z near/far combination (z' = z + w) -
// exactly one always applies, never both, and never neither.
static Mat4x4<float> effective_projection() noexcept { static Mat4x4<float> effective_projection() noexcept {
const auto& vp = g_gxState.renderViewport; const auto& vp = g_gxState.renderViewport;
const bool flip = (vp.znear <= vp.zfar) == UseReversedZ; const bool flip = (vp.znear <= vp.zfar) == UseReversedZ;
Mat4x4<float> proj = g_gxState.proj; Mat4x4<float> proj = g_gxState.proj;
for (size_t i = 0; i < 4; ++i) { if (flip) {
proj.m2.m[i] = flip ? -proj.m2.m[i] : (proj.m2.m[i] + proj.m3.m[i]); for (size_t i = 0; i < 4; ++i) {
proj.m2.m[i] = -(proj.m2.m[i] + proj.m3.m[i]);
}
} }
return proj; return proj;
} }
-349
View File
@@ -1,349 +0,0 @@
# Building WiiCompiled and Retro Rewind on macOS
This guide covers building **WiiCompiled** (base game) and **Retro Rewind** from source on macOS for Apple Silicon (`arm64`). Follow these instructions to compile the native executables directly.
> [!NOTE]
> If you only want to build the base game (**WiiCompiled**), look for sections marked **`(Skip if only building WiiCompiled)`** to bypass Retro Rewind and online payload steps.
---
## 1. Prerequisites
### System Requirements
- **Hardware**: Apple Silicon Mac (M1/M2/M3/M4)
- **Operating System**: macOS 14 (Sonoma) or later
- **Xcode Command Line Tools**:
```bash
xcode-select --install
```
### Toolchain Dependencies
Install the required tools using [Homebrew](https://brew.sh):
```bash
brew install cmake ninja
brew install --cask dotnet-sdk@8
```
Verify that Clang, CMake, Ninja, and the .NET 8 runtime are available:
```bash
clang --version
cmake --version
ninja --version
dotnet --list-runtimes # Must list Microsoft.NETCore.App 8.x
```
---
## 2. Required Game and Mod Assets
Due to legal requirements, no proprietary Nintendo assets or code are included in this repository. You must provide your own legally dumped game files.
1. **Mario Kart Wii PAL (`RMCP01`) Disc Image** *(Required)*:
- Supported formats: `.iso`, `.wbfs`, `.ciso`, `.rvz`, `.gcm`, `.gcz`.
2. **nodtool** *(Required for disc extraction)*:
- Download the macOS Apple Silicon binary of [nodtool](https://github.com/encounter/nod/releases):
```bash
curl -fsSL "https://github.com/encounter/nod/releases/download/v2.0.0-alpha.10/nodtool-macos-arm64" -o nodtool
chmod +x nodtool
```
3. **Retro Rewind Distribution** *(Skip if only building WiiCompiled)*:
- Download the [Retro Rewind](https://wiki.tockdom.com/wiki/Retro_Rewind) release package. You will need the `RetroRewind6` folder (which contains `Binaries/Code.pul`).
4. **Retro-WFC Payload** *(Skip if only building WiiCompiled or building offline)*:
- Required for online multiplayer on Retro Rewind. Downloaded during setup from `https://rwfc.net/api/wfc/payload?g=RMCPD00`.
---
## 3. Step 1: Extract Disc Assets
Extract your clean PAL `RMCP01` disc into the `Assets/` directory of the repository:
```bash
# Using nodtool directly into a temporary scratch directory
mkdir -p /tmp/mkw-extract
./nodtool extract /path/to/RMCP01.iso /tmp/mkw-extract
# Copy extracted assets into the repository Assets directory
rm -rf Assets/DATA/files Assets/DATA/sys
mkdir -p Assets/DATA
cp /tmp/mkw-extract/*/sys/main.dol Assets/main.dol
cp /tmp/mkw-extract/*/files/rel/StaticR.rel Assets/StaticR.rel
cp -R /tmp/mkw-extract/*/files Assets/DATA/files
cp -R /tmp/mkw-extract/*/sys Assets/DATA/sys
# Clean up temporary files
rm -rf /tmp/mkw-extract
```
> [!TIP]
> Alternatively, you can use the repository's helper script:
> ```bash
> Launcher/macos/extract-disc.command --game /path/to/RMCP01.iso --assets-dir Assets --nodtool ./nodtool
> ```
### Verify Extracted Asset Hashes
Confirm that the extracted files match the expected clean PAL revision:
```bash
shasum -a 256 Assets/main.dol Assets/StaticR.rel
```
- `Assets/main.dol`: `80d18895b39c63bd80f457398bfcbb91b7d16ac116a41a88967e954080155b05`
- `Assets/StaticR.rel`: `16d9d146112541fefea701ecb5bc1a496f9d50e4a752fbb5b6778e7c6399f67d`
---
## 4. Step 2: Build the Translator CLI
Compile the static recompiler CLI:
```bash
dotnet build translator/src/Translator.Cli/Translator.Cli.csproj -c Release
```
Define a shell function to invoke the translator (ensuring paths with spaces are handled safely):
```bash
translator() {
dotnet "$(pwd)/translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll" "$@"
}
```
---
## 5. Step 3: Translation
### A. Translate Base Game Functions
```bash
mkdir -p generated/functions build/base
translator translate-recursive 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 "https://rwfc.net/api/wfc/payload?g=RMCPD00" \
-o build/retro-wfc/binary/payload.RMCPD00.bin
# 2. Run the automated build with the payload directory:
Launcher/local-build-macos.command \
--profile both \
--output-dir build-macos/Products \
--base-output-dir build-macos/Products \
--game /path/to/RMCP01.iso \
--nodtool ./nodtool \
--retro-rewind-package-dir /path/to/RetroRewind6 \
--retro-wfc-offline-dir build/retro-wfc
```
### Building Both (Offline, Skipping Payload):
```bash
Launcher/local-build-macos.command \
--profile both \
--output-dir build-macos/Products \
--base-output-dir build-macos/Products \
--game /path/to/RMCP01.iso \
--nodtool ./nodtool \
--retro-rewind-package-dir /path/to/RetroRewind6 \
--skip-retro-wfc-payload
```
When finished, the compiled executables reside in `native-build-macos/` and the bundled `.app` packages are placed in `build-macos/Products/`.
+1 -1
View File
@@ -58,7 +58,7 @@ profiles:
module_link_base: 0x803992E0 module_link_base: 0x803992E0
output: build/mods/retro_rewind_full_cpp output: build/mods/retro_rewind_full_cpp
enable_retro_wfc: true enable_retro_wfc: true
retro_wfc_payload: https://rwfc.net/api/wfc/payload?g=RMCPD00 retro_wfc_payload: http://nas.play.rwfc.net/payload?g=RMCPD00
retro_wfc_legacy_bootstrap_hook: 0x800ED6E8 retro_wfc_legacy_bootstrap_hook: 0x800ED6E8
riivolution: riivolution:
xml: xml/RetroRewind6.xml xml: xml/RetroRewind6.xml
-12
View File
@@ -277,23 +277,11 @@ target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17) target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests) 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") 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_include_directories(mkw_nand_settings_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17) target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests) 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 # The input expression engine is self-contained, so it can be exercised without
# linking the runtime or SDL. # linking the runtime or SDL.
add_executable(mkw_input_expr_tests add_executable(mkw_input_expr_tests
+6 -17
View File
@@ -48,19 +48,8 @@ struct NativeButtonItem {
uint32_t nativeButton; uint32_t nativeButton;
}; };
inline constexpr auto kNativeButtons = std::to_array<NativeButtonItem>({ inline constexpr std::array<NativeButtonItem, SDL_GAMEPAD_BUTTON_COUNT + 1> kNativeButtons = {{
{"disabled", "Unmapped", PAD_NATIVE_BUTTON_DISABLED}, {"unmapped", "Unmapped / analog trigger", PAD_NATIVE_BUTTON_INVALID},
{"left_trigger", "Left trigger (LT / L2)", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFT_TRIGGER, false)},
{"right_trigger", "Right trigger (RT / R2)", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, false)},
{"left_stick_left", "Left stick left", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTX, true)},
{"left_stick_right", "Left stick right", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTX, false)},
{"left_stick_up", "Left stick up", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTY, true)},
{"left_stick_down", "Left stick down", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTY, false)},
{"right_stick_left", "Right stick left", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTX, true)},
{"right_stick_right", "Right stick right", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTX, false)},
{"right_stick_up", "Right stick up", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTY, true)},
{"right_stick_down", "Right stick down", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTY, false)},
{"unmapped", "Default", PAD_NATIVE_BUTTON_INVALID},
{"south", "South (A / Cross)", SDL_GAMEPAD_BUTTON_SOUTH}, {"south", "South (A / Cross)", SDL_GAMEPAD_BUTTON_SOUTH},
{"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST}, {"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST},
{"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST}, {"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST},
@@ -87,7 +76,7 @@ inline constexpr auto kNativeButtons = std::to_array<NativeButtonItem>({
{"misc4", "Misc 4 / GC R click", SDL_GAMEPAD_BUTTON_MISC4}, {"misc4", "Misc 4 / GC R click", SDL_GAMEPAD_BUTTON_MISC4},
{"misc5", "Misc 5", SDL_GAMEPAD_BUTTON_MISC5}, {"misc5", "Misc 5", SDL_GAMEPAD_BUTTON_MISC5},
{"misc6", "Misc 6", SDL_GAMEPAD_BUTTON_MISC6}, {"misc6", "Misc 6", SDL_GAMEPAD_BUTTON_MISC6},
}); }};
inline std::string TrimToken(std::string_view token) { inline std::string TrimToken(std::string_view token) {
const size_t begin = token.find_first_not_of(" \t"); const size_t begin = token.find_first_not_of(" \t");
@@ -99,7 +88,7 @@ inline std::string TrimToken(std::string_view token) {
} }
inline const NativeButtonItem* FindNativeButton(std::string_view configName) { inline const NativeButtonItem* FindNativeButton(std::string_view configName) {
const std::string name = TrimToken(configName.substr(0, configName.find('@'))); const std::string name = TrimToken(configName);
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(), const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
[&](const NativeButtonItem& item) { return name == item.configName; }); [&](const NativeButtonItem& item) { return name == item.configName; });
return it == kNativeButtons.end() ? nullptr : &*it; return it == kNativeButtons.end() ? nullptr : &*it;
@@ -108,8 +97,8 @@ inline const NativeButtonItem* FindNativeButton(std::string_view configName) {
// Falls back to the "unmapped" entry so callers always have a label to draw. // Falls back to the "unmapped" entry so callers always have a label to draw.
inline const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) { inline const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) {
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(), const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
[&](const NativeButtonItem& item) { return PADAxisButtonIdentity(nativeButton) == PADAxisButtonIdentity(item.nativeButton); }); [&](const NativeButtonItem& item) { return nativeButton == item.nativeButton; });
return it == kNativeButtons.end() ? *FindNativeButton("unmapped") : *it; return it == kNativeButtons.end() ? kNativeButtons.front() : *it;
} }
inline const GameCubeButtonItem* FindGameCubeButton(std::string_view configKey) { inline const GameCubeButtonItem* FindGameCubeButton(std::string_view configKey) {
+1 -14
View File
@@ -1,7 +1,6 @@
#pragma once #pragma once
#include "runtime_config.h" #include "runtime_config.h"
#include "nand_settings.h"
#include "runtime_log.h" #include "runtime_log.h"
#include "system_bridge.h" #include "system_bridge.h"
@@ -164,7 +163,7 @@ inline std::filesystem::path CreateManagedNandRoot() {
return root; return root;
} }
inline std::filesystem::path ResolveNandRootPath() { inline std::filesystem::path DiscoverNandRootPath() {
const std::string configPath = RuntimeConfigFile::NandRoot(); const std::string configPath = RuntimeConfigFile::NandRoot();
if (!configPath.empty()) { if (!configPath.empty()) {
const auto path = ResolveConfiguredPath(configPath); const auto path = ResolveConfiguredPath(configPath);
@@ -180,16 +179,4 @@ inline std::filesystem::path ResolveNandRootPath() {
return CreateManagedNandRoot(); 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 } // namespace RuntimeNandPath
-59
View File
@@ -1,59 +0,0 @@
#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
+2 -153
View File
@@ -1,9 +1,6 @@
#pragma once #pragma once
#include <array> #include <array>
#include <atomic>
#include <chrono>
#include <ctime>
#include <cstdint> #include <cstdint>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
@@ -12,23 +9,14 @@
#include <string> #include <string>
#include <utility> #include <utility>
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif
namespace RuntimeNandSettings { namespace RuntimeNandSettings {
using Settings = std::map<std::string, std::string>; 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. // Wii setting.txt is a 256-byte buffer encrypted with a rotating XOR key.
inline std::optional<Settings> Read(const std::filesystem::path& nandRoot) { inline std::optional<Settings> Read(const std::filesystem::path& nandRoot) {
std::ifstream input(FilePath(nandRoot), std::ios::binary); std::ifstream input(nandRoot / "title/00000001/00000002/data/setting.txt",
std::ios::binary);
std::array<uint8_t, 256> bytes{}; std::array<uint8_t, 256> bytes{};
if (!input.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) { if (!input.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) {
return std::nullopt; return std::nullopt;
@@ -78,143 +66,4 @@ inline bool HasIdentity(const Settings& settings) {
return true; 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 } // namespace RuntimeNandSettings
-26
View File
@@ -1,26 +0,0 @@
#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
+13 -29
View File
@@ -78,38 +78,22 @@ bool ProcessSleepTimers(CpuContext* cpu)
{ {
using Clock = std::chrono::steady_clock; using Clock = std::chrono::steady_clock;
// Pop and process ONE due timer at a time, straight from the shared table. Resuming a std::vector<SleepTimerEntry> dueTimers;
// sleeper re-enters the scheduler (OSResumeThread -> SelectThread) and can switch fibers
// away from this call. Timers that had already been popped into a private list would then
// sit on the suspended fiber's stack with their threads parked and no entry in the table:
// exactly the "park-shaped with no pending wake timer" strand the reconciler below heals
// 100ms late, followed by a "sleep-timer stale" drop when this fiber finally resumes.
// Leaving unprocessed timers in the table keeps them visible to every other pump (idle
// loop, other threads' SelectThread) while this one is switched away.
bool processedAny = false;
constexpr size_t kMaxTimersPerCall = 64;
size_t processedCount = 0;
const auto now = Clock::now(); const auto now = Clock::now();
while (processedCount < kMaxTimersPerCall) { {
SleepTimerEntry timer{0, {}}; std::lock_guard<std::mutex> lock(gSleepTimerMutex);
bool found = false; auto it = gSleepTimers.begin();
{ while (it != gSleepTimers.end()) {
std::lock_guard<std::mutex> lock(gSleepTimerMutex); if (it->deadline > now) {
for (auto it = gSleepTimers.begin(); it != gSleepTimers.end(); ++it) { ++it;
if (it->deadline <= now) { continue;
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; const uint32_t threadPtr = timer.threadPtr;
if (threadPtr == 0 || if (threadPtr == 0 ||
!Memory::Contains(threadPtr + kThreadSuspendOffset, sizeof(uint32_t))) { !Memory::Contains(threadPtr + kThreadSuspendOffset, sizeof(uint32_t))) {
@@ -235,7 +219,7 @@ bool ProcessSleepTimers(CpuContext* cpu)
} }
} }
return processedAny; return !dueTimers.empty();
} }
} // namespace OsHleInternal } // namespace OsHleInternal
+6 -4
View File
@@ -1,7 +1,6 @@
#include "hle_stubs.h" #include "hle_stubs.h"
#include "console_identity.h" #include "console_identity.h"
#include "sc_serial_contract.h"
#include <cstdlib> #include <cstdlib>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
@@ -98,9 +97,12 @@ PPC_NATIVE_OVERRIDE(801B2424, SCGetProductCode_HLE, uint32_t, (), ());
extern "C" uint32_t SCGetProductSN_HLE(uint32_t serialAddress) extern "C" uint32_t SCGetProductSN_HLE(uint32_t serialAddress)
{ {
const std::string& serial = RuntimeConsoleIdentity::Current().serial; const std::string& serial = RuntimeConsoleIdentity::Current().serial;
return RuntimeScSerial::Write(serial, serialAddress, if (!serialAddress || !Memory::Contains(serialAddress, serial.size() + 1)) {
[](uint32_t address, size_t size) { return Memory::Contains(address, size); }, return 0;
[](uint32_t address, uint32_t value) { Memory::Write32(address, value); }); }
std::memcpy(Memory::GetPointer(serialAddress, serial.size() + 1),
serial.c_str(), serial.size() + 1);
return 1;
} }
PPC_NATIVE_OVERRIDE(801B2460, SCGetProductSN_HLE, uint32_t, (uint32_t serialAddress), (serialAddress)); PPC_NATIVE_OVERRIDE(801B2460, SCGetProductSN_HLE, uint32_t, (uint32_t serialAddress), (serialAddress));
-3
View File
@@ -93,9 +93,6 @@ extern "C" int32_t NANDOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint32_t
const std::filesystem::path hostPath = TranslateNandPath(path); 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 // 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 // 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. // sub-ranges, e.g. ghost saves at a non-zero offset). New files still create in place.
-2
View File
@@ -411,8 +411,6 @@ extern "C" int32_t NANDSafeOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint
if (mode == 1) { if (mode == 1) {
// Read-only safe open reads the original in place; the library builds no scratch // Read-only safe open reads the original in place; the library builds no scratch
// copy for this case. // copy for this case.
if (const auto result = NandCheckSystemSaveRead("NANDSafeOpen", hostPath, mode))
return *result;
FILE* file = NandFopen(hostPath, "rb"); FILE* file = NandFopen(hostPath, "rb");
if (!file && IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) { if (!file && IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) {
file = NandFopen(hostPath, "rb"); file = NandFopen(hostPath, "rb");
-20
View File
@@ -411,26 +411,6 @@ bool IsFaceLibResourcePath(const char* path) {
return std::strcmp(path, "/shared2/menu/FaceLib/RFL_Res.dat") == 0; 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 // Create directories recursively
bool CreateDirectoryPath(const std::filesystem::path& path) { bool CreateDirectoryPath(const std::filesystem::path& path) {
if (path.empty()) { if (path.empty()) {
-7
View File
@@ -9,7 +9,6 @@
#include "hle/runtime_parse_helpers.h" #include "hle/runtime_parse_helpers.h"
#include "memory.h" #include "memory.h"
#include "nand_path.h" #include "nand_path.h"
#include "nand_save_probe.h"
#include "hle/net/network.h" #include "hle/net/network.h"
#include "recomp_mod_loader.h" #include "recomp_mod_loader.h"
#include "runtime_config.h" #include "runtime_config.h"
@@ -27,7 +26,6 @@
#include <deque> #include <deque>
#include <map> #include <map>
#include <mutex> #include <mutex>
#include <optional>
#include <vector> #include <vector>
#include <filesystem> #include <filesystem>
#include <string> #include <string>
@@ -58,11 +56,6 @@ constexpr uint32_t kNandTitleIdLo = 0x524D4350; // "RMCP" fallback
void LogNandError(const char* func, const char* fmt, ...); void LogNandError(const char* func, const char* fmt, ...);
void LogNandWarning(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 // File Descriptor Management
// ============================================================================ // ============================================================================
-3
View File
@@ -391,9 +391,6 @@ extern "C" int32_t NAND_IOS_Open_HLE(uint32_t pathPtr, uint32_t mode) {
// It's a NAND file path // It's a NAND file path
const std::filesystem::path hostPath = TranslateNandPath(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 // Seed FaceLib resources before the existence check so every open mode can
// still find them on a fresh managed NAND. // still find them on a fresh managed NAND.
+1 -6
View File
@@ -94,12 +94,7 @@ double ReadInput(SDL_Gamepad* gamepad, const std::string& name) {
// Fall back to this project's own positional names, so a binding written // Fall back to this project's own positional names, so a binding written
// here does not have to use Dolphin vocabulary. // here does not have to use Dolphin vocabulary.
if (const auto* native = ControllerNames::FindNativeButton(name)) { if (const auto* native = ControllerNames::FindNativeButton(name)) {
if (PADIsAxisButton(native->nativeButton)) { if (native->nativeButton != PAD_NATIVE_BUTTON_INVALID) {
const auto axis = static_cast<SDL_GamepadAxis>(PADAxisButtonAxis(native->nativeButton));
const double sign = PADAxisButtonNegative(native->nativeButton) ? -1.0 : 1.0;
return std::clamp(SDL_GetGamepadAxis(gamepad, axis) / 32767.0 * sign, 0.0, 1.0);
}
if (native->nativeButton < SDL_GAMEPAD_BUTTON_COUNT) {
return SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(native->nativeButton)) ? 1.0 return SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(native->nativeButton)) ? 1.0
: 0.0; : 0.0;
} }
+42 -307
View File
@@ -20,7 +20,6 @@
#include <algorithm> #include <algorithm>
#include <atomic> #include <atomic>
#include <cctype> #include <cctype>
#include <charconv>
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
@@ -187,18 +186,6 @@ void LimitResolutionForFrameRate() {
using ControllerNames::FindNativeButton; using ControllerNames::FindNativeButton;
uint32_t ConfiguredNativeButton(const NativeButtonItem& item, const std::string& token) {
if (!PADIsAxisButton(item.nativeButton)) return item.nativeButton;
const size_t separator = token.find('@');
if (separator == std::string::npos) return item.nativeButton;
uint32_t threshold = 0;
const char* end = token.data() + token.size();
const auto parsed = std::from_chars(token.data() + separator + 1, end, threshold);
if (parsed.ec != std::errc{} || parsed.ptr != end || threshold < 1 || threshold > 100)
return item.nativeButton;
return PADAxisButtonIdentity(item.nativeButton) | (threshold << 8);
}
struct ControllerBindingPair { struct ControllerBindingPair {
std::string primary; std::string primary;
std::string secondary; std::string secondary;
@@ -217,13 +204,6 @@ ControllerBindingPair SplitControllerBinding(const std::string& value) {
using ControllerNames::NativeButtonForValue; using ControllerNames::NativeButtonForValue;
std::string NativeBindingConfig(uint32_t binding) {
std::string value = NativeButtonForValue(binding).configName;
if (PADIsAxisButton(binding)) value += '@' + std::to_string(PADAxisButtonThreshold(binding));
return value;
}
void SetTopBarVisible(bool visible) { void SetTopBarVisible(bool visible) {
if (g_topBarVisible == visible) { if (g_topBarVisible == visible) {
return; return;
@@ -262,7 +242,7 @@ void ApplyConfiguredMappings() {
} }
const ControllerBindingPair binding = SplitControllerBinding(*configured); const ControllerBindingPair binding = SplitControllerBinding(*configured);
if (const NativeButtonItem* native = FindNativeButton(binding.primary)) { if (const NativeButtonItem* native = FindNativeButton(binding.primary)) {
PADSetButtonMapping(port, PADButtonMapping{ConfiguredNativeButton(*native, binding.primary), kControllerButtons[i].padButton}); PADSetButtonMapping(port, PADButtonMapping{native->nativeButton, kControllerButtons[i].padButton});
} else { } else {
RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey
<< " button '" << binding.primary << "'" << std::endl; << " button '" << binding.primary << "'" << std::endl;
@@ -270,7 +250,7 @@ void ApplyConfiguredMappings() {
uint32_t altNative = PAD_NATIVE_BUTTON_INVALID; uint32_t altNative = PAD_NATIVE_BUTTON_INVALID;
if (!binding.secondary.empty()) { if (!binding.secondary.empty()) {
if (const NativeButtonItem* native = FindNativeButton(binding.secondary)) { if (const NativeButtonItem* native = FindNativeButton(binding.secondary)) {
altNative = ConfiguredNativeButton(*native, binding.secondary); altNative = native->nativeButton;
} else { } else {
RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey
<< " secondary button '" << binding.secondary << "'" << std::endl; << " secondary button '" << binding.secondary << "'" << std::endl;
@@ -415,219 +395,6 @@ void DrawWiiRemoteSettings(uint32_t selectedGamePort) {
ImGui::EndMenu(); ImGui::EndMenu();
} }
const char* KeyBindingName(int scancode) {
switch (scancode) {
case PAD_KEY_MOUSE_LEFT: return "Mouse left";
case PAD_KEY_MOUSE_RIGHT: return "Mouse right";
case PAD_KEY_MOUSE_MIDDLE: return "Mouse middle";
case PAD_KEY_MOUSE_X1: return "Mouse side 1";
case PAD_KEY_MOUSE_X2: return "Mouse side 2";
case PAD_KEY_INVALID: return "Unmapped";
default:
return scancode >= 0 && scancode < SDL_SCANCODE_COUNT
? SDL_GetScancodeName(static_cast<SDL_Scancode>(scancode)) : "Unknown";
}
}
enum class RebindKind { KeyboardButton, KeyboardAxis, Controller };
struct RebindState {
bool active = false;
bool openPopup = false;
RebindKind kind{};
uint32_t port = 0;
uint16_t target = 0;
bool secondary = false;
SDL_JoystickID instance = 0;
Clock::time_point deadline{};
std::string label;
std::array<bool, SDL_SCANCODE_COUNT> keys{};
uint32_t mouse = 0;
std::array<bool, SDL_GAMEPAD_BUTTON_COUNT> buttons{};
std::array<bool, SDL_GAMEPAD_AXIS_COUNT> axesReady{};
} g_rebind;
void BeginRebind(RebindKind kind, uint16_t target, const char* label, bool secondary = false) {
g_rebind = {};
g_rebind.active = true;
g_rebind.openPopup = true;
g_rebind.kind = kind;
g_rebind.port = static_cast<uint32_t>(g_controllerPort);
g_rebind.target = target;
g_rebind.secondary = secondary;
g_rebind.label = label;
g_rebind.deadline = Clock::now() + std::chrono::seconds(10);
int count = 0;
const bool* keys = SDL_GetKeyboardState(&count);
std::copy_n(keys, std::min(count, static_cast<int>(g_rebind.keys.size())), g_rebind.keys.begin());
g_rebind.mouse = SDL_GetMouseState(nullptr, nullptr);
const int index = PADGetIndexForPort(g_rebind.port);
if (kind == RebindKind::Controller && index >= 0) {
if (auto* pad = PADGetSDLGamepadForIndex(index)) {
g_rebind.instance = SDL_GetGamepadID(pad);
for (int i = 0; i < SDL_GAMEPAD_BUTTON_COUNT; ++i)
g_rebind.buttons[i] = SDL_GetGamepadButton(pad, static_cast<SDL_GamepadButton>(i));
for (int i = 0; i < SDL_GAMEPAD_AXIS_COUNT; ++i)
g_rebind.axesReady[i] = std::abs(static_cast<int>(SDL_GetGamepadAxis(pad, static_cast<SDL_GamepadAxis>(i)))) < 8000;
}
}
}
void CompleteRebind(uint32_t value) {
const auto& capture = g_rebind;
if (capture.kind == RebindKind::Controller) {
const int index = PADGetIndexForPort(capture.port);
auto* pad = index >= 0 ? PADGetSDLGamepadForIndex(index) : nullptr;
if (pad == nullptr || SDL_GetGamepadID(pad) != capture.instance) {
g_rebind.active = false;
return;
}
if (capture.secondary) PADSetAltButtonMapping(capture.port, {value, capture.target});
else PADSetButtonMapping(capture.port, {value, capture.target});
uint32_t count = 0, altCount = 0;
auto* primary = PADGetButtonMappings(capture.port, &count);
auto* alternate = PADGetAltButtonMappings(capture.port, &altCount);
uint32_t primaryValue = PAD_NATIVE_BUTTON_INVALID, alternateValue = PAD_NATIVE_BUTTON_INVALID;
for (uint32_t i = 0; i < count; ++i)
if (primary[i].padButton == capture.target) primaryValue = primary[i].nativeButton;
for (uint32_t i = 0; i < altCount; ++i)
if (alternate[i].padButton == capture.target) alternateValue = alternate[i].nativeButton;
std::string config = NativeBindingConfig(primaryValue);
if (alternateValue != PAD_NATIVE_BUTTON_INVALID) config += ',' + NativeBindingConfig(alternateValue);
for (size_t i = 0; i < kControllerButtons.size(); ++i)
if (kControllerButtons[i].padButton == capture.target) RuntimeConfigFile::SetControllerButton(i, config);
} else if (capture.kind == RebindKind::KeyboardButton) {
PADSetKeyButtonBinding(capture.port, {static_cast<int32_t>(value), capture.target});
} else {
PADSetKeyAxisBinding(capture.port, {static_cast<int32_t>(value), capture.target, 1});
}
PADSerializeMappings();
g_rebind.active = false;
}
void DrawRebindPrompt() {
if (g_rebind.openPopup) {
ImGui::OpenPopup("Rebind input");
g_rebind.openPopup = false;
}
if (!ImGui::BeginPopupModal("Rebind input", &g_rebind.active, ImGuiWindowFlags_AlwaysAutoResize)) {
g_rebind.active = false;
return;
}
if (g_rebind.active) {
ImGui::Text("Rebind: %s", g_rebind.label.c_str());
ImGui::TextUnformatted(g_rebind.kind == RebindKind::Controller
? "Press a controller button, pull a trigger, or move a stick."
: "Press a keyboard key or click a mouse button.");
ImGui::TextUnformatted("Release any held input first. Backspace or Delete clears the mapping.");
ImGui::TextUnformatted("Escape can be bound. F10 is reserved for settings.");
const float remaining = std::chrono::duration<float>(g_rebind.deadline - Clock::now()).count();
ImGui::Text("Unmapped in %d seconds", std::max(0, static_cast<int>(std::ceil(remaining))));
const bool clear = ImGui::Button("Clear mapping");
ImGui::SameLine();
if (ImGui::Button("Cancel")) g_rebind.active = false;
// UI clicks must not become mouse bindings (buttons activate on release).
const bool overControl = ImGui::IsAnyItemHovered();
if (g_rebind.active && (clear || remaining <= 0.0f)) {
CompleteRebind(g_rebind.kind == RebindKind::Controller ? PAD_NATIVE_BUTTON_DISABLED
: static_cast<uint32_t>(PAD_KEY_INVALID));
} else if (g_rebind.active && SDL_GetKeyboardFocus() != nullptr && g_rebind.kind != RebindKind::Controller) {
int count = 0;
const bool* keys = SDL_GetKeyboardState(&count);
for (int i = 1; i < std::min(count, static_cast<int>(SDL_SCANCODE_COUNT)) && g_rebind.active; ++i) {
if (keys[i] && !g_rebind.keys[i] && i != SDL_SCANCODE_F10) CompleteRebind(i);
g_rebind.keys[i] = keys[i];
}
const uint32_t mouse = SDL_GetMouseState(nullptr, nullptr);
for (int i = 1; i <= 5 && g_rebind.active; ++i)
if (!overControl && (mouse & ~g_rebind.mouse & (1u << (i - 1))) != 0) CompleteRebind(static_cast<uint32_t>(-i - 1));
g_rebind.mouse = mouse;
} else if (g_rebind.active && SDL_GetKeyboardFocus() != nullptr && g_rebind.kind == RebindKind::Controller) {
auto* pad = SDL_GetGamepadFromID(g_rebind.instance);
if (pad != nullptr) {
for (int i = 0; i < SDL_GAMEPAD_BUTTON_COUNT && g_rebind.active; ++i) {
const bool pressed = SDL_GetGamepadButton(pad, static_cast<SDL_GamepadButton>(i));
if (pressed && !g_rebind.buttons[i]) CompleteRebind(i);
g_rebind.buttons[i] = pressed;
}
for (int i = 0; i < SDL_GAMEPAD_AXIS_COUNT && g_rebind.active; ++i) {
const int value = SDL_GetGamepadAxis(pad, static_cast<SDL_GamepadAxis>(i));
if (std::abs(value) < 8000) g_rebind.axesReady[i] = true;
if (g_rebind.axesReady[i] && std::abs(value) >= 16384)
CompleteRebind(PADEncodeAxisButton(i, value < 0));
}
}
}
}
if (!g_rebind.active) ImGui::CloseCurrentPopup();
ImGui::EndPopup();
}
void DrawKeyBinding(const char* label, int scancode, RebindKind kind, uint16_t target) {
const std::string caption = std::string(KeyBindingName(scancode)) + "##binding";
if (ImGui::Button(caption.c_str(), ImVec2(220.0f, 0.0f))) BeginRebind(kind, target, label);
ImGui::SameLine();
ImGui::TextUnformatted(label);
}
bool DrawKeyboardSettings(uint32_t port) {
uint32_t count = 0;
auto* buttons = PADGetKeyButtonBindings(port, &count);
bool enabled = buttons != nullptr;
bool usePreset = false;
if (ImGui::Checkbox("Keyboard and mouse", &enabled)) {
PADSetKeyboardActive(port, enabled);
PADSerializeMappings();
buttons = PADGetKeyButtonBindings(port, &count);
usePreset = enabled && std::all_of(buttons, buttons + count, [](const auto& binding) {
return binding.scancode == PAD_KEY_INVALID;
});
}
if (!enabled) return false;
ImGui::TextDisabled("Replaces the gamepad on this port. F10 opens settings.");
if (ImGui::Button("Use WASD + mouse preset") || usePreset) {
const std::array<int, PAD_BUTTON_COUNT> keys = {
PAD_KEY_MOUSE_LEFT, SDL_SCANCODE_SPACE, SDL_SCANCODE_E, SDL_SCANCODE_Q,
SDL_SCANCODE_RETURN, PAD_KEY_MOUSE_MIDDLE, SDL_SCANCODE_LSHIFT, PAD_KEY_MOUSE_RIGHT,
SDL_SCANCODE_UP, SDL_SCANCODE_DOWN, SDL_SCANCODE_LEFT, SDL_SCANCODE_RIGHT,
};
for (size_t i = 0; i < keys.size(); ++i)
PADSetKeyButtonBinding(port, {keys[i], kControllerButtons[i].padButton});
const std::array<int, PAD_AXIS_COUNT> axes = {
SDL_SCANCODE_D, SDL_SCANCODE_A, SDL_SCANCODE_W, SDL_SCANCODE_S,
SDL_SCANCODE_L, SDL_SCANCODE_J, SDL_SCANCODE_I, SDL_SCANCODE_K,
SDL_SCANCODE_LSHIFT, PAD_KEY_MOUSE_RIGHT,
};
uint32_t axisCount = 0;
auto* mappings = PADGetKeyAxisBindings(port, &axisCount);
for (uint32_t i = 0; i < axisCount; ++i)
PADSetKeyAxisBinding(port, {axes[i], mappings[i].padAxis, 1});
PADSerializeMappings();
}
ImGui::SeparatorText("Button mapping");
for (uint32_t i = 0; i < count; ++i) {
int key = buttons[i].scancode;
ImGui::PushID(static_cast<int>(i));
ImGui::SetNextItemWidth(220.0f);
DrawKeyBinding(PADGetButtonName(buttons[i].padButton), key, RebindKind::KeyboardButton, buttons[i].padButton);
ImGui::PopID();
}
ImGui::SeparatorText("Stick and trigger mapping");
uint32_t axisCount = 0;
auto* axes = PADGetKeyAxisBindings(port, &axisCount);
for (uint32_t i = 0; i < axisCount; ++i) {
int key = axes[i].scancode;
ImGui::PushID(static_cast<int>(count + i));
const char* direction = PADGetAxisDirectionLabel(axes[i].padAxis);
const std::string label = std::string(PADGetAxisName(axes[i].padAxis)) + " " +
(direction != nullptr ? direction : "");
ImGui::SetNextItemWidth(220.0f);
DrawKeyBinding(label.c_str(), key, RebindKind::KeyboardAxis, axes[i].padAxis);
ImGui::PopID();
}
return true;
}
// Controller settings menu: port selection, controller assignment and button mapping. // Controller settings menu: port selection, controller assignment and button mapping.
int ExpressionResizeCallback(ImGuiInputTextCallbackData* data) { int ExpressionResizeCallback(ImGuiInputTextCallbackData* data) {
if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) { if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
@@ -734,10 +501,6 @@ void DrawControllerSettings() {
ImGui::Separator(); ImGui::Separator();
const uint32_t selectedGamePort = static_cast<uint32_t>(g_controllerPort); const uint32_t selectedGamePort = static_cast<uint32_t>(g_controllerPort);
if (DrawKeyboardSettings(selectedGamePort)) {
return;
}
ImGui::Separator();
const char* currentName = PADGetName(selectedGamePort); const char* currentName = PADGetName(selectedGamePort);
ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None"); ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None");
if (ImGui::MenuItem("Unassign controller")) { if (ImGui::MenuItem("Unassign controller")) {
@@ -779,10 +542,10 @@ void DrawControllerSettings() {
PADGetAltButtonMappings(static_cast<uint32_t>(g_controllerPort), &altMappingCount); PADGetAltButtonMappings(static_cast<uint32_t>(g_controllerPort), &altMappingCount);
const auto writeBinding = [](size_t index, uint32_t primaryNative, uint32_t altNative) { const auto writeBinding = [](size_t index, uint32_t primaryNative, uint32_t altNative) {
std::string value = NativeBindingConfig(primaryNative); std::string value = NativeButtonForValue(primaryNative).configName;
if (altNative != PAD_NATIVE_BUTTON_INVALID) { if (altNative != PAD_NATIVE_BUTTON_INVALID) {
value += ','; value += ',';
value += NativeBindingConfig(altNative); value += NativeButtonForValue(altNative).configName;
} }
RuntimeConfigFile::SetControllerButton(index, value); RuntimeConfigFile::SetControllerButton(index, value);
}; };
@@ -840,11 +603,6 @@ void DrawControllerSettings() {
} }
ImGui::SeparatorText("Button mapping"); ImGui::SeparatorText("Button mapping");
ImGui::TextDisabled("LT / L2 = left trigger. RT / R2 = right trigger.");
ImGui::TextDisabled("LB / L1 = left shoulder. RB / R1 = right shoulder.");
ImGui::TextDisabled("Click a binding, then press an input. No input for 10 seconds clears it.");
const float bindingWidth = ImGui::CalcTextSize("Right shoulder (RB / R1)").x +
ImGui::GetFrameHeight() + ImGui::GetStyle().FramePadding.x * 2.0f;
for (size_t i = 0; i < kControllerButtons.size(); ++i) { for (size_t i = 0; i < kControllerButtons.size(); ++i) {
auto mappingIt = std::find_if(mappings, mappings + mappingCount, [&](const PADButtonMapping& mapping) { auto mappingIt = std::find_if(mappings, mappings + mappingCount, [&](const PADButtonMapping& mapping) {
return mapping.padButton == kControllerButtons[i].padButton; return mapping.padButton == kControllerButtons[i].padButton;
@@ -864,40 +622,30 @@ void DrawControllerSettings() {
const NativeButtonItem& current = NativeButtonForValue(mappingIt->nativeButton); const NativeButtonItem& current = NativeButtonForValue(mappingIt->nativeButton);
ImGui::PushID(static_cast<int>(i)); ImGui::PushID(static_cast<int>(i));
const auto drawThreshold = [&](PADButtonMapping* mapping, bool secondary) { ImGui::SetNextItemWidth(190.0f);
if (!PADIsAxisButton(mapping->nativeButton)) return; if (ImGui::BeginCombo("##primary", current.label)) {
int threshold = static_cast<int>(PADAxisButtonThreshold(mapping->nativeButton)); for (const auto& candidate : kNativeButtons) {
ImGui::SetNextItemWidth(bindingWidth); const bool selected = candidate.nativeButton == mappingIt->nativeButton;
if (ImGui::SliderInt(secondary ? "##altThreshold" : "##primaryThreshold", &threshold, if (ImGui::Selectable(candidate.label, selected)) {
1, 100, "Threshold: %d%%", ImGuiSliderFlags_AlwaysClamp)) { const uint32_t port = static_cast<uint32_t>(g_controllerPort);
const PADButtonMapping updated = { PADSetButtonMapping(port, PADButtonMapping{candidate.nativeButton, kControllerButtons[i].padButton});
PADAxisButtonIdentity(mapping->nativeButton) | (static_cast<uint32_t>(threshold) << 8), writeBinding(i, candidate.nativeButton,
mapping->padButton, altIt != nullptr ? altIt->nativeButton : PAD_NATIVE_BUTTON_INVALID);
}; PADSerializeMappings();
if (secondary) PADSetAltButtonMapping(selectedGamePort, updated); mappings = PADGetButtonMappings(port, &mappingCount);
else PADSetButtonMapping(selectedGamePort, updated); }
if (selected) {
ImGui::SetItemDefaultFocus();
}
} }
if (ImGui::IsItemDeactivatedAfterEdit()) { ImGui::EndCombo();
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) { if (altIt != nullptr) {
const bool altBound = altIt->nativeButton != PAD_NATIVE_BUTTON_INVALID; const bool altBound = altIt->nativeButton != PAD_NATIVE_BUTTON_INVALID;
if (!altBound && !altRowExpanded[i]) { if (!altBound && !altRowExpanded[i]) {
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::SmallButton("+")) { if (ImGui::SmallButton("+")) {
altRowExpanded[i] = true; altRowExpanded[i] = true;
BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label, true);
} }
if (ImGui::IsItemHovered()) { if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Add a second binding; pressing either one works"); ImGui::SetTooltip("Add a second binding; pressing either one works");
@@ -906,15 +654,27 @@ void DrawControllerSettings() {
ImGui::SameLine(); ImGui::SameLine();
ImGui::TextUnformatted("or"); ImGui::TextUnformatted("or");
ImGui::SameLine(); ImGui::SameLine();
ImGui::BeginGroup();
const char* altLabel = altBound ? NativeButtonForValue(altIt->nativeButton).label : "None"; const char* altLabel = altBound ? NativeButtonForValue(altIt->nativeButton).label : "None";
ImGui::SetNextItemWidth(bindingWidth); ImGui::SetNextItemWidth(190.0f);
const std::string altCaption = std::string(altLabel) + "##alt"; if (ImGui::BeginCombo("##alt", altLabel)) {
if (ImGui::Button(altCaption.c_str(), ImVec2(bindingWidth, 0.0f))) { for (const auto& candidate : kNativeButtons) {
BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label, true); 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();
} }
drawThreshold(altIt, true);
ImGui::EndGroup();
} }
} }
ImGui::SameLine(); ImGui::SameLine();
@@ -1176,27 +936,10 @@ void DrawStartupScreen() {
} }
void DrawTopBar() { void DrawTopBar() {
if (!g_topBarVisible) { if (!g_topBarVisible || !ImGui::BeginMainMenuBar()) {
return; return;
} }
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::GetBackgroundDrawList()->AddRectFilled(viewport->Pos,
ImVec2(viewport->Pos.x + viewport->Size.x, viewport->Pos.y + viewport->Size.y),
IM_COL32(0, 0, 0, 70));
ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x + viewport->Size.x * 0.5f,
viewport->Pos.y + viewport->Size.y - 24.0f),
ImGuiCond_Always, ImVec2(0.5f, 1.0f));
ImGui::SetNextWindowBgAlpha(0.85f);
if (ImGui::Begin("Settings input hint", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoFocusOnAppearing)) {
ImGui::TextUnformatted("Settings open - game controls disabled. Press F10 to return to the game.");
}
ImGui::End();
if (!ImGui::BeginMainMenuBar()) return;
ImGui::TextUnformatted("WiiCompiled"); ImGui::TextUnformatted("WiiCompiled");
ImGui::Separator(); ImGui::Separator();
const auto resolutionIt = std::find_if(kResolutions.begin(), kResolutions.end(), [](const ResolutionItem& item) { const auto resolutionIt = std::find_if(kResolutions.begin(), kResolutions.end(), [](const ResolutionItem& item) {
@@ -1225,9 +968,6 @@ void DrawTopBar() {
if (ImGui::BeginMenu("Controller settings")) { if (ImGui::BeginMenu("Controller settings")) {
DrawControllerSettings(); DrawControllerSettings();
// Nest capture under this menu so opening/closing the modal preserves
// the settings popup and its current port and scroll position.
DrawRebindPrompt();
ImGui::EndMenu(); ImGui::EndMenu();
} }
@@ -1334,12 +1074,7 @@ void HandleEvents(const AuroraEvent* events) noexcept {
continue; continue;
} }
controller_mapping_wizard::HandleSdlEvent(ev->sdl); controller_mapping_wizard::HandleSdlEvent(ev->sdl);
if (g_rebind.active && (IsToggleKey(ev->sdl, SDL_SCANCODE_BACKSPACE) || if (IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) {
IsToggleKey(ev->sdl, SDL_SCANCODE_DELETE))) {
CompleteRebind(g_rebind.kind == RebindKind::Controller ? PAD_NATIVE_BUTTON_DISABLED
: static_cast<uint32_t>(PAD_KEY_INVALID));
}
if (!g_rebind.active && IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) {
SetTopBarVisible(!g_topBarVisible); SetTopBarVisible(!g_topBarVisible);
} }
if (IsMouseActivity(ev->sdl)) { if (IsMouseActivity(ev->sdl)) {
@@ -1367,7 +1102,7 @@ void Draw() noexcept {
DrawTopBar(); DrawTopBar();
controller_mapping_wizard::Draw(); controller_mapping_wizard::Draw();
// The wizard captures raw presses; keep them out of the game. // The wizard captures raw presses; keep them out of the game.
const bool inputBlocked = controller_mapping_wizard::IsActive() || g_rebind.active; const bool inputBlocked = controller_mapping_wizard::IsActive();
PADBlockInput(inputBlocked); PADBlockInput(inputBlocked);
InputBindings::SetInputBlocked(inputBlocked); InputBindings::SetInputBlocked(inputBlocked);
DrawStartupScreen(); DrawStartupScreen();
-142
View File
@@ -1,142 +0,0 @@
#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;
}
}
+2 -120
View File
@@ -3,78 +3,22 @@
#include <chrono> #include <chrono>
#include <iostream> #include <iostream>
#include <stdexcept> #include <stdexcept>
#include <thread>
#include <vector>
static void Require(bool condition, const char* message = "NAND settings check failed") { static void Require(bool condition) {
if (!condition) { if (!condition) {
throw std::runtime_error(message); throw std::runtime_error("NAND settings check failed");
} }
} }
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() { int main() {
const auto root = std::filesystem::temp_directory_path() / const auto root = std::filesystem::temp_directory_path() /
("wiicomp-nand-settings-" + std::to_string( ("wiicomp-nand-settings-" + std::to_string(
std::chrono::steady_clock::now().time_since_epoch().count())); std::chrono::steady_clock::now().time_since_epoch().count()));
const auto path = root / "title/00000001/00000002/data/setting.txt"; const auto path = root / "title/00000001/00000002/data/setting.txt";
try { 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(!RuntimeNandSettings::Read(root));
Require(!std::filesystem::exists(root)); Require(!std::filesystem::exists(root));
std::filesystem::create_directories(path.parent_path()); 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"; const std::string plain = "AREA=USA\r\n\nCODE=LU\r\nSERNO=987654321\r\nGAME=US\r\n";
std::array<uint8_t, 256> fixture{}; std::array<uint8_t, 256> fixture{};
for (size_t i = 0; i < fixture.size(); ++i) { for (size_t i = 0; i < fixture.size(); ++i) {
@@ -91,7 +35,6 @@ int main() {
Require(settings && RuntimeNandSettings::HasIdentity(*settings)); Require(settings && RuntimeNandSettings::HasIdentity(*settings));
Require(settings->at("SERNO") == "987654321" && settings->at("CODE") == "LU"); Require(settings->at("SERNO") == "987654321" && settings->at("CODE") == "LU");
Require(settings->at("AREA") == "USA" && settings->at("GAME") == "US"); 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::array<uint8_t, 256> after{};
{ {
std::ifstream input(path, std::ios::binary); std::ifstream input(path, std::ios::binary);
@@ -111,67 +54,6 @@ int main() {
Require(!RuntimeNandSettings::HasIdentity(*settings)); Require(!RuntimeNandSettings::HasIdentity(*settings));
std::filesystem::resize_file(path, 128); std::filesystem::resize_file(path, 128);
Require(!RuntimeNandSettings::Read(root)); 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::filesystem::remove_all(root);
std::cout << "NAND settings checks passed\n"; std::cout << "NAND settings checks passed\n";
return 0; return 0;
-81
View File
@@ -1,81 +0,0 @@
#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;
}
}
+126 -24
View File
@@ -1,4 +1,4 @@
using System; using System;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
@@ -1891,7 +1891,6 @@ int RunTranslateModCore(string[] argsTail, string? outputDirectoryOverride)
overlayBuild, overlayBuild,
continuationPlan, continuationPlan,
retroWfcResolvedExecutableHooks, retroWfcResolvedExecutableHooks,
patchPlan,
kamekFunctionStarts, kamekFunctionStarts,
moduleLinkBase, moduleLinkBase,
selected.CodeSize, selected.CodeSize,
@@ -2232,7 +2231,6 @@ int EmitModCpp(
OverlayBuildResult overlayBuild, OverlayBuildResult overlayBuild,
ContinuationPlan continuationPlan, ContinuationPlan continuationPlan,
IReadOnlyCollection<RetroWfcExecutableHookPlan>? retroWfcExecutableHooks, IReadOnlyCollection<RetroWfcExecutableHookPlan>? retroWfcExecutableHooks,
KamekPatchPlan patchPlan,
IReadOnlyList<ModFunctionStart> kamekFunctionStarts, IReadOnlyList<ModFunctionStart> kamekFunctionStarts,
uint moduleLinkBase, uint moduleLinkBase,
uint moduleLinkedCodeSize, uint moduleLinkedCodeSize,
@@ -2274,25 +2272,14 @@ int EmitModCpp(
.ToHashSet(); .ToHashSet();
var queuedContinuationAddresses = continuationPlan.Entries.Select(e => e.Address).ToHashSet(); var queuedContinuationAddresses = continuationPlan.Entries.Select(e => e.Address).ToHashSet();
var discoveredContinuationQueue = new Queue<ContinuationEntry>(); var discoveredContinuationQueue = new Queue<ContinuationEntry>();
var hookLrBases = new List<(uint TargetAddress, uint ContinuationAddress)>(); var linkedHookLrBasesByTarget = retroWfcExecutableHooks is null
if (retroWfcExecutableHooks is not null) ? new Dictionary<uint, uint[]>()
{ : retroWfcExecutableHooks
hookLrBases.AddRange( .Where(h => h.TargetAddress.HasValue && RetroWfcHookSetsLinkRegister(h))
retroWfcExecutableHooks .GroupBy(h => h.TargetAddress!.Value)
.Where(h => h.TargetAddress.HasValue && RetroWfcHookSetsLinkRegister(h)) .ToDictionary(
.Select(h => (h.TargetAddress!.Value, h.ContinuationAddress))); g => g.Key,
} g => g.Select(h => h.ContinuationAddress).Distinct().ToArray());
foreach (var patch in patchPlan.ExecutablePatches.Where(p => p.CommandId == KamekCommandId.BranchLink && p.Arguments.Count > 0))
{
var target = KamekAddress.Resolve(patch.Arguments[0], patchPlan.ModuleGuestBase);
hookLrBases.Add((target, checked(patch.CommandAddress + 4u)));
}
var linkedHookLrBasesByTarget = hookLrBases
.GroupBy(h => h.TargetAddress)
.ToDictionary(
g => g.Key,
g => g.Select(h => h.ContinuationAddress).Distinct().ToArray());
var lrContinuationCallTargets = linkedHookLrBasesByTarget.Keys.ToHashSet(); var lrContinuationCallTargets = linkedHookLrBasesByTarget.Keys.ToHashSet();
var linkedCallFallthroughLrOverrides = retroWfcExecutableHooks is null var linkedCallFallthroughLrOverrides = retroWfcExecutableHooks is null
? new Dictionary<uint, uint>() ? new Dictionary<uint, uint>()
@@ -2763,8 +2750,123 @@ IEnumerable<uint> DirectModuleTargets(FunctionTranslationResult result, uint mod
} }
} }
IEnumerable<int> DiscoverLrRelativeIndirectJumpOffsets(FunctionTranslationResult result) => IEnumerable<int> DiscoverLrRelativeIndirectJumpOffsets(FunctionTranslationResult result)
ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(result.Instructions); {
var lrOffsets = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
int? ctrOffset = null;
foreach (var instruction in result.Instructions)
{
var mnemonic = instruction.Mnemonic.ToLowerInvariant();
if (mnemonic == "mflr" && TryGetInstructionReg(instruction, 0, out var lrDest))
{
lrOffsets[lrDest] = 0;
continue;
}
if ((mnemonic == "mr" || mnemonic == "or") &&
TryGetInstructionReg(instruction, 0, out var moveDest) &&
TryGetInstructionReg(instruction, 1, out var moveSource) &&
(mnemonic == "mr" ||
(instruction.Operands.Count >= 3 &&
instruction.Operands[2] is PpcRegisterOperand moveSource2 &&
string.Equals(NormalizeInstructionReg(moveSource2.Name), moveSource, StringComparison.OrdinalIgnoreCase))))
{
if (lrOffsets.TryGetValue(moveSource, out var sourceOffset))
{
lrOffsets[moveDest] = sourceOffset;
}
else
{
lrOffsets.Remove(moveDest);
}
continue;
}
if (mnemonic == "addi" &&
TryGetInstructionReg(instruction, 0, out var addDest) &&
TryGetInstructionReg(instruction, 1, out var addBase) &&
TryGetInstructionImm(instruction, 2, out var imm))
{
if (lrOffsets.TryGetValue(addBase, out var baseOffset))
{
lrOffsets[addDest] = checked(baseOffset + imm);
}
else
{
lrOffsets.Remove(addDest);
}
continue;
}
if (mnemonic == "mtctr" && TryGetInstructionReg(instruction, 0, out var ctrSource))
{
ctrOffset = lrOffsets.TryGetValue(ctrSource, out var sourceOffset) ? sourceOffset : null;
continue;
}
if (mnemonic == "bctr")
{
if (ctrOffset.HasValue)
{
yield return ctrOffset.Value;
}
ctrOffset = null;
continue;
}
if (TryInstructionWritesDest(instruction, out var dest))
{
lrOffsets.Remove(dest);
}
}
static bool TryGetInstructionReg(PpcInstruction instruction, int index, out string register)
{
if (instruction.Operands.Count > index && instruction.Operands[index] is PpcRegisterOperand operand)
{
register = NormalizeInstructionReg(operand.Name);
return true;
}
register = string.Empty;
return false;
}
static bool TryGetInstructionImm(PpcInstruction instruction, int index, out int immediate)
{
if (instruction.Operands.Count > index && instruction.Operands[index] is PpcImmediateOperand operand)
{
immediate = operand.Value;
return true;
}
immediate = 0;
return false;
}
static bool TryInstructionWritesDest(PpcInstruction instruction, out string destination)
{
destination = string.Empty;
if (instruction.Operands.Count == 0 || instruction.Operands[0] is not PpcRegisterOperand operand)
{
return false;
}
var mnemonic = instruction.Mnemonic.ToLowerInvariant();
if (mnemonic.StartsWith("st", StringComparison.Ordinal) ||
mnemonic.StartsWith("b", StringComparison.Ordinal) ||
mnemonic.StartsWith("cmp", StringComparison.Ordinal))
{
return false;
}
destination = NormalizeInstructionReg(operand.Name);
return true;
}
static string NormalizeInstructionReg(string register) => register.ToLowerInvariant();
}
static bool RetroWfcHookSetsLinkRegister(RetroWfcExecutableHookPlan hook) => static bool RetroWfcHookSetsLinkRegister(RetroWfcExecutableHookPlan hook) =>
hook.TypeName is "call" or "branchCtrLink" || hook.TypeName is "call" or "branchCtrLink" ||
@@ -1,5 +1,4 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Text.Json; using System.Text.Json;
using Translator.Core.Disassembly; using Translator.Core.Disassembly;
using Translator.Core.Parsing.Kamek; using Translator.Core.Parsing.Kamek;
@@ -274,530 +273,4 @@ public static class ContinuationPlanner
Or, Or,
AddSigned AddSigned
} }
public static IEnumerable<int> DiscoverLrRelativeIndirectJumpOffsets(IReadOnlyList<PpcInstruction> instructions)
{
if (instructions.Count == 0)
{
yield break;
}
var indexByAddress = new Dictionary<uint, int>(instructions.Count);
for (var i = 0; i < instructions.Count; i++)
{
indexByAddress.TryAdd(instructions[i].Address, i);
}
var visited = new HashSet<PathState>[instructions.Count];
for (var i = 0; i < instructions.Count; i++)
{
visited[i] = new HashSet<PathState>();
}
var seenOffsets = new HashSet<int>();
var worklist = new Queue<(int Index, PathState State)>();
const int MaxStatesPerInstruction = 16;
void Enqueue(int targetIndex, PathState stateToEnqueue)
{
worklist.Enqueue((targetIndex, stateToEnqueue));
}
int? GetFallthroughIndex(PpcInstruction instruction)
{
if (indexByAddress.TryGetValue(instruction.EndAddress, out var nextIndex))
{
return nextIndex;
}
return null;
}
Enqueue(0, PathState.Empty);
while (worklist.Count > 0)
{
var (idx, state) = worklist.Dequeue();
if (!visited[idx].Add(state))
{
continue;
}
if (visited[idx].Count > MaxStatesPerInstruction)
{
continue;
}
var instruction = instructions[idx];
var mnemonic = instruction.Mnemonic.ToLowerInvariant();
var nextState = state;
if (mnemonic == "mflr" && TryGetInstructionReg(instruction, 0, out var lrDest))
{
if (lrDest == "r1")
{
nextState = nextState.WithClearedStackOffsets();
}
nextState = nextState.LrReturnOffset.HasValue
? nextState.WithLrOffset(lrDest, nextState.LrReturnOffset.Value)
: nextState.WithoutLrOffset(lrDest);
}
else if ((mnemonic == "mr" || mnemonic == "or") &&
TryGetInstructionReg(instruction, 0, out var moveDest) &&
TryGetInstructionReg(instruction, 1, out var moveSource) &&
(mnemonic == "mr" ||
(instruction.Operands.Count >= 3 &&
instruction.Operands[2] is PpcRegisterOperand moveSource2 &&
string.Equals(NormalizeInstructionReg(moveSource2.Name), moveSource, StringComparison.OrdinalIgnoreCase))))
{
if (moveDest == "r1" && moveSource != "r1")
{
nextState = nextState.WithClearedStackOffsets();
}
nextState = nextState.LrOffsets.TryGetValue(moveSource, out var sourceOffset)
? nextState.WithLrOffset(moveDest, sourceOffset)
: nextState.WithoutLrOffset(moveDest);
}
else if ((mnemonic == "addi" || mnemonic == "addic") &&
TryGetInstructionReg(instruction, 0, out var addDest) &&
TryGetInstructionReg(instruction, 1, out var addBase) &&
TryGetInstructionImm(instruction, 2, out var imm))
{
if (addDest == "r1")
{
nextState = addBase == "r1"
? nextState.WithSpDelta(unchecked(nextState.SpDelta + imm))
: nextState.WithClearedStackOffsets();
}
nextState = nextState.LrOffsets.TryGetValue(addBase, out var baseOffset)
? nextState.WithLrOffset(addDest, unchecked(baseOffset + imm))
: nextState.WithoutLrOffset(addDest);
}
else if (mnemonic == "mtctr" && TryGetInstructionReg(instruction, 0, out var ctrSource))
{
var newCtrOffset = nextState.LrOffsets.TryGetValue(ctrSource, out var sourceOffset) ? sourceOffset : (int?)null;
nextState = nextState.WithCtrOffset(newCtrOffset);
}
else if (mnemonic == "mtlr" && TryGetInstructionReg(instruction, 0, out var lrSource))
{
var newLrReturnOffset = nextState.LrOffsets.TryGetValue(lrSource, out var sourceOffset) ? sourceOffset : (int?)null;
nextState = nextState.WithLrReturnOffset(newLrReturnOffset);
}
else if (mnemonic == "stw" &&
TryGetInstructionReg(instruction, 0, out var storeSrc) &&
TryGetInstructionDisplacement(instruction, 1, out var storeDisp, out var storeBase, out _))
{
if (storeBase == "r1")
{
var targetSlot = nextState.SpDelta + storeDisp;
nextState = nextState.LrOffsets.TryGetValue(storeSrc, out var offset)
? nextState.WithStackOffset(targetSlot, offset)
: nextState.WithoutStackOffset(targetSlot);
}
}
else if (mnemonic == "stwu" &&
TryGetInstructionReg(instruction, 0, out var stwuSrc) &&
TryGetInstructionDisplacement(instruction, 1, out var stwuDisp, out var stwuBase, out _))
{
if (stwuBase == "r1")
{
var targetSlot = nextState.SpDelta + stwuDisp;
nextState = nextState.LrOffsets.TryGetValue(stwuSrc, out var offset)
? nextState.WithStackOffset(targetSlot, offset)
: nextState.WithoutStackOffset(targetSlot);
nextState = nextState.WithAdjustedStackPointer(stwuDisp);
}
else
{
nextState = nextState.WithoutLrOffset(stwuBase);
}
}
else if (TryGetStackStoreRange(instruction, out var storeOffset, out var storeSize, out var updatesStackPointer))
{
nextState = nextState.WithoutStackOffsetsInRange(
nextState.SpDelta + storeOffset,
storeSize);
if (updatesStackPointer)
{
nextState = nextState.WithAdjustedStackPointer(storeOffset);
}
}
else if (mnemonic == "lwz" &&
TryGetInstructionReg(instruction, 0, out var loadDest) &&
TryGetInstructionDisplacement(instruction, 1, out var loadDisp, out var loadBase, out _))
{
if (loadBase == "r1")
{
var targetSlot = nextState.SpDelta + loadDisp;
var hasStackOffset = nextState.StackOffsets.TryGetValue(targetSlot, out var offset);
if (loadDest == "r1")
{
nextState = nextState.WithClearedStackOffsets();
}
nextState = hasStackOffset
? nextState.WithLrOffset(loadDest, offset)
: nextState.WithoutLrOffset(loadDest);
}
else
{
nextState = nextState.WithoutLrOffset(loadDest);
if (loadDest == "r1")
{
nextState = nextState.WithClearedStackOffsets();
}
}
}
else
{
if (TryInstructionWritesDest(instruction, out var destinations))
{
foreach (var dest in destinations)
{
nextState = nextState.WithoutLrOffset(dest);
if (dest == "r1")
{
nextState = nextState.WithClearedStackOffsets();
}
}
}
}
if (instruction.IsCall || mnemonic == "bl" || mnemonic == "blrl")
{
nextState = nextState.WithLrReturnOffset(null).WithCtrOffset(null);
for (var register = 0; register <= 12; register++)
{
if (register != 1 && register != 2)
{
nextState = nextState.WithoutLrOffset($"r{register}");
}
}
}
if (mnemonic == "bctr")
{
if (state.CtrOffset.HasValue && seenOffsets.Add(state.CtrOffset.Value))
{
yield return state.CtrOffset.Value;
}
nextState = nextState.WithCtrOffset(null);
if (instruction.BranchTargets.Count == 0)
{
continue;
}
}
var isReturn = !instruction.IsCall && (instruction.IsReturn || mnemonic == "blr" || mnemonic == "bclr" ||
(mnemonic.StartsWith("b", StringComparison.Ordinal) && mnemonic.EndsWith("lr", StringComparison.Ordinal)));
if (isReturn)
{
if (state.LrReturnOffset.HasValue && state.LrReturnOffset.Value != 0 && seenOffsets.Add(state.LrReturnOffset.Value))
{
yield return state.LrReturnOffset.Value;
}
if (!instruction.IsConditionalBranch)
{
continue;
}
}
if (instruction.IsUnconditionalBranch)
{
foreach (var target in instruction.BranchTargets)
{
if (indexByAddress.TryGetValue(target, out var targetIndex))
{
Enqueue(targetIndex, nextState);
}
}
}
else if (instruction.IsConditionalBranch)
{
var fallthrough = GetFallthroughIndex(instruction);
if (fallthrough.HasValue)
{
Enqueue(fallthrough.Value, nextState);
}
if (!isReturn)
{
foreach (var target in instruction.BranchTargets)
{
if (indexByAddress.TryGetValue(target, out var targetIndex))
{
Enqueue(targetIndex, nextState);
}
}
}
}
else
{
var fallthrough = GetFallthroughIndex(instruction);
if (fallthrough.HasValue)
{
Enqueue(fallthrough.Value, nextState);
}
}
}
static bool TryGetInstructionReg(PpcInstruction instruction, int index, out string register)
{
if (instruction.Operands.Count > index && instruction.Operands[index] is PpcRegisterOperand operand)
{
register = NormalizeInstructionReg(operand.Name);
return true;
}
register = string.Empty;
return false;
}
static bool TryGetInstructionDisplacement(PpcInstruction instruction, int index, out int offset, out string baseRegister, out int baseRegisterNumber)
{
if (instruction.Operands.Count > index && instruction.Operands[index] is PpcDisplacementOperand operand)
{
offset = operand.Offset;
baseRegister = NormalizeInstructionReg(operand.BaseRegister);
baseRegisterNumber = operand.BaseRegisterNumber;
return true;
}
offset = 0;
baseRegister = string.Empty;
baseRegisterNumber = -1;
return false;
}
static bool TryGetInstructionImm(PpcInstruction instruction, int index, out int immediate)
{
if (instruction.Operands.Count > index && instruction.Operands[index] is PpcImmediateOperand operand)
{
immediate = operand.Value;
return true;
}
immediate = 0;
return false;
}
static bool TryInstructionWritesDest(PpcInstruction instruction, out IReadOnlyList<string> destinations)
{
if (instruction.Operands.Count == 0 || instruction.Operands[0] is not PpcRegisterOperand operand)
{
destinations = Array.Empty<string>();
return false;
}
var mnemonic = instruction.Mnemonic.ToLowerInvariant();
if (mnemonic.StartsWith("st", StringComparison.Ordinal) ||
mnemonic.StartsWith("b", StringComparison.Ordinal) ||
mnemonic.StartsWith("cmp", StringComparison.Ordinal))
{
destinations = Array.Empty<string>();
return false;
}
if (mnemonic == "lmw")
{
var startReg = Math.Clamp(operand.Number, 0, 31);
var regs = new string[32 - startReg];
for (var r = startReg; r <= 31; r++)
{
regs[r - startReg] = $"r{r}";
}
destinations = regs;
return true;
}
destinations = [NormalizeInstructionReg(operand.Name)];
return true;
}
static bool TryGetStackStoreRange(PpcInstruction instruction, out int offset, out int size, out bool updatesStackPointer)
{
offset = 0;
size = 0;
updatesStackPointer = false;
if (!TryGetInstructionDisplacement(instruction, 1, out offset, out var baseRegister, out _) ||
baseRegister != "r1")
{
return false;
}
switch (instruction.Mnemonic.ToLowerInvariant())
{
case "stfs":
size = 4;
return true;
case "stfsu":
size = 4;
updatesStackPointer = true;
return true;
case "stfd":
size = 8;
return true;
case "stfdu":
size = 8;
updatesStackPointer = true;
return true;
case "stmw" when instruction.Operands[0] is PpcRegisterOperand register:
size = checked((32 - Math.Clamp(register.Number, 0, 31)) * 4);
return true;
default:
return false;
}
}
static string NormalizeInstructionReg(string register) => register.ToLowerInvariant();
}
private sealed class PathState : IEquatable<PathState>
{
public ImmutableDictionary<string, int> LrOffsets { get; }
public int? CtrOffset { get; }
public int? LrReturnOffset { get; }
public int SpDelta { get; }
public ImmutableDictionary<int, int> StackOffsets { get; }
public PathState(
ImmutableDictionary<string, int> lrOffsets,
int? ctrOffset,
int? lrReturnOffset,
int spDelta,
ImmutableDictionary<int, int> stackOffsets)
{
LrOffsets = lrOffsets;
CtrOffset = ctrOffset;
LrReturnOffset = lrReturnOffset;
SpDelta = spDelta;
StackOffsets = stackOffsets;
}
public static readonly PathState Empty = new(
ImmutableDictionary<string, int>.Empty.WithComparers(StringComparer.OrdinalIgnoreCase),
null,
0,
0,
ImmutableDictionary<int, int>.Empty);
public PathState WithLrOffset(string register, int offset) =>
LrOffsets.TryGetValue(register, out var cur) && cur == offset
? this
: new(LrOffsets.SetItem(register, offset), CtrOffset, LrReturnOffset, SpDelta, StackOffsets);
public PathState WithoutLrOffset(string register) =>
LrOffsets.ContainsKey(register)
? new(LrOffsets.Remove(register), CtrOffset, LrReturnOffset, SpDelta, StackOffsets)
: this;
public PathState WithCtrOffset(int? ctrOffset) =>
ctrOffset == CtrOffset
? this
: new(LrOffsets, ctrOffset, LrReturnOffset, SpDelta, StackOffsets);
public PathState WithLrReturnOffset(int? lrReturnOffset) =>
lrReturnOffset == LrReturnOffset
? this
: new(LrOffsets, CtrOffset, lrReturnOffset, SpDelta, StackOffsets);
public PathState WithSpDelta(int spDelta) =>
spDelta == SpDelta
? this
: new(LrOffsets, CtrOffset, LrReturnOffset, spDelta, StackOffsets);
public PathState WithAdjustedStackPointer(int displacement)
{
// r1 can hold an LR-relative address too. Update both relations;
// guest address arithmetic wraps at 32 bits.
var updated = WithSpDelta(unchecked(SpDelta + displacement));
return LrOffsets.TryGetValue("r1", out var offset)
? updated.WithLrOffset("r1", unchecked(offset + displacement))
: updated;
}
public PathState WithStackOffset(int slot, int offset) =>
StackOffsets.TryGetValue(slot, out var cur) && cur == offset
? this
: new(LrOffsets, CtrOffset, LrReturnOffset, SpDelta, StackOffsets.SetItem(slot, offset));
public PathState WithoutStackOffset(int slot) =>
StackOffsets.ContainsKey(slot)
? new(LrOffsets, CtrOffset, LrReturnOffset, SpDelta, StackOffsets.Remove(slot))
: this;
public PathState WithoutStackOffsetsInRange(int start, int size)
{
var end = checked(start + size);
var remaining = StackOffsets;
foreach (var slot in StackOffsets.Keys)
{
if (slot < end && start < checked(slot + 4))
{
remaining = remaining.Remove(slot);
}
}
return remaining.Count == StackOffsets.Count
? this
: new(LrOffsets, CtrOffset, LrReturnOffset, SpDelta, remaining);
}
public PathState WithClearedStackOffsets() =>
StackOffsets.IsEmpty
? this
: new(LrOffsets, CtrOffset, LrReturnOffset, SpDelta, ImmutableDictionary<int, int>.Empty);
public bool Equals(PathState? other)
{
if (ReferenceEquals(this, other)) return true;
if (other is null) return false;
if (CtrOffset != other.CtrOffset || LrReturnOffset != other.LrReturnOffset || SpDelta != other.SpDelta) return false;
if (LrOffsets.Count != other.LrOffsets.Count || StackOffsets.Count != other.StackOffsets.Count) return false;
foreach (var (k, v) in LrOffsets)
{
if (!other.LrOffsets.TryGetValue(k, out var otherV) || v != otherV)
{
return false;
}
}
foreach (var (k, v) in StackOffsets)
{
if (!other.StackOffsets.TryGetValue(k, out var otherV) || v != otherV)
{
return false;
}
}
return true;
}
public override bool Equals(object? obj) => obj is PathState other && Equals(other);
public override int GetHashCode()
{
var hash = new HashCode();
hash.Add(CtrOffset);
hash.Add(LrReturnOffset);
hash.Add(SpDelta);
hash.Add(LrOffsets.Count);
var regHash = 0;
foreach (var (k, v) in LrOffsets)
{
regHash ^= HashCode.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(k), v);
}
hash.Add(regHash);
hash.Add(StackOffsets.Count);
var stackHash = 0;
foreach (var (k, v) in StackOffsets)
{
stackHash ^= HashCode.Combine(k, v);
}
hash.Add(stackHash);
return hash.ToHashCode();
}
}
} }
@@ -1,6 +1,4 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Linq;
using Translator.Core.Disassembly;
using Translator.Core.Mods; using Translator.Core.Mods;
using Translator.Core.Mods.Mkwii; using Translator.Core.Mods.Mkwii;
using Translator.Core.Parsing.Kamek; using Translator.Core.Parsing.Kamek;
@@ -130,54 +128,6 @@ public class ContinuationPlannerTests
Assert.Contains("Retro WFC executable hook continuation", entry.Reason); Assert.Contains("Retro WFC executable hook continuation", entry.Reason);
} }
[Fact]
public void DiscoverLrRelativeIndirectJumpOffsets_DiscoversSkipReturnOffset()
{
var instructions = new[]
{
PpcDecoder.Decode(0x8180D8E8, 0x7FE802A6u), // mflr r31
PpcDecoder.Decode(0x8180D8EC, 0x3BFF0014u), // addi r31, r31, 20
PpcDecoder.Decode(0x8180D8F0, 0x7FE803A6u), // mtlr r31
PpcDecoder.Decode(0x8180D8F4, 0x4E800020u), // blr
};
var offsets = ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(instructions).ToArray();
var offset = Assert.Single(offsets);
Assert.Equal(20, offset);
}
[Fact]
public void DiscoverLrRelativeIndirectJumpOffsets_IgnoresStandardLrRestore()
{
var instructions = new[]
{
PpcDecoder.Decode(0x8180D8E8, 0x7FE802A6u), // mflr r31
PpcDecoder.Decode(0x8180D8EC, 0x93E10008u), // stw r31, 8(r1)
PpcDecoder.Decode(0x8180D8F0, 0x83E10008u), // lwz r31, 8(r1)
PpcDecoder.Decode(0x8180D8F4, 0x7FE803A6u), // mtlr r31
PpcDecoder.Decode(0x8180D8F8, 0x4E800020u), // blr
};
var offsets = ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(instructions);
Assert.Empty(offsets);
}
[Fact]
public void DiscoverLrRelativeIndirectJumpOffsets_SupportsBctrOffset()
{
var instructions = new[]
{
PpcDecoder.Decode(0x8180D8E8, 0x7FE802A6u), // mflr r31
PpcDecoder.Decode(0x8180D8EC, 0x397F0008u), // addi r11, r31, 8
PpcDecoder.Decode(0x8180D8F0, 0x7D6903A6u), // mtctr r11
PpcDecoder.Decode(0x8180D8F4, 0x4E800420u), // bctr
};
var offsets = ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(instructions).ToArray();
var offset = Assert.Single(offsets);
Assert.Equal(8, offset);
}
private static KamekChunk EmptyChunk() => private static KamekChunk EmptyChunk() =>
new( new(
0, 0,
@@ -1,63 +0,0 @@
using Translator.Core.Analysis.Ssa;
using Translator.Core.Analysis.Representation;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
// This binary-free code-generation regression must run in the default suite.
public class LrContinuationCodeGenTests
{
[Fact]
public void CodeGenerator_DispatchesGuestCallLrContinuationWithoutMarkingTargetNonReturning()
{
var function = new IrFunction(
"lr_continuation_call",
"0x800E591C",
new[]
{
new IrBasicBlock("0x800E591C", new IrInstruction[]
{
new IrAssign("lr", IrValue.Imm(unchecked((int)0x800E5920u))),
new IrCall(string.Empty, "0x8179AC3C", Array.Empty<IrValue>()),
new IrAssign("r3", IrValue.Imm(8)),
new IrReturn(null)
}),
new IrBasicBlock("0x800E5934", new IrInstruction[]
{
new IrAssign("r3", IrValue.Imm(1)),
new IrReturn(null)
})
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["lr"] = ValueRepresentation.UInt32,
["r3"] = ValueRepresentation.UInt32
});
var signature = new FunctionAbiClassification("lr_continuation_call", ValueRepresentation.Void);
var ssa = new SsaTransformer().Convert(function);
var code = new CxxLinearCodeGenerator().Emit(
0x800E591C,
ssa,
signature,
types,
lrContinuationCallTargets: new HashSet<uint> { 0x8179AC3Cu });
var callIndex = code.IndexOf("InvokeDirectCpu<0x8179AC3Cu>(ctx);", StringComparison.Ordinal);
var fallthroughGuardIndex = code.IndexOf("if (ctx->lr != 0x800E5920u)", callIndex, StringComparison.Ordinal);
var localCaseIndex = code.IndexOf("case 0x800E5934u:", fallthroughGuardIndex, StringComparison.Ordinal);
var returnIndex = code.IndexOf("return;", localCaseIndex, StringComparison.Ordinal);
var fallthroughAssignmentIndex = code.IndexOf("r3 = 8;", callIndex, StringComparison.Ordinal);
Assert.True(callIndex >= 0);
Assert.True(fallthroughGuardIndex > callIndex);
Assert.True(localCaseIndex > fallthroughGuardIndex);
Assert.True(returnIndex > localCaseIndex);
Assert.True(fallthroughAssignmentIndex > returnIndex, code);
Assert.Contains("goto loc_800E5934;", code);
}
}
@@ -1,448 +0,0 @@
using System.Buffers.Binary;
using Translator.Core.Disassembly;
using Translator.Core.Loading;
using Translator.Core.Mods;
using Xunit;
namespace Translator.Tests;
public class LrRelativeContinuationTests
{
[Theory]
[InlineData(20, 40)]
[InlineData(40, 20)]
public void MutuallyExclusiveAdjustmentsKeepBothOffsets(int firstOffset, int secondOffset)
{
// Both arms start with the incoming LR and join at mtlr. Adding the
// offsets together invents a continuation that neither arm can reach.
var offsets = DiscoverOffsets(
0x7FE802A6u, // +00: mflr r31
0x2C030000u, // +04: cmpwi r3,0
0x4182000Cu, // +08: beq +0x14
AddiR31(firstOffset), // +0C: addi r31,r31,firstOffset
0x48000008u, // +10: b +0x18
AddiR31(secondOffset), // +14: addi r31,r31,secondOffset
0x7FE803A6u, // +18: mtlr r31
0x4E800020u);// +1C: blr
Assert.Equal(new[] { 20, 40 }, offsets);
}
[Fact]
public void NormalReturnArmDoesNotEraseSkipReturnAtSharedBlr()
{
// The normal arm writes the original LR; it must not overwrite the
// other arm's LR + 20 in the analysis of the shared return.
var offsets = DiscoverOffsets(
0x7FE802A6u, // +00: mflr r31
0x2C030000u, // +04: cmpwi r3,0
0x41820010u, // +08: beq +0x18
0x397F0014u, // +0C: addi r11,r31,20
0x7D6803A6u, // +10: mtlr r11
0x48000008u, // +14: b +0x1C
0x7FE803A6u, // +18: mtlr r31
0x4E800020u);// +1C: blr
Assert.Equal(new[] { 20 }, offsets);
}
[Fact]
public void ConditionalNormalReturnStillDiscoversSkipOnFallthrough()
{
var offsets = DiscoverOffsets(
0x7FE802A6u, // mflr r31
0x2C030000u, // cmpwi r3,0
0x4D820020u, // beqlr
0x3BFF0014u, // addi r31,r31,20
0x7FE803A6u, // mtlr r31
0x4E800020u);// blr
Assert.Equal(new[] { 20 }, offsets);
}
[Fact]
public void SavedNonvolatileLrSurvivesHelperCall()
{
// r31 survives a normal ABI call even though the call replaces LR.
var offsets = DiscoverOffsets(
0x7FE802A6u, // mflr r31
0x48000101u, // bl helper outside this function
0x3BFF0014u, // addi r31,r31,20
0x7FE803A6u, // mtlr r31
0x4E800020u);// blr
Assert.Equal(new[] { 20 }, offsets);
}
[Fact]
public void ReloadingSavedRegisterAfterMtlrDoesNotEraseSkipReturn()
{
// A hook epilogue restores the caller's r31 after committing its
// adjusted return address to LR.
var offsets = DiscoverOffsets(
0x7FE802A6u, // mflr r31
0x3BFF0014u, // addi r31,r31,20
0x7FE803A6u, // mtlr r31
0x83E10008u, // lwz r31,8(r1)
0x4E800020u);// blr
Assert.Equal(new[] { 20 }, offsets);
}
[Fact]
public void UnknownLrWriteReplacesEarlierSkipReturn()
{
var offsets = DiscoverOffsets(
0x7FE802A6u, // mflr r31
0x3BFF0014u, // addi r31,r31,20
0x7FE803A6u, // mtlr r31
0x80010008u, // lwz r0,8(r1)
0x7C0803A6u, // mtlr r0
0x4E800020u);// blr
Assert.Empty(offsets);
}
[Fact]
public void UnadjustedRegisterReturnDoesNotAddAContinuation()
{
var offsets = DiscoverOffsets(
0x7FE802A6u, // mflr r31
0x7FE803A6u, // mtlr r31
0x4E800020u);// blr
Assert.Empty(offsets);
}
[Fact]
public void LoadMultipleWordOverwritesSavedRegistersThroughR31()
{
var offsets = DiscoverOffsets(
0x7FE802A6u, // mflr r31
0x3BFF0014u, // addi r31,r31,20
0xBB610008u, // lmw r30,8(r1)
0x7FE803A6u, // mtlr r31
0x4E800020u);// blr
Assert.Empty(offsets);
}
[Fact]
public void BlrlCallIsNotTreatedAsReturn()
{
var offsets = DiscoverOffsets(
0x7FE802A6u, // mflr r31
0x4E800021u, // blrl
0x3BFF0014u, // addi r31,r31,20
0x7FE803A6u, // mtlr r31
0x4E800020u);// blr
Assert.Equal(new[] { 20 }, offsets);
}
[Fact]
public void MflrAfterCallDoesNotTreatClobberedLrAsIncomingLr()
{
var offsets = DiscoverOffsets(
0x48000101u, // bl helper
0x7FE802A6u, // mflr r31
0x3BFF0014u, // addi r31,r31,20
0x7FE803A6u, // mtlr r31
0x4E800020u);// blr
Assert.Empty(offsets);
}
[Fact]
public void RestoredIncomingLrBeforeCtrSkipStillDiscoversOffset()
{
// The helper replaces LR, but the stack save/restore recovers the
// incoming LR before the hook jumps to the caller's continuation.
var offsets = DiscoverOffsets(
0x7C0802A6u, // mflr r0
0x90010004u, // stw r0,4(r1)
0x9421FFF0u, // stwu r1,-16(r1)
0x48000101u, // bl helper outside this function
0x38210010u, // addi r1,r1,16
0x80010004u, // lwz r0,4(r1)
0x7C0803A6u, // mtlr r0
0x7D6802A6u, // mflr r11
0x396B0008u, // addi r11,r11,8
0x7D6903A6u, // mtctr r11
0x4E800420u);// bctr
Assert.Equal(new[] { 8 }, offsets);
}
[Fact]
public void BoundedLoopBeforeCtrSkipStillDiscoversOffset()
{
// Updating an LR-derived register in a two-iteration loop must not
// starve analysis of the exit, whose target uses unchanged r31.
var offsets = DiscoverOffsets(
0x7FE802A6u, // mflr r31
0x7FC802A6u, // mflr r30
0x38600002u, // li r3,2
0x7C6903A6u, // mtctr r3
0x3BDE0004u, // addi r30,r30,4
0x4200FFFCu, // bdnz -4
0x397F0008u, // addi r11,r31,8
0x7D6903A6u, // mtctr r11
0x4E800420u);// bctr
Assert.Equal(new[] { 8 }, offsets);
}
[Fact]
public void UntrackedR1WriteInvalidatesStackTracking()
{
// If r1 is overwritten from an untracked source, previously saved stack slots
// must not be used to recover LR state.
var offsets = DiscoverOffsets(
0x7FE802A6u, // mflr r31
0x3BFF0014u, // addi r31,r31,20
0x93E10008u, // stw r31,8(r1)
0x80230000u, // lwz r1,0(r3)
0x80010008u, // lwz r0,8(r1)
0x7C0803A6u, // mtlr r0
0x4E800020u);// blr
Assert.Empty(offsets);
}
[Fact]
public void LargeStraightLineHandlerStillDiscoversSkipReturn()
{
// The analyzer's global step budget is spent one step per (instruction,
// state) pair, so a long enough handler exhausts it before reaching the
// return and silently reports no continuation at all. Main's linear
// scanner had no budget and always found the offset.
var words = new List<uint>
{
0x7FE802A6u, // mflr r31
0x3BFF0014u // addi r31,r31,20
};
for (var i = 0; i < 10_010; i++)
{
words.Add(0x60000000u); // nop
}
words.Add(0x7FE803A6u); // mtlr r31
words.Add(0x4E800020u); // blr
Assert.Equal(new[] { 20 }, DiscoverOffsets(words.ToArray()));
}
[Fact]
public void LargeBranchingHandlerStillDiscoversCtrSkip()
{
// Same budget, reached far sooner once the handler branches: this is the
// bctr shape the pre-PR scanner discovered at any function size.
var words = new List<uint> { 0x7FE802A6u }; // mflr r31
for (var i = 0; i < 160; i++)
{
var displacement = (uint)((i + 1) * 4 & 0xFFFF);
words.Add(0x2C030000u); // cmpwi r3,0
words.Add(0x4182000Cu); // beq +0xC
words.Add(0x3BDF0000u | displacement); // addi r30,r31,disp
words.Add(0x48000008u); // b +8
words.Add(0x3BBF0000u | displacement); // addi r29,r31,disp
}
words.Add(0x397F0008u); // addi r11,r31,8
words.Add(0x7D6903A6u); // mtctr r11
words.Add(0x4E800420u); // bctr
Assert.Equal(new[] { 8 }, DiscoverOffsets(words.ToArray()));
}
[Fact]
public void FloatStoreOverSavedSlotInvalidatesStackTracking()
{
// stfd writes 0x10..0x17, which covers the slot the adjusted LR was
// saved to. Only stw/stwu invalidate slots today, so the reload is
// credited with a return address the stack no longer holds.
var offsets = DiscoverOffsets(
0x7FE802A6u, // mflr r31
0x3BFF0014u, // addi r31,r31,20
0x93E10014u, // stw r31,0x14(r1)
0xD8410010u, // stfd f2,0x10(r1)
0x80010014u, // lwz r0,0x14(r1)
0x7C0803A6u, // mtlr r0
0x4E800020u);// blr
Assert.Empty(offsets);
}
[Fact]
public void StoreMultipleOverSavedSlotInvalidatesStackTracking()
{
var offsets = DiscoverOffsets(
0x7FE802A6u, // mflr r31
0x3BFF0014u, // addi r31,r31,20
0x93E10008u, // stw r31,8(r1)
0xBFC10008u, // stmw r30,8(r1)
0x80010008u, // lwz r0,8(r1)
0x7C0803A6u, // mtlr r0
0x4E800020u);// blr
Assert.Empty(offsets);
}
[Fact]
public void StackPointerUpdatePreservesAdjustedLrOffset()
{
var offsets = DiscoverOffsets(
0x7C2802A6u, // mflr r1
0xDC410004u, // stfdu f2,4(r1)
0x7C2803A6u, // mtlr r1
0x4E800020u);// blr
Assert.Equal(new[] { 4 }, offsets);
}
[Fact]
public void VolatileRegisterDoesNotSurviveHelperCall()
{
// r3 is caller-saved, so the callee is free to destroy the adjusted
// return address this hook staged before the call.
var offsets = DiscoverOffsets(
0x7C6802A6u, // mflr r3
0x38630014u, // addi r3,r3,20
0x48000101u, // bl helper outside this function
0x7C6803A6u, // mtlr r3
0x4E800020u);// blr
Assert.Empty(offsets);
}
[Fact]
public void CtrDoesNotSurviveHelperCall()
{
// CTR is volatile across a call for the same reason.
var offsets = DiscoverOffsets(
0x7FE802A6u, // mflr r31
0x3BFF0014u, // addi r31,r31,20
0x7FE903A6u, // mtctr r31
0x48000101u, // bl helper outside this function
0x4E800420u);// bctr
Assert.Empty(offsets);
}
[Fact]
public void MflrR1InvalidatesOldStackSlots()
{
// After mflr r1, 8(r1) refers to incoming LR + 8, not the old
// stack slot. Its contents are unknown; do not invent a +20 return.
Assert.Empty(DiscoverOffsets(
0x7FE802A6u, // mflr r31
0x3BFF0014u, // addi r31,r31,20
0x93E10008u, // stw r31,8(r1)
0x7C2802A6u, // mflr r1
0x80010008u, // lwz r0,8(r1)
0x7C0803A6u, // mtlr r0
0x4E800020u)); // blr
}
[Fact]
public void AddiR1UpdatesLrRelativeOffset()
{
// Like StackPointerUpdatePreservesAdjustedLrOffset, r1 holds incoming
// LR here. Updating r1 must update that relation as well as stack state.
Assert.Equal(new[] { 4 }, DiscoverOffsets(
0x7C2802A6u, // mflr r1
0x38210004u, // addi r1,r1,4
0x7C2803A6u, // mtlr r1
0x4E800020u)); // blr
}
[Theory]
[InlineData(0x38210004u, 4)] // addi r1,r1,4
[InlineData(0x30210004u, 4)] // addic r1,r1,4
[InlineData(0x94210004u, 4)] // stwu r1,4(r1)
[InlineData(0xD4410004u, 4)] // stfsu f2,4(r1)
[InlineData(0xDC410004u, 4)] // stfdu f2,4(r1)
[InlineData(0x3821FFFCu, -4)] // addi r1,r1,-4
public void StackPointerUpdatesPreserveLrRelation(uint update, int expectedOffset)
{
Assert.Equal(new[] { expectedOffset }, DiscoverOffsets(
0x7C2802A6u, // mflr r1
update,
0x7C2803A6u, // mtlr r1
0x4E800020u)); // blr
}
[Theory]
[InlineData(0x7FE1FB78u)] // mr r1,r31
[InlineData(0x383F0000u)] // addi r1,r31,0
public void CopyingLrIntoR1PreservesReturnButInvalidatesOldStack(uint copy)
{
var prefix = new uint[]
{
0x7FE802A6u, // mflr r31
0x3BFF0014u, // addi r31,r31,20
0x93E10008u, // stw r31,8(r1)
copy,
};
Assert.Equal(new[] { 20 }, DiscoverOffsets(
prefix.Concat(new uint[] { 0x7C2803A6u, 0x4E800020u }).ToArray()));
Assert.Empty(DiscoverOffsets(prefix.Concat(new uint[]
{
0x80010008u, // lwz r0,8(r1): no longer the old stack slot
0x7C0803A6u, // mtlr r0
0x4E800020u,
}).ToArray()));
}
[Fact]
public void StackPointerSelfMovePreservesSavedLr()
{
Assert.Equal(new[] { 20 }, DiscoverOffsets(
0x7FE802A6u, // mflr r31
0x3BFF0014u, // addi r31,r31,20
0x93E10008u, // stw r31,8(r1)
0x7C210B78u, // mr r1,r1
0x80010008u, // lwz r0,8(r1)
0x7C0803A6u, // mtlr r0
0x4E800020u));
}
[Fact]
public void IncompleteInstructionListDoesNotInventFallthroughAcrossGap()
{
// Defensive incomplete-input test, not a production disassembly trace:
// the missing instruction could overwrite r31 or branch elsewhere.
// Address sorting alone does not establish a fallthrough edge.
var instructions = new[]
{
PpcDecoder.Decode(0x81800000u, 0x7FE802A6u), // mflr r31
PpcDecoder.Decode(0x81800008u, 0x3BFF0014u), // addi r31,r31,20
PpcDecoder.Decode(0x8180000Cu, 0x7FE803A6u), // mtlr r31
PpcDecoder.Decode(0x81800010u, 0x4E800020u), // blr
};
Assert.Empty(ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(instructions));
}
private static uint AddiR31(int offset) => 0x3BFF0000u | (uint)(offset & 0xFFFF);
private static int[] DiscoverOffsets(params uint[] words)
{
const uint entry = 0x81800000u;
var memory = new byte[words.Length * 4];
for (var i = 0; i < words.Length; i++)
{
BinaryPrimitives.WriteUInt32BigEndian(memory.AsSpan(i * 4, 4), words[i]);
}
var range = AddressRange.FromStartAndSize(entry, (uint)memory.Length);
var image = new ProgramImage(memory, range, range, default, "lr-continuation-test", entry);
using var disassembler = new PpcDisassembler();
// Use the production reachable-instruction traversal and ordering,
// rather than handing the planner an artificial execution trace.
var instructions = disassembler.DisassembleFunction(
image, entry, maxInstructions: words.Length + 1, maxBytes: memory.Length);
return ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(instructions)
.Distinct().OrderBy(offset => offset).ToArray();
}
}
@@ -35,39 +35,6 @@ public class RetroWfcPayloadLoweringTests
Assert.Equal("moduleFunction", pointer.TargetKind); Assert.Equal("moduleFunction", pointer.TargetKind);
} }
[Fact]
public void ProductionPayloadValidatesAndTranslatesEverySupportedPatch()
{
var payloadRoot = Path.Combine(
AppContext.BaseDirectory,
"TestAssets",
"RetroWfcPayload");
WiiCompiled.Setup.Common.RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(payloadRoot);
var payloadPath = Path.Combine(
payloadRoot,
"binary",
"payload.RMCPD00.bin");
var payload = File.ReadAllBytes(payloadPath);
var result = RetroWfcPayload.Parse(
payload,
ProductionPayloadManifest(),
0x81800000u,
0x00200000u,
"TestAssets/RetroWfcPayload/binary/payload.RMCPD00.bin");
Assert.Equal("RMCPD00", result.Summary.Game);
Assert.Equal(payload.Length, result.Summary.PayloadImageSize);
Assert.True(result.LoweringPlan.IsPlannable);
Assert.Empty(result.LoweringPlan.Issues);
Assert.NotEmpty(result.LoweringPlan.StaticBytePatches);
Assert.NotEmpty(result.LoweringPlan.ExecutableHooks);
Assert.NotEmpty(result.LoweringPlan.StaticPointers);
Assert.All(result.LoweringPlan.ExecutableHooks, hook => Assert.NotNull(hook.TargetAddress));
Assert.All(result.LoweringPlan.StaticPointers, pointer => Assert.NotNull(pointer.TargetAddress));
Assert.NotEmpty(result.Summary.InitializationCallbacks);
}
private static BaseManifest TestManifest() => private static BaseManifest TestManifest() =>
new( new(
"test", "test",
@@ -85,51 +52,6 @@ public class RetroWfcPayloadLoweringTests
], ],
"ranges.json"); "ranges.json");
// The payload parser needs the base image's address classes and containing
// function ranges to prove every patch can be lowered. A single synthetic
// executable and writable ranges are sufficient here: the assertions above
// test the real production payload without checking proprietary game bytes
// into CI. The split also proves pointer patches lower as data writes.
private static BaseManifest ProductionPayloadManifest() =>
new(
"test",
1,
"RMCP01",
"P",
"",
0,
[
new BaseSectionMetadata(
".synthetic-text",
"synthetic.dol",
0x80000000u,
0x80800000u,
true,
false,
"synthetic_text.bin",
0),
new BaseSectionMetadata(
".synthetic-data",
"synthetic.dol",
0x80800000u,
0x81000000u,
false,
true,
"synthetic_data.bin",
0)
],
[
new BaseFunctionRangeMetadata(
0x80000000u,
0x80800000u,
"synthetic_base",
".synthetic-text",
0,
"test",
["Executable"])
],
"ranges.json");
private static byte[] BuildSharedPayloadFixture() private static byte[] BuildSharedPayloadFixture()
{ {
var payload = new byte[0x240]; var payload = new byte[0x240];
@@ -26,11 +26,6 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Translator.Core\Translator.Core.csproj" /> <ProjectReference Include="..\..\src\Translator.Core\Translator.Core.csproj" />
<ProjectReference Include="..\..\src\Translator.Cli\Translator.Cli.csproj" /> <ProjectReference Include="..\..\src\Translator.Cli\Translator.Cli.csproj" />
<ProjectReference Include="..\..\..\Launcher\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="TestAssets\RetroWfcPayload\binary\payload.RMCPD00.bin" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup> </ItemGroup>
<!-- <!--