mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-15 10:56:47 -04:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fb593749e | |||
| 8ec3a1b752 | |||
| 8705e957c7 | |||
| 8e0cc96898 | |||
| 6458ec6abe | |||
| 209405dfb7 | |||
| 4bdaff01fc | |||
| b555ede2d3 | |||
| 53d8f71c68 | |||
| 149cfef608 | |||
| 25c69ae28e | |||
| 0bb15f0a44 | |||
| 466d06d7db |
@@ -281,7 +281,7 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
|
||||
|
||||
$manifest = [ordered]@{
|
||||
SchemaVersion = 2
|
||||
ProductVersion = '0.2.31'
|
||||
ProductVersion = '0.2.32'
|
||||
ExpectedGameId = $pins.GameId
|
||||
ExpectedDolSha256 = $pins.DolSha256
|
||||
ExpectedRelSha256 = $pins.RelSha256
|
||||
|
||||
@@ -39,9 +39,9 @@ $packages = @(
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'dawn_prebuilt'; File = 'dawn-v20260603.191052-windows-amd64.tar.gz'
|
||||
Uris = @('https://github.com/encounter/dawn-build/releases/download/v20260603.191052/dawn-windows-amd64.tar.gz')
|
||||
Uris = @('https://github.com/theofficialgman/dawn-build/releases/download/v20260603.191052/dawn-windows-amd64.tar.gz')
|
||||
Pins = @(@{ File = $auroraCMake; Text = 'set(AURORA_DAWN_VERSION "v20260603.191052"' },
|
||||
@{ File = $auroraDawn; Text = 'SHA256=7785373d569b3b0237918ec9c523239f7d0667857c5ea8242e3cdfde95e6aeab' })
|
||||
@{ File = $auroraDawn; Text = 'SHA256=13be9cff8b9b179c42dcd16aeabb6effcc8f0dfdcc14463eda2a5caeda225142' })
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'fmt'; File = 'fmt-11.1.4.tar.gz'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace>
|
||||
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
|
||||
<Version>0.2.31</Version>
|
||||
<Version>0.2.32</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
|
||||
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
|
||||
<Version>0.2.31</Version>
|
||||
<Version>0.2.32</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description>
|
||||
|
||||
@@ -13,7 +13,8 @@ internal static class BuildRunner
|
||||
string workspace, string profile, string outputDir, string? baseOutputDir,
|
||||
string? retroDir, string? retroWfcOfflineDir, bool skipRetroWfcPayload,
|
||||
bool forceCleanBuild, string? translatorBin, string? ccBin, string? cxxBin, string? fuseLd,
|
||||
string? cmakeBin, string? ninjaBin, string? nativePrebuiltDir, IInstallReporter reporter,
|
||||
string? cmakeBin, string? ninjaBin, string? nativePrebuiltDir, string? sysroot,
|
||||
IInstallReporter reporter,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var script = Path.Combine(workspace, "Launcher", "local-build.sh");
|
||||
@@ -77,6 +78,10 @@ internal static class BuildRunner
|
||||
{
|
||||
startInfo.ArgumentList.Add("--native-prebuilt-dir"); startInfo.ArgumentList.Add(nativePrebuiltDir);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(sysroot))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--sysroot"); startInfo.ArgumentList.Add(sysroot);
|
||||
}
|
||||
|
||||
using var process = new Process { StartInfo = startInfo };
|
||||
var window = new BuildProgressWindow(reporter, InstallStages.Build, start: 6, end: 96);
|
||||
|
||||
@@ -3,7 +3,7 @@ namespace WiiCompiled.Setup.Linux;
|
||||
internal static class ProductInfo
|
||||
{
|
||||
public const string Name = "WiiCompiled";
|
||||
public const string Version = "0.2.31";
|
||||
public const string Version = "0.2.32";
|
||||
}
|
||||
|
||||
/// <summary>One installed product's record inside install-state.json.</summary>
|
||||
|
||||
@@ -143,6 +143,15 @@ internal static class Program
|
||||
retroWfcOfflineDir = cacheDir;
|
||||
}
|
||||
|
||||
var sysroot = flags.GetValueOrDefault("sysroot");
|
||||
// --sysroot explicitly provided (even as bare flag at end of argv, which ParseArgs
|
||||
// stores as null) must carry a path; omitting --sysroot entirely is fine (local-build.sh
|
||||
// adds -UCMAKE_SYSROOT to clear any stale cached value from a prior configure).
|
||||
if (flags.ContainsKey("sysroot") && string.IsNullOrWhiteSpace(sysroot))
|
||||
{
|
||||
throw new ArgumentException("--sysroot requires a non-empty directory path.");
|
||||
}
|
||||
|
||||
await BuildRunner.RunAsync(
|
||||
workspace, profile, installDir, baseInstallDir,
|
||||
retroDir,
|
||||
@@ -156,6 +165,7 @@ internal static class Program
|
||||
flags.GetValueOrDefault("cmake"),
|
||||
flags.GetValueOrDefault("ninja"),
|
||||
flags.GetValueOrDefault("native-prebuilt-dir"),
|
||||
sysroot,
|
||||
reporter, token);
|
||||
|
||||
reporter.Progress(InstallStages.Shortcuts, "Creating shortcuts", 98);
|
||||
@@ -324,7 +334,7 @@ internal static class Program
|
||||
{--download-retro-wfc-payload | --skip-retro-wfc-payload}]
|
||||
[--force-clean-build] [--translator-bin PATH] [--disc-tool-bin PATH]
|
||||
[--cc PATH] [--cxx PATH] [--fuse-ld NAME_OR_PATH] [--cmake PATH] [--ninja PATH]
|
||||
[--native-prebuilt-dir DIR] [--progress-json] [--workspace DIR]
|
||||
[--native-prebuilt-dir DIR] [--sysroot PATH] [--progress-json] [--workspace DIR]
|
||||
uninstall
|
||||
launch-base
|
||||
launch-retro
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>WiiCompiled.Setup.Linux</AssemblyName>
|
||||
<RootNamespace>WiiCompiled.Setup.Linux</RootNamespace>
|
||||
<Version>0.2.31</Version>
|
||||
<Version>0.2.32</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Command-line installer and launcher for WiiCompiled on Linux</Description>
|
||||
|
||||
@@ -121,7 +121,7 @@ internal static class PlatformChecks
|
||||
internal static class ProductInfo
|
||||
{
|
||||
public const string Name = "WiiCompiled";
|
||||
public const string Version = "0.2.31";
|
||||
public const string Version = "0.2.32";
|
||||
|
||||
/// <summary>
|
||||
/// The setup executable is copied into the installation under this name. It is the launcher and
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<AssemblyName>WiiCompiled.Setup</AssemblyName>
|
||||
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Version>0.2.31</Version>
|
||||
<Version>0.2.32</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Command-line installer and launcher for WiiCompiled</Description>
|
||||
|
||||
@@ -65,6 +65,7 @@ translator_dll_override=""
|
||||
translator_bin_override=""
|
||||
fuse_ld_override=""
|
||||
native_prebuilt_dir=""
|
||||
sysroot=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
@@ -87,6 +88,8 @@ Usage: local-build.sh --output-dir DIR [options]
|
||||
--translator-bin PATH Self-contained Translator.Cli executable (skips building AND needs no dotnet at all)
|
||||
--native-prebuilt-dir DIR Precompiled aurora/third-party package (see Prepare-NativePrebuilt.sh);
|
||||
skips compiling aurora-main from source entirely
|
||||
--sysroot PATH Passed to CMake as -DCMAKE_SYSROOT: where the compiler resolves
|
||||
standard headers/startup files
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -110,6 +113,7 @@ while [[ $# -gt 0 ]]; do
|
||||
--translator-dll) translator_dll_override=$2; shift 2 ;;
|
||||
--translator-bin) translator_bin_override=$2; shift 2 ;;
|
||||
--native-prebuilt-dir) native_prebuilt_dir=$2; shift 2 ;;
|
||||
--sysroot) sysroot=$2; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown argument: $1" ;;
|
||||
esac
|
||||
@@ -414,6 +418,14 @@ fi
|
||||
if [[ -n "$native_prebuilt_dir" ]]; then
|
||||
configure_args+=(-DMKW_NATIVE_PREBUILT_DIR="$native_prebuilt_dir")
|
||||
fi
|
||||
if [[ -n "$sysroot" ]]; then
|
||||
configure_args+=(-DCMAKE_SYSROOT="$sysroot")
|
||||
else
|
||||
# Explicitly clear any cached CMAKE_SYSROOT from a prior configure so an
|
||||
# incremental build that transitions from one sysroot to none does not
|
||||
# silently keep the stale cached path.
|
||||
configure_args+=(-UCMAKE_SYSROOT)
|
||||
fi
|
||||
|
||||
log_step configure-native "Configuring the native toolchain"
|
||||
"$cmake_bin" "${configure_args[@]}"
|
||||
|
||||
@@ -52,13 +52,13 @@ done
|
||||
|
||||
case "$arch" in
|
||||
x86_64) llvm_release_arch=X64; target_triple=x86_64-unknown-linux-gnu
|
||||
llvm_release_sha256=df0e1ecf16caf3489a272a5eea4eec9b0d82878f6477fa309504f918a0006384
|
||||
llvm_release_sha256=fccecb1906e7ddf5ec040aec5b646b650e2daaafa4423b41341c4717db5bdec0
|
||||
cmake_release_arch=x86_64
|
||||
cmake_sha256=927b2368a946c37269c3a66225ab00544e756459cdd0b5d0da438694fb9ff802
|
||||
ninja_asset=ninja-linux.zip
|
||||
ninja_sha256=5749cbc4e668273514150a80e387a957f933c6ed3f5f11e03fb30955e2bbead6 ;;
|
||||
aarch64) llvm_release_arch=ARM64; target_triple=aarch64-unknown-linux-gnu
|
||||
llvm_release_sha256=805efad2bb91cb4967fa569e0881d10c0f69c04461cf671cccbae19f547acc34
|
||||
llvm_release_sha256=d431eff9f064c86ee7c4c94af570a8f74fcccd1f74c6f0da3af32ce34a1e1b05
|
||||
cmake_release_arch=aarch64
|
||||
cmake_sha256=9ea38356dbd3e32e51029a3e09a0f2f8e117ef4fbcaad7a21ffb36409bbd5cb4
|
||||
ninja_asset=ninja-linux-aarch64.zip
|
||||
@@ -101,11 +101,14 @@ rm -rf "$work"
|
||||
mkdir -p "$work/bin" "$work/lib/$target_triple" "$work/include/$target_triple/c++/v1"
|
||||
|
||||
# --- clang/lld/llvm-ar, pruned from the official LLVM release ---
|
||||
|
||||
llvm_archive_name="LLVM-$llvm_version-Linux-$llvm_release_arch.tar.xz"
|
||||
# built from PR https://github.com/llvm/llvm-project/pull/222821 on official LLVM Github Actions Runner
|
||||
# only switch to an official stable LLVM release again once:
|
||||
# - this PR has merged https://github.com/llvm/llvm-project/pull/221365 and been backported to LLVM stable branch
|
||||
# - this bug has been fixed with a workaround in the Wiicompiled translator https://github.com/patchzyy/Wiicompiled/issues/208 or in LLVM and been backported to LLVM stable branch
|
||||
llvm_archive_name="LLVM-PR222821-5ae1c7c43a11b4cdc5ce4dd483c28357bab7dae2-Linux-$llvm_release_arch.tar.xz"
|
||||
llvm_archive="$downloads/$llvm_archive_name"
|
||||
download_verified "$llvm_archive" \
|
||||
"https://github.com/llvm/llvm-project/releases/download/llvmorg-$llvm_version/$llvm_archive_name" \
|
||||
"https://github.com/theofficialgman/llvm-project/releases/download/llvmorg-22.1.8-patched/$llvm_archive_name" \
|
||||
"$llvm_release_sha256"
|
||||
|
||||
extract_root="$script_dir/artifacts/.extract-clang-$arch"
|
||||
@@ -113,7 +116,7 @@ rm -rf "$extract_root"
|
||||
mkdir -p "$extract_root"
|
||||
echo "prepare-portable-tools.sh: extracting $llvm_archive_name (this is the full ~1.9 GiB release; only a fraction is kept)..."
|
||||
tar -xf "$llvm_archive" -C "$extract_root"
|
||||
src="$extract_root/LLVM-$llvm_version-Linux-$llvm_release_arch"
|
||||
src="$extract_root/${llvm_archive_name%.tar.xz}"
|
||||
[[ -d "$src" ]] || { echo "prepare-portable-tools.sh: unexpected archive layout, expected $src" >&2; exit 1; }
|
||||
|
||||
echo "prepare-portable-tools.sh: pruning to the minimal compile+link toolchain..."
|
||||
@@ -165,7 +168,8 @@ rm -rf "$cmake_extract_root"
|
||||
mkdir -p "$cmake_extract_root"
|
||||
echo "prepare-portable-tools.sh: extracting $cmake_archive_name..."
|
||||
tar -xzf "$cmake_archive" -C "$cmake_extract_root"
|
||||
cmake_src="$cmake_extract_root/cmake-$cmake_version-linux-$cmake_release_arch"
|
||||
|
||||
cmake_src="$cmake_extract_root/${cmake_archive_name%.tar.gz}"
|
||||
[[ -d "$cmake_src" ]] || { echo "prepare-portable-tools.sh: unexpected archive layout, expected $cmake_src" >&2; exit 1; }
|
||||
|
||||
mkdir -p "$work/share/cmake-$cmake_share_version"
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
<img width="4190" height="1232" alt="wiicomplogofinalfinalfinalev2MADEBY_INKWRECK_plzcredit" src="https://github.com/user-attachments/assets/df7a3f2e-5336-479a-b4c0-968dd578726d" />
|
||||
|
||||
# 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&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&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&logoColor=white"></a>
|
||||
</p>
|
||||
|
||||
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
|
||||
@@ -54,10 +67,24 @@ Press **F10** while the game window has focus:
|
||||
Everything you change is saved to `Config.toml` on the spot and restored next launch.
|
||||
|
||||
**Real controller support.**
|
||||
|
||||
Controllers are fed to the game as a GameCube controller.
|
||||
Mappings are positional (`south`, `east`, `west`, `north`) rather than Xbox-labelled, so the
|
||||
same config makes sense on Xbox, PlayStation, Nintendo and generic SDL pads alike, and extra
|
||||
inputs like paddles, touchpads and share buttons show up when the hardware reports them.
|
||||
Both button-binding slots also accept SDL triggers and stick directions. Selecting an analog
|
||||
input shows a threshold slider beneath it (1–100%, default 50%); reaching that amount of travel
|
||||
holds the chosen digital button. Each binding's threshold is saved independently in `Config.toml`
|
||||
(for example, `a = "right_trigger@35,south"`).
|
||||
|
||||
**Keyboard and Mouse support.**
|
||||
|
||||
Keyboard and mouse are also available through **F10 > Controller settings > Keyboard and mouse**
|
||||
for each port. Enabling this replaces that port's gamepad input. The default preset uses WASD
|
||||
for the main stick, left mouse for A (accelerate), Space for B (brake), right mouse for R
|
||||
(drift), middle mouse for Z (item), arrow keys for the D-pad (tricks), and Enter for Start.
|
||||
Keys and mouse buttons can be remapped, including both sticks and triggers; mouse movement
|
||||
is not used. These settings are saved in `keyboard_bindings.dat` and restored next launch.
|
||||
|
||||
**Dolphin-compatible input expressions.**
|
||||
Each GameCube control can carry an expression in Dolphin's input syntax, with the same operators
|
||||
@@ -201,7 +228,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).
|
||||
|
||||
## Credits
|
||||
|
||||
- **inkwreck** - making the logo
|
||||
- **[aurora](https://github.com/encounter/aurora)** - the GX rendering/windowing backend this
|
||||
project's whole graphics layer sits on. MIT licensed.
|
||||
- **[Dawn](https://dawn.googlesource.com/dawn)** - Google's WebGPU implementation, powering
|
||||
|
||||
@@ -151,15 +151,36 @@ elseif (_aurora_dawn_provider STREQUAL "package")
|
||||
endif ()
|
||||
endif ()
|
||||
set(AURORA_DAWN_PACKAGE_URL
|
||||
"https://github.com/encounter/dawn-build/releases/download/${AURORA_DAWN_VERSION}/dawn-${_dawn_system}-${_dawn_arch}.tar.gz")
|
||||
"https://github.com/theofficialgman/dawn-build/releases/download/${AURORA_DAWN_VERSION}/dawn-${_dawn_system}-${_dawn_arch}.tar.gz")
|
||||
|
||||
# A release asset is mutable: the same tag has already served two different windows-amd64 archives,
|
||||
# and a cached extraction is never re-verified. Pin the digest for the combinations we ship.
|
||||
if (NOT AURORA_DAWN_PACKAGE_URL_HASH
|
||||
AND AURORA_DAWN_VERSION STREQUAL "v20260603.191052"
|
||||
AND _dawn_system STREQUAL "windows" AND _dawn_arch STREQUAL "amd64")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=7785373d569b3b0237918ec9c523239f7d0667857c5ea8242e3cdfde95e6aeab")
|
||||
if (NOT AURORA_DAWN_PACKAGE_URL_HASH AND AURORA_DAWN_VERSION STREQUAL "v20260603.191052")
|
||||
if (_dawn_system STREQUAL "windows" AND _dawn_arch STREQUAL "amd64")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=13be9cff8b9b179c42dcd16aeabb6effcc8f0dfdcc14463eda2a5caeda225142")
|
||||
elseif (_dawn_system STREQUAL "windows" AND _dawn_arch STREQUAL "arm64")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=bf2d921110f14a1d6553f673c5597988e66c02af5587e4a1fee167937d247734")
|
||||
elseif (_dawn_system STREQUAL "linux" AND _dawn_arch STREQUAL "x86_64")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=7adcf241bb2a24ec0c576609f2d67203e0e65db9c5a286ca2bbb6281fa644b35")
|
||||
elseif (_dawn_system STREQUAL "linux" AND _dawn_arch STREQUAL "aarch64")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=2415e253d46f91b2d72fc73bf6055fb31b98b67c773dc546e1991b1cf019732f")
|
||||
elseif (_dawn_system STREQUAL "darwin" AND _dawn_arch STREQUAL "arm64")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=0a8ea8eb0159fc0ba1083c52155d9376fb173cffe690b400464a6ad8881bb461")
|
||||
elseif (_dawn_system STREQUAL "darwin" AND _dawn_arch STREQUAL "x86_64")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=5fe2c7a2a8b4cb82acee4af16779a83ae333c7657b9dc1a5008f5fd1f5ad5f80")
|
||||
elseif (_dawn_system STREQUAL "ios" AND _dawn_arch STREQUAL "arm64")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=f97701d26fd1f25bbcc260b4c31736ede134c730c12556029e2470fde967f424")
|
||||
elseif (_dawn_system STREQUAL "android" AND _dawn_arch STREQUAL "aarch64")
|
||||
set(AURORA_DAWN_PACKAGE_URL_HASH
|
||||
"SHA256=0e63e8cbf53551f703f582d1306f4257c0380353f66b53369d96952ce6d9f934")
|
||||
endif ()
|
||||
endif ()
|
||||
endif ()
|
||||
message(STATUS "aurora: Fetching prebuilt Dawn package from ${AURORA_DAWN_PACKAGE_URL}")
|
||||
|
||||
@@ -171,6 +171,22 @@ typedef struct PADButtonMapping {
|
||||
PADButton padButton;
|
||||
} PADButtonMapping;
|
||||
|
||||
// Explicitly disabled, unlike INVALID which permits default L/R trigger input.
|
||||
#define PAD_NATIVE_BUTTON_DISABLED 0xfffffffeu
|
||||
|
||||
// Axis-to-button bindings share the persisted nativeButton field without
|
||||
// changing the binary layout of existing controller mapping files.
|
||||
constexpr u32 PADEncodeAxisButton(u32 axis, bool negative, u32 threshold = 50) {
|
||||
return 0x10000u | axis | (negative ? 0x80u : 0u) | (threshold << 8);
|
||||
}
|
||||
constexpr bool PADIsAxisButton(u32 binding) { return (binding & 0xffff0000u) == 0x10000u; }
|
||||
constexpr u32 PADAxisButtonThreshold(u32 binding) { return (binding >> 8) & 0xffu; }
|
||||
constexpr u32 PADAxisButtonAxis(u32 binding) { return binding & 0x7fu; }
|
||||
constexpr bool PADAxisButtonNegative(u32 binding) { return (binding & 0x80u) != 0; }
|
||||
constexpr u32 PADAxisButtonIdentity(u32 binding) {
|
||||
return PADIsAxisButton(binding) ? (binding & ~0xff00u) : binding;
|
||||
}
|
||||
|
||||
typedef struct PADAxisMapping {
|
||||
PADSignedNativeAxis nativeAxis;
|
||||
s32 nativeButton;
|
||||
|
||||
@@ -319,6 +319,18 @@ std::array<bool, PAD_CHANMAX> g_suppressLeftTrigger{};
|
||||
std::array<bool, PAD_CHANMAX> g_suppressRightTrigger{};
|
||||
|
||||
bool is_mouse_scancode(const s32 scancode) { return scancode < PAD_KEY_INVALID; }
|
||||
bool is_native_binding_pressed(SDL_Gamepad* gamepad, u32 binding) {
|
||||
if (PADIsAxisButton(binding)) {
|
||||
const u32 axis = PADAxisButtonAxis(binding);
|
||||
const u32 threshold = PADAxisButtonThreshold(binding);
|
||||
if (axis >= SDL_GAMEPAD_AXIS_COUNT || threshold < 1 || threshold > 100) return false;
|
||||
int value = SDL_GetGamepadAxis(gamepad, static_cast<SDL_GamepadAxis>(axis));
|
||||
if (PADAxisButtonNegative(binding)) value = -value;
|
||||
return value > 0 && value * 100 >= static_cast<int>(threshold) * 32767;
|
||||
}
|
||||
return binding < SDL_GAMEPAD_BUTTON_COUNT &&
|
||||
SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(binding));
|
||||
}
|
||||
bool is_mouse_button_pressed(const s32 scancode) {
|
||||
const int32_t buttonNum = -(scancode + 1);
|
||||
if (buttonNum < 1 || buttonNum > 5) {
|
||||
@@ -724,10 +736,10 @@ u32 PADRead(PADStatus* status) {
|
||||
}
|
||||
|
||||
status[i].err = PAD_ERR_NONE;
|
||||
if (g_keyboardBindings[i].m_mappingsSet) {
|
||||
if (g_keyboardBindings[i].m_mappingsSet && SDL_GetKeyboardFocus() != nullptr) {
|
||||
std::ranges::for_each(
|
||||
g_keyboardBindings[i].m_buttonMapping, [&kbState, &i, &status](const PADKeyButtonBinding& mapping) {
|
||||
if (mapping.scancode > PAD_KEY_INVALID && kbState[mapping.scancode]) {
|
||||
g_keyboardBindings[i].m_buttonMapping, [&kbState, &numKeys, &i, &status](const PADKeyButtonBinding& mapping) {
|
||||
if (mapping.scancode > PAD_KEY_INVALID && mapping.scancode < numKeys && kbState[mapping.scancode]) {
|
||||
status[i].button |= mapping.padButton;
|
||||
} else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) {
|
||||
status[i].button |= mapping.padButton;
|
||||
@@ -788,7 +800,7 @@ u32 PADRead(PADStatus* status) {
|
||||
status[i].triggerRight = static_cast<u8>(std::min(static_cast<int>(status[i].triggerRight) + tr, 255));
|
||||
}
|
||||
|
||||
if (controller) {
|
||||
if (controller && !g_keyboardBindings[i].m_mappingsSet) {
|
||||
EnsureMappingLoaded(controller);
|
||||
|
||||
// Wii U Pro Controller raw D-pad fallback. SDL's HIDAPI Wii driver posts
|
||||
@@ -835,7 +847,7 @@ u32 PADRead(PADStatus* status) {
|
||||
bool rightTriggerSet = false;
|
||||
std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet,
|
||||
&rightTriggerSet](const auto& mapping) {
|
||||
if (SDL_GetGamepadButton(controller->m_controller, static_cast<SDL_GamepadButton>(mapping.nativeButton))) {
|
||||
if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) {
|
||||
status[i].button |= mapping.padButton;
|
||||
}
|
||||
|
||||
@@ -852,7 +864,7 @@ u32 PADRead(PADStatus* status) {
|
||||
if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) {
|
||||
return;
|
||||
}
|
||||
if (SDL_GetGamepadButton(controller->m_controller, static_cast<SDL_GamepadButton>(mapping.nativeButton))) {
|
||||
if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) {
|
||||
status[i].button |= mapping.padButton;
|
||||
}
|
||||
|
||||
@@ -946,6 +958,17 @@ u32 PADRead(PADStatus* status) {
|
||||
Sint16 tl = std::max(static_cast<Sint16>(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_L));
|
||||
Sint16 tr = std::max(static_cast<Sint16>(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_R));
|
||||
|
||||
// Games can read either the digital L/R bits or their analog pressure.
|
||||
// An explicit button binding must drive both, otherwise the original
|
||||
// L2/R2 axis still activates L/R even when it was rebound to L1/R1.
|
||||
// Real GC pads retain independent analog travel and end-stop clicks.
|
||||
if (!(controller->m_isGameCube ||
|
||||
(SDL_GetGamepadType(controller->m_controller) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO &&
|
||||
controller->m_pid == 0x2073))) {
|
||||
if (leftTriggerSet) tl = (status[i].button & PAD_TRIGGER_L) != 0 ? 32767 : 0;
|
||||
if (rightTriggerSet) tr = (status[i].button & PAD_TRIGGER_R) != 0 ? 32767 : 0;
|
||||
}
|
||||
|
||||
if (controller->m_deadZones.emulateTriggers) {
|
||||
if (!leftTriggerSet && tl > controller->m_deadZones.leftTriggerActivationZone) {
|
||||
status[i].button |= PAD_TRIGGER_L;
|
||||
@@ -990,12 +1013,13 @@ void PADControlMotor(const u32 chan, const u32 cmd) {
|
||||
}
|
||||
|
||||
if (controller->m_isGameCube) {
|
||||
if (cmd == PAD_MOTOR_STOP) {
|
||||
aurora::input::controller_rumble(instance, 0, 1, 0);
|
||||
if (cmd == PAD_MOTOR_STOP || cmd == PAD_MOTOR_STOP_HARD) {
|
||||
// Use an unambiguous motor-off request. The (0, 1) coast encoding
|
||||
// requires SDL's GameCube brake mode; other backends or an overridden
|
||||
// hint interpret it as rumble and can leave the controller vibrating.
|
||||
aurora::input::controller_rumble(instance, 0, 0, 0);
|
||||
} else if (cmd == PAD_MOTOR_RUMBLE) {
|
||||
aurora::input::controller_rumble(instance, 1, 1, 0);
|
||||
} else if (cmd == PAD_MOTOR_STOP_HARD) {
|
||||
aurora::input::controller_rumble(instance, 0, 0, 0);
|
||||
}
|
||||
} else {
|
||||
if (cmd == PAD_MOTOR_STOP) {
|
||||
@@ -1278,6 +1302,11 @@ BOOL PADSetKeyButtonBindings(const u32 port, PADKeyButtonBinding bindings[PAD_BU
|
||||
}
|
||||
|
||||
PADKeyButtonBinding* PADGetKeyButtonBindings(const u32 port, u32* buttonCount) {
|
||||
PADInit();
|
||||
if (!g_keyboardBindingsLoaded) {
|
||||
g_keyboardBindingsLoaded = true;
|
||||
load_keyboard_bindings();
|
||||
}
|
||||
if (port >= PAD_MAX_CONTROLLERS || !g_keyboardBindings[port].m_mappingsSet) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ Due to legal requirements, no proprietary Nintendo assets or code are included i
|
||||
3. **Retro Rewind Distribution** *(Skip if only building WiiCompiled)*:
|
||||
- Download the [Retro Rewind](https://wiki.tockdom.com/wiki/Retro_Rewind) release package. You will need the `RetroRewind6` folder (which contains `Binaries/Code.pul`).
|
||||
4. **Retro-WFC Payload** *(Skip if only building WiiCompiled or building offline)*:
|
||||
- Required for online multiplayer on Retro Rewind. Downloaded during setup from `http://nas.play.rwfc.net/payload?g=RMCPD00`.
|
||||
- Required for online multiplayer on Retro Rewind. Downloaded during setup from `https://rwfc.net/api/wfc/payload?g=RMCPD00`.
|
||||
|
||||
---
|
||||
|
||||
@@ -113,7 +113,7 @@ translator() {
|
||||
```bash
|
||||
mkdir -p generated/functions build/base
|
||||
|
||||
translator translate-recursive 0x8000629c \
|
||||
translator translate-recursive 0x800060A4 \
|
||||
--project projects/mkwii/recomp.yml \
|
||||
--outdir generated/functions \
|
||||
--output-metadata generated/base_translation_output.json \
|
||||
@@ -148,7 +148,7 @@ translator emit-base-manifest \
|
||||
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" \
|
||||
curl -fsSL --retry 3 "https://rwfc.net/api/wfc/payload?g=RMCPD00" \
|
||||
-o build/retro-wfc/binary/payload.RMCPD00.bin
|
||||
|
||||
# Validate payload signature and integrity
|
||||
@@ -320,7 +320,7 @@ Launcher/local-build-macos.command \
|
||||
```bash
|
||||
# 1. Download Retro-WFC payload into a staging directory:
|
||||
mkdir -p build/retro-wfc/binary
|
||||
curl -fsSL --retry 3 "http://nas.play.rwfc.net/payload?g=RMCPD00" \
|
||||
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:
|
||||
|
||||
@@ -58,6 +58,7 @@ inline void Flush(bool force = false) {
|
||||
// still active. A window close is an intentional successful exit, so end the
|
||||
// process directly and do not run the crash/atexit paths.
|
||||
[[noreturn]] inline void ExitForAuroraWindowClose() noexcept {
|
||||
settings_overlay::ReleaseControllers();
|
||||
WindowPlacementPersistence::Flush(true);
|
||||
#if defined(_WIN32)
|
||||
::ExitProcess(0);
|
||||
|
||||
@@ -48,8 +48,19 @@ struct NativeButtonItem {
|
||||
uint32_t nativeButton;
|
||||
};
|
||||
|
||||
inline constexpr std::array<NativeButtonItem, SDL_GAMEPAD_BUTTON_COUNT + 1> kNativeButtons = {{
|
||||
{"unmapped", "Unmapped / analog trigger", PAD_NATIVE_BUTTON_INVALID},
|
||||
inline constexpr auto kNativeButtons = std::to_array<NativeButtonItem>({
|
||||
{"disabled", "Unmapped", PAD_NATIVE_BUTTON_DISABLED},
|
||||
{"left_trigger", "Left trigger (LT / L2)", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFT_TRIGGER, false)},
|
||||
{"right_trigger", "Right trigger (RT / R2)", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, false)},
|
||||
{"left_stick_left", "Left stick left", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTX, true)},
|
||||
{"left_stick_right", "Left stick right", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTX, false)},
|
||||
{"left_stick_up", "Left stick up", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTY, true)},
|
||||
{"left_stick_down", "Left stick down", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTY, false)},
|
||||
{"right_stick_left", "Right stick left", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTX, true)},
|
||||
{"right_stick_right", "Right stick right", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTX, false)},
|
||||
{"right_stick_up", "Right stick up", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTY, true)},
|
||||
{"right_stick_down", "Right stick down", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTY, false)},
|
||||
{"unmapped", "Default", PAD_NATIVE_BUTTON_INVALID},
|
||||
{"south", "South (A / Cross)", SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
{"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST},
|
||||
{"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST},
|
||||
@@ -76,7 +87,7 @@ inline constexpr std::array<NativeButtonItem, SDL_GAMEPAD_BUTTON_COUNT + 1> kNat
|
||||
{"misc4", "Misc 4 / GC R click", SDL_GAMEPAD_BUTTON_MISC4},
|
||||
{"misc5", "Misc 5", SDL_GAMEPAD_BUTTON_MISC5},
|
||||
{"misc6", "Misc 6", SDL_GAMEPAD_BUTTON_MISC6},
|
||||
}};
|
||||
});
|
||||
|
||||
inline std::string TrimToken(std::string_view token) {
|
||||
const size_t begin = token.find_first_not_of(" \t");
|
||||
@@ -88,7 +99,7 @@ inline std::string TrimToken(std::string_view token) {
|
||||
}
|
||||
|
||||
inline const NativeButtonItem* FindNativeButton(std::string_view configName) {
|
||||
const std::string name = TrimToken(configName);
|
||||
const std::string name = TrimToken(configName.substr(0, configName.find('@')));
|
||||
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
|
||||
[&](const NativeButtonItem& item) { return name == item.configName; });
|
||||
return it == kNativeButtons.end() ? nullptr : &*it;
|
||||
@@ -97,8 +108,8 @@ inline const NativeButtonItem* FindNativeButton(std::string_view configName) {
|
||||
// Falls back to the "unmapped" entry so callers always have a label to draw.
|
||||
inline const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) {
|
||||
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
|
||||
[&](const NativeButtonItem& item) { return nativeButton == item.nativeButton; });
|
||||
return it == kNativeButtons.end() ? kNativeButtons.front() : *it;
|
||||
[&](const NativeButtonItem& item) { return PADAxisButtonIdentity(nativeButton) == PADAxisButtonIdentity(item.nativeButton); });
|
||||
return it == kNativeButtons.end() ? *FindNativeButton("unmapped") : *it;
|
||||
}
|
||||
|
||||
inline const GameCubeButtonItem* FindGameCubeButton(std::string_view configKey) {
|
||||
|
||||
@@ -91,6 +91,7 @@ struct RuntimeUserConfig {
|
||||
// "dpad_up,left_shoulder") as values; pressing either bound button counts.
|
||||
std::array<std::optional<std::string>, 12> controllerButtons;
|
||||
std::optional<bool> rumbleEnabled;
|
||||
std::optional<int32_t> muteHotkey;
|
||||
std::map<std::string, std::string> controllerExpressions;
|
||||
};
|
||||
|
||||
@@ -411,6 +412,9 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
}
|
||||
|
||||
config.rumbleEnabled = FindConfigValue<bool>(document, "controller", "rumble");
|
||||
if (auto value = FindConfigInt(document, "audio", "mute_key")) {
|
||||
config.muteHotkey = *value;
|
||||
}
|
||||
|
||||
if (const auto* section = document.contains("controller") ? &document.at("controller") : nullptr;
|
||||
section != nullptr && section->is_table()) {
|
||||
@@ -710,6 +714,15 @@ inline bool SetRumbleEnabled(bool value) {
|
||||
return WriteSetting("controller", "rumble", value ? "true" : "false");
|
||||
}
|
||||
|
||||
inline int32_t MuteHotkey(int32_t fallback) {
|
||||
return Get().muteHotkey.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool SetMuteHotkey(int32_t value) {
|
||||
Mutable().muteHotkey = value;
|
||||
return WriteSetting("audio", "mute_key", std::to_string(value));
|
||||
}
|
||||
|
||||
inline bool SetAudioVolume(float value) {
|
||||
value = std::clamp(value, 0.0f, 1.0f);
|
||||
Mutable().audioVolume = value;
|
||||
|
||||
@@ -12,4 +12,6 @@ void Draw() noexcept;
|
||||
bool StartupScreenVisible() noexcept;
|
||||
void NotifyStrapInputAccepted() noexcept;
|
||||
void AdvancePresentedFrame() noexcept;
|
||||
// Put host controllers back to a neutral state before the process ends.
|
||||
void ReleaseControllers() noexcept;
|
||||
} // namespace settings_overlay
|
||||
|
||||
@@ -94,7 +94,12 @@ double ReadInput(SDL_Gamepad* gamepad, const std::string& name) {
|
||||
// Fall back to this project's own positional names, so a binding written
|
||||
// here does not have to use Dolphin vocabulary.
|
||||
if (const auto* native = ControllerNames::FindNativeButton(name)) {
|
||||
if (native->nativeButton != PAD_NATIVE_BUTTON_INVALID) {
|
||||
if (PADIsAxisButton(native->nativeButton)) {
|
||||
const auto axis = static_cast<SDL_GamepadAxis>(PADAxisButtonAxis(native->nativeButton));
|
||||
const double sign = PADAxisButtonNegative(native->nativeButton) ? -1.0 : 1.0;
|
||||
return std::clamp(SDL_GetGamepadAxis(gamepad, axis) / 32767.0 * sign, 0.0, 1.0);
|
||||
}
|
||||
if (native->nativeButton < SDL_GAMEPAD_BUTTON_COUNT) {
|
||||
return SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(native->nativeButton)) ? 1.0
|
||||
: 0.0;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "settings_overlay.h"
|
||||
#include "audio_backend.h"
|
||||
#include "aurora_events.h"
|
||||
#include "controller_button_names.h"
|
||||
#include "controller_mapping_wizard.h"
|
||||
#include "input_bindings.h"
|
||||
@@ -15,11 +16,13 @@
|
||||
#include <SDL3/SDL_keyboard.h>
|
||||
#include <SDL3/SDL_mouse.h>
|
||||
#include <SDL3/SDL_scancode.h>
|
||||
#include <SDL3/SDL_timer.h>
|
||||
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <charconv>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
@@ -69,6 +72,7 @@ const char* GraphicsApiDisplayName() {
|
||||
}
|
||||
|
||||
bool g_topBarVisible = false;
|
||||
bool g_exitPromptOpen = false;
|
||||
bool g_rumbleEnabled = RuntimeConfigFile::RumbleEnabled(true);
|
||||
int g_controllerPort = 0;
|
||||
float g_resolutionScale = RuntimeConfigFile::ResolutionMultiplier(1.0f);
|
||||
@@ -79,6 +83,7 @@ int g_soundEffectsVolumePercent =
|
||||
int g_uiVolumePercent = static_cast<int>(std::lround(RuntimeConfigFile::UiVolume(1.0f) * 100.0f));
|
||||
int g_voicesVolumePercent = static_cast<int>(std::lround(RuntimeConfigFile::VoicesVolume(1.0f) * 100.0f));
|
||||
bool g_audioMuted = RuntimeConfigFile::AudioMuted(false);
|
||||
int32_t g_muteHotkey = RuntimeConfigFile::MuteHotkey(SDL_SCANCODE_BACKSLASH);
|
||||
bool g_audioMixWorker = RuntimeConfigFile::AudioMixWorkerEnabled(true);
|
||||
bool g_attenuateMusicWhenMediaPlays = RuntimeConfigFile::AttenuateMusicWhenMediaPlays(false);
|
||||
int g_frameInterpolationMode = [] {
|
||||
@@ -186,6 +191,18 @@ void LimitResolutionForFrameRate() {
|
||||
|
||||
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 {
|
||||
std::string primary;
|
||||
std::string secondary;
|
||||
@@ -204,6 +221,13 @@ ControllerBindingPair SplitControllerBinding(const std::string& value) {
|
||||
|
||||
using ControllerNames::NativeButtonForValue;
|
||||
|
||||
std::string NativeBindingConfig(uint32_t binding) {
|
||||
std::string value = NativeButtonForValue(binding).configName;
|
||||
if (PADIsAxisButton(binding)) value += '@' + std::to_string(PADAxisButtonThreshold(binding));
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
void SetTopBarVisible(bool visible) {
|
||||
if (g_topBarVisible == visible) {
|
||||
return;
|
||||
@@ -242,7 +266,7 @@ void ApplyConfiguredMappings() {
|
||||
}
|
||||
const ControllerBindingPair binding = SplitControllerBinding(*configured);
|
||||
if (const NativeButtonItem* native = FindNativeButton(binding.primary)) {
|
||||
PADSetButtonMapping(port, PADButtonMapping{native->nativeButton, kControllerButtons[i].padButton});
|
||||
PADSetButtonMapping(port, PADButtonMapping{ConfiguredNativeButton(*native, binding.primary), kControllerButtons[i].padButton});
|
||||
} else {
|
||||
RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey
|
||||
<< " button '" << binding.primary << "'" << std::endl;
|
||||
@@ -250,7 +274,7 @@ void ApplyConfiguredMappings() {
|
||||
uint32_t altNative = PAD_NATIVE_BUTTON_INVALID;
|
||||
if (!binding.secondary.empty()) {
|
||||
if (const NativeButtonItem* native = FindNativeButton(binding.secondary)) {
|
||||
altNative = native->nativeButton;
|
||||
altNative = ConfiguredNativeButton(*native, binding.secondary);
|
||||
} else {
|
||||
RT_LOG(RT_TAG_CONFIG) << "Unknown controller." << kControllerButtons[i].configKey
|
||||
<< " secondary button '" << binding.secondary << "'" << std::endl;
|
||||
@@ -395,6 +419,228 @@ void DrawWiiRemoteSettings(uint32_t selectedGamePort) {
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
const char* KeyBindingName(int scancode) {
|
||||
switch (scancode) {
|
||||
case PAD_KEY_MOUSE_LEFT: return "Mouse left";
|
||||
case PAD_KEY_MOUSE_RIGHT: return "Mouse right";
|
||||
case PAD_KEY_MOUSE_MIDDLE: return "Mouse middle";
|
||||
case PAD_KEY_MOUSE_X1: return "Mouse side 1";
|
||||
case PAD_KEY_MOUSE_X2: return "Mouse side 2";
|
||||
case PAD_KEY_INVALID: return "Unmapped";
|
||||
default:
|
||||
return scancode >= 0 && scancode < SDL_SCANCODE_COUNT
|
||||
? SDL_GetScancodeName(static_cast<SDL_Scancode>(scancode)) : "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
enum class RebindKind { KeyboardButton, KeyboardAxis, Controller, MuteHotkey };
|
||||
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::MuteHotkey) {
|
||||
g_muteHotkey = static_cast<int32_t>(value);
|
||||
RuntimeConfigFile::SetMuteHotkey(g_muteHotkey);
|
||||
g_rebind.active = false;
|
||||
return;
|
||||
} 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."
|
||||
: g_rebind.kind == RebindKind::MuteHotkey
|
||||
? "Press a keyboard key."
|
||||
: "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 && g_rebind.kind != RebindKind::MuteHotkey &&
|
||||
(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,
|
||||
float width = 220.0f) {
|
||||
const std::string caption = std::string(KeyBindingName(scancode)) + "##binding";
|
||||
if (ImGui::Button(caption.c_str(), ImVec2(width, 0.0f))) BeginRebind(kind, target, label);
|
||||
ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x);
|
||||
ImGui::TextUnformatted(label);
|
||||
|
||||
}
|
||||
|
||||
bool DrawKeyboardSettings(uint32_t port) {
|
||||
uint32_t count = 0;
|
||||
auto* buttons = PADGetKeyButtonBindings(port, &count);
|
||||
bool enabled = buttons != nullptr;
|
||||
bool usePreset = false;
|
||||
if (ImGui::Checkbox("Keyboard and mouse", &enabled)) {
|
||||
PADSetKeyboardActive(port, enabled);
|
||||
PADSerializeMappings();
|
||||
buttons = PADGetKeyButtonBindings(port, &count);
|
||||
usePreset = enabled && std::all_of(buttons, buttons + count, [](const auto& binding) {
|
||||
return binding.scancode == PAD_KEY_INVALID;
|
||||
});
|
||||
}
|
||||
if (!enabled) return false;
|
||||
ImGui::TextDisabled("Replaces the gamepad on this port. F10 opens settings.");
|
||||
if (ImGui::Button("Use WASD + mouse preset") || usePreset) {
|
||||
const std::array<int, PAD_BUTTON_COUNT> keys = {
|
||||
PAD_KEY_MOUSE_LEFT, SDL_SCANCODE_SPACE, SDL_SCANCODE_E, SDL_SCANCODE_Q,
|
||||
SDL_SCANCODE_RETURN, PAD_KEY_MOUSE_MIDDLE, SDL_SCANCODE_LSHIFT, PAD_KEY_MOUSE_RIGHT,
|
||||
SDL_SCANCODE_UP, SDL_SCANCODE_DOWN, SDL_SCANCODE_LEFT, SDL_SCANCODE_RIGHT,
|
||||
};
|
||||
for (size_t i = 0; i < keys.size(); ++i)
|
||||
PADSetKeyButtonBinding(port, {keys[i], kControllerButtons[i].padButton});
|
||||
const std::array<int, PAD_AXIS_COUNT> axes = {
|
||||
SDL_SCANCODE_D, SDL_SCANCODE_A, SDL_SCANCODE_W, SDL_SCANCODE_S,
|
||||
SDL_SCANCODE_L, SDL_SCANCODE_J, SDL_SCANCODE_I, SDL_SCANCODE_K,
|
||||
SDL_SCANCODE_LSHIFT, PAD_KEY_MOUSE_RIGHT,
|
||||
};
|
||||
uint32_t axisCount = 0;
|
||||
auto* mappings = PADGetKeyAxisBindings(port, &axisCount);
|
||||
for (uint32_t i = 0; i < axisCount; ++i)
|
||||
PADSetKeyAxisBinding(port, {axes[i], mappings[i].padAxis, 1});
|
||||
PADSerializeMappings();
|
||||
}
|
||||
ImGui::SeparatorText("Button mapping");
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
int key = buttons[i].scancode;
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
ImGui::SetNextItemWidth(220.0f);
|
||||
DrawKeyBinding(PADGetButtonName(buttons[i].padButton), key, RebindKind::KeyboardButton, buttons[i].padButton);
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::SeparatorText("Stick and trigger mapping");
|
||||
uint32_t axisCount = 0;
|
||||
auto* axes = PADGetKeyAxisBindings(port, &axisCount);
|
||||
for (uint32_t i = 0; i < axisCount; ++i) {
|
||||
int key = axes[i].scancode;
|
||||
ImGui::PushID(static_cast<int>(count + i));
|
||||
const char* direction = PADGetAxisDirectionLabel(axes[i].padAxis);
|
||||
const std::string label = std::string(PADGetAxisName(axes[i].padAxis)) + " " +
|
||||
(direction != nullptr ? direction : "");
|
||||
ImGui::SetNextItemWidth(220.0f);
|
||||
DrawKeyBinding(label.c_str(), key, RebindKind::KeyboardAxis, axes[i].padAxis);
|
||||
ImGui::PopID();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Controller settings menu: port selection, controller assignment and button mapping.
|
||||
int ExpressionResizeCallback(ImGuiInputTextCallbackData* data) {
|
||||
if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
|
||||
@@ -501,6 +747,10 @@ void DrawControllerSettings() {
|
||||
|
||||
ImGui::Separator();
|
||||
const uint32_t selectedGamePort = static_cast<uint32_t>(g_controllerPort);
|
||||
if (DrawKeyboardSettings(selectedGamePort)) {
|
||||
return;
|
||||
}
|
||||
ImGui::Separator();
|
||||
const char* currentName = PADGetName(selectedGamePort);
|
||||
ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None");
|
||||
if (ImGui::MenuItem("Unassign controller")) {
|
||||
@@ -542,10 +792,10 @@ void DrawControllerSettings() {
|
||||
PADGetAltButtonMappings(static_cast<uint32_t>(g_controllerPort), &altMappingCount);
|
||||
|
||||
const auto writeBinding = [](size_t index, uint32_t primaryNative, uint32_t altNative) {
|
||||
std::string value = NativeButtonForValue(primaryNative).configName;
|
||||
std::string value = NativeBindingConfig(primaryNative);
|
||||
if (altNative != PAD_NATIVE_BUTTON_INVALID) {
|
||||
value += ',';
|
||||
value += NativeButtonForValue(altNative).configName;
|
||||
value += NativeBindingConfig(altNative);
|
||||
}
|
||||
RuntimeConfigFile::SetControllerButton(index, value);
|
||||
};
|
||||
@@ -603,6 +853,11 @@ void DrawControllerSettings() {
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Button mapping");
|
||||
ImGui::TextDisabled("LT / L2 = left trigger. RT / R2 = right trigger.");
|
||||
ImGui::TextDisabled("LB / L1 = left shoulder. RB / R1 = right shoulder.");
|
||||
ImGui::TextDisabled("Click a binding, then press an input. No input for 10 seconds clears it.");
|
||||
const float bindingWidth = ImGui::CalcTextSize("Right shoulder (RB / R1)").x +
|
||||
ImGui::GetFrameHeight() + ImGui::GetStyle().FramePadding.x * 2.0f;
|
||||
for (size_t i = 0; i < kControllerButtons.size(); ++i) {
|
||||
auto mappingIt = std::find_if(mappings, mappings + mappingCount, [&](const PADButtonMapping& mapping) {
|
||||
return mapping.padButton == kControllerButtons[i].padButton;
|
||||
@@ -622,30 +877,40 @@ void DrawControllerSettings() {
|
||||
|
||||
const NativeButtonItem& current = NativeButtonForValue(mappingIt->nativeButton);
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
ImGui::SetNextItemWidth(190.0f);
|
||||
if (ImGui::BeginCombo("##primary", current.label)) {
|
||||
for (const auto& candidate : kNativeButtons) {
|
||||
const bool selected = candidate.nativeButton == mappingIt->nativeButton;
|
||||
if (ImGui::Selectable(candidate.label, selected)) {
|
||||
const uint32_t port = static_cast<uint32_t>(g_controllerPort);
|
||||
PADSetButtonMapping(port, PADButtonMapping{candidate.nativeButton, kControllerButtons[i].padButton});
|
||||
writeBinding(i, candidate.nativeButton,
|
||||
altIt != nullptr ? altIt->nativeButton : PAD_NATIVE_BUTTON_INVALID);
|
||||
PADSerializeMappings();
|
||||
mappings = PADGetButtonMappings(port, &mappingCount);
|
||||
}
|
||||
if (selected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
const auto drawThreshold = [&](PADButtonMapping* mapping, bool secondary) {
|
||||
if (!PADIsAxisButton(mapping->nativeButton)) return;
|
||||
int threshold = static_cast<int>(PADAxisButtonThreshold(mapping->nativeButton));
|
||||
ImGui::SetNextItemWidth(bindingWidth);
|
||||
if (ImGui::SliderInt(secondary ? "##altThreshold" : "##primaryThreshold", &threshold,
|
||||
1, 100, "Threshold: %d%%", ImGuiSliderFlags_AlwaysClamp)) {
|
||||
const PADButtonMapping updated = {
|
||||
PADAxisButtonIdentity(mapping->nativeButton) | (static_cast<uint32_t>(threshold) << 8),
|
||||
mapping->padButton,
|
||||
};
|
||||
if (secondary) PADSetAltButtonMapping(selectedGamePort, updated);
|
||||
else PADSetButtonMapping(selectedGamePort, updated);
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
||||
writeBinding(i, mappingIt->nativeButton,
|
||||
altIt != nullptr ? altIt->nativeButton : PAD_NATIVE_BUTTON_INVALID);
|
||||
PADSerializeMappings();
|
||||
}
|
||||
};
|
||||
ImGui::BeginGroup();
|
||||
ImGui::SetNextItemWidth(bindingWidth);
|
||||
const std::string primaryCaption = std::string(current.label) + "##primary";
|
||||
if (ImGui::Button(primaryCaption.c_str(), ImVec2(bindingWidth, 0.0f))) {
|
||||
BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label);
|
||||
}
|
||||
drawThreshold(mappingIt, false);
|
||||
ImGui::EndGroup();
|
||||
if (altIt != nullptr) {
|
||||
const bool altBound = altIt->nativeButton != PAD_NATIVE_BUTTON_INVALID;
|
||||
if (!altBound && !altRowExpanded[i]) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("+")) {
|
||||
altRowExpanded[i] = true;
|
||||
BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label, true);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Add a second binding; pressing either one works");
|
||||
@@ -654,27 +919,15 @@ void DrawControllerSettings() {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted("or");
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginGroup();
|
||||
const char* altLabel = altBound ? NativeButtonForValue(altIt->nativeButton).label : "None";
|
||||
ImGui::SetNextItemWidth(190.0f);
|
||||
if (ImGui::BeginCombo("##alt", altLabel)) {
|
||||
for (const auto& candidate : kNativeButtons) {
|
||||
const bool isNone = candidate.nativeButton == PAD_NATIVE_BUTTON_INVALID;
|
||||
const bool selected = candidate.nativeButton == altIt->nativeButton;
|
||||
if (ImGui::Selectable(isNone ? "None" : candidate.label, selected)) {
|
||||
const uint32_t port = static_cast<uint32_t>(g_controllerPort);
|
||||
PADSetAltButtonMapping(
|
||||
port, PADButtonMapping{candidate.nativeButton, kControllerButtons[i].padButton});
|
||||
writeBinding(i, mappingIt->nativeButton, candidate.nativeButton);
|
||||
if (isNone) {
|
||||
altRowExpanded[i] = false;
|
||||
}
|
||||
}
|
||||
if (selected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
ImGui::SetNextItemWidth(bindingWidth);
|
||||
const std::string altCaption = std::string(altLabel) + "##alt";
|
||||
if (ImGui::Button(altCaption.c_str(), ImVec2(bindingWidth, 0.0f))) {
|
||||
BeginRebind(RebindKind::Controller, kControllerButtons[i].padButton, kControllerButtons[i].label, true);
|
||||
}
|
||||
drawThreshold(altIt, true);
|
||||
ImGui::EndGroup();
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
@@ -712,10 +965,14 @@ void DrawAudioSettings() {
|
||||
MusicAttenuation::SetVoicesVolume(volume);
|
||||
RuntimeConfigFile::SetVoicesVolume(volume);
|
||||
}
|
||||
const float labelColumn = ImGui::GetCursorPosX() + ImGui::CalcItemWidth();
|
||||
if (ImGui::Checkbox("Mute", &g_audioMuted)) {
|
||||
AudioBackend::Instance().SetMuted(g_audioMuted);
|
||||
RuntimeConfigFile::SetAudioMuted(g_audioMuted);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
DrawKeyBinding("Mute shortcut", g_muteHotkey, RebindKind::MuteHotkey, 0,
|
||||
std::max(60.0f, labelColumn - ImGui::GetCursorPosX()));
|
||||
ImGui::Separator();
|
||||
if (ImGui::Checkbox("Mix audio on a worker thread", &g_audioMixWorker)) {
|
||||
// Applies immediately: SetMixWorkerEnabled joins any in-flight mix
|
||||
@@ -935,11 +1192,40 @@ void DrawStartupScreen() {
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
void DrawExitPrompt() {
|
||||
constexpr const char* kTitle = "Exit";
|
||||
if (g_exitPromptOpen && !ImGui::IsPopupOpen(kTitle)) ImGui::OpenPopup(kTitle);
|
||||
if (!ImGui::BeginPopupModal(kTitle, &g_exitPromptOpen, ImGuiWindowFlags_AlwaysAutoResize)) return;
|
||||
ImGui::TextUnformatted("Quit the game?");
|
||||
if (ImGui::Button("Exit", ImVec2(120.0f, 0.0f))) ExitForAuroraWindowClose();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(120.0f, 0.0f))) g_exitPromptOpen = false;
|
||||
if (!g_exitPromptOpen) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
void DrawTopBar() {
|
||||
if (!g_topBarVisible || !ImGui::BeginMainMenuBar()) {
|
||||
if (!g_topBarVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
ImGui::GetBackgroundDrawList()->AddRectFilled(viewport->Pos,
|
||||
ImVec2(viewport->Pos.x + viewport->Size.x, viewport->Pos.y + viewport->Size.y),
|
||||
IM_COL32(0, 0, 0, 70));
|
||||
ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x + viewport->Size.x * 0.5f,
|
||||
viewport->Pos.y + viewport->Size.y - 24.0f),
|
||||
ImGuiCond_Always, ImVec2(0.5f, 1.0f));
|
||||
ImGui::SetNextWindowBgAlpha(0.85f);
|
||||
if (ImGui::Begin("Settings input hint", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize |
|
||||
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoSavedSettings |
|
||||
ImGuiWindowFlags_NoFocusOnAppearing)) {
|
||||
ImGui::TextUnformatted("Settings open - game controls disabled. Press F10 to return to the game.");
|
||||
}
|
||||
ImGui::End();
|
||||
if (!ImGui::BeginMainMenuBar()) return;
|
||||
|
||||
ImGui::TextUnformatted("WiiCompiled");
|
||||
ImGui::Separator();
|
||||
const auto resolutionIt = std::find_if(kResolutions.begin(), kResolutions.end(), [](const ResolutionItem& item) {
|
||||
@@ -968,6 +1254,9 @@ void DrawTopBar() {
|
||||
|
||||
if (ImGui::BeginMenu("Controller settings")) {
|
||||
DrawControllerSettings();
|
||||
// Nest capture under this menu so opening/closing the modal preserves
|
||||
// the settings popup and its current port and scroll position.
|
||||
DrawRebindPrompt();
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
@@ -980,14 +1269,21 @@ void DrawTopBar() {
|
||||
const std::string audioMenuLabel = audioLabel + "###AudioSettingsMenu";
|
||||
if (ImGui::BeginMenu(audioMenuLabel.c_str())) {
|
||||
DrawAudioSettings();
|
||||
DrawRebindPrompt();
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
const float hideWidth = ImGui::CalcTextSize("Hide (F10)").x + ImGui::GetStyle().FramePadding.x * 2.0f;
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), ImGui::GetWindowWidth() - hideWidth - 8.0f));
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
const float hideWidth = ImGui::CalcTextSize("Hide (F10)").x + style.FramePadding.x * 2.0f;
|
||||
const float exitWidth = ImGui::CalcTextSize("X").x + style.FramePadding.x * 2.0f;
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(),
|
||||
ImGui::GetWindowWidth() - hideWidth - exitWidth - style.ItemSpacing.x - 8.0f));
|
||||
if (ImGui::MenuItem("Hide (F10)")) {
|
||||
SetTopBarVisible(false);
|
||||
}
|
||||
if (ImGui::MenuItem("X")) {
|
||||
g_exitPromptOpen = true;
|
||||
}
|
||||
ImGui::EndMainMenuBar();
|
||||
}
|
||||
|
||||
@@ -1016,9 +1312,12 @@ void UpdateCursorAutoHide() {
|
||||
return;
|
||||
}
|
||||
g_cursorHidden = shouldHide;
|
||||
// ImGui_ImplSDL3_NewFrame calls SDL_ShowCursor every frame unless this flag is set.
|
||||
if (shouldHide) {
|
||||
ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange;
|
||||
SDL_HideCursor();
|
||||
} else {
|
||||
ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange;
|
||||
SDL_ShowCursor();
|
||||
}
|
||||
}
|
||||
@@ -1074,15 +1373,50 @@ void HandleEvents(const AuroraEvent* events) noexcept {
|
||||
continue;
|
||||
}
|
||||
controller_mapping_wizard::HandleSdlEvent(ev->sdl);
|
||||
if (IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) {
|
||||
if (g_rebind.active && (IsToggleKey(ev->sdl, SDL_SCANCODE_BACKSPACE) ||
|
||||
IsToggleKey(ev->sdl, SDL_SCANCODE_DELETE))) {
|
||||
CompleteRebind(g_rebind.kind == RebindKind::Controller ? PAD_NATIVE_BUTTON_DISABLED
|
||||
: static_cast<uint32_t>(PAD_KEY_INVALID));
|
||||
}
|
||||
if (!g_rebind.active && IsToggleKey(ev->sdl, SDL_SCANCODE_F10)) {
|
||||
SetTopBarVisible(!g_topBarVisible);
|
||||
}
|
||||
if (!g_rebind.active && g_muteHotkey != PAD_KEY_INVALID &&
|
||||
IsToggleKey(ev->sdl, static_cast<SDL_Scancode>(g_muteHotkey))) {
|
||||
g_audioMuted = !g_audioMuted;
|
||||
AudioBackend::Instance().SetMuted(g_audioMuted);
|
||||
RuntimeConfigFile::SetAudioMuted(g_audioMuted);
|
||||
}
|
||||
if (!g_rebind.active && !g_topBarVisible && IsToggleKey(ev->sdl, SDL_SCANCODE_ESCAPE)) {
|
||||
g_exitPromptOpen = true;
|
||||
}
|
||||
if (IsMouseActivity(ev->sdl)) {
|
||||
g_lastMouseActivity = Clock::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ReleaseControllers() noexcept {
|
||||
// Aurora drives the LED white on first PADRead and never clears it, and the
|
||||
// exit paths terminate the process outright, so do it here.
|
||||
bool queued = false;
|
||||
for (uint32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
||||
const s32 index = PADGetIndexForPort(port);
|
||||
if (index < 0) continue;
|
||||
if (SDL_Gamepad* pad = PADGetSDLGamepadForIndex(static_cast<u32>(index))) {
|
||||
SDL_SetGamepadLED(pad, 0, 0, 0);
|
||||
queued = true;
|
||||
}
|
||||
}
|
||||
constexpr std::array<uint32_t, PAD_MAX_CONTROLLERS> stopAll{
|
||||
PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD};
|
||||
PADControlAllMotors(stopAll.data());
|
||||
// SDL hands LED and rumble reports to its own HIDAPI sender thread rather
|
||||
// than writing them here, so without this the process dies before the
|
||||
// controller ever receives them.
|
||||
if (queued) SDL_Delay(120);
|
||||
}
|
||||
|
||||
void Draw() noexcept {
|
||||
// Wait for the frame worker's DONE phase: it has replayed the previous frame's ImGui draw lists
|
||||
// and started the next ImGui frame, so all overlay callers can now safely issue ImGui commands.
|
||||
@@ -1100,9 +1434,10 @@ void Draw() noexcept {
|
||||
}
|
||||
DrawFpsOverlay();
|
||||
DrawTopBar();
|
||||
DrawExitPrompt();
|
||||
controller_mapping_wizard::Draw();
|
||||
// The wizard captures raw presses; keep them out of the game.
|
||||
const bool inputBlocked = controller_mapping_wizard::IsActive();
|
||||
const bool inputBlocked = controller_mapping_wizard::IsActive() || g_rebind.active;
|
||||
PADBlockInput(inputBlocked);
|
||||
InputBindings::SetInputBlocked(inputBlocked);
|
||||
DrawStartupScreen();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@@ -1891,6 +1891,7 @@ int RunTranslateModCore(string[] argsTail, string? outputDirectoryOverride)
|
||||
overlayBuild,
|
||||
continuationPlan,
|
||||
retroWfcResolvedExecutableHooks,
|
||||
patchPlan,
|
||||
kamekFunctionStarts,
|
||||
moduleLinkBase,
|
||||
selected.CodeSize,
|
||||
@@ -2231,6 +2232,7 @@ int EmitModCpp(
|
||||
OverlayBuildResult overlayBuild,
|
||||
ContinuationPlan continuationPlan,
|
||||
IReadOnlyCollection<RetroWfcExecutableHookPlan>? retroWfcExecutableHooks,
|
||||
KamekPatchPlan patchPlan,
|
||||
IReadOnlyList<ModFunctionStart> kamekFunctionStarts,
|
||||
uint moduleLinkBase,
|
||||
uint moduleLinkedCodeSize,
|
||||
@@ -2272,14 +2274,53 @@ int EmitModCpp(
|
||||
.ToHashSet();
|
||||
var queuedContinuationAddresses = continuationPlan.Entries.Select(e => e.Address).ToHashSet();
|
||||
var discoveredContinuationQueue = new Queue<ContinuationEntry>();
|
||||
var linkedHookLrBasesByTarget = retroWfcExecutableHooks is null
|
||||
? new Dictionary<uint, uint[]>()
|
||||
: retroWfcExecutableHooks
|
||||
.Where(h => h.TargetAddress.HasValue && RetroWfcHookSetsLinkRegister(h))
|
||||
.GroupBy(h => h.TargetAddress!.Value)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g => g.Select(h => h.ContinuationAddress).Distinct().ToArray());
|
||||
var hookLrBases = new List<(uint TargetAddress, uint ContinuationAddress)>();
|
||||
if (retroWfcExecutableHooks is not null)
|
||||
{
|
||||
hookLrBases.AddRange(
|
||||
retroWfcExecutableHooks
|
||||
.Where(h => h.TargetAddress.HasValue && RetroWfcHookSetsLinkRegister(h))
|
||||
.Select(h => (h.TargetAddress!.Value, h.ContinuationAddress)));
|
||||
}
|
||||
var hookLrAnalysis = new Dictionary<uint, LrContinuationAnalysis>();
|
||||
var hookDiscoveryCache = new Dictionary<uint, IReadOnlyList<PpcInstruction>>();
|
||||
IReadOnlyList<PpcInstruction> DiscoverHookBody(uint target)
|
||||
{
|
||||
if (!hookDiscoveryCache.TryGetValue(target, out var instructions))
|
||||
{
|
||||
instructions = modTranslator.Discover(target,
|
||||
new TranslationOptions(KnownFunctionEntryPoints: knownFunctionEntryPoints)).Instructions;
|
||||
hookDiscoveryCache.Add(target, instructions);
|
||||
}
|
||||
return instructions;
|
||||
}
|
||||
|
||||
LrContinuationAnalysis AnalyzeHook(uint target)
|
||||
{
|
||||
if (!hookLrAnalysis.TryGetValue(target, out var analysis))
|
||||
{
|
||||
analysis = LrContinuationAnalysis.Analyze(target, DiscoverHookBody);
|
||||
hookLrAnalysis.Add(target, analysis);
|
||||
}
|
||||
return analysis;
|
||||
}
|
||||
|
||||
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);
|
||||
if (!AnalyzeHook(target).MaySkipReturn)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
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 linkedCallFallthroughLrOverrides = retroWfcExecutableHooks is null
|
||||
? new Dictionary<uint, uint>()
|
||||
@@ -2374,50 +2415,42 @@ int EmitModCpp(
|
||||
}
|
||||
}
|
||||
|
||||
void RecordDiscoveredLrRelativeBaseContinuations(
|
||||
FunctionTranslationResult result,
|
||||
IReadOnlyList<uint> lrBases,
|
||||
string reason)
|
||||
void RecordHookContinuations(uint hookTarget, IReadOnlyList<uint> lrBases)
|
||||
{
|
||||
if (lrBases.Count == 0)
|
||||
var analysis = AnalyzeHook(hookTarget);
|
||||
foreach (var lrBase in lrBases)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var offset in DiscoverLrRelativeIndirectJumpOffsets(result).Distinct())
|
||||
{
|
||||
foreach (var lrBase in lrBases)
|
||||
var targets = analysis.Offsets.Select(offset => unchecked(lrBase + (uint)offset));
|
||||
if (analysis.WasTruncated && baseFunctions.FindContaining(lrBase - 4u) is { } caller)
|
||||
{
|
||||
// Unknown offsets can resume at any aligned instruction in this caller.
|
||||
targets = targets.Concat(Enumerable.Range(0, checked((int)((caller.End - caller.Start) / 4)))
|
||||
.Select(index => caller.Start + (uint)index * 4u));
|
||||
}
|
||||
foreach (var target in targets.Distinct())
|
||||
{
|
||||
var target = unchecked(lrBase + (uint)offset);
|
||||
var section = baseManifest.Sections.FirstOrDefault(s => target >= s.GuestStart && target < s.GuestEnd);
|
||||
if (section is null || !section.Executable)
|
||||
{
|
||||
if (section is null || !section.Executable || (target & 3u) != 0)
|
||||
continue;
|
||||
}
|
||||
|
||||
var containing = baseFunctions.FindContaining(target);
|
||||
if (containing is null || containing.Start == target)
|
||||
{
|
||||
if (containing is null || containing.Start == target || !queuedContinuationAddresses.Add(target))
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!queuedContinuationAddresses.Add(target))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
discoveredContinuationQueue.Enqueue(new ContinuationEntry(
|
||||
target,
|
||||
containing.Start,
|
||||
containing.End,
|
||||
section.Name,
|
||||
result.EntryPoint,
|
||||
hookTarget,
|
||||
KamekCommandId.Branch,
|
||||
$"{reason}; LR-relative jump offset {offset:+#;-#;0}"));
|
||||
$"LR-relative hook target 0x{hookTarget:X8}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (target, lrBases) in linkedHookLrBasesByTarget)
|
||||
RecordHookContinuations(target, lrBases);
|
||||
|
||||
ModTranslationWork CreateContinuationWork(ContinuationEntry continuation)
|
||||
{
|
||||
var name = $"rr_continue_{continuation.Address:X8}";
|
||||
@@ -2583,16 +2616,7 @@ int EmitModCpp(
|
||||
}
|
||||
|
||||
CommitWave(attempts, (result, work) =>
|
||||
{
|
||||
RecordDiscoveredBaseContinuations(result, $"base continuation discovered from module 0x{work.Address:X8}");
|
||||
if (linkedHookLrBasesByTarget.TryGetValue(work.Address, out var lrBases))
|
||||
{
|
||||
RecordDiscoveredLrRelativeBaseContinuations(
|
||||
result,
|
||||
lrBases,
|
||||
$"base continuation discovered from LR-relative hook target 0x{work.Address:X8}");
|
||||
}
|
||||
});
|
||||
RecordDiscoveredBaseContinuations(result, $"base continuation discovered from module 0x{work.Address:X8}"));
|
||||
}
|
||||
|
||||
DrainDiscoveredContinuations();
|
||||
@@ -2750,124 +2774,6 @@ IEnumerable<uint> DirectModuleTargets(FunctionTranslationResult result, uint mod
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<int> DiscoverLrRelativeIndirectJumpOffsets(FunctionTranslationResult result)
|
||||
{
|
||||
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) =>
|
||||
hook.TypeName is "call" or "branchCtrLink" ||
|
||||
hook.Intent.Contains("Call", StringComparison.Ordinal);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using Translator.Core.Disassembly;
|
||||
using Translator.Core.Parsing.Kamek;
|
||||
@@ -273,4 +274,541 @@ public static class ContinuationPlanner
|
||||
Or,
|
||||
AddSigned
|
||||
}
|
||||
|
||||
// Dropped states make a negative result inconclusive.
|
||||
private const int MaxStatesPerInstruction = 512;
|
||||
|
||||
public static IEnumerable<int> DiscoverLrRelativeIndirectJumpOffsets(
|
||||
IReadOnlyList<PpcInstruction> instructions,
|
||||
Action? onStateCapExceeded = null,
|
||||
Action? onUnresolvedExit = null)
|
||||
{
|
||||
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)>();
|
||||
|
||||
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)
|
||||
{
|
||||
onStateCapExceeded?.Invoke();
|
||||
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;
|
||||
}
|
||||
|
||||
if (!state.CtrOffset.HasValue && instruction.BranchTargets.Count == 0)
|
||||
onUnresolvedExit?.Invoke();
|
||||
|
||||
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)
|
||||
onUnresolvedExit?.Invoke();
|
||||
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 && !instruction.IsCall)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
using Translator.Core.Disassembly;
|
||||
|
||||
namespace Translator.Core.Mods;
|
||||
|
||||
// Opaque exits prevent classification; truncated exploration may also hide offsets.
|
||||
public sealed record LrContinuationAnalysis(IReadOnlyList<int> Offsets, bool IsComplete, bool WasTruncated)
|
||||
{
|
||||
public bool MaySkipReturn => !IsComplete || Offsets.Count != 0;
|
||||
|
||||
public static LrContinuationAnalysis Analyze(
|
||||
uint entryPoint,
|
||||
Func<uint, IReadOnlyList<PpcInstruction>> discover)
|
||||
{
|
||||
const int maxFunctions = 256;
|
||||
const int maxInstructions = 65536;
|
||||
var instructions = new Dictionary<uint, PpcInstruction>();
|
||||
var pending = new Queue<uint>();
|
||||
var visited = new HashSet<uint>();
|
||||
var complete = true;
|
||||
var truncated = false;
|
||||
pending.Enqueue(entryPoint);
|
||||
|
||||
while (pending.TryDequeue(out var entry))
|
||||
{
|
||||
if (instructions.ContainsKey(entry) || !visited.Add(entry))
|
||||
continue;
|
||||
if (visited.Count > maxFunctions)
|
||||
{
|
||||
complete = false;
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
|
||||
IReadOnlyList<PpcInstruction> body;
|
||||
try
|
||||
{
|
||||
body = discover(entry);
|
||||
}
|
||||
catch (Exception ex) when (ex is InvalidOperationException or ArgumentException
|
||||
or IndexOutOfRangeException or NotSupportedException or OverflowException)
|
||||
{
|
||||
complete = false;
|
||||
truncated = true;
|
||||
continue;
|
||||
}
|
||||
if (!body.Any(instruction => instruction.Address == entry) ||
|
||||
instructions.Count + body.Count > maxInstructions)
|
||||
{
|
||||
complete = false;
|
||||
truncated = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var instruction in body)
|
||||
instructions.TryAdd(instruction.Address, instruction);
|
||||
foreach (var instruction in body)
|
||||
{
|
||||
if (!instruction.IsCall)
|
||||
{
|
||||
foreach (var target in instruction.BranchTargets)
|
||||
if (!instructions.ContainsKey(target)) pending.Enqueue(target);
|
||||
}
|
||||
if ((!instruction.IsReturn && !instruction.IsUnconditionalBranch) ||
|
||||
instruction.IsConditionalBranch)
|
||||
{
|
||||
if (!instructions.ContainsKey(instruction.EndAddress))
|
||||
pending.Enqueue(instruction.EndAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!instructions.TryGetValue(entryPoint, out var first))
|
||||
return new LrContinuationAnalysis([], false, true);
|
||||
|
||||
// Keep the original entry first, including when a tail target precedes it.
|
||||
var ordered = new[] { first }.Concat(instructions.Values
|
||||
.Where(instruction => instruction.Address != entryPoint)
|
||||
.OrderBy(instruction => instruction.Address)).ToArray();
|
||||
var offsets = ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(
|
||||
ordered, () => { complete = false; truncated = true; }, () => complete = false).ToArray();
|
||||
return new LrContinuationAnalysis(offsets, complete, truncated);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Linq;
|
||||
using Translator.Core.Disassembly;
|
||||
using Translator.Core.Mods;
|
||||
using Translator.Core.Mods.Mkwii;
|
||||
using Translator.Core.Parsing.Kamek;
|
||||
@@ -128,6 +130,54 @@ public class ContinuationPlannerTests
|
||||
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() =>
|
||||
new(
|
||||
0,
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text.Json;
|
||||
using Translator.Cli.Configuration;
|
||||
using Translator.Core.Build;
|
||||
using Translator.Core.Mods;
|
||||
using Translator.Core.Parsing.Kamek;
|
||||
|
||||
namespace Translator.Tests;
|
||||
|
||||
public sealed class KamekLrContinuationIntegrationTests
|
||||
{
|
||||
private const uint Caller = 0x80004000;
|
||||
private const uint Module = 0x80010000;
|
||||
private const uint Hook = Module + 0x40;
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
public void SkipReturnThroughKnownTailCallsRetainsCallerDispatch(int tailDepth)
|
||||
{
|
||||
var bundle = Translate(tailDepth, SkipReturn(20));
|
||||
AssertResumeDispatch(Source(bundle, Caller), Caller, Caller + 24);
|
||||
Assert.Contains(bundle.Entries, entry => entry.EntryPoint == Caller + 24 &&
|
||||
entry.VirtualPath.Contains("rr_continue_"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
public void OrdinaryReturnsThroughKnownTailCallsStayLightweight(int tailDepth)
|
||||
{
|
||||
var bundle = Translate(tailDepth, [0x38630001u, 0x4E800020u]); // addi r3,r3,1; blr
|
||||
var caller = Source(bundle, Caller);
|
||||
Assert.DoesNotContain("switch (ctx->lr)", caller);
|
||||
Assert.DoesNotContain("if (ctx->lr !=", caller);
|
||||
Assert.DoesNotContain(bundle.Entries, entry => entry.VirtualPath.Contains("rr_continue_"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DirectSkipReturnRegistersTheAdjustedBaseAddress()
|
||||
{
|
||||
var bundle = Translate(0, SkipReturn(20));
|
||||
AssertResumeDispatch(Source(bundle, Caller), Caller, Caller + 24);
|
||||
Assert.Contains(bundle.Entries, entry => entry.EntryPoint == Caller + 24 &&
|
||||
entry.VirtualPath.Contains("rr_continue_"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CtrSkipRetainsDispatchAndRegistersItsContinuation()
|
||||
{
|
||||
var bundle = Translate(0,
|
||||
[
|
||||
0x7D8802A6u, // mflr r12
|
||||
0x398C0014u, // addi r12,r12,20
|
||||
0x7D8903A6u, // mtctr r12
|
||||
0x4E800420u // bctr
|
||||
]);
|
||||
AssertResumeDispatch(Source(bundle, Caller), Caller, Caller + 24);
|
||||
Assert.Contains(bundle.Entries, entry => entry.EntryPoint == Caller + 24 &&
|
||||
entry.VirtualPath.Contains("rr_continue_"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalReturnArmDoesNotHideTailCalledSkipReturn()
|
||||
{
|
||||
var bundle = Translate(1, SkipReturn(20), conditionalWrapper: true);
|
||||
AssertResumeDispatch(Source(bundle, Caller), Caller, Caller + 24);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
public void SharedHookRetainsResumeDispatchAtEveryCallSite(int tailDepth)
|
||||
{
|
||||
var bundle = Translate(tailDepth, SkipReturn(20), sharedTarget: true);
|
||||
AssertResumeDispatch(Source(bundle, Caller), Caller, Caller + 24);
|
||||
AssertResumeDispatch(Source(bundle, Caller + 0x40), Caller + 0x40, Caller + 0x58);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
public void SkipReturnHandlingDoesNotSpreadToAnOrdinaryPatchedCaller(int tailDepth)
|
||||
{
|
||||
var bundle = Translate(tailDepth, SkipReturn(20), companionBody: [0x38630001u, 0x4E800020u]);
|
||||
var ordinaryCaller = Source(bundle, Caller + 0x40);
|
||||
Assert.DoesNotContain("switch (ctx->lr)", ordinaryCaller);
|
||||
Assert.DoesNotContain("if (ctx->lr !=", ordinaryCaller);
|
||||
AssertResumeDispatch(Source(bundle, Caller), Caller, Caller + 24);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(20)]
|
||||
public void SavedAndRestoredLrDistinguishesOrdinaryAndSkipReturns(int offset)
|
||||
{
|
||||
var bundle = Translate(0,
|
||||
[
|
||||
0x7D8802A6u, // mflr r12
|
||||
0x91810004u, // stw r12,4(r1)
|
||||
0x81810004u, // lwz r12,4(r1)
|
||||
0x398C0000u | (ushort)offset, // addi r12,r12,offset
|
||||
0x7D8803A6u, // mtlr r12
|
||||
0x4E800020u // blr
|
||||
]);
|
||||
var caller = Source(bundle, Caller);
|
||||
if (offset == 0)
|
||||
Assert.DoesNotContain("if (ctx->lr !=", caller);
|
||||
else
|
||||
AssertResumeDispatch(caller, Caller, Caller + 4 + (uint)offset);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CappedAnalysisDoesNotClassifyAHookAsAnOrdinaryReturn()
|
||||
{
|
||||
var bundle = Translate(0, SyntheticLrHookFactory.ManyOrdinaryReturnPaths());
|
||||
AssertResumeDispatch(Source(bundle, Caller), Caller, Caller + 24);
|
||||
Assert.Contains(bundle.Entries, entry => entry.EntryPoint == Caller + 24 &&
|
||||
entry.VirtualPath.Contains("rr_continue_"));
|
||||
}
|
||||
|
||||
private static void AssertResumeDispatch(string source, uint callSite, uint continuation)
|
||||
{
|
||||
var call = source.IndexOf($"InvokeDirectCpu<0x{Hook:X8}u>(ctx);", StringComparison.Ordinal);
|
||||
Assert.True(call >= 0, source);
|
||||
var guard = source.IndexOf($"if (ctx->lr != 0x{callSite + 4:X8}u)", call, StringComparison.Ordinal);
|
||||
Assert.True(guard > call, $"The hook must dispatch its adjusted return address.\n{source}");
|
||||
Assert.Contains($"case 0x{continuation:X8}u:", source[guard..]);
|
||||
Assert.Contains($"goto loc_{continuation:X8};", source[guard..]);
|
||||
Assert.Contains($"loc_{continuation:X8}:", source);
|
||||
}
|
||||
|
||||
private static uint[] SkipReturn(int offset) =>
|
||||
[
|
||||
0x7D8802A6u, // mflr r12
|
||||
0x398C0000u | (ushort)offset, // addi r12,r12,offset
|
||||
0x7D8803A6u, // mtlr r12
|
||||
0x4E800020u // blr
|
||||
];
|
||||
|
||||
private static string Source(TranslationSourceBundle bundle, uint address) =>
|
||||
Assert.Single(bundle.Entries.Where(entry => entry.EntryPoint == address &&
|
||||
entry.VirtualPath.StartsWith("overlays/", StringComparison.Ordinal))).Source;
|
||||
|
||||
private static TranslationSourceBundle Translate(int tailDepth, uint[] body,
|
||||
bool conditionalWrapper = false, bool sharedTarget = false, uint[]? companionBody = null)
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), $"kamek-lr-integration-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(root);
|
||||
try
|
||||
{
|
||||
uint[] caller = [0x60000000u, 0x38630001u, 0x38630001u, 0x38630001u,
|
||||
0x38630001u, 0x38630001u, 0x60000000u, 0x4E800020u];
|
||||
var hasSecondCaller = sharedTarget || companionBody is not null;
|
||||
var baseWords = Enumerable.Repeat(0x4E800020u, hasSecondCaller ? 24 : 8).ToArray();
|
||||
caller.CopyTo(baseWords, 0);
|
||||
if (hasSecondCaller) caller.CopyTo(baseWords, 16);
|
||||
var baseBytes = Words(baseWords);
|
||||
File.WriteAllBytes(Path.Combine(root, "main.dol"), SyntheticDolFactory.CreateBytes(
|
||||
Caller, sections: [SyntheticDolFactory.Text(0, Caller, baseWords)]));
|
||||
File.WriteAllBytes(Path.Combine(root, "base.bin"), baseBytes);
|
||||
// translate-mod requires a REL; this fixture has no REL code or relocations.
|
||||
File.WriteAllBytes(Path.Combine(root, "empty.rel"), new byte[0x48]);
|
||||
var functions = new List<BaseFunctionRangeMetadata>
|
||||
{
|
||||
new(Caller, Caller + 32, "caller", ".text", 0, "synthetic", ["Executable"])
|
||||
};
|
||||
if (hasSecondCaller)
|
||||
functions.Add(new(Caller + 0x40, Caller + 0x60, "second_caller", ".text", 0x40,
|
||||
"synthetic", ["Executable"]));
|
||||
var manifest = new BaseManifest("synthetic", 1, "TEST01", "P", "", 0,
|
||||
[new BaseSectionMetadata(".text", "main.dol", Caller, Caller + (uint)baseBytes.Length,
|
||||
true, false, "base.bin", 0)],
|
||||
functions, "ranges.json");
|
||||
var manifestPath = Path.Combine(root, "base.json");
|
||||
File.WriteAllText(manifestPath, JsonSerializer.Serialize(manifest));
|
||||
var projectPath = Path.Combine(root, "recomp.yml");
|
||||
Directory.CreateDirectory(Path.Combine(root, "native"));
|
||||
Directory.CreateDirectory(Path.Combine(root, "generated", "functions"));
|
||||
File.WriteAllText(projectPath, """
|
||||
schema_version: 1
|
||||
project:
|
||||
id: kamek-lr-test
|
||||
memory:
|
||||
base: 0x80000000
|
||||
size: 0x00020000
|
||||
sda_base: 0x80002000
|
||||
sda2_base: 0x80003000
|
||||
inputs:
|
||||
dol:
|
||||
path: main.dol
|
||||
rel:
|
||||
path: empty.rel
|
||||
load_address: 0x80008000
|
||||
runtime:
|
||||
native_registration_root: native
|
||||
output:
|
||||
root: generated
|
||||
""");
|
||||
|
||||
var moduleSize = companionBody is null ? 0x40 * (tailDepth + 1) + body.Length * 4
|
||||
: 0x300 + companionBody.Length * 4;
|
||||
var moduleWords = Enumerable.Repeat(0x4E800020u, moduleSize / 4).ToArray();
|
||||
body.CopyTo(moduleWords, (0x40 * (tailDepth + 1)) / 4);
|
||||
var commands = new List<uint> { ((uint)KamekCommandId.BranchLink << 24) | 0x00FFFFFEu, Caller, Hook - Module };
|
||||
if (hasSecondCaller)
|
||||
commands.AddRange([((uint)KamekCommandId.BranchLink << 24) | 0x00FFFFFEu,
|
||||
Caller + 0x40, sharedTarget ? Hook - Module : 0x300u]);
|
||||
companionBody?.CopyTo(moduleWords, 0x300 / 4);
|
||||
for (var depth = 0; depth < tailDepth; depth++)
|
||||
{
|
||||
var offset = 0x40u * (uint)(depth + 1);
|
||||
var branchOffset = offset;
|
||||
if (conditionalWrapper && depth == 0)
|
||||
{
|
||||
moduleWords[offset / 4] = 0x2C030000u; // cmpwi r3,0
|
||||
moduleWords[offset / 4 + 1] = 0x4D820020u; // beqlr
|
||||
branchOffset += 8;
|
||||
}
|
||||
// Explicit Kamek branch targets make every helper a known function boundary.
|
||||
commands.Add(((uint)KamekCommandId.Branch << 24) | branchOffset);
|
||||
commands.Add(offset + 0x40);
|
||||
}
|
||||
var code = Words(moduleWords);
|
||||
var commandBytes = Words(commands.ToArray());
|
||||
var pul = new byte[KamekChunk.HeaderSize + code.Length + commandBytes.Length];
|
||||
Write(pul, 0, KamekChunk.Magic0);
|
||||
Write(pul, 4, KamekChunk.Magic1);
|
||||
Write(pul, 12, (uint)code.Length);
|
||||
Write(pul, 24, (uint)pul.Length);
|
||||
code.CopyTo(pul, KamekChunk.HeaderSize);
|
||||
commandBytes.CopyTo(pul, KamekChunk.HeaderSize + code.Length);
|
||||
var pulPath = Path.Combine(root, "Code.pul");
|
||||
File.WriteAllBytes(pulPath, pul);
|
||||
var output = Path.Combine(root, "mod");
|
||||
string[] args = ["translate-mod", "--project", projectPath, "--code-pul", pulPath,
|
||||
"--base-manifest", manifestPath, "--out", output, "--module-guest-base", $"0x{Module:X8}",
|
||||
"--module-link-base", $"0x{Module:X8}", "--skip-retro-wfc", "--emit-cpp", "--threads", "1"];
|
||||
var entryPoint = typeof(TranslationProjectConfig).Assembly.EntryPoint!;
|
||||
var exitCode = Assert.IsType<int>(entryPoint.Invoke(null, [args]));
|
||||
Assert.Equal(0, exitCode);
|
||||
return TranslationSourceBundle.Read(Path.Combine(output, "translated_sources.bin"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] Words(uint[] words)
|
||||
{
|
||||
var bytes = new byte[words.Length * 4];
|
||||
for (var index = 0; index < words.Length; index++) Write(bytes, index * 4, words[index]);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static void Write(byte[] bytes, int offset, uint value) =>
|
||||
BinaryPrimitives.WriteUInt32BigEndian(bytes.AsSpan(offset, 4), value);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using Translator.Core.Disassembly;
|
||||
using Translator.Core.Mods;
|
||||
|
||||
namespace Translator.Tests;
|
||||
|
||||
public sealed class LrContinuationAnalysisTests
|
||||
{
|
||||
private const uint Entry = 0x80010000;
|
||||
private const uint Tail = Entry - 0x40;
|
||||
|
||||
[Fact]
|
||||
public void TailHelperUsesTheWrappersLrState()
|
||||
{
|
||||
var result = Analyze(new Dictionary<uint, uint[]>
|
||||
{
|
||||
[Entry] = [0x7D8802A6, 0x398C0014, 0x4BFFFFB8], // mflr; addi +20; b Tail
|
||||
[Tail] = [0x7D8803A6, 0x4E800020] // mtlr r12; blr
|
||||
});
|
||||
Assert.True(result.IsComplete);
|
||||
Assert.Equal(new[] { 20 }, result.Offsets);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TailHelperUsesTheWrappersStackFrame()
|
||||
{
|
||||
var result = Analyze(new Dictionary<uint, uint[]>
|
||||
{
|
||||
[Entry] = [0x7D8802A6, 0x9421FFF0, 0x91810014, 0x4BFFFFB4],
|
||||
[Tail] = [0x38210010, 0x81810004, 0x398C0014, 0x7D8803A6, 0x4E800020]
|
||||
});
|
||||
Assert.True(result.IsComplete);
|
||||
Assert.Equal(new[] { 20 }, result.Offsets);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrdinaryTailCycleTerminatesWithoutInventingOffsets()
|
||||
{
|
||||
var result = Analyze(new Dictionary<uint, uint[]>
|
||||
{
|
||||
[Entry] = [0x4BFFFFC0],
|
||||
[Tail] = [0x48000040]
|
||||
});
|
||||
Assert.True(result.IsComplete);
|
||||
Assert.False(result.MaySkipReturn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustingTailCycleReportsIncompleteAnalysis()
|
||||
{
|
||||
var result = Analyze(new Dictionary<uint, uint[]>
|
||||
{
|
||||
[Entry] = [0x7D8802A6, 0x4800003C],
|
||||
[Entry + 0x40] = [0x398C0004, 0x4BFFFFFC]
|
||||
});
|
||||
Assert.False(result.IsComplete);
|
||||
Assert.True(result.WasTruncated);
|
||||
Assert.True(result.MaySkipReturn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UndecodableTailDoesNotProveAnOrdinaryReturn()
|
||||
{
|
||||
var result = Analyze(new Dictionary<uint, uint[]> { [Entry] = [0x4BFFFFC0] });
|
||||
Assert.False(result.IsComplete);
|
||||
Assert.True(result.WasTruncated);
|
||||
Assert.True(result.MaySkipReturn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalCallDoesNotInheritItsCalleesSkipOffset()
|
||||
{
|
||||
var visited = new List<uint>();
|
||||
var result = LrContinuationAnalysis.Analyze(Entry, address =>
|
||||
{
|
||||
visited.Add(address);
|
||||
return Decode(address, [0x7FE802A6, 0x4BFFFFBD, 0x7FE803A6, 0x4E800020]);
|
||||
});
|
||||
Assert.Equal(new[] { Entry }, visited);
|
||||
Assert.True(result.IsComplete);
|
||||
Assert.False(result.MaySkipReturn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownIndirectTailIsIncomplete()
|
||||
{
|
||||
var result = Analyze(new Dictionary<uint, uint[]> { [Entry] = [0x7D8903A6, 0x4E800420] });
|
||||
Assert.False(result.IsComplete);
|
||||
Assert.False(result.WasTruncated);
|
||||
Assert.True(result.MaySkipReturn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConditionalCallDoesNotTraverseATargetAlsoUsedByATailBranch()
|
||||
{
|
||||
var result = Analyze(new Dictionary<uint, uint[]>
|
||||
{
|
||||
[Entry] = [0x7FE802A6, 0x4182FFBD, 0x3BE00000, 0x4BFFFFB4],
|
||||
[Tail] = [0x3BFF0014, 0x7FE803A6, 0x4E800020]
|
||||
});
|
||||
Assert.Empty(result.Offsets);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TailDiscoveryBudgetDoesNotProveAnOrdinaryReturn()
|
||||
{
|
||||
var calls = 0;
|
||||
var result = LrContinuationAnalysis.Analyze(Entry, address =>
|
||||
{
|
||||
calls++;
|
||||
return Decode(address, [0x48000040]);
|
||||
});
|
||||
Assert.InRange(calls, 1, 256);
|
||||
Assert.False(result.IsComplete);
|
||||
Assert.True(result.WasTruncated);
|
||||
Assert.True(result.MaySkipReturn);
|
||||
}
|
||||
|
||||
private static LrContinuationAnalysis Analyze(Dictionary<uint, uint[]> functions) =>
|
||||
LrContinuationAnalysis.Analyze(Entry, address => functions.TryGetValue(address, out var words)
|
||||
? Decode(address, words)
|
||||
: throw new InvalidOperationException("No synthetic function at this address."));
|
||||
|
||||
private static PpcInstruction[] Decode(uint address, uint[] words) =>
|
||||
words.Select((word, index) => PpcDecoder.Decode(address + (uint)index * 4, word)).ToArray();
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using Translator.Core.Disassembly;
|
||||
using Translator.Core.Mods;
|
||||
|
||||
namespace Translator.Tests;
|
||||
|
||||
public sealed class LrRelativeAnalysisCompletenessTests
|
||||
{
|
||||
[Fact]
|
||||
public void EmptyResultReportsWhenPathStatesWereDropped()
|
||||
{
|
||||
var capped = false;
|
||||
var offsets = Analyze(SyntheticLrHookFactory.ManyOrdinaryReturnPaths(), () => capped = true);
|
||||
|
||||
Assert.Empty(offsets);
|
||||
Assert.True(capped, "An empty result from capped exploration must not prove an ordinary return.");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(20)]
|
||||
public void ExhaustiveAnalysisDoesNotReportAStateCap(int offset)
|
||||
{
|
||||
var capped = false;
|
||||
var offsets = Analyze(
|
||||
[
|
||||
0x7D8802A6u, // mflr r12
|
||||
0x398C0000u | (ushort)offset, // addi r12,r12,offset
|
||||
0x7D8803A6u, // mtlr r12
|
||||
0x4E800020u // blr
|
||||
], () => capped = true);
|
||||
|
||||
Assert.False(capped);
|
||||
Assert.Equal(offset == 0 ? Array.Empty<int>() : [offset], offsets);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CappedArmDoesNotDiscardAnOffsetFoundOnAnotherArm()
|
||||
{
|
||||
var capped = false;
|
||||
var offsets = Analyze(
|
||||
[
|
||||
0x7D8802A6u, // +00: mflr r12
|
||||
0x2C030000u, // +04: cmpwi r3,0
|
||||
0x41820010u, // +08: beq +0x18
|
||||
0x398C0014u, // +0C: addi r12,r12,20
|
||||
0x7D8803A6u, // +10: mtlr r12
|
||||
0x4E800020u, // +14: blr
|
||||
.. SyntheticLrHookFactory.ManyOrdinaryReturnPaths() // +18
|
||||
], () => capped = true);
|
||||
|
||||
Assert.True(capped);
|
||||
Assert.Equal(new[] { 20 }, offsets);
|
||||
}
|
||||
|
||||
private static int[] Analyze(uint[] words, Action onStateCapExceeded)
|
||||
{
|
||||
var instructions = words.Select((word, index) =>
|
||||
PpcDecoder.Decode(0x80010000u + (uint)index * 4, word)).ToArray();
|
||||
return ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(instructions, onStateCapExceeded).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
internal static class SyntheticLrHookFactory
|
||||
{
|
||||
public static uint[] ManyOrdinaryReturnPaths()
|
||||
{
|
||||
// 1,024 possible states exceed the 512-state cap without an unbounded loop.
|
||||
var words = new List<uint> { 0x7FE802A6u }; // mflr r31
|
||||
for (uint register = 3; register <= 12; register++)
|
||||
{
|
||||
words.Add(0x2C000000u | (register << 16)); // cmpwi rN,0
|
||||
words.Add(0x41820008u); // beq +8
|
||||
words.Add(0x38000004u | (register << 21) | (31u << 16)); // addi rN,r31,4
|
||||
}
|
||||
words.Add(0x4E800020u); // blr with unchanged LR
|
||||
return words.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user