4 Commits

Author SHA1 Message Date
Cristian Boehm 149cfef608 keyboard and mouse support, rebinding overhaul, analog triggers to digital inputs (#162)
* add keyboard support, analog triggers to digital input, and rebinding overhaul

* Update README.md

* Update README.md

readme typo

* implemented code rabbits suggestions

- Preserved NSO GameCube analog triggers.
  - Made modal closure and Escape cancel every rebind kind.
  - Deduced the native button array size; <array> already existed.
  - Centralized axis/sign decoding.
  - Kept threshold updates live, saving only when editing ends.

* add dimming when in settings and add clear mapping button

* Update settings_overlay.cpp

---------

Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
2026-09-10 17:37:38 +02:00
Michael G 25c69ae28e Fix crash from Kamek skip-return hooks (Item Rain crash) (#182)
* fix: Kamek LR-continuation hook discovery and dispatch

* test: cover branching Kamek LR continuations

* review fix

* another review fix

fix: get the new tests to pass
test: expose LR restore and loop continuation regressions

* Update translator/src/Translator.Core/Mods/ContinuationPlanner.cs

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

* test: cover continuation regressions from the new path-sensitive planner

* Update translator/src/Translator.Core/Mods/ContinuationPlanner.cs

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

* test: cover continuation regressions from the new path-sensitive planner

* fix: preserve LR continuation analysis across large handlers and clobbers

* fix: track LR-relative r1 across update-form stack stores

* Harden LR-relative continuation test coverage

* Fix LR/SP continuation state tracking

---------

Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-09-10 15:51:16 +02:00
patchzyy 0bb15f0a44 Update building-macos.md 2026-09-10 09:48:03 +02:00
patchzyy 466d06d7db Update README with image and credit modifications
Added an image to the README and updated credits section.
2026-09-10 08:38:04 +02:00
7 changed files with 1116 additions and 129 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
<img width="4190" height="1232" alt="wiicomplogofinalfinalfinalev2MADEBY_INKWRECK_plzcredit" src="https://github.com/user-attachments/assets/df7a3f2e-5336-479a-b4c0-968dd578726d" />
# WiiCompiled # WiiCompiled
@@ -215,7 +216,7 @@ AI coding tools were used during development of this project.
All translated output is verified against real hardware behavior and most importantly, physics accuracy is proven synced across Wii, Dolphin, and WiiCompiled (see FAQ). All translated output is verified against real hardware behavior and most importantly, physics accuracy is proven synced across Wii, Dolphin, and WiiCompiled (see FAQ).
## Credits ## Credits
- **inkwreck** - making the logo
- **[aurora](https://github.com/encounter/aurora)** - the GX rendering/windowing backend this - **[aurora](https://github.com/encounter/aurora)** - the GX rendering/windowing backend this
project's whole graphics layer sits on. MIT licensed. project's whole graphics layer sits on. MIT licensed.
- **[Dawn](https://dawn.googlesource.com/dawn)** - Google's WebGPU implementation, powering - **[Dawn](https://dawn.googlesource.com/dawn)** - Google's WebGPU implementation, powering
+2 -2
View File
@@ -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)*: 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`). - 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)*: 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`.
--- ---
@@ -320,7 +320,7 @@ Launcher/local-build-macos.command \
```bash ```bash
# 1. Download Retro-WFC payload into a staging directory: # 1. Download Retro-WFC payload into a staging directory:
mkdir -p build/retro-wfc/binary 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 -o build/retro-wfc/binary/payload.RMCPD00.bin
# 2. Run the automated build with the payload directory: # 2. Run the automated build with the payload directory:
+24 -126
View File
@@ -1,4 +1,4 @@
using System; using System;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
@@ -1891,6 +1891,7 @@ int RunTranslateModCore(string[] argsTail, string? outputDirectoryOverride)
overlayBuild, overlayBuild,
continuationPlan, continuationPlan,
retroWfcResolvedExecutableHooks, retroWfcResolvedExecutableHooks,
patchPlan,
kamekFunctionStarts, kamekFunctionStarts,
moduleLinkBase, moduleLinkBase,
selected.CodeSize, selected.CodeSize,
@@ -2231,6 +2232,7 @@ int EmitModCpp(
OverlayBuildResult overlayBuild, OverlayBuildResult overlayBuild,
ContinuationPlan continuationPlan, ContinuationPlan continuationPlan,
IReadOnlyCollection<RetroWfcExecutableHookPlan>? retroWfcExecutableHooks, IReadOnlyCollection<RetroWfcExecutableHookPlan>? retroWfcExecutableHooks,
KamekPatchPlan patchPlan,
IReadOnlyList<ModFunctionStart> kamekFunctionStarts, IReadOnlyList<ModFunctionStart> kamekFunctionStarts,
uint moduleLinkBase, uint moduleLinkBase,
uint moduleLinkedCodeSize, uint moduleLinkedCodeSize,
@@ -2272,14 +2274,25 @@ int EmitModCpp(
.ToHashSet(); .ToHashSet();
var queuedContinuationAddresses = continuationPlan.Entries.Select(e => e.Address).ToHashSet(); var queuedContinuationAddresses = continuationPlan.Entries.Select(e => e.Address).ToHashSet();
var discoveredContinuationQueue = new Queue<ContinuationEntry>(); var discoveredContinuationQueue = new Queue<ContinuationEntry>();
var linkedHookLrBasesByTarget = retroWfcExecutableHooks is null var hookLrBases = new List<(uint TargetAddress, uint ContinuationAddress)>();
? new Dictionary<uint, uint[]>() if (retroWfcExecutableHooks is not null)
: retroWfcExecutableHooks {
.Where(h => h.TargetAddress.HasValue && RetroWfcHookSetsLinkRegister(h)) hookLrBases.AddRange(
.GroupBy(h => h.TargetAddress!.Value) retroWfcExecutableHooks
.ToDictionary( .Where(h => h.TargetAddress.HasValue && RetroWfcHookSetsLinkRegister(h))
g => g.Key, .Select(h => (h.TargetAddress!.Value, h.ContinuationAddress)));
g => g.Select(h => h.ContinuationAddress).Distinct().ToArray()); }
foreach (var patch in patchPlan.ExecutablePatches.Where(p => p.CommandId == KamekCommandId.BranchLink && p.Arguments.Count > 0))
{
var target = KamekAddress.Resolve(patch.Arguments[0], patchPlan.ModuleGuestBase);
hookLrBases.Add((target, checked(patch.CommandAddress + 4u)));
}
var linkedHookLrBasesByTarget = hookLrBases
.GroupBy(h => h.TargetAddress)
.ToDictionary(
g => g.Key,
g => g.Select(h => h.ContinuationAddress).Distinct().ToArray());
var lrContinuationCallTargets = linkedHookLrBasesByTarget.Keys.ToHashSet(); var lrContinuationCallTargets = linkedHookLrBasesByTarget.Keys.ToHashSet();
var linkedCallFallthroughLrOverrides = retroWfcExecutableHooks is null var linkedCallFallthroughLrOverrides = retroWfcExecutableHooks is null
? new Dictionary<uint, uint>() ? new Dictionary<uint, uint>()
@@ -2750,123 +2763,8 @@ IEnumerable<uint> DirectModuleTargets(FunctionTranslationResult result, uint mod
} }
} }
IEnumerable<int> DiscoverLrRelativeIndirectJumpOffsets(FunctionTranslationResult result) IEnumerable<int> DiscoverLrRelativeIndirectJumpOffsets(FunctionTranslationResult result) =>
{ ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(result.Instructions);
var lrOffsets = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
int? ctrOffset = null;
foreach (var instruction in result.Instructions)
{
var mnemonic = instruction.Mnemonic.ToLowerInvariant();
if (mnemonic == "mflr" && TryGetInstructionReg(instruction, 0, out var lrDest))
{
lrOffsets[lrDest] = 0;
continue;
}
if ((mnemonic == "mr" || mnemonic == "or") &&
TryGetInstructionReg(instruction, 0, out var moveDest) &&
TryGetInstructionReg(instruction, 1, out var moveSource) &&
(mnemonic == "mr" ||
(instruction.Operands.Count >= 3 &&
instruction.Operands[2] is PpcRegisterOperand moveSource2 &&
string.Equals(NormalizeInstructionReg(moveSource2.Name), moveSource, StringComparison.OrdinalIgnoreCase))))
{
if (lrOffsets.TryGetValue(moveSource, out var sourceOffset))
{
lrOffsets[moveDest] = sourceOffset;
}
else
{
lrOffsets.Remove(moveDest);
}
continue;
}
if (mnemonic == "addi" &&
TryGetInstructionReg(instruction, 0, out var addDest) &&
TryGetInstructionReg(instruction, 1, out var addBase) &&
TryGetInstructionImm(instruction, 2, out var imm))
{
if (lrOffsets.TryGetValue(addBase, out var baseOffset))
{
lrOffsets[addDest] = checked(baseOffset + imm);
}
else
{
lrOffsets.Remove(addDest);
}
continue;
}
if (mnemonic == "mtctr" && TryGetInstructionReg(instruction, 0, out var ctrSource))
{
ctrOffset = lrOffsets.TryGetValue(ctrSource, out var sourceOffset) ? sourceOffset : null;
continue;
}
if (mnemonic == "bctr")
{
if (ctrOffset.HasValue)
{
yield return ctrOffset.Value;
}
ctrOffset = null;
continue;
}
if (TryInstructionWritesDest(instruction, out var dest))
{
lrOffsets.Remove(dest);
}
}
static bool TryGetInstructionReg(PpcInstruction instruction, int index, out string register)
{
if (instruction.Operands.Count > index && instruction.Operands[index] is PpcRegisterOperand operand)
{
register = NormalizeInstructionReg(operand.Name);
return true;
}
register = string.Empty;
return false;
}
static bool TryGetInstructionImm(PpcInstruction instruction, int index, out int immediate)
{
if (instruction.Operands.Count > index && instruction.Operands[index] is PpcImmediateOperand operand)
{
immediate = operand.Value;
return true;
}
immediate = 0;
return false;
}
static bool TryInstructionWritesDest(PpcInstruction instruction, out string destination)
{
destination = string.Empty;
if (instruction.Operands.Count == 0 || instruction.Operands[0] is not PpcRegisterOperand operand)
{
return false;
}
var mnemonic = instruction.Mnemonic.ToLowerInvariant();
if (mnemonic.StartsWith("st", StringComparison.Ordinal) ||
mnemonic.StartsWith("b", StringComparison.Ordinal) ||
mnemonic.StartsWith("cmp", StringComparison.Ordinal))
{
return false;
}
destination = NormalizeInstructionReg(operand.Name);
return true;
}
static string NormalizeInstructionReg(string register) => register.ToLowerInvariant();
}
static bool RetroWfcHookSetsLinkRegister(RetroWfcExecutableHookPlan hook) => static bool RetroWfcHookSetsLinkRegister(RetroWfcExecutableHookPlan hook) =>
hook.TypeName is "call" or "branchCtrLink" || hook.TypeName is "call" or "branchCtrLink" ||
@@ -1,4 +1,5 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Text.Json; using System.Text.Json;
using Translator.Core.Disassembly; using Translator.Core.Disassembly;
using Translator.Core.Parsing.Kamek; using Translator.Core.Parsing.Kamek;
@@ -273,4 +274,530 @@ public static class ContinuationPlanner
Or, Or,
AddSigned AddSigned
} }
public static IEnumerable<int> DiscoverLrRelativeIndirectJumpOffsets(IReadOnlyList<PpcInstruction> instructions)
{
if (instructions.Count == 0)
{
yield break;
}
var indexByAddress = new Dictionary<uint, int>(instructions.Count);
for (var i = 0; i < instructions.Count; i++)
{
indexByAddress.TryAdd(instructions[i].Address, i);
}
var visited = new HashSet<PathState>[instructions.Count];
for (var i = 0; i < instructions.Count; i++)
{
visited[i] = new HashSet<PathState>();
}
var seenOffsets = new HashSet<int>();
var worklist = new Queue<(int Index, PathState State)>();
const int MaxStatesPerInstruction = 16;
void Enqueue(int targetIndex, PathState stateToEnqueue)
{
worklist.Enqueue((targetIndex, stateToEnqueue));
}
int? GetFallthroughIndex(PpcInstruction instruction)
{
if (indexByAddress.TryGetValue(instruction.EndAddress, out var nextIndex))
{
return nextIndex;
}
return null;
}
Enqueue(0, PathState.Empty);
while (worklist.Count > 0)
{
var (idx, state) = worklist.Dequeue();
if (!visited[idx].Add(state))
{
continue;
}
if (visited[idx].Count > MaxStatesPerInstruction)
{
continue;
}
var instruction = instructions[idx];
var mnemonic = instruction.Mnemonic.ToLowerInvariant();
var nextState = state;
if (mnemonic == "mflr" && TryGetInstructionReg(instruction, 0, out var lrDest))
{
if (lrDest == "r1")
{
nextState = nextState.WithClearedStackOffsets();
}
nextState = nextState.LrReturnOffset.HasValue
? nextState.WithLrOffset(lrDest, nextState.LrReturnOffset.Value)
: nextState.WithoutLrOffset(lrDest);
}
else if ((mnemonic == "mr" || mnemonic == "or") &&
TryGetInstructionReg(instruction, 0, out var moveDest) &&
TryGetInstructionReg(instruction, 1, out var moveSource) &&
(mnemonic == "mr" ||
(instruction.Operands.Count >= 3 &&
instruction.Operands[2] is PpcRegisterOperand moveSource2 &&
string.Equals(NormalizeInstructionReg(moveSource2.Name), moveSource, StringComparison.OrdinalIgnoreCase))))
{
if (moveDest == "r1" && moveSource != "r1")
{
nextState = nextState.WithClearedStackOffsets();
}
nextState = nextState.LrOffsets.TryGetValue(moveSource, out var sourceOffset)
? nextState.WithLrOffset(moveDest, sourceOffset)
: nextState.WithoutLrOffset(moveDest);
}
else if ((mnemonic == "addi" || mnemonic == "addic") &&
TryGetInstructionReg(instruction, 0, out var addDest) &&
TryGetInstructionReg(instruction, 1, out var addBase) &&
TryGetInstructionImm(instruction, 2, out var imm))
{
if (addDest == "r1")
{
nextState = addBase == "r1"
? nextState.WithSpDelta(unchecked(nextState.SpDelta + imm))
: nextState.WithClearedStackOffsets();
}
nextState = nextState.LrOffsets.TryGetValue(addBase, out var baseOffset)
? nextState.WithLrOffset(addDest, unchecked(baseOffset + imm))
: nextState.WithoutLrOffset(addDest);
}
else if (mnemonic == "mtctr" && TryGetInstructionReg(instruction, 0, out var ctrSource))
{
var newCtrOffset = nextState.LrOffsets.TryGetValue(ctrSource, out var sourceOffset) ? sourceOffset : (int?)null;
nextState = nextState.WithCtrOffset(newCtrOffset);
}
else if (mnemonic == "mtlr" && TryGetInstructionReg(instruction, 0, out var lrSource))
{
var newLrReturnOffset = nextState.LrOffsets.TryGetValue(lrSource, out var sourceOffset) ? sourceOffset : (int?)null;
nextState = nextState.WithLrReturnOffset(newLrReturnOffset);
}
else if (mnemonic == "stw" &&
TryGetInstructionReg(instruction, 0, out var storeSrc) &&
TryGetInstructionDisplacement(instruction, 1, out var storeDisp, out var storeBase, out _))
{
if (storeBase == "r1")
{
var targetSlot = nextState.SpDelta + storeDisp;
nextState = nextState.LrOffsets.TryGetValue(storeSrc, out var offset)
? nextState.WithStackOffset(targetSlot, offset)
: nextState.WithoutStackOffset(targetSlot);
}
}
else if (mnemonic == "stwu" &&
TryGetInstructionReg(instruction, 0, out var stwuSrc) &&
TryGetInstructionDisplacement(instruction, 1, out var stwuDisp, out var stwuBase, out _))
{
if (stwuBase == "r1")
{
var targetSlot = nextState.SpDelta + stwuDisp;
nextState = nextState.LrOffsets.TryGetValue(stwuSrc, out var offset)
? nextState.WithStackOffset(targetSlot, offset)
: nextState.WithoutStackOffset(targetSlot);
nextState = nextState.WithAdjustedStackPointer(stwuDisp);
}
else
{
nextState = nextState.WithoutLrOffset(stwuBase);
}
}
else if (TryGetStackStoreRange(instruction, out var storeOffset, out var storeSize, out var updatesStackPointer))
{
nextState = nextState.WithoutStackOffsetsInRange(
nextState.SpDelta + storeOffset,
storeSize);
if (updatesStackPointer)
{
nextState = nextState.WithAdjustedStackPointer(storeOffset);
}
}
else if (mnemonic == "lwz" &&
TryGetInstructionReg(instruction, 0, out var loadDest) &&
TryGetInstructionDisplacement(instruction, 1, out var loadDisp, out var loadBase, out _))
{
if (loadBase == "r1")
{
var targetSlot = nextState.SpDelta + loadDisp;
var hasStackOffset = nextState.StackOffsets.TryGetValue(targetSlot, out var offset);
if (loadDest == "r1")
{
nextState = nextState.WithClearedStackOffsets();
}
nextState = hasStackOffset
? nextState.WithLrOffset(loadDest, offset)
: nextState.WithoutLrOffset(loadDest);
}
else
{
nextState = nextState.WithoutLrOffset(loadDest);
if (loadDest == "r1")
{
nextState = nextState.WithClearedStackOffsets();
}
}
}
else
{
if (TryInstructionWritesDest(instruction, out var destinations))
{
foreach (var dest in destinations)
{
nextState = nextState.WithoutLrOffset(dest);
if (dest == "r1")
{
nextState = nextState.WithClearedStackOffsets();
}
}
}
}
if (instruction.IsCall || mnemonic == "bl" || mnemonic == "blrl")
{
nextState = nextState.WithLrReturnOffset(null).WithCtrOffset(null);
for (var register = 0; register <= 12; register++)
{
if (register != 1 && register != 2)
{
nextState = nextState.WithoutLrOffset($"r{register}");
}
}
}
if (mnemonic == "bctr")
{
if (state.CtrOffset.HasValue && seenOffsets.Add(state.CtrOffset.Value))
{
yield return state.CtrOffset.Value;
}
nextState = nextState.WithCtrOffset(null);
if (instruction.BranchTargets.Count == 0)
{
continue;
}
}
var isReturn = !instruction.IsCall && (instruction.IsReturn || mnemonic == "blr" || mnemonic == "bclr" ||
(mnemonic.StartsWith("b", StringComparison.Ordinal) && mnemonic.EndsWith("lr", StringComparison.Ordinal)));
if (isReturn)
{
if (state.LrReturnOffset.HasValue && state.LrReturnOffset.Value != 0 && seenOffsets.Add(state.LrReturnOffset.Value))
{
yield return state.LrReturnOffset.Value;
}
if (!instruction.IsConditionalBranch)
{
continue;
}
}
if (instruction.IsUnconditionalBranch)
{
foreach (var target in instruction.BranchTargets)
{
if (indexByAddress.TryGetValue(target, out var targetIndex))
{
Enqueue(targetIndex, nextState);
}
}
}
else if (instruction.IsConditionalBranch)
{
var fallthrough = GetFallthroughIndex(instruction);
if (fallthrough.HasValue)
{
Enqueue(fallthrough.Value, nextState);
}
if (!isReturn)
{
foreach (var target in instruction.BranchTargets)
{
if (indexByAddress.TryGetValue(target, out var targetIndex))
{
Enqueue(targetIndex, nextState);
}
}
}
}
else
{
var fallthrough = GetFallthroughIndex(instruction);
if (fallthrough.HasValue)
{
Enqueue(fallthrough.Value, nextState);
}
}
}
static bool TryGetInstructionReg(PpcInstruction instruction, int index, out string register)
{
if (instruction.Operands.Count > index && instruction.Operands[index] is PpcRegisterOperand operand)
{
register = NormalizeInstructionReg(operand.Name);
return true;
}
register = string.Empty;
return false;
}
static bool TryGetInstructionDisplacement(PpcInstruction instruction, int index, out int offset, out string baseRegister, out int baseRegisterNumber)
{
if (instruction.Operands.Count > index && instruction.Operands[index] is PpcDisplacementOperand operand)
{
offset = operand.Offset;
baseRegister = NormalizeInstructionReg(operand.BaseRegister);
baseRegisterNumber = operand.BaseRegisterNumber;
return true;
}
offset = 0;
baseRegister = string.Empty;
baseRegisterNumber = -1;
return false;
}
static bool TryGetInstructionImm(PpcInstruction instruction, int index, out int immediate)
{
if (instruction.Operands.Count > index && instruction.Operands[index] is PpcImmediateOperand operand)
{
immediate = operand.Value;
return true;
}
immediate = 0;
return false;
}
static bool TryInstructionWritesDest(PpcInstruction instruction, out IReadOnlyList<string> destinations)
{
if (instruction.Operands.Count == 0 || instruction.Operands[0] is not PpcRegisterOperand operand)
{
destinations = Array.Empty<string>();
return false;
}
var mnemonic = instruction.Mnemonic.ToLowerInvariant();
if (mnemonic.StartsWith("st", StringComparison.Ordinal) ||
mnemonic.StartsWith("b", StringComparison.Ordinal) ||
mnemonic.StartsWith("cmp", StringComparison.Ordinal))
{
destinations = Array.Empty<string>();
return false;
}
if (mnemonic == "lmw")
{
var startReg = Math.Clamp(operand.Number, 0, 31);
var regs = new string[32 - startReg];
for (var r = startReg; r <= 31; r++)
{
regs[r - startReg] = $"r{r}";
}
destinations = regs;
return true;
}
destinations = [NormalizeInstructionReg(operand.Name)];
return true;
}
static bool TryGetStackStoreRange(PpcInstruction instruction, out int offset, out int size, out bool updatesStackPointer)
{
offset = 0;
size = 0;
updatesStackPointer = false;
if (!TryGetInstructionDisplacement(instruction, 1, out offset, out var baseRegister, out _) ||
baseRegister != "r1")
{
return false;
}
switch (instruction.Mnemonic.ToLowerInvariant())
{
case "stfs":
size = 4;
return true;
case "stfsu":
size = 4;
updatesStackPointer = true;
return true;
case "stfd":
size = 8;
return true;
case "stfdu":
size = 8;
updatesStackPointer = true;
return true;
case "stmw" when instruction.Operands[0] is PpcRegisterOperand register:
size = checked((32 - Math.Clamp(register.Number, 0, 31)) * 4);
return true;
default:
return false;
}
}
static string NormalizeInstructionReg(string register) => register.ToLowerInvariant();
}
private sealed class PathState : IEquatable<PathState>
{
public ImmutableDictionary<string, int> LrOffsets { get; }
public int? CtrOffset { get; }
public int? LrReturnOffset { get; }
public int SpDelta { get; }
public ImmutableDictionary<int, int> StackOffsets { get; }
public PathState(
ImmutableDictionary<string, int> lrOffsets,
int? ctrOffset,
int? lrReturnOffset,
int spDelta,
ImmutableDictionary<int, int> stackOffsets)
{
LrOffsets = lrOffsets;
CtrOffset = ctrOffset;
LrReturnOffset = lrReturnOffset;
SpDelta = spDelta;
StackOffsets = stackOffsets;
}
public static readonly PathState Empty = new(
ImmutableDictionary<string, int>.Empty.WithComparers(StringComparer.OrdinalIgnoreCase),
null,
0,
0,
ImmutableDictionary<int, int>.Empty);
public PathState WithLrOffset(string register, int offset) =>
LrOffsets.TryGetValue(register, out var cur) && cur == offset
? this
: new(LrOffsets.SetItem(register, offset), CtrOffset, LrReturnOffset, SpDelta, StackOffsets);
public PathState WithoutLrOffset(string register) =>
LrOffsets.ContainsKey(register)
? new(LrOffsets.Remove(register), CtrOffset, LrReturnOffset, SpDelta, StackOffsets)
: this;
public PathState WithCtrOffset(int? ctrOffset) =>
ctrOffset == CtrOffset
? this
: new(LrOffsets, ctrOffset, LrReturnOffset, SpDelta, StackOffsets);
public PathState WithLrReturnOffset(int? lrReturnOffset) =>
lrReturnOffset == LrReturnOffset
? this
: new(LrOffsets, CtrOffset, lrReturnOffset, SpDelta, StackOffsets);
public PathState WithSpDelta(int spDelta) =>
spDelta == SpDelta
? this
: new(LrOffsets, CtrOffset, LrReturnOffset, spDelta, StackOffsets);
public PathState WithAdjustedStackPointer(int displacement)
{
// r1 can hold an LR-relative address too. Update both relations;
// guest address arithmetic wraps at 32 bits.
var updated = WithSpDelta(unchecked(SpDelta + displacement));
return LrOffsets.TryGetValue("r1", out var offset)
? updated.WithLrOffset("r1", unchecked(offset + displacement))
: updated;
}
public PathState WithStackOffset(int slot, int offset) =>
StackOffsets.TryGetValue(slot, out var cur) && cur == offset
? this
: new(LrOffsets, CtrOffset, LrReturnOffset, SpDelta, StackOffsets.SetItem(slot, offset));
public PathState WithoutStackOffset(int slot) =>
StackOffsets.ContainsKey(slot)
? new(LrOffsets, CtrOffset, LrReturnOffset, SpDelta, StackOffsets.Remove(slot))
: this;
public PathState WithoutStackOffsetsInRange(int start, int size)
{
var end = checked(start + size);
var remaining = StackOffsets;
foreach (var slot in StackOffsets.Keys)
{
if (slot < end && start < checked(slot + 4))
{
remaining = remaining.Remove(slot);
}
}
return remaining.Count == StackOffsets.Count
? this
: new(LrOffsets, CtrOffset, LrReturnOffset, SpDelta, remaining);
}
public PathState WithClearedStackOffsets() =>
StackOffsets.IsEmpty
? this
: new(LrOffsets, CtrOffset, LrReturnOffset, SpDelta, ImmutableDictionary<int, int>.Empty);
public bool Equals(PathState? other)
{
if (ReferenceEquals(this, other)) return true;
if (other is null) return false;
if (CtrOffset != other.CtrOffset || LrReturnOffset != other.LrReturnOffset || SpDelta != other.SpDelta) return false;
if (LrOffsets.Count != other.LrOffsets.Count || StackOffsets.Count != other.StackOffsets.Count) return false;
foreach (var (k, v) in LrOffsets)
{
if (!other.LrOffsets.TryGetValue(k, out var otherV) || v != otherV)
{
return false;
}
}
foreach (var (k, v) in StackOffsets)
{
if (!other.StackOffsets.TryGetValue(k, out var otherV) || v != otherV)
{
return false;
}
}
return true;
}
public override bool Equals(object? obj) => obj is PathState other && Equals(other);
public override int GetHashCode()
{
var hash = new HashCode();
hash.Add(CtrOffset);
hash.Add(LrReturnOffset);
hash.Add(SpDelta);
hash.Add(LrOffsets.Count);
var regHash = 0;
foreach (var (k, v) in LrOffsets)
{
regHash ^= HashCode.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(k), v);
}
hash.Add(regHash);
hash.Add(StackOffsets.Count);
var stackHash = 0;
foreach (var (k, v) in StackOffsets)
{
stackHash ^= HashCode.Combine(k, v);
}
hash.Add(stackHash);
return hash.ToHashCode();
}
}
} }
@@ -1,4 +1,6 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Linq;
using Translator.Core.Disassembly;
using Translator.Core.Mods; using Translator.Core.Mods;
using Translator.Core.Mods.Mkwii; using Translator.Core.Mods.Mkwii;
using Translator.Core.Parsing.Kamek; using Translator.Core.Parsing.Kamek;
@@ -128,6 +130,54 @@ public class ContinuationPlannerTests
Assert.Contains("Retro WFC executable hook continuation", entry.Reason); Assert.Contains("Retro WFC executable hook continuation", entry.Reason);
} }
[Fact]
public void DiscoverLrRelativeIndirectJumpOffsets_DiscoversSkipReturnOffset()
{
var instructions = new[]
{
PpcDecoder.Decode(0x8180D8E8, 0x7FE802A6u), // mflr r31
PpcDecoder.Decode(0x8180D8EC, 0x3BFF0014u), // addi r31, r31, 20
PpcDecoder.Decode(0x8180D8F0, 0x7FE803A6u), // mtlr r31
PpcDecoder.Decode(0x8180D8F4, 0x4E800020u), // blr
};
var offsets = ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(instructions).ToArray();
var offset = Assert.Single(offsets);
Assert.Equal(20, offset);
}
[Fact]
public void DiscoverLrRelativeIndirectJumpOffsets_IgnoresStandardLrRestore()
{
var instructions = new[]
{
PpcDecoder.Decode(0x8180D8E8, 0x7FE802A6u), // mflr r31
PpcDecoder.Decode(0x8180D8EC, 0x93E10008u), // stw r31, 8(r1)
PpcDecoder.Decode(0x8180D8F0, 0x83E10008u), // lwz r31, 8(r1)
PpcDecoder.Decode(0x8180D8F4, 0x7FE803A6u), // mtlr r31
PpcDecoder.Decode(0x8180D8F8, 0x4E800020u), // blr
};
var offsets = ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(instructions);
Assert.Empty(offsets);
}
[Fact]
public void DiscoverLrRelativeIndirectJumpOffsets_SupportsBctrOffset()
{
var instructions = new[]
{
PpcDecoder.Decode(0x8180D8E8, 0x7FE802A6u), // mflr r31
PpcDecoder.Decode(0x8180D8EC, 0x397F0008u), // addi r11, r31, 8
PpcDecoder.Decode(0x8180D8F0, 0x7D6903A6u), // mtctr r11
PpcDecoder.Decode(0x8180D8F4, 0x4E800420u), // bctr
};
var offsets = ContinuationPlanner.DiscoverLrRelativeIndirectJumpOffsets(instructions).ToArray();
var offset = Assert.Single(offsets);
Assert.Equal(8, offset);
}
private static KamekChunk EmptyChunk() => private static KamekChunk EmptyChunk() =>
new( new(
0, 0,
@@ -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,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();
}
}