This commit is contained in:
patchzyy committed 2026-08-23 17:10:50 +02:00
commit ec226e8348
1099 files changed
+400981

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
# Translator tests
`Translator.Tests` is the default binary-free suite. It covers the decoder, IR/SSA, code
generation helpers, parsers with synthetic inputs, project-manifest loading, DOL-only image
construction, and generic entry-point translation.
The older asset-backed and host-C++-compiler-backed test sources are intentionally retained in
`Translator.Tests` but excluded in its project file. They mixed MKWii fixtures, several compiler
selection strategies, and private-method reflection checks, which made the normal suite both slow
and red on a clean Windows setup. They should become a separate opt-in integration project when
that work is useful again.
An external generic DOL can be included without adding it to the repository:
```powershell
$env:RECOMP_GENERIC_DOL = 'D:\path\to\main.dol'
dotnet test translator/Translator.sln -c Release
```
The test treats the file as a generic DOL: it reads the DOL entry point, builds a DOL-only image,
and translates that entry point without game-specific addresses or bindings.
@@ -0,0 +1,671 @@
using System;
using System.Collections.Generic;
using Translator.Core.Analysis.Ssa;
using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
public class ResidentAbiCodeGenTests
{
[Fact]
public void StateFreeLeafVariantHasOnlyLiveNativeInputsAndNoCpuContext()
{
var function = new IrFunction("state_free_store", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrStore(new IrAddress("r3", 4), IrValue.Register("r4"), 4),
new IrReturn(null)
})
});
var code = new CxxLinearCodeGenerator().Emit(
0x80063FF0,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("state_free_store", ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["r3"] = ValueRepresentation.UInt32,
["r4"] = ValueRepresentation.UInt32
}),
emitStateFreeLeafVariant: true,
stateFreeCallSymbols: new Dictionary<uint, string>
{
[0x80063FF0] = "state_free_store_native"
},
stateFreeEntryVariants: new[]
{
new GuestStateFreeCallVariant(
0x80063FF0, "state_free_store_native_v0", GuestAbiContractAnalyzer.Analyze(function))
});
var variantStart = code.IndexOf(
"state_free_store_native(uint32_t native_r3, uint32_t native_r4)",
StringComparison.Ordinal);
Assert.True(variantStart >= 0, code);
var markerEnd = code.IndexOf('\n', code.IndexOf("RECOMP_STATE_FREE_ABI", variantStart, StringComparison.Ordinal));
var variant = code[variantStart..markerEnd];
Assert.Contains("uint32_t native_r3, uint32_t native_r4", variant, StringComparison.Ordinal);
Assert.DoesNotContain("CpuContext", variant, StringComparison.Ordinal);
Assert.DoesNotContain("ctx->", variant, StringComparison.Ordinal);
Assert.DoesNotContain("native_r5", variant, StringComparison.Ordinal);
Assert.Contains("RECOMP_STATE_FREE_ABI", variant, StringComparison.Ordinal);
Assert.Contains(
"extern \"C\" MKW_PPC_ALWAYS_INLINE_BODY void state_free_store_native_v0(",
code,
StringComparison.Ordinal);
var publicStart = code.IndexOf(
"extern \"C\" void state_free_store(CpuContext* MKW_RESTRICT ctx)",
StringComparison.Ordinal);
var publicEnd = code.IndexOf(
"extern \"C\" MKW_PPC_ALWAYS_INLINE_BODY",
publicStart,
StringComparison.Ordinal);
var publicBody = code[publicStart..publicEnd];
Assert.Contains("r3", publicBody, StringComparison.Ordinal);
Assert.DoesNotContain("cached_r3", publicBody, StringComparison.Ordinal);
Assert.Contains("cached_r3", variant, StringComparison.Ordinal);
}
[Fact]
public void StateFreeLeafElidesDeadLrAssignmentWithNoncanonicalSource()
{
const uint address = 0x80063FE0u;
var function = new IrFunction("state_free_dead_lr", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrAssign("lr", IrValue.Register("r0")),
new IrReturn(null)
})
});
var planned = GuestAbiContractAnalyzer.Analyze(function) with { MayWriteLr = false };
var code = new CxxLinearCodeGenerator().Emit(
address,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("state_free_dead_lr", ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["r0"] = ValueRepresentation.UInt32,
["lr"] = ValueRepresentation.UInt32
}),
emitStateFreeLeafVariant: true,
stateFreeAbiContracts: new Dictionary<uint, GuestAbiContract> { [address] = planned },
stateFreeCallSymbols: new Dictionary<uint, string> { [address] = "state_free_dead_lr_native" });
var start = code.IndexOf("state_free_dead_lr_native(", StringComparison.Ordinal);
var marker = code.IndexOf("RECOMP_STATE_FREE_ABI", start, StringComparison.Ordinal);
var variant = code[start..marker];
Assert.DoesNotContain("ctx->lr", variant, StringComparison.Ordinal);
Assert.DoesNotContain("CpuContext", variant, StringComparison.Ordinal);
}
[Fact]
public void DirectStateFreeCallUsesLiveArgumentsAndRetainsNormalAbiFallback()
{
const uint calleeAddress = 0x80063FF0u;
var function = new IrFunction("state_free_caller", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall(string.Empty, $"0x{calleeAddress:X8}", Array.Empty<IrValue>()),
new IrReturn(null)
})
});
var contract = new GuestAbiContract(
GprReadBeforeWriteMask: (1u << 3) | (1u << 4),
GprPossibleWriteMask: 0,
GprReturnMask: 0,
FprReadBeforeWriteMask: 0,
FprPossibleWriteMask: 0,
FprReturnMask: 0,
CrReadBeforeWriteMask: 0,
CrPossibleWriteMask: 0,
ReadsXerBeforeWrite: false,
MayWriteXer: false,
ReadsCtrBeforeWrite: false,
MayWriteCtr: false,
ReadsLrBeforeWrite: false,
MayWriteLr: false,
BoundaryFlags: GuestCallBoundaryFlags.None,
DirectCallTargets: Array.Empty<uint>());
var code = new CxxLinearCodeGenerator().Emit(
0x80063FE0,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("state_free_caller", ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
// The caller itself is emitted as a state-free interface so its own
// architectural GPRs live in native locals; that is what makes the
// direct state-free call site pass cached values instead of context.
emitStateFreeLeafVariant: true,
stateFreeAbiContracts: new Dictionary<uint, GuestAbiContract>
{
[calleeAddress] = contract,
[0x80063FE0u] = contract
},
stateFreeCallSymbols: new Dictionary<uint, string>
{
[calleeAddress] = "state_free_store_native",
[0x80063FE0u] = "state_free_caller_native"
});
Assert.Contains($"IsBaseTranslatedCpuTargetActive<0x{calleeAddress:X8}u>()", code, StringComparison.Ordinal);
Assert.Contains($"InvokeDirectCpu<0x{calleeAddress:X8}u>(ctx);", code, StringComparison.Ordinal);
// Inside the caller's own state-free body the call site must consume the
// native register locals; the retained public CpuContext entry keeps the
// ordinary ctx-based fallback and is deliberately excluded here.
var variantStart = code.IndexOf("void state_free_caller_native(", StringComparison.Ordinal);
Assert.True(variantStart >= 0, "Missing state-free variant for the caller.");
var variant = code[variantStart..code.IndexOf("RECOMP_STATE_FREE_ABI", variantStart, StringComparison.Ordinal)];
Assert.Contains("state_free_store_native(cached_r3, cached_r4);", variant, StringComparison.Ordinal);
Assert.DoesNotContain("state_free_store_native(ctx", variant, StringComparison.Ordinal);
}
[Fact]
public void OversizedStateFreeRegionCannotBypassContextBoundary()
{
const uint calleeAddress = 0x80063FF0u;
var function = new IrFunction("oversized_state_free_caller", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall(string.Empty, $"0x{calleeAddress:X8}", Array.Empty<IrValue>()),
new IrReturn(null)
})
});
var contract = new GuestAbiContract(
GprReadBeforeWriteMask: 0x1Fu,
GprPossibleWriteMask: 1u << 3,
GprReturnMask: 1u << 3,
FprReadBeforeWriteMask: 0,
FprPossibleWriteMask: 0,
FprReturnMask: 0,
CrReadBeforeWriteMask: 0,
CrPossibleWriteMask: 0,
ReadsXerBeforeWrite: false,
MayWriteXer: false,
ReadsCtrBeforeWrite: false,
MayWriteCtr: false,
ReadsLrBeforeWrite: false,
MayWriteLr: false,
BoundaryFlags: GuestCallBoundaryFlags.None,
DirectCallTargets: Array.Empty<uint>());
var code = new CxxLinearCodeGenerator().Emit(
0x80063FE0,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("oversized_state_free_caller", ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
stateFreeAbiContracts: new Dictionary<uint, GuestAbiContract>
{
[calleeAddress] = contract
},
stateFreeCallSymbols: new Dictionary<uint, string>
{
[calleeAddress] = "oversized_state_free_native"
});
Assert.Contains("if (false) {", code, StringComparison.Ordinal);
Assert.Contains($"InvokeDirectCpu<0x{calleeAddress:X8}u>(ctx);", code, StringComparison.Ordinal);
}
[Fact]
public void ExactCallSiteCanSelectCompactSpecializedResultContract()
{
const uint calleeAddress = 0x80063FF0u;
var function = new IrFunction("state_free_specialized_caller", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall(string.Empty, $"0x{calleeAddress:X8}", Array.Empty<IrValue>()),
new IrAssign("r7", IrValue.Imm(1)),
new IrCall(string.Empty, $"0x{calleeAddress:X8}", Array.Empty<IrValue>()),
new IrReturn(null)
})
});
var globalContract = new GuestAbiContract(
(1u << 3) | (1u << 5), (1u << 4) | (1u << 6), 0,
0, 0, 0, 0, 0,
false, false, false, false, false, false,
GuestCallBoundaryFlags.None, Array.Empty<uint>());
var specializedContract = globalContract with
{
GprReadBeforeWriteMask = 1u << 3,
GprPossibleWriteMask = 1u << 4
};
var variant = new GuestStateFreeCallVariant(
calleeAddress, "state_free_store_native_v0", specializedContract);
var code = new CxxLinearCodeGenerator().Emit(
0x80063FE0,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("state_free_specialized_caller", ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
stateFreeAbiContracts: new Dictionary<uint, GuestAbiContract> { [calleeAddress] = globalContract },
stateFreeCallSymbols: new Dictionary<uint, string> { [calleeAddress] = "state_free_store_native" },
stateFreeCallSiteVariants: new Dictionary<GuestStateFreeCallSiteKey, GuestStateFreeCallVariant>
{
[new GuestStateFreeCallSiteKey("entry", calleeAddress, 0)] = variant
});
Assert.Contains("state_free_store_native_v0(r3)", code, StringComparison.Ordinal);
Assert.Contains("state_free_store_native(r3, r5)", code, StringComparison.Ordinal);
Assert.DoesNotContain("state_free_store_native_v0(r5)", code, StringComparison.Ordinal);
}
[Fact]
public void StateFreeDirectCallPassesDefinedFallthroughLrInsteadOfContextLr()
{
const uint calleeAddress = 0x80063FF0u;
var function = new IrFunction("state_free_lr_caller", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrAssign("lr", IrValue.Imm(0x80064008u)),
new IrCall("lr", $"0x{calleeAddress:X8}", Array.Empty<IrValue>()),
new IrReturn(null)
})
});
var contract = new GuestAbiContract(
1u << 3, 0, 0, 0, 0, 0, 0, 0,
false, false, false, false, true, false,
GuestCallBoundaryFlags.None, Array.Empty<uint>());
var code = new CxxLinearCodeGenerator().Emit(
0x80064000u,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("state_free_lr_caller", ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
stateFreeAbiContracts: new Dictionary<uint, GuestAbiContract> { [calleeAddress] = contract },
stateFreeCallSymbols: new Dictionary<uint, string> { [calleeAddress] = "state_free_lr_native" });
Assert.Contains("state_free_lr_native(r3, 0x80064008u)", code, StringComparison.Ordinal);
Assert.DoesNotContain("state_free_lr_native(r3, ctx->lr)", code, StringComparison.Ordinal);
}
[Fact]
public void StateFreeLeafReturnsExplicitGprAndFprStateWithoutCpuContext()
{
var function = new IrFunction("state_free_results", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrAssign("r3", IrValue.Register("r4")),
new IrAssign("f1", IrValue.Register("f2")),
new IrReturn(null)
})
});
var code = new CxxLinearCodeGenerator().Emit(
0x80063FD0,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("state_free_results", ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["r3"] = ValueRepresentation.UInt32,
["r4"] = ValueRepresentation.UInt32,
["f1"] = ValueRepresentation.Float64,
["f2"] = ValueRepresentation.Float64
}),
emitStateFreeLeafVariant: true,
stateFreeCallSymbols: new Dictionary<uint, string>
{
[0x80063FD0] = "state_free_results_native"
});
var variantStart = code.IndexOf("MkwStateFreeResult2 state_free_results_native(", StringComparison.Ordinal);
var markerEnd = code.IndexOf('\n', code.IndexOf("RECOMP_STATE_FREE_ABI", variantStart, StringComparison.Ordinal));
var variant = code[variantStart..markerEnd];
Assert.Contains("MkwStateFreeResult2", variant, StringComparison.Ordinal);
Assert.Contains("state_free_results_native(", variant, StringComparison.Ordinal);
Assert.Contains("PPC_FPR native_f1", variant, StringComparison.Ordinal);
Assert.Contains("PPC_FPR native_f2", variant, StringComparison.Ordinal);
Assert.Contains("return { static_cast<uint64_t>(cached_r3), cached_f1.raw };", variant, StringComparison.Ordinal);
Assert.DoesNotContain("ctx->", variant, StringComparison.Ordinal);
}
[Fact]
public void TwoValueSpecializationDoesNotReuseLargerAddressResultStruct()
{
const uint address = 0x80063FC0u;
var function = new IrFunction("state_free_result_collision", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrAssign("r3", IrValue.Imm(1)),
new IrAssign("r4", IrValue.Imm(2)),
new IrAssign("r5", IrValue.Imm(3)),
new IrReturn(null)
})
});
var publicContract = GuestAbiContractAnalyzer.Analyze(function);
var compactContract = publicContract with { GprPossibleWriteMask = (1u << 3) | (1u << 4) };
var code = new CxxLinearCodeGenerator().Emit(
address,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("state_free_result_collision", ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
emitStateFreeLeafVariant: true,
stateFreeAbiContracts: new Dictionary<uint, GuestAbiContract> { [address] = publicContract },
stateFreeCallSymbols: new Dictionary<uint, string> { [address] = "state_free_result_collision_native" },
stateFreeEntryVariants: new[]
{
new GuestStateFreeCallVariant(address, "state_free_result_collision_native_v0", compactContract)
});
Assert.Contains("MkwStateFreeResult_80063FC0 state_free_result_collision_native(", code, StringComparison.Ordinal);
Assert.Contains("MkwStateFreeResult2 state_free_result_collision_native_v0(", code, StringComparison.Ordinal);
Assert.DoesNotContain("using MkwStateFreeResult_80063FC0", code, StringComparison.Ordinal);
}
[Fact]
public void DirectStateFreeCallWritesOnlyExplicitResults()
{
const uint calleeAddress = 0x80063FD0u;
var function = new IrFunction("state_free_result_caller", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall(string.Empty, $"0x{calleeAddress:X8}", Array.Empty<IrValue>()),
new IrReturn(null)
})
});
var contract = new GuestAbiContract(
GprReadBeforeWriteMask: 1u << 4,
GprPossibleWriteMask: 1u << 3,
GprReturnMask: 1u << 3,
FprReadBeforeWriteMask: 1u << 2,
FprPossibleWriteMask: 1u << 1,
FprReturnMask: 1u << 1,
CrReadBeforeWriteMask: 0,
CrPossibleWriteMask: 0,
ReadsXerBeforeWrite: false,
MayWriteXer: false,
ReadsCtrBeforeWrite: false,
MayWriteCtr: false,
ReadsLrBeforeWrite: false,
MayWriteLr: false,
BoundaryFlags: GuestCallBoundaryFlags.None,
DirectCallTargets: Array.Empty<uint>());
var code = new CxxLinearCodeGenerator().Emit(
0x80063FC0,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("state_free_result_caller", ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
stateFreeAbiContracts: new Dictionary<uint, GuestAbiContract> { [calleeAddress] = contract },
stateFreeCallSymbols: new Dictionary<uint, string> { [calleeAddress] = "state_free_results_native" });
Assert.Matches(@"const auto state_free_result_[0-9A-F_]+ = state_free_results_native\(", code);
Assert.Matches(@"r3 = static_cast<uint32_t>\(state_free_result_[0-9A-F_]+\[0\]\);", code);
Assert.Matches(@"f1\.raw = static_cast<uint64_t>\(state_free_result_[0-9A-F_]+\[1\]\);", code);
Assert.DoesNotContain("r4 = state_free_result", code, StringComparison.Ordinal);
Assert.DoesNotContain("f2 = state_free_result", code, StringComparison.Ordinal);
}
[Fact]
public void DirectStateFreeCallExtractsRawBitsFromStructFprResults()
{
const uint calleeAddress = 0x80063FBCu;
var function = new IrFunction("state_free_struct_result_caller", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall(string.Empty, $"0x{calleeAddress:X8}", Array.Empty<IrValue>()),
new IrReturn(null)
})
});
var contract = new GuestAbiContract(
0, 0, 0, 0, (1u << 0) | (1u << 2) | (1u << 3) | (1u << 4), 0,
0, 0, false, false, false, false, false, false,
GuestCallBoundaryFlags.None, Array.Empty<uint>());
var code = new CxxLinearCodeGenerator().Emit(
0x80063FB8,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("state_free_struct_result_caller", ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
stateFreeAbiContracts: new Dictionary<uint, GuestAbiContract> { [calleeAddress] = contract },
stateFreeCallSymbols: new Dictionary<uint, string> { [calleeAddress] = "state_free_struct_native" });
Assert.Matches(@"f0\.raw = static_cast<uint64_t>\(state_free_result_[0-9A-F_]+\.f0\.raw\);", code);
Assert.Matches(@"f4\.raw = static_cast<uint64_t>\(state_free_result_[0-9A-F_]+\.f4\.raw\);", code);
}
[Fact]
public void NonLeafStateFreeVariantCallsNativeCalleeWithoutContextFallback()
{
const uint callerAddress = 0x80063FB0u;
const uint calleeAddress = 0x80063FA0u;
var callee = new GuestAbiContract(
1u << 3, 1u << 4, 1u << 4, 0, 0, 0, 0, 0,
false, false, false, false, false, false,
GuestCallBoundaryFlags.None, Array.Empty<uint>());
var caller = new GuestAbiContract(
1u << 3, (1u << 4) | (1u << 5), 0, 0, 0, 0, 0, 0,
false, false, false, false, false, false,
GuestCallBoundaryFlags.None, new[] { calleeAddress });
var function = new IrFunction("state_free_nonleaf", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall("lr", $"0x{calleeAddress:X8}", Array.Empty<IrValue>()),
new IrAssign("r5", IrValue.Register("r4")),
new IrReturn(null)
})
});
var code = new CxxLinearCodeGenerator().Emit(
callerAddress,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("state_free_nonleaf", ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
emitStateFreeLeafVariant: true,
guestAbiContracts: new Dictionary<uint, GuestAbiContract> { [calleeAddress] = callee },
stateFreeAbiContracts: new Dictionary<uint, GuestAbiContract>
{
[callerAddress] = caller with { GprPossibleWriteMask = 1u << 5 },
[calleeAddress] = callee
},
stateFreeCallSymbols: new Dictionary<uint, string>
{
[callerAddress] = "state_free_nonleaf_native",
[calleeAddress] = "state_free_callee_native"
});
var start = code.IndexOf("state_free_nonleaf_native(", StringComparison.Ordinal);
var marker = code.IndexOf("RECOMP_STATE_FREE_ABI", start, StringComparison.Ordinal);
var variant = code[start..marker];
Assert.Contains("state_free_callee_native(", variant, StringComparison.Ordinal);
Assert.Contains("return static_cast<uint64_t>(cached_r5);", variant, StringComparison.Ordinal);
Assert.DoesNotContain("CpuContext", variant, StringComparison.Ordinal);
Assert.DoesNotContain("ctx->", variant, StringComparison.Ordinal);
Assert.DoesNotContain("InvokeDirectCpu", variant, StringComparison.Ordinal);
Assert.DoesNotContain("KnownTranslatedCpuCall", variant, StringComparison.Ordinal);
}
[Fact]
public void StateFreeTailCallerForwardsIncomingLrWithoutContextAccess()
{
const uint callerAddress = 0x80063FB0u;
const uint calleeAddress = 0x80063FA0u;
var callee = new GuestAbiContract(
1u << 3, 1u << 3, 1u << 3, 0, 0, 0, 0, 0,
false, false, false, false, true, false,
GuestCallBoundaryFlags.None, Array.Empty<uint>());
var caller = new GuestAbiContract(
1u << 3, 1u << 3, 1u << 3, 0, 0, 0, 0, 0,
false, false, false, false, false, false,
GuestCallBoundaryFlags.None, new[] { calleeAddress });
var function = new IrFunction("state_free_tail_caller", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall(string.Empty, $"0x{calleeAddress:X8}", Array.Empty<IrValue>()),
new IrReturn(null)
})
});
var code = new CxxLinearCodeGenerator().Emit(
callerAddress,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("state_free_tail_caller", ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
emitStateFreeLeafVariant: true,
guestAbiContracts: new Dictionary<uint, GuestAbiContract> { [calleeAddress] = callee },
stateFreeAbiContracts: new Dictionary<uint, GuestAbiContract>
{
[callerAddress] = caller,
[calleeAddress] = callee
},
stateFreeCallSymbols: new Dictionary<uint, string>
{
[callerAddress] = "state_free_tail_caller_native",
[calleeAddress] = "state_free_tail_callee_native"
});
var start = code.IndexOf("state_free_tail_caller_native(", StringComparison.Ordinal);
var marker = code.IndexOf("RECOMP_STATE_FREE_ABI", start, StringComparison.Ordinal);
var variant = code[start..marker];
Assert.Contains("uint32_t native_lr", variant, StringComparison.Ordinal);
Assert.Contains("state_free_tail_callee_native(cached_r3, native_lr)", variant, StringComparison.Ordinal);
Assert.DoesNotContain("ctx->", variant, StringComparison.Ordinal);
}
[Fact]
public void PublicLeafDoesNotElideNonvolatileGprStackRoundTrip()
{
var function = new IrFunction("public_leaf_gpr_save", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrStore(new IrAddress("r1", 12), IrValue.Register("r31"), 4),
new IrAssign("r31", IrValue.Imm(7)),
new IrLoad("r31", new IrAddress("r1", 12), 4),
new IrReturn(null)
})
});
var code = new CxxLinearCodeGenerator().Emit(
0x80064008,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("public_leaf_gpr_save", ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
enableLeafAbiSpillElision: true);
Assert.Contains("FlatWriteRam32", code, StringComparison.Ordinal);
Assert.Contains("FlatRead32", code, StringComparison.Ordinal);
Assert.Contains("r31 = 7", code, StringComparison.Ordinal);
Assert.Contains("r31 = MemoryInline::FlatRead32", code, StringComparison.Ordinal);
}
[Fact]
public void GprOnlyLeafCacheDoesNotImplicitlyElideGuestAbiSpills()
{
var function = new IrFunction("cached_leaf_gpr_save", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrStore(new IrAddress("r1", 12), IrValue.Register("r31"), 4),
new IrAssign("r31", IrValue.Imm(7)),
new IrLoad("r31", new IrAddress("r1", 12), 4),
new IrReturn(null)
})
});
var code = new CxxLinearCodeGenerator().Emit(
0x80064010,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("cached_leaf_gpr_save", ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()));
Assert.Contains("FlatWriteRam32", code, StringComparison.Ordinal);
Assert.Contains("FlatRead32", code, StringComparison.Ordinal);
}
[Fact]
public void StackFastPathFollowsFramePointerCopiedIntoArchitecturalGpr()
{
var function = new IrFunction("derived_stack_gpr", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrBinary("r1", IrValue.Register("r1"), IrValue.Imm(64), "sub"),
new IrBinary("r11", IrValue.Register("r1"), IrValue.Imm(64), "add"),
new IrStore(new IrAddress("r11", -28), IrValue.Register("r25"), 4),
new IrLoad("r25", new IrAddress("r11", -28), 4),
new IrReturn(null)
})
});
var code = new CxxLinearCodeGenerator().Emit(
0x80064014,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("derived_stack_gpr", ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()));
// A stack slot store skips the MMIO policy check (FlatWriteRam) instead of the general guarded
// write (FlatWrite); reads have no such distinction, both spell as FlatRead.
Assert.Contains("MemoryInline::FlatWriteRam32", code, StringComparison.Ordinal);
Assert.Contains("MemoryInline::FlatRead32", code, StringComparison.Ordinal);
Assert.DoesNotContain("MemoryInline::FlatWrite32", code, StringComparison.Ordinal);
}
[Fact]
public void StackFastPathFollowsMultipleSsaDerivedAddressRegisters()
{
var function = new IrFunction("transitive_stack_gpr", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrAssign("r11", IrValue.Register("r1")),
new IrBinary("r12", IrValue.Register("r11"), IrValue.Imm(32), "add"),
new IrStore(new IrAddress("r12", -8), IrValue.Register("r31"), 4),
new IrLoad("r31", new IrAddress("r12", -8), 4),
new IrReturn(null)
})
});
var code = new CxxLinearCodeGenerator().Emit(
0x80064018,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("transitive_stack_gpr", ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()));
// See StackFastPathFollowsFramePointerCopiedIntoArchitecturalGpr: only
// the store side still distinguishes the stack fast path in the emitted
// helper name.
Assert.Contains("MemoryInline::FlatWriteRam32", code, StringComparison.Ordinal);
Assert.Contains("MemoryInline::FlatRead32", code, StringComparison.Ordinal);
Assert.DoesNotContain("MemoryInline::FlatWrite32", code, StringComparison.Ordinal);
}
[Fact]
public void GuestAbiContractModelsInlineGprThunkRegisterEffects()
{
var function = new IrFunction("thunk_contract", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall(string.Empty, "0x80021580", Array.Empty<IrValue>()), // save r19-r31
new IrCall(string.Empty, "0x800215CC", Array.Empty<IrValue>()), // rest r19-r31
new IrReturn(null)
})
});
var contract = GuestAbiContractAnalyzer.Analyze(function);
const uint expected = 0xFFF80000u;
Assert.Equal(expected, contract.GprReadBeforeWriteMask & expected);
Assert.Equal(expected, contract.GprPossibleWriteMask & expected);
Assert.Empty(contract.DirectCallTargets);
}
}
@@ -0,0 +1,37 @@
using Translator.Core.Mods;
using Xunit;
namespace Translator.Tests;
public class BaseFunctionIndexTests
{
[Fact]
public void FindContainingUsesHalfOpenRanges()
{
var index = new BaseFunctionIndex(
[
new BaseFunctionRangeMetadata(
0x80533600,
0x80533820,
"func_80533600",
"StaticR.rel:1",
0x123600,
"test",
["Executable"]),
new BaseFunctionRangeMetadata(
0x80533820,
0x80533900,
"func_80533820",
"StaticR.rel:1",
0x123820,
"test",
["Executable"])
]);
Assert.Null(index.FindContaining(0x805335FC));
Assert.Equal(0x80533600u, index.FindContaining(0x80533600)!.Start);
Assert.Equal(0x80533600u, index.FindContaining(0x8053381C)!.Start);
Assert.Equal(0x80533820u, index.FindContaining(0x80533820)!.Start);
Assert.Null(index.FindContaining(0x80533900));
}
}
@@ -0,0 +1,95 @@
using System;
using System.IO;
using Translator.Core.Mods;
using Xunit;
namespace Translator.Tests;
public sealed class BaseManifestBuilderTests
{
[Fact]
public void EmbeddedSwitchCaseTranslationDoesNotSplitOwningFunctionRange()
{
var directory = Path.Combine(Path.GetTempPath(), $"translator-base-manifest-{Guid.NewGuid():N}");
Directory.CreateDirectory(directory);
try
{
File.WriteAllText(
Path.Combine(directory, "func_8062C3A4.cpp"),
"extern \"C\" void func_8062C3A4() { goto loc_8062C64C; loc_8062C64C:; }");
File.WriteAllText(
Path.Combine(directory, "func_8062C64C.cpp"),
"extern \"C\" void func_8062C64C() { loc_8062C64C:; }");
File.WriteAllText(
Path.Combine(directory, "func_80630094.cpp"),
"extern \"C\" void func_80630094() { loc_80630094:; }");
var starts = BaseManifestBuilder.DiscoverCanonicalGeneratedFunctionStarts(directory);
Assert.Equal([0x8062C3A4u, 0x80630094u], starts);
}
finally
{
Directory.Delete(directory, recursive: true);
}
}
[Fact]
public void MetadataEmbeddedLabelDoesNotSplitOwningFunctionRange()
{
var metadata = BaseTranslationOutputMetadata.Create([
new BaseTranslationFunctionMetadata("func_8062C3A4.cpp", 100, new string('a', 64), 0x8062C3A4u, [0x8062C64Cu]),
new BaseTranslationFunctionMetadata("func_8062C64C.cpp", 40, new string('b', 64), 0x8062C64Cu, []),
new BaseTranslationFunctionMetadata("func_80630094.cpp", 80, new string('c', 64), 0x80630094u, [])
], TranslationQualityMetadata.Clean);
var starts = BaseManifestBuilder.DiscoverCanonicalGeneratedFunctionStarts(metadata);
Assert.Equal([0x8062C3A4u, 0x80630094u], starts);
}
[Fact]
public void OutputMetadataWriteIsDeterministicAndChangeAware()
{
var directory = Path.Combine(Path.GetTempPath(), $"translator-output-metadata-{Guid.NewGuid():N}");
var path = Path.Combine(directory, "base_output.json");
var metadata = BaseTranslationOutputMetadata.Create([
new BaseTranslationFunctionMetadata("b.cpp", 2, new string('b', 64), 2, []),
new BaseTranslationFunctionMetadata("a.cpp", 1, new string('a', 64), 1, [3])
], TranslationQualityMetadata.Clean, "identity");
try
{
Assert.True(BaseTranslationOutputMetadataFile.WriteIfChangedAtomic(path, metadata));
var timestamp = File.GetLastWriteTimeUtc(path);
Assert.False(BaseTranslationOutputMetadataFile.WriteIfChangedAtomic(path, metadata));
Assert.Equal(timestamp, File.GetLastWriteTimeUtc(path));
var roundTrip = BaseTranslationOutputMetadataFile.Read(path);
Assert.Equal("a.cpp", roundTrip.Functions[0].RelativePath);
Assert.Equal("identity", roundTrip.TranslationIdentityHash);
Assert.Equal(TranslationQualityMetadata.Clean, roundTrip.Quality);
}
finally
{
if (Directory.Exists(directory)) Directory.Delete(directory, recursive: true);
}
}
[Theory]
[InlineData(1, 0)]
[InlineData(0, 1)]
public void ReleaseEligibilityRejectsTranslationQualityFailures(
int unsupportedInstructionCount,
int invalidSsaFunctionCount)
{
var metadata = BaseTranslationOutputMetadata.Create([
new BaseTranslationFunctionMetadata(
"func_80001000.cpp", 1, new string('a', 64), 0x80001000u, [])
], new TranslationQualityMetadata(unsupportedInstructionCount, invalidSsaFunctionCount));
var error = Assert.Throws<InvalidDataException>(() =>
metadata.RequireReleaseEligible("test metadata"));
Assert.Contains("Release translation quality failure", error.Message, StringComparison.Ordinal);
}
}
@@ -0,0 +1,162 @@
using System.Buffers.Binary;
using Translator.Core.Mods;
using Translator.Core.Parsing.Kamek;
using Xunit;
namespace Translator.Tests;
/// <summary>
/// Pins base-translation reuse decisions: a Code.pul that touches nothing inside a translated
/// function should be reusable, and one that moves a patch on/off a translated function must not be.
/// </summary>
public class BaseTranslationModAwarenessTests
{
// Two translated functions with a gap between them, so an address can be provably outside both.
private static readonly Dictionary<uint, uint> FunctionEnds = new()
{
[0x80005F34] = 0x8000608C,
[0x80543BB4] = 0x80543C40
};
[Fact]
public void ReusesTheBaseTranslationForAPulThatPatchesTheSameTranslatedFunctions()
{
using var original = TemporaryPul(
Absolute(KamekCommandId.BranchLink, 0x80005F40),
Absolute(KamekCommandId.Write32, 0x80001920));
using var candidate = TemporaryPul(
// The low-memory write is gone and a new one appears, but neither is inside a translated
// function, so the translation could not have seen either.
Absolute(KamekCommandId.BranchLink, 0x80005F40),
Absolute(KamekCommandId.Write32, 0x80003000));
var awareness = Awareness(original.Path);
Assert.True(awareness.CoversCodePul("retro-rewind", candidate.Path, out var reason), reason);
}
[Fact]
public void RefusesAPulThatMovesAPatchOntoADifferentTranslatedFunction()
{
using var original = TemporaryPul(Absolute(KamekCommandId.BranchLink, 0x80005F40));
using var candidate = TemporaryPul(Absolute(KamekCommandId.BranchLink, 0x80543BB8));
var awareness = Awareness(original.Path);
Assert.False(awareness.CoversCodePul("retro-rewind", candidate.Path, out var reason));
Assert.Contains("80543BB8", reason, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void RefusesAPulThatStopsPatchingATranslatedFunction()
{
using var original = TemporaryPul(
Absolute(KamekCommandId.BranchLink, 0x80005F40),
Absolute(KamekCommandId.Branch, 0x80543BB8));
using var candidate = TemporaryPul(Absolute(KamekCommandId.BranchLink, 0x80005F40));
var awareness = Awareness(original.Path);
Assert.False(awareness.CoversCodePul("retro-rewind", candidate.Path, out _));
}
[Fact]
public void RefusesAProfileTheBaseTranslationNeverSaw()
{
using var original = TemporaryPul(Absolute(KamekCommandId.BranchLink, 0x80005F40));
var awareness = Awareness(original.Path);
Assert.False(awareness.CoversCodePul("some-other-mod", original.Path, out var reason));
Assert.Contains("retro-rewind", reason, StringComparison.Ordinal);
}
[Fact]
public void RefusesWhenSeveralProfilesShapedTheTranslation()
{
using var first = TemporaryPul(Absolute(KamekCommandId.BranchLink, 0x80005F40));
using var second = TemporaryPul(Absolute(KamekCommandId.Branch, 0x80543BB8));
var awareness = BaseTranslationModAwareness.Create(null, 2,
[
("retro-rewind", "P", new string('a', 64),
BaseTranslationModAwareness.PatchedAddresses(first.Path, "P")),
("other", "P", new string('b', 64),
BaseTranslationModAwareness.PatchedAddresses(second.Path, "P"))
],
FunctionEnds);
Assert.False(awareness.CoversCodePul("retro-rewind", first.Path, out var reason));
Assert.Contains("2 mod profile", reason, StringComparison.Ordinal);
}
[Fact]
public void SurvivesAWriteAndReadRoundTrip()
{
using var original = TemporaryPul(
Absolute(KamekCommandId.BranchLink, 0x80005F40),
Absolute(KamekCommandId.Write32, 0x80001920));
var path = Path.Combine(Path.GetTempPath(), $"mkwc-awareness-{Guid.NewGuid():N}.json");
try
{
BaseTranslationModAwarenessFile.WriteIfChangedAtomic(path, Awareness(original.Path));
var round = BaseTranslationModAwarenessFile.Read(path);
Assert.Equal(2, round.TranslatedFunctionCount);
Assert.Equal(4, round.TranslatedFunctionRanges.Count);
// Only the patch inside a translated function is recorded as consequential.
Assert.Equal([0x80005F40u], round.Profiles[0].ConsequentialPatchedAddresses);
Assert.True(round.CoversCodePul("retro-rewind", original.Path, out var reason), reason);
}
finally
{
File.Delete(path);
}
}
private static BaseTranslationModAwareness Awareness(string codePulPath) =>
BaseTranslationModAwareness.Create(null, FunctionEnds.Count,
[("retro-rewind", "P", new string('a', 64),
BaseTranslationModAwareness.PatchedAddresses(codePulPath, "P"))],
FunctionEnds);
private static TemporaryFile TemporaryPul(params byte[][] commands)
{
var file = new TemporaryFile();
File.WriteAllBytes(file.Path, BuildChunk([0x60, 0x00, 0x00, 0x00], commands));
return file;
}
private static byte[] BuildChunk(byte[] code, byte[][] commands)
{
var chunkSize = KamekChunk.HeaderSize + code.Length + commands.Sum(command => command.Length);
var data = new byte[chunkSize];
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(0x00), KamekChunk.Magic0);
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(0x04), KamekChunk.Magic1);
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(0x08), 0x10);
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(0x0C), (uint)code.Length);
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(0x18), (uint)chunkSize);
code.CopyTo(data, KamekChunk.HeaderSize);
var offset = KamekChunk.HeaderSize + code.Length;
foreach (var command in commands)
{
command.CopyTo(data, offset);
offset += command.Length;
}
return data;
}
private static byte[] Absolute(KamekCommandId id, uint address)
{
var data = new byte[12];
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(0), ((uint)(byte)id << 24) | 0x00FFFFFEu);
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(4), address);
return data;
}
private sealed class TemporaryFile : IDisposable
{
public string Path { get; } =
System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"mkwc-pul-{Guid.NewGuid():N}.pul");
public void Dispose() => File.Delete(Path);
}
}
@@ -0,0 +1,69 @@
using System.IO;
using System.Linq;
using Translator.Core.Analysis.BasicBlocks;
using Translator.Core.Disassembly;
using Translator.Core.Loading;
using Xunit;
namespace Translator.Tests;
public class BasicBlockTests
{
private static (ProgramImage image, PpcDisassembler disassembler) Setup()
{
var root = ProjectPaths.FindRepositoryRoot();
var assets = Path.Combine(root, "assets");
var image = new ProgramImageBuilder().Build(Path.Combine(assets, "main.dol"), Path.Combine(assets, "StaticR.rel"));
return (image, new PpcDisassembler());
}
[Fact]
public void BuildsBasicBlocksWithTerminatingBranches()
{
var (image, dis) = Setup();
using (dis)
{
var instructions = dis.DisassembleFunction(image, 0x800060A4, maxInstructions: 128);
var blocks = BasicBlockBuilder.Build(instructions);
Assert.NotEmpty(blocks);
Assert.True(blocks.All(b => b.Instructions.Count > 0), "Every block should contain at least one instruction.");
Assert.True(blocks.All(b =>
b.Terminator.IsReturn ||
b.Terminator.IsConditionalBranch ||
b.Terminator.IsUnconditionalBranch ||
b.Terminator.IsCall ||
b.Successors.Count > 0),
"Each block must either end with a control-transfer or have an explicit fallthrough successor.");
// CFG stitching: every successor must list this block as predecessor.
foreach (var block in blocks)
{
foreach (var succAddr in block.Successors)
{
var succ = blocks.Single(b => b.StartAddress == succAddr);
Assert.Contains(block.StartAddress, succ.Predecessors);
}
}
}
}
[Fact]
public void BuildSplitsAtRequestedEntryLeaderInsideDecodedRange()
{
var instructions = new[]
{
PpcInstruction.Synthetic(0x8064F7EC, 0x60000000, "nop", []),
PpcInstruction.Synthetic(0x8064F7F0, 0x60000000, "nop", []),
PpcInstruction.Synthetic(0x8064F7F4, 0x60000000, "nop", []),
PpcInstruction.Synthetic(0x8064F7F8, 0x60000000, "nop", []),
PpcInstruction.Synthetic(0x8064F7FC, 0x4E800020, "blr", [], isReturn: true)
};
var blocks = BasicBlockBuilder.Build(instructions, new[] { 0x8064F7F4u });
Assert.Contains(blocks, b => b.StartAddress == 0x8064F7ECu);
Assert.Contains(blocks, b => b.StartAddress == 0x8064F7F4u);
Assert.Equal(new[] { 0x8064F7ECu, 0x8064F7F4u }, blocks.Select(b => b.StartAddress).ToArray());
}
}
@@ -0,0 +1,72 @@
using System;
using System.IO;
using Translator.Core.IO;
using Xunit;
namespace Translator.Tests;
public class BigEndianBinaryReaderTests
{
[Fact]
public void ReadsBigEndianValuesAndTracksPosition()
{
using var stream = new MemoryStream(new byte[]
{
0x12,
0x34, 0x56,
0x78, 0x9A, 0xBC, 0xDE,
0xF0, 0x0D
});
using var reader = new BigEndianBinaryReader(stream);
Assert.Equal(0, reader.Position);
Assert.Equal(0x12, reader.ReadByte());
Assert.Equal(1, reader.Position);
Assert.Equal(0x3456, reader.ReadUInt16());
Assert.Equal(0x789ABCDEu, reader.ReadUInt32());
Assert.Equal(new byte[] { 0xF0, 0x0D }, reader.ReadBytes(2));
Assert.Equal(stream.Length, reader.Position);
}
[Fact]
public void SeekMovesWithinStream()
{
using var stream = new MemoryStream(new byte[] { 0x00, 0x01, 0xAA, 0xBB, 0xCC, 0xDD });
using var reader = new BigEndianBinaryReader(stream);
reader.Seek(2, SeekOrigin.Begin);
Assert.Equal(2, reader.Position);
Assert.Equal(0xAABB, reader.ReadUInt16());
reader.Seek(-2, SeekOrigin.Current);
Assert.Equal(2, reader.Position);
Assert.Equal(0xAABBCCDDu, reader.ReadUInt32());
}
[Fact]
public void ThrowsOnShortReads()
{
using var stream = new MemoryStream(new byte[] { 0x12, 0x34, 0x56 });
using var reader = new BigEndianBinaryReader(stream);
Assert.Throws<EndOfStreamException>(() => reader.ReadUInt32());
reader.Seek(0, SeekOrigin.Begin);
Assert.Throws<EndOfStreamException>(() => reader.ReadBytes(4));
}
[Fact]
public void LeaveOpenPreservesUnderlyingStream()
{
var stream = new MemoryStream(new byte[] { 0xDE, 0xAD, 0xBE, 0xEF });
using (var reader = new BigEndianBinaryReader(stream, leaveOpen: true))
{
Assert.Equal(0xDEAD, reader.ReadUInt16());
}
Assert.True(stream.CanRead);
Assert.Equal(2, stream.Position);
using var reader2 = new BigEndianBinaryReader(stream, leaveOpen: true);
Assert.Equal(0xBEEF, reader2.ReadUInt16());
stream.Dispose();
}
}
@@ -0,0 +1,39 @@
using System.Buffers.Binary;
using Translator.Core.Loading;
using Translator.Core.Translation;
using Xunit;
namespace Translator.Tests;
public sealed class CanonicalIrStoreTests
{
[Fact]
public void RoundTripFeedsIdenticalFinalLowering()
{
var memory = new byte[8];
BinaryPrimitives.WriteUInt32BigEndian(memory.AsSpan(0, 4), 0x38630001u); // addi r3,r3,1
BinaryPrimitives.WriteUInt32BigEndian(memory.AsSpan(4, 4), 0x4E800020u); // blr
var image = new ProgramImage(
memory,
AddressRange.FromStartAndSize(MemoryLayout.RamBase, (uint)memory.Length),
AddressRange.FromStartAndSize(MemoryLayout.RamBase, (uint)memory.Length),
default,
"canonical-test");
var translator = new FunctionTranslator(image);
var options = TranslationOptions.Default with
{
PreferredName = "canonical_test",
AllowUnsupportedInstructions = true
};
var direct = translator.Translate(MemoryLayout.RamBase, options);
var discovery = translator.Discover(MemoryLayout.RamBase, options);
var store = new CanonicalIrStore();
store.Put(MemoryLayout.RamBase, discovery.Ssa.Function);
Assert.Equal(1, store.Count);
Assert.True(store.TryGet(MemoryLayout.RamBase, out var restored));
var lowered = translator.LowerCanonical(MemoryLayout.RamBase, restored, options);
Assert.Equal(direct.CxxCode, lowered.CxxCode);
}
}
@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using Translator.Core.Analysis;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
public class CarryHelperCodeGenTests
{
[Fact]
public void CarryHelpersUseCpuContextWithoutRuntimeCalls()
{
var method = typeof(CxxLinearCodeGenerator).GetMethod(
"TryEmitInlinePpc",
BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(method);
var cases = new[]
{
new IrCall("xer", "PPC_UpdateCarrySub", new[] { IrValue.Register("r3"), IrValue.Register("r4") }),
new IrCall("xer", "PPC_UpdateCarryAdd", new[] { IrValue.Register("r5"), IrValue.Register("r6"), IrValue.Imm(1) }),
new IrCall("xer", "PPC_UpdateCarryShiftRight", new[] { IrValue.Register("r7"), IrValue.Register("r8") }),
new IrCall("r9_ca", "PPC_GetCarry", Array.Empty<IrValue>())
};
foreach (var call in cases)
{
var output = new StringBuilder();
var emitted = (bool)method!.Invoke(null, new object?[]
{
call,
output,
" ",
new RepresentationEnvironment(),
new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase),
StackAddressFacts.Empty
})!;
Assert.True(emitted, call.Target);
Assert.Contains("ctx->xer", output.ToString(), StringComparison.Ordinal);
Assert.DoesNotContain($"{call.Target}(", output.ToString(), StringComparison.Ordinal);
if (call.Target == "PPC_UpdateCarryAdd")
{
Assert.Contains("static_cast<uint64_t>(static_cast<uint32_t>", output.ToString(), StringComparison.Ordinal);
}
}
}
[Fact]
public void CountLeadingZerosUsesInlinePrimitive()
{
var method = typeof(CxxLinearCodeGenerator).GetMethod(
"TryEmitInlinePpc", BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(method);
var output = new StringBuilder();
var call = new IrCall("r4", "PPC_Cntlzw", new[] { IrValue.Register("r3") });
var emitted = (bool)method!.Invoke(null, new object?[]
{
call,
output,
" ",
new RepresentationEnvironment(),
new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase),
StackAddressFacts.Empty
})!;
Assert.True(emitted);
Assert.Contains("PPC_CntlzwInline", output.ToString(), StringComparison.Ordinal);
Assert.DoesNotContain("PPC_Cntlzw(", output.ToString(), StringComparison.Ordinal);
}
}
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace Translator.Tests;
public sealed class CliHelperTests
{
[Fact]
public void IndexedParallelPreservesIndexedResultsAcrossExecutionModes()
{
var sequential = new int[8];
var parallel = new int[8];
IndexedParallel.For(
sequential.Length,
new ParallelOptions { MaxDegreeOfParallelism = 1 },
index => sequential[index] = index * 3);
IndexedParallel.For(
parallel.Length,
new ParallelOptions { MaxDegreeOfParallelism = 4 },
index => parallel[index] = index * 3);
Assert.Equal(sequential, parallel);
Assert.Equal(Enumerable.Range(0, sequential.Length).Select(index => index * 3), sequential);
}
[Fact]
public void IndexedParallelUnwrapsA_singleWorkerFailure()
{
var exception = Assert.Throws<InvalidOperationException>(() => IndexedParallel.For(
8,
new ParallelOptions { MaxDegreeOfParallelism = 4 },
index =>
{
if (index == 0)
{
throw new InvalidOperationException("indexed failure");
}
}));
Assert.Equal("indexed failure", exception.Message);
}
[Fact]
public void QueueBatchPreservesFifoAndSkipsUntilTheBatchIsFull()
{
var queue = new Queue<int>(new[] { 1, 2, 3, 4, 5 });
var first = QueueBatch.Dequeue(queue, 2, value => value % 2 == 0);
Assert.Equal(new[] { 1, 3 }, first);
Assert.Equal(new[] { 4, 5 }, queue);
}
}
@@ -0,0 +1,115 @@
using System;
using System.Reflection;
using Translator.Core.Analysis.Representation;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
public class CodeGenExpressionCoverageTests
{
private static readonly Type GeneratorType = typeof(CxxLinearCodeGenerator);
private static T InvokePrivate<T>(string name, params object?[] args)
{
var method = GeneratorType.GetMethod(name, BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(method);
return (T)method!.Invoke(null, args)!;
}
private static string FormatBinary(IrBinary binary, RepresentationEnvironment types)
{
var left = InvokePrivate<string>("ToExpression", binary.Left, types);
var right = InvokePrivate<string>("ToExpression", binary.Right, types);
return InvokePrivate<string>("BinaryExpressionWithOperands", binary, left, right);
}
[Fact]
public void BinaryExpression_FormatsRareOperations()
{
var types = new RepresentationEnvironment();
Assert.Equal(
"(static_cast<int32_t>(ctx->gpr[3]))",
FormatBinary(new IrBinary("r4", IrValue.Register("r3"), IrValue.Imm(12), "sext"), types));
Assert.Equal(
"~(ctx->gpr[3] & ctx->gpr[4])",
FormatBinary(new IrBinary("r5", IrValue.Register("r3"), IrValue.Register("r4"), "nand"), types));
Assert.Equal(
"~(ctx->gpr[3] ^ ctx->gpr[4])",
FormatBinary(new IrBinary("r5", IrValue.Register("r3"), IrValue.Register("r4"), "eqv"), types));
Assert.Equal(
"std::sqrt(ctx->fpr[1].d)",
FormatBinary(new IrBinary("f2", IrValue.Register("f1"), IrValue.Register("f3"), "fsqrt"), types));
Assert.Equal(
"(ctx->fpr[1].d - ctx->fpr[2].d)",
FormatBinary(new IrBinary("f3", IrValue.Register("f1"), IrValue.Register("f2"), "fcmp"), types));
Assert.Equal(
"PPC_Divw(static_cast<int32_t>(ctx->gpr[7]), static_cast<int32_t>(ctx->gpr[8]))",
FormatBinary(new IrBinary("r9", IrValue.Register("r7"), IrValue.Register("r8"), "div"), types));
Assert.Equal(
"(static_cast<int32_t>(static_cast<int16_t>(ctx->gpr[9])))",
FormatBinary(new IrBinary("r10", IrValue.Register("r9"), IrValue.Imm(0), "sext16"), types));
Assert.Equal(
"(CompareUnsigned(static_cast<uint32_t>(ctx->gpr[11]), static_cast<uint32_t>(ctx->gpr[12])))",
FormatBinary(new IrBinary("r13", IrValue.Register("r11"), IrValue.Register("r12"), "sub_u"), types));
Assert.Equal(
"PPC_Fctiwz(ctx->fpr[4].d)",
FormatBinary(new IrBinary("f5", IrValue.Register("f4"), IrValue.Imm(0), "fctiwz"), types));
Assert.Equal(
"PPC_Fctiw(ctx->fpr[4].d)",
FormatBinary(new IrBinary("f5", IrValue.Register("f4"), IrValue.Imm(0), "fctiw"), types));
Assert.Equal(
"PPC_Slw(static_cast<uint32_t>(ctx->gpr[3]), static_cast<uint32_t>(ctx->gpr[4]))",
FormatBinary(new IrBinary("r5", IrValue.Register("r3"), IrValue.Register("r4"), "ppc_slw"), types));
Assert.Equal(
"PPC_Srw(static_cast<uint32_t>(ctx->gpr[3]), static_cast<uint32_t>(ctx->gpr[4]))",
FormatBinary(new IrBinary("r5", IrValue.Register("r3"), IrValue.Register("r4"), "ppc_srw"), types));
Assert.Equal(
"PPC_Sraw(static_cast<uint32_t>(ctx->gpr[3]), static_cast<uint32_t>(ctx->gpr[4]))",
FormatBinary(new IrBinary("r5", IrValue.Register("r3"), IrValue.Register("r4"), "ppc_sraw"), types));
}
[Fact]
public void RegisterHelpers_MapSpecialRegistersAndConditions()
{
var types = new RepresentationEnvironment();
Assert.Equal("ctx->cr", InvokePrivate<string>("RegisterToCtxRead", "cr", types));
Assert.Equal("((ctx->cr >> 0) & 0xF)", InvokePrivate<string>("RegisterToCtxRead", "cr7", types));
Assert.Equal("ctx->srr0", InvokePrivate<string>("RegisterToCtxRead", "srr0", types));
Assert.Equal("ctx->srr1", InvokePrivate<string>("RegisterToCtxRead", "srr1", types));
Assert.Equal("ctx->hid0", InvokePrivate<string>("RegisterToCtxRead", "hid0", types));
Assert.Equal("ctx->hid1", InvokePrivate<string>("RegisterToCtxRead", "hid1", types));
Assert.Equal("ctx->hid2", InvokePrivate<string>("RegisterToCtxRead", "hid2", types));
Assert.Equal("ctx->gqr[3]", InvokePrivate<string>("RegisterToCtxRead", "gqr3", types));
Assert.Equal("0", InvokePrivate<string>("RegisterToCtxRead", "gqr9", types));
Assert.Equal("ctx->cr", InvokePrivate<string>("RegisterToCtxWrite", "cr", types));
Assert.Equal("cr6", InvokePrivate<string>("RegisterToCtxWrite", "cr6", types));
Assert.Equal("ctx->hid2", InvokePrivate<string>("RegisterToCtxWrite", "hid2", types));
Assert.Equal("ctx->gqr[5]", InvokePrivate<string>("RegisterToCtxWrite", "gqr5", types));
Assert.Equal("gqr8", InvokePrivate<string>("RegisterToCtxWrite", "gqr8", types));
Assert.Equal("0", InvokePrivate<string>("ToExpression", new IrValue("temporary"), types));
Assert.Equal("tmp_local", InvokePrivate<string>("ToExpression", IrValue.Register("tmp_local"), types));
Assert.Equal("(ctx->ctr != 0)", InvokePrivate<string>("ToCondition", "bdnz", "ctr", types));
Assert.Equal("(ctx->ctr == 0)", InvokePrivate<string>("ToCondition", "bdz", "ctr", types));
Assert.Equal("((ctx->cr & 0x00000001u) != 0)", InvokePrivate<string>("ToCondition", "bso", "cr7", types));
Assert.Equal("((ctx->cr & 0x00000001u) == 0)", InvokePrivate<string>("ToCondition", "bns", "cr7", types));
Assert.Equal("flag_local", InvokePrivate<string>("ToCondition", "custom", "flag_local", types));
Assert.Equal("ctx->gpr[4]", InvokePrivate<string>("Address", new IrAddress("r4", 0), types));
Assert.Equal("(ctx->gpr[4] + 16)", InvokePrivate<string>("Address", new IrAddress("r4", 16), types));
Assert.Equal("r3", InvokePrivate<string>("BaseRegister", "r3_17"));
Assert.Equal("tmp", InvokePrivate<string>("BaseRegister", "tmp_local"));
Assert.Equal(8, InvokePrivate<int>("Bits", 1));
Assert.Equal(16, InvokePrivate<int>("Bits", 2));
Assert.Equal(64, InvokePrivate<int>("Bits", 8));
Assert.Equal(32, InvokePrivate<int>("Bits", 4));
}
}
@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
public class CodeGenInlineCoverageTests
{
private static readonly Type GeneratorType = typeof(CxxLinearCodeGenerator);
private static bool InvokeTryEmitInline(
IrCall call,
StringBuilder sb,
RepresentationEnvironment types,
Dictionary<string, bool> localPaired)
{
var method = GeneratorType.GetMethod("TryEmitInlinePpc", BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(method);
return (bool)method!.Invoke(null, new object?[] { call, sb, " ", types, localPaired })!;
}
[Fact]
public void TryEmitInlinePpc_EmitsSupportedHelperBodies()
{
var types = new RepresentationEnvironment();
var paired = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase)
{
["f1"] = true,
["f2"] = false,
["f3"] = true
};
var supported = new (IrCall Call, string Fragment)[]
{
(new IrCall("f5", "PPC_PsFromScalar", new[] { IrValue.Register("f2") }), "PPC_PsFromScalarInline"),
(new IrCall("f6", "PPC_PsToScalar", new[] { IrValue.Register("f1") }), "PPC_PsToScalarInline"),
(new IrCall("f7", "PPC_PsNeg", new[] { IrValue.Imm(7) }), "PPC_PsNegInline"),
(new IrCall("f8", "PPC_PsAbs", new[] { IrValue.Register("f2") }), "PPC_PsAbsInline"),
(new IrCall("f9", "PPC_PsSum0", new[] { IrValue.Register("f2"), IrValue.Register("f3"), IrValue.Register("f1") }), "PPC_PsSum0Inline"),
(new IrCall("f10", "PPC_PsSum1", new[] { IrValue.Register("f2"), IrValue.Register("f3"), IrValue.Register("f1") }), "PPC_PsSum1Inline"),
(new IrCall("f21", "PPC_PsDiv", new[] { IrValue.Register("f1"), IrValue.Register("f3") }), "PPC_PsDivInline"),
(new IrCall("f11", "PPC_Fadds", new[] { IrValue.Register("f1"), IrValue.Register("f2") }), "PpcForceSingleValueInline"),
(new IrCall("f12", "PPC_Fsubs", new[] { IrValue.Register("f2"), IrValue.Register("f1") }), "PpcForceSingleValueInline"),
(new IrCall("f13", "PPC_Fmuls", new[] { IrValue.Register("f1"), IrValue.Register("f2") }), "PpcFmulsInline"),
(new IrCall("f14", "PPC_Fdivs", new[] { IrValue.Register("f2"), IrValue.Register("f1") }), "PpcForceSingleValueInline"),
(new IrCall("f15", "PPC_Fsqrt", new[] { IrValue.Register("f1") }), "std::sqrt"),
(new IrCall("f16", "PPC_Fmadd", new[] { IrValue.Register("f2"), IrValue.Register("f1"), IrValue.Register("f3") }), "PpcFmaddInline"),
(new IrCall("f17", "PPC_Fmsub", new[] { IrValue.Register("f2"), IrValue.Register("f1"), IrValue.Register("f3") }), "PpcFmsubInline"),
(new IrCall("f18", "PPC_Fnmadd", new[] { IrValue.Register("f2"), IrValue.Register("f1"), IrValue.Register("f3") }), "PpcFnmaddInline"),
(new IrCall("f19", "PPC_Fnmsub", new[] { IrValue.Register("f2"), IrValue.Register("f1"), IrValue.Register("f3") }), "PpcFnmsubInline"),
(new IrCall("f20", "PPC_PsqL", new[] { IrValue.Register("r3"), IrValue.Imm(1), IrValue.Imm(5) }), "PPC_PsqLInline<1u, 5u>"),
(new IrCall(string.Empty, "PPC_PsqSt", new[] { IrValue.Register("r3"), IrValue.Register("f1"), IrValue.Imm(0), IrValue.Imm(4) }), "PPC_PsqStInline<0u, 4u>")
};
foreach (var (call, fragment) in supported)
{
var sb = new StringBuilder();
Assert.True(InvokeTryEmitInline(call, sb, types, paired), call.Target);
Assert.Contains(fragment, sb.ToString(), StringComparison.Ordinal);
}
}
[Fact]
public void TryEmitInlinePpc_CoversFallbacksAndMissingArguments()
{
var types = new RepresentationEnvironment();
var paired = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
Assert.False(InvokeTryEmitInline(new IrCall("f1", "", Array.Empty<IrValue>()), new StringBuilder(), types, paired));
foreach (var target in new[]
{
"unknown_helper"
})
{
var sb = new StringBuilder();
Assert.False(InvokeTryEmitInline(new IrCall("f1", target, new[] { IrValue.Register("f1") }), sb, types, paired), target);
Assert.Equal(0, sb.Length);
}
var psqMissingImmediates = new StringBuilder();
Assert.False(
InvokeTryEmitInline(
new IrCall("f1", "PPC_PsqL", new[] { IrValue.Register("r3"), IrValue.Register("r4"), IrValue.Imm(0) }),
psqMissingImmediates,
types,
paired));
Assert.Equal(0, psqMissingImmediates.Length);
var missingArgs = new StringBuilder();
Assert.True(InvokeTryEmitInline(new IrCall("f3", "PPC_Fsqrt", Array.Empty<IrValue>()), missingArgs, types, paired));
Assert.Contains("std::sqrt(0);", missingArgs.ToString(), StringComparison.Ordinal);
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,161 @@
using System.Buffers.Binary;
using Translator.Core.Mods;
using Translator.Core.Mods.Mkwii;
using Translator.Core.Parsing.Kamek;
using Xunit;
namespace Translator.Tests;
public class ContinuationPlannerTests
{
[Fact]
public void AddModuleTailJumpContinuationsAddsInternalBaseTarget()
{
const uint moduleBase = 0x81200000u;
var moduleImage = new byte[0x20];
WriteU32(moduleImage, 0x00, 0x3D80807Eu); // lis r12,0x807E
WriteU32(moduleImage, 0x04, 0x618C3064u); // ori r12,r12,0x3064
WriteU32(moduleImage, 0x08, 0x7D8903A6u); // mtctr r12
WriteU32(moduleImage, 0x0C, 0x4E800420u); // bctr
var plan = ContinuationPlanner.Build(EmptyChunk(), TestManifest(), moduleBase);
var result = ContinuationPlanner.AddModuleTailJumpContinuations(
plan,
TestManifest(),
moduleBase,
moduleImage);
var entry = Assert.Single(result.Entries);
Assert.Equal(0x807E3064u, entry.Address);
Assert.Equal(0x807E2D18u, entry.ContainingFunctionStart);
Assert.Equal(0x807E30ACu, entry.ContainingFunctionEnd);
Assert.Equal(0x8120000Cu, entry.SourceCommandAddress);
Assert.Contains("tail jump", entry.Reason);
}
[Fact]
public void AddModuleTailJumpContinuationsSupportsSignedAddiLowImmediate()
{
const uint moduleBase = 0x81200000u;
var moduleImage = new byte[0x20];
WriteU32(moduleImage, 0x00, 0x3D80807Fu); // lis r12,0x807F
WriteU32(moduleImage, 0x04, 0x398CFFF0u); // addi r12,r12,-0x10
WriteU32(moduleImage, 0x08, 0x7D8903A6u); // mtctr r12
WriteU32(moduleImage, 0x0C, 0x4E800420u); // bctr
var manifest = new BaseManifest(
"test",
1,
"RMCP01",
"P",
"",
0,
[
new BaseSectionMetadata(".text", "StaticR.rel", 0x807EFFF0u, 0x807F0100u, true, false, "base_text.bin", 0)
],
[
new BaseFunctionRangeMetadata(0x807EFFE0u, 0x807F0100u, "func_807EFFE0", ".text", 0, "test", ["Executable"])
],
"ranges.json");
var plan = ContinuationPlanner.Build(EmptyChunk(), manifest, moduleBase);
var result = ContinuationPlanner.AddModuleTailJumpContinuations(plan, manifest, moduleBase, moduleImage);
var entry = Assert.Single(result.Entries);
Assert.Equal(0x807EFFF0u, entry.Address);
Assert.Equal(0x807EFFE0u, entry.ContainingFunctionStart);
}
[Fact]
public void AddModuleTailJumpContinuationsIgnoresFunctionEntries()
{
const uint moduleBase = 0x81200000u;
var moduleImage = new byte[0x20];
WriteU32(moduleImage, 0x00, 0x3D80807Eu); // lis r12,0x807E
WriteU32(moduleImage, 0x04, 0x618C2D18u); // ori r12,r12,0x2D18
WriteU32(moduleImage, 0x08, 0x7D8903A6u); // mtctr r12
WriteU32(moduleImage, 0x0C, 0x4E800420u); // bctr
var manifest = TestManifest();
var plan = ContinuationPlanner.Build(EmptyChunk(), manifest, moduleBase);
var result = ContinuationPlanner.AddModuleTailJumpContinuations(plan, manifest, moduleBase, moduleImage);
Assert.Empty(result.Entries);
}
[Fact]
public void AddRetroWfcExecutableHookContinuationsAddsInternalBaseTarget()
{
var manifest = new BaseManifest(
"test",
1,
"RMCP01",
"P",
"",
0,
[
new BaseSectionMetadata(".text", "main.dol", 0x800F164Cu, 0x800F1BC8u, true, false, "base_text.bin", 0)
],
[
new BaseFunctionRangeMetadata(0x800F164Cu, 0x800F1BC8u, "func_800F164C", ".text", 0, "test", ["Executable"])
],
"ranges.json");
var hook = new RetroWfcExecutableHookPlan(
5,
"executableHookWithContinuation",
"branchCtr",
0x800F164Cu,
4,
0x800F164Cu,
0x800F1BC8u,
".text",
["support.hostnameRewrite.gethostbyname"],
"overlayHookPatch",
0x8179E1A4u,
"support.hostnameRewrite.gethostbyname",
"moduleFunction",
null,
0x800F165Cu);
var plan = ContinuationPlanner.Build(EmptyChunk(), manifest, 0x81200000u);
var result = ContinuationPlanner.AddRetroWfcExecutableHookContinuations(plan, manifest, [hook]);
var entry = Assert.Single(result.Entries);
Assert.Equal(0x800F165Cu, entry.Address);
Assert.Equal(0x800F164Cu, entry.ContainingFunctionStart);
Assert.Equal(0x800F1BC8u, entry.ContainingFunctionEnd);
Assert.Equal(0x800F164Cu, entry.SourceCommandAddress);
Assert.Contains("Retro WFC executable hook continuation", entry.Reason);
}
private static KamekChunk EmptyChunk() =>
new(
0,
0,
0,
0,
0,
0,
KamekChunk.HeaderSize,
[],
[]);
private static BaseManifest TestManifest() =>
new(
"test",
1,
"RMCP01",
"P",
"",
0,
[
new BaseSectionMetadata(".text", "StaticR.rel", 0x807E2D18u, 0x807E30ACu, true, false, "base_text.bin", 0)
],
[
new BaseFunctionRangeMetadata(0x807E2D18u, 0x807E30ACu, "func_807E2D18", ".text", 0, "test", ["Executable"])
],
"ranges.json");
private static void WriteU32(byte[] data, int offset, uint value) =>
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(offset, 4), value);
}
@@ -0,0 +1,67 @@
using System.Collections.Generic;
using Translator.Core.Analysis;
using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis.Ssa;
using Translator.Core.CodeGen;
using Translator.Core.Representation;
namespace Translator.Tests;
/// <summary>
/// Test-only wrapper around <see cref="CxxLinearCodeGenerator.EmitWithFacts"/> that returns just the
/// emitted C++ text, so codegen tests don't have to thread an unused facts object through every call.
/// </summary>
internal static class CxxLinearCodeGeneratorEmitExtensions
{
public static string Emit(
this CxxLinearCodeGenerator generator,
uint entryPoint,
SsaResult ssa,
FunctionAbiClassification signature,
RepresentationEnvironment types,
bool emitModRegistration = false,
uint modRegistrationPriority = 100,
ulong modRegistrationModuleId = 1,
IReadOnlySet<uint>? nonReturningCallTargets = null,
IReadOnlySet<uint>? lrContinuationCallTargets = null,
IReadOnlyDictionary<uint, GuestAbiContract>? guestAbiContracts = null,
bool emitStateFreeLeafVariant = false,
IReadOnlyDictionary<uint, GuestAbiContract>? stateFreeAbiContracts = null,
IReadOnlyDictionary<uint, string>? stateFreeCallSymbols = null,
IReadOnlyDictionary<GuestStateFreeCallSiteKey, GuestStateFreeCallVariant>? stateFreeCallSiteVariants = null,
IReadOnlyList<GuestStateFreeCallVariant>? stateFreeEntryVariants = null,
uint? moduleLinkBase = null,
uint? moduleGuestBase = null,
uint moduleLinkedCodeSize = 0,
IReadOnlyDictionary<string, uint>? gqrEntryConstants = null,
IReadOnlyDictionary<uint, byte>? gqrCalleeWriteMasks = null,
bool gqrConstantsRequireRuntimeGuard = false,
bool enableLeafAbiSpillElision = false,
IReadOnlySet<uint>? modOverridableCallTargets = null,
bool enableGpuFifoBurstCoalescing = true) =>
generator.EmitWithFacts(
entryPoint: entryPoint,
ssa: ssa,
signature: signature,
types: types,
emitModRegistration: emitModRegistration,
modRegistrationPriority: modRegistrationPriority,
modRegistrationModuleId: modRegistrationModuleId,
nonReturningCallTargets: nonReturningCallTargets,
lrContinuationCallTargets: lrContinuationCallTargets,
guestAbiContracts: guestAbiContracts,
emitStateFreeLeafVariant: emitStateFreeLeafVariant,
stateFreeAbiContracts: stateFreeAbiContracts,
stateFreeCallSymbols: stateFreeCallSymbols,
stateFreeCallSiteVariants: stateFreeCallSiteVariants,
stateFreeEntryVariants: stateFreeEntryVariants,
moduleLinkBase: moduleLinkBase,
moduleGuestBase: moduleGuestBase,
moduleLinkedCodeSize: moduleLinkedCodeSize,
gqrEntryConstants: gqrEntryConstants,
gqrCalleeWriteMasks: gqrCalleeWriteMasks,
gqrConstantsRequireRuntimeGuard: gqrConstantsRequireRuntimeGuard,
enableLeafAbiSpillElision: enableLeafAbiSpillElision,
modOverridableCallTargets: modOverridableCallTargets,
enableGpuFifoBurstCoalescing: enableGpuFifoBurstCoalescing).Code;
}
@@ -0,0 +1,137 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using Translator.Core.Disassembly;
using Xunit;
namespace Translator.Tests;
public class DifferentialDisassemblyTests
{
[Theory]
[InlineData(0x7C032800u, 0x80000000u, "cmpw")]
[InlineData(0x41820010u, 0x80000000u, "beq")]
[InlineData(0x3C608038u, 0x80000000u, "lis")]
[InlineData(0x6063F780u, 0x80000004u, "ori")]
[InlineData(0x7C0903A6u, 0x80000000u, "mtctr")]
public void DecoderMnemonicMatchesDolphinOracle(uint opcode, uint address, string expectedMnemonic)
{
if (!LlvmDisassemblerOracle.IsAvailable())
{
return;
}
var oracle = LlvmDisassemblerOracle.Disassemble(opcode, address);
var decoded = PpcDecoder.Decode(address, opcode);
Assert.Equal(expectedMnemonic, decoded.Mnemonic);
Assert.Equal(expectedMnemonic, NormalizeMnemonic(oracle));
}
private static string NormalizeMnemonic(string disassembly)
{
foreach (var line in disassembly.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
{
var match = Regex.Match(line.ToLowerInvariant(), @"^\s*[0-9a-f]+:\s+(?:[0-9a-f]{2}\s+){4}([a-z0-9_.]+)");
if (match.Success)
{
return match.Groups[1].Value;
}
}
throw new Xunit.Sdk.XunitException($"Failed to parse oracle disassembly '{disassembly}'.");
}
private static class LlvmDisassemblerOracle
{
public static bool IsAvailable()
{
try
{
if (OperatingSystem.IsWindows())
{
var output = Run("wsl.exe", "bash -lc 'command -v llvm-mc-18 >/dev/null && command -v llvm-objdump-18 >/dev/null && echo ok'");
return output.Contains("ok", StringComparison.OrdinalIgnoreCase);
}
return !string.IsNullOrWhiteSpace(Run("bash", "-lc 'command -v llvm-mc-18 && command -v llvm-objdump-18'"));
}
catch
{
return false;
}
}
public static string Disassemble(uint opcode, uint address)
{
var tempDir = Path.Combine(Path.GetTempPath(), "mkw_llvm_disasm_oracle");
Directory.CreateDirectory(tempDir);
var asmPath = Path.Combine(tempDir, "oracle.s");
var objPath = Path.Combine(tempDir, "oracle.o");
File.WriteAllText(
asmPath,
$".text{Environment.NewLine}.long 0x{opcode.ToString("X8", CultureInfo.InvariantCulture)}{Environment.NewLine}");
if (OperatingSystem.IsWindows())
{
var asmWsl = ToWslPath(asmPath);
var objWsl = ToWslPath(objPath);
RunWsl($"/usr/bin/llvm-mc-18 -triple=powerpc-unknown-unknown -filetype=obj -o '{objWsl}' '{asmWsl}'");
return RunWsl(
$"/usr/bin/llvm-objdump-18 --triple=powerpc-unknown-unknown --adjust-vma=0x{address.ToString("X", CultureInfo.InvariantCulture)} -d '{objWsl}'");
}
Run("llvm-mc-18", $"-triple=powerpc-unknown-unknown -filetype=obj -o \"{objPath}\" \"{asmPath}\"");
return Run(
"llvm-objdump-18",
$"--triple=powerpc-unknown-unknown --adjust-vma=0x{address.ToString("X", CultureInfo.InvariantCulture)} -d \"{objPath}\"");
}
private static string Run(string fileName, string arguments)
{
var psi = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
using var process = Process.Start(psi)!;
process.WaitForExit(10000);
var stdout = process.StandardOutput.ReadToEnd();
var stderr = process.StandardError.ReadToEnd();
Assert.True(process.ExitCode == 0, $"{fileName} {arguments}\nstdout:\n{stdout}\nstderr:\n{stderr}");
return stdout + stderr;
}
private static string RunWsl(string command)
{
var escaped = command.Replace("'", "'\"'\"'");
return Run("wsl.exe", $"bash -lc '{escaped}'");
}
private static string ToWslPath(string windowsPath)
{
var psi = new ProcessStartInfo
{
FileName = "wsl.exe",
Arguments = $"wslpath -a \"{windowsPath}\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
using var process = Process.Start(psi)!;
process.WaitForExit(10000);
var stdout = process.StandardOutput.ReadToEnd().Trim();
var stderr = process.StandardError.ReadToEnd();
Assert.True(process.ExitCode == 0, $"wslpath failed for '{windowsPath}': {stderr}");
Assert.False(string.IsNullOrWhiteSpace(stdout), $"wslpath returned empty output for '{windowsPath}'.");
return stdout;
}
}
}
@@ -0,0 +1,60 @@
using System.IO;
using System.Linq;
using System.Buffers.Binary;
using Translator.Core.Loading;
using Translator.Core.Parsing.Dol;
using Xunit;
namespace Translator.Tests;
public class DolFileTests
{
private static string DolPath => Path.Combine(ProjectPaths.FindRepositoryRoot(), "assets", "main.dol");
[Fact]
public void ParsesExpectedSectionsAndRanges()
{
var dol = DolFile.Load(DolPath);
Assert.Equal(0x800060A4u, dol.EntryPoint);
Assert.Equal(0x80004000u, dol.MemoryRange.Start);
Assert.Equal(0x8038917Cu, dol.MemoryRange.End);
Assert.Equal(11, dol.Sections.Count);
Assert.Equal(2, dol.Sections.Count(s => s.Kind == SectionKind.Text));
Assert.Equal(8, dol.Sections.Count(s => s.Kind == SectionKind.Data));
Assert.Equal(1, dol.Sections.Count(s => s.Kind == SectionKind.Bss));
var init = dol.Sections.Single(s => s.Name == ".init");
Assert.Equal(0x80004000u, init.VirtualAddress);
Assert.Equal(0x2460u, init.Size);
var text = dol.Sections.Single(s => s.Name == ".text");
Assert.Equal(0x800072C0u, text.VirtualAddress);
Assert.Equal(0x23DB20u, text.Size);
var data = dol.Sections.Single(s => s.Name == ".data");
Assert.Equal(0x80258580u, data.VirtualAddress);
Assert.Equal(0x4BAC0u, data.Size);
var bss = dol.Sections.Single(s => s.Kind == SectionKind.Bss);
Assert.Equal(0x802A4080u, bss.VirtualAddress);
Assert.Equal(0xE50FCu, bss.Size);
}
[Fact]
public void MkwOutlineEffectConstantLivesInInitializedSdata2DespiteDolHeaderBssOverlap()
{
var dol = DolFile.Load(DolPath);
var sdata2 = dol.Sections.Single(s => s.Name == ".sdata2");
const uint outlineEffectScaleAddress = 0x80389060;
Assert.True(sdata2.Range.Contains(outlineEffectScaleAddress));
Assert.True(dol.Sections.Single(s => s.Kind == SectionKind.Bss).Range.Contains(outlineEffectScaleAddress));
var offset = checked((int)(outlineEffectScaleAddress - sdata2.VirtualAddress));
var rawValue = BinaryPrimitives.ReadUInt32BigEndian(sdata2.Data.Span.Slice(offset, sizeof(uint)));
Assert.Equal(0x41200000u, rawValue);
}
}
@@ -0,0 +1,156 @@
using System.Collections.Generic;
using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis.Ssa;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
/// <summary>
/// Shape of the emitted text. These assertions are about readability of the
/// generated C++; each one is semantics-preserving by construction.
/// </summary>
public class EmittedOutputShapeTests
{
private static string Emit(IrFunction function, RepresentationEnvironment types, uint entryPoint = 0x80001000u) =>
new CxxLinearCodeGenerator().Emit(
entryPoint,
new SsaTransformer().Convert(function),
new FunctionAbiClassification(function.Name, ValueRepresentation.Void),
types);
private static RepresentationEnvironment UInt32Registers(params string[] names)
{
var map = new Dictionary<string, ValueRepresentation>();
foreach (var name in names) map[name] = ValueRepresentation.UInt32;
return new RepresentationEnvironment(map);
}
[Fact]
public void BranchWithAPureFallthroughFalseEdgeOmitsTheElseBlock()
{
var function = new IrFunction("branch_fallthrough", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(0), false),
new IrBranch("beq", "taken", "next", "cr0")
}),
new IrBasicBlock("next", new IrInstruction[]
{
new IrBinary("r3", IrValue.Register("r3"), IrValue.Imm(1), "add"),
new IrReturn(null)
}),
new IrBasicBlock("taken", new IrInstruction[] { new IrReturn(null) })
});
var code = Emit(function, UInt32Registers("r3"));
Assert.Contains("goto loc_taken;", code, StringComparison.Ordinal);
// The false edge falls through to the next emitted block and needs no
// representation normalization, so there is nothing to put in an else.
Assert.DoesNotContain("} else {", code, StringComparison.Ordinal);
}
[Fact]
public void BranchWithARealFalseEdgeKeepsTheElseBlock()
{
var function = new IrFunction("branch_two_gotos", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(0), false),
new IrBranch("beq", "taken", "other", "cr0")
}),
new IrBasicBlock("filler", new IrInstruction[] { new IrReturn(null) }),
new IrBasicBlock("taken", new IrInstruction[] { new IrReturn(null) }),
new IrBasicBlock("other", new IrInstruction[] { new IrReturn(null) })
});
var code = Emit(function, UInt32Registers("r3"));
Assert.Contains("} else {", code, StringComparison.Ordinal);
Assert.Contains("goto loc_other;", code, StringComparison.Ordinal);
}
[Fact]
public void AddressConstantsAssignedToUnsignedRegistersPrintAsHex()
{
var function = new IrFunction("address_constants", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
// 0x801B4ABC as a signed int immediate.
new IrAssign("lr", IrValue.Imm(unchecked((int)0x801B4ABCu))),
new IrAssign("r4", IrValue.Imm(unchecked((int)0x80000000u))),
// Not an address: a genuinely signed small immediate, and a
// negative value outside the guest address window.
new IrBinary("r3", IrValue.Register("r3"), IrValue.Imm(-16), "add"),
new IrAssign("r5", IrValue.Imm(-1)),
new IrReturn(null)
})
});
var code = Emit(function, UInt32Registers("r3", "r4", "r5"));
Assert.Contains("ctx->lr = 0x801B4ABCu;", code, StringComparison.Ordinal);
Assert.Contains("r4 = 0x80000000u;", code, StringComparison.Ordinal);
Assert.Contains("r3 = (r3 + -16);", code, StringComparison.Ordinal);
Assert.Contains("r5 = -1;", code, StringComparison.Ordinal);
}
[Fact]
public void SubWordStoresCastToTheExactMemoryParameterType()
{
var function = new IrFunction("sub_word_stores", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrStore(new IrAddress("r4", 0), IrValue.Register("r3"), 1),
new IrStore(new IrAddress("r4", 4), IrValue.Register("r3"), 2),
new IrStore(new IrAddress("r4", 8), IrValue.Register("r3"), 4),
new IrReturn(null)
})
});
var code = Emit(function, UInt32Registers("r3", "r4"));
Assert.Contains("MemoryInline::FlatWrite8(r4, static_cast<uint8_t>(r3));", code, StringComparison.Ordinal);
Assert.Contains("MemoryInline::FlatWrite16((r4 + 4), static_cast<uint16_t>(r3));", code, StringComparison.Ordinal);
Assert.Contains("MemoryInline::FlatWrite32((r4 + 8), r3);", code, StringComparison.Ordinal);
Assert.DoesNotContain("static_cast<uint32_t>(r3)", code, StringComparison.Ordinal);
}
[Fact]
public void FloatFlatHelpersPreserveTheWidthMapping()
{
var function = new IrFunction("float_width_helpers", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrStore(new IrAddress("r4", 0), IrValue.Register("f1"), 4),
new IrStore(new IrAddress("r4", 8), IrValue.Register("f2"), 8),
new IrLoad("f3", new IrAddress("r4", 16), 4),
new IrLoad("f4", new IrAddress("r4", 24), 8),
new IrReturn(null)
})
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["r4"] = ValueRepresentation.UInt32,
["f1"] = ValueRepresentation.Float64,
["f2"] = ValueRepresentation.Float64,
["f3"] = ValueRepresentation.Float64,
["f4"] = ValueRepresentation.Float64
});
var code = Emit(function, types);
Assert.Contains("MemoryInline::FlatWriteFloat32(r4, f1.d);", code, StringComparison.Ordinal);
Assert.Contains("MemoryInline::FlatWriteFloat64((r4 + 8), f2.d);", code, StringComparison.Ordinal);
Assert.Contains("f3.d = MemoryInline::FlatReadFloat32((r4 + 16));", code, StringComparison.Ordinal);
Assert.Contains("f4.d = MemoryInline::FlatReadFloat64((r4 + 24));", code, StringComparison.Ordinal);
}
}
@@ -0,0 +1,672 @@
using System;
using System.Collections.Generic;
using Translator.Core.Analysis;
using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis.Ssa;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
/// <summary>
/// Compare+branch fusion, dead CR-field/XER.CA elimination, and LR store elision. Negative cases
/// outnumber positive ones because each would be a silent-corruption bug if it started firing.
/// </summary>
public class FlagElisionCodeGenTests
{
private static string Emit(
IrFunction function,
IReadOnlyDictionary<uint, GuestAbiContract>? guestAbiContracts = null,
IReadOnlySet<uint>? modOverridableCallTargets = null,
IReadOnlySet<uint>? lrContinuationCallTargets = null) =>
new CxxLinearCodeGenerator().Emit(
0x80001000u,
new SsaTransformer().Convert(function),
new FunctionAbiClassification(function.Name, ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
guestAbiContracts: guestAbiContracts,
lrContinuationCallTargets: lrContinuationCallTargets,
modOverridableCallTargets: modOverridableCallTargets);
private static GuestAbiContract Contract(
bool readsLr = false,
bool readsXer = false,
bool writesXer = false,
byte crRead = 0,
byte crWrite = 0,
GuestCallBoundaryFlags flags = GuestCallBoundaryFlags.None) =>
new(
GprReadBeforeWriteMask: 0,
GprPossibleWriteMask: 0,
GprReturnMask: 0,
FprReadBeforeWriteMask: 0,
FprPossibleWriteMask: 0,
FprReturnMask: 0,
CrReadBeforeWriteMask: crRead,
CrPossibleWriteMask: crWrite,
ReadsXerBeforeWrite: readsXer,
MayWriteXer: writesXer,
ReadsCtrBeforeWrite: false,
MayWriteCtr: false,
ReadsLrBeforeWrite: readsLr,
MayWriteLr: false,
BoundaryFlags: flags,
DirectCallTargets: Array.Empty<uint>());
/// <summary>A block that overwrites CR0 and returns, so reaching it on every outgoing edge
/// makes the CR0 producer dead (the function's return boundary always counts as a reader).</summary>
private static IrBasicBlock Cr0KillingExit(string label, string register) =>
new(label, new IrInstruction[]
{
new IrSetCrField(0, IrValue.Register(register), IrValue.Imm(0), false),
new IrReturn(null)
});
private static IrFunction CompareThenBranch(
string name,
string condition,
bool isUnsigned,
IrValue right)
{
return new IrFunction(name, "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrSetCrField(0, IrValue.Register("r3"), right, isUnsigned),
new IrBranch(condition, "taken", "fallthrough", "cr0")
}),
Cr0KillingExit("taken", "r4"),
Cr0KillingExit("fallthrough", "r5")
});
}
[Theory]
[InlineData("beq", false, "(static_cast<int32_t>(r3) == static_cast<int32_t>(0))")]
[InlineData("bne", false, "(static_cast<int32_t>(r3) != static_cast<int32_t>(0))")]
[InlineData("blt", false, "(static_cast<int32_t>(r3) < static_cast<int32_t>(0))")]
[InlineData("bge", false, "(static_cast<int32_t>(r3) >= static_cast<int32_t>(0))")]
[InlineData("bgt", false, "(static_cast<int32_t>(r3) > static_cast<int32_t>(0))")]
[InlineData("ble", false, "(static_cast<int32_t>(r3) <= static_cast<int32_t>(0))")]
[InlineData("blt", true, "(static_cast<uint32_t>(r3) < static_cast<uint32_t>(0))")]
[InlineData("bge", true, "(static_cast<uint32_t>(r3) >= static_cast<uint32_t>(0))")]
public void CompareFeedingItsOnlyBranchBecomesADirectComparison(
string condition, bool isUnsigned, string expected)
{
var code = Emit(CompareThenBranch($"fuse_{condition}", condition, isUnsigned, IrValue.Imm(0)));
Assert.Contains($"if ({expected})", code, StringComparison.Ordinal);
// The two exit blocks still write CR0, so exactly two compares survive.
Assert.Equal(2, CountOccurrences(code, "SetCRResident("));
}
[Fact]
public void FusionUsesTheRegisterOperandOfARegisterToRegisterCompare()
{
var code = Emit(CompareThenBranch("fuse_reg", "bgt", isUnsigned: false, IrValue.Register("r6")));
Assert.Contains(
"if ((static_cast<int32_t>(r3) > static_cast<int32_t>(r6)))",
code, StringComparison.Ordinal);
}
[Fact]
public void FusionReachesTheCompareThroughASingleEntrySinglExitPredecessor()
{
// The basic-block builder frequently starts a new block at the branch,
// which puts the compare in the sole predecessor.
var function = new IrFunction("fuse_across_block", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(7), false)
}),
new IrBasicBlock("branch", new IrInstruction[]
{
new IrBranch("beq", "taken", "fallthrough", "cr0")
}),
Cr0KillingExit("taken", "r4"),
Cr0KillingExit("fallthrough", "r5")
});
var code = Emit(function);
Assert.Contains(
"if ((static_cast<int32_t>(r3) == static_cast<int32_t>(7)))",
code, StringComparison.Ordinal);
Assert.Equal(2, CountOccurrences(code, "SetCRResident("));
}
[Theory]
[InlineData("blt", "if ((f1.d < f2.d))")]
[InlineData("bge", "if ((!(f1.d < f2.d)))")]
[InlineData("beq", "if ((f1.d == f2.d))")]
[InlineData("ble", "if ((!(f1.d > f2.d)))")]
public void FloatCompareFusionKeepsThePowerPcBitSemantics(string condition, string expected)
{
// A cleared LT bit means "not less than", which is true for an unordered
// compare; spelling it as >= would be wrong for NaN.
var function = new IrFunction($"fuse_float_{condition}", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrSetCrField(0, IrValue.Register("f1"), IrValue.Register("f2"), false),
new IrBranch(condition, "taken", "fallthrough", "cr0")
}),
Cr0KillingExit("taken", "r4"),
Cr0KillingExit("fallthrough", "r5")
});
var code = Emit(function);
Assert.Contains(expected, code, StringComparison.Ordinal);
Assert.DoesNotContain("SetCRFloatResident(", code, StringComparison.Ordinal);
}
[Fact]
public void CompareIsNotFusedWhenTheFieldIsStillReadAfterTheBranch()
{
var function = new IrFunction("live_across_branch", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(0), false),
new IrBranch("beq", "taken", "fallthrough", "cr0")
}),
// Reads CR0 again before writing it: the packed field is live.
new IrBasicBlock("taken", new IrInstruction[]
{
new IrBranch("bgt", "fallthrough", "second", "cr0")
}),
Cr0KillingExit("second", "r4"),
Cr0KillingExit("fallthrough", "r5")
});
var code = Emit(function);
Assert.DoesNotContain("static_cast<int32_t>(r3) == static_cast<int32_t>(0)", code, StringComparison.Ordinal);
Assert.Contains("SetCRResident(cr, xer, 0, static_cast<int32_t>(r3)", code, StringComparison.Ordinal);
}
[Fact]
public void CompareIsNotFusedWhenTheBranchTestsTheSummaryOverflowBit()
{
// bso reads the XER.SO copy, which a direct comparison cannot rebuild.
var code = Emit(CompareThenBranch("so_consumer", "bso", isUnsigned: false, IrValue.Imm(0)));
Assert.Contains("SetCRResident(cr, xer, 0, static_cast<int32_t>(r3)", code, StringComparison.Ordinal);
Assert.Equal(3, CountOccurrences(code, "SetCRResident("));
}
[Fact]
public void CompareIsNotFusedWhenAnMfcrObservesThePackedRegister()
{
var function = new IrFunction("mfcr_reader", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(0), false),
new IrBranch("beq", "taken", "fallthrough", "cr0")
}),
new IrBasicBlock("taken", new IrInstruction[]
{
new IrAssign("r7", IrValue.Register("cr")),
new IrReturn(null)
}),
Cr0KillingExit("fallthrough", "r5")
});
var code = Emit(function);
Assert.Contains("SetCRResident(cr, xer, 0, static_cast<int32_t>(r3)", code, StringComparison.Ordinal);
}
[Fact]
public void CompareIsNotFusedWhenAnOperandIsRedefinedBeforeTheBranch()
{
// The fused comparison is evaluated at the branch, where r3 no longer
// holds the compared value.
var function = new IrFunction("clobbered_operand", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(0), false),
new IrBinary("r3", IrValue.Register("r3"), IrValue.Imm(1), "add"),
new IrBranch("beq", "taken", "fallthrough", "cr0")
}),
Cr0KillingExit("taken", "r4"),
Cr0KillingExit("fallthrough", "r5")
});
var code = Emit(function);
Assert.Contains("SetCRResident(cr, xer, 0, static_cast<int32_t>(r3)", code, StringComparison.Ordinal);
Assert.DoesNotContain("if ((static_cast<int32_t>(r3) == static_cast<int32_t>(0)))", code, StringComparison.Ordinal);
}
[Fact]
public void CompareIsNotFusedAcrossACall()
{
var function = new IrFunction("call_between", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(0), false),
new IrCall(string.Empty, "0x80002000", Array.Empty<IrValue>()),
new IrBranch("beq", "taken", "fallthrough", "cr0")
}),
Cr0KillingExit("taken", "r4"),
Cr0KillingExit("fallthrough", "r5")
});
var code = Emit(
function,
guestAbiContracts: new Dictionary<uint, GuestAbiContract> { [0x80002000u] = Contract() });
Assert.Contains("SetCRResident(cr, xer, 0, static_cast<int32_t>(r3)", code, StringComparison.Ordinal);
}
[Fact]
public void CompareIsNotFusedIntoTheFunctionEntryBlock()
{
// "branch" is the entry label, so control reaches it without ever
// executing the block that happens to be its only recorded predecessor.
var function = new IrFunction("entry_is_branch", "branch", new[]
{
new IrBasicBlock("branch", new IrInstruction[]
{
new IrBranch("beq", "taken", "fallthrough", "cr0")
}),
new IrBasicBlock("producer", new IrInstruction[]
{
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(0), false),
new IrJump("branch")
}),
Cr0KillingExit("taken", "r4"),
Cr0KillingExit("fallthrough", "r5")
});
var code = Emit(function);
Assert.DoesNotContain("static_cast<int32_t>(r3) == static_cast<int32_t>(0)", code, StringComparison.Ordinal);
Assert.Contains("SetCRResident(cr, xer, 0, static_cast<int32_t>(r3)", code, StringComparison.Ordinal);
}
[Fact]
public void NoFlagIsElidedInABodyThatCanResumeThroughALinkRegisterDispatch()
{
// A callee that returns into one of this function's own labels makes the
// static CFG incomplete, so every flag statement derived from it is void.
var function = new IrFunction("lr_continuation", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrAssign("lr", IrValue.Imm(0x80001010)),
new IrCall(string.Empty, "0x80002000", Array.Empty<IrValue>()),
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(0), false),
new IrBranch("beq", "taken", "fallthrough", "cr0")
}),
Cr0KillingExit("taken", "r4"),
Cr0KillingExit("fallthrough", "r5")
});
var code = Emit(
function,
guestAbiContracts: new Dictionary<uint, GuestAbiContract> { [0x80002000u] = Contract() },
lrContinuationCallTargets: new HashSet<uint> { 0x80002000u });
Assert.Equal(3, CountOccurrences(code, "SetCRResident("));
Assert.Contains("ctx->lr = 2147487760;", code, StringComparison.Ordinal);
}
[Fact]
public void ACalleeThatDoesNotReadTheFieldDoesNotKeepItAlive()
{
// This is what makes the transform pay off in a real build: without an
// ABI contract every call after the branch is a potential CR reader.
var function = new IrFunction("call_after_branch", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(0), false),
new IrBranch("beq", "taken", "fallthrough", "cr0")
}),
new IrBasicBlock("taken", new IrInstruction[]
{
new IrCall(string.Empty, "0x80002000", Array.Empty<IrValue>()),
new IrSetCrField(0, IrValue.Register("r4"), IrValue.Imm(0), false),
new IrReturn(null)
}),
Cr0KillingExit("fallthrough", "r5")
});
var contracts = new Dictionary<uint, GuestAbiContract> { [0x80002000u] = Contract() };
Assert.Contains(
"if ((static_cast<int32_t>(r3) == static_cast<int32_t>(0)))",
Emit(function, guestAbiContracts: contracts), StringComparison.Ordinal);
// The same callee declared as a CR0 reader keeps the compare.
var readerContracts = new Dictionary<uint, GuestAbiContract>
{
[0x80002000u] = Contract(crRead: 0x01)
};
Assert.Contains(
"SetCRResident(cr, xer, 0, static_cast<int32_t>(r3)",
Emit(function, guestAbiContracts: readerContracts), StringComparison.Ordinal);
// ...and so does an unknown callee.
Assert.Contains(
"SetCRResident(cr, xer, 0, static_cast<int32_t>(r3)",
Emit(function), StringComparison.Ordinal);
}
[Fact]
public void RecordFormWriteOverwrittenBeforeAnyReadIsRemoved()
{
// add. followed by cmpwi into the same field with no reader in between:
// only the compare can be observed.
var function = new IrFunction("dead_record_form", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r4"), "add"),
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(0), false),
new IrBinary("r5", IrValue.Register("r5"), IrValue.Register("r6"), "add"),
new IrSetCrField(0, IrValue.Register("r5"), IrValue.Imm(0), false),
new IrReturn(null)
})
});
var code = Emit(function);
Assert.DoesNotContain("SetCRResident(cr, xer, 0, static_cast<int32_t>(r3)", code, StringComparison.Ordinal);
Assert.Contains("SetCRResident(cr, xer, 0, static_cast<int32_t>(r5)", code, StringComparison.Ordinal);
Assert.Equal(1, CountOccurrences(code, "SetCRResident("));
}
[Fact]
public void LastCrWriteBeforeReturnIsAlwaysKept()
{
// The return boundary publishes ctx->cr and the emitter has no caller
// information there.
var function = new IrFunction("cr_live_out", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrSetCrField(3, IrValue.Register("r3"), IrValue.Imm(0), false),
new IrReturn(null)
})
});
var code = Emit(function);
Assert.Contains("SetCRResident(cr, xer, 3,", code, StringComparison.Ordinal);
}
[Fact]
public void RedundantCrWriteIsEliminated()
{
var function = new IrFunction("dead_cr_off", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(0), false),
new IrSetCrField(0, IrValue.Register("r5"), IrValue.Imm(0), false),
new IrReturn(null)
})
});
Assert.Equal(1, CountOccurrences(Emit(function), "SetCRResident("));
}
[Fact]
public void XerBecomesAResidentLocal()
{
var function = new IrFunction("resident_xer", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(0), false),
new IrReturn(null)
})
});
var code = Emit(function);
Assert.Contains("uint32_t xer = ctx->xer;", code, StringComparison.Ordinal);
Assert.Contains("SetCRResident(cr, xer, 0,", code, StringComparison.Ordinal);
Assert.DoesNotContain("SetCRResident(cr, ctx->xer", code, StringComparison.Ordinal);
}
[Fact]
public void CarryUpdatesAndReadsUseTheResidentXerLocal()
{
var function = new IrFunction("resident_carry", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall("xer", "PPC_UpdateCarryAdd", new[]
{
IrValue.Register("r3"), IrValue.Register("r4"), IrValue.Imm(0)
}),
new IrCall("r7", "PPC_GetCarry", Array.Empty<IrValue>()),
new IrReturn(null)
})
});
var code = Emit(function);
Assert.Contains("xer = (xer & 0xDFFFFFFFu)", code, StringComparison.Ordinal);
Assert.Contains("r7 = (xer >> 29) & 1u;", code, StringComparison.Ordinal);
Assert.Contains("ctx->xer = xer;", code, StringComparison.Ordinal);
Assert.DoesNotContain("ctx->xer >> 29", code, StringComparison.Ordinal);
}
[Fact]
public void CarryUpdateOverwrittenBeforeAnyReadIsRemoved()
{
var function = new IrFunction("dead_carry", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall("xer", "PPC_UpdateCarryAdd", new[]
{
IrValue.Register("r3"), IrValue.Register("r4"), IrValue.Imm(0)
}),
new IrCall("xer", "PPC_UpdateCarrySub", new[]
{
IrValue.Register("r5"), IrValue.Register("r6")
}),
new IrReturn(null)
})
});
var code = Emit(function);
Assert.Equal(0, CountOccurrences(code, "const uint64_t ppcCarryWide"));
Assert.Contains(
"xer = (xer & 0xDFFFFFFFu) | ((static_cast<uint32_t>(r5) >= static_cast<uint32_t>(r6) ? 1u : 0u) << 29);",
code, StringComparison.Ordinal);
}
[Fact]
public void CarryUpdateIsKeptWhenAnAddeChainReadsIt()
{
var function = new IrFunction("live_carry", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall("xer", "PPC_UpdateCarryAdd", new[]
{
IrValue.Register("r3"), IrValue.Register("r4"), IrValue.Imm(0)
}),
new IrCall("r8", "PPC_GetCarry", Array.Empty<IrValue>()),
new IrCall("xer", "PPC_UpdateCarrySub", new[]
{
IrValue.Register("r5"), IrValue.Register("r6")
}),
new IrReturn(null)
})
});
var code = Emit(function);
Assert.Equal(1, CountOccurrences(code, "const uint64_t ppcCarryWide"));
}
[Fact]
public void CarryUpdateIsKeptWhenTheOnlyReaderIsTheReturnBoundary()
{
var function = new IrFunction("carry_live_out", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall("xer", "PPC_UpdateCarryAdd", new[]
{
IrValue.Register("r3"), IrValue.Register("r4"), IrValue.Imm(0)
}),
new IrReturn(null)
})
});
Assert.Equal(1, CountOccurrences(Emit(function), "const uint64_t ppcCarryWide"));
}
[Fact]
public void CarryUpdateIsKeptAcrossACalleeThatReadsXer()
{
var function = new IrFunction("carry_across_call", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall("xer", "PPC_UpdateCarryAdd", new[]
{
IrValue.Register("r3"), IrValue.Register("r4"), IrValue.Imm(0)
}),
new IrCall(string.Empty, "0x80002000", Array.Empty<IrValue>()),
new IrCall("xer", "PPC_UpdateCarrySub", new[]
{
IrValue.Register("r5"), IrValue.Register("r6")
}),
new IrReturn(null)
})
});
var code = Emit(
function,
guestAbiContracts: new Dictionary<uint, GuestAbiContract>
{
[0x80002000u] = Contract(readsXer: true)
});
Assert.Equal(1, CountOccurrences(code, "const uint64_t ppcCarryWide"));
}
private static IrFunction CallWithLinkRegister(string name, uint target) =>
new(name, "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrAssign("lr", IrValue.Imm(0x80001010)),
new IrCall(string.Empty, $"0x{target:X8}", Array.Empty<IrValue>()),
new IrReturn(null)
})
});
[Fact]
public void LinkRegisterStoreIsElidedForACalleeThatNeverReadsLr()
{
var code = Emit(
CallWithLinkRegister("leaf_caller", 0x80002000u),
guestAbiContracts: new Dictionary<uint, GuestAbiContract> { [0x80002000u] = Contract() });
Assert.Contains("InvokeDirectCpu<0x80002000u>(ctx);", code, StringComparison.Ordinal);
Assert.DoesNotContain("ctx->lr = ", code, StringComparison.Ordinal);
}
[Fact]
public void LinkRegisterStoreIsKeptForAnMflrCallee()
{
var code = Emit(
CallWithLinkRegister("nonleaf_caller", 0x80002000u),
guestAbiContracts: new Dictionary<uint, GuestAbiContract>
{
[0x80002000u] = Contract(readsLr: true)
});
Assert.Contains("ctx->lr = 2147487760;", code, StringComparison.Ordinal);
}
[Fact]
public void LinkRegisterStoreIsKeptWithoutACalleeContract()
{
var code = Emit(CallWithLinkRegister("unknown_callee", 0x80002000u));
Assert.Contains("ctx->lr = 2147487760;", code, StringComparison.Ordinal);
}
[Theory]
[InlineData(GuestCallBoundaryFlags.RequiresCompleteContext)]
[InlineData(GuestCallBoundaryFlags.CanSuspend)]
[InlineData(GuestCallBoundaryFlags.CanSwitchThreads)]
[InlineData(GuestCallBoundaryFlags.InvokesGuestCode)]
public void LinkRegisterStoreIsKeptForACalleeThatEscapesAnalysis(GuestCallBoundaryFlags flags)
{
// setjmp-style natives (OSSaveContext) and anything that re-enters guest
// code can observe the architectural return address.
var code = Emit(
CallWithLinkRegister("escaping_callee", 0x80002000u),
guestAbiContracts: new Dictionary<uint, GuestAbiContract>
{
[0x80002000u] = Contract(flags: flags)
});
Assert.Contains("ctx->lr = 2147487760;", code, StringComparison.Ordinal);
}
[Fact]
public void LinkRegisterStoreIsKeptForAModOverridableCallee()
{
var code = Emit(
CallWithLinkRegister("mod_callee", 0x80002000u),
guestAbiContracts: new Dictionary<uint, GuestAbiContract> { [0x80002000u] = Contract() },
modOverridableCallTargets: new HashSet<uint> { 0x80002000u });
Assert.Contains("ctx->lr = 2147487760;", code, StringComparison.Ordinal);
}
[Fact]
public void DefinitionsCarryRestrictWhileDeclarationsDoNot()
{
var function = new IrFunction("restrict_probe", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall(string.Empty, "func_helper_target", Array.Empty<IrValue>()),
new IrReturn(null)
})
});
var code = Emit(function);
Assert.Contains("extern \"C\" void restrict_probe(CpuContext* MKW_RESTRICT ctx)", code, StringComparison.Ordinal);
Assert.Contains("extern \"C\" void func_80001000(CpuContext* MKW_RESTRICT ctx)", code, StringComparison.Ordinal);
// Top-level restrict is not part of the function type, so forward
// declarations stay plain and still match.
Assert.Contains("extern \"C\" void func_helper_target(CpuContext* ctx);", code, StringComparison.Ordinal);
}
private static int CountOccurrences(string text, string value)
{
var count = 0;
var index = text.IndexOf(value, StringComparison.Ordinal);
while (index >= 0)
{
++count;
index = text.IndexOf(value, index + value.Length, StringComparison.Ordinal);
}
return count;
}
}
@@ -0,0 +1,264 @@
using System;
using System.Linq;
using Translator.Core.Disassembly;
using Translator.Core.Ir;
using Translator.Core.Lifting;
using Xunit;
namespace Translator.Tests;
public class FloatingLifterTests
{
private static PpcRegisterOperand Fpr(int index) => new($"f{index}", index);
private static PpcInstruction FloatingInstruction(uint raw, string mnemonic, params PpcOperand[] operands)
=> PpcInstruction.Synthetic(0x80000000, raw, mnemonic, operands);
[Fact]
public void LiftsFloatingStatusRegisterHelpersFromRawBits()
{
var lifter = new PpcLifter();
var mffs = FloatingInstruction((63u << 26) | (5u << 21), "mffs");
var mffsIr = Assert.Single(lifter.Lift(new[] { mffs })).Ir;
var mffsCall = Assert.IsType<IrCall>(Assert.Single(mffsIr));
Assert.Equal("PPC_Mffs", mffsCall.Target);
Assert.Equal("f5", mffsCall.Destination);
Assert.Empty(mffsCall.Arguments);
var mtfsf = FloatingInstruction((63u << 26) | (0xAAu << 17) | (7u << 11), "mtfsf");
var mtfsfIr = Assert.Single(lifter.Lift(new[] { mtfsf })).Ir;
var mtfsfCall = Assert.IsType<IrCall>(Assert.Single(mtfsfIr));
Assert.Equal("PPC_Mtfsf", mtfsfCall.Target);
Assert.Equal(0xAA, mtfsfCall.Arguments[0].Constant);
Assert.Equal("f7", mtfsfCall.Arguments[1].RegisterName);
var mtfsb1 = FloatingInstruction((63u << 26) | (9u << 21), "mtfsb1");
var mtfsb1Ir = Assert.Single(lifter.Lift(new[] { mtfsb1 })).Ir;
var mtfsb1Call = Assert.IsType<IrCall>(Assert.Single(mtfsb1Ir));
Assert.Equal("PPC_Mtfsb1", mtfsb1Call.Target);
Assert.Equal(9, Assert.Single(mtfsb1Call.Arguments).Constant);
var mtfsb0 = FloatingInstruction((63u << 26) | (10u << 21), "mtfsb0");
var mtfsb0Ir = Assert.Single(lifter.Lift(new[] { mtfsb0 })).Ir;
var mtfsb0Call = Assert.IsType<IrCall>(Assert.Single(mtfsb0Ir));
Assert.Equal("PPC_Mtfsb0", mtfsb0Call.Target);
Assert.Equal(10, Assert.Single(mtfsb0Call.Arguments).Constant);
var mtfsfi = FloatingInstruction((63u << 26) | (3u << 23) | (0xCu << 12), "mtfsfi");
var mtfsfiIr = Assert.Single(lifter.Lift(new[] { mtfsfi })).Ir;
var mtfsfiCall = Assert.IsType<IrCall>(Assert.Single(mtfsfiIr));
Assert.Equal("PPC_Mtfsfi", mtfsfiCall.Target);
Assert.Equal(new long?[] { 3, 0xC }, mtfsfiCall.Arguments.Select(a => a.Constant).ToArray());
}
[Theory]
[InlineData("fadd", "add")]
[InlineData("fsub", "sub")]
[InlineData("fmul", "mul")]
[InlineData("fdiv", "fdiv")]
public void LiftsFloatingBinaryMnemonics(string mnemonic, string op)
{
var instruction = FloatingInstruction(0, mnemonic, Fpr(1), Fpr(2), Fpr(3));
var ir = Assert.Single(new PpcLifter().Lift(new[] { instruction })).Ir;
var binary = Assert.IsType<IrBinary>(Assert.Single(ir));
Assert.Equal("f1", binary.Destination);
Assert.Equal(op, binary.Op);
Assert.Equal("f2", binary.Left.RegisterName);
Assert.Equal("f3", binary.Right.RegisterName);
}
[Theory]
[InlineData("fadd.", "add")]
[InlineData("fsub.", "sub")]
[InlineData("fmul.", "mul")]
[InlineData("fdiv.", "fdiv")]
public void LiftsFloatingBinaryDotMnemonics(string mnemonic, string op)
{
var instruction = FloatingInstruction(0, mnemonic, Fpr(1), Fpr(2), Fpr(3));
var ir = Assert.Single(new PpcLifter().Lift(new[] { instruction })).Ir;
var binary = Assert.IsType<IrBinary>(Assert.Single(ir));
Assert.Equal("f1", binary.Destination);
Assert.Equal(op, binary.Op);
}
[Theory]
[InlineData("frsp", "frsp")]
[InlineData("fneg", "fneg")]
[InlineData("fabs", "fabs")]
[InlineData("fctiw", "fctiw")]
[InlineData("fctiwz", "fctiwz")]
public void LiftsFloatingUnaryMnemonics(string mnemonic, string op)
{
var instruction = FloatingInstruction(0, mnemonic, Fpr(4), Fpr(7));
var ir = Assert.Single(new PpcLifter().Lift(new[] { instruction })).Ir;
var binary = Assert.IsType<IrBinary>(Assert.Single(ir));
Assert.Equal("f4", binary.Destination);
Assert.Equal(op, binary.Op);
Assert.Equal("f7", binary.Left.RegisterName);
Assert.Equal(0, binary.Right.Constant);
}
[Theory]
[InlineData("frsp.", "frsp")]
[InlineData("fneg.", "fneg")]
[InlineData("fabs.", "fabs")]
[InlineData("fctiw.", "fctiw")]
[InlineData("fctiwz.", "fctiwz")]
public void LiftsFloatingUnaryDotMnemonics(string mnemonic, string op)
{
var instruction = FloatingInstruction(0, mnemonic, Fpr(4), Fpr(7));
var ir = Assert.Single(new PpcLifter().Lift(new[] { instruction })).Ir;
var binary = Assert.IsType<IrBinary>(Assert.Single(ir));
Assert.Equal("f4", binary.Destination);
Assert.Equal(op, binary.Op);
}
[Fact]
public void LiftsFmrAsRegisterAssign()
{
var instruction = FloatingInstruction(0, "fmr", Fpr(6), Fpr(2));
var ir = Assert.Single(new PpcLifter().Lift(new[] { instruction })).Ir;
var assign = Assert.IsType<IrAssign>(Assert.Single(ir));
Assert.Equal("f6", assign.Destination);
Assert.Equal("f2", assign.Value.RegisterName);
}
[Fact]
public void LiftsFnabsAsNegativeAbsoluteValue()
{
var instruction = FloatingInstruction(0, "fnabs", Fpr(6), Fpr(2));
var ir = Assert.Single(new PpcLifter().Lift(new[] { instruction })).Ir;
Assert.Equal("fabs", Assert.IsType<IrBinary>(ir[0]).Op);
Assert.Equal("fneg", Assert.IsType<IrBinary>(ir[1]).Op);
}
[Fact]
public void LiftsFloatingRawAliasesAndFallbacks()
{
var lifter = new PpcLifter();
var fp57 = FloatingInstruction((63u << 26) | (3u << 21) | (1u << 16) | (4u << 6), "fp_57");
var fp57Ir = Assert.Single(lifter.Lift(new[] { fp57 })).Ir;
var fp57Binary = Assert.IsType<IrBinary>(Assert.Single(fp57Ir));
Assert.Equal("f3", fp57Binary.Destination);
Assert.Equal("mul", fp57Binary.Op);
Assert.Equal("f1", fp57Binary.Left.RegisterName);
Assert.Equal("f4", fp57Binary.Right.RegisterName);
var fp12 = FloatingInstruction((63u << 26) | (6u << 21) | (9u << 11), "fp_12");
var fp12Ir = Assert.Single(lifter.Lift(new[] { fp12 })).Ir;
var fp12Binary = Assert.IsType<IrBinary>(Assert.Single(fp12Ir));
Assert.Equal("f6", fp12Binary.Destination);
Assert.Equal("frsp", fp12Binary.Op);
Assert.Equal("f9", fp12Binary.Left.RegisterName);
var fp281 = FloatingInstruction((63u << 26) | (8u << 21) | (2u << 16) | (5u << 6), "fp_281");
var fp281Ir = Assert.Single(lifter.Lift(new[] { fp281 })).Ir;
var fp281Binary = Assert.IsType<IrBinary>(Assert.Single(fp281Ir));
Assert.Equal("f8", fp281Binary.Destination);
Assert.Equal("mul", fp281Binary.Op);
Assert.Equal("f2", fp281Binary.Left.RegisterName);
Assert.Equal("f5", fp281Binary.Right.RegisterName);
}
[Fact]
public void LiftsFloatingCompareAndRawOpc59Forms()
{
var lifter = new PpcLifter();
var compare = FloatingInstruction(
0,
"fcmpu",
new PpcOperand[]
{
new PpcConditionRegisterOperand("cr6", 24),
Fpr(2),
Fpr(3)
});
var compareIr = Assert.Single(lifter.Lift(new[] { compare })).Ir;
var compareSet = Assert.IsType<IrSetCrField>(Assert.Single(compareIr));
Assert.Equal(6, compareSet.FieldIndex);
Assert.Equal("f2", compareSet.Left.RegisterName);
Assert.Equal("f3", compareSet.Right.RegisterName);
var opc59 = FloatingInstruction((59u << 26) | (4u << 21) | (1u << 16) | (2u << 11) | (3u << 6) | (23u << 1), "opc_59");
var opc59Ir = Assert.Single(lifter.Lift(new[] { opc59 })).Ir;
var opc59Call = Assert.IsType<IrCall>(Assert.Single(opc59Ir));
Assert.Equal("PPC_Fsel", opc59Call.Target);
Assert.Equal("f4", opc59Call.Destination);
Assert.Equal(new[] { "f1", "f2", "f3" }, opc59Call.Arguments.Select(a => a.RegisterName));
var fallback = FloatingInstruction((63u << 26) | (4u << 21) | (1u << 16) | (2u << 11) | (3u << 6) | (28u << 1), "fp_999");
var fallbackIr = Assert.Single(lifter.Lift(new[] { fallback })).Ir;
var fallbackCall = Assert.IsType<IrCall>(Assert.Single(fallbackIr));
Assert.Equal("PPC_Fmsub", fallbackCall.Target);
Assert.Equal("f4", fallbackCall.Destination);
Assert.Equal(new[] { "f1", "f3", "f2" }, fallbackCall.Arguments.Select(a => a.RegisterName));
}
[Theory]
[InlineData(22u, "PPC_Fsqrt", "f2")]
[InlineData(26u, "PPC_Frsqrte", "f2")]
[InlineData(29u, "PPC_Fmadds", "f1")]
[InlineData(30u, "PPC_Fnmsubs", "f1")]
[InlineData(31u, "PPC_Fnmadds", "f1")]
public void LiftsAdditionalOpc59Branches(uint xo, string target, string expectedFirstArgument)
{
var instruction = FloatingInstruction(
(59u << 26) | (4u << 21) | (1u << 16) | (2u << 11) | (3u << 6) | (xo << 1),
"opc_59");
var ir = Assert.Single(new PpcLifter().Lift(new[] { instruction })).Ir;
var call = Assert.IsType<IrCall>(Assert.Single(ir));
Assert.Equal(target, call.Target);
Assert.Equal("f4", call.Destination);
Assert.Equal(expectedFirstArgument, call.Arguments[0].RegisterName);
}
[Fact]
public void RejectsUnknownOpc59Subop()
{
var instruction = FloatingInstruction((59u << 26) | (3u << 21) | (27u << 1), "opc_59");
Assert.Throws<NotImplementedException>(() => new PpcLifter().Lift(new[] { instruction }));
}
[Theory]
[InlineData(18u, "binary", "fdiv")]
[InlineData(20u, "binary", "sub")]
[InlineData(21u, "binary", "add")]
[InlineData(22u, "call", "PPC_Fsqrt")]
[InlineData(23u, "call", "PPC_Fsel")]
[InlineData(24u, "call", "PPC_Fres")]
[InlineData(26u, "call", "PPC_Frsqrte")]
[InlineData(29u, "call", "PPC_Fmadd")]
[InlineData(30u, "call", "PPC_Fnmsub")]
[InlineData(31u, "call", "PPC_Fnmadd")]
[InlineData(12u, "binary", "frsp")]
[InlineData(8u, "binary", "fcmp")]
public void LiftsAdditionalFpFallbackBranches(uint xo, string kind, string opOrTarget)
{
var instruction = FloatingInstruction(
(63u << 26) | (6u << 21) | (1u << 16) | (2u << 11) | (3u << 6) | (xo << 1),
"fp_4242");
var ir = Assert.Single(new PpcLifter().Lift(new[] { instruction })).Ir;
if (kind == "call")
{
var call = Assert.IsType<IrCall>(Assert.Single(ir));
Assert.Equal(opOrTarget, call.Target);
}
else
{
var binary = Assert.IsType<IrBinary>(Assert.Single(ir));
Assert.Equal(opOrTarget, binary.Op);
}
}
}
@@ -0,0 +1,94 @@
using System.IO;
using Translator.Core.Analysis;
using Translator.Core.Loading;
using Xunit;
namespace Translator.Tests;
public sealed class FunctionMapTests
{
private static FunctionMap Parse(params string[] lines) => FunctionMap.Parse(lines, "test-map");
[Fact]
public void ParsesNamedAndUnnamedEntriesInAscendingOrder()
{
var map = Parse(
"80006210 __init_registers",
"800018a8 0x800018a8",
"",
"# comment",
"800060a4 __start",
"800018a8 0x800018a8");
Assert.Equal(new uint[] { 0x800018A8u, 0x800060A4u, 0x80006210u }, map.Addresses);
Assert.Equal(3, map.Addresses.Count);
Assert.Equal(2, map.NamedCount);
Assert.True(map.Contains(0x800060A4u));
Assert.False(map.Contains(0x800060A8u));
}
[Fact]
public void LooksUpBothWaysAndTreatsRepeatedAddressPlaceholdersAsUnnamed()
{
var map = Parse(
"800018a8 0x800018a8",
"800060a4 __start");
Assert.Equal("__start", map.NameOf(0x800060A4u));
Assert.Null(map.NameOf(0x800018A8u));
Assert.Null(map.NameOf(0x80009999u));
Assert.True(map.TryGetAddress("__start", out var start));
Assert.Equal(0x800060A4u, start);
Assert.False(map.TryGetAddress("0x800018a8", out _));
}
[Fact]
public void RejectsNonHexAndEmptyMaps()
{
Assert.Throws<InvalidDataException>(() => Parse("not_an_address foo"));
Assert.Throws<InvalidDataException>(() => Parse("# only a comment"));
}
[Fact]
public void RecoversSaveRestoreThunkRangesFromNamedFamilies()
{
var map = Parse(
"800214f8 _save_fpr_23",
"8002150c _save_fpr_28",
"80021544 _rest_fpr_23",
"80021558 _rest_fpr_28",
"8002156c _save_gpr_14",
"80021570 _save_gpr_15",
"800215a0 _save_gpr_27",
"800215b8 _rest_gpr_14",
"800215ec _rest_gpr_27");
var thunks = GuestSaveRestoreThunks.FromFunctionMap(map);
Assert.Equal(new GuestSaveRestoreThunkRange(0x8002156Cu, 14, 27), thunks.SaveGpr);
Assert.Equal(new GuestSaveRestoreThunkRange(0x800215B8u, 14, 27), thunks.RestGpr);
Assert.Equal(new GuestSaveRestoreThunkRange(0x800214F8u, 23, 28), thunks.SaveFpr);
Assert.Equal(new GuestSaveRestoreThunkRange(0x80021544u, 23, 28), thunks.RestFpr);
Assert.False(thunks.IsEmpty);
}
[Fact]
public void ThunkRecoveryRejectsRunsThatAreNotFourBytesPerRegister()
{
var map = Parse(
"8002156c _save_gpr_14",
"800215a4 _save_gpr_27"); // one slot too far
Assert.Throws<InvalidDataException>(() => GuestSaveRestoreThunks.FromFunctionMap(map));
}
[Fact]
public void MapWithoutThunkSymbolsYieldsNoRanges()
{
var thunks = GuestSaveRestoreThunks.FromFunctionMap(Parse("800060a4 __start"));
Assert.True(thunks.IsEmpty);
Assert.Null(thunks.SaveGpr);
}
}
@@ -0,0 +1,478 @@
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Reflection;
using Translator.Core.Analysis.Ssa;
using Translator.Core.Analysis.Representation;
using Translator.Core.Ir;
using Translator.Core.Loading;
using Translator.Core.Translation;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
public class FunctionTranslatorCoverageTests
{
private static readonly Type TranslatorType = typeof(FunctionTranslator);
private static T InvokePrivate<T>(string name, params object?[] args)
{
var method = TranslatorType.GetMethod(name, BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(method);
return (T)method!.Invoke(null, args)!;
}
[Fact]
public void DiscoverStopsAfterSsaWithoutTypingOrCodeGeneration()
{
var memory = new byte[4];
WriteWord(memory, MemoryLayout.RamBase, 0x4E800020);
var image = new ProgramImage(
memory,
AddressRange.FromStartAndSize(MemoryLayout.RamBase, 4),
AddressRange.FromStartAndSize(MemoryLayout.RamBase, 4),
default,
"discovery-only");
var discovery = new FunctionTranslator(image).Discover(
MemoryLayout.RamBase,
TranslationOptions.Default with { AllowUnsupportedInstructions = true });
Assert.Single(discovery.Instructions);
Assert.NotEmpty(discovery.Ssa.Function.Blocks);
Assert.Equal(TimeSpan.Zero, discovery.Metrics.RepresentationClassification);
Assert.Equal(TimeSpan.Zero, discovery.Metrics.CodeGen);
}
[Fact]
public void Translate_PreservesFallthroughAfterDirectLinkBranch()
{
var memory = new byte[MemoryLayout.RamSize];
WriteWord(memory, 0x80001000, 0x48001001); // bl 0x80002000
WriteWord(memory, 0x80001004, 0x80630048); // lwz r3, 0x48(r3)
WriteWord(memory, 0x80001008, 0x38800000); // li r4, 0
WriteWord(memory, 0x8000100C, 0x80630000); // lwz r3, 0(r3)
WriteWord(memory, 0x80001010, 0x48000FF0); // b 0x80002000
WriteWord(memory, 0x80002000, 0x4E800020); // blr
var image = new ProgramImage(
memory,
AddressRange.FromStartAndSize(0x80001000, 0x1004),
AddressRange.FromStartAndSize(0x80001000, 0x1004),
default,
"test");
// Leaf inlining would otherwise splice the trivial (blr-only) 0x80002000
// callee straight into the caller, eliding the direct-call boundary this
// test exists to check the fallthrough ordering around.
var result = new FunctionTranslator(image).Translate(
0x80001000,
TranslationOptions.Default with
{
MaxBytes = 0x20,
AllowUnsupportedInstructions = true,
EnableLeafInlining = false
});
var callIndex = result.CxxCode.IndexOf("InvokeDirectCpu<0x80002000u>(ctx);", StringComparison.Ordinal);
var fallthroughIndex = result.CxxCode.IndexOf("MemoryInline::FlatRead32((r3 + 72))", StringComparison.Ordinal);
Assert.True(callIndex >= 0, result.CxxCode);
Assert.True(fallthroughIndex > callIndex);
// 0x80001004 as an unsigned address rather than the signed decimal the
// int-typed IR immediate used to print.
Assert.Contains("ctx->lr = 0x80001004u;", result.CxxCode, StringComparison.Ordinal);
}
[Fact]
public void Translate_KnownSiblingEntryBranchEmitsTailCallInsteadOfInliningBody()
{
var memory = new byte[MemoryLayout.RamSize];
WriteWord(memory, 0x80001000, 0x48000020); // b 0x80001020
WriteWord(memory, 0x80001020, 0x38600007); // li r3, 7
WriteWord(memory, 0x80001024, 0x4E800020); // blr
var image = new ProgramImage(
memory,
AddressRange.FromStartAndSize(0x80001000, 0x28),
AddressRange.FromStartAndSize(0x80001000, 0x28),
default,
"test");
// Leaf inlining would otherwise treat this unconditional branch to a
// known sibling entry as an inlinable leaf; isolate the tail-call
// boundary this test exists to check.
var result = new FunctionTranslator(image).Translate(
0x80001000,
TranslationOptions.Default with
{
MaxBytes = 0x40,
AllowUnsupportedInstructions = true,
KnownFunctionEntryPoints = new HashSet<uint> { 0x80001000u, 0x80001020u },
EnableLeafInlining = false
});
Assert.Single(result.Instructions);
Assert.Contains("InvokeDirectCpu<0x80001020u>(ctx);", result.CxxCode, StringComparison.Ordinal);
Assert.DoesNotContain("loc_80001020", result.CxxCode, StringComparison.Ordinal);
}
[Fact]
public void Translate_RecognizesFramePointerBasedMultipleRegisterSaveAreaAsStackMemory()
{
const uint entry = 0x80001000;
var image = TranslatorCppTestHarness.CreateImage(
(entry + 0x00, 0x9421FFC0), // stwu r1, -64(r1)
(entry + 0x04, 0x7C0802A6), // mflr r0
(entry + 0x08, 0x90010044), // stw r0, 68(r1)
(entry + 0x0C, 0x39610040), // addi r11, r1, 64
(entry + 0x10, 0xBF2BFFE4), // stmw r25, -28(r11)
(entry + 0x14, 0xBB2BFFE4), // lmw r25, -28(r11)
(entry + 0x18, 0x80010044), // lwz r0, 68(r1)
(entry + 0x1C, 0x7C0803A6), // mtlr r0
(entry + 0x20, 0x38210040), // addi r1, r1, 64
(entry + 0x24, 0x4E800020)); // blr
var result = new FunctionTranslator(image).Translate(
entry,
TranslationOptions.Default with { MaxBytes = 0x28 });
// Stores still distinguish the stack fast path (FlatWriteRam skips the MMIO policy check)
// from the general guarded write (FlatWrite); reads have no such distinction, both spell FlatRead.
Assert.Contains("MemoryInline::FlatWriteRam32((r11 + -28)", result.CxxCode,
StringComparison.Ordinal);
Assert.Contains("MemoryInline::FlatRead32((r11 + -28)", result.CxxCode,
StringComparison.Ordinal);
Assert.DoesNotContain("MemoryInline::FlatWrite32((r11 + -28)", result.CxxCode,
StringComparison.Ordinal);
}
[Fact]
public void Translate_UsesStackFastPathForLocallyProvenEabiSaveRestoreThunks()
{
const uint entry = 0x80001000;
var image = TranslatorCppTestHarness.CreateImage(
(entry + 0x00, 0x9421FFC0), // stwu r1, -64(r1)
(entry + 0x04, 0x39610040), // addi r11, r1, 64
(entry + 0x08, EncodeB(entry + 0x08, 0x80021598, link: true)), // _savegpr_25
(entry + 0x0C, 0x39610040), // addi r11, r1, 64
(entry + 0x10, EncodeB(entry + 0x10, 0x800215E4, link: true)), // _restgpr_25
(entry + 0x14, 0x38210040), // addi r1, r1, 64
(entry + 0x18, 0x4E800020)); // blr
var result = new FunctionTranslator(image).Translate(
entry,
TranslationOptions.Default with { MaxBytes = 0x1C });
Assert.Contains("MemoryInline::ResolveRangeHost((r11 + -28), 0, 28u, false, true)", result.CxxCode,
StringComparison.Ordinal);
Assert.Contains("MemoryInline::ResolveRangeHost((r11 + -28), 0, 28u, true, false)", result.CxxCode,
StringComparison.Ordinal);
Assert.DoesNotContain("MemoryInline::FlatWrite32((r11 + -28)", result.CxxCode,
StringComparison.Ordinal);
Assert.DoesNotContain("MemoryInline::FlatRead32((r11 + -28)", result.CxxCode,
StringComparison.Ordinal);
}
[Fact]
public void Translate_KeepsEabiThunkOnGenericMemoryWithoutLocalStackProof()
{
const uint entry = 0x80001000;
var image = TranslatorCppTestHarness.CreateImage(
(entry + 0x00, 0x39630000), // addi r11, r3, 0
(entry + 0x04, EncodeB(entry + 0x04, 0x80021598, link: true)), // _savegpr_25
(entry + 0x08, 0x4E800020)); // blr
var result = new FunctionTranslator(image).Translate(
entry,
TranslationOptions.Default with { MaxBytes = 0xC });
Assert.Contains("MemoryInline::FlatWrite32((r11 + -28)", result.CxxCode,
StringComparison.Ordinal);
Assert.DoesNotContain("MemoryInline::FlatWriteRam32((r11 + -28)", result.CxxCode,
StringComparison.Ordinal);
}
[Fact]
public void Translate_KnownSiblingEntryFallthroughEmitsTailCall()
{
var memory = new byte[MemoryLayout.RamSize];
WriteWord(memory, 0x80001000, 0x38600007); // li r3, 7
WriteWord(memory, 0x80001004, 0x38800009); // li r4, 9
WriteWord(memory, 0x80001008, 0x4E800020); // sibling continuation: blr
var image = new ProgramImage(
memory,
AddressRange.FromStartAndSize(0x80001000, 0xC),
AddressRange.FromStartAndSize(0x80001000, 0xC),
default,
"test");
var result = new FunctionTranslator(image).Translate(
0x80001000,
TranslationOptions.Default with
{
MaxBytes = 0x8,
AllowUnsupportedInstructions = true,
KnownFunctionEntryPoints = new HashSet<uint> { 0x80001000u, 0x80001008u }
});
Assert.Contains("InvokeDirectCpu<0x80001008u>(ctx);", result.CxxCode, StringComparison.Ordinal);
Assert.Contains("return;", result.CxxCode, StringComparison.Ordinal);
}
[Fact]
public void Translate_KnownConditionalBranchTargetRemainsLocalControlFlow()
{
var memory = new byte[MemoryLayout.RamSize];
WriteWord(memory, 0x80001000, 0x2C030000); // cmpwi r3, 0
WriteWord(memory, 0x80001004, EncodeBc(12, 2, 0x10)); // beq 0x80001014
WriteWord(memory, 0x80001008, 0x38800001); // li r4, 1
WriteWord(memory, 0x8000100C, 0x4E800020); // blr
WriteWord(memory, 0x80001014, 0x38800002); // li r4, 2
WriteWord(memory, 0x80001018, 0x4E800020); // blr
var image = new ProgramImage(
memory,
AddressRange.FromStartAndSize(0x80001000, 0x1C),
AddressRange.FromStartAndSize(0x80001000, 0x1C),
default,
"test");
var result = new FunctionTranslator(image).Translate(
0x80001000,
TranslationOptions.Default with
{
MaxBytes = 0x40,
AllowUnsupportedInstructions = true,
KnownFunctionEntryPoints = new HashSet<uint> { 0x80001000u, 0x80001014u }
});
Assert.Contains("loc_80001014", result.CxxCode, StringComparison.Ordinal);
Assert.DoesNotContain("InvokeDirectCpu<0x80001014u>(ctx);", result.CxxCode, StringComparison.Ordinal);
}
[Fact]
public void Translate_JumpTableKnownContinuationRemainsLocalControlFlow()
{
const uint entry = 0x80000000;
const uint table = 0x80000100;
const uint case0 = entry + 0x1C;
const uint case1 = entry + 0x28;
const uint common = entry + 0x34;
var image = TranslatorCppTestHarness.CreateImage(
(entry + 0x00, 0x28030002), // cmplwi r3, 2
(entry + 0x04, 0x5463103A), // slwi r3, r3, 2
(entry + 0x08, 0x3D808000), // lis r12, 0x8000
(entry + 0x0C, 0x398C0100), // addi r12, r12, 0x100
(entry + 0x10, 0x7C0C182E), // lwzx r0, r12, r3
(entry + 0x14, 0x7C0903A6), // mtctr r0
(entry + 0x18, 0x4E800420), // bctr
(case0 + 0x00, 0x38600007), // li r3, 7
(case0 + 0x04, 0x48000014), // b common
(case1 + 0x00, 0x38600009), // li r3, 9
(case1 + 0x04, 0x48000008), // b common
(common + 0x00, 0x4E800020), // blr
(table + 0x00, case0),
(table + 0x04, case1),
(table + 0x08, common));
var result = new FunctionTranslator(image).Translate(
entry,
TranslationOptions.Default with
{
MaxBytes = 0x80,
AllowUnsupportedInstructions = true,
KnownFunctionEntryPoints = new HashSet<uint> { entry, case0, case1, common }
});
Assert.Contains("loc_80000034", result.CxxCode, StringComparison.Ordinal);
Assert.DoesNotContain("InvokeDirectCpu<0x80000034u>(ctx);", result.CxxCode, StringComparison.Ordinal);
}
[Fact]
public void Translate_ExternalConditionalBranchEmitsTailCallBlock()
{
var memory = new byte[MemoryLayout.RamSize];
WriteWord(memory, 0x80001000, EncodeBc(12, 2, 0x1000)); // beq 0x80002000
WriteWord(memory, 0x80001004, 0x4E800020); // blr
var image = new ProgramImage(
memory,
AddressRange.FromStartAndSize(0x80001000, 0x8),
AddressRange.FromStartAndSize(0x80001000, 0x8),
default,
"test");
var result = new FunctionTranslator(image).Translate(
0x80001000,
TranslationOptions.Default with { MaxBytes = 0x8, AllowUnsupportedInstructions = true });
Assert.Contains("InvokeDirectCpu<0x80002000u>(ctx);", result.CxxCode, StringComparison.Ordinal);
Assert.DoesNotContain("synthetic missing target", result.CxxCode, StringComparison.Ordinal);
}
private static void WriteWord(byte[] memory, uint address, uint word)
{
var offset = checked((int)(address - MemoryLayout.RamBase));
BinaryPrimitives.WriteUInt32BigEndian(memory.AsSpan(offset, 4), word);
}
private static uint EncodeBc(uint bo, uint bi, int branchDelta, bool aa = false, bool lk = false)
=> (16u << 26) | (bo << 21) | (bi << 16) | ((((uint)branchDelta >> 2) & 0x3FFFu) << 2) | ((aa ? 1u : 0u) << 1) | (lk ? 1u : 0u);
private static uint EncodeB(uint address, uint target, bool link = false)
=> (18u << 26) | ((target - address) & 0x03FFFFFCu) | (link ? 1u : 0u);
[Fact]
public void TranslationHelpers_ParseHexCallLabelsAndAbiArgs()
{
var hexArgs = new object?[] { "0x8000ABCD", 0u };
Assert.True(InvokePrivate<bool>("TryParseHex", hexArgs));
Assert.Equal(0x8000ABCDu, (uint)hexArgs[1]!);
var plainArgs = new object?[] { "DEADBEEF", 0u };
Assert.True(InvokePrivate<bool>("TryParseHex", plainArgs));
Assert.Equal(0xDEADBEEFu, (uint)plainArgs[1]!);
var invalidArgs = new object?[] { "not_hex", 0u };
Assert.False(InvokePrivate<bool>("TryParseHex", invalidArgs));
Assert.Equal(0u, (uint)invalidArgs[1]!);
var callLabelArgs = new object?[] { "call_ctr_80001000_80001004", "call_ctr_", 0u, 0u };
Assert.True(InvokePrivate<bool>("TryParseCallLabel", callLabelArgs));
Assert.Equal(0x80001000u, (uint)callLabelArgs[2]!);
Assert.Equal(0x80001004u, (uint)callLabelArgs[3]!);
var shortLabelArgs = new object?[] { "call_ctr_80001000", "call_ctr_", 0u, 0u };
Assert.False(InvokePrivate<bool>("TryParseCallLabel", shortLabelArgs));
var wrongPrefixArgs = new object?[] { "indirect_ctr_80001000", "call_ctr_", 0u, 0u };
Assert.False(InvokePrivate<bool>("TryParseCallLabel", wrongPrefixArgs));
var abiArgs = InvokePrivate<IReadOnlyList<IrValue>>("BuildAbiCallArgs");
Assert.Equal(21, abiArgs.Count);
Assert.Equal("r3", abiArgs[0].RegisterName);
Assert.Equal("r10", abiArgs[7].RegisterName);
Assert.Equal("f1", abiArgs[8].RegisterName);
Assert.Equal("f13", abiArgs[^1].RegisterName);
}
[Fact]
public void TryBuildSyntheticBlock_CoversIndirectAndCallForms()
{
var indirectArgs = new object?[] { "indirect_ctr_80001234", null! };
Assert.True(InvokePrivate<bool>("TryBuildSyntheticBlock", indirectArgs));
var indirect = Assert.IsType<IrBasicBlock>(indirectArgs[1]);
Assert.Equal("indirect_ctr_80001234", indirect.Label);
Assert.Equal("ctr", Assert.IsType<IrIndirectJump>(Assert.Single(indirect.Instructions)).Target.RegisterName);
var callCtrArgs = new object?[] { "call_ctr_80001000_80001004", null! };
Assert.True(InvokePrivate<bool>("TryBuildSyntheticBlock", callCtrArgs));
var callCtr = Assert.IsType<IrBasicBlock>(callCtrArgs[1]);
Assert.Collection(
callCtr.Instructions,
ins =>
{
var assign = Assert.IsType<IrAssign>(ins);
Assert.Equal("lr", assign.Destination);
Assert.Equal(unchecked((int)0x80001004u), assign.Value.Constant);
},
ins =>
{
var call = Assert.IsType<IrIndirectCall>(ins);
Assert.Equal("ctr", call.Target.RegisterName);
Assert.Equal(21, call.Arguments.Count);
},
ins => Assert.Equal("0x80001004", Assert.IsType<IrJump>(ins).TargetLabel));
var callLrArgs = new object?[] { "call_lr_80002000_80002004", null! };
Assert.True(InvokePrivate<bool>("TryBuildSyntheticBlock", callLrArgs));
var callLr = Assert.IsType<IrBasicBlock>(callLrArgs[1]);
Assert.Collection(
callLr.Instructions,
ins =>
{
var assign = Assert.IsType<IrAssign>(ins);
Assert.Equal("addr_bclrl_80002000_loc", assign.Destination);
Assert.Equal("lr", assign.Value.RegisterName);
},
ins =>
{
var assign = Assert.IsType<IrAssign>(ins);
Assert.Equal("lr", assign.Destination);
Assert.Equal(unchecked((int)0x80002004u), assign.Value.Constant);
},
ins =>
{
var call = Assert.IsType<IrIndirectCall>(ins);
Assert.Equal("addr_bclrl_80002000_loc", call.Target.RegisterName);
Assert.Equal(21, call.Arguments.Count);
},
ins => Assert.Equal("0x80002004", Assert.IsType<IrJump>(ins).TargetLabel));
var absoluteArgs = new object?[] { "0x800D7FCC", null! };
Assert.True(InvokePrivate<bool>("TryBuildSyntheticBlock", absoluteArgs));
var absolute = Assert.IsType<IrBasicBlock>(absoluteArgs[1]);
Assert.Equal("0x800D7FCC", absolute.Label);
Assert.Collection(
absolute.Instructions,
ins =>
{
var call = Assert.IsType<IrCall>(ins);
Assert.Equal("0x800D7FCC", call.Target);
Assert.Equal(21, call.Arguments.Count);
},
ins => Assert.Null(Assert.IsType<IrReturn>(ins).Value));
var invalidArgs = new object?[] { "plain_label", null! };
Assert.False(InvokePrivate<bool>("TryBuildSyntheticBlock", invalidArgs));
Assert.Null(invalidArgs[1]);
}
[Fact]
public void PublicRecordsExposeStoredValues()
{
var metrics = new TranslationMetrics();
Assert.Same(TranslationOptions.Default, TranslationOptions.Default);
var instructions = new[]
{
Translator.Core.Disassembly.PpcInstruction.Synthetic(
0x800060A4,
0x4E800020,
"blr",
Array.Empty<Translator.Core.Disassembly.PpcOperand>())
};
var blocks = new[] { new Translator.Core.Analysis.BasicBlocks.BasicBlock(0x800060A4, instructions) };
var irFunction = new IrFunction(
"coverage_result",
"entry",
new[] { new IrBasicBlock("entry", new IrInstruction[] { new IrReturn(IrValue.Register("r3")) }) });
var cfg = new IrCfg(
new Dictionary<string, IrBasicBlock>(StringComparer.OrdinalIgnoreCase) { ["entry"] = irFunction.Blocks[0] },
"entry",
new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase) { ["entry"] = new() });
var ssa = new SsaResult(irFunction, cfg);
var types = new RepresentationEnvironment();
var signature = new FunctionAbiClassification("coverage_result", ValueRepresentation.Void);
var result = new FunctionTranslationResult(
0x800060A4,
"coverage_result",
instructions,
blocks,
irFunction,
ssa,
types,
signature,
"// generated",
metrics);
Assert.Equal(0x800060A4u, result.EntryPoint);
Assert.Equal("coverage_result", result.Name);
Assert.Same(instructions, result.Instructions);
Assert.Same(blocks, result.Blocks);
Assert.Same(irFunction, result.LinearIr);
Assert.Same(ssa, result.Ssa);
Assert.Same(types, result.Representations);
Assert.Same(signature, result.AbiClassification);
Assert.Equal("// generated", result.CxxCode);
Assert.Same(metrics, result.Metrics);
}
}
@@ -0,0 +1,90 @@
using Translator.Core;
using Translator.Core.Analysis;
using Xunit;
namespace Translator.Tests;
/// <summary>
/// Proves marker text and its read-back patterns stay in sync: every marker the emitter writes
/// must be matched, group for group, by the pattern consumers parse it with.
/// </summary>
public sealed class GeneratedMarkersTests
{
[Fact]
public void GuestAbiMarkerRoundTripsThroughItsPattern()
{
var contract = new GuestAbiContract(
GprReadBeforeWriteMask: 0x00000018u,
GprPossibleWriteMask: 0x0000000Cu,
GprReturnMask: 0x00000008u,
FprReadBeforeWriteMask: 0x00000002u,
FprPossibleWriteMask: 0x00000006u,
FprReturnMask: 0x00000002u,
CrReadBeforeWriteMask: 0x12,
CrPossibleWriteMask: 0x34,
ReadsXerBeforeWrite: true,
MayWriteXer: false,
ReadsCtrBeforeWrite: false,
MayWriteCtr: false,
ReadsLrBeforeWrite: false,
MayWriteLr: false,
BoundaryFlags: GuestCallBoundaryFlags.None,
DirectCallTargets: Array.Empty<uint>());
var marker = GeneratedMarkers.GuestAbi(contract);
var match = GeneratedMarkers.GuestAbiPattern().Match(marker);
Assert.True(match.Success, marker);
Assert.Equal("00000018", match.Groups["gr"].Value);
Assert.Equal("0000000C", match.Groups["gw"].Value);
Assert.Equal("00000008", match.Groups["gret"].Value);
Assert.Equal("00000002", match.Groups["fr"].Value);
Assert.Equal("00000006", match.Groups["fw"].Value);
Assert.Equal("00000002", match.Groups["fret"].Value);
Assert.Equal("12", match.Groups["crr"].Value);
Assert.Equal("34", match.Groups["crw"].Value);
Assert.Equal("1", match.Groups["xr"].Value);
Assert.Equal("0", match.Groups["xw"].Value);
Assert.Equal(contract.HasFullSynchronizationFence ? "1" : "0", match.Groups["fence"].Value);
}
[Fact]
public void BaseRegistrationMarkerRoundTripsThroughItsPattern()
{
var marker = GeneratedMarkers.BaseRegistration(0x80001234u, "func_80001234", true, 0xFC000000u);
Assert.Equal(
"// RECOMP_REGISTRATION base 0x80001234 func_80001234 preserves=true fpr_mask=0xFC000000",
marker);
var match = GeneratedMarkers.BaseRegistrationPattern().Match(marker);
Assert.True(match.Success);
Assert.Equal("80001234", match.Groups["address"].Value);
Assert.Equal("func_80001234", match.Groups["symbol"].Value);
Assert.Equal("true", match.Groups["preserves"].Value);
Assert.Equal("FC000000", match.Groups["mask"].Value);
Assert.Matches(GeneratedMarkers.RegistrationLinePattern(), marker + "\n");
}
[Fact]
public void ModRegistrationMarkerRoundTripsThroughItsPattern()
{
var marker = GeneratedMarkers.ModRegistration(
0x80002000u, "rr_80002000", "Pretty::Name", false, 0x00004000u, 100u, 7ul);
Assert.Equal(
"// RECOMP_REGISTRATION mod 0x80002000 rr_80002000 \"Pretty::Name\" " +
"preserves=false fpr_mask=0x00004000 priority=100 module_id=7",
marker);
var match = GeneratedMarkers.ModRegistrationPattern().Match(marker);
Assert.True(match.Success);
Assert.Equal("80002000", match.Groups["address"].Value);
Assert.Equal("rr_80002000", match.Groups["symbol"].Value);
Assert.Equal("Pretty::Name", match.Groups["name"].Value);
Assert.Equal("false", match.Groups["preserves"].Value);
Assert.Equal("00004000", match.Groups["mask"].Value);
Assert.Equal("100", match.Groups["priority"].Value);
Assert.Equal("7", match.Groups["moduleId"].Value);
}
}
@@ -0,0 +1,88 @@
using Translator.Core.Build;
using Translator.Core.Mods;
namespace Translator.Tests;
public sealed class GeneratedOutputPrunerTests
{
[Fact]
public void MetadataPruningRemovesStaleTranslatedBody()
{
var root = Path.Combine(Path.GetTempPath(), "mkw_prune_test", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
try
{
var currentCpp = Path.Combine(root, "current.cpp");
var staleCpp = Path.Combine(root, "stale.cpp");
File.WriteAllText(currentCpp, "// current");
File.WriteAllText(staleCpp, "// stale");
var metadata = BaseTranslationOutputMetadata.Create([
FunctionMetadata("current.cpp", 0x80001000),
FunctionMetadata("stale.cpp", 0x80001004)
], TranslationQualityMetadata.Clean);
var emitted = new HashSet<string>(
[Path.GetFullPath(currentCpp)],
OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
var result = GeneratedOutputPruner.Prune(root, emitted, metadata);
Assert.Equal(1, result.RemovedFiles);
Assert.Empty(result.Warnings);
Assert.True(File.Exists(currentCpp));
Assert.False(File.Exists(staleCpp));
}
finally
{
if (Directory.Exists(root))
Directory.Delete(root, recursive: true);
}
}
[Fact]
public void MetadataPruningRefusesPathOutsideOutputRoot()
{
var parent = Path.Combine(Path.GetTempPath(), "mkw_prune_test", Guid.NewGuid().ToString("N"));
var root = Path.Combine(parent, "generated");
Directory.CreateDirectory(root);
var outside = Path.Combine(parent, "outside.cpp");
File.WriteAllText(outside, "// user owned");
try
{
var metadata = BaseTranslationOutputMetadata.Create([
FunctionMetadata("../outside.cpp", 0x80001000)
], TranslationQualityMetadata.Clean);
var result = GeneratedOutputPruner.Prune(root, new HashSet<string>(), metadata);
Assert.Equal(0, result.RemovedFiles);
Assert.Contains(result.Warnings, warning => warning.Contains("outside output root", StringComparison.Ordinal));
Assert.True(File.Exists(outside));
}
finally
{
if (Directory.Exists(parent))
Directory.Delete(parent, recursive: true);
}
}
private static BaseTranslationFunctionMetadata FunctionMetadata(
string relativePath,
uint address)
{
var build = new BaseTranslationFunctionBuildMetadata(
$"func_{address:X8}",
true,
0,
[],
null,
1);
return new BaseTranslationFunctionMetadata(
relativePath,
1,
new string('b', 64),
address,
[],
build);
}
}
@@ -0,0 +1,165 @@
using Translator.Cli.Configuration;
using Translator.Core.Loading;
using Translator.Core.Parsing.Dol;
using Translator.Core.Translation;
using Xunit;
namespace Translator.Tests;
public sealed class GenericProjectTests
{
[Fact]
public void FunctionMapIsPathOnlyWhileBinaryHashesRemainEnforced()
{
var temp = CreateTempDirectory();
try
{
File.WriteAllBytes(Path.Combine(temp, "main.dol"), new byte[0x100]);
var mapPath = Path.Combine(temp, "MAP.txt");
File.WriteAllText(mapPath, "80001000 entry\n");
var projectPath = Path.Combine(temp, "recomp.yml");
const string manifest = """
schema_version: 1
workspace_root: .
project:
id: map-config-test
inputs:
dol:
path: main.dol
""";
File.WriteAllText(
projectPath,
manifest + Environment.NewLine + """
translation:
function_map:
path: MAP.txt
""");
var project = TranslationProjectConfig.Load(projectPath);
Assert.Equal(mapPath, project.Translation.FunctionMapPath);
Assert.Equal(0x80001000u, Assert.Single(FunctionMap.Load(project.Translation.FunctionMapPath!).Addresses));
File.WriteAllText(
projectPath,
manifest + Environment.NewLine + """
translation:
function_map:
path: MAP.txt
sha256: deliberately-rejected-legacy-key
""");
// The function map binds by path alone; the legacy sha256 key is an
// unknown property and rejects the whole manifest.
var mapHashError = Assert.ThrowsAny<Exception>(() => TranslationProjectConfig.Load(projectPath));
Assert.Contains("sha256", mapHashError.Message, StringComparison.Ordinal);
File.WriteAllText(
projectPath,
manifest.Replace(
" path: main.dol",
" path: main.dol\n sha256: deliberately-wrong",
StringComparison.Ordinal));
var dolError = Assert.Throws<InvalidDataException>(() => TranslationProjectConfig.Load(projectPath));
Assert.Contains("Configured DOL SHA-256 does not match", dolError.Message, StringComparison.Ordinal);
File.WriteAllText(
projectPath,
manifest + Environment.NewLine + """
translation:
function_map:
sha256: rejected-and-not-a-path
""");
var mapError = Assert.ThrowsAny<Exception>(() => TranslationProjectConfig.Load(projectPath));
Assert.Contains("sha256", mapError.Message, StringComparison.Ordinal);
}
finally
{
Directory.Delete(temp, recursive: true);
}
}
[Fact]
public void DolOnlyProjectLoadsAndTranslatesItsConfiguredEntryPoint()
{
var temp = CreateTempDirectory();
try
{
const uint entry = 0x80001000u;
var dolPath = Path.Combine(temp, "main.dol");
File.WriteAllBytes(
dolPath,
SyntheticDolFactory.CreateBytes(
entry,
sections: [SyntheticDolFactory.Text(0, entry, 0x38630001u, 0x4E800020u)]));
var projectPath = Path.Combine(temp, "recomp.yml");
File.WriteAllText(
projectPath,
"""
schema_version: 1
project:
id: synthetic-dol
display_name: Synthetic DOL
inputs:
dol:
path: main.dol
translation:
entry_points: [0x80001000]
allow_unsupported_instructions: true
output:
root: out
""");
var project = TranslationProjectConfig.Load(projectPath);
Assert.Null(project.Inputs.Rel);
Assert.Equal(entry, Assert.Single(project.Translation.EntryPoints));
var dol = DolFile.Load(project.Inputs.Dol.Path);
var image = new ProgramImageBuilder().Build(dol, ramBase: project.Memory.Base, ramSize: project.Memory.Size);
var result = new FunctionTranslator(image).Translate(
entry,
new TranslationOptions(AllowUnsupportedInstructions: true));
Assert.Equal(2, result.Metrics.PpcInstructionCount);
Assert.Contains("func_80001000", result.CxxCode, StringComparison.Ordinal);
}
finally
{
Directory.Delete(temp, recursive: true);
}
}
[Fact]
public void ExternalGenericDolParsesAndTranslatesWhenConfigured()
{
var path = Environment.GetEnvironmentVariable("RECOMP_GENERIC_DOL");
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
{
return;
}
var dol = DolFile.Load(path);
var image = new ProgramImageBuilder().Build(dol);
var result = new FunctionTranslator(image).Translate(
dol.EntryPoint,
new TranslationOptions(
MaxInstructions: 2048,
MaxBytes: 0x10000,
AllowUnsupportedInstructions: true));
Assert.NotEmpty(dol.ExecutableSections);
Assert.NotEmpty(result.CxxCode);
Assert.True(result.Metrics.PpcInstructionCount > 0);
}
private static string CreateTempDirectory()
{
var path = Path.Combine(Path.GetTempPath(), "generic_recomp_project", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(path);
return path;
}
}
@@ -0,0 +1 @@
global using Xunit;
@@ -0,0 +1,265 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis.Ssa;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
/// <summary>
/// Coalesces consecutive statically-known gather-pipe stores into one <c>GX_HLE_FIFO_WriteBurst</c>.
/// Output must stay byte-identical to individual GX_HLE_FIFO_Write* calls, and a run must never span
/// a guest load, since a recording display list can observe the deferred write in guest memory.
/// </summary>
public class GpuFifoBurstCodeGenTests
{
// lis rX, 0xCC01 / stX rY, -0x8000(rX) - how every nw4r GD* helper spells a
// gather-pipe write.
private const int GatherPipeBase = unchecked((int)0xCC010000);
private const int GatherPipeDisplacement = -32768;
private static string Emit(IrFunction function) =>
new CxxLinearCodeGenerator().Emit(
0x80001000u,
new SsaTransformer().Convert(function),
new FunctionAbiClassification(function.Name, ValueRepresentation.Void),
new RepresentationEnvironment(Enumerable.Range(0, 12)
.ToDictionary(index => $"r{index}", _ => ValueRepresentation.UInt32)));
private static IrStore FifoStore(string source, int sizeBytes) =>
new(new IrAddress("r10", GatherPipeDisplacement), IrValue.Register(source), sizeBytes);
private static IrFunction Function(string name, params IrInstruction[] body) =>
new(name, "entry",
[
new IrBasicBlock("entry",
new IrInstruction[] { new IrAssign("r10", IrValue.Imm(GatherPipeBase)) }
.Concat(body).ToArray())
]);
[Fact]
public void ThirteenMixedWidthStoresBecomeOneBurstWithBigEndianLayout()
{
// Alternating byte/word fields, the shape a GD command header plus its
// payload registers has.
var stores = new List<IrInstruction>();
for (var index = 0; index < 13; index++)
stores.Add(FifoStore("r3", index % 2 == 0 ? 1 : 4));
stores.Add(new IrReturn(null));
var code = Emit(Function("burst_mixed", stores.ToArray()));
// 7 single bytes + 6 words = 31 bytes, one buffer, one call.
Assert.Contains("uint8_t mkw_fifo_burst_0[31];", code, StringComparison.Ordinal);
Assert.Contains("GX_HLE_FIFO_WriteBurst(mkw_fifo_burst_0, 31u);", code, StringComparison.Ordinal);
Assert.Equal(1, CountOccurrences(code, "GX_HLE_FIFO_WriteBurst("));
Assert.DoesNotContain("GX_HLE_FIFO_Write8(", code, StringComparison.Ordinal);
Assert.DoesNotContain("GX_HLE_FIFO_Write32(", code, StringComparison.Ordinal);
// Byte 0 is the first 8-bit field; bytes 1..4 are the first word,
// most-significant byte first.
Assert.Contains("mkw_fifo_burst_0[0] = static_cast<uint8_t>(r3);", code, StringComparison.Ordinal);
Assert.Contains("mkw_fifo_burst_0[1] = static_cast<uint8_t>((mkw_fifo_word >> 24));", code, StringComparison.Ordinal);
Assert.Contains("mkw_fifo_burst_0[2] = static_cast<uint8_t>((mkw_fifo_word >> 16));", code, StringComparison.Ordinal);
Assert.Contains("mkw_fifo_burst_0[3] = static_cast<uint8_t>((mkw_fifo_word >> 8));", code, StringComparison.Ordinal);
Assert.Contains("mkw_fifo_burst_0[4] = static_cast<uint8_t>(mkw_fifo_word);", code, StringComparison.Ordinal);
// ... and the run ends on the 13th store, at byte 30.
Assert.Contains("mkw_fifo_burst_0[30] = static_cast<uint8_t>(r3);", code, StringComparison.Ordinal);
}
[Fact]
public void SixteenBitStoresSerializeTwoBigEndianBytes()
{
var stores = Enumerable.Range(0, 4)
.Select(_ => (IrInstruction)FifoStore("r3", 2))
.Append(new IrReturn(null))
.ToArray();
var code = Emit(Function("burst_halfwords", stores));
Assert.Contains("uint8_t mkw_fifo_burst_0[8];", code, StringComparison.Ordinal);
Assert.Contains("mkw_fifo_burst_0[0] = static_cast<uint8_t>((mkw_fifo_word >> 8));", code, StringComparison.Ordinal);
Assert.Contains("mkw_fifo_burst_0[1] = static_cast<uint8_t>(mkw_fifo_word);", code, StringComparison.Ordinal);
}
[Fact]
public void FloatStoresSerializeTheirIeeeBitsBigEndian()
{
var stores = Enumerable.Range(0, 4)
.Select(_ => (IrInstruction)new IrStore(
new IrAddress("r10", GatherPipeDisplacement), IrValue.Register("f1"), 4))
.Append(new IrReturn(null))
.ToArray();
var code = Emit(Function("burst_floats", stores));
Assert.Contains("uint8_t mkw_fifo_burst_0[16];", code, StringComparison.Ordinal);
Assert.Contains("PpcBitCastToU32Inline(static_cast<float>(", code, StringComparison.Ordinal);
Assert.Contains("GX_HLE_FIFO_WriteBurst(mkw_fifo_burst_0, 16u);", code, StringComparison.Ordinal);
Assert.DoesNotContain("GX_HLE_FIFO_WriteFloat(", code, StringComparison.Ordinal);
}
[Fact]
public void RegisterArithmeticBetweenStoresStaysInsideTheRun()
{
var stores = new List<IrInstruction>();
for (var index = 0; index < 4; index++)
{
stores.Add(new IrBinary("r3", IrValue.Register("r3"), IrValue.Imm(1), "add"));
stores.Add(FifoStore("r3", 4));
}
stores.Add(new IrReturn(null));
var code = Emit(Function("burst_with_arithmetic", stores.ToArray()));
Assert.Contains("GX_HLE_FIFO_WriteBurst(mkw_fifo_burst_0, 16u);", code, StringComparison.Ordinal);
Assert.Equal(1, CountOccurrences(code, "GX_HLE_FIFO_WriteBurst("));
}
[Fact]
public void RunOfThreeIsNotCoalesced()
{
var stores = Enumerable.Range(0, 3)
.Select(_ => (IrInstruction)FifoStore("r3", 4))
.Append(new IrReturn(null))
.ToArray();
var code = Emit(Function("short_run", stores));
Assert.DoesNotContain("GX_HLE_FIFO_WriteBurst", code, StringComparison.Ordinal);
Assert.DoesNotContain("mkw_fifo_burst_0", code, StringComparison.Ordinal);
Assert.Equal(3, CountOccurrences(code, "GX_HLE_FIFO_Write32("));
}
[Fact]
public void OrdinaryGuestStoreSplitsTheRun()
{
// A store to guest RAM cannot be reordered past a FIFO write: while a
// display list is recording, the FIFO writer itself writes guest memory.
var instructions = new List<IrInstruction>();
for (var index = 0; index < 4; index++) instructions.Add(FifoStore("r3", 4));
instructions.Add(new IrStore(new IrAddress("r4", 0), IrValue.Register("r5"), 4));
for (var index = 0; index < 4; index++) instructions.Add(FifoStore("r3", 4));
instructions.Add(new IrReturn(null));
var code = Emit(Function("split_by_store", instructions.ToArray()));
Assert.Equal(2, CountOccurrences(code, "GX_HLE_FIFO_WriteBurst("));
Assert.Contains("GX_HLE_FIFO_WriteBurst(mkw_fifo_burst_0, 16u);", code, StringComparison.Ordinal);
Assert.Contains("GX_HLE_FIFO_WriteBurst(mkw_fifo_burst_1, 16u);", code, StringComparison.Ordinal);
}
[Fact]
public void GuestLoadSplitsTheRun()
{
var instructions = new List<IrInstruction>();
for (var index = 0; index < 4; index++) instructions.Add(FifoStore("r3", 4));
instructions.Add(new IrLoad("r5", new IrAddress("r4", 0), 4));
for (var index = 0; index < 4; index++) instructions.Add(FifoStore("r3", 4));
instructions.Add(new IrReturn(null));
var code = Emit(Function("split_by_load", instructions.ToArray()));
Assert.Equal(2, CountOccurrences(code, "GX_HLE_FIFO_WriteBurst("));
}
[Fact]
public void CallSplitsTheRun()
{
var instructions = new List<IrInstruction>();
for (var index = 0; index < 4; index++) instructions.Add(FifoStore("r3", 4));
instructions.Add(new IrCall(string.Empty, "func_80002000", []));
for (var index = 0; index < 4; index++) instructions.Add(FifoStore("r3", 4));
instructions.Add(new IrReturn(null));
var code = Emit(Function("split_by_call", instructions.ToArray()));
Assert.Equal(2, CountOccurrences(code, "GX_HLE_FIFO_WriteBurst("));
}
[Fact]
public void BranchSplitsTheRunAndEachSideBurstsIndependently()
{
IrInstruction[] Stores(int count, IrInstruction terminator) =>
Enumerable.Range(0, count)
.Select(_ => (IrInstruction)FifoStore("r3", 4))
.Append(terminator)
.ToArray();
var function = new IrFunction("split_by_branch", "entry",
[
new IrBasicBlock("entry",
new IrInstruction[] { new IrAssign("r10", IrValue.Imm(GatherPipeBase)) }
.Concat(Stores(4, new IrBranch("ne", "left", "right"))).ToArray()),
new IrBasicBlock("left", Stores(4, new IrReturn(null))),
// Three stores on this side stay individual: below the run minimum.
new IrBasicBlock("right", Stores(3, new IrReturn(null)))
]);
var code = Emit(function);
Assert.Equal(2, CountOccurrences(code, "GX_HLE_FIFO_WriteBurst("));
Assert.Equal(3, CountOccurrences(code, "GX_HLE_FIFO_Write32("));
}
[Fact]
public void StoresOutsideTheGatherPipeAreNeverCoalesced()
{
var instructions = new List<IrInstruction>
{
new IrAssign("r10", IrValue.Imm(unchecked((int)0x80300000)))
};
for (var index = 0; index < 8; index++)
instructions.Add(new IrStore(new IrAddress("r10", index * 4), IrValue.Register("r3"), 4));
instructions.Add(new IrReturn(null));
var code = Emit(new IrFunction("not_gather_pipe", "entry",
[new IrBasicBlock("entry", instructions)]));
Assert.DoesNotContain("GX_HLE_FIFO_WriteBurst", code, StringComparison.Ordinal);
}
[Fact]
public void UnsupportedIntegerWidthFallsBackToGuestMemory()
{
var code = Emit(Function(
"fifo_integer_width8",
FifoStore("r3", 8),
new IrReturn(null)));
Assert.DoesNotContain("GX_HLE_FIFO_Write", code, StringComparison.Ordinal);
Assert.Contains("MemoryInline::FlatWrite64", code, StringComparison.Ordinal);
}
[Fact]
public void UnsupportedFloatWidthFallsBackToGuestMemory()
{
var code = Emit(Function(
"fifo_float_width8",
new IrStore(
new IrAddress("r10", GatherPipeDisplacement),
IrValue.Register("f1"),
8),
new IrReturn(null)));
Assert.DoesNotContain("GX_HLE_FIFO_Write", code, StringComparison.Ordinal);
Assert.Contains("MemoryInline::FlatWriteFloat64", code, StringComparison.Ordinal);
}
private static int CountOccurrences(string text, string value)
{
var count = 0;
var index = text.IndexOf(value, StringComparison.Ordinal);
while (index >= 0)
{
++count;
index = text.IndexOf(value, index + value.Length, StringComparison.Ordinal);
}
return count;
}
}
@@ -0,0 +1,242 @@
using Translator.Core.Analysis.Ssa;
using Translator.Core.Analysis.Representation;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Translation;
using Translator.Core.Representation;
namespace Translator.Tests;
public sealed class GqrConstantPropagationTests
{
[Fact]
public void SpecializesKnownGqrAfterConstantWrite()
{
var function = Function(new IrInstruction[]
{
new IrAssign("r3", IrValue.Imm(0x00040004)),
new IrAssign("gqr2", IrValue.Register("r3")),
PsqLoad(2),
new IrReturn(null)
});
var specialized = GqrConstantPropagation.Specialize(function);
var call = Assert.IsType<IrCall>(specialized.Blocks[0].Instructions[2]);
Assert.Equal("PPC_PsqLKnown_00040004", call.Target);
}
[Fact]
public void MergeKeepsOnlyEqualMustConstants()
{
var equal = Diamond(0x00040004, 0x00040004);
var unequal = Diamond(0x00040004, 0x00050005);
Assert.Contains(GqrConstantPropagation.Specialize(equal).Blocks.Single(b => b.Label == "join").Instructions,
i => i is IrCall { Target: "PPC_PsqLKnown_00040004" });
Assert.Contains(GqrConstantPropagation.Specialize(unequal).Blocks.Single(b => b.Label == "join").Instructions,
i => i is IrCall { Target: "PPC_PsqL" });
}
[Fact]
public void LoopAndOutOfOrderBlocksDoNotEraseEstablishedConstants()
{
var function = new IrFunction("gqr_loop", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrAssign("gqr5", IrValue.Imm(0x00070007)),
new IrJump("loop")
}),
// Deliberately precedes its only reachable predecessor in storage order.
new IrBasicBlock("exit", new IrInstruction[] { PsqLoad(5), new IrReturn(null) }),
new IrBasicBlock("loop", new IrInstruction[] { new IrBranch("beq", "loop", "exit") })
});
var specialized = GqrConstantPropagation.Specialize(function);
Assert.Contains(specialized.Blocks.Single(block => block.Label == "exit").Instructions,
instruction => instruction is IrCall { Target: "PPC_PsqLKnown_00070007" });
}
[Fact]
public void UnknownWriteAndGuestCallsInvalidateGqrConstants()
{
var unknownWrite = Function(new IrInstruction[]
{
new IrAssign("gqr2", IrValue.Imm(0x00040004)),
new IrLoad("r4", new IrAddress("r3", 0), 4),
new IrAssign("gqr2", IrValue.Register("r4")),
PsqLoad(2), new IrReturn(null)
});
var guestCall = Function(new IrInstruction[]
{
new IrAssign("gqr2", IrValue.Imm(0x00040004)),
new IrCall(string.Empty, "func_80001000", Array.Empty<IrValue>()),
PsqLoad(2), new IrReturn(null)
});
Assert.Contains(GqrConstantPropagation.Specialize(unknownWrite).Blocks[0].Instructions,
i => i is IrCall { Target: "PPC_PsqL" });
Assert.Contains(GqrConstantPropagation.Specialize(guestCall).Blocks[0].Instructions,
i => i is IrCall { Target: "PPC_PsqL" });
}
[Fact]
public void CodegenEmitsKnownGqrTemplateWithoutContextRead()
{
var function = Function(new IrInstruction[]
{
new IrAssign("r3", IrValue.Imm(0x3D043D04)),
new IrAssign("gqr2", IrValue.Register("r3")),
new IrCall(string.Empty, "PPC_PsqSt", new[] { IrValue.Register("r4"), IrValue.Register("f1"), IrValue.Imm(0), IrValue.Imm(2) }),
new IrReturn(null)
});
var ssa = new SsaTransformer().Convert(function);
var code = new CxxLinearCodeGenerator().Emit(0x80001000, ssa,
new FunctionAbiClassification("known_gqr", ValueRepresentation.Void),
new RepresentationEnvironment());
Assert.Contains("PPC_PsqStKnownInline<0u, 2u, 0x3D043D04u>", code, StringComparison.Ordinal);
}
[Fact]
public void GuardedEntryConstantEmitsKnownFastPathAndDynamicFallback()
{
var function = Function(new IrInstruction[] { PsqLoad(5), new IrReturn(null) });
var ssa = new SsaTransformer().Convert(function);
var code = new CxxLinearCodeGenerator().Emit(0x80001000, ssa,
new FunctionAbiClassification("guarded_gqr", ValueRepresentation.Void),
new RepresentationEnvironment(),
gqrEntryConstants: new Dictionary<string, uint> { ["gqr5"] = 0x00070007u },
gqrConstantsRequireRuntimeGuard: true);
Assert.Contains("ctx->gqr[5u] == 0x00070007u", code, StringComparison.Ordinal);
Assert.Contains("PPC_PsqLKnownInline<0u, 5u, 0x00070007u>", code, StringComparison.Ordinal);
// The guard's dynamic arm is the generic helper, reading the GQR value
// hoisted into the prologue rather than ctx->gqr[5] per access.
Assert.Contains("[[maybe_unused]] uint32_t mkw_gqr5 = ctx->gqr[5];", code, StringComparison.Ordinal);
Assert.Contains("PPC_PsqLGqrInline<0u, 5u>(ctx, mkw_gqr5,", code, StringComparison.Ordinal);
}
[Fact]
public void RepeatedGuardedPsqSitesUseOneFunctionVersionGuard()
{
var instructions = Enumerable.Range(0, 8)
.Select(_ => (IrInstruction)PsqLoad(5))
.Append(new IrReturn(null))
.ToArray();
var function = Function(instructions);
var code = new CxxLinearCodeGenerator().Emit(0x80001000,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("versioned_gqr", ValueRepresentation.Void),
new RepresentationEnvironment(),
gqrEntryConstants: new Dictionary<string, uint> { ["gqr5"] = 0x00070007u },
gqrConstantsRequireRuntimeGuard: true);
Assert.Contains("template <bool gqr_entry_profile>", code, StringComparison.Ordinal);
Assert.Contains("versioned_gqr_gqr_impl<true>(ctx);", code, StringComparison.Ordinal);
Assert.Contains("versioned_gqr_gqr_impl<false>(ctx);", code, StringComparison.Ordinal);
Assert.DoesNotContain("const bool gqr_entry_5_00070007", code, StringComparison.Ordinal);
}
[Fact]
public void InterproceduralAnalysisPropagatesConstantsThroughDirectCalls()
{
var functions = new Dictionary<uint, IrFunction>
{
[0x80001000] = Function(new IrInstruction[]
{
new IrAssign("gqr5", IrValue.Imm(0x00070007)),
new IrAssign("gqr6", IrValue.Imm(0x3D043D04)),
new IrCall(string.Empty, "0x80002000", Array.Empty<IrValue>()),
new IrReturn(null)
}),
[0x80002000] = Function(new IrInstruction[]
{
new IrCall(string.Empty, "0x80003000", Array.Empty<IrValue>()),
new IrReturn(null)
}),
[0x80003000] = Function(new IrInstruction[] { PsqLoad(5), new IrReturn(null) })
};
var result = GqrInterproceduralAnalysis.Analyze(
functions.ToDictionary(pair => pair.Key, pair => GqrFunctionSummary.Create(pair.Value)),
new[] { 0x80001000u });
Assert.Equal(0x00070007u, result.EntryConstants[0x80002000]["gqr5"]);
Assert.Equal(0x3D043D04u, result.EntryConstants[0x80003000]["gqr6"]);
var specialized = GqrConstantPropagation.Specialize(
functions[0x80003000], result.EntryConstants[0x80003000], result.WriteMasks);
Assert.Contains(specialized.Blocks[0].Instructions,
instruction => instruction is IrCall { Target: "PPC_PsqLKnown_00070007" });
}
[Fact]
public void InterproceduralAnalysisIntersectsConflictingCallers()
{
var functions = new Dictionary<uint, IrFunction>
{
[0x80001000] = Function(new IrInstruction[]
{
new IrAssign("gqr5", IrValue.Imm(0x00070007)),
new IrCall(string.Empty, "0x80003000", Array.Empty<IrValue>()),
new IrCall(string.Empty, "0x80002000", Array.Empty<IrValue>()),
new IrReturn(null)
}),
[0x80002000] = Function(new IrInstruction[]
{
new IrAssign("gqr5", IrValue.Imm(0x00050005)),
new IrCall(string.Empty, "0x80003000", Array.Empty<IrValue>()),
new IrReturn(null)
}),
[0x80003000] = Function(new IrInstruction[] { PsqLoad(5), new IrReturn(null) })
};
var result = GqrInterproceduralAnalysis.Analyze(
functions.ToDictionary(pair => pair.Key, pair => GqrFunctionSummary.Create(pair.Value)),
new[] { 0x80001000u });
Assert.False(result.EntryConstants[0x80003000].ContainsKey("gqr5"));
}
[Fact]
public void CalleeWriteSummaryPreservesOnlyUnmodifiedGqrs()
{
var functions = new Dictionary<uint, IrFunction>
{
[0x80001000] = Function(new IrInstruction[]
{
new IrAssign("gqr5", IrValue.Imm(0x00070007)),
new IrAssign("gqr6", IrValue.Imm(0x3D043D04)),
new IrCall(string.Empty, "0x80002000", Array.Empty<IrValue>()),
PsqLoad(5),
PsqLoad(6),
new IrReturn(null)
}),
[0x80002000] = Function(new IrInstruction[]
{
new IrAssign("gqr6", IrValue.Register("r3")),
new IrReturn(null)
})
};
var result = GqrInterproceduralAnalysis.Analyze(
functions.ToDictionary(pair => pair.Key, pair => GqrFunctionSummary.Create(pair.Value)),
new[] { 0x80001000u });
var specialized = GqrConstantPropagation.Specialize(
functions[0x80001000], result.EntryConstants[0x80001000], result.WriteMasks);
Assert.Equal(1 << 6, result.WriteMasks[0x80002000]);
Assert.Contains(specialized.Blocks[0].Instructions,
instruction => instruction is IrCall { Target: "PPC_PsqLKnown_00070007" });
Assert.Contains(specialized.Blocks[0].Instructions,
instruction => instruction is IrCall { Target: "PPC_PsqL" } call &&
call.Arguments[2].Constant == 6);
}
private static IrCall PsqLoad(int index) => new("f1", "PPC_PsqL", new[] { IrValue.Register("r4"), IrValue.Imm(0), IrValue.Imm(index) });
private static IrFunction Function(IReadOnlyList<IrInstruction> instructions) =>
new("gqr_test", "entry", new[] { new IrBasicBlock("entry", instructions) });
private static IrFunction Diamond(uint left, uint right) => new("gqr_diamond", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[] { new IrBranch("beq", "left", "right") }),
new IrBasicBlock("left", new IrInstruction[] { new IrAssign("gqr2", IrValue.Imm(left)), new IrJump("join") }),
new IrBasicBlock("right", new IrInstruction[] { new IrAssign("gqr2", IrValue.Imm(right)), new IrJump("join") }),
new IrBasicBlock("join", new IrInstruction[] { PsqLoad(2), new IrReturn(null) })
});
}
@@ -0,0 +1,269 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis.Ssa;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
/// <summary>
/// Prologue hoisting of ctx-&gt;gqr[I] (audit T-GQR). Pins that the hoisted local is re-read after
/// anything that could write the register (mtspr to GQR0-7, unproven calls), so it stays valid on
/// entry to every block regardless of how control flow reaches it.
/// </summary>
public class GqrPrologueHoistingCodeGenTests
{
private static string Emit(
IrFunction function,
IReadOnlyDictionary<uint, byte>? gqrCalleeWriteMasks = null) =>
new CxxLinearCodeGenerator().Emit(
0x80001000u,
new SsaTransformer().Convert(function),
new FunctionAbiClassification(function.Name, ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
gqrCalleeWriteMasks: gqrCalleeWriteMasks);
private static IrCall PsqLoad(int index, string address = "r4") =>
new("f1", "PPC_PsqL", [IrValue.Register(address), IrValue.Imm(0), IrValue.Imm(index)]);
private static IrCall PsqStore(int index, string address = "r4") =>
new(string.Empty, "PPC_PsqSt",
[IrValue.Register(address), IrValue.Register("f1"), IrValue.Imm(0), IrValue.Imm(index)]);
private static IrFunction Function(string name, params IrInstruction[] instructions) =>
new(name, "entry", [new IrBasicBlock("entry", instructions)]);
[Fact]
public void GenericPsqAccessesReadTheHoistedLocalInsteadOfTheContext()
{
var code = Emit(Function("hoisted",
PsqLoad(0), PsqLoad(0), PsqStore(0), new IrReturn(null)));
Assert.Contains("[[maybe_unused]] uint32_t mkw_gqr0 = ctx->gqr[0];", code, StringComparison.Ordinal);
Assert.Equal(1, CountOccurrences(code, "ctx->gqr[0]"));
Assert.Equal(2, CountOccurrences(code, "PPC_PsqLGqrInline<0u, 0u>(ctx, mkw_gqr0,"));
Assert.Equal(1, CountOccurrences(code, "PPC_PsqStGqrInline<0u, 0u>(ctx, mkw_gqr0,"));
Assert.DoesNotContain("PPC_PsqLInline<", code, StringComparison.Ordinal);
Assert.DoesNotContain("PPC_PsqStInline<", code, StringComparison.Ordinal);
}
[Fact]
public void EachUsedIndexGetsItsOwnLocalAndUnusedIndicesGetNone()
{
var code = Emit(Function("two_indices",
PsqLoad(0), PsqLoad(5), new IrReturn(null)));
Assert.Contains("[[maybe_unused]] uint32_t mkw_gqr0 = ctx->gqr[0];", code, StringComparison.Ordinal);
Assert.Contains("[[maybe_unused]] uint32_t mkw_gqr5 = ctx->gqr[5];", code, StringComparison.Ordinal);
Assert.DoesNotContain("mkw_gqr1", code, StringComparison.Ordinal);
}
[Fact]
public void MtsprToTheGraphicsQuantizationRegisterReloadsTheLocal()
{
var code = Emit(Function("mtspr_reload",
PsqLoad(3),
new IrAssign("gqr3", IrValue.Register("r5")),
PsqLoad(3),
new IrReturn(null)));
// Prologue read plus the mtspr's own write, then the reload.
Assert.Contains("[[maybe_unused]] uint32_t mkw_gqr3 = ctx->gqr[3];", code, StringComparison.Ordinal);
Assert.Contains("mkw_gqr3 = ctx->gqr[3];", code, StringComparison.Ordinal);
Assert.Equal(2, CountOccurrences(code, "mkw_gqr3 = ctx->gqr[3];"));
}
[Fact]
public void MtsprToADifferentIndexDoesNotReloadTheOther()
{
var code = Emit(Function("unrelated_mtspr",
PsqLoad(3),
new IrAssign("gqr6", IrValue.Register("r5")),
PsqLoad(3),
new IrReturn(null)));
Assert.Equal(1, CountOccurrences(code, "mkw_gqr3 = ctx->gqr[3];"));
}
[Fact]
public void GuestCallWithoutAWriteMaskReloadsConservatively()
{
var code = Emit(Function("call_reload",
PsqLoad(3),
new IrCall(string.Empty, "func_80002000", []),
PsqLoad(3),
new IrReturn(null)));
Assert.Equal(2, CountOccurrences(code, "mkw_gqr3 = ctx->gqr[3];"));
}
[Fact]
public void IndirectCallAlwaysReloads()
{
var code = Emit(Function("indirect_reload",
PsqLoad(3),
new IrIndirectCall(string.Empty, IrValue.Register("ctr"), []),
PsqLoad(3),
new IrReturn(null)));
Assert.Equal(2, CountOccurrences(code, "mkw_gqr3 = ctx->gqr[3];"));
}
[Fact]
public void CalleeWriteMaskProvingTheRegisterUntouchedSkipsTheReload()
{
var code = Emit(
Function("masked_call",
PsqLoad(3),
new IrCall(string.Empty, "func_80002000", []),
PsqLoad(3),
new IrReturn(null)),
gqrCalleeWriteMasks: new Dictionary<uint, byte> { [0x80002000u] = 0b0100_0000 });
Assert.Equal(1, CountOccurrences(code, "mkw_gqr3 = ctx->gqr[3];"));
}
[Fact]
public void CalleeWriteMaskCoveringTheRegisterStillReloads()
{
var code = Emit(
Function("masked_call_hit",
PsqLoad(3),
new IrCall(string.Empty, "func_80002000", []),
PsqLoad(3),
new IrReturn(null)),
gqrCalleeWriteMasks: new Dictionary<uint, byte> { [0x80002000u] = 0b0000_1000 });
Assert.Equal(2, CountOccurrences(code, "mkw_gqr3 = ctx->gqr[3];"));
}
[Fact]
public void RuntimeHelperCallsDoNotForceAReload()
{
var code = Emit(Function("helper_call",
PsqLoad(3),
new IrCall("r5", "PPC_Cntlzw", [IrValue.Register("r6")]),
PsqLoad(3),
new IrReturn(null)));
Assert.Equal(1, CountOccurrences(code, "mkw_gqr3 = ctx->gqr[3];"));
}
[Fact]
public void GenericSprWriterTargetingAGqrForcesAReload()
{
var code = Emit(Function("write_spr",
PsqLoad(3),
new IrCall(string.Empty, "PPC_WriteSpr", [IrValue.Imm(915), IrValue.Register("r5")]),
PsqLoad(3),
new IrReturn(null)));
Assert.Equal(2, CountOccurrences(code, "mkw_gqr3 = ctx->gqr[3];"));
}
[Fact]
public void GenericSprWriterTargetingAnUnrelatedSprDoesNotReload()
{
var code = Emit(Function("write_spr_other",
PsqLoad(3),
new IrCall(string.Empty, "PPC_WriteSpr", [IrValue.Imm(9), IrValue.Register("r5")]),
PsqLoad(3),
new IrReturn(null)));
Assert.Equal(1, CountOccurrences(code, "mkw_gqr3 = ctx->gqr[3];"));
}
[Fact]
public void SchedulerBoundaryHelpersForceAReload()
{
var code = Emit(Function("scheduler_boundary",
PsqLoad(3),
new IrCall(string.Empty, "Fiber_YieldToScheduler", []),
PsqLoad(3),
new IrReturn(null)));
Assert.Equal(2, CountOccurrences(code, "mkw_gqr3 = ctx->gqr[3];"));
}
[Fact]
public void AnotherPairedAccessDoesNotForceAReload()
{
// PPC_PsqL/PPC_PsqSt read the register; they never write it.
var code = Emit(Function("psq_only",
PsqLoad(3), PsqLoad(3), PsqStore(3), new IrReturn(null)));
Assert.Equal(1, CountOccurrences(code, "mkw_gqr3 = ctx->gqr[3];"));
}
[Fact]
public void StackAddressedAccessesKeepTheStackHelper()
{
// There is no GQR-value overload of the stack form, so those sites must
// keep reading the register through the context.
var code = Emit(Function("stack_psq",
new IrBinary("tmp_psq_addr", IrValue.Register("r1"), IrValue.Imm(16), "add"),
new IrCall("f1", "PPC_PsqL",
[IrValue.Register("tmp_psq_addr"), IrValue.Imm(0), IrValue.Imm(0)]),
new IrReturn(null)));
Assert.DoesNotContain("PPC_PsqLGqrInline", code, StringComparison.Ordinal);
Assert.DoesNotContain("mkw_gqr0", code, StringComparison.Ordinal);
}
[Fact]
public void ThreeGuardedSitesAreEnoughToVersionTheFunction()
{
var instructions = Enumerable.Range(0, 3)
.Select(_ => (IrInstruction)PsqLoad(5))
.Append(new IrReturn(null))
.ToArray();
var code = new CxxLinearCodeGenerator().Emit(0x80001000u,
new SsaTransformer().Convert(Function("versioned_three", instructions)),
new FunctionAbiClassification("versioned_three", ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
gqrEntryConstants: new Dictionary<string, uint> { ["gqr5"] = 0x00070007u },
gqrConstantsRequireRuntimeGuard: true);
Assert.Contains("template <bool gqr_entry_profile>", code, StringComparison.Ordinal);
Assert.Contains("versioned_three_gqr_impl<true>(ctx);", code, StringComparison.Ordinal);
Assert.Contains("versioned_three_gqr_impl<false>(ctx);", code, StringComparison.Ordinal);
Assert.DoesNotContain("const bool gqr_entry_5_00070007", code, StringComparison.Ordinal);
}
[Fact]
public void TwoGuardedSitesStayOnTheRuntimeGuard()
{
var instructions = Enumerable.Range(0, 2)
.Select(_ => (IrInstruction)PsqLoad(5))
.Append(new IrReturn(null))
.ToArray();
var code = new CxxLinearCodeGenerator().Emit(0x80001000u,
new SsaTransformer().Convert(Function("unversioned_two", instructions)),
new FunctionAbiClassification("unversioned_two", ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
gqrEntryConstants: new Dictionary<string, uint> { ["gqr5"] = 0x00070007u },
gqrConstantsRequireRuntimeGuard: true);
Assert.DoesNotContain("_gqr_impl", code, StringComparison.Ordinal);
Assert.Contains("const bool gqr_entry_5_00070007", code, StringComparison.Ordinal);
}
private static int CountOccurrences(string text, string value)
{
var count = 0;
var index = text.IndexOf(value, StringComparison.Ordinal);
while (index >= 0)
{
++count;
index = text.IndexOf(value, index + value.Length, StringComparison.Ordinal);
}
return count;
}
}
@@ -0,0 +1,120 @@
using Translator.Core.Analysis;
using Translator.Core.Ir;
namespace Translator.Tests;
public sealed class GuestAbiContractAnalyzerTests
{
[Fact]
public void TracksResolvedMemoryAddressesSourcesAndDestinations()
{
var function = new IrFunction("resolved_contract", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrResolveGuestMemoryRange("range", IrValue.Register("r3"), 0, 64, true, true),
new IrResolvedLoad("r4", "range", new IrAddress("r3", 0), 0, 4),
new IrResolvedStore("range", new IrAddress("r3", 4), 4, IrValue.Register("r5"), 4),
new IrResolvedPsqLoad("f1", "range", IrValue.Register("r6"), 8, 0, 0, 0),
new IrResolvedPsqStore("range", IrValue.Register("r8"), 24, IrValue.Register("f2"), 0, 0, 0),
new IrResolvedLoadPair("r9", "f3", "range", new IrAddress("r10", 0), new IrAddress("r10", 4), 32, 4),
new IrResolvedStorePair("range", new IrAddress("r11", 0), new IrAddress("r11", 4), 40,
IrValue.Register("r12"), IrValue.Register("f4"), 4),
new IrReturn(null)
})
});
var contract = GuestAbiContractAnalyzer.Analyze(function);
var expectedGprReads = (1u << 3) | (1u << 5) | (1u << 6) |
(1u << 8) | (1u << 10) | (1u << 11) | (1u << 12);
var expectedGprWrites = (1u << 4) | (1u << 9);
var expectedFprReads = (1u << 2) | (1u << 4);
var expectedFprWrites = (1u << 1) | (1u << 3);
Assert.Equal(expectedGprReads, contract.GprReadBeforeWriteMask);
Assert.Equal(expectedGprWrites, contract.GprPossibleWriteMask);
Assert.Equal(expectedFprReads, contract.FprReadBeforeWriteMask);
Assert.Equal(expectedFprWrites, contract.FprPossibleWriteMask);
}
[Fact]
public void TracksPreciseHiddenStringAndCrHelperEffects()
{
var function = new IrFunction("helper_contract", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall(string.Empty, "PPC_Stswi", new[]
{
IrValue.Imm(30), IrValue.Register("r3"), IrValue.Imm(12)
}),
new IrCall(string.Empty, "PPC_Lswi", new[]
{
IrValue.Imm(4), IrValue.Register("r3"), IrValue.Imm(8)
}),
new IrCall("cr", "PPC_CrLogical", new[]
{
IrValue.Imm(0), IrValue.Imm(12), IrValue.Imm(4), IrValue.Imm(28)
}),
new IrReturn(null)
})
});
var contract = GuestAbiContractAnalyzer.Analyze(function);
Assert.Equal((1u << 3) | (1u << 30) | (1u << 31) | 1u, contract.GprReadBeforeWriteMask);
Assert.Equal((1u << 4) | (1u << 5), contract.GprPossibleWriteMask);
Assert.Equal(byte.MaxValue, contract.CrReadBeforeWriteMask);
Assert.Equal((byte)(1 << 3), contract.CrPossibleWriteMask);
Assert.False(contract.HasFullSynchronizationFence);
}
[Fact]
public void UnknownHelperIsACompleteContextBoundary()
{
var function = new IrFunction("unknown_helper", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall(string.Empty, "PPC_NewUncataloguedHelper", Array.Empty<IrValue>()),
new IrReturn(null)
})
});
var contract = GuestAbiContractAnalyzer.Analyze(function);
Assert.True(contract.HasFullSynchronizationFence);
Assert.Equal(uint.MaxValue, contract.GprReadBeforeWriteMask);
Assert.Equal(uint.MaxValue, contract.GprPossibleWriteMask);
Assert.Equal(uint.MaxValue, contract.FprReadBeforeWriteMask);
Assert.Equal(uint.MaxValue, contract.FprPossibleWriteMask);
Assert.Equal(byte.MaxValue, contract.CrReadBeforeWriteMask);
Assert.Equal(byte.MaxValue, contract.CrPossibleWriteMask);
Assert.True(contract.ReadsXerBeforeWrite);
Assert.True(contract.MayWriteXer);
Assert.True(contract.ReadsCtrBeforeWrite);
Assert.True(contract.MayWriteCtr);
Assert.True(contract.ReadsLrBeforeWrite);
Assert.True(contract.MayWriteLr);
}
[Fact]
public void TracksSpecialRegistersEmbeddedInRawBranchConditions()
{
var function = new IrFunction("raw_branch", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrBranch("raw", "taken", "fallthrough",
"((ctx->ctr != 0) && GetCRBit(ctx, 2, 1))")
}),
new IrBasicBlock("taken", new IrInstruction[] { new IrReturn(null) }),
new IrBasicBlock("fallthrough", new IrInstruction[] { new IrReturn(null) })
});
var contract = GuestAbiContractAnalyzer.Analyze(function);
Assert.True(contract.ReadsCtrBeforeWrite);
Assert.Equal(byte.MaxValue, contract.CrReadBeforeWriteMask);
}
}
@@ -0,0 +1,111 @@
using Translator.Core.Analysis;
using Translator.Core.Ir;
using Xunit;
namespace Translator.Tests;
public sealed class GuestAbiInterproceduralAnalyzerTests
{
[Fact]
public void CallerDefinitionSuppressesCalleeReadAtFunctionEntry()
{
const uint caller = 0x80001000u;
const uint callee = 0x80002000u;
var functions = new Dictionary<uint, IrFunction>
{
[caller] = Function("caller",
new IrAssign("r3", IrValue.Imm(7)),
new IrCall("lr", "0x80002000", Array.Empty<IrValue>()),
new IrReturn(null)),
[callee] = Function("callee",
new IrAssign("r4", IrValue.Register("r3")),
new IrReturn(null))
};
var result = GuestAbiInterproceduralAnalyzer.Analyze(functions);
Assert.Equal(0u, result.Contracts[caller].GprReadBeforeWriteMask & (1u << 3));
Assert.NotEqual(0u, result.Contracts[caller].GprPossibleWriteMask & ((1u << 3) | (1u << 4)));
Assert.Equal(1u << 3, result.Contracts[callee].GprReadBeforeWriteMask);
}
[Fact]
public void KnownDirectCallIgnoresSyntacticAbiArgumentsNotReadByCallee()
{
const uint caller = 0x80001000u;
const uint callee = 0x80002000u;
var functions = new Dictionary<uint, IrFunction>
{
[caller] = Function("caller",
new IrCall("lr", "0x80002000", new[]
{
IrValue.Register("r3"),
IrValue.Register("r10"),
IrValue.Register("f1"),
IrValue.Register("f13")
}),
new IrReturn(null)),
[callee] = Function("callee",
new IrAssign("r4", IrValue.Register("r3")),
new IrReturn(null))
};
var result = GuestAbiInterproceduralAnalyzer.Analyze(functions);
var callerContract = result.Contracts[caller];
Assert.Equal(1u << 3, callerContract.GprReadBeforeWriteMask);
Assert.Equal(0u, callerContract.FprReadBeforeWriteMask);
}
[Fact]
public void RecursiveFunctionsFormOneComponentAndReachFixedPoint()
{
const uint first = 0x80001000u;
const uint second = 0x80002000u;
var functions = new Dictionary<uint, IrFunction>
{
[first] = Function("first",
new IrAssign("r5", IrValue.Register("r3")),
new IrCall("lr", "0x80002000", Array.Empty<IrValue>()),
new IrReturn(null)),
[second] = Function("second",
new IrAssign("r6", IrValue.Register("r4")),
new IrCall("lr", "0x80001000", Array.Empty<IrValue>()),
new IrReturn(null))
};
var result = GuestAbiInterproceduralAnalyzer.Analyze(functions);
Assert.Contains(result.StronglyConnectedComponents,
component => component.SequenceEqual(new[] { first, second }));
Assert.Equal((1u << 3) | (1u << 4),
result.Contracts[first].GprReadBeforeWriteMask & ((1u << 3) | (1u << 4)));
Assert.Equal((1u << 3) | (1u << 4),
result.Contracts[second].GprReadBeforeWriteMask & ((1u << 3) | (1u << 4)));
}
[Fact]
public void DeepCallGraphDoesNotConsumeTheNativeStack()
{
const int count = 20_000;
const uint start = 0x80000000u;
var functions = new Dictionary<uint, IrFunction>(count);
for (var index = 0; index < count; ++index)
{
var address = start + (uint)(index * 4);
functions[address] = index + 1 == count
? Function($"f{index}", new IrReturn(null))
: Function($"f{index}",
new IrCall("lr", $"0x{address + 4:X8}", Array.Empty<IrValue>()),
new IrReturn(null));
}
var result = GuestAbiInterproceduralAnalyzer.Analyze(functions);
Assert.Equal(count, result.StronglyConnectedComponents.Count);
Assert.All(result.StronglyConnectedComponents, static component => Assert.Single(component));
}
private static IrFunction Function(string name, params IrInstruction[] instructions) =>
new(name, "entry", new[] { new IrBasicBlock("entry", instructions) });
}
@@ -0,0 +1,636 @@
using System.Linq;
using System.Collections.Generic;
using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis.Ssa;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
public sealed class GuestMemoryRangeLoweringTests
{
[Fact]
public void KeepsNonOverlappingReadAndWriteRangesSeparate()
{
var lowered = Lower(
new IrLoad("r4", new IrAddress("r3", 12), 4),
new IrLoad("r5", new IrAddress("r3", 16), 4),
new IrStore(new IrAddress("r3", 20), IrValue.Register("r6"), 4),
new IrStore(new IrAddress("r3", 24), IrValue.Register("r7"), 4));
var resolves = lowered.OfType<IrResolveGuestMemoryRange>().ToArray();
Assert.Equal(2, resolves.Length);
Assert.Contains(resolves, resolve => resolve.NeedsReadAccess && !resolve.NeedsWriteAccess);
Assert.Contains(resolves, resolve => !resolve.NeedsReadAccess && resolve.NeedsWriteAccess);
Assert.Single(lowered.OfType<IrResolvedLoadPair>());
Assert.Single(lowered.OfType<IrResolvedStorePair>());
}
[Fact]
public void DoesNotSplitOverlappingReadAndWriteAliasesIntoIndependentRanges()
{
var lowered = Lower(
new IrLoad("r4", new IrAddress("r3", 0), 8),
new IrLoad("r5", new IrAddress("r3", 8), 8),
new IrStore(new IrAddress("r3", 4), IrValue.Register("r6"), 8),
new IrStore(new IrAddress("r3", 12), IrValue.Register("r7"), 8));
Assert.Empty(lowered.OfType<IrResolveGuestMemoryRange>());
Assert.Equal(2, lowered.OfType<IrLoad>().Count());
Assert.Equal(2, lowered.OfType<IrStore>().Count());
}
[Fact]
public void CanonicalizesReusedEffectiveAddressCalculations()
{
var lowered = Lower(
new IrBinary("ea0", IrValue.Register("r3"), IrValue.Imm(16), "add"),
new IrLoad("r4", new IrAddress("ea0", 0), 4),
new IrBinary("ea1", IrValue.Register("r3"), IrValue.Imm(20), "add"),
new IrLoad("r5", new IrAddress("ea1", 0), 4),
new IrLoad("r6", new IrAddress("r3", 24), 4));
var resolve = Assert.Single(lowered.OfType<IrResolveGuestMemoryRange>());
Assert.Equal("r3", resolve.Base.RegisterName);
Assert.Equal(16, resolve.MinOffset);
Assert.Equal(12, resolve.Length);
}
[Fact]
public void DoesNotRetainRangeAcrossCallBoundary()
{
var lowered = Lower(
new IrLoad("r4", new IrAddress("r3", 0), 4),
new IrCall("", "func_80001000", []),
new IrLoad("r5", new IrAddress("r3", 4), 4));
Assert.Empty(lowered.OfType<IrResolveGuestMemoryRange>());
Assert.Equal(2, lowered.OfType<IrLoad>().Count());
}
[Fact]
public void ReusesLiveInRangeAcrossCfgInsideCallFreeSuffixEpoch()
{
var function = new IrFunction("call_suffix", "entry",
[
new IrBasicBlock("entry",
[
new IrCall("", "func_80001000", []),
new IrJump("body")
]),
new IrBasicBlock("body",
[
new IrLoad("r4", new IrAddress("r3_0", 0), 4),
new IrJump("tail")
]),
new IrBasicBlock("tail",
[
new IrLoad("r5", new IrAddress("r3_0", 4), 4),
new IrReturn(null)
])
]);
var lowered = GuestMemoryRangeLowering.Lower(function, minimumAccesses: 2);
Assert.DoesNotContain(lowered.Blocks[0].Instructions,
instruction => instruction is IrResolveGuestMemoryRange);
Assert.IsType<IrResolveGuestMemoryRange>(lowered.Blocks[1].Instructions[0]);
Assert.IsType<IrResolvedLoad>(lowered.Blocks[1].Instructions[1]);
Assert.Equal(2, lowered.Blocks.SelectMany(static block => block.Instructions)
.OfType<IrResolvedLoad>().Count());
}
[Fact]
public void ReusesRangeAcrossBranchesWhenAnActualAccessDominatesConsumers()
{
var function = new IrFunction("branch", "entry",
[
new IrBasicBlock("entry",
[
new IrLoad("r4", new IrAddress("r3_0", 0), 4),
new IrBranch("ne", "left", "right")
]),
new IrBasicBlock("left",
[
new IrLoad("r5", new IrAddress("r3_0", 4), 4),
new IrReturn(null)
]),
new IrBasicBlock("right",
[
new IrLoad("r6", new IrAddress("r3_0", 8), 4),
new IrReturn(null)
])
]);
var lowered = GuestMemoryRangeLowering.Lower(function, minimumAccesses: 2);
Assert.IsType<IrResolveGuestMemoryRange>(lowered.Blocks[0].Instructions[0]);
Assert.Equal(3, lowered.Blocks.SelectMany(static block => block.Instructions)
.OfType<IrResolvedLoad>().Count());
}
[Fact]
public void CallOnLoopBackedgePreventsLoopCarriedHostPointer()
{
var function = new IrFunction("loop_boundary", "entry",
[
new IrBasicBlock("entry", [new IrJump("loop")]),
new IrBasicBlock("loop",
[
new IrLoad("r4", new IrAddress("r3_0", 0), 4),
new IrCall("", "func_80001000", []),
new IrLoad("r5", new IrAddress("r3_0", 4), 4),
new IrJump("loop")
])
]);
var lowered = GuestMemoryRangeLowering.Lower(function, minimumAccesses: 2);
Assert.Empty(lowered.Blocks.SelectMany(static block => block.Instructions)
.OfType<IrResolveGuestMemoryRange>());
}
[Fact]
public void ArchitecturalRegisterRedefinitionOnOnePathPreventsReuseAtJoin()
{
var function = new IrFunction("base_redefinition", "entry",
[
new IrBasicBlock("entry",
[
new IrLoad("r4_1", new IrAddress("r3_0", 0), 4),
new IrBranch("ne", "stable", "changed")
]),
new IrBasicBlock("stable", [new IrJump("join")]),
new IrBasicBlock("changed",
[
new IrAssign("r3_1", IrValue.Register("r5_0")),
new IrJump("join")
]),
new IrBasicBlock("join",
[
new IrLoad("r6_1", new IrAddress("r3_0", 4), 4),
new IrReturn(null)
])
]);
var lowered = GuestMemoryRangeLowering.Lower(function, minimumAccesses: 2);
Assert.Empty(lowered.Blocks.SelectMany(static block => block.Instructions)
.OfType<IrResolveGuestMemoryRange>());
}
[Fact]
public void BaseRedefinitionAfterLoopConsumerPreventsReuseOnNextIteration()
{
var function = new IrFunction("loop_redefinition", "entry",
[
new IrBasicBlock("entry",
[
new IrLoad("r4_1", new IrAddress("r3_0", 0), 4),
new IrJump("loop")
]),
new IrBasicBlock("loop",
[
new IrLoad("r5_1", new IrAddress("r3_0", 4), 4),
new IrAssign("r3_1", IrValue.Register("r6_0")),
new IrJump("loop")
])
]);
var lowered = GuestMemoryRangeLowering.Lower(function, minimumAccesses: 2);
Assert.Empty(lowered.Blocks.SelectMany(static block => block.Instructions)
.OfType<IrResolveGuestMemoryRange>());
}
[Fact]
public void CatalogHiddenGprWriteEndsEpochEvenForPpcNamedHelper()
{
var lowered = Lower(
new IrLoad("r4", new IrAddress("r3", 0), 4),
new IrCall("", "PPC_Lswi", [IrValue.Imm(4), IrValue.Register("r5"), IrValue.Imm(4)]),
new IrLoad("r5", new IrAddress("r3", 4), 4));
Assert.Empty(lowered.OfType<IrResolveGuestMemoryRange>());
}
[Fact]
public void OrdinaryCxxPipelineEnablesEightAccessScalarSuffixAfterCall()
{
var instructions = new List<IrInstruction>
{
new IrCall("", "func_80002000", [])
};
for (var index = 0; index < 8; index++)
instructions.Add(new IrLoad($"r{index + 4}", new IrAddress("r3", index * 4), 4));
instructions.Add(new IrReturn(null));
var function = new IrFunction("ordinary_epoch", "entry",
[new IrBasicBlock("entry", instructions)]);
var types = new RepresentationEnvironment(Enumerable.Range(0, 12)
.ToDictionary(index => $"r{index}", _ => ValueRepresentation.UInt32));
var code = new CxxLinearCodeGenerator().Emit(
0x80001000,
new SsaResult(function, IrCfg.Build(function)),
new FunctionAbiClassification("ordinary_epoch", ValueRepresentation.UInt32),
types);
Assert.Contains("MemoryInline::ResolveRangeHost", code, System.StringComparison.Ordinal);
Assert.Equal(8, System.Text.RegularExpressions.Regex.Matches(code, "ReadResolved32").Count);
}
[Theory]
[InlineData("Memory_RemapGuestPages")]
[InlineData("Fiber_YieldToScheduler")]
public void RemapAndSchedulerCallsAlwaysSplitMemoryEpochs(string target)
{
var function = new IrFunction("boundary", "entry",
[
new IrBasicBlock("entry",
[
new IrLoad("r4", new IrAddress("r3_0", 0), 4),
new IrCall("", target, []),
new IrJump("tail")
]),
new IrBasicBlock("tail",
[
new IrLoad("r5", new IrAddress("r3_0", 4), 4),
new IrReturn(null)
])
]);
var lowered = GuestMemoryRangeLowering.Lower(function, minimumAccesses: 2);
Assert.Empty(lowered.Blocks.SelectMany(static block => block.Instructions)
.OfType<IrResolveGuestMemoryRange>());
Assert.Equal(2, lowered.Blocks.SelectMany(static block => block.Instructions)
.OfType<IrLoad>().Count());
}
[Fact]
public void IndirectCallAlwaysSplitsMemoryEpochs()
{
var function = new IrFunction("indirect_boundary", "entry",
[
new IrBasicBlock("entry",
[
new IrLoad("r4", new IrAddress("r3_0", 0), 4),
new IrIndirectCall("", IrValue.Register("ctr_0"), []),
new IrJump("tail")
]),
new IrBasicBlock("tail",
[
new IrLoad("r5", new IrAddress("r3_0", 4), 4),
new IrReturn(null)
])
]);
var lowered = GuestMemoryRangeLowering.Lower(function, minimumAccesses: 2);
Assert.Empty(lowered.Blocks.SelectMany(static block => block.Instructions)
.OfType<IrResolveGuestMemoryRange>());
}
[Fact]
public void KeepsSeparateRangesForBasesLoadedThroughAFunctionLiveInRange()
{
// Regression: the affine tracker must recognize IrResolvedLoad results (from the
// function-level live-in pass) as fresh r3 definitions, or every store after the
// first redirects through the first pointer and the minimap shadow panes lose their scale.
var lowered = Lower(
new IrLoad("r3", new IrAddress("r28", 440), 4),
new IrStore(new IrAddress("r3", 68), IrValue.Register("f0"), 4),
new IrStore(new IrAddress("r3", 72), IrValue.Register("f0"), 4),
new IrLoad("r3", new IrAddress("r28", 444), 4),
new IrStore(new IrAddress("r3", 68), IrValue.Register("f1"), 4),
new IrStore(new IrAddress("r3", 72), IrValue.Register("f1"), 4),
new IrLoad("r3", new IrAddress("r28", 448), 4),
new IrStore(new IrAddress("r3", 68), IrValue.Register("f2"), 4),
new IrStore(new IrAddress("r3", 72), IrValue.Register("f2"), 4));
// One write range per pane, each spanning only that pane's scale pair.
var writeRanges = lowered.OfType<IrResolveGuestMemoryRange>()
.Where(static range => range.NeedsWriteAccess)
.ToArray();
Assert.Equal(3, writeRanges.Length);
Assert.All(writeRanges, static range => Assert.Equal(8, range.Length));
}
[Fact]
public void TracksAffineBaseUpdateBetweenAccesses()
{
// lwzu-style walk: the affine tracker folds the constant base update into the
// second access's offset, so both loads share one range anchored at the original base.
var lowered = Lower(
new IrLoad("r4", new IrAddress("r3", 0), 4),
new IrBinary("r3", IrValue.Register("r3"), IrValue.Imm(4), "add"),
new IrLoad("r5", new IrAddress("r3", 0), 4));
var resolve = Assert.Single(lowered.OfType<IrResolveGuestMemoryRange>());
Assert.Equal("r3", resolve.Base.RegisterName);
Assert.Equal(0, resolve.MinOffset);
Assert.Equal(8, resolve.Length);
}
[Fact]
public void TracksAffineBaseUpdateThroughTemporaryReassignment()
{
// psq_stu emission shape: the effective address lives in a temp and is
// then copied back into the architectural base.
var lowered = Lower(
new IrStore(new IrAddress("r10", 8), IrValue.Register("r6"), 8),
new IrBinary("r10_psq_ea_0", IrValue.Register("r10"), IrValue.Imm(8), "add"),
new IrAssign("r10", IrValue.Register("r10_psq_ea_0")),
new IrStore(new IrAddress("r10", 8), IrValue.Register("r7"), 8),
new IrBinary("r10_psq_ea_1", IrValue.Register("r10"), IrValue.Imm(8), "add"),
new IrAssign("r10", IrValue.Register("r10_psq_ea_1")),
new IrStore(new IrAddress("r10", 8), IrValue.Register("r8"), 8));
var resolve = Assert.Single(lowered.OfType<IrResolveGuestMemoryRange>());
Assert.Equal("r10", resolve.Base.RegisterName);
Assert.Equal(8, resolve.MinOffset);
Assert.Equal(24, resolve.Length);
var stores = lowered.OfType<IrResolvedStore>().ToArray();
Assert.Equal(3, stores.Length);
Assert.Equal(0, stores[0].RangeOffset);
Assert.Equal(8, stores[1].RangeOffset);
Assert.Equal(16, stores[2].RangeOffset);
}
[Fact]
public void RejectsGroupWhenBaseChangesUnpredictablyBetweenAccesses()
{
// A base redefinition the tracker can't follow starts a new generation, so accesses
// through different runtime values never share one range proof.
var lowered = Lower(
new IrLoad("r4", new IrAddress("r3", 0), 4),
new IrLoad("r3", new IrAddress("r4", 0), 4),
new IrLoad("r5", new IrAddress("r3", 0), 4));
Assert.Empty(lowered.OfType<IrResolveGuestMemoryRange>());
}
[Fact]
public void RejectsCrossPageSizedRanges()
{
var lowered = Lower(
new IrLoad("r4", new IrAddress("r3", 0), 4),
new IrLoad("r5", new IrAddress("r3", 5000), 4));
Assert.Empty(lowered.OfType<IrResolveGuestMemoryRange>());
}
[Fact]
public void GroupsAdjacentPairedSingleLoadsAndPreservesKnownGqr()
{
var lowered = Lower(
new IrCall("f1", "PPC_PsqLKnown_00050000", [IrValue.Register("r3"), IrValue.Imm(0), IrValue.Imm(2)]),
new IrBinary("ea", IrValue.Register("r3"), IrValue.Imm(4), "add"),
new IrCall("f2", "PPC_PsqLKnown_00050000", [IrValue.Register("ea"), IrValue.Imm(1), IrValue.Imm(2)]),
new IrCall("f3", "PPC_PsqLKnown_00050000", [IrValue.Register("r3"), IrValue.Imm(1), IrValue.Imm(2)]));
var resolve = Assert.Single(lowered.OfType<IrResolveGuestMemoryRange>());
Assert.True(resolve.NeedsReadAccess);
Assert.False(resolve.NeedsWriteAccess);
Assert.Equal(8, resolve.Length);
var loads = lowered.OfType<IrResolvedPsqLoad>().ToArray();
Assert.Equal(3, loads.Length);
Assert.Equal(0x00050000u, loads[0].KnownGqr);
Assert.Equal(4, loads[1].RangeOffset);
}
[Fact]
public void CombinesConsecutiveAdjacentHalfwordLoads()
{
var lowered = Lower(
new IrLoad("r4", new IrAddress("r3", 8), 2),
new IrLoad("r5", new IrAddress("r3", 10), 2));
var pair = Assert.Single(lowered.OfType<IrResolvedLoadPair>());
Assert.Equal(2, pair.ElementSizeBytes);
Assert.Equal(0, pair.RangeOffset);
Assert.Equal("r4", pair.FirstDestination);
Assert.Equal("r5", pair.SecondDestination);
}
[Fact]
public void CombinesConsecutiveAdjacentWordStores()
{
var lowered = Lower(
new IrStore(new IrAddress("r3", 0), IrValue.Register("r4"), 4),
new IrStore(new IrAddress("r3", 4), IrValue.Register("r5"), 4));
var pair = Assert.Single(lowered.OfType<IrResolvedStorePair>());
Assert.Equal(4, pair.ElementSizeBytes);
Assert.Equal(0, pair.RangeOffset);
}
[Fact]
public void CombinesAdjacentStoresAcrossPureEffectiveAddressCalculation()
{
var lowered = Lower(
new IrStore(new IrAddress("r3", 0), IrValue.Register("r4"), 4),
new IrBinary("ea_next", IrValue.Register("r3"), IrValue.Imm(4), "add"),
new IrStore(new IrAddress("ea_next", 0), IrValue.Register("r5"), 4));
var pair = Assert.Single(lowered.OfType<IrResolvedStorePair>());
Assert.False(pair.Descending);
Assert.Contains(lowered, instruction => instruction is IrBinary { Destination: "ea_next" });
}
[Fact]
public void CombinesDescendingAdjacentLoadsAndPreservesDirection()
{
var lowered = Lower(
new IrLoad("r4", new IrAddress("r3", 12), 4),
new IrLoad("r5", new IrAddress("r3", 8), 4));
var pair = Assert.Single(lowered.OfType<IrResolvedLoadPair>());
Assert.True(pair.Descending);
Assert.Equal(0, pair.RangeOffset);
Assert.Equal("r4", pair.FirstDestination);
Assert.Equal("r5", pair.SecondDestination);
}
[Fact]
public void DoesNotInferRuntimeCachingFromCfgCycleAlone()
{
var function = new IrFunction("loop", "entry",
[
new IrBasicBlock("entry", [new IrJump("loop")]),
new IrBasicBlock("loop",
[
new IrLoad("r4", new IrAddress("r3", 0), 4),
new IrLoad("r5", new IrAddress("r3", 4), 4),
new IrLoad("r6", new IrAddress("r3", 8), 4),
new IrJump("loop")
])
]);
var resolve = Assert.Single(GuestMemoryRangeLowering.Lower(function, minimumAccesses: 2).Blocks
.SelectMany(static block => block.Instructions).OfType<IrResolveGuestMemoryRange>());
}
[Fact]
public void RejectsSmallStraightLineGroupAsUnprofitable()
{
var function = new IrFunction("straight", "entry",
[
new IrBasicBlock("entry",
[
new IrLoad("r4", new IrAddress("r3", 0), 4),
new IrLoad("r5", new IrAddress("r3", 4), 4),
new IrReturn(null)
])
]);
Assert.Empty(GuestMemoryRangeLowering.Lower(function).Blocks[0].Instructions
.OfType<IrResolveGuestMemoryRange>());
}
[Fact]
public void RetainsLiveInRangeAcrossDominatedCfgBlocks()
{
var function = new IrFunction("cfg", "entry",
[
new IrBasicBlock("entry",
[
new IrLoad("r4", new IrAddress("r3_0", 0), 4),
new IrJump("body")
]),
new IrBasicBlock("body",
[
new IrLoad("r5", new IrAddress("r3_0", 4), 4),
new IrReturn(null)
])
]);
var lowered = GuestMemoryRangeLowering.Lower(function, minimumAccesses: 2);
Assert.Single(lowered.Blocks.SelectMany(static block => block.Instructions)
.OfType<IrResolveGuestMemoryRange>());
Assert.Equal(2, lowered.Blocks.SelectMany(static block => block.Instructions)
.OfType<IrResolvedLoad>().Count());
}
[Fact]
public void PlacesLoopCarriedPhiRangeAfterAuthoritativeDefinition()
{
var function = new IrFunction("phi_loop", "entry",
[
new IrBasicBlock("entry", [new IrJump("loop")]),
new IrBasicBlock("loop",
[
new IrPhi("r3_1", new Dictionary<string, string>
{
["entry"] = "r3_0",
["loop"] = "r3_2"
}),
new IrLoad("r4", new IrAddress("r3_1", 0), 4),
new IrLoad("r5", new IrAddress("r3_1", 4), 4),
new IrBinary("r3_2", IrValue.Register("r3_1"), IrValue.Imm(8), "add"),
new IrJump("loop")
])
]);
var loop = GuestMemoryRangeLowering.Lower(function, minimumAccesses: 2).Blocks[1].Instructions;
Assert.IsType<IrPhi>(loop[0]);
Assert.IsType<IrResolveGuestMemoryRange>(loop[1]);
Assert.Single(loop.OfType<IrResolvedLoadPair>());
}
[Fact]
public void KeepsProvenGatherPipeStoresOutOfResolvedRanges()
{
// Gather-pipe writes (`lis rX, 0xCC01; stX rY, -0x8000(rX)`) must not get hoisted into a
// resolved range: ResolveRangeHost returns nullptr for that address at runtime, which
// would drop the write into the cold path instead of the emitter's direct GX_HLE_FIFO_Write*.
var instructions = new List<IrInstruction>
{
new IrAssign("r10", IrValue.Imm(unchecked((int)0xCC010000)))
};
for (var index = 0; index < 13; index++)
instructions.Add(new IrStore(new IrAddress("r10", -32768), IrValue.Register($"r{index % 8}"), 4));
instructions.Add(new IrReturn(null));
var function = new IrFunction("gather_pipe", "entry",
[new IrBasicBlock("entry", instructions)]);
var lowered = GuestMemoryRangeLowering.Lower(function, minimumAccesses: 2);
Assert.Empty(lowered.Blocks.SelectMany(static block => block.Instructions)
.OfType<IrResolveGuestMemoryRange>());
Assert.Equal(13, lowered.Blocks.SelectMany(static block => block.Instructions)
.OfType<IrStore>().Count());
}
[Fact]
public void KeepsProvenHardwareRegisterLoadsOutOfResolvedRanges()
{
var instructions = new List<IrInstruction>
{
new IrAssign("r10", IrValue.Imm(unchecked((int)0xCD000000)))
};
for (var index = 0; index < 8; index++)
instructions.Add(new IrLoad($"r{index + 3}", new IrAddress("r10", index * 4), 4));
instructions.Add(new IrReturn(null));
var function = new IrFunction("hardware_reads", "entry",
[new IrBasicBlock("entry", instructions)]);
var lowered = GuestMemoryRangeLowering.Lower(function, minimumAccesses: 2);
Assert.Empty(lowered.Blocks.SelectMany(static block => block.Instructions)
.OfType<IrResolveGuestMemoryRange>());
Assert.Equal(8, lowered.Blocks.SelectMany(static block => block.Instructions)
.OfType<IrLoad>().Count());
}
[Fact]
public void StillHoistsStoresThroughAProvenMainMemoryBase()
{
// The exclusion is keyed on the hardware window only: an ordinary
// absolute MEM1 base must keep its range proof.
var instructions = new List<IrInstruction>
{
new IrAssign("r10", IrValue.Imm(unchecked((int)0x80300000)))
};
for (var index = 0; index < 13; index++)
instructions.Add(new IrStore(new IrAddress("r10", index * 4), IrValue.Register($"r{index % 8}"), 4));
instructions.Add(new IrReturn(null));
var function = new IrFunction("main_memory", "entry",
[new IrBasicBlock("entry", instructions)]);
var lowered = GuestMemoryRangeLowering.Lower(function, minimumAccesses: 2);
Assert.NotEmpty(lowered.Blocks.SelectMany(static block => block.Instructions)
.OfType<IrResolveGuestMemoryRange>());
Assert.Empty(lowered.Blocks.SelectMany(static block => block.Instructions)
.OfType<IrStore>());
}
[Fact]
public void EmitsDirectFifoWritesForProvenGatherPipeStores()
{
var instructions = new List<IrInstruction>
{
new IrAssign("r10", IrValue.Imm(unchecked((int)0xCC010000)))
};
for (var index = 0; index < 13; index++)
instructions.Add(new IrStore(new IrAddress("r10", -32768), IrValue.Register("r3"), 4));
instructions.Add(new IrReturn(null));
var function = new IrFunction("gather_pipe_emit", "entry",
[new IrBasicBlock("entry", instructions)]);
var types = new RepresentationEnvironment(Enumerable.Range(0, 12)
.ToDictionary(index => $"r{index}", _ => ValueRepresentation.UInt32));
var code = new CxxLinearCodeGenerator().Emit(
0x800605C0,
new SsaResult(function, IrCfg.Build(function)),
new FunctionAbiClassification("gather_pipe_emit", ValueRepresentation.UInt32),
types);
Assert.DoesNotContain("MemoryInline::ResolveRangeHost", code, System.StringComparison.Ordinal);
Assert.DoesNotContain("WriteResolved", code, System.StringComparison.Ordinal);
Assert.Contains("GX_HLE_FIFO_", code, System.StringComparison.Ordinal);
}
private static IrInstruction[] Lower(params IrInstruction[] instructions)
{
var loopInstructions = instructions.Append<IrInstruction>(new IrJump("entry")).ToArray();
var function = new IrFunction("test", "entry",
[new IrBasicBlock("entry", loopInstructions)]);
return GuestMemoryRangeLowering.Lower(function, minimumAccesses: 2).Blocks[0].Instructions.ToArray();
}
}
@@ -0,0 +1,29 @@
using System.Runtime.CompilerServices;
using Translator.Core.Analysis;
using Translator.Core.Loading;
namespace Translator.Tests;
/// <summary>
/// Installs the MKWii PAL save/restore thunk address ranges once for the whole test assembly,
/// since several suites translate hand-assembled fragments that call these thunks by address.
/// </summary>
internal static class GuestSaveRestoreThunkTestDefaults
{
[ModuleInitializer]
internal static void Install()
{
GuestSaveRestoreThunks.Current = GuestSaveRestoreThunks.FromFunctionMap(FunctionMap.Parse(
[
"800214f8 _save_fpr_23",
"8002150c _save_fpr_28",
"80021544 _rest_fpr_23",
"80021558 _rest_fpr_28",
"8002156c _save_gpr_14",
"800215a0 _save_gpr_27",
"800215b8 _rest_gpr_14",
"800215ec _rest_gpr_27"
],
"mkwii-pal-test-defaults"));
}
}
@@ -0,0 +1,259 @@
using Translator.Core.Analysis;
using Translator.Core.Ir;
using Xunit;
namespace Translator.Tests;
public sealed class GuestStateLivenessAnalyzerTests
{
[Fact]
public void IndirectControlFlowRequiresMaterializedContext()
{
var indirectCall = Function(
new IrIndirectCall("lr", IrValue.Register("ctr"), Array.Empty<IrValue>()),
new IrReturn(null));
var indirectJump = Function(new IrIndirectJump(IrValue.Register("ctr")));
var jumpTable = Function(new IrJumpTable("r3", new[]
{
new IrJumpTableCase(0x80001000u, "case_0")
}));
Assert.False(GuestStateLivenessAnalyzer.CanDeconstructWithoutContext(indirectCall));
Assert.False(GuestStateLivenessAnalyzer.CanDeconstructWithoutContext(indirectJump));
Assert.False(GuestStateLivenessAnalyzer.CanDeconstructWithoutContext(jumpTable));
}
[Fact]
public void DirectCallPassesOnlyCalleeInputsAndReturnsOnlyCallerLiveWrites()
{
const uint target = 0x80002000u;
var callee = Contract(
gprRead: (1u << 3) | (1u << 7),
gprWrite: (1u << 3) | (1u << 4) | (1u << 8));
var function = Function(
new IrCall("lr", "0x80002000", Array.Empty<IrValue>()),
new IrBinary("r9", IrValue.Register("r4"), IrValue.Imm(1), "add"),
new IrReturn(null));
var result = GuestStateLivenessAnalyzer.Analyze(
function,
new Dictionary<uint, GuestAbiContract> { [target] = callee },
GuestStateMask.Empty);
var call = Assert.Single(result.DirectCalls);
Assert.Equal((1u << 3) | (1u << 7), call.Inputs.Gpr);
Assert.Equal(1u << 4, call.Outputs.Gpr);
Assert.Equal(0u, call.Outputs.Gpr & ((1u << 3) | (1u << 8)));
}
[Fact]
public void CalleeWriteKillsTheIncomingVersion()
{
const uint target = 0x80002000u;
var callee = Contract(gprRead: 0, gprWrite: 1u << 6) with
{
GprDefiniteWriteMask = 1u << 6
};
var function = Function(
new IrCall("lr", "0x80002000", Array.Empty<IrValue>()),
new IrAssign("r8", IrValue.Register("r6")),
new IrReturn(null));
var result = GuestStateLivenessAnalyzer.Analyze(
function,
new Dictionary<uint, GuestAbiContract> { [target] = callee },
GuestStateMask.Empty);
Assert.Equal(0u, result.BlockLiveIn["entry"].Gpr & (1u << 6));
Assert.Equal(1u << 6, Assert.Single(result.DirectCalls).Outputs.Gpr);
}
[Fact]
public void ConditionalCalleeWriteDoesNotKillTheIncomingVersion()
{
const uint target = 0x80002000u;
var callee = Contract(gprRead: 0, gprWrite: 1u << 6);
var function = Function(
new IrCall("lr", "0x80002000", Array.Empty<IrValue>()),
new IrAssign("r8", IrValue.Register("r6")),
new IrReturn(null));
var result = GuestStateLivenessAnalyzer.Analyze(
function,
new Dictionary<uint, GuestAbiContract> { [target] = callee },
GuestStateMask.Empty);
Assert.Equal(1u << 6, result.BlockLiveIn["entry"].Gpr & (1u << 6));
Assert.Equal(1u << 6, Assert.Single(result.DirectCalls).Outputs.Gpr);
}
[Fact]
public void StateFreeConditionalOutputAlsoRequiresTheIncomingGpr()
{
var contract = Contract(gprRead: 1u << 3, gprWrite: (1u << 4) | (1u << 6)) with
{
GprDefiniteWriteMask = 1u << 4
};
var inputs = GuestStateLivenessAnalyzer.RequiredStateFreeGprInputs(
contract, (1u << 4) | (1u << 6));
Assert.Equal((1u << 3) | (1u << 6), inputs);
}
[Fact]
public void DirectCallOrdinalsRemainStableWhenUnrelatedInstructionsDiffer()
{
const uint target = 0x80002000u;
var function = Function(
new IrCall("lr", "0x80002000", Array.Empty<IrValue>()),
new IrAssign("r9", IrValue.Imm(1)),
new IrCall("lr", "0x80002000", Array.Empty<IrValue>()),
new IrReturn(null));
var calls = GuestStateLivenessAnalyzer.Analyze(
function,
new Dictionary<uint, GuestAbiContract> { [target] = Contract(1u << 3, 0) },
GuestStateMask.Empty).DirectCalls;
Assert.Equal(new[] { 0, 1 }, calls.Select(static call => call.CallOrdinal));
Assert.Equal(new[] { 0, 2 }, calls.Select(static call => call.InstructionIndex));
}
[Fact]
public void TailCallWithoutLinkDestinationKeepsIncomingLrLive()
{
const uint target = 0x80002000u;
var function = Function(
new IrCall(string.Empty, "0x80002000", Array.Empty<IrValue>()),
new IrReturn(null));
var result = GuestStateLivenessAnalyzer.Analyze(
function,
new Dictionary<uint, GuestAbiContract> { [target] = Contract(1u << 3, 0) },
GuestStateMask.Empty);
Assert.True(result.BlockLiveIn["entry"].Lr);
Assert.True(Assert.Single(result.DirectCalls).Inputs.Lr);
}
[Fact]
public void ImplicitSequentialBlockFallthroughPropagatesLaterInputsToEntry()
{
var function = new IrFunction("fallthrough", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrLoad("r3", new IrAddress("r3", 0), 4),
new IrTracePpc(0x80001000u, "lwz r3, 0(r3)", "0x80630000")
}),
new IrBasicBlock("next", new IrInstruction[]
{
new IrLoad("f0", new IrAddress("r2", -4), 4),
new IrReturn(null)
})
});
var result = GuestStateLivenessAnalyzer.Analyze(
function, new Dictionary<uint, GuestAbiContract>(), GuestStateMask.Empty);
Assert.Equal((1u << 2) | (1u << 3), result.BlockLiveIn["entry"].Gpr);
Assert.Equal(1u << 2, result.BlockLiveOut["entry"].Gpr);
}
[Fact]
public void TracksPairedFprCrXerFpscrAndGqrState()
{
const uint target = 0x80002000u;
var callee = Contract(gprRead: 0, gprWrite: 0) with
{
FprReadBeforeWriteMask = 1u << 2,
FprPossibleWriteMask = (1u << 1) | (1u << 2),
CrReadBeforeWriteMask = 1 << 3,
CrPossibleWriteMask = (1 << 2) | (1 << 3),
ReadsXerBeforeWrite = true,
MayWriteXer = true,
ReadsFpscrBeforeWrite = true,
MayWriteFpscr = true,
GqrReadBeforeWriteMask = 1 << 5,
GqrPossibleWriteMask = (1 << 4) | (1 << 5)
};
var function = Function(
new IrCall("lr", "0x80002000", Array.Empty<IrValue>()),
new IrAssign("f6", IrValue.Register("f1")),
new IrAssign("r3", IrValue.Register("cr2")),
new IrAssign("r4", IrValue.Register("fpscr")),
new IrAssign("r5", IrValue.Register("gqr4")),
new IrReturn(null));
var call = Assert.Single(GuestStateLivenessAnalyzer.Analyze(
function,
new Dictionary<uint, GuestAbiContract> { [target] = callee },
GuestStateMask.Empty).DirectCalls);
Assert.Equal(1u << 2, call.Inputs.Fpr);
Assert.Equal(1u << 1, call.Outputs.Fpr);
Assert.Equal(1 << 3, call.Inputs.Cr);
Assert.Equal(1 << 2, call.Outputs.Cr);
Assert.True(call.Inputs.Xer);
Assert.False(call.Outputs.Xer);
Assert.True(call.Inputs.Fpscr);
Assert.True(call.Outputs.Fpscr);
Assert.Equal(1 << 5, call.Inputs.Gqr);
Assert.Equal(1 << 4, call.Outputs.Gqr);
}
[Fact]
public void MaterializedContextExitKeepsNonReturnArchitecturalWritesLive()
{
const uint target = 0x80002000u;
var callee = Contract(gprRead: 0, gprWrite: 0) with
{
FprPossibleWriteMask = 0x1Fu,
FprReturnMask = 1u << 1,
CrPossibleWriteMask = 1,
MayWriteLr = true
};
var callerContract = Contract(gprRead: 0, gprWrite: 0) with
{
FprPossibleWriteMask = 0x1Fu,
FprReturnMask = 1u << 1,
CrPossibleWriteMask = 1,
MayWriteLr = true
};
var function = Function(
new IrCall("lr", "0x80002000", Array.Empty<IrValue>()),
new IrReturn(null));
var call = Assert.Single(GuestStateLivenessAnalyzer.Analyze(
function,
new Dictionary<uint, GuestAbiContract> { [target] = callee },
GuestStateLivenessAnalyzer.MaterializedContextExit(callerContract)).DirectCalls);
Assert.Equal(0x1Fu, call.Outputs.Fpr);
Assert.Equal(1, call.Outputs.Cr);
Assert.True(call.Outputs.Lr);
}
private static IrFunction Function(params IrInstruction[] instructions) =>
new("caller", "entry", new[] { new IrBasicBlock("entry", instructions) });
private static GuestAbiContract Contract(uint gprRead, uint gprWrite) =>
new(
gprRead,
gprWrite,
gprWrite & ((1u << 3) | (1u << 4)),
0,
0,
0,
0,
0,
false,
false,
false,
false,
false,
false,
GuestCallBoundaryFlags.None,
Array.Empty<uint>());
}
@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis.Ssa;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
public sealed class GuestThunkCodeGenTests
{
[Fact]
public void GprSaveAndRestoreUseTheSameInlineRangeShape()
{
var save = Emit(
"gpr_save",
0x8002156Cu,
new Dictionary<string, ValueRepresentation>
{
["r11"] = ValueRepresentation.UInt32,
["r14"] = ValueRepresentation.UInt32,
["r31"] = ValueRepresentation.UInt32
});
var restore = Emit(
"gpr_restore",
0x800215B8u,
new Dictionary<string, ValueRepresentation>
{
["r11"] = ValueRepresentation.UInt32,
["r14"] = ValueRepresentation.UInt32,
["r31"] = ValueRepresentation.UInt32
});
Assert.Contains(
"MemoryInline::FlatWrite32((r11 + -72), static_cast<uint32_t>(r14));",
save,
StringComparison.Ordinal);
Assert.Contains(
"r14 = MemoryInline::FlatRead32((r11 + -72));",
restore,
StringComparison.Ordinal);
Assert.DoesNotContain("InvokeDirectCpu<0x8002156Cu>(ctx);", save, StringComparison.Ordinal);
Assert.DoesNotContain("InvokeDirectCpu<0x800215B8u>(ctx);", restore, StringComparison.Ordinal);
}
[Fact]
public void FprSaveAndRestoreUseEightByteInlineHelpers()
{
var types = new Dictionary<string, ValueRepresentation>
{
["r11"] = ValueRepresentation.UInt32,
["f23"] = ValueRepresentation.Float64,
["f31"] = ValueRepresentation.Float64
};
var save = Emit("fpr_save", 0x800214F8u, types);
var restore = Emit("fpr_restore", 0x80021544u, types);
Assert.Contains(
"MemoryInline::FlatWriteFloat64((r11 + -72), f23.d);",
save,
StringComparison.Ordinal);
Assert.Contains(
"f23.d = MemoryInline::FlatReadFloat64((r11 + -72));",
restore,
StringComparison.Ordinal);
}
private static string Emit(
string name,
uint thunkAddress,
IReadOnlyDictionary<string, ValueRepresentation> typeMap)
{
var function = new IrFunction(
name,
"entry",
new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall(string.Empty, $"func_{thunkAddress:X8}", Array.Empty<IrValue>()),
new IrReturn(null)
})
});
var code = new CxxLinearCodeGenerator().Emit(
thunkAddress,
new SsaTransformer().Convert(function),
new FunctionAbiClassification(name, ValueRepresentation.Void),
new RepresentationEnvironment(typeMap));
return code;
}
}
@@ -0,0 +1,98 @@
using System.Buffers.Binary;
using Translator.Core.CodeGen;
using Translator.Core.Loading;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
public sealed class InferredGuestFunctionAbiProviderDeterminismTests
{
private const uint FunctionA = MemoryLayout.RamBase;
private const uint FunctionB = MemoryLayout.RamBase + 0x10;
[Fact]
public void MutuallyRecursiveFunctions_AreIndependentOfRootRequestOrder()
{
var forward = CreateProvider();
forward.Prewarm(new[] { FunctionA, FunctionB });
var forwardA = GetSnapshot(forward, FunctionA);
var forwardB = GetSnapshot(forward, FunctionB);
var reverse = CreateProvider();
reverse.Prewarm(new[] { FunctionB, FunctionA });
var reverseB = GetSnapshot(reverse, FunctionB);
var reverseA = GetSnapshot(reverse, FunctionA);
Assert.Equal(forwardA, reverseA);
Assert.Equal(forwardB, reverseB);
}
[Fact]
public void MutuallyRecursiveFunctions_AreDeterministicUnderConcurrentRequests()
{
var expectedProvider = CreateProvider();
expectedProvider.Prewarm(new[] { FunctionA, FunctionB });
var expected = new Dictionary<uint, AbiSnapshot>
{
[FunctionA] = GetSnapshot(expectedProvider, FunctionA),
[FunctionB] = GetSnapshot(expectedProvider, FunctionB)
};
var provider = CreateProvider();
var addresses = Enumerable.Range(0, 64)
.Select(index => index % 2 == 0 ? FunctionB : FunctionA)
.ToArray();
provider.Prewarm(addresses);
var actual = new AbiSnapshot[addresses.Length];
Parallel.For(0, addresses.Length, index =>
{
actual[index] = GetSnapshot(provider, addresses[index]);
});
for (var index = 0; index < addresses.Length; index++)
{
Assert.Equal(expected[addresses[index]], actual[index]);
}
}
private static InferredGuestFunctionAbiProvider CreateProvider()
{
var memory = new byte[0x18];
WriteWord(memory, 0x00, 0x38800000); // li r4, 0
WriteWord(memory, 0x04, LinkBranch(FunctionA + 4, FunctionB));
WriteWord(memory, 0x08, 0x4E800020); // blr
WriteWord(memory, 0x0C, 0x4E800020);
WriteWord(memory, 0x10, LinkBranch(FunctionB, FunctionA));
WriteWord(memory, 0x14, 0x4E800020); // blr
var range = AddressRange.FromStartAndSize(MemoryLayout.RamBase, (uint)memory.Length);
var image = new ProgramImage(memory, range, range, default, "abi-cycle-determinism");
return new InferredGuestFunctionAbiProvider(image);
}
private static AbiSnapshot GetSnapshot(InferredGuestFunctionAbiProvider provider, uint address)
{
Assert.True(provider.TryGetGuestFunctionAbi($"func_{address:X8}", out var abi));
return new AbiSnapshot(
string.Join(",", abi.ArgumentRegisters.Order(StringComparer.OrdinalIgnoreCase)),
string.Join(",", abi.ScalarFloatArgumentRegisters.Order(StringComparer.OrdinalIgnoreCase)),
abi.ReturnsPairedScalarFloat,
abi.WritesFloatReturnRegister,
abi.WritesGprReturnRegister,
abi.PreservesVolatileContext);
}
private static uint LinkBranch(uint from, uint to) =>
0x48000001u | ((to - from) & 0x03FFFFFCu);
private static void WriteWord(byte[] memory, int offset, uint value) =>
BinaryPrimitives.WriteUInt32BigEndian(memory.AsSpan(offset, 4), value);
private sealed record AbiSnapshot(
string ArgumentRegisters,
string ScalarFloatArgumentRegisters,
bool ReturnsPairedScalarFloat,
bool WritesFloatReturnRegister,
bool WritesGprReturnRegister,
bool PreservesVolatileContext);
}
@@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis.Ssa;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
/// <summary>
/// Coverage for the descriptor-driven arithmetic inline emitters, going through the public
/// C++ generator (not the private emitter directly) so lookup and output are tested together.
/// </summary>
public sealed class InlineArithmeticTableCodeGenTests
{
public static IEnumerable<object[]> PairedCases()
{
yield return Case("PPC_PsMerge00", "PPC_PsMerge00Inline");
yield return Case("PPC_PsMerge01", "PPC_PsMerge01Inline");
yield return Case("PPC_PsMerge10", "PPC_PsMerge10Inline");
yield return Case("PPC_PsMerge11", "PPC_PsMerge11Inline");
yield return Case("PPC_PsAdd", "PPC_PsAddInline");
yield return Case("PPC_PsAddNoNi", "PPC_PsAddNoNiInline");
yield return Case("PPC_PsSub", "PPC_PsSubInline");
yield return Case("PPC_PsSubNoNi", "PPC_PsSubNoNiInline");
yield return Case("PPC_PsDiv", "PPC_PsDivInline");
yield return Case("PPC_PsMul", "PPC_PsMulInline");
yield return Case("PPC_PsMulNoNi", "PPC_PsMulNoNiInline");
yield return Case("PPC_PsNeg", "PPC_PsNegInline");
yield return Case("PPC_PsAbs", "PPC_PsAbsInline");
yield return Case("PPC_PsMuls0", "PPC_PsMuls0Inline");
yield return Case("PPC_PsMuls1", "PPC_PsMuls1Inline");
yield return Case("PPC_PsMadd", "PPC_PsMaddInline");
yield return Case("PPC_PsMaddNoNi", "PPC_PsMaddNoNiInline");
yield return Case("PPC_PsMsub", "PPC_PsMsubInline");
yield return Case("PPC_PsMsubNoNi", "PPC_PsMsubNoNiInline");
yield return Case("PPC_PsNmsub", "PPC_PsNmsubInline");
yield return Case("PPC_PsNmsubNoNi", "PPC_PsNmsubNoNiInline");
yield return Case("PPC_PsNmadd", "PPC_PsNmaddInline");
yield return Case("PPC_PsMadds0", "PPC_PsMadds0Inline");
yield return Case("PPC_PsMadds1", "PPC_PsMadds1Inline");
yield return Case("PPC_PsSum0", "PPC_PsSum0Inline");
yield return Case("PPC_PsSum1", "PPC_PsSum1Inline");
}
public static IEnumerable<object[]> ScalarCases()
{
yield return Case("PPC_Fadds", "PpcForceSingleValueInline");
yield return Case("PPC_FaddsNoNi", "static_cast<float>");
yield return Case("PPC_Fsubs", "PpcForceSingleValueInline");
yield return Case("PPC_FsubsNoNi", "static_cast<float>");
yield return Case("PPC_Fmuls", "PpcFmulsInline");
yield return Case("PPC_FmulsNoNi", "PpcFmulsNoNiInline");
yield return Case("PPC_Fdivs", "PpcForceSingleValueInline");
yield return Case("PPC_FdivsNoNi", "static_cast<float>");
yield return Case("PPC_Fsqrt", "std::sqrt");
yield return Case("PPC_Fmadd", "PpcFmaddInline");
yield return Case("PPC_Fmsub", "PpcFmsubInline");
yield return Case("PPC_Fnmadd", "PpcFnmaddInline");
yield return Case("PPC_Fnmsub", "PpcFnmsubInline");
}
[Theory]
[MemberData(nameof(PairedCases))]
public void PairedArithmeticTargetsUseExpectedInlineHelper(string target, string expected)
{
var code = EmitCall(
target,
"f0",
IrValue.Register("f1"),
IrValue.Register("f2"),
IrValue.Register("f3"));
Assert.Contains(expected + "(", code, StringComparison.Ordinal);
}
[Theory]
[MemberData(nameof(ScalarCases))]
public void ScalarArithmeticTargetsUseExpectedInlineHelper(string target, string expected)
{
var code = EmitCall(
target,
"f0",
IrValue.Register("f1"),
IrValue.Register("f2"),
IrValue.Register("f3"));
Assert.Contains(expected, code, StringComparison.Ordinal);
}
[Fact]
public void PairedTablePreservesOperandOrderAndNoNiPacking()
{
var code = EmitCall(
"PPC_PsMaddNoNi",
"f0",
IrValue.Imm(1),
IrValue.Imm(2),
IrValue.Imm(3));
Assert.Contains(
"PPC_PsMaddNoNiInline(PPC_PsFromScalarNoNiInline(1), PPC_PsFromScalarNoNiInline(2), PPC_PsFromScalarNoNiInline(3))",
code,
StringComparison.Ordinal);
}
[Fact]
public void ScalarTablePreservesFmaOperandOrder()
{
var code = EmitCall(
"PPC_Fmsub",
"f0",
IrValue.Imm(1),
IrValue.Imm(2),
IrValue.Imm(3));
Assert.Contains("PpcFmsubInline(1, 2, 3)", code, StringComparison.Ordinal);
}
[Fact]
public void TableTargetsRemainCaseInsensitive()
{
var code = EmitCall(
"ppc_psaddnoni",
"f0",
IrValue.Imm(1),
IrValue.Imm(2));
Assert.Contains("PPC_PsAddNoNiInline(", code, StringComparison.Ordinal);
}
[Fact]
public void TableTargetsKeepMissingDestinationSuppression()
{
var code = EmitCall(
"PPC_PsAdd",
string.Empty,
IrValue.Imm(1),
IrValue.Imm(2));
Assert.DoesNotContain("PPC_PsAddInline(", code, StringComparison.Ordinal);
Assert.DoesNotContain("PPC_PsAdd(", code, StringComparison.Ordinal);
}
[Fact]
public void TableTargetsKeepMissingArgumentLiteralZeroFallback()
{
var code = EmitCall("PPC_PsAdd", "f0", IrValue.Imm(1));
Assert.Contains(
"PPC_PsAddInline(PPC_PsFromScalarInline(1), 0)",
code,
StringComparison.Ordinal);
}
private static object[] Case(string target, string expected) => new object[] { target, expected };
private static string EmitCall(string target, string destination, params IrValue[] arguments)
{
var function = new IrFunction(
"inline_table_probe",
"entry",
new[]
{
new IrBasicBlock(
"entry",
new IrInstruction[]
{
new IrCall(destination, target, arguments),
new IrReturn(null)
})
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["f0"] = ValueRepresentation.Float64,
["f1"] = ValueRepresentation.Float64,
["f2"] = ValueRepresentation.Float64,
["f3"] = ValueRepresentation.Float64
});
var signature = new FunctionAbiClassification("inline_table_probe", ValueRepresentation.Void);
return new CxxLinearCodeGenerator().Emit(
0x80040000,
new SsaTransformer().Convert(function),
signature,
types);
}
}
@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis.Ssa;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
public sealed class IrRegisterDataFlowTests
{
[Fact]
public void ResolvedInstructionsExposeAllRegisterOperandsAndDestinations()
{
var instructions = new IrInstruction[]
{
new IrResolvedLoad(
"r3", "range", new IrAddress("r4", 4), 0, 4),
new IrResolvedStore(
"range", new IrAddress("r5", 8), 0, IrValue.Register("r6"), 4),
new IrResolvedPsqLoad(
"f1", "range", IrValue.Register("r7"), 0, 0, 2, KnownGqr: null),
new IrResolvedPsqStore(
"range", IrValue.Register("r8"), 0, IrValue.Register("r9"),
0, 3, KnownGqr: 0x1234u),
new IrResolvedLoadPair(
"r10", "r11", "range", new IrAddress("r12", 0),
new IrAddress("r13", 4), 0, 4),
new IrResolvedStorePair(
"range", new IrAddress("r14", 0), new IrAddress("r15", 4),
0, IrValue.Register("r16"), IrValue.Register("r17"), 4)
};
var uses = instructions.SelectMany(IrRegisterDataFlow.Uses).ToArray();
var definitions = instructions.SelectMany(IrRegisterDataFlow.Definitions).ToArray();
Assert.Equal(
new[] { "r4", "r5", "r6", "r7", "gqr2", "r8", "r9", "r12", "r13", "r14", "r15", "r16", "r17" },
uses);
Assert.Equal(new[] { "r3", "f1", "r10", "r11" }, definitions);
}
[Fact]
public void JumpTableSelectorsAndRepeatedOperandsRemainVisible()
{
var selector = new IrJumpTable(
"r9_jump_selector",
new[] { new IrJumpTableCase(0x80001000u, "case_0") });
var duplicateUse = new IrBinary(
"temporary", IrValue.Register("r3"), IrValue.Register("r3"), "add");
Assert.Equal(new[] { "r9_jump_selector" }, IrRegisterDataFlow.Uses(selector));
Assert.Equal(new[] { "r3", "r3" }, IrRegisterDataFlow.Uses(duplicateUse));
}
[Fact]
public void NumericSsaSuffixIsArchitecturalButNamedLifterSuffixIsTemporary()
{
Assert.Equal("r3", IrRegisterDataFlow.BaseName("r3_7"));
Assert.Equal("r3_addc_left", IrRegisterDataFlow.BaseName("r3_addc_left"));
Assert.True(IrRegisterDataFlow.IsRegisterName("r3_7"));
Assert.False(IrRegisterDataFlow.IsRegisterName("r3_addc_left"));
}
[Fact]
public void RangePointerDestinationIsNotDeclaredAsAValueLocal()
{
var function = new IrFunction(
"range_pointer_local_policy",
"entry",
new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrResolveGuestMemoryRange(
"range", IrValue.Register("r13"), 0, 64,
NeedsReadAccess: true, NeedsWriteAccess: false),
new IrResolvedLoad(
"r3", "range", new IrAddress("r2", 4), 4, 4),
new IrReturn(IrValue.Register("r3"))
})
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["r2"] = ValueRepresentation.UInt32,
["r3"] = ValueRepresentation.UInt32,
["r13"] = ValueRepresentation.UInt32
});
var code = new CxxLinearCodeGenerator().Emit(
0x80006203,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("range_pointer_local_policy", ValueRepresentation.UInt32),
types);
Assert.Equal(new[] { "range" }, IrRegisterDataFlow.Definitions(function.Blocks[0].Instructions[0]));
Assert.Contains("uint8_t* range = nullptr;", code, StringComparison.Ordinal);
Assert.DoesNotContain("uint32_t range", code, StringComparison.Ordinal);
}
}
@@ -0,0 +1,334 @@
using System.Collections.Generic;
using Translator.Core.Disassembly;
using Xunit;
namespace Translator.Tests;
public class JumpTableDetectorTests
{
private static IReadOnlyDictionary<uint, int> BuildIndex(IReadOnlyList<PpcInstruction> ordered)
=> ordered.Select((ins, idx) => new { ins.Address, idx }).ToDictionary(x => x.Address, x => x.idx);
[Fact]
public void RecognizesLwzxBackedSwitchTable()
{
const uint baseAddr = 0x80000000;
const uint tableAddr = 0x80000100;
var ordered = new List<PpcInstruction>
{
PpcInstruction.Synthetic(baseAddr + 0x00, 0, "cmplwi", new PpcOperand[] { new PpcRegisterOperand("r3", 3), new PpcImmediateOperand(2) }),
PpcInstruction.Synthetic(baseAddr + 0x04, 0, "slwi", new PpcOperand[] { new PpcRegisterOperand("r3", 3), new PpcRegisterOperand("r3", 3), new PpcImmediateOperand(2) }),
PpcInstruction.Synthetic(baseAddr + 0x08, 0, "lis", new PpcOperand[] { new PpcRegisterOperand("r12", 12), new PpcImmediateOperand(unchecked((short)0x8000)) }),
PpcInstruction.Synthetic(baseAddr + 0x0C, 0, "addi", new PpcOperand[] { new PpcRegisterOperand("r12", 12), new PpcRegisterOperand("r12", 12), new PpcImmediateOperand(0x0100) }),
PpcInstruction.Synthetic(baseAddr + 0x10, 0, "lwzx", new PpcOperand[] { new PpcRegisterOperand("r0", 0), new PpcRegisterOperand("r12", 12), new PpcRegisterOperand("r3", 3) }),
PpcInstruction.Synthetic(baseAddr + 0x14, 0, "mtctr", new PpcOperand[] { new PpcRegisterOperand("r0", 0) }),
PpcInstruction.Synthetic(baseAddr + 0x18, 0, "bctr", System.Array.Empty<PpcOperand>(), isReturn: false, isCall: false, isConditional: false),
};
var indexByAddress = BuildIndex(ordered);
var image = TranslatorCppTestHarness.CreateImage(
(tableAddr + 0x00, baseAddr + 0x40),
(tableAddr + 0x04, baseAddr + 0x50),
(tableAddr + 0x08, baseAddr + 0x60));
var recognized = JumpTableDetector.TryRecognize(
ordered,
indexByAddress,
baseAddr + 0x18,
baseAddr,
baseAddr + 0x100,
image,
out var targets);
Assert.True(recognized);
Assert.Equal(new uint[] { baseAddr + 0x40, baseAddr + 0x50, baseAddr + 0x60 }, targets);
}
[Fact]
public void UsesFallbackUpperBoundWhenCompareUsesLogicalCounter()
{
const uint baseAddr = 0x80001000;
const uint tableAddr = 0x80001200;
var ordered = new List<PpcInstruction>
{
PpcInstruction.Synthetic(baseAddr + 0x00, 0, "cmplwi", new PpcOperand[] { new PpcRegisterOperand("r18", 18), new PpcImmediateOperand(1) }),
PpcInstruction.Synthetic(baseAddr + 0x04, 0, "slwi", new PpcOperand[] { new PpcRegisterOperand("r30", 30), new PpcRegisterOperand("r18", 18), new PpcImmediateOperand(2) }),
PpcInstruction.Synthetic(baseAddr + 0x08, 0, "lis", new PpcOperand[] { new PpcRegisterOperand("r12", 12), new PpcImmediateOperand(unchecked((short)0x8000)) }),
PpcInstruction.Synthetic(baseAddr + 0x0C, 0, "addi", new PpcOperand[] { new PpcRegisterOperand("r12", 12), new PpcRegisterOperand("r12", 12), new PpcImmediateOperand(0x1200) }),
PpcInstruction.Synthetic(baseAddr + 0x10, 0, "lwzx", new PpcOperand[] { new PpcRegisterOperand("r0", 0), new PpcRegisterOperand("r12", 12), new PpcRegisterOperand("r30", 30) }),
PpcInstruction.Synthetic(baseAddr + 0x14, 0, "mtctr", new PpcOperand[] { new PpcRegisterOperand("r0", 0) }),
PpcInstruction.Synthetic(baseAddr + 0x18, 0, "bctr", System.Array.Empty<PpcOperand>(), isReturn: false, isCall: false, isConditional: false),
};
var indexByAddress = BuildIndex(ordered);
var image = TranslatorCppTestHarness.CreateImage(
(tableAddr + 0x00, baseAddr + 0x30),
(tableAddr + 0x04, baseAddr + 0x34));
var recognized = JumpTableDetector.TryRecognize(
ordered,
indexByAddress,
baseAddr + 0x18,
baseAddr,
baseAddr + 0x100,
image,
out var targets);
Assert.True(recognized);
Assert.Equal(new uint[] { baseAddr + 0x30, baseAddr + 0x34 }, targets);
}
[Fact]
public void RecognizesPcRelativeTablePointerWithRelativeEntries()
{
const uint baseAddr = 0x80010000;
const uint linkAddress = baseAddr + 0x04;
const uint picBase = 0x80011000;
const uint tableAddr = 0x80012000;
var ordered = new List<PpcInstruction>
{
PpcInstruction.Synthetic(baseAddr + 0x00, 0, "bl", new PpcOperand[] { new PpcBranchTargetOperand(linkAddress) }, [linkAddress], isCall: true),
PpcInstruction.Synthetic(baseAddr + 0x04, 0, "mflr", new PpcOperand[] { new PpcRegisterOperand("r30", 30) }),
PpcInstruction.Synthetic(baseAddr + 0x08, 0, "lwz", new PpcOperand[] { new PpcRegisterOperand("r0", 0), new PpcDisplacementOperand(-20, "r30", 30) }),
PpcInstruction.Synthetic(baseAddr + 0x0C, 0, "add", new PpcOperand[] { new PpcRegisterOperand("r30", 30), new PpcRegisterOperand("r0", 0), new PpcRegisterOperand("r30", 30) }),
PpcInstruction.Synthetic(baseAddr + 0x10, 0, "cmplwi", new PpcOperand[] { new PpcRegisterOperand("r9", 9), new PpcImmediateOperand(1) }),
PpcInstruction.Synthetic(baseAddr + 0x14, 0, "slwi", new PpcOperand[] { new PpcRegisterOperand("r9", 9), new PpcRegisterOperand("r9", 9), new PpcImmediateOperand(2) }),
PpcInstruction.Synthetic(baseAddr + 0x18, 0, "lwz", new PpcOperand[] { new PpcRegisterOperand("r10", 10), new PpcDisplacementOperand(-0x20, "r30", 30) }),
PpcInstruction.Synthetic(baseAddr + 0x1C, 0, "lwzx", new PpcOperand[] { new PpcRegisterOperand("r9", 9), new PpcRegisterOperand("r10", 10), new PpcRegisterOperand("r9", 9) }),
PpcInstruction.Synthetic(baseAddr + 0x20, 0, "add", new PpcOperand[] { new PpcRegisterOperand("r9", 9), new PpcRegisterOperand("r9", 9), new PpcRegisterOperand("r10", 10) }),
PpcInstruction.Synthetic(baseAddr + 0x24, 0, "mtctr", new PpcOperand[] { new PpcRegisterOperand("r9", 9) }),
PpcInstruction.Synthetic(baseAddr + 0x28, 0, "bctr", System.Array.Empty<PpcOperand>(), isReturn: false, isCall: false, isConditional: false),
};
var image = TranslatorCppTestHarness.CreateImage(
(linkAddress - 20, unchecked(picBase - linkAddress)),
(picBase - 0x20, tableAddr),
(tableAddr + 0x00, unchecked((baseAddr + 0x40) - tableAddr)),
(tableAddr + 0x04, unchecked((baseAddr + 0x50) - tableAddr)));
var recognized = JumpTableDetector.TryRecognize(
ordered,
BuildIndex(ordered),
baseAddr + 0x28,
baseAddr,
baseAddr + 0x100,
image,
out var targets);
Assert.True(recognized);
Assert.Equal(new uint[] { baseAddr + 0x40, baseAddr + 0x50 }, targets);
}
[Fact]
public void RecognizesPcRelativeTablePointerWhenTocSetupIsFarBack()
{
const uint baseAddr = 0x80018000;
const uint linkAddress = baseAddr + 0x04;
const uint picBase = 0x8001A000;
const uint tableAddr = 0x8001B000;
var ordered = new List<PpcInstruction>
{
PpcInstruction.Synthetic(baseAddr + 0x00, 0, "bl", new PpcOperand[] { new PpcBranchTargetOperand(linkAddress) }, [linkAddress], isCall: true),
PpcInstruction.Synthetic(baseAddr + 0x04, 0, "mflr", new PpcOperand[] { new PpcRegisterOperand("r30", 30) }),
PpcInstruction.Synthetic(baseAddr + 0x08, 0, "lwz", new PpcOperand[] { new PpcRegisterOperand("r0", 0), new PpcDisplacementOperand(-20, "r30", 30) }),
PpcInstruction.Synthetic(baseAddr + 0x0C, 0, "add", new PpcOperand[] { new PpcRegisterOperand("r30", 30), new PpcRegisterOperand("r0", 0), new PpcRegisterOperand("r30", 30) }),
};
for (var i = 0; i < 76; i++)
{
ordered.Add(PpcInstruction.Synthetic(baseAddr + 0x10 + (uint)(i * 4), 0, "nop", System.Array.Empty<PpcOperand>()));
}
var switchBase = baseAddr + 0x10 + (76u * 4);
ordered.AddRange(new[]
{
PpcInstruction.Synthetic(switchBase + 0x00, 0, "cmplwi", new PpcOperand[] { new PpcRegisterOperand("r9", 9), new PpcImmediateOperand(1) }),
PpcInstruction.Synthetic(switchBase + 0x04, 0, "slwi", new PpcOperand[] { new PpcRegisterOperand("r9", 9), new PpcRegisterOperand("r9", 9), new PpcImmediateOperand(2) }),
PpcInstruction.Synthetic(switchBase + 0x08, 0, "lwz", new PpcOperand[] { new PpcRegisterOperand("r10", 10), new PpcDisplacementOperand(-0x20, "r30", 30) }),
PpcInstruction.Synthetic(switchBase + 0x0C, 0, "lwzx", new PpcOperand[] { new PpcRegisterOperand("r9", 9), new PpcRegisterOperand("r10", 10), new PpcRegisterOperand("r9", 9) }),
PpcInstruction.Synthetic(switchBase + 0x10, 0, "add", new PpcOperand[] { new PpcRegisterOperand("r9", 9), new PpcRegisterOperand("r9", 9), new PpcRegisterOperand("r10", 10) }),
PpcInstruction.Synthetic(switchBase + 0x14, 0, "mtctr", new PpcOperand[] { new PpcRegisterOperand("r9", 9) }),
PpcInstruction.Synthetic(switchBase + 0x18, 0, "bctr", System.Array.Empty<PpcOperand>(), isReturn: false, isCall: false, isConditional: false),
});
var image = TranslatorCppTestHarness.CreateImage(
(linkAddress - 20, unchecked(picBase - linkAddress)),
(picBase - 0x20, tableAddr),
(tableAddr + 0x00, unchecked((baseAddr + 0x180) - tableAddr)),
(tableAddr + 0x04, unchecked((baseAddr + 0x190) - tableAddr)));
var recognized = JumpTableDetector.TryRecognize(
ordered,
BuildIndex(ordered),
switchBase + 0x18,
baseAddr,
baseAddr + 0x200,
image,
out var targets);
Assert.True(recognized);
Assert.Equal(new uint[] { baseAddr + 0x180, baseAddr + 0x190 }, targets);
}
[Fact]
public void RejectsWhenEntryPointOrMtctrChainIsMissing()
{
const uint baseAddr = 0x80002000;
var ordered = new List<PpcInstruction>
{
PpcInstruction.Synthetic(baseAddr + 0x00, 0, "lwzx", new PpcOperand[] { new PpcRegisterOperand("r0", 0), new PpcRegisterOperand("r12", 12), new PpcRegisterOperand("r3", 3) }),
PpcInstruction.Synthetic(baseAddr + 0x04, 0, "bctr", System.Array.Empty<PpcOperand>(), isReturn: false, isCall: false, isConditional: false),
};
var image = TranslatorCppTestHarness.CreateImage((0x80002100u, baseAddr + 0x20));
Assert.False(JumpTableDetector.TryRecognize(
ordered,
BuildIndex(ordered),
baseAddr + 0x10,
baseAddr,
baseAddr + 0x100,
image,
out _));
Assert.False(JumpTableDetector.TryRecognize(
ordered,
BuildIndex(ordered),
baseAddr + 0x04,
baseAddr,
baseAddr + 0x100,
image,
out _));
}
[Fact]
public void RejectsWhenCtrLoadChainIsInvalid()
{
const uint baseAddr = 0x80003000;
const uint tableAddr = 0x80003100;
var image = TranslatorCppTestHarness.CreateImage((tableAddr + 0x00, baseAddr + 0x20));
var wrongMtctrOperand = new List<PpcInstruction>
{
PpcInstruction.Synthetic(baseAddr + 0x00, 0, "cmplwi", new PpcOperand[] { new PpcRegisterOperand("r3", 3), new PpcImmediateOperand(0) }),
PpcInstruction.Synthetic(baseAddr + 0x04, 0, "lis", new PpcOperand[] { new PpcRegisterOperand("r12", 12), new PpcImmediateOperand(unchecked((short)0x8000)) }),
PpcInstruction.Synthetic(baseAddr + 0x08, 0, "addi", new PpcOperand[] { new PpcRegisterOperand("r12", 12), new PpcRegisterOperand("r12", 12), new PpcImmediateOperand(0x0100) }),
PpcInstruction.Synthetic(baseAddr + 0x0C, 0, "lwzx", new PpcOperand[] { new PpcRegisterOperand("r0", 0), new PpcRegisterOperand("r12", 12), new PpcRegisterOperand("r3", 3) }),
PpcInstruction.Synthetic(baseAddr + 0x10, 0, "mtctr", System.Array.Empty<PpcOperand>()),
PpcInstruction.Synthetic(baseAddr + 0x14, 0, "bctr", System.Array.Empty<PpcOperand>(), isReturn: false, isCall: false, isConditional: false),
};
Assert.False(JumpTableDetector.TryRecognize(
wrongMtctrOperand,
BuildIndex(wrongMtctrOperand),
baseAddr + 0x14,
baseAddr,
baseAddr + 0x100,
image,
out _));
var wrongLoadDest = new List<PpcInstruction>
{
PpcInstruction.Synthetic(baseAddr + 0x00, 0, "cmplwi", new PpcOperand[] { new PpcRegisterOperand("r3", 3), new PpcImmediateOperand(0) }),
PpcInstruction.Synthetic(baseAddr + 0x04, 0, "lis", new PpcOperand[] { new PpcRegisterOperand("r12", 12), new PpcImmediateOperand(unchecked((short)0x8000)) }),
PpcInstruction.Synthetic(baseAddr + 0x08, 0, "addi", new PpcOperand[] { new PpcRegisterOperand("r12", 12), new PpcRegisterOperand("r12", 12), new PpcImmediateOperand(0x0100) }),
PpcInstruction.Synthetic(baseAddr + 0x0C, 0, "lbzx", new PpcOperand[] { new PpcRegisterOperand("r0", 0), new PpcRegisterOperand("r12", 12), new PpcRegisterOperand("r3", 3) }),
PpcInstruction.Synthetic(baseAddr + 0x10, 0, "mtctr", new PpcOperand[] { new PpcRegisterOperand("r0", 0) }),
PpcInstruction.Synthetic(baseAddr + 0x14, 0, "bctr", System.Array.Empty<PpcOperand>(), isReturn: false, isCall: false, isConditional: false),
};
Assert.False(JumpTableDetector.TryRecognize(
wrongLoadDest,
BuildIndex(wrongLoadDest),
baseAddr + 0x14,
baseAddr,
baseAddr + 0x100,
image,
out _));
}
[Fact]
public void RejectsWhenTableBaseOrUpperBoundCannotBeResolved()
{
const uint baseAddr = 0x80004000;
const uint tableAddr = 0x80004100;
var image = TranslatorCppTestHarness.CreateImage((tableAddr + 0x00, baseAddr + 0x20));
var unresolvedBase = new List<PpcInstruction>
{
PpcInstruction.Synthetic(baseAddr + 0x00, 0, "cmplwi", new PpcOperand[] { new PpcRegisterOperand("r3", 3), new PpcImmediateOperand(0) }),
PpcInstruction.Synthetic(baseAddr + 0x04, 0, "addi", new PpcOperand[] { new PpcRegisterOperand("r12", 12), new PpcRegisterOperand("r11", 11), new PpcImmediateOperand(0x0100) }),
PpcInstruction.Synthetic(baseAddr + 0x08, 0, "lwzx", new PpcOperand[] { new PpcRegisterOperand("r0", 0), new PpcRegisterOperand("r12", 12), new PpcRegisterOperand("r3", 3) }),
PpcInstruction.Synthetic(baseAddr + 0x0C, 0, "mtctr", new PpcOperand[] { new PpcRegisterOperand("r0", 0) }),
PpcInstruction.Synthetic(baseAddr + 0x10, 0, "bctr", System.Array.Empty<PpcOperand>(), isReturn: false, isCall: false, isConditional: false),
};
Assert.False(JumpTableDetector.TryRecognize(
unresolvedBase,
BuildIndex(unresolvedBase),
baseAddr + 0x10,
baseAddr,
baseAddr + 0x100,
image,
out _));
var oversizeBound = new List<PpcInstruction>
{
PpcInstruction.Synthetic(baseAddr + 0x00, 0, "cmplwi", new PpcOperand[] { new PpcRegisterOperand("r3", 3), new PpcImmediateOperand(600) }),
PpcInstruction.Synthetic(baseAddr + 0x04, 0, "lis", new PpcOperand[] { new PpcRegisterOperand("r12", 12), new PpcImmediateOperand(unchecked((short)0x8000)) }),
PpcInstruction.Synthetic(baseAddr + 0x08, 0, "addi", new PpcOperand[] { new PpcRegisterOperand("r12", 12), new PpcRegisterOperand("r12", 12), new PpcImmediateOperand(0x0100) }),
PpcInstruction.Synthetic(baseAddr + 0x0C, 0, "lwzx", new PpcOperand[] { new PpcRegisterOperand("r0", 0), new PpcRegisterOperand("r12", 12), new PpcRegisterOperand("r3", 3) }),
PpcInstruction.Synthetic(baseAddr + 0x10, 0, "mtctr", new PpcOperand[] { new PpcRegisterOperand("r0", 0) }),
PpcInstruction.Synthetic(baseAddr + 0x14, 0, "bctr", System.Array.Empty<PpcOperand>(), isReturn: false, isCall: false, isConditional: false),
};
Assert.False(JumpTableDetector.TryRecognize(
oversizeBound,
BuildIndex(oversizeBound),
baseAddr + 0x14,
baseAddr,
baseAddr + 0x100,
image,
out _));
}
[Fact]
public void RejectsUnreadableAndOutOfWindowTargets()
{
const uint baseAddr = 0x80005000;
const uint tableAddr = 0x80005100;
var ordered = new List<PpcInstruction>
{
PpcInstruction.Synthetic(baseAddr + 0x00, 0, "cmplwi", new PpcOperand[] { new PpcRegisterOperand("r3", 3), new PpcImmediateOperand(1) }),
PpcInstruction.Synthetic(baseAddr + 0x04, 0, "lis", new PpcOperand[] { new PpcRegisterOperand("r12", 12), new PpcImmediateOperand(unchecked((short)0x8000)) }),
PpcInstruction.Synthetic(baseAddr + 0x08, 0, "addi", new PpcOperand[] { new PpcRegisterOperand("r12", 12), new PpcRegisterOperand("r12", 12), new PpcImmediateOperand(0x0100) }),
PpcInstruction.Synthetic(baseAddr + 0x0C, 0, "lwzx", new PpcOperand[] { new PpcRegisterOperand("r0", 0), new PpcRegisterOperand("r12", 12), new PpcRegisterOperand("r3", 3) }),
PpcInstruction.Synthetic(baseAddr + 0x10, 0, "mtctr", new PpcOperand[] { new PpcRegisterOperand("r0", 0) }),
PpcInstruction.Synthetic(baseAddr + 0x14, 0, "bctr", System.Array.Empty<PpcOperand>(), isReturn: false, isCall: false, isConditional: false),
};
var missingEntryImage = TranslatorCppTestHarness.CreateImage((tableAddr + 0x00, baseAddr + 0x20));
Assert.False(JumpTableDetector.TryRecognize(
ordered,
BuildIndex(ordered),
baseAddr + 0x14,
baseAddr,
baseAddr + 0x100,
missingEntryImage,
out _));
var outOfWindowImage = TranslatorCppTestHarness.CreateImage(
(tableAddr + 0x00, 0x90000000u),
(tableAddr + 0x04, baseAddr + 0x20));
Assert.False(JumpTableDetector.TryRecognize(
ordered,
BuildIndex(ordered),
baseAddr + 0x14,
baseAddr,
baseAddr + 0x100,
outOfWindowImage,
out _));
}
}
@@ -0,0 +1,151 @@
using System.Buffers.Binary;
using Translator.Core.Parsing.Kamek;
using Xunit;
namespace Translator.Tests;
public class KamekPulFileTests
{
[Fact]
public void ParsesRawSingleChunk()
{
var chunk = BuildChunk(
bssSize: 0x20,
code: [0x60, 0x00, 0x00, 0x00],
commands:
[
BuildCommand(KamekCommandId.Write32, absolute: true, address: 0x805850BC, args: [0x60000000])
]);
var pul = KamekPulFile.Parse(chunk);
var selected = pul.SelectRegion("P");
Assert.False(pul.IsCombined);
Assert.Single(pul.Chunks);
Assert.Equal(0x20u, selected.BssSize);
Assert.Equal(4u, selected.CodeSize);
Assert.Single(selected.Commands);
Assert.Equal(KamekCommandId.Write32, selected.Commands[0].Id);
Assert.True(selected.Commands[0].AddressIsAbsolute);
Assert.Equal(0x805850BCu, selected.Commands[0].Address);
}
[Fact]
public void ParsesCombinedChunksAndSelectsRegion()
{
var p = BuildChunk(0x10, [0x4E, 0x80, 0x00, 0x20], [BuildCommand(KamekCommandId.Branch, true, 0x8053369C, [0x100])]);
var e = BuildChunk(0x20, [0x60, 0x00, 0x00, 0x00], [BuildCommand(KamekCommandId.Write8, false, 0x10, [0x7F])]);
var combined = new byte[0x10 + p.Length + e.Length];
WriteU32(combined, 0, (uint)p.Length);
WriteU32(combined, 4, (uint)e.Length);
p.CopyTo(combined, 0x10);
e.CopyTo(combined, 0x10 + p.Length);
var pul = KamekPulFile.Parse(combined);
Assert.True(pul.IsCombined);
Assert.Equal(2, pul.Chunks.Count);
Assert.Equal(0x10u, pul.SelectRegion("P").BssSize);
Assert.Equal(0x20u, pul.SelectRegion("E").BssSize);
}
[Fact]
public void EncodesPpcBranchLikeKamek()
{
Assert.Equal(0x48000144u, KamekPpcEncoding.EncodeBranch(0x8053369C, 0x805337E0, link: false));
Assert.Equal(0x4BFFFFFDu, KamekPpcEncoding.EncodeBranch(0x8053369C, 0x80533698, link: true));
Assert.Throws<ArgumentOutOfRangeException>(() => KamekPpcEncoding.EncodeBranch(0x8053369C, 0x8053369D, link: false));
}
[Fact]
public void ParsesBundledRetroRewindCodePulWhenPresent()
{
var repoRoot = ProjectPathsForTests.FindRepositoryRoot();
var codePul = Path.Combine(repoRoot, "PulsarPacks", "completed", "RetroRewind", "RetroRewind6", "Binaries", "Code.pul");
if (!File.Exists(codePul))
{
return;
}
var pul = KamekPulFile.Load(codePul);
var chunk = pul.SelectRegion("P");
Assert.True(pul.IsCombined);
Assert.Equal(3, pul.Chunks.Count);
Assert.All(pul.CombinedChunkSizes.Take(3), size => Assert.True(size >= KamekChunk.HeaderSize));
Assert.Equal(0u, pul.CombinedChunkSizes[3]);
Assert.NotEmpty(chunk.CodeBlob);
Assert.True(chunk.BssSize > 0);
Assert.True(chunk.CtorStart <= chunk.CtorEnd);
Assert.True(chunk.CtorEnd <= chunk.CodeSize);
Assert.NotEmpty(chunk.Commands);
Assert.Equal(chunk.Commands.Count, chunk.RelativeCommandCount + chunk.AbsoluteCommandCount);
Assert.Equal(chunk.Commands.Count, chunk.CommandCounts.Values.Sum());
}
private static byte[] BuildChunk(uint bssSize, byte[] code, byte[][] commands)
{
var commandSize = commands.Sum(c => c.Length);
var chunkSize = KamekChunk.HeaderSize + code.Length + commandSize;
var data = new byte[chunkSize];
WriteU32(data, 0x00, KamekChunk.Magic0);
WriteU32(data, 0x04, KamekChunk.Magic1);
WriteU32(data, 0x08, bssSize);
WriteU32(data, 0x0C, (uint)code.Length);
WriteU32(data, 0x10, 0);
WriteU32(data, 0x14, 0);
WriteU32(data, 0x18, (uint)chunkSize);
code.CopyTo(data, KamekChunk.HeaderSize);
var offset = KamekChunk.HeaderSize + code.Length;
foreach (var command in commands)
{
command.CopyTo(data, offset);
offset += command.Length;
}
return data;
}
private static byte[] BuildCommand(KamekCommandId id, bool absolute, uint address, uint[] args)
{
var size = 4 + (absolute ? 4 : 0) + args.Length * 4;
var data = new byte[size];
var commandWord = ((uint)(byte)id << 24) | (absolute ? 0x00FFFFFEu : address);
WriteU32(data, 0, commandWord);
var offset = 4;
if (absolute)
{
WriteU32(data, offset, address);
offset += 4;
}
foreach (var arg in args)
{
WriteU32(data, offset, arg);
offset += 4;
}
return data;
}
private static void WriteU32(byte[] data, int offset, uint value) =>
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(offset, 4), value);
}
internal static class ProjectPathsForTests
{
public static string FindRepositoryRoot()
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir is not null)
{
if (File.Exists(Path.Combine(dir.FullName, "translator", "Translator.sln")) &&
File.Exists(Path.Combine(dir.FullName, "projects", "mkwii", "recomp.yml")))
{
return dir.FullName;
}
dir = dir.Parent;
}
throw new DirectoryNotFoundException("Could not find repository root.");
}
}
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis.Ssa;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
public class LeafFprStackElisionCodeGenTests
{
[Fact]
public void ProvenScalarAndPairedAbiSpillUsesNativeLocal()
{
var function = new IrFunction(
"leaf_fpr_abi_spill_elision",
"entry",
new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrBinary("r1", IrValue.Register("r1"), IrValue.Imm(-128), "add"),
new IrStore(new IrAddress("r1", 112), IrValue.Register("f31"), 8),
new IrBinary("save_ps1", IrValue.Register("r1"), IrValue.Imm(120), "add"),
new IrCall(string.Empty, "PPC_PsqSt", new[]
{
IrValue.Register("save_ps1"), IrValue.Register("f31"), IrValue.Imm(0), IrValue.Imm(0)
}),
new IrCall("f31", "PPC_PsAdd", new[] { IrValue.Register("f1"), IrValue.Register("f2") }),
new IrBinary("restore_ps1", IrValue.Register("r1"), IrValue.Imm(120), "add"),
new IrCall("f31", "PPC_PsqL", new[]
{
IrValue.Register("restore_ps1"), IrValue.Imm(0), IrValue.Imm(0)
}),
new IrLoad("f31", new IrAddress("r1", 112), 8),
new IrBinary("r1", IrValue.Register("r1"), IrValue.Imm(128), "add"),
new IrReturn(null)
})
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["r1"] = ValueRepresentation.UInt32,
["f1"] = ValueRepresentation.Float64,
["f2"] = ValueRepresentation.Float64,
["f31"] = ValueRepresentation.Float64,
["save_ps1"] = ValueRepresentation.UInt32,
["restore_ps1"] = ValueRepresentation.UInt32
});
var code = new CxxLinearCodeGenerator().Emit(
0x80006221,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("leaf_fpr_abi_spill_elision", ValueRepresentation.Void),
types,
enableLeafAbiSpillElision: true);
Assert.Contains("double leaf_stack_saved_f31_entry", code, StringComparison.Ordinal);
Assert.Contains("leaf_stack_saved_f31_entry = f31.d;", code, StringComparison.Ordinal);
Assert.Contains("f31.d = leaf_stack_saved_f31_entry;", code, StringComparison.Ordinal);
Assert.DoesNotContain("WriteStackFloat64", code, StringComparison.Ordinal);
Assert.DoesNotContain("ReadStackFloat64", code, StringComparison.Ordinal);
Assert.DoesNotContain("PPC_PsqSt", code, StringComparison.Ordinal);
Assert.DoesNotContain("PPC_PsqL", code, StringComparison.Ordinal);
Assert.Contains(
"// RECOMP_REGISTRATION base 0x80006221 leaf_fpr_abi_spill_elision preserves=true fpr_mask=0x00000000",
code,
StringComparison.Ordinal);
}
}
@@ -0,0 +1,901 @@
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Translator.Core.Ir;
using Translator.Core.Loading;
using Translator.Core.Translation;
using Xunit;
namespace Translator.Tests;
/// <summary>
/// IR-level leaf inlining (performance audit T-INLINE). The splice happens before SSA, so every
/// negative case is a silent-corruption bug if it starts firing; hence more rejection tests than positive ones.
/// </summary>
public class LeafInliningTests
{
private const uint Caller = MemoryLayout.RamBase;
private const uint Callee = MemoryLayout.RamBase + 0x100;
private const uint Callee2 = MemoryLayout.RamBase + 0x180;
private const int ImageSize = 0x400;
// --- minimal PowerPC encoder -------------------------------------------
private static uint Blr() => 0x4E800020u;
private static uint Bl(uint from, uint to) => 0x48000001u | ((to - from) & 0x03FFFFFCu);
private static uint B(uint from, uint to) => 0x48000000u | ((to - from) & 0x03FFFFFCu);
private static uint Bne(uint from, uint to) => 0x40820000u | ((to - from) & 0xFFFCu);
private static uint Addi(int d, int a, int simm) =>
0x38000000u | ((uint)d << 21) | ((uint)a << 16) | (uint)(simm & 0xFFFF);
private static uint Lwz(int d, int a, int offset) =>
0x80000000u | ((uint)d << 21) | ((uint)a << 16) | (uint)(offset & 0xFFFF);
private static uint Stwu(int s, int a, int offset) =>
0x94000000u | ((uint)s << 21) | ((uint)a << 16) | (uint)(offset & 0xFFFF);
private static uint Add(int d, int a, int b) =>
0x7C000214u | ((uint)d << 21) | ((uint)a << 16) | ((uint)b << 11);
private static uint Cmpwi(int crf, int a, int simm) =>
0x2C000000u | ((uint)crf << 23) | ((uint)a << 16) | (uint)(simm & 0xFFFF);
private static uint Mflr(int d) => 0x7C0802A6u | ((uint)d << 21);
private static uint Bctrl() => 0x4E800421u;
private static uint Bctr() => 0x4E800420u;
private static ProgramImage BuildImage(params (uint Address, uint Word)[] words)
{
var memory = new byte[ImageSize];
foreach (var (address, word) in words)
{
BinaryPrimitives.WriteUInt32BigEndian(
memory.AsSpan((int)(address - MemoryLayout.RamBase), 4), word);
}
return new ProgramImage(
memory,
AddressRange.FromStartAndSize(MemoryLayout.RamBase, ImageSize),
AddressRange.FromStartAndSize(MemoryLayout.RamBase, ImageSize),
default,
"leaf-inlining");
}
private static TranslationOptions Options(
bool inlining = true,
IReadOnlySet<uint>? blocked = null,
int maxCalleeInstructions = 32,
int maxGrowthPercent = 50,
bool multiBlock = true) =>
TranslationOptions.Default with
{
AllowUnsupportedInstructions = true,
KnownFunctionEntryPoints = new HashSet<uint> { Caller, Callee, Callee2 },
EnableLeafInlining = inlining,
LeafInliningBlockedTargets = blocked,
LeafInliningMaxCalleeInstructions = maxCalleeInstructions,
LeafInliningMaxCallerGrowthPercent = maxGrowthPercent,
LeafInliningAllowMultiBlockCallees = multiBlock
};
private static FunctionTranslationResult Translate(
ProgramImage image, uint entryPoint, TranslationOptions options) =>
new FunctionTranslator(image).Translate(entryPoint, options);
/// <summary>Caller that calls the leaf once and returns.</summary>
private static (uint Address, uint Word)[] SimpleCaller(uint callee = Callee) =>
[
(Caller + 0, Addi(3, 3, 4)),
(Caller + 4, Bl(Caller + 4, callee)),
(Caller + 8, Addi(3, 3, 1)),
(Caller + 12, Blr())
];
// --- positive cases -----------------------------------------------------
[Fact]
public void CalleeWritesLandInTheCallerAndTheCallDisappears()
{
var image = BuildImage(
[
.. SimpleCaller(),
(Callee + 0, Lwz(3, 3, 8)),
(Callee + 4, Blr())
]);
var inlined = Translate(image, Caller, Options());
var reference = Translate(image, Caller, Options(inlining: false));
Assert.Equal(1, inlined.Metrics.InlinedCallSites);
Assert.Equal(2, inlined.Metrics.InlinedGuestInstructions);
Assert.Equal(0, reference.Metrics.InlinedCallSites);
Assert.Contains("InvokeDirectCpu<0x80000100u>(ctx);", reference.CxxCode, StringComparison.Ordinal);
Assert.DoesNotContain("InvokeDirectCpu<0x80000100u>(ctx);", inlined.CxxCode);
// The callee's load is now a load of the caller's own r3.
Assert.Contains("MemoryInline::FlatRead32", inlined.CxxCode, StringComparison.Ordinal);
Assert.Contains("inline leaf 0x80000100", inlined.CxxCode, StringComparison.Ordinal);
}
[Fact]
public void ArgumentRegistersAreReadFromTheCallerStateAtTheCallPoint()
{
// The caller computes r3 = r3 + 4 immediately before the call, and the
// callee's first act is to consume r3. After the splice, the callee's
// read must see the +4 value without any context round trip.
var image = BuildImage(
[
.. SimpleCaller(),
(Callee + 0, Add(3, 3, 4)),
(Callee + 4, Blr())
]);
var result = Translate(image, Caller, Options());
Assert.Equal(1, result.Metrics.InlinedCallSites);
// Residency keeps r3 in a local across the splice, so there is no flush
// or reload of the guest register file where the call used to be.
Assert.DoesNotContain("InvokeDirectCpu<0x80000100u>(ctx);", result.CxxCode);
Assert.Contains("r3 = ", result.CxxCode, StringComparison.Ordinal);
var ir = result.LinearIr.Blocks.SelectMany(block => block.Instructions).ToArray();
Assert.DoesNotContain(ir, instruction => instruction is IrCall call && call.Target == "0x80000100");
}
[Fact]
public void ConditionRegisterWrittenInsideTheSpliceReachesTheCallersBranch()
{
// The callee sets CR0; the caller branches on it. Fusing the two across
// the splice is only possible because the compare became an ordinary
// instruction of the caller.
var image = BuildImage(
[
(Caller + 0, Bl(Caller + 0, Callee)),
(Caller + 4, Bne(Caller + 4, Caller + 12)),
(Caller + 8, Addi(3, 3, 1)),
(Caller + 12, Blr()),
(Callee + 0, Cmpwi(0, 4, 0)),
(Callee + 4, Blr())
]);
var inlined = Translate(image, Caller, Options());
var reference = Translate(image, Caller, Options(inlining: false));
Assert.Equal(1, inlined.Metrics.InlinedCallSites);
// Without inlining the branch has to reload the architectural CR after
// the call because the producing compare lives in another function.
Assert.Contains("InvokeDirectCpu<0x80000100u>(ctx);", reference.CxxCode, StringComparison.Ordinal);
Assert.Equal(2, CountOccurrences(reference.CxxCode, "cr = ctx->cr;"));
// With inlining the compare writes the caller's resident CR directly and
// the branch reads it with no context round trip at all.
Assert.Contains(
"SetCRResident(cr, xer, 0, static_cast<int32_t>(r4), static_cast<int32_t>(0));",
inlined.CxxCode, StringComparison.Ordinal);
Assert.Equal(1, CountOccurrences(inlined.CxxCode, "cr = ctx->cr;"));
}
[Fact]
public void CompareInsideTheSpliceIsFusedIntoTheFollowingBranch()
{
// Both successors immediately overwrite CR0, which is what makes the
// spliced compare's only consumer the caller's branch.
var image = BuildImage(
[
(Caller + 0, Bl(Caller + 0, Callee)),
(Caller + 4, Bne(Caller + 4, Caller + 16)),
(Caller + 8, Cmpwi(0, 3, 0)),
(Caller + 12, Blr()),
(Caller + 16, Cmpwi(0, 5, 0)),
(Caller + 20, Blr()),
(Callee + 0, Cmpwi(0, 4, 7)),
(Callee + 4, Blr())
]);
var code = Translate(image, Caller, Options()).CxxCode;
Assert.Contains(
"if ((static_cast<int32_t>(r4) != static_cast<int32_t>(7)))",
code, StringComparison.Ordinal);
}
[Fact]
public void TheLinkRegisterWriteOfAnInlinedCallIsNotEmitted()
{
var image = BuildImage(
[
.. SimpleCaller(),
(Callee + 0, Addi(4, 4, 1)),
(Callee + 4, Blr())
]);
var code = Translate(image, Caller, Options()).CxxCode;
Assert.DoesNotContain("ctx->lr = 0x80000008", code);
Assert.DoesNotContain("lr = 0x80000008u", code);
}
[Fact]
public void TheStandaloneCalleeBodyIsStillTranslatedOnItsOwn()
{
// Indirect dispatch, mod overrides and the registry all resolve the
// callee by address, so the splice must never be a replacement for it.
var image = BuildImage(
[
.. SimpleCaller(),
(Callee + 0, Lwz(3, 3, 8)),
(Callee + 4, Blr())
]);
var standalone = Translate(image, Callee, Options());
Assert.Contains("func_80000100", standalone.CxxCode, StringComparison.Ordinal);
Assert.Equal(0, standalone.Metrics.InlinedCallSites);
}
[Fact]
public void EveryCallSiteOfTheSameCalleeIsSplicedIndependently()
{
var image = BuildImage(
[
(Caller + 0, Bl(Caller + 0, Callee)),
(Caller + 4, Bl(Caller + 4, Callee)),
(Caller + 8, Blr()),
(Callee + 0, Lwz(3, 3, 8)),
(Callee + 4, Blr())
]);
var result = Translate(image, Caller, Options());
Assert.Equal(2, result.Metrics.InlinedCallSites);
Assert.Equal(2, CountOccurrences(result.CxxCode, "inline leaf 0x80000100"));
Assert.DoesNotContain("InvokeDirectCpu<0x80000100u>(ctx);", result.CxxCode);
}
[Fact]
public void TailCallToALeafIsSplicedAheadOfTheCallersReturn()
{
var image = BuildImage(
[
(Caller + 0, Addi(3, 3, 4)),
(Caller + 4, B(Caller + 4, Callee)),
(Callee + 0, Lwz(3, 3, 8)),
(Callee + 4, Blr())
]);
var result = Translate(image, Caller, Options());
Assert.Equal(1, result.Metrics.InlinedCallSites);
Assert.DoesNotContain("InvokeDirectCpu<0x80000100u>(ctx);", result.CxxCode);
}
// --- acyclic multi-block callees ----------------------------------------
/// <summary>Compare, branch, one arm, join - the shape of nw4r list walkers.</summary>
private static (uint Address, uint Word)[] DiamondCallee() =>
[
(Callee + 0, Cmpwi(0, 3, 0)),
(Callee + 4, Bne(Callee + 4, Callee + 12)),
(Callee + 8, Addi(3, 3, 1)),
(Callee + 12, Blr())
];
[Fact]
public void AnAcyclicMultiBlockCalleeIsSplicedWithItsOwnControlFlow()
{
var image = BuildImage([.. SimpleCaller(), .. DiamondCallee()]);
var result = Translate(image, Caller, Options());
Assert.Equal(1, result.Metrics.InlinedCallSites);
Assert.Equal(4, result.Metrics.InlinedGuestInstructions);
Assert.DoesNotContain("InvokeDirectCpu<0x80000100u>(ctx);", result.CxxCode);
// The callee's blocks are now blocks of the caller, under call-site
// local labels, and the branch skips the arm exactly as it did inside
// the callee.
Assert.Contains("loc_inl0_0x80000100:", result.CxxCode, StringComparison.Ordinal);
Assert.Contains("loc_inl0_0x80000108:", result.CxxCode, StringComparison.Ordinal);
Assert.Contains("loc_inl0_0x8000010C:", result.CxxCode, StringComparison.Ordinal);
Assert.Contains("goto loc_inl0_0x8000010C;", result.CxxCode, StringComparison.Ordinal);
// The callee's return became the caller's continuation, which is where
// the code after the call site now lives.
Assert.Contains("loc_inl0_cont_80000100:", result.CxxCode, StringComparison.Ordinal);
Assert.Equal(
new IrJump("inl0_cont_80000100"),
Block(result, "inl0_0x8000010C").Instructions[^1]);
result.Ssa.ValidateUseDef();
}
[Fact]
public void TheCallersOwnCodeAroundAMultiBlockSpliceStillRuns()
{
// The call site sits in the middle of the caller's only block, so the
// splice has to cut that block in two and keep both halves wired up.
var image = BuildImage([.. SimpleCaller(), .. DiamondCallee()]);
var result = Translate(image, Caller, Options());
var labels = result.LinearIr.Blocks.Select(block => block.Label).ToArray();
// Entry block keeps its label, so the function still starts where the
// emitter's prologue jumps.
Assert.Equal("0x80000000", labels[0]);
Assert.Equal(result.LinearIr.EntryLabel, labels[0]);
Assert.Equal(labels.Length, labels.Distinct(StringComparer.OrdinalIgnoreCase).Count());
// Caller prefix, the callee's three blocks, then the continuation that
// carries everything the caller did after the call.
Assert.Equal(
[
"0x80000000",
"inl0_0x80000100",
"inl0_0x80000104",
"inl0_0x80000108",
"inl0_0x8000010C",
"inl0_cont_80000100"
],
labels);
Assert.IsType<IrReturn>(result.LinearIr.Blocks[^1].Instructions[^1]);
}
[Fact]
public void EachMultiBlockCallSiteGetsItsOwnCopyOfTheCalleesLabels()
{
var image = BuildImage(
[
(Caller + 0, Bl(Caller + 0, Callee)),
(Caller + 4, Bl(Caller + 4, Callee)),
(Caller + 8, Blr()),
.. DiamondCallee()
]);
var result = Translate(image, Caller, Options(maxGrowthPercent: 100_000));
Assert.Equal(2, result.Metrics.InlinedCallSites);
Assert.Contains("loc_inl0_cont_80000100:", result.CxxCode, StringComparison.Ordinal);
Assert.Contains("loc_inl1_cont_80000100:", result.CxxCode, StringComparison.Ordinal);
result.Ssa.ValidateUseDef();
}
[Fact]
public void SplicedLabelsCanNeverBeReadBackAsAGuestAddress()
{
// A `loc_XXXXXXXX` label is how the emitter reports an interior resume
// point of the caller. A spliced copy of the callee is not one, so its
// labels must not parse as an address.
var image = BuildImage([.. SimpleCaller(), .. DiamondCallee()]);
var result = Translate(image, Caller, Options());
var spliced = result.LinearIr.Blocks
.Select(block => block.Label)
.Where(label => label.StartsWith("inl", StringComparison.Ordinal))
.ToArray();
Assert.NotEmpty(spliced);
foreach (var label in spliced)
{
Assert.False(
uint.TryParse(label, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _),
$"Spliced label '{label}' is readable as a guest address.");
}
}
[Fact]
public void ARaisedInstructionCapAdmitsALeafTheOldCapRefused()
{
// 45 arithmetic instructions plus the terminating blr: past the old cap
// of 32, inside the current one.
var words = new List<(uint, uint)>(SimpleCaller());
for (var index = 0; index < 45; index++)
{
words.Add((Callee + (uint)(index * 4), Addi(3, 3, 1)));
}
words.Add((Callee + 180, Blr()));
var image = BuildImage([.. words]);
Assert.Equal(56, TranslationOptions.Default.LeafInliningMaxCalleeInstructions);
Assert.Equal(
1,
Translate(image, Caller, Options(maxCalleeInstructions: 56, maxGrowthPercent: 100_000))
.Metrics.InlinedCallSites);
Assert.Equal(
0,
Translate(image, Caller, Options(maxCalleeInstructions: 32, maxGrowthPercent: 100_000))
.Metrics.InlinedCallSites);
}
// --- rejection cases ----------------------------------------------------
[Fact]
public void AMultiBlockCalleeIsRejectedWhenMultiBlockSplicingIsOff()
{
var image = BuildImage([.. SimpleCaller(), .. DiamondCallee()]);
Assert.Equal(0, Translate(image, Caller, Options(multiBlock: false)).Metrics.InlinedCallSites);
Assert.Equal(1, Translate(image, Caller, Options()).Metrics.InlinedCallSites);
}
[Fact]
public void ACalleeWithALoopIsRejected()
{
// The back edge to the entry makes this a loop, not a one-way region
// between the call point and its continuation.
var image = BuildImage(
[
.. SimpleCaller(),
(Callee + 0, Addi(4, 4, -1)),
(Callee + 4, Cmpwi(0, 4, 0)),
(Callee + 8, Bne(Callee + 8, Callee + 0)),
(Callee + 12, Blr())
]);
AssertNotInlined(image);
}
[Fact]
public void AMultiBlockCalleeThatTouchesTheLinkRegisterIsRejected()
{
// Reachable only on one arm, and still disqualifying: the call's own LR
// write is dropped, so the mflr would read the caller's incoming value.
var image = BuildImage(
[
.. SimpleCaller(),
(Callee + 0, Cmpwi(0, 3, 0)),
(Callee + 4, Bne(Callee + 4, Callee + 12)),
(Callee + 8, Mflr(0)),
(Callee + 12, Blr())
]);
AssertNotInlined(image);
}
[Fact]
public void AMultiBlockCalleeThatCallsAnotherFunctionIsRejected()
{
var image = BuildImage(
[
.. SimpleCaller(),
(Callee + 0, Cmpwi(0, 3, 0)),
(Callee + 4, Bne(Callee + 4, Callee + 12)),
(Callee + 8, Bl(Callee + 8, Callee2)),
(Callee + 12, Blr()),
(Callee2 + 0, Addi(3, 3, 1)),
(Callee2 + 4, Blr())
]);
AssertNotInlined(image);
}
[Fact]
public void AMultiBlockCalleeIsStillSubjectToTheBlockList()
{
var image = BuildImage([.. SimpleCaller(), .. DiamondCallee()]);
// A patch site on the callee's second block is still a patch site.
Assert.Equal(
0,
Translate(image, Caller, Options(blocked: new HashSet<uint> { Callee + 8 })).Metrics.InlinedCallSites);
}
[Fact]
public void AdmittingMultiBlockCalleesCannotChangeACallerThatHasNone()
{
// The invariant the whole transform rests on: a caller whose admitted
// set does not change must emit byte-identical text.
var image = BuildImage(
[
.. SimpleCaller(),
(Callee + 0, Lwz(3, 3, 8)),
(Callee + 4, Blr())
]);
var withMultiBlock = Translate(image, Caller, Options());
var withoutMultiBlock = Translate(image, Caller, Options(multiBlock: false));
var atTheOldCap = Translate(image, Caller, Options(maxCalleeInstructions: 32));
var atTheNewCap = Translate(image, Caller, Options(maxCalleeInstructions: 56));
Assert.Equal(1, withMultiBlock.Metrics.InlinedCallSites);
Assert.Equal(withoutMultiBlock.CxxCode, withMultiBlock.CxxCode);
Assert.Equal(atTheOldCap.CxxCode, atTheNewCap.CxxCode);
}
[Fact]
public void InliningIsOffUnlessRequested()
{
var image = BuildImage(
[
.. SimpleCaller(),
(Callee + 0, Lwz(3, 3, 8)),
(Callee + 4, Blr())
]);
var result = Translate(image, Caller, Options(inlining: false));
Assert.Equal(0, result.Metrics.InlinedCallSites);
Assert.Contains("InvokeDirectCpu<0x80000100u>(ctx);", result.CxxCode, StringComparison.Ordinal);
}
[Fact]
public void ACalleeThatCallsAnotherFunctionIsRejected()
{
var image = BuildImage(
[
.. SimpleCaller(),
(Callee + 0, Bl(Callee + 0, Callee2)),
(Callee + 4, Blr()),
(Callee2 + 0, Addi(3, 3, 1)),
(Callee2 + 4, Blr())
]);
AssertNotInlined(image);
}
[Fact]
public void ACalleeWithAnIndirectCallIsRejected()
{
var image = BuildImage(
[
.. SimpleCaller(),
(Callee + 0, Bctrl()),
(Callee + 4, Blr())
]);
AssertNotInlined(image);
}
[Fact]
public void ACalleeWithAnIndirectBranchIsRejected()
{
var image = BuildImage(
[
.. SimpleCaller(),
(Callee + 0, Bctr())
]);
AssertNotInlined(image);
}
[Fact]
public void ACalleeThatTouchesTheLinkRegisterIsRejected()
{
// The call's own LR write is dropped, so a callee that observes LR
// would read the caller's incoming value instead of a return address.
var image = BuildImage(
[
.. SimpleCaller(),
(Callee + 0, Mflr(0)),
(Callee + 4, Blr())
]);
AssertNotInlined(image);
}
[Fact]
public void ACalleeThatCreatesAStackFrameIsRejected()
{
// Splicing a second r1 epoch into the caller invalidates the stack
// aliasing model used after inlining, even when the callee restores r1.
var image = BuildImage(
[
.. SimpleCaller(),
(Callee + 0, Stwu(1, 1, -16)),
(Callee + 4, Addi(3, 3, 1)),
(Callee + 8, Addi(1, 1, 16)),
(Callee + 12, Blr())
]);
AssertNotInlined(image);
}
[Fact]
public void AnOversizedCalleeIsRejected()
{
var body = new List<(uint, uint)>(SimpleCaller());
for (var index = 0; index < 8; index++)
{
body.Add((Callee + (uint)(index * 4), Addi(3, 3, 1)));
}
body.Add((Callee + 32, Blr()));
var image = BuildImage([.. body]);
Assert.Equal(1, Translate(image, Caller, Options(maxCalleeInstructions: 16)).Metrics.InlinedCallSites);
Assert.Equal(0, Translate(image, Caller, Options(maxCalleeInstructions: 4)).Metrics.InlinedCallSites);
}
[Fact]
public void ABlockedAddressAnywhereInsideTheCalleeIsRejected()
{
// A native registration or mod patch inside the decoded body means the
// runtime winner is not these bytes.
var image = BuildImage(
[
.. SimpleCaller(),
(Callee + 0, Lwz(3, 3, 8)),
(Callee + 4, Addi(3, 3, 1)),
(Callee + 8, Blr())
]);
Assert.Equal(
0,
Translate(image, Caller, Options(blocked: new HashSet<uint> { Callee })).Metrics.InlinedCallSites);
Assert.Equal(
0,
Translate(image, Caller, Options(blocked: new HashSet<uint> { Callee + 4 })).Metrics.InlinedCallSites);
Assert.Equal(1, Translate(image, Caller, Options()).Metrics.InlinedCallSites);
}
[Fact]
public void GrowthBudgetStopsInliningOnceTheCallerWouldBalloon()
{
var words = new List<(uint, uint)>
{
(Caller + 0, Bl(Caller + 0, Callee)),
(Caller + 4, Bl(Caller + 4, Callee)),
(Caller + 8, Bl(Caller + 8, Callee)),
(Caller + 12, Blr())
};
for (var index = 0; index < 12; index++)
{
words.Add((Callee + (uint)(index * 4), Addi(3, 3, 1)));
}
words.Add((Callee + 48, Blr()));
var image = BuildImage([.. words]);
var generous = Translate(image, Caller, Options(maxGrowthPercent: 100_000)).Metrics.InlinedCallSites;
var stingy = Translate(image, Caller, Options(maxGrowthPercent: 0)).Metrics.InlinedCallSites;
Assert.Equal(3, generous);
Assert.InRange(stingy, 0, 2);
}
[Fact]
public void ContinuationDispatchDisablesInliningForTheWholeCaller()
{
// Non-returning and continuation call sites place labels keyed by guest
// instruction address inside the emitted body; a duplicated guest
// instruction stream cannot coexist with that.
var image = BuildImage(
[
.. SimpleCaller(),
(Callee + 0, Lwz(3, 3, 8)),
(Callee + 4, Blr())
]);
var options = Options() with
{
LrContinuationCallTargets = new HashSet<uint> { Callee2 }
};
Assert.Equal(0, Translate(image, Caller, options).Metrics.InlinedCallSites);
}
[Fact]
public void ModuleTranslationsAreNeverInlined()
{
var image = BuildImage(
[
.. SimpleCaller(),
(Callee + 0, Lwz(3, 3, 8)),
(Callee + 4, Blr())
]);
var options = Options() with
{
ModuleLinkBase = 0x80800000u,
ModuleGuestBase = 0x80900000u,
ModuleLinkedCodeSize = 0x1000
};
Assert.Equal(0, Translate(image, Caller, options).Metrics.InlinedCallSites);
}
[Fact]
public void ASelfRecursiveCallIsNeverInlined()
{
var image = BuildImage(
[
(Caller + 0, Bl(Caller + 0, Caller)),
(Caller + 4, Blr())
]);
Assert.Equal(0, Translate(image, Caller, Options()).Metrics.InlinedCallSites);
}
// --- classifier unit tests ---------------------------------------------
[Fact]
public void CandidateClassifierReportsWhyACalleeWasRefused()
{
var policy = new LeafInliningPolicy();
LeafFunctionInliner.TryCreateCandidate(
0x80000100u,
new IrFunction("two_blocks", "a",
[
new IrBasicBlock("a", [new IrJump("b")]),
new IrBasicBlock("b", [new IrReturn(null)])
]),
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
policy,
out var jumpBetweenBlocks);
Assert.Equal(LeafInlineRejection.ControlFlow, jumpBetweenBlocks);
LeafFunctionInliner.TryCreateCandidate(
0x80000100u,
new IrFunction("many_blocks", "a",
[
new IrBasicBlock("a", []),
new IrBasicBlock("b", []),
new IrBasicBlock("c", []),
new IrBasicBlock("d", []),
new IrBasicBlock("e", [new IrReturn(null)])
]),
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
policy,
out var multiBlock);
Assert.Equal(LeafInlineRejection.MultipleBlocks, multiBlock);
LeafFunctionInliner.TryCreateCandidate(
0x80000100u,
Single(new IrCall(string.Empty, "0x80002000", Array.Empty<IrValue>()), new IrReturn(null)),
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
policy,
out var guestCall);
Assert.Equal(LeafInlineRejection.GuestCall, guestCall);
LeafFunctionInliner.TryCreateCandidate(
0x80000100u,
Single(new IrCall(string.Empty, "OSSystemCall", Array.Empty<IrValue>()), new IrReturn(null)),
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
policy,
out var fence);
Assert.Equal(LeafInlineRejection.OpaqueHelper, fence);
LeafFunctionInliner.TryCreateCandidate(
0x80000100u,
Single(new IrAssign("r0", IrValue.Register("lr")), new IrReturn(null)),
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
policy,
out var link);
Assert.Equal(LeafInlineRejection.TouchesLinkRegister, link);
LeafFunctionInliner.TryCreateCandidate(
0x80000100u,
Single(new IrAssign("r1", IrValue.Imm(0)), new IrReturn(null)),
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
policy,
out var stackPointer);
Assert.Equal(LeafInlineRejection.TouchesStackPointer, stackPointer);
LeafFunctionInliner.TryCreateCandidate(
0x80000100u,
Single(new IrAssign("r3", IrValue.Imm(1))),
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
policy,
out var noReturn);
Assert.Equal(LeafInlineRejection.NoTerminatingReturn, noReturn);
var accepted = LeafFunctionInliner.TryCreateCandidate(
0x80000100u,
Single(
new IrTracePpc(0x80000100u, "addi r3,r3,1", "0x38630001"),
new IrAssign("r3", IrValue.Imm(1)),
new IrReturn(null)),
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
policy,
out var none);
Assert.Equal(LeafInlineRejection.None, none);
Assert.NotNull(accepted);
Assert.Equal(1, accepted!.GuestInstructionCount);
// The terminating return became a fall-through.
Assert.DoesNotContain(accepted.Body, instruction => instruction is IrReturn);
}
[Fact]
public void CandidateClassifierAdmitsAnAcyclicRegionAndRefusesALoop()
{
var policy = new LeafInliningPolicy();
// if (cr0) goto join; arm; join: return
var diamond = LeafFunctionInliner.TryCreateCandidate(
0x80000100u,
new IrFunction("diamond", "0x80000100",
[
new IrBasicBlock("0x80000100",
[
new IrTracePpc(0x80000100u, "cmpwi r3,0", "0x2C030000"),
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(0), false),
new IrTracePpc(0x80000104u, "bne 0x8000010C", "0x40820008"),
new IrBranch("ne", "0x8000010C", "0x80000108")
]),
new IrBasicBlock("0x80000108",
[
new IrTracePpc(0x80000108u, "addi r3,r3,1", "0x38630001"),
new IrAssign("r3", IrValue.Imm(1))
]),
new IrBasicBlock("0x8000010C",
[
new IrTracePpc(0x8000010Cu, "blr", "0x4E800020"),
new IrReturn(null)
])
]),
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
policy,
out var acyclic);
Assert.Equal(LeafInlineRejection.None, acyclic);
Assert.NotNull(diamond);
Assert.Equal(4, diamond!.GuestInstructionCount);
Assert.Empty(diamond.Body);
Assert.NotNull(diamond.Blocks);
Assert.Equal(3, diamond.Blocks!.Count);
// The arm's implicit fall-through onto the join is materialized, so the
// splice does not depend on where the caller places these blocks.
Assert.Equal(new IrJump("0x8000010C"), diamond.Blocks[1].Instructions[^1]);
// Same shape, but the branch goes back to the entry.
LeafFunctionInliner.TryCreateCandidate(
0x80000100u,
new IrFunction("loop", "0x80000100",
[
new IrBasicBlock("0x80000100",
[
new IrTracePpc(0x80000100u, "cmpwi r3,0", "0x2C030000"),
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(0), false),
new IrTracePpc(0x80000104u, "bne 0x80000100", "0x4082FFFC"),
new IrBranch("ne", "0x80000100", "0x80000108")
]),
new IrBasicBlock("0x80000108",
[
new IrTracePpc(0x80000108u, "blr", "0x4E800020"),
new IrReturn(null)
])
]),
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
policy,
out var cyclic);
Assert.Equal(LeafInlineRejection.CyclicControlFlow, cyclic);
// Turning the shape off restores the straight-line-only verdict.
LeafFunctionInliner.TryCreateCandidate(
0x80000100u,
new IrFunction("diamond", "0x80000100",
[
new IrBasicBlock("0x80000100",
[
new IrTracePpc(0x80000100u, "b 0x80000104", "0x48000004"),
new IrJump("0x80000104")
]),
new IrBasicBlock("0x80000104",
[
new IrTracePpc(0x80000104u, "blr", "0x4E800020"),
new IrReturn(null)
])
]),
Array.Empty<Translator.Core.Disassembly.PpcInstruction>(),
policy with { AllowAcyclicMultiBlockCallees = false },
out var disabled);
Assert.Equal(LeafInlineRejection.ControlFlow, disabled);
}
private static IrFunction Single(params IrInstruction[] instructions) =>
new("leaf", "entry", [new IrBasicBlock("entry", instructions)]);
private static IrBasicBlock Block(FunctionTranslationResult result, string label) =>
result.LinearIr.Blocks.Single(
block => string.Equals(block.Label, label, StringComparison.Ordinal));
private static void AssertNotInlined(ProgramImage image)
{
var result = Translate(image, Caller, Options());
Assert.Equal(0, result.Metrics.InlinedCallSites);
Assert.Contains("InvokeDirectCpu<0x80000100u>(ctx);", result.CxxCode, StringComparison.Ordinal);
}
private static int CountOccurrences(string haystack, string needle)
{
var count = 0;
var index = haystack.IndexOf(needle, StringComparison.Ordinal);
while (index >= 0)
{
count++;
index = haystack.IndexOf(needle, index + needle.Length, StringComparison.Ordinal);
}
return count;
}
}
@@ -0,0 +1,100 @@
using System.Collections.Generic;
using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis.Ssa;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
public class LeafRegisterCacheCodeGenTests
{
// Text-rewritten "cached_*" locals survive only for the state-free clone.
// Register residency owns every primary body and achieves the same property
// (a CR access promoted once to a local, reused for every subsequent
// read/write in the function) directly at emission time, under its own
// plain-named locals - so that is what these tests exercise.
private static string Emit(IrFunction function, RepresentationEnvironment types, uint entryPoint) =>
new CxxLinearCodeGenerator().Emit(
entryPoint,
new SsaTransformer().Convert(function),
new FunctionAbiClassification(function.Name, ValueRepresentation.Void),
types);
[Fact]
public void LeafCachedConditionRegisterBitReadUsesTheResidentHelper()
{
// PpcLifter builds the general bc/bcctr condition as raw C++ text
// containing GetCRBit(ctx, field, bit). When the leaf register cache owns
// CR, that read has to move onto the local as well: otherwise the branch
// observes the stale architectural CR while SetCRResident is updating
// cached_cr, which is a live miscompile in shipped shards.
var function = new IrFunction(
"leaf_cached_cr_bit",
"entry",
new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Register("r4"), false),
new IrBranch("raw", "taken", "exit", "((true) && ((GetCRBit(ctx, 0, 0) == true)))")
}),
new IrBasicBlock("taken", new IrInstruction[]
{
new IrBinary("r3", IrValue.Register("r3"), IrValue.Imm(1), "add"),
new IrReturn(null)
}),
new IrBasicBlock("exit", new IrInstruction[] { new IrReturn(null) })
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["r3"] = ValueRepresentation.UInt32,
["r4"] = ValueRepresentation.UInt32
});
var code = Emit(function, types, 0x8000622Au);
Assert.Contains("uint32_t cr = ctx->cr;", code, StringComparison.Ordinal);
Assert.Contains("SetCRResident(cr, xer, 0,", code, StringComparison.Ordinal);
Assert.Contains("GetCRBitResident(cr, 0, 0)", code, StringComparison.Ordinal);
Assert.DoesNotContain("GetCRBit(ctx,", code, StringComparison.Ordinal);
Assert.Contains(" ctx->cr = cr;", code, StringComparison.Ordinal);
}
[Fact]
public void LeafCachedConditionRegisterBitReadWithoutAnyCrWriteStillLocalizesTheRead()
{
// A body whose only CR access is the raw condition read must declare the
// local, use it, and not write CR back: cached_cr is a pure copy there.
var function = new IrFunction(
"leaf_cached_cr_bit_read_only",
"entry",
new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrBranch("raw", "taken", "exit", "((true) && ((GetCRBit(ctx, 1, 2) == true)))")
}),
new IrBasicBlock("taken", new IrInstruction[]
{
new IrBinary("r3", IrValue.Register("r3"), IrValue.Imm(1), "add"),
new IrReturn(null)
}),
new IrBasicBlock("exit", new IrInstruction[] { new IrReturn(null) })
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["r3"] = ValueRepresentation.UInt32
});
var code = Emit(function, types, 0x8000622Cu);
Assert.Contains("uint32_t cr = ctx->cr;", code, StringComparison.Ordinal);
Assert.Contains("GetCRBitResident(cr, 1, 2)", code, StringComparison.Ordinal);
Assert.DoesNotContain("GetCRBit(ctx,", code, StringComparison.Ordinal);
Assert.DoesNotContain("ctx->cr = cr;", code, StringComparison.Ordinal);
}
}
@@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Linq;
using Translator.Core.Disassembly;
using Translator.Core.Ir;
using Translator.Core.Lifting;
using Xunit;
namespace Translator.Tests;
public class LifterTests
{
[Fact]
public void LiftsSimpleBlock()
{
// Synthetic block: li r3,5; mr r4,r3; blr
var instructions = new List<PpcInstruction>
{
PpcInstruction.Synthetic(0x80000000, 0x38600005, "li", new PpcOperand[] { new PpcRegisterOperand("r3", 3), new PpcImmediateOperand(5) }),
PpcInstruction.Synthetic(0x80000004, 0x7C832378, "mr", new PpcOperand[] { new PpcRegisterOperand("r4", 4), new PpcRegisterOperand("r3", 3), new PpcRegisterOperand("r3", 3) }),
PpcInstruction.Synthetic(0x80000008, 0x4E800020, "blr", Array.Empty<PpcOperand>())
};
var lifter = new PpcLifter();
var lifted = lifter.Lift(instructions);
Assert.Equal(instructions, lifted.Select(item => item.Origin));
Assert.IsType<IrAssign>(lifted[0].Ir.First());
Assert.IsType<IrAssign>(lifted[1].Ir.First());
Assert.IsType<IrReturn>(lifted[2].Ir.Single());
}
}
@@ -0,0 +1,145 @@
using Translator.Core.Analysis;
using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis.Ssa;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
namespace Translator.Tests;
public sealed class LocalCodeGenOptimizationTests
{
[Fact]
public void DeadPureLocalAssignmentsAreRemovedButMemoryReadsRemainObservable()
{
var function = new IrFunction("dead_pure_locals", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrAssign("dead0", IrValue.Imm(7)),
new IrBinary("dead1", IrValue.Register("dead0"), IrValue.Imm(5), "add"),
new IrLoad("dead_load", new IrAddress("r3", 0), 4),
new IrReturn(null)
})
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["dead0"] = ValueRepresentation.UInt32,
["dead1"] = ValueRepresentation.UInt32,
["dead_load"] = ValueRepresentation.UInt32,
["r3"] = ValueRepresentation.UInt32
});
var code = Emit(function, types, 0x80008010);
Assert.DoesNotContain("dead0", code, StringComparison.Ordinal);
Assert.DoesNotContain("dead1", code, StringComparison.Ordinal);
Assert.Contains("MemoryInline::FlatRead32(r3)", code, StringComparison.Ordinal);
}
[Fact]
public void FctiwLowWordLoadUsesNativeExtractionAndElidesPrivateStackStore()
{
var function = FctiwFunction(includeGuestCallAfterLoad: false);
var code = Emit(function, FctiwTypes(), 0x80008014);
Assert.Contains("PPC_FprLowWordInline(f0.d)", code, StringComparison.Ordinal);
Assert.Contains("r3 = fctiwzword0;", code, StringComparison.Ordinal);
Assert.DoesNotContain("MemoryInline::FlatWriteFloat64((r1 + -16)", code, StringComparison.Ordinal);
Assert.DoesNotContain("MemoryInline::FlatRead32((r1 + -12))", code, StringComparison.Ordinal);
}
[Fact]
public void FctiwPrivateStackStoreRemainsVisibleAcrossGuestCall()
{
var function = FctiwFunction(includeGuestCallAfterLoad: true);
var code = Emit(function, FctiwTypes(), 0x80008018);
Assert.Contains("PPC_FprLowWordInline(f0.d)", code, StringComparison.Ordinal);
Assert.Contains("MemoryInline::FlatWriteRamFloat64((r1 + -16)", code, StringComparison.Ordinal);
Assert.Contains("InvokeDirectCpu<0x80001234u>(ctx);", code, StringComparison.Ordinal);
}
[Fact]
public void OverwrittenPairedStackRestoreIsRemoved()
{
var function = PairedRestoreFunction(usePairedValue: false);
var code = Emit(function, PairedRestoreTypes(), 0x8000801C);
Assert.DoesNotContain("PPC_PsqL", code, StringComparison.Ordinal);
Assert.DoesNotContain("tmp_psq_load", code, StringComparison.Ordinal);
Assert.Contains("MemoryInline::FlatReadFloat64((r1 + 112))", code, StringComparison.Ordinal);
}
[Fact]
public void PairedStackRestoreIsKeptWhenConsumedBeforeScalarRestore()
{
var function = PairedRestoreFunction(usePairedValue: true);
var code = Emit(function, PairedRestoreTypes(), 0x80008020);
Assert.Contains("PPC_PsqLStackInline<0u, 0u>", code, StringComparison.Ordinal);
Assert.Contains("MemoryInline::FlatWriteRamFloat64((r1 + 40), f31.d)", code, StringComparison.Ordinal);
}
private static IrFunction FctiwFunction(bool includeGuestCallAfterLoad)
{
var instructions = new List<IrInstruction>
{
new IrBinary("f0", IrValue.Register("f1"), IrValue.Imm(0), "fctiwz"),
new IrStore(new IrAddress("r1", -16), IrValue.Register("f0"), 8),
new IrLoad("r3", new IrAddress("r1", -12), 4)
};
if (includeGuestCallAfterLoad)
instructions.Add(new IrCall(string.Empty, "func_80001234", Array.Empty<IrValue>()));
instructions.Add(new IrReturn(null));
return new IrFunction("fctiw_stack", "entry", new[]
{
new IrBasicBlock("entry", instructions)
});
}
private static RepresentationEnvironment FctiwTypes() => new(new Dictionary<string, ValueRepresentation>
{
["r1"] = ValueRepresentation.UInt32,
["r3"] = ValueRepresentation.UInt32,
["f0"] = ValueRepresentation.Float64,
["f1"] = ValueRepresentation.Float64
});
private static IrFunction PairedRestoreFunction(bool usePairedValue)
{
var instructions = new List<IrInstruction>
{
new IrBinary("tmp_psq_load", IrValue.Register("r1"), IrValue.Imm(120), "add"),
new IrCall("f31", "PPC_PsqL", new[]
{
IrValue.Register("tmp_psq_load"), IrValue.Imm(0), IrValue.Imm(0)
})
};
if (usePairedValue)
instructions.Add(new IrStore(new IrAddress("r1", 40), IrValue.Register("f31"), 8));
instructions.Add(new IrLoad("f31", new IrAddress("r1", 112), 8));
instructions.Add(new IrReturn(null));
return new IrFunction("paired_stack_restore", "entry", new[]
{
new IrBasicBlock("entry", instructions)
});
}
private static RepresentationEnvironment PairedRestoreTypes() => new(new Dictionary<string, ValueRepresentation>
{
["r1"] = ValueRepresentation.UInt32,
["f31"] = ValueRepresentation.Float64,
["tmp_psq_load"] = ValueRepresentation.UInt32
});
private static string Emit(
IrFunction function,
RepresentationEnvironment types,
uint address)
{
var ssa = new SsaTransformer().Convert(function);
var signature = new FunctionAbiClassification(function.Name, ValueRepresentation.Void);
return new CxxLinearCodeGenerator().Emit(address, ssa, signature, types);
}
}
@@ -0,0 +1,151 @@
using Translator.Core.Mods;
namespace Translator.Tests;
public sealed class ModDataPatchWriterTests
{
[Fact]
public void WriteEmitsBinaryBlobsAndStableAssemblyWithoutHexArrays()
{
var root = Path.Combine(Path.GetTempPath(), "mkw-mod-data-" + Guid.NewGuid().ToString("N"));
try
{
var cppPath = Path.Combine(root, "cpp", "mod_data_patches.cpp");
var moduleImage = Enumerable.Range(0, 257).Select(i => (byte)(i * 37)).ToArray();
var digest = Enumerable.Range(0, 20).Select(i => (byte)(0xA0 + i)).ToArray();
Write(cppPath, moduleImage, digest);
var blobRoot = Path.Combine(root, "cpp", "mod_data_patches_blobs");
Assert.Equal(moduleImage, File.ReadAllBytes(Path.Combine(blobRoot, "module_image.bin")));
Assert.Equal(digest, File.ReadAllBytes(Path.Combine(blobRoot, "kamek_code_sha1.bin")));
var cpp = File.ReadAllText(cppPath);
Assert.Contains("extern const uint8_t kModuleImage[];", cpp, StringComparison.Ordinal);
Assert.Contains("Memory::GetPointer(kModuleGuestBase, kModuleImageSize), kModuleImage, kModuleImageSize", cpp, StringComparison.Ordinal);
Assert.DoesNotContain("alignas(16) const uint8_t", cpp, StringComparison.Ordinal);
Assert.DoesNotContain("0x00, 0x25,", cpp, StringComparison.Ordinal);
var assemblyPath = Path.Combine(root, "cpp", "mod_data_patches_blobs.S");
var assembly = File.ReadAllText(assemblyPath);
Assert.Contains(".globl kModuleImage", assembly, StringComparison.Ordinal);
Assert.Contains(".globl kKamekCodeSha1Digest", assembly, StringComparison.Ordinal);
Assert.Contains(".incbin", assembly, StringComparison.Ordinal);
var timestamps = new[] { cppPath, assemblyPath }
.Concat(Directory.GetFiles(blobRoot, "*.bin"))
.ToDictionary(path => path, File.GetLastWriteTimeUtc);
Write(cppPath, moduleImage, digest);
foreach (var (path, timestamp) in timestamps)
{
Assert.Equal(timestamp, File.GetLastWriteTimeUtc(path));
}
}
finally
{
if (Directory.Exists(root))
{
Directory.Delete(root, recursive: true);
}
}
}
[Fact]
public void WritePrunesOptionalDigestWhenItDisappears()
{
var root = Path.Combine(Path.GetTempPath(), "mkw-mod-data-" + Guid.NewGuid().ToString("N"));
try
{
var cppPath = Path.Combine(root, "mod_data_patches.cpp");
Write(cppPath, [1, 2, 3, 4], new byte[20]);
Write(cppPath, [1, 2, 3, 4], null);
var blobRoot = Path.Combine(root, "mod_data_patches_blobs");
Assert.Equal(["module_image.bin"], Directory.GetFiles(blobRoot, "*.bin").Select(Path.GetFileName).ToArray());
var assembly = File.ReadAllText(Path.Combine(root, "mod_data_patches_blobs.S"));
Assert.DoesNotContain("kKamekCodeSha1Digest", assembly, StringComparison.Ordinal);
}
finally
{
if (Directory.Exists(root))
{
Directory.Delete(root, recursive: true);
}
}
}
[Fact]
public void WriteCanReferencePublishedBlobsFromTransactionalStaging()
{
var root = Path.Combine(Path.GetTempPath(), "mkw-mod-data-" + Guid.NewGuid().ToString("N"));
try
{
var stagingCpp = Path.Combine(root, ".staging", "cpp", "mod_data_patches.cpp");
var publishedBlobs = Path.Combine(root, "published", "cpp", "mod_data_patches_blobs");
Write(stagingCpp, [1, 2, 3, 4], null, publishedBlobDirectory: publishedBlobs);
var assembly = File.ReadAllText(Path.Combine(root, ".staging", "cpp", "mod_data_patches_blobs.S"));
var expected = Path.GetFullPath(Path.Combine(publishedBlobs, "module_image.bin")).Replace('\\', '/');
Assert.Contains($".incbin \"{expected}\"", assembly, StringComparison.Ordinal);
Assert.DoesNotContain(".staging", assembly, StringComparison.OrdinalIgnoreCase);
}
finally
{
if (Directory.Exists(root))
{
Directory.Delete(root, recursive: true);
}
}
}
[Fact]
public void WriteChangesAssemblyWhenSameSizeBlobContentChanges()
{
var root = Path.Combine(Path.GetTempPath(), "mkw-mod-data-" + Guid.NewGuid().ToString("N"));
try
{
var cppPath = Path.Combine(root, "mod_data_patches.cpp");
Write(cppPath, [1, 2, 3, 4], null);
var assemblyPath = Path.Combine(root, "mod_data_patches_blobs.S");
var before = File.ReadAllText(assemblyPath);
Write(cppPath, [1, 2, 3, 5], null);
var after = File.ReadAllText(assemblyPath);
Assert.NotEqual(before, after);
Assert.Contains("sha256=", after, StringComparison.Ordinal);
}
finally
{
if (Directory.Exists(root))
{
Directory.Delete(root, recursive: true);
}
}
}
private static void Write(
string path,
byte[] moduleImage,
byte[]? digest,
string? publishedBlobDirectory = null)
{
var plan = new KamekPatchPlan { ModuleGuestBase = 0x81700000u, Classifications = [] };
var manifest = new BaseManifest("test", 1, "RMCP01", "P", "hash", 0, [], [], "ranges.json");
ModDataPatchWriter.Write(
path,
plan,
0x803992E0u,
manifest,
[],
moduleImage,
digest,
null,
(uint)moduleImage.Length,
0x40u,
0x81700000u,
0x81700000u,
false,
publishedBlobDirectory: publishedBlobDirectory);
}
}
@@ -0,0 +1,666 @@
using System.Buffers.Binary;
using Translator.Core.Mods;
using Translator.Core.Parsing.Kamek;
using Xunit;
namespace Translator.Tests;
public sealed class ModFunctionDiscoveryTests
{
[Fact]
public void DiscoversModuleDataPointersAndPrologues()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x80];
WriteU32(code, 0x20, 0x9421FFF0u); // stwu r1,-0x10(r1)
WriteU32(code, 0x24, 0x7C0802A6u); // mflr r0
WriteU32(code, 0x28, 0x90010014u); // stw r0,0x14(r1)
WriteU32(code, 0x2C, 0x4E800020u); // blr
WriteU32(code, 0x40, moduleBase + 0x50);
WriteU32(code, 0x50, 0x4E800020u); // blr
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: (uint)code.Length,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(KamekChunk.HeaderSize + code.Length),
codeBlob: code,
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module prologue scan", starts[moduleBase + 0x20]);
Assert.Equal("module data pointer", starts[moduleBase + 0x50]);
}
[Fact]
public void DiscoversModuleBaseBctrThunk()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x40];
WriteU32(code, 0x00, 0x7C6C1B78u); // mr r12,r3
WriteU32(code, 0x04, 0x7D8903A6u); // mtctr r12
WriteU32(code, 0x08, 0x4E800420u); // bctr
WriteU32(code, 0x24, 0x9421FFF0u); // stwu r1,-0x10(r1)
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: (uint)code.Length,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module base", starts[moduleBase]);
}
[Fact]
public void DiscoversSynthesizedLeafFunctionPointers()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x9800];
WriteU32(code, 0x40, 0x3C608121u); // lis r3,0x8121
WriteU32(code, 0x44, 0x38639710u); // addi r3,r3,-0x68f0 => 0x81209710
WriteU32(code, 0x48, 0x907EAD54u); // stw r3,-0x52ac(r30)
WriteU32(code, 0x9710, 0x3C808038u); // lis r4,0x8038
WriteU32(code, 0x9714, 0x38600016u); // li r3,0x16
WriteU32(code, 0x9718, 0x80846E3Cu); // lwz r4,0x6e3c(r4)
WriteU32(code, 0x971C, 0x38000010u); // li r0,0x10
WriteU32(code, 0x9720, 0xB0640020u); // sth r3,0x20(r4)
WriteU32(code, 0x9724, 0xB004001Cu); // sth r0,0x1c(r4)
WriteU32(code, 0x9728, 0x4E800020u); // blr
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: (uint)code.Length,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module synthesized pointer", starts[0x81209710]);
}
[Fact]
public void DiscoversSynthesizedCallbackStoredAfterRegisterSaveSetup()
{
const uint moduleBase = 0x81700000u;
var code = new byte[0x300];
// Placed past the declared code size so only the synthesized-pointer
// scan, and not the prologue scan, can attribute this start.
WriteU32(code, 0x80, 0x9421FFF0u); // callback: stwu r1,-0x10(r1)
WriteU32(code, 0x84, 0x4E800020u); // blr
WriteU32(code, 0x40, 0x9421FFF0u); // outer function
WriteU32(code, 0x44, 0x7C0802A6u); // mflr r0
WriteU32(code, 0x100, 0x3C608170u); // lis r3,0x8170
WriteU32(code, 0x104, 0x3CA0817Bu); // unrelated setup
WriteU32(code, 0x108, 0x90010024u); // save r0
WriteU32(code, 0x10C, 0x38630080u); // addi r3,r3,0x80
WriteU32(code, 0x110, 0x93E1001Cu); // save r31
WriteU32(code, 0x114, 0x3FE08177u); // more setup
WriteU32(code, 0x118, 0x38DFEC94u);
WriteU32(code, 0x11C, 0x93C10018u);
WriteU32(code, 0x120, 0x3FC08170u);
WriteU32(code, 0x124, 0x93A10014u);
WriteU32(code, 0x128, 0x3FA08174u);
WriteU32(code, 0x12C, 0x389DCCB4u);
WriteU32(code, 0x130, 0x93810010u);
WriteU32(code, 0x134, 0x3F80817Bu);
WriteU32(code, 0x138, 0x907C0180u); // stw r3,0x180(r28)
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: 0x80,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module synthesized pointer", starts[moduleBase + 0x80]);
}
[Fact]
public void DiscoversSynthesizedLongLeafFunctionPointers()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x40000];
WriteU32(code, 0xF574, 0x3C608124u); // lis r3,0x8124
WriteU32(code, 0xF578, 0x38800003u); // li r4,3
WriteU32(code, 0xF57C, 0x90010014u); // stw r0,0x14(r1)
WriteU32(code, 0xF580, 0x3863F1C0u); // addi r3,r3,-0xe40 => 0x8123F1C0
WriteU32(code, 0xF584, 0x907E1D50u); // stw r3,0x1d50(r30)
WriteU32(code, 0x3F1C0, 0x3D008129u); // lis r8,0x8129
for (var offset = 0x3F1C4; offset < 0x3F204; offset += 4)
{
WriteU32(code, offset, 0x60000000u); // nop-like body
}
WriteU32(code, 0x3F204, 0x4E800020u); // blr, past the old 64-byte leaf scan window
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: (uint)code.Length,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module synthesized pointer", starts[0x8123F1C0]);
}
[Fact]
public void DiscoversSynthesizedPointersWrittenToDifferentRegister()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x40000];
WriteU32(code, 0x200, 0x3FE08124u); // lis r31,0x8124
WriteU32(code, 0x204, 0x389FE6B4u); // addi r4,r31,-0x194c => 0x8123E6B4, not directly stored
WriteU32(code, 0x208, 0x38A50000u); // addi r5,r5,0
WriteU32(code, 0x20C, 0x381FE6B4u); // addi r0,r31,-0x194c => 0x8123E6B4
WriteU32(code, 0x210, 0x901E003Cu); // stw r0,0x3c(r30)
WriteU32(code, 0x3E6B4, 0x3C60801Cu); // lis r3,0x801c
WriteU32(code, 0x3E6B8, 0x8063AB20u); // lwz r3,-0x54e0(r3)
WriteU32(code, 0x3E6BC, 0x3C03B180u); // addis r0,r3,-0x4e80
WriteU32(code, 0x3E6C0, 0x28000020u); // cmplwi r0,0x20
WriteU32(code, 0x3E6C4, 0x4D820020u); // beqlr
WriteU32(code, 0x3E6C8, 0x3C608126u); // lis r3,0x8126
WriteU32(code, 0x3E6CC, 0x38636650u); // addi r3,r3,0x6650
WriteU32(code, 0x3E6D0, 0x4BFCAF08u); // b some helper
WriteU32(code, 0x3E6D4, 0x4E800020u); // blr
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: (uint)code.Length,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module synthesized pointer", starts[0x8123E6B4]);
}
[Fact]
public void DiscoversSynthesizedComparatorPassedThroughExternalTailCall()
{
const uint moduleBase = 0x81700000u;
var code = new byte[0x20000];
// Mirrors the Retro Rewind trampoline at 0x8171FCB4: a lone backward
// branch referenced only as a callback argument to a base-game routine.
WriteU32(code, 0x1FBC8, 0x9421FFF0u); // helper: stwu r1,-0x10(r1)
WriteU32(code, 0x1FBCC, 0x7C0802A6u); // mflr r0
WriteU32(code, 0x1FBD0, 0x4E800020u); // blr
WriteU32(code, 0x1FCB4, EncodeBranch(moduleBase + 0x1FCB4, moduleBase + 0x1FBC8)); // trampoline
WriteU32(code, 0x1FCCC, 0x3CC08172u); // lis r6,0x8172
WriteU32(code, 0x1FCD0, 0x38A00018u); // li r5,0x18
WriteU32(code, 0x1FCD4, 0x38C6FCB4u); // addi r6,r6,-0x34c => 0x8171FCB4
WriteU32(code, 0x1FCD8, EncodeBranch(moduleBase + 0x1FCD8, 0x80011B00u)); // tail call into base sort routine
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: (uint)code.Length,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module synthesized pointer", starts[moduleBase + 0x1FCB4]);
}
[Fact]
public void DiscoversSynthesizedCallbackPassedAsCallArgument()
{
const uint moduleBase = 0x81700000u;
var code = new byte[0x20000];
WriteU32(code, 0x1000, 0x38600001u); // prologue-less leaf callback: li r3,1
WriteU32(code, 0x1004, 0x4E800020u); // blr
WriteU32(code, 0x2000, 0x3C808170u); // lis r4,0x8170
WriteU32(code, 0x2004, 0x38841000u); // addi r4,r4,0x1000 => callback
WriteU32(code, 0x2008, 0x48000001u | (0x3000u - 0x2008u)); // bl moduleBase+0x3000
WriteU32(code, 0x3000, 0x4E800020u); // callee: blr
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: (uint)code.Length,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module synthesized pointer", starts[moduleBase + 0x1000]);
}
[Fact]
public void IgnoresAsciiDataThatDecodesAsForwardBranch()
{
const uint moduleBase = 0x81700000u;
var code = new byte[0x20000];
// "HeyhoShipGBA.brres" - 'H' (0x48) decodes as an unconditional forward
// branch whose target lands far outside executable MEM1.
var name = "HeyhoShipGBA.brres\0\0"u8.ToArray();
name.CopyTo(code, 0x1B5FC);
WriteU32(code, 0x2000, 0x3C808172u); // lis r4,0x8172
WriteU32(code, 0x2004, 0x3884B5FCu); // addi r4,r4,-0x4a04 => 0x8171B5FC string address
WriteU32(code, 0x2008, 0x48000001u | (0x3000u - 0x2008u)); // bl: string passed as argument
WriteU32(code, 0x3000, 0x4E800020u); // callee: blr
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: (uint)code.Length,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.False(starts.ContainsKey(moduleBase + 0x1B5FC));
}
[Fact]
public void IgnoresSynthesizedAddressWhoseRegisterDiesAtCall()
{
const uint moduleBase = 0x81700000u;
var code = new byte[0x20000];
WriteU32(code, 0x1000, 0x38600001u); // leaf-looking target: li r3,1
WriteU32(code, 0x1004, 0x4E800020u); // blr
WriteU32(code, 0x2000, 0x3D808170u); // lis r12,0x8170
WriteU32(code, 0x2004, 0x398C1000u); // addi r12,r12,0x1000
WriteU32(code, 0x2008, 0x48000001u | (0x3000u - 0x2008u)); // bl clobbers r12 before any use
WriteU32(code, 0x3000, 0x4E800020u); // callee: blr
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: (uint)code.Length,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.DoesNotContain(starts, kvp => kvp.Value == "module synthesized pointer");
}
[Fact]
public void DiscoversModuleDataPointerToLeafFunction()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x200];
WriteU32(code, 0x20, moduleBase + 0x100);
WriteU32(code, 0x100, 0x806300DCu); // lwz r3,0xdc(r3)
WriteU32(code, 0x104, 0x7C802378u); // mr r0,r4
WriteU32(code, 0x108, 0x2C030000u); // cmpwi r3,0
WriteU32(code, 0x10C, 0x38600000u); // li r3,0
WriteU32(code, 0x110, 0x4E800020u); // blr
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: (uint)code.Length,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module data pointer", starts[moduleBase + 0x100]);
}
[Fact]
public void DiscoversAppendedModuleDataPointerToLeafFunction()
{
const uint moduleBase = 0x81200000u;
var relocatedImage = new byte[0x300];
WriteU32(relocatedImage, 0x20, moduleBase + 0x200);
WriteU32(relocatedImage, 0x200, 0x38600001u); // li r3,1
WriteU32(relocatedImage, 0x204, 0x4E800020u); // blr
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: 0x100,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(0x100 + KamekChunk.HeaderSize),
codeBlob: relocatedImage[..0x100],
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, relocatedImage)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module data pointer", starts[moduleBase + 0x200]);
}
[Fact]
public void DiscoversShortTailEntryInsideMappedPayloadFunction()
{
const uint moduleBase = 0x81700000u;
var relocatedImage = new byte[0x300];
WriteU32(relocatedImage, 0x150, 0x9421FFF0u); // stwu r1,-0x10(r1), executable tail target
WriteU32(relocatedImage, 0x154, 0x4E800020u); // blr
WriteU32(relocatedImage, 0x200, 0x9421FFF0u); // mapped public payload function
WriteU32(relocatedImage, 0x204, 0x4E800020u); // blr
WriteU32(relocatedImage, 0x230, EncodeBranch(moduleBase + 0x230, moduleBase + 0x200)); // previous helper tail
WriteU32(relocatedImage, 0x234, 0x38800001u); // li r4,1
WriteU32(relocatedImage, 0x238, EncodeBranch(moduleBase + 0x238, moduleBase + 0x150)); // b target
WriteU32(relocatedImage, 0x23C, 0x000058B4u); // inline data between tail entry and next prologue
WriteU32(relocatedImage, 0x240, 0x9421FFF0u); // stwu r1,-0x10(r1)
WriteU32(relocatedImage, 0x244, 0x4E800020u); // blr
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: 0x100,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(0x100 + KamekChunk.HeaderSize),
codeBlob: relocatedImage[..0x100],
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, relocatedImage)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module tail-entry scan", starts[moduleBase + 0x234]);
Assert.False(starts.ContainsKey(moduleBase + 0x23C));
}
[Fact]
public void KeepsPrologueScanScopedToOriginalCodeSize()
{
const uint moduleBase = 0x81200000u;
var relocatedImage = new byte[0x300];
WriteU32(relocatedImage, 0x200, 0x9421FFF0u); // stwu r1,-0x10(r1)
WriteU32(relocatedImage, 0x204, 0x4E800020u); // blr
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: 0x100,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(0x100 + KamekChunk.HeaderSize),
codeBlob: relocatedImage[..0x100],
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, relocatedImage);
Assert.DoesNotContain(starts, start => start.Address == moduleBase + 0x200);
}
[Fact]
public void DiscoversLeafFunctionBetweenReturnedFunctionAndNextPrologue()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x500];
WriteU32(code, 0x320, 0x9421FFF0u); // stwu r1,-0x10(r1)
WriteU32(code, 0x324, 0x7C0802A6u); // mflr r0
WriteU32(code, 0x328, 0x90010014u); // stw r0,0x14(r1)
WriteU32(code, 0x32C, 0x80010014u); // lwz r0,0x14(r1)
WriteU32(code, 0x330, 0x7C0803A6u); // mtlr r0
WriteU32(code, 0x334, 0x38210010u); // addi r1,r1,0x10
WriteU32(code, 0x338, 0x4E800020u); // blr
WriteU32(code, 0x33C, 0x3C60809Cu); // lis r3,0x809c
WriteU32(code, 0x340, 0x3CA08129u); // lis r5,0x8129
WriteU32(code, 0x344, 0x80831E38u); // lwz r4,0x1e38(r3)
WriteU32(code, 0x348, 0x8065FD1Cu); // lwz r3,-0x2e4(r5)
WriteU32(code, 0x34C, 0x2C040048u); // cmpwi r4,0x48
WriteU32(code, 0x350, 0x4D820020u); // beqlr
WriteU32(code, 0x354, 0x38000000u); // li r0,0
WriteU32(code, 0x358, 0x90030060u); // stw r0,0x60(r3)
WriteU32(code, 0x35C, 0x4E800020u); // blr
WriteU32(code, 0x3A0, 0x9421FFF0u); // stwu r1,-0x10(r1)
WriteU32(code, 0x3A4, 0x4E800020u); // blr
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: (uint)code.Length,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module leaf boundary scan", starts[moduleBase + 0x33C]);
}
[Fact]
public void DiscoversTailThunkBetweenBranchedFunctionAndNextPrologue()
{
const uint moduleBase = 0x81200000u;
var code = new byte[0x500];
WriteU32(code, 0x240, 0x9421FFF0u); // stwu r1,-0x10(r1)
WriteU32(code, 0x244, 0x4E800020u); // blr
WriteU32(code, 0x320, 0x9421FFF0u); // stwu r1,-0x10(r1)
WriteU32(code, 0x324, 0x7C0802A6u); // mflr r0
WriteU32(code, 0x328, 0x90010014u); // stw r0,0x14(r1)
WriteU32(code, 0x32C, 0x38630020u); // addi r3,r3,0x20
WriteU32(code, 0x330, 0x48000010u); // b moduleBase+0x340
WriteU32(code, 0x334, 0x3C608129u); // lis r3,0x8129
WriteU32(code, 0x338, 0x38631C5Cu); // addi r3,r3,0x1c5c
WriteU32(code, 0x33C, 0x4BFFFF04u); // b moduleBase+0x240
WriteU32(code, 0x340, 0x9421FFF0u); // stwu r1,-0x10(r1)
WriteU32(code, 0x344, 0x4E800020u); // blr
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: (uint)code.Length,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module leaf boundary scan", starts[moduleBase + 0x334]);
}
[Fact]
public void DoesNotSplitCountedLoopContinuationAfterConditionalReturn()
{
const uint moduleBase = 0x81750000u;
var code = new byte[0x200];
WriteU32(code, 0x100, 0x9421FFF0u); // stwu r1,-0x10(r1)
WriteU32(code, 0x120, 0x80050008u); // lwz r0,8(r5)
WriteU32(code, 0x124, 0x7C002000u); // cmpw r0,r4
WriteU32(code, 0x128, 0x40820008u); // bne moduleBase+0x130
WriteU32(code, 0x12C, 0x4E800020u); // blr
WriteU32(code, 0x130, 0x38A50004u); // addi r5,r5,4
WriteU32(code, 0x134, 0x4200FFECu); // bdnz moduleBase+0x120
WriteU32(code, 0x138, 0x4E800020u); // blr
WriteU32(code, 0x180, 0x9421FFF0u); // stwu r1,-0x10(r1)
WriteU32(code, 0x184, 0x4E800020u); // blr
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: (uint)code.Length,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.False(starts.ContainsKey(moduleBase + 0x130));
}
[Fact]
public void DiscoversLeafFunctionThatTailBranchesToBaseCode()
{
const uint moduleBase = 0x8174FBA0u;
var code = new byte[0x500];
WriteU32(code, 0x300, 0x9421FFF0u); // stwu r1,-0x10(r1)
WriteU32(code, 0x304, 0x38210010u); // addi r1,r1,0x10
WriteU32(code, 0x308, 0x4E800020u); // blr
WriteU32(code, 0x30C, 0x38000008u); // li r0,8
WriteU32(code, 0x310, 0x3880FFFFu); // li r4,-1
WriteU32(code, 0x314, 0x90832AB4u); // stw r4,0x2ab4(r3)
WriteU32(code, 0x318, 0x38A00000u); // li r5,0
WriteU32(code, 0x31C, 0x80C3072Cu); // lwz r6,0x72c(r3)
WriteU32(code, 0x320, 0x7C0903A6u); // mtctr r0
WriteU32(code, 0x324, 0x80860000u); // lwz r4,0(r6)
WriteU32(code, 0x328, 0x38C60004u); // addi r6,r6,4
WriteU32(code, 0x32C, 0x98A401E9u); // stb r5,0x1e9(r4)
WriteU32(code, 0x330, 0x4200FFF4u); // bdnz -0xc
WriteU32(code, 0x334, 0x38000000u); // li r0,0
WriteU32(code, 0x338, EncodeBranch(moduleBase + 0x338, 0x80841010u)); // b base code
WriteU32(code, 0x33C, 0x9421FFC0u); // stwu r1,-0x40(r1)
WriteU32(code, 0x340, 0x7C0802A6u); // mflr r0
WriteU32(code, 0x344, 0x4E800020u); // blr
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: (uint)code.Length,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module leaf boundary scan", starts[moduleBase + 0x30C]);
}
[Fact]
public void DiscoversLeafFunctionAfterExternalTailBranch()
{
const uint moduleBase = 0x81700000u;
var code = new byte[0x44000];
WriteU32(code, 0x42FF8, 0x806302BCu); // lwz r3,0x2bc(r3)
WriteU32(code, 0x42FFC, 0x38843000u); // addi r4,r4,0x3000
WriteU32(code, 0x43000, EncodeBranch(moduleBase + 0x43000, 0x807E9A38u)); // b base code
WriteU32(code, 0x43004, 0x80630048u); // lwz r3,0x48(r3)
WriteU32(code, 0x43008, 0x38800000u); // li r4,0
WriteU32(code, 0x4300C, 0x80630000u); // lwz r3,0(r3)
WriteU32(code, 0x43010, EncodeBranch(moduleBase + 0x43010, 0x805BDF44u)); // b base code
WriteU32(code, 0x43014, 0x9421FFE0u); // stwu r1,-0x20(r1)
WriteU32(code, 0x43018, 0x7C0802A6u); // mflr r0
var chunk = new KamekChunk(
index: 0,
fileOffset: 0,
bssSize: 0,
codeSize: (uint)code.Length,
ctorStart: 0,
ctorEnd: 0,
chunkSize: (uint)(code.Length + KamekChunk.HeaderSize),
codeBlob: code,
commands: []);
var starts = ModFunctionDiscovery.DiscoverKamekFunctions(chunk, moduleBase, code)
.ToDictionary(start => start.Address, start => start.Reason);
Assert.Equal("module leaf boundary scan", starts[moduleBase + 0x43004]);
}
private static void WriteU32(byte[] data, int offset, uint value) =>
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(offset, 4), value);
private static uint EncodeBranch(uint source, uint target)
{
var delta = unchecked((int)(target - source));
return 0x48000000u | ((uint)delta & 0x03FFFFFCu);
}
}
@@ -0,0 +1,68 @@
using System.Buffers.Binary;
using Translator.Core.Loading;
using Translator.Core.Translation;
using Xunit;
namespace Translator.Tests;
public sealed class ModTranslationWaveRunnerTests
{
[Fact]
public void Translate_IsDeterministicAcrossThreadCounts()
{
var memory = new byte[24];
WriteWord(memory, 0, 0x38600001); // li r3, 1
WriteWord(memory, 4, 0x4E800020); // blr
WriteWord(memory, 8, 0x38600002); // li r3, 2
WriteWord(memory, 12, 0x4E800020); // blr
WriteWord(memory, 16, 0x38600003); // li r3, 3
WriteWord(memory, 20, 0x4E800020); // blr
var image = new ProgramImage(
memory,
AddressRange.FromStartAndSize(MemoryLayout.RamBase, (uint)memory.Length),
AddressRange.FromStartAndSize(MemoryLayout.RamBase, (uint)memory.Length),
default,
"mod-wave-test");
var knownEntries = new HashSet<uint>
{
MemoryLayout.RamBase,
MemoryLayout.RamBase + 8,
MemoryLayout.RamBase + 16
};
ModTranslationWork Work(uint address) => new(
address,
TranslationOptions.Default with
{
PreferredName = $"wave_{address:X8}",
MaxBytes = 8,
AllowUnsupportedInstructions = true,
KnownFunctionEntryPoints = knownEntries
},
$"{address:X8}.cpp");
var shuffled = new[]
{
Work(MemoryLayout.RamBase + 16),
Work(MemoryLayout.RamBase),
Work(MemoryLayout.RamBase + 8)
};
var sequentialTranslator = new FunctionTranslator(image);
var parallelTranslator = new FunctionTranslator(image);
var sequential = ModTranslationWaveRunner.Translate(
() => sequentialTranslator,
shuffled,
new ParallelOptions { MaxDegreeOfParallelism = 1 });
var parallel = ModTranslationWaveRunner.Translate(
() => parallelTranslator,
shuffled,
new ParallelOptions { MaxDegreeOfParallelism = 4 });
Assert.Equal(sequential.Select(item => item.Work.Address), parallel.Select(item => item.Work.Address));
Assert.Equal(sequential.Select(item => item.Error), parallel.Select(item => item.Error));
Assert.Equal(sequential.Select(item => item.Result!.CxxCode), parallel.Select(item => item.Result!.CxxCode));
Assert.Equal(knownEntries.Order(), parallel.Select(item => item.Work.Address));
}
private static void WriteWord(byte[] memory, int offset, uint value) =>
BinaryPrimitives.WriteUInt32BigEndian(memory.AsSpan(offset, 4), value);
}
@@ -0,0 +1,926 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using Translator.Core.Analysis;
using Translator.Core.Analysis.Representation;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Loading;
using Translator.Core.Parsing.Dol;
using Translator.Core.Parsing.Rel;
using Translator.Core.Translation;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
public class MtxIntegrationTests
{
private static string RepositoryRoot => Path.GetFullPath(Path.Combine(
AppContext.BaseDirectory, "..", "..", "..", "..", "..", ".."));
private enum ParamKind
{
Gpr,
Fpr
}
private enum ReturnKind
{
Void,
Gpr,
Float
}
private record MtxFuncSpec(uint Address, string Name, ReturnKind Return, IReadOnlyList<ParamKind> Params);
private static (int ExitCode, string Output) RunProcess(string fileName, string arguments, TimeSpan? timeout = null)
{
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
RedirectStandardError = true,
RedirectStandardOutput = true,
UseShellExecute = false
}
};
process.Start();
var stdoutTask = process.StandardOutput.ReadToEndAsync();
var stderrTask = process.StandardError.ReadToEndAsync();
var exitedInTime = true;
if (timeout.HasValue)
{
exitedInTime = process.WaitForExit((int)timeout.Value.TotalMilliseconds);
}
else
{
process.WaitForExit();
}
if (!exitedInTime)
{
try
{
process.Kill(entireProcessTree: true);
}
catch
{
// ignored: process might have already exited between WaitForExit and Kill
}
process.WaitForExit();
var staleOutput = stdoutTask.GetAwaiter().GetResult() + stderrTask.GetAwaiter().GetResult();
throw new TimeoutException($"Process '{fileName}' timed out after {timeout!.Value.TotalSeconds:F0}s. Output before kill:\n{staleOutput}");
}
process.WaitForExit();
var output = stdoutTask.GetAwaiter().GetResult() + stderrTask.GetAwaiter().GetResult();
return (process.ExitCode, output);
}
private static FunctionTranslator BuildTranslator()
{
var root = RepositoryRoot;
var dol = DolFile.Load(Path.Combine(root, "assets", "main.dol"));
var rel = RelFile.Load(Path.Combine(root, "assets", "StaticR.rel"))
.BuildImage(0x805102E0);
var image = new ProgramImageBuilder().Build(dol, rel);
return new FunctionTranslator(image);
}
[Fact]
public void Translated_MTX_Primitives_ExecuteCorrectly()
{
var specs = new[]
{
Spec(0x80199D04, "PSMTXIdentity_80199d04", ReturnKind.Void, ParamKind.Gpr),
Spec(0x80199D30, "PSMTXCopy_80199d30", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x80199D64, "PSMTXConcat_80199d64", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x80199E30, "PSMTXConcatArray_80199e30", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x80199FC8, "PSMTXInverse_80199fc8", ReturnKind.Gpr, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x8019A0C0, "PSMTXInvXpose_8019a0c0", ReturnKind.Gpr, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x8019A188, "PSMTXRotRad_8019a188", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Fpr),
Spec(0x8019A204, "PSMTXRotTrig_8019a204", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Fpr, ParamKind.Fpr),
Spec(0x8019A2B4, "PSMTXRotAxisRadInternal_8019a2b4", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Fpr, ParamKind.Fpr),
Spec(0x8019A364, "PSMTXRotAxisRad_8019a364", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Fpr),
Spec(0x8019A3E0, "PSMTXTrans_8019a3e0", ReturnKind.Void, ParamKind.Gpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr),
Spec(0x8019A414, "PSMTXTransApply_8019a414", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr),
Spec(0x8019A460, "PSMTXScale_8019a460", ReturnKind.Void, ParamKind.Gpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr),
Spec(0x8019A488, "PSMTXScaleApply_8019a488", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr),
Spec(0x8019A4E0, "PSMTXQuat_8019a4e0", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x8019A584, "C_MTXLookAt_8019a584", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x8019A6F8, "C_MTXLightFrustum_8019a6f8", ReturnKind.Void, ParamKind.Gpr,
ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr),
Spec(0x8019A79C, "C_MTXLightPerspective_8019a79c", ReturnKind.Void, ParamKind.Gpr,
ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr),
Spec(0x8019A894, "C_MTXLightOrtho_8019a894", ReturnKind.Void, ParamKind.Gpr,
ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr),
Spec(0x8019A91C, "PSMTXMultVec_8019a91c", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x8019A970, "PSMTXMultVecSR_8019a970", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x8019A9C4, "C_MTXFrustum_8019a9c4", ReturnKind.Void, ParamKind.Gpr,
ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr),
Spec(0x8019AA60, "C_MTXPerspective_8019aa60", ReturnKind.Void, ParamKind.Gpr,
ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr),
Spec(0x8019AB4C, "C_MTXOrtho_8019ab4c", ReturnKind.Void, ParamKind.Gpr,
ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr, ParamKind.Fpr),
Spec(0x8019ABE4, "MTX__PSVECAdd_8019abe4", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x8019AC08, "MTX__PSVECScale_8019ac08", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Fpr),
Spec(0x8019AC24, "MTX__PSVECNormalize_8019ac24", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x8019AC68, "MTX__PSVECMag_8019ac68", ReturnKind.Float, ParamKind.Gpr),
Spec(0x8019ACAC, "MTX__PSVECDotProduct_8019acac", ReturnKind.Float, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x8019ACCC, "MTX__PSVECCrossProduct_8019accc", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x8019AD08, "MTX__C_VECHalfAngle_8019ad08", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x8019ADE0, "MTX__PSVECSquareDistance_8019ade0", ReturnKind.Float, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x8019AE08, "MTX__PSQUATMultiply_8019ae08", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x8019AE64, "MTX__PSQUATScale_8019ae64", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Fpr),
Spec(0x8019AE80, "MTX__PSQUATDotProduct_8019ae80", ReturnKind.Float, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x8019AEA0, "MTX__PSQUATNormalize_8019aea0", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x8019AEF4, "MTX__PSQUATInverse_8019aef4", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x8019AF48, "MTX__C_QUATMtx_8019af48", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr),
Spec(0x8019B114, "MTX__C_QUATLerp_8019b114", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Fpr),
Spec(0x8019B178, "MTX__C_QUATSlerp_8019b178", ReturnKind.Void, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Gpr, ParamKind.Fpr),
};
var translator = BuildTranslator();
var repoRoot = RepositoryRoot;
var tempRoot = Path.Combine(repoRoot, "test_output", "mtx");
Directory.CreateDirectory(tempRoot);
var generatedFiles = new List<string>();
var translations = TranslateWithDependencies(
translator,
specs.Select(s => (s.Address, s.Name)),
tempRoot,
generatedFiles);
var translationMap = translations.ToDictionary(t => t.Name, StringComparer.Ordinal);
foreach (var spec in specs)
{
Assert.True(translationMap.TryGetValue(spec.Name, out var translation), $"Missing translation for {spec.Name}.");
AssertReturnAbiMatches(spec, translation.AbiClassification);
}
var harness = new StringBuilder();
harness.AppendLine("#include <cassert>");
harness.AppendLine("#include <cmath>");
harness.AppendLine("#include <cstdlib>");
harness.AppendLine("#include <cstdint>");
harness.AppendLine("#include <cstdio>");
harness.AppendLine("#include <array>");
harness.AppendLine("#include <type_traits>");
harness.AppendLine("#include \"memory.h\"");
harness.AppendLine("#include \"ppc_runtime.h\"");
harness.AppendLine("#include \"RuntimeConfig.h\"");
// Stubs for runtime dependencies
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_Write8(uint8_t) {}");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_Write16(uint16_t) {}");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_Write32(uint32_t) {}");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_WriteFloat(float) {}");
harness.AppendLine("extern \"C\" void InsertAlarm_801a0620(CpuContext*) {}");
harness.AppendLine("extern \"C\" void OS____TimeToSystemTime_801aade0(CpuContext*) {}");
harness.AppendLine("void VI_HLE_SimulateRetrace(CpuContext*) {}");
harness.AppendLine("void DumpRecentPcTrace(size_t) {}");
harness.AppendLine("void MarkFatalErrorReported() {}");
harness.AppendLine("extern \"C\" void InitializeDataSections();");
foreach (var translation in translations)
{
harness.AppendLine(PrototypeFor(translation.AbiClassification));
}
harness.AppendLine("template <typename T>");
harness.AppendLine("uint32_t ToGpr(T v) {");
harness.AppendLine(" if constexpr (std::is_pointer_v<T>) {");
harness.AppendLine(" return static_cast<uint32_t>(reinterpret_cast<uintptr_t>(v));");
harness.AppendLine(" } else {");
harness.AppendLine(" return static_cast<uint32_t>(v);");
harness.AppendLine(" }");
harness.AppendLine("}");
harness.AppendLine("static void WriteF32(uint32_t addr, float value) {");
harness.AppendLine(" Memory::WriteFloat32(addr, value);");
harness.AppendLine("}");
harness.AppendLine("static float ReadF32(uint32_t addr) {");
harness.AppendLine(" return Memory::ReadFloat32(addr);");
harness.AppendLine("}");
harness.AppendLine("static void WriteMat34(uint32_t addr, const float* m) {");
harness.AppendLine(" for (int i = 0; i < 12; ++i) WriteF32(addr + static_cast<uint32_t>(i * 4), m[i]);");
harness.AppendLine("}");
harness.AppendLine("static void ReadMat34(uint32_t addr, float* m) {");
harness.AppendLine(" for (int i = 0; i < 12; ++i) m[i] = ReadF32(addr + static_cast<uint32_t>(i * 4));");
harness.AppendLine("}");
harness.AppendLine("static void WriteMat44(uint32_t addr, const float* m) {");
harness.AppendLine(" for (int i = 0; i < 16; ++i) WriteF32(addr + static_cast<uint32_t>(i * 4), m[i]);");
harness.AppendLine("}");
harness.AppendLine("static void ReadMat44(uint32_t addr, float* m) {");
harness.AppendLine(" for (int i = 0; i < 16; ++i) m[i] = ReadF32(addr + static_cast<uint32_t>(i * 4));");
harness.AppendLine("}");
harness.AppendLine("static void WriteVec3(uint32_t addr, const float* v) {");
harness.AppendLine(" for (int i = 0; i < 3; ++i) WriteF32(addr + static_cast<uint32_t>(i * 4), v[i]);");
harness.AppendLine("}");
harness.AppendLine("static void ReadVec3(uint32_t addr, float* v) {");
harness.AppendLine(" for (int i = 0; i < 3; ++i) v[i] = ReadF32(addr + static_cast<uint32_t>(i * 4));");
harness.AppendLine("}");
harness.AppendLine("static void WriteQuat(uint32_t addr, const float* q) {");
harness.AppendLine(" for (int i = 0; i < 4; ++i) WriteF32(addr + static_cast<uint32_t>(i * 4), q[i]);");
harness.AppendLine("}");
harness.AppendLine("static void ReadQuat(uint32_t addr, float* q) {");
harness.AppendLine(" for (int i = 0; i < 4; ++i) q[i] = ReadF32(addr + static_cast<uint32_t>(i * 4));");
harness.AppendLine("}");
harness.AppendLine("static float ReadFprPs0(const CpuContext& cpu, int index) {");
harness.AppendLine(" return static_cast<float>(PPC_PsToScalar(cpu.fpr[index].d));");
harness.AppendLine("}");
harness.AppendLine("static float ReadFprFloat(const CpuContext& cpu, int index) {");
harness.AppendLine(" return static_cast<float>(cpu.fpr[index].d);");
harness.AppendLine("}");
harness.AppendLine("static bool Near(float a, float b, float eps = 1e-4f) {");
harness.AppendLine(" const float diff = std::fabs(a - b);");
harness.AppendLine(" const float scale = std::fmax(1.0f, std::fmax(std::fabs(a), std::fabs(b)));\n return diff <= eps * scale;\n}");
harness.AppendLine("static float VecDot(const float* a, const float* b) {");
harness.AppendLine(" return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];");
harness.AppendLine("}");
harness.AppendLine("static float VecMag(const float* v) {");
harness.AppendLine(" return std::sqrt(VecDot(v, v));");
harness.AppendLine("}");
harness.AppendLine("static void VecNormalize(const float* v, float* out) {");
harness.AppendLine(" const float m = VecMag(v);\n if (m == 0.0f) { out[0] = out[1] = out[2] = 0.0f; return; }\n out[0] = v[0] / m; out[1] = v[1] / m; out[2] = v[2] / m;");
harness.AppendLine("}");
harness.AppendLine("static void VecCross(const float* a, const float* b, float* out) {");
harness.AppendLine(" out[0] = a[1] * b[2] - a[2] * b[1];\n out[1] = a[2] * b[0] - a[0] * b[2];\n out[2] = a[0] * b[1] - a[1] * b[0];");
harness.AppendLine("}");
harness.AppendLine("static float VecSquareDistance(const float* a, const float* b) {");
harness.AppendLine(" const float dx = a[0] - b[0]; const float dy = a[1] - b[1]; const float dz = a[2] - b[2];");
harness.AppendLine(" return dx * dx + dy * dy + dz * dz;");
harness.AppendLine("}");
harness.AppendLine("static void VecHalfAngle(const float* a, const float* b, float* out) {");
harness.AppendLine(" float an[3] = { -a[0], -a[1], -a[2] };");
harness.AppendLine(" float bn[3] = { -b[0], -b[1], -b[2] };");
harness.AppendLine(" VecNormalize(an, an);");
harness.AppendLine(" VecNormalize(bn, bn);");
harness.AppendLine(" float sum[3] = { an[0] + bn[0], an[1] + bn[1], an[2] + bn[2] };");
harness.AppendLine(" const float len2 = VecDot(sum, sum);");
harness.AppendLine(" if (len2 <= 1e-7f) { out[0] = sum[0]; out[1] = sum[1]; out[2] = sum[2]; return; }");
harness.AppendLine(" VecNormalize(sum, out);");
harness.AppendLine("}");
harness.AppendLine("static void QuatMul(const float* a, const float* b, float* out) {");
harness.AppendLine(" const float ax = a[0], ay = a[1], az = a[2], aw = a[3];");
harness.AppendLine(" const float bx = b[0], by = b[1], bz = b[2], bw = b[3];");
harness.AppendLine(" out[0] = aw * bx + bw * ax + ay * bz - az * by;");
harness.AppendLine(" out[1] = aw * by + bw * ay + az * bx - ax * bz;");
harness.AppendLine(" out[2] = aw * bz + bw * az + ax * by - ay * bx;");
harness.AppendLine(" out[3] = aw * bw - (ax * bx + ay * by + az * bz);");
harness.AppendLine("}");
harness.AppendLine("static float QuatDot(const float* a, const float* b) {");
harness.AppendLine(" return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];");
harness.AppendLine("}");
harness.AppendLine("static void QuatNormalize(const float* q, float* out) {");
harness.AppendLine(" const float d = QuatDot(q, q);\n if (d == 0.0f) { out[0] = out[1] = out[2] = 0.0f; out[3] = 1.0f; return; }\n const float inv = 1.0f / std::sqrt(d);\n out[0] = q[0] * inv; out[1] = q[1] * inv; out[2] = q[2] * inv; out[3] = q[3] * inv;");
harness.AppendLine("}");
harness.AppendLine("static void QuatInverse(const float* q, float* out) {");
harness.AppendLine(" const float d = QuatDot(q, q);\n if (d == 0.0f) { out[0] = out[1] = out[2] = 0.0f; out[3] = 1.0f; return; }\n const float inv = 1.0f / d;\n out[0] = -q[0] * inv; out[1] = -q[1] * inv; out[2] = -q[2] * inv; out[3] = q[3] * inv;");
harness.AppendLine("}");
harness.AppendLine("static void QuatLerp(const float* a, const float* b, float t, float* out) {");
harness.AppendLine(" out[0] = a[0] + (b[0] - a[0]) * t;\n out[1] = a[1] + (b[1] - a[1]) * t;\n out[2] = a[2] + (b[2] - a[2]) * t;\n out[3] = a[3] + (b[3] - a[3]) * t;");
harness.AppendLine("}");
harness.AppendLine("static void QuatSlerp(const float* a, const float* bIn, float t, float* out) {");
harness.AppendLine(" float b[4] = { bIn[0], bIn[1], bIn[2], bIn[3] };");
harness.AppendLine(" float dot = QuatDot(a, b);");
harness.AppendLine(" if (dot < 0.0f) { dot = -dot; b[0] = -b[0]; b[1] = -b[1]; b[2] = -b[2]; b[3] = -b[3]; }");
harness.AppendLine(" if (dot > 0.9995f) { QuatLerp(a, b, t, out); QuatNormalize(out, out); return; }");
harness.AppendLine(" const float theta = std::acos(dot);\n const float sinT = std::sin(theta);\n const float w1 = std::sin((1.0f - t) * theta) / sinT;\n const float w2 = std::sin(t * theta) / sinT;");
harness.AppendLine(" out[0] = a[0] * w1 + b[0] * w2;\n out[1] = a[1] * w1 + b[1] * w2;\n out[2] = a[2] * w1 + b[2] * w2;\n out[3] = a[3] * w1 + b[3] * w2;");
harness.AppendLine("}");
harness.AppendLine("static void ExpectMat34(const float* got, const float* exp, const char* label) {");
harness.AppendLine(" for (int i = 0; i < 12; ++i) {");
harness.AppendLine(" if (!Near(got[i], exp[i])) {");
harness.AppendLine(" std::printf(\"[mtx] %s mismatch at %d: got %.6f exp %.6f\\n\", label, i, got[i], exp[i]);");
harness.AppendLine(" assert(false);");
harness.AppendLine(" }");
harness.AppendLine(" }");
harness.AppendLine("}");
harness.AppendLine("static void ExpectMat44(const float* got, const float* exp, const char* label) {");
harness.AppendLine(" for (int i = 0; i < 16; ++i) {");
harness.AppendLine(" if (!Near(got[i], exp[i])) {");
harness.AppendLine(" std::printf(\"[mtx] %s mismatch at %d: got %.6f exp %.6f\\n\", label, i, got[i], exp[i]);");
harness.AppendLine(" assert(false);");
harness.AppendLine(" }");
harness.AppendLine(" }");
harness.AppendLine("}");
harness.AppendLine("static void RefIdentity(float* m) {");
harness.AppendLine(" for (int i = 0; i < 12; ++i) m[i] = 0.0f;");
harness.AppendLine(" m[0] = 1.0f; m[5] = 1.0f; m[10] = 1.0f;");
harness.AppendLine("}");
harness.AppendLine("static void RefScale(float* m, float x, float y, float z) {");
harness.AppendLine(" for (int i = 0; i < 12; ++i) m[i] = 0.0f;");
harness.AppendLine(" m[0] = x; m[5] = y; m[10] = z;");
harness.AppendLine("}");
harness.AppendLine("static void RefTrans(float* m, float x, float y, float z) {");
harness.AppendLine(" RefIdentity(m);");
harness.AppendLine(" m[3] = x; m[7] = y; m[11] = z;");
harness.AppendLine("}");
harness.AppendLine("static void RefRotTrig(float* m, char axis, float sinA, float cosA) {");
harness.AppendLine(" for (int i = 0; i < 12; ++i) m[i] = 0.0f;");
harness.AppendLine(" switch (axis) {");
harness.AppendLine(" case 'x': case 'X':");
harness.AppendLine(" m[0] = 1.0f; m[5] = cosA; m[6] = -sinA; m[9] = sinA; m[10] = cosA; break;");
harness.AppendLine(" case 'y': case 'Y':");
harness.AppendLine(" m[0] = cosA; m[2] = sinA; m[5] = 1.0f; m[8] = -sinA; m[10] = cosA; break;");
harness.AppendLine(" case 'z': case 'Z':");
harness.AppendLine(" m[0] = cosA; m[1] = -sinA; m[4] = sinA; m[5] = cosA; m[10] = 1.0f; break;");
harness.AppendLine(" default: break;");
harness.AppendLine(" }");
harness.AppendLine("}");
harness.AppendLine("static void RefRotRad(float* m, char axis, float rad) {");
harness.AppendLine(" RefRotTrig(m, axis, sinf(rad), cosf(rad));");
harness.AppendLine("}");
harness.AppendLine("static void RefRotAxisRadInternal(float* m, const float* axis, float sinA, float cosA) {");
harness.AppendLine(" float x = axis[0], y = axis[1], z = axis[2];");
harness.AppendLine(" float len = std::sqrt(x * x + y * y + z * z);");
harness.AppendLine(" if (len == 0.0f) { RefIdentity(m); return; }");
harness.AppendLine(" x /= len; y /= len; z /= len;\n const float t = 1.0f - cosA;\n m[0] = t * x * x + cosA;\n m[1] = t * x * y - sinA * z;\n m[2] = t * x * z + sinA * y;\n m[3] = 0.0f;\n m[4] = t * x * y + sinA * z;\n m[5] = t * y * y + cosA;\n m[6] = t * y * z - sinA * x;\n m[7] = 0.0f;\n m[8] = t * x * z - sinA * y;\n m[9] = t * y * z + sinA * x;\n m[10] = t * z * z + cosA;\n m[11] = 0.0f;\n}");
harness.AppendLine("static void RefRotAxisRad(float* m, const float* axis, float rad) {");
harness.AppendLine(" RefRotAxisRadInternal(m, axis, sinf(rad), cosf(rad));");
harness.AppendLine("}");
harness.AppendLine("static void RefQuat(float* m, const float* q) {");
harness.AppendLine(" const float x = q[0], y = q[1], z = q[2], w = q[3];");
harness.AppendLine(" const float norm = x*x + y*y + z*z + w*w;\n const float s = norm == 0.0f ? 0.0f : (2.0f / norm);\n m[0] = 1.0f - (y*y + z*z) * s;\n m[1] = (x*y - z*w) * s;\n m[2] = (x*z + y*w) * s;\n m[3] = 0.0f;\n m[4] = (x*y + z*w) * s;\n m[5] = 1.0f - (x*x + z*z) * s;\n m[6] = (y*z - x*w) * s;\n m[7] = 0.0f;\n m[8] = (x*z - y*w) * s;\n m[9] = (y*z + x*w) * s;\n m[10] = 1.0f - (x*x + y*y) * s;\n m[11] = 0.0f;\n}");
harness.AppendLine("static void RefConcat(const float* a, const float* b, float* out) {");
harness.AppendLine(" for (int r = 0; r < 3; ++r) {\n const int r0 = r * 4;\n out[r0 + 0] = a[r0 + 0] * b[0] + a[r0 + 1] * b[4] + a[r0 + 2] * b[8];\n out[r0 + 1] = a[r0 + 0] * b[1] + a[r0 + 1] * b[5] + a[r0 + 2] * b[9];\n out[r0 + 2] = a[r0 + 0] * b[2] + a[r0 + 1] * b[6] + a[r0 + 2] * b[10];\n out[r0 + 3] = a[r0 + 0] * b[3] + a[r0 + 1] * b[7] + a[r0 + 2] * b[11] + a[r0 + 3];\n }\n}");
harness.AppendLine("static void RefConcatArray(const float* a, const float* b, float* out, int count) {");
harness.AppendLine(" for (int i = 0; i < count; ++i) { RefConcat(a, b + i * 12, out + i * 12); }");
harness.AppendLine("}");
harness.AppendLine("static bool RefInverse(const float* src, float* inv) {");
harness.AppendLine(" const float a00 = src[0], a01 = src[1], a02 = src[2];\n const float a10 = src[4], a11 = src[5], a12 = src[6];\n const float a20 = src[8], a21 = src[9], a22 = src[10];\n const float b00 = a11 * a22 - a12 * a21;\n const float b01 = a12 * a20 - a10 * a22;\n const float b02 = a10 * a21 - a11 * a20;\n const float det = a00 * b00 + a01 * b01 + a02 * b02;\n if (det == 0.0f) { return false; }\n const float invDet = 1.0f / det;\n const float r00 = b00 * invDet;\n const float r01 = (a02 * a21 - a01 * a22) * invDet;\n const float r02 = (a01 * a12 - a02 * a11) * invDet;\n const float r10 = b01 * invDet;\n const float r11 = (a00 * a22 - a02 * a20) * invDet;\n const float r12 = (a02 * a10 - a00 * a12) * invDet;\n const float r20 = b02 * invDet;\n const float r21 = (a01 * a20 - a00 * a21) * invDet;\n const float r22 = (a00 * a11 - a01 * a10) * invDet;\n inv[0] = r00; inv[1] = r01; inv[2] = r02;\n inv[4] = r10; inv[5] = r11; inv[6] = r12;\n inv[8] = r20; inv[9] = r21; inv[10] = r22;\n inv[3] = -(r00 * src[3] + r01 * src[7] + r02 * src[11]);\n inv[7] = -(r10 * src[3] + r11 * src[7] + r12 * src[11]);\n inv[11] = -(r20 * src[3] + r21 * src[7] + r22 * src[11]);\n return true;\n}");
harness.AppendLine("static bool RefInvXpose(const float* src, float* out) {");
harness.AppendLine(" const float a00 = src[0], a01 = src[1], a02 = src[2];\n const float a10 = src[4], a11 = src[5], a12 = src[6];\n const float a20 = src[8], a21 = src[9], a22 = src[10];\n const float c00 = a11 * a22 - a12 * a21;\n const float c01 = a12 * a20 - a10 * a22;\n const float c02 = a10 * a21 - a11 * a20;\n const float c10 = a02 * a21 - a01 * a22;\n const float c11 = a00 * a22 - a02 * a20;\n const float c12 = a01 * a20 - a00 * a21;\n const float c20 = a01 * a12 - a02 * a11;\n const float c21 = a02 * a10 - a00 * a12;\n const float c22 = a00 * a11 - a01 * a10;\n const float det = a00 * c00 + a01 * c01 + a02 * c02;\n if (det == 0.0f) { return false; }\n const float invDet = 1.0f / det;\n out[0] = c00 * invDet; out[1] = c01 * invDet; out[2] = c02 * invDet; out[3] = 0.0f;\n out[4] = c10 * invDet; out[5] = c11 * invDet; out[6] = c12 * invDet; out[7] = 0.0f;\n out[8] = c20 * invDet; out[9] = c21 * invDet; out[10] = c22 * invDet; out[11] = 0.0f;\n return true;\n}");
harness.AppendLine("static void RefTransApply(const float* src, float* dst, float x, float y, float z) {");
harness.AppendLine(" for (int i = 0; i < 12; ++i) dst[i] = src[i];\n dst[3] += x; dst[7] += y; dst[11] += z;\n}");
harness.AppendLine("static void RefScaleApply(const float* src, float* dst, float x, float y, float z) {");
harness.AppendLine(" for (int i = 0; i < 12; ++i) dst[i] = src[i];\n for (int c = 0; c < 4; ++c) { dst[c] *= x; dst[4 + c] *= y; dst[8 + c] *= z; }\n}");
harness.AppendLine("static void RefMultVec(const float* m, const float* v, float* out) {");
harness.AppendLine(" out[0] = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3];\n out[1] = m[4] * v[0] + m[5] * v[1] + m[6] * v[2] + m[7];\n out[2] = m[8] * v[0] + m[9] * v[1] + m[10] * v[2] + m[11];\n}");
harness.AppendLine("static void RefMultVecSR(const float* m, const float* v, float* out) {");
harness.AppendLine(" out[0] = m[0] * v[0] + m[1] * v[1] + m[2] * v[2];\n out[1] = m[4] * v[0] + m[5] * v[1] + m[6] * v[2];\n out[2] = m[8] * v[0] + m[9] * v[1] + m[10] * v[2];\n}");
harness.AppendLine("static void RefLookAt(float* m, const float* camPos, const float* camUp, const float* target) {");
harness.AppendLine(" float f[3] = { camPos[0] - target[0], camPos[1] - target[1], camPos[2] - target[2] };\n auto norm = [](float* v) { float len = std::sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]); if (len != 0.0f) { v[0] /= len; v[1] /= len; v[2] /= len; } };\n norm(f);\n float s[3] = { camUp[1] * f[2] - camUp[2] * f[1], camUp[2] * f[0] - camUp[0] * f[2], camUp[0] * f[1] - camUp[1] * f[0] };\n norm(s);\n float u[3] = { f[1] * s[2] - f[2] * s[1], f[2] * s[0] - f[0] * s[2], f[0] * s[1] - f[1] * s[0] };\n m[0] = s[0]; m[1] = s[1]; m[2] = s[2]; m[3] = -(camPos[0]*s[0] + camPos[1]*s[1] + camPos[2]*s[2]);\n m[4] = u[0]; m[5] = u[1]; m[6] = u[2]; m[7] = -(camPos[0]*u[0] + camPos[1]*u[1] + camPos[2]*u[2]);\n m[8] = f[0]; m[9] = f[1]; m[10] = f[2]; m[11] = -(camPos[0]*f[0] + camPos[1]*f[1] + camPos[2]*f[2]);\n}");
harness.AppendLine("static void RefFrustum(float* m, float t, float b, float l, float r, float n, float f) {");
harness.AppendLine(" for (int i = 0; i < 16; ++i) m[i] = 0.0f;\n const float invRL = 1.0f / (r - l);\n const float invTB = 1.0f / (t - b);\n const float invFN = 1.0f / (f - n);\n m[0] = 2.0f * n * invRL;\n m[5] = 2.0f * n * invTB;\n m[2] = (r + l) * invRL;\n m[6] = (t + b) * invTB;\n m[10] = -n * invFN;\n m[11] = -(f * n) * invFN;\n m[14] = -1.0f;\n}");
harness.AppendLine("static void RefPerspective(float* m, float fovY, float aspect, float n, float f) {");
harness.AppendLine(" for (int i = 0; i < 16; ++i) m[i] = 0.0f;\n const float fov = 0.5f * (3.14159265358979323846f / 180.0f) * fovY;\n const float cot = 1.0f / tanf(fov);\n const float invFN = 1.0f / (f - n);\n m[0] = cot / aspect;\n m[5] = cot;\n m[10] = -n * invFN;\n m[11] = -(f * n) * invFN;\n m[14] = -1.0f;\n}");
harness.AppendLine("static void RefOrtho(float* m, float t, float b, float l, float r, float n, float f) {");
harness.AppendLine(" for (int i = 0; i < 16; ++i) m[i] = 0.0f;\n const float invRL = 1.0f / (r - l);\n const float invTB = 1.0f / (t - b);\n const float invFN = 1.0f / (f - n);\n m[0] = 2.0f * invRL;\n m[3] = -(r + l) * invRL;\n m[5] = 2.0f * invTB;\n m[7] = -(t + b) * invTB;\n m[10] = -1.0f * invFN;\n m[11] = -f * invFN;\n m[15] = 1.0f;\n}");
harness.AppendLine("static void RefLightPerspective(float* m, float fovY, float aspect, float scaleS, float scaleT, float transS, float transT) {");
harness.AppendLine(" for (int i = 0; i < 12; ++i) m[i] = 0.0f;\n const float fov = 0.5f * (3.14159265358979323846f / 180.0f) * fovY;\n const float cot = 1.0f / tanf(fov);\n m[0] = scaleS * (cot / aspect);\n m[5] = scaleT * cot;\n m[2] = -transS;\n m[6] = -transT;\n m[10] = -1.0f;\n}");
harness.AppendLine("static void RefLightFrustum(float* m, float t, float b, float l, float r, float n, float scaleS, float scaleT, float transS, float transT) {");
harness.AppendLine(" for (int i = 0; i < 12; ++i) m[i] = 0.0f;\n const float invRL = 1.0f / (r - l);\n const float invTB = 1.0f / (t - b);\n m[0] = scaleS * (2.0f * n * invRL);\n m[2] = scaleS * (invRL * (r + l)) - transS;\n m[5] = scaleT * (2.0f * n * invTB);\n m[6] = scaleT * (invTB * (t + b)) - transT;\n m[10] = -1.0f;\n}");
harness.AppendLine("static void RefLightOrtho(float* m, float t, float b, float l, float r, float scaleS, float scaleT, float transS, float transT) {");
harness.AppendLine(" for (int i = 0; i < 12; ++i) m[i] = 0.0f;\n const float invRL = 1.0f / (r - l);\n const float invTB = 1.0f / (t - b);\n m[0] = scaleS * (2.0f * invRL);\n m[3] = transS + scaleS * (invRL * -(r + l));\n m[5] = scaleT * (2.0f * invTB);\n m[7] = transT + scaleT * (invTB * -(t + b));\n m[11] = 1.0f;\n}");
harness.AppendLine("int main() {");
harness.AppendLine(" std::puts(\"mtx harness start\");");
harness.AppendLine(" Memory::Init(0x02000000);");
harness.AppendLine(" InitializeDataSections();");
harness.AppendLine(" CpuContext g_cpu{};");
harness.AppendLine(" g_cpu.gpr[1] = 0x81700000;");
harness.AppendLine(" g_cpu.gpr[2] = RuntimeConfig::SDA2_BASE;");
harness.AppendLine(" g_cpu.gpr[13] = RuntimeConfig::SDA1_BASE;");
harness.AppendLine(" CpuContextScope cpuScope(&g_cpu);");
harness.AppendLine(" const uint32_t base = 0x80000000;");
harness.AppendLine(" const uint32_t matA = base + 0x100;");
harness.AppendLine(" const uint32_t matB = base + 0x200;");
harness.AppendLine(" const uint32_t matC = base + 0x300;");
harness.AppendLine(" const uint32_t matD = base + 0x400;");
harness.AppendLine(" const uint32_t matArr = base + 0x500;");
harness.AppendLine(" const uint32_t vecA = base + 0x800;");
harness.AppendLine(" const uint32_t vecB = base + 0x820;");
harness.AppendLine(" const uint32_t vecC = base + 0x840;");
harness.AppendLine(" const uint32_t quatA = base + 0x900;");
harness.AppendLine(" const uint32_t quatB = base + 0x920;");
harness.AppendLine(" const uint32_t quatC = base + 0x940;");
harness.AppendLine(" const uint32_t mat44A = base + 0xA00;");
harness.AppendLine(" const uint32_t mat44B = base + 0xB00;");
harness.AppendLine(" float a[12] = { 1.2f, 0.3f, -0.7f, 2.0f, -0.4f, 1.5f, 0.25f, -1.0f, 0.8f, -0.2f, 1.1f, 0.5f };");
harness.AppendLine(" float b[12] = { 0.9f, -0.1f, 0.4f, -0.3f, 0.2f, 1.3f, -0.6f, 0.7f, -0.5f, 0.8f, 1.2f, -0.9f };");
harness.AppendLine(" float c[12] = { -0.2f, 1.1f, 0.0f, 0.3f, 0.4f, -0.7f, 0.9f, -0.1f, 0.6f, 0.2f, 1.3f, -0.8f };");
harness.AppendLine(" float vec[3] = { 1.0f, 2.0f, -3.0f };");
harness.AppendLine(" float vecOut[3] = { 0.0f, 0.0f, 0.0f };");
harness.AppendLine(" float quat[4] = { 0.2f, -0.5f, 0.3f, 0.7f };");
harness.AppendLine(" float zero34[12] = {};");
harness.AppendLine(" float zero44[16] = {};");
// PSMTXIdentity
harness.AppendLine(" { float got[12]; float exp[12]; RefIdentity(exp); WriteMat34(matA, a);");
EmitCall(harness, specs, "PSMTXIdentity_80199d04", new[] { "matA" });
harness.AppendLine(" ReadMat34(matA, got); ExpectMat34(got, exp, \"PSMTXIdentity\"); }");
// PSMTXCopy
harness.AppendLine(" { float got[12]; WriteMat34(matA, a); WriteMat34(matB, b);");
EmitCall(harness, specs, "PSMTXCopy_80199d30", new[] { "matA", "matB" });
harness.AppendLine(" ReadMat34(matB, got); ExpectMat34(got, a, \"PSMTXCopy\"); }");
// PSMTXConcat
harness.AppendLine(" { float got[12]; float exp[12]; RefConcat(a, b, exp); WriteMat34(matA, a); WriteMat34(matB, b);");
EmitCall(harness, specs, "PSMTXConcat_80199d64", new[] { "matA", "matB", "matC" });
harness.AppendLine(" ReadMat34(matC, got); ExpectMat34(got, exp, \"PSMTXConcat\"); }");
// PSMTXConcatArray
harness.AppendLine(" { float got[24]; float exp[24]; WriteMat34(matA, a); WriteMat34(matArr, b); WriteMat34(matArr + 0x30, c);\n RefConcat(a, b, exp);\n RefConcat(a, c, exp + 12);\n for (int i = 0; i < 24; ++i) got[i] = 0.0f;\n WriteMat34(matC, zero34);\n WriteMat34(matC + 0x30, zero34);\n ");
EmitCall(harness, specs, "PSMTXConcatArray_80199e30", new[] { "matA", "matArr", "matC", "2" });
harness.AppendLine(" ReadMat34(matC, got); ReadMat34(matC + 0x30, got + 12); ExpectMat34(got, exp, \"PSMTXConcatArray[0]\"); ExpectMat34(got + 12, exp + 12, \"PSMTXConcatArray[1]\"); }");
// PSMTXInverse
harness.AppendLine(" { float got[12]; float exp[12]; WriteMat34(matA, a);\n bool ok = RefInverse(a, exp);\n assert(ok);\n");
EmitCall(harness, specs, "PSMTXInverse_80199fc8", new[] { "matA", "matB" }, resultVar: "ret");
harness.AppendLine(" ReadMat34(matB, got); ExpectMat34(got, exp, \"PSMTXInverse\"); assert(ret == 1); }");
// PSMTXInvXpose
harness.AppendLine(" { float got[12]; float exp[12]; WriteMat34(matA, a);\n bool ok = RefInvXpose(a, exp);\n assert(ok);\n");
EmitCall(harness, specs, "PSMTXInvXpose_8019a0c0", new[] { "matA", "matB" }, resultVar: "ret");
harness.AppendLine(" ReadMat34(matB, got); ExpectMat34(got, exp, \"PSMTXInvXpose\"); assert(ret == 1); }");
// PSMTXRotRad
harness.AppendLine(" { float got[12]; float exp[12]; const float rad = 0.7f; RefRotRad(exp, 'y', rad);\n WriteMat34(matA, a);");
EmitCall(harness, specs, "PSMTXRotRad_8019a188", new[] { "matA", "'y'", "rad" });
harness.AppendLine(" ReadMat34(matA, got); ExpectMat34(got, exp, \"PSMTXRotRad\"); }");
// PSMTXRotTrig (test x,y,z)
harness.AppendLine(" { float got[12]; float exp[12]; const float rad = 0.4f; const float s = sinf(rad); const float c = cosf(rad);\n");
harness.AppendLine(" RefRotTrig(exp, 'x', s, c); WriteMat34(matA, a);");
EmitCall(harness, specs, "PSMTXRotTrig_8019a204", new[] { "matA", "'x'", "s", "c" });
harness.AppendLine(" ReadMat34(matA, got); ExpectMat34(got, exp, \"PSMTXRotTrigX\");\n RefRotTrig(exp, 'z', s, c); WriteMat34(matA, a);");
EmitCall(harness, specs, "PSMTXRotTrig_8019a204", new[] { "matA", "'z'", "s", "c" });
harness.AppendLine(" ReadMat34(matA, got); ExpectMat34(got, exp, \"PSMTXRotTrigZ\"); }");
// PSMTXRotAxisRadInternal
harness.AppendLine(" { float got[12]; float exp[12]; float axis[3] = { 1.0f, 2.0f, 3.0f }; const float rad = 0.9f;\n const float s = sinf(rad); const float c0 = cosf(rad); RefRotAxisRadInternal(exp, axis, s, c0);\n WriteVec3(vecA, axis); WriteMat34(matA, a);");
EmitCall(harness, specs, "PSMTXRotAxisRadInternal_8019a2b4", new[] { "matA", "vecA", "s", "c0" });
harness.AppendLine(" ReadMat34(matA, got); ExpectMat34(got, exp, \"PSMTXRotAxisRadInternal\"); }");
// PSMTXRotAxisRad
harness.AppendLine(" { float got[12]; float exp[12]; float axis[3] = { -0.5f, 1.3f, 0.8f }; const float rad = -0.6f; RefRotAxisRad(exp, axis, rad);\n WriteVec3(vecA, axis); WriteMat34(matA, a);");
EmitCall(harness, specs, "PSMTXRotAxisRad_8019a364", new[] { "matA", "vecA", "rad" });
harness.AppendLine(" ReadMat34(matA, got); ExpectMat34(got, exp, \"PSMTXRotAxisRad\"); }");
// PSMTXTrans
harness.AppendLine(" { float got[12]; float exp[12]; RefTrans(exp, 1.5f, -2.0f, 0.75f);\n WriteMat34(matA, a);");
EmitCall(harness, specs, "PSMTXTrans_8019a3e0", new[] { "matA", "1.5f", "-2.0f", "0.75f" });
harness.AppendLine(" ReadMat34(matA, got); ExpectMat34(got, exp, \"PSMTXTrans\"); }");
// PSMTXTransApply
harness.AppendLine(" { float got[12]; float exp[12]; RefTransApply(a, exp, 0.25f, -0.5f, 1.2f); WriteMat34(matA, a);");
EmitCall(harness, specs, "PSMTXTransApply_8019a414", new[] { "matA", "matB", "0.25f", "-0.5f", "1.2f" });
harness.AppendLine(" ReadMat34(matB, got); ExpectMat34(got, exp, \"PSMTXTransApply\"); }");
// PSMTXScale
harness.AppendLine(" { float got[12]; float exp[12]; RefScale(exp, 2.0f, 0.5f, -1.5f); WriteMat34(matA, a);");
EmitCall(harness, specs, "PSMTXScale_8019a460", new[] { "matA", "2.0f", "0.5f", "-1.5f" });
harness.AppendLine(" ReadMat34(matA, got); ExpectMat34(got, exp, \"PSMTXScale\"); }");
// PSMTXScaleApply
harness.AppendLine(" { float got[12]; float exp[12]; RefScaleApply(a, exp, 1.1f, -0.7f, 0.6f); WriteMat34(matA, a);");
EmitCall(harness, specs, "PSMTXScaleApply_8019a488", new[] { "matA", "matB", "1.1f", "-0.7f", "0.6f" });
harness.AppendLine(" ReadMat34(matB, got); ExpectMat34(got, exp, \"PSMTXScaleApply\"); }");
// PSMTXQuat
harness.AppendLine(" { float got[12]; float exp[12]; RefQuat(exp, quat);\n WriteF32(quatA + 0, quat[0]); WriteF32(quatA + 4, quat[1]); WriteF32(quatA + 8, quat[2]); WriteF32(quatA + 12, quat[3]);\n WriteMat34(matA, a);");
EmitCall(harness, specs, "PSMTXQuat_8019a4e0", new[] { "matA", "quatA" });
harness.AppendLine(" ReadMat34(matA, got); ExpectMat34(got, exp, \"PSMTXQuat\"); }");
// C_MTXLookAt
harness.AppendLine(" { float got[12]; float exp[12]; float camPos[3] = { 1.0f, 2.0f, 3.0f }; float camUp[3] = { 0.0f, 1.0f, 0.0f }; float target[3] = { 0.0f, 0.0f, 0.0f };\n RefLookAt(exp, camPos, camUp, target); WriteVec3(vecA, camPos); WriteVec3(vecB, camUp); WriteVec3(vecC, target);\n WriteMat34(matA, a);");
EmitCall(harness, specs, "C_MTXLookAt_8019a584", new[] { "matA", "vecA", "vecB", "vecC" });
harness.AppendLine(" ReadMat34(matA, got); ExpectMat34(got, exp, \"C_MTXLookAt\"); }");
// C_MTXLightPerspective
harness.AppendLine(" { float got[12]; float exp[12]; RefLightPerspective(exp, 30.0f, 1.2f, 0.5f, 0.75f, 0.5f, 0.25f); WriteMat34(matA, a);");
EmitCall(harness, specs, "C_MTXLightPerspective_8019a79c", new[] { "matA", "30.0f", "1.2f", "0.5f", "0.75f", "0.5f", "0.25f" });
harness.AppendLine(" ReadMat34(matA, got); ExpectMat34(got, exp, \"C_MTXLightPerspective\"); }");
// C_MTXLightFrustum (with stack arg for transT)
harness.AppendLine(" { float got[12]; float exp[12]; RefLightFrustum(exp, 1.2f, -0.8f, -1.0f, 1.4f, 0.6f, 0.5f, 0.5f, 0.4f, 0.6f); WriteMat34(matA, a);\n WriteF32(g_cpu.gpr[1] + 8, 0.6f);");
EmitCall(harness, specs, "C_MTXLightFrustum_8019a6f8", new[] { "matA", "1.2f", "-0.8f", "-1.0f", "1.4f", "0.6f", "0.5f", "0.5f", "0.4f" });
harness.AppendLine(" ReadMat34(matA, got); ExpectMat34(got, exp, \"C_MTXLightFrustum\"); }");
// C_MTXLightOrtho
harness.AppendLine(" { float got[12]; float exp[12]; RefLightOrtho(exp, 1.0f, -1.0f, -2.0f, 2.0f, 0.5f, 0.7f, 0.2f, 0.3f); WriteMat34(matA, a);");
EmitCall(harness, specs, "C_MTXLightOrtho_8019a894", new[] { "matA", "1.0f", "-1.0f", "-2.0f", "2.0f", "0.5f", "0.7f", "0.2f", "0.3f" });
harness.AppendLine(" ReadMat34(matA, got); ExpectMat34(got, exp, \"C_MTXLightOrtho\"); }");
// PSMTXMultVec
harness.AppendLine(" { float got[3]; float exp[3]; RefMultVec(a, vec, exp); WriteMat34(matA, a); WriteVec3(vecA, vec);\n");
EmitCall(harness, specs, "PSMTXMultVec_8019a91c", new[] { "matA", "vecA", "vecB" });
harness.AppendLine(" ReadVec3(vecB, got); for (int i = 0; i < 3; ++i) { if (!Near(got[i], exp[i])) { std::printf(\"[mtx] PSMTXMultVec mismatch %d\\n\", i); assert(false);} } }");
// PSMTXMultVecSR
harness.AppendLine(" { float got[3]; float exp[3]; RefMultVecSR(a, vec, exp); WriteMat34(matA, a); WriteVec3(vecA, vec);\n");
EmitCall(harness, specs, "PSMTXMultVecSR_8019a970", new[] { "matA", "vecA", "vecB" });
harness.AppendLine(" ReadVec3(vecB, got); for (int i = 0; i < 3; ++i) { if (!Near(got[i], exp[i])) { std::printf(\"[mtx] PSMTXMultVecSR mismatch %d\\n\", i); assert(false);} } }");
// C_MTXFrustum
harness.AppendLine(" { float got[16]; float exp[16]; RefFrustum(exp, 1.0f, -1.0f, -1.5f, 1.5f, 1.0f, 10.0f);\n WriteMat44(mat44A, zero44);");
EmitCall(harness, specs, "C_MTXFrustum_8019a9c4", new[] { "mat44A", "1.0f", "-1.0f", "-1.5f", "1.5f", "1.0f", "10.0f" });
harness.AppendLine(" ReadMat44(mat44A, got); ExpectMat44(got, exp, \"C_MTXFrustum\"); }");
// C_MTXPerspective
harness.AppendLine(" { float got[16]; float exp[16]; RefPerspective(exp, 60.0f, 1.3333f, 1.0f, 100.0f);\n WriteMat44(mat44A, zero44);");
EmitCall(harness, specs, "C_MTXPerspective_8019aa60", new[] { "mat44A", "60.0f", "1.3333f", "1.0f", "100.0f" });
harness.AppendLine(" ReadMat44(mat44A, got); ExpectMat44(got, exp, \"C_MTXPerspective\"); }");
// C_MTXOrtho
harness.AppendLine(" { float got[16]; float exp[16]; RefOrtho(exp, 1.0f, -1.0f, -2.0f, 2.0f, 0.5f, 20.0f);\n WriteMat44(mat44A, zero44);");
EmitCall(harness, specs, "C_MTXOrtho_8019ab4c", new[] { "mat44A", "1.0f", "-1.0f", "-2.0f", "2.0f", "0.5f", "20.0f" });
harness.AppendLine(" ReadMat44(mat44A, got); ExpectMat44(got, exp, \"C_MTXOrtho\"); }");
// MTX__PSVECAdd
harness.AppendLine(" { float aV[3] = { 1.0f, 2.0f, 3.0f }; float bV[3] = { 4.0f, -5.0f, 6.0f }; float got[3]; float exp[3] = { 5.0f, -3.0f, 9.0f };");
harness.AppendLine(" WriteVec3(vecA, aV); WriteVec3(vecB, bV);");
EmitCall(harness, specs, "MTX__PSVECAdd_8019abe4", new[] { "vecA", "vecB", "vecC" });
harness.AppendLine(" ReadVec3(vecC, got); for (int i = 0; i < 3; ++i) { if (!Near(got[i], exp[i])) { std::printf(\"[mtx] PSVECAdd mismatch %d\\n\", i); assert(false); } } }");
// MTX__PSVECScale
harness.AppendLine(" { float src[3] = { 1.5f, -2.0f, 3.0f }; float got[3]; float exp[3] = { 3.0f, -4.0f, 6.0f };");
harness.AppendLine(" WriteVec3(vecA, src);");
EmitCall(harness, specs, "MTX__PSVECScale_8019ac08", new[] { "vecA", "vecB", "2.0f" });
harness.AppendLine(" ReadVec3(vecB, got); for (int i = 0; i < 3; ++i) { if (!Near(got[i], exp[i])) { std::printf(\"[mtx] PSVECScale mismatch %d\\n\", i); assert(false); } } }");
// MTX__PSVECNormalize
harness.AppendLine(" { float src[3] = { 3.0f, 4.0f, 0.0f }; float got[3]; float exp[3]; VecNormalize(src, exp);");
harness.AppendLine(" WriteVec3(vecA, src);");
EmitCall(harness, specs, "MTX__PSVECNormalize_8019ac24", new[] { "vecA", "vecB" });
harness.AppendLine(" ReadVec3(vecB, got); for (int i = 0; i < 3; ++i) { if (!Near(got[i], exp[i])) { std::printf(\"[mtx] PSVECNormalize mismatch %d\\n\", i); assert(false); } } }");
// MTX__PSVECMag
harness.AppendLine(" { float src[3] = { 3.0f, 4.0f, 0.0f }; WriteVec3(vecA, src);");
EmitCall(harness, specs, "MTX__PSVECMag_8019ac68", new[] { "vecA" });
harness.AppendLine(" const float got = ReadFprFloat(g_cpu, 1); const float exp = VecMag(src); if (!Near(got, exp)) { std::printf(\"[mtx] PSVECMag mismatch got %.6f exp %.6f\\n\", got, exp); assert(false); } }");
// MTX__PSVECDotProduct
harness.AppendLine(" { float aV[3] = { 1.0f, 2.0f, 3.0f }; float bV[3] = { 4.0f, -5.0f, 6.0f }; WriteVec3(vecA, aV); WriteVec3(vecB, bV);");
EmitCall(harness, specs, "MTX__PSVECDotProduct_8019acac", new[] { "vecA", "vecB" });
harness.AppendLine(" const float got = ReadFprPs0(g_cpu, 1); const float exp = VecDot(aV, bV); if (!Near(got, exp)) { std::printf(\"[mtx] PSVECDotProduct mismatch got %.6f exp %.6f\\n\", got, exp); assert(false); } }");
// MTX__PSVECCrossProduct
harness.AppendLine(" { float aV[3] = { 1.0f, 0.0f, 0.0f }; float bV[3] = { 0.0f, 1.0f, 0.0f }; float got[3]; float exp[3]; VecCross(aV, bV, exp);");
harness.AppendLine(" WriteVec3(vecA, aV); WriteVec3(vecB, bV);");
EmitCall(harness, specs, "MTX__PSVECCrossProduct_8019accc", new[] { "vecA", "vecB", "vecC" });
harness.AppendLine(" ReadVec3(vecC, got); for (int i = 0; i < 3; ++i) { if (!Near(got[i], exp[i])) { std::printf(\"[mtx] PSVECCrossProduct mismatch %d\\n\", i); assert(false); } } }");
// MTX__C_VECHalfAngle
harness.AppendLine(" { float aV[3] = { 1.0f, 0.0f, 0.0f }; float bV[3] = { 0.0f, 1.0f, 0.0f }; float got[3]; float exp[3]; VecHalfAngle(aV, bV, exp);");
harness.AppendLine(" WriteVec3(vecA, aV); WriteVec3(vecB, bV);");
EmitCall(harness, specs, "MTX__C_VECHalfAngle_8019ad08", new[] { "vecA", "vecB", "vecC" });
harness.AppendLine(" ReadVec3(vecC, got); for (int i = 0; i < 3; ++i) { if (!Near(got[i], exp[i])) { std::printf(\"[mtx] C_VECHalfAngle mismatch %d\\n\", i); assert(false); } } }");
// MTX__PSVECSquareDistance
harness.AppendLine(" { float aV[3] = { 1.0f, 2.0f, 3.0f }; float bV[3] = { 4.0f, -2.0f, 6.0f }; WriteVec3(vecA, aV); WriteVec3(vecB, bV);");
EmitCall(harness, specs, "MTX__PSVECSquareDistance_8019ade0", new[] { "vecA", "vecB" });
harness.AppendLine(" const float got = ReadFprPs0(g_cpu, 1); const float exp = VecSquareDistance(aV, bV); if (!Near(got, exp)) { std::printf(\"[mtx] PSVECSquareDistance mismatch got %.6f exp %.6f\\n\", got, exp); assert(false); } }");
// MTX__PSQUATMultiply
harness.AppendLine(" { float aQ[4] = { 0.1f, 0.2f, 0.3f, 0.9f }; float bQ[4] = { -0.3f, 0.5f, 0.1f, 0.8f }; float got[4]; float exp[4]; QuatMul(aQ, bQ, exp);");
harness.AppendLine(" WriteQuat(quatA, aQ); WriteQuat(quatB, bQ);");
EmitCall(harness, specs, "MTX__PSQUATMultiply_8019ae08", new[] { "quatA", "quatB", "quatC" });
harness.AppendLine(" ReadQuat(quatC, got); for (int i = 0; i < 4; ++i) { if (!Near(got[i], exp[i])) { std::printf(\"[mtx] PSQUATMultiply mismatch %d\\n\", i); assert(false); } } }");
// MTX__PSQUATScale
harness.AppendLine(" { float src[4] = { 1.0f, -2.0f, 3.0f, -4.0f }; float got[4]; float exp[4] = { 0.5f, -1.0f, 1.5f, -2.0f };");
harness.AppendLine(" WriteQuat(quatA, src);");
EmitCall(harness, specs, "MTX__PSQUATScale_8019ae64", new[] { "quatA", "quatB", "0.5f" });
harness.AppendLine(" ReadQuat(quatB, got); for (int i = 0; i < 4; ++i) { if (!Near(got[i], exp[i])) { std::printf(\"[mtx] PSQUATScale mismatch %d\\n\", i); assert(false); } } }");
// MTX__PSQUATDotProduct
harness.AppendLine(" { float aQ[4] = { 1.0f, 2.0f, 3.0f, 4.0f }; float bQ[4] = { -2.0f, 0.5f, 1.0f, -1.5f }; WriteQuat(quatA, aQ); WriteQuat(quatB, bQ);");
EmitCall(harness, specs, "MTX__PSQUATDotProduct_8019ae80", new[] { "quatA", "quatB" });
harness.AppendLine(" const float got = ReadFprPs0(g_cpu, 1); const float exp = QuatDot(aQ, bQ); if (!Near(got, exp)) { std::printf(\"[mtx] PSQUATDotProduct mismatch got %.6f exp %.6f\\n\", got, exp); assert(false); } }");
// MTX__PSQUATNormalize
harness.AppendLine(" { float src[4] = { 0.0f, 0.0f, 0.0f, 2.0f }; float got[4]; float exp[4]; QuatNormalize(src, exp);");
harness.AppendLine(" WriteQuat(quatA, src);");
EmitCall(harness, specs, "MTX__PSQUATNormalize_8019aea0", new[] { "quatA", "quatB" });
harness.AppendLine(" ReadQuat(quatB, got); for (int i = 0; i < 4; ++i) { if (!Near(got[i], exp[i])) { std::printf(\"[mtx] PSQUATNormalize mismatch %d\\n\", i); assert(false); } } }");
// MTX__PSQUATInverse
harness.AppendLine(" { float src[4] = { 0.2f, -0.3f, 0.4f, 0.5f }; float got[4]; float exp[4]; QuatInverse(src, exp);");
harness.AppendLine(" WriteQuat(quatA, src);");
EmitCall(harness, specs, "MTX__PSQUATInverse_8019aef4", new[] { "quatA", "quatB" });
harness.AppendLine(" ReadQuat(quatB, got); for (int i = 0; i < 4; ++i) { if (!Near(got[i], exp[i])) { std::printf(\"[mtx] PSQUATInverse mismatch %d got %.6f exp %.6f\\n\", i, got[i], exp[i]); assert(false); } } }");
// MTX__C_QUATMtx
harness.AppendLine(" { float ident[12] = { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f }; float got[4]; float exp[4] = { 0.0f, 0.0f, 0.0f, 1.0f };");
harness.AppendLine(" WriteMat34(matA, ident);");
EmitCall(harness, specs, "MTX__C_QUATMtx_8019af48", new[] { "quatA", "matA" });
harness.AppendLine(" ReadQuat(quatA, got); for (int i = 0; i < 4; ++i) { if (!Near(got[i], exp[i])) { std::printf(\"[mtx] C_QUATMtx mismatch %d\\n\", i); assert(false); } } }");
// MTX__C_QUATLerp
harness.AppendLine(" { float aQ[4] = { 0.0f, 0.5f, 0.0f, 0.5f }; float bQ[4] = { 1.0f, -0.5f, 0.5f, 0.0f }; float got[4]; float exp[4]; QuatLerp(aQ, bQ, 0.25f, exp);");
harness.AppendLine(" WriteQuat(quatA, aQ); WriteQuat(quatB, bQ);");
EmitCall(harness, specs, "MTX__C_QUATLerp_8019b114", new[] { "quatA", "quatB", "quatC", "0.25f" });
harness.AppendLine(" ReadQuat(quatC, got); for (int i = 0; i < 4; ++i) { if (!Near(got[i], exp[i])) { std::printf(\"[mtx] C_QUATLerp mismatch %d\\n\", i); assert(false); } } }");
// MTX__C_QUATSlerp
harness.AppendLine(" { float aQ[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; float bQ[4] = { 0.1f, 0.2f, 0.3f, 0.9f }; float got[4]; float exp[4]; QuatSlerp(aQ, bQ, 0.5f, exp);");
harness.AppendLine(" WriteQuat(quatA, aQ); WriteQuat(quatB, bQ);");
EmitCall(harness, specs, "MTX__C_QUATSlerp_8019b178", new[] { "quatA", "quatB", "quatC", "0.5f" });
harness.AppendLine(" ReadQuat(quatC, got); for (int i = 0; i < 4; ++i) { if (!Near(got[i], exp[i])) { std::printf(\"[mtx] C_QUATSlerp mismatch %d\\n\", i); assert(false); } } }");
harness.AppendLine(" return 0;");
harness.AppendLine("}");
var harnessPath = Path.Combine(tempRoot, "harness_mtx.cpp");
File.WriteAllText(harnessPath, harness.ToString());
var root = RepositoryRoot;
var runnerBasePath = Path.Combine(tempRoot, "runner_mtx");
var compileArgs = TranslatorCppTestHarness.BuildCompileArguments(root, tempRoot, generatedFiles, harnessPath, runnerBasePath);
compileArgs += " -ldbghelp";
Console.WriteLine("[compile] clang++ (mtx)");
var (exitCode, compileOutput) = RunProcess("clang++", compileArgs.ToString());
Assert.True(exitCode == 0, $"clang++ failed: {compileOutput}");
var runnerPath = runnerBasePath;
if (OperatingSystem.IsWindows() && !File.Exists(runnerPath))
{
runnerPath += ".exe";
}
Console.WriteLine($"[run] {runnerPath}");
var (runExitCode, runOutput) = RunProcess(runnerPath, string.Empty, TimeSpan.FromSeconds(60));
Console.Write(runOutput);
Assert.True(runExitCode == 0, $"Runner failed (exit code {runExitCode}). Output:\n{runOutput}");
}
/// <summary>
/// Leaf-inlining admission checked against real MTX bodies. Stackless primitives stay eligible,
/// while <c>PSMTXConcat</c> must not: its 64-byte r1 frame is unsafe to splice into a caller's stack analysis.
/// </summary>
[Fact]
public void LeafInliningRejectsStackFramedMatrixPrimitives()
{
var translator = BuildTranslator();
var options = TranslationOptions.Default with
{
AllowUnsupportedInstructions = true
};
LeafInlineCandidate? Classify(uint address, LeafInliningPolicy policy, out LeafInlineRejection rejection)
{
var translation = translator.Translate(address, options);
return LeafFunctionInliner.TryCreateCandidate(
address, translation.LinearIr, translation.Instructions, policy, out rejection);
}
var shipped = new LeafInliningPolicy(
MaxCalleeGuestInstructions: TranslationOptions.Default.LeafInliningMaxCalleeInstructions);
var oldCap = new LeafInliningPolicy(MaxCalleeGuestInstructions: 32);
// PSMTXCopy was admitted before the cap moved and must be admitted in
// exactly the same form afterwards - a flat statement list, not a
// spliced region - or every caller that already inlines it changes.
var copy = Classify(0x80199D30u, shipped, out var copyRejection);
Assert.Equal(LeafInlineRejection.None, copyRejection);
Assert.NotNull(copy);
Assert.Null(copy!.Blocks);
Assert.Equal(13, copy.GuestInstructionCount);
Classify(0x80199D30u, oldCap, out var copyAtOldCap);
Assert.Equal(LeafInlineRejection.None, copyAtOldCap);
var concat = Classify(0x80199D64u, shipped, out var concatRejection);
Assert.Equal(LeafInlineRejection.TouchesStackPointer, concatRejection);
Assert.Null(concat);
Classify(0x80199D64u, oldCap, out var concatAtOldCap);
Assert.Equal(LeafInlineRejection.TooLarge, concatAtOldCap);
}
/// <summary>
/// <c>nw4r::ut::List_GetNext</c> is the acyclic multi-block case: eight guest instructions across
/// four blocks, a compare/branch and two return arms that must converge on one caller continuation.
/// </summary>
[Fact]
public void LeafInliningAdmitsAnAcyclicMultiBlockListWalker()
{
var translator = BuildTranslator();
var options = TranslationOptions.Default with
{
AllowUnsupportedInstructions = true
};
var translation = translator.Translate(0x800AF180u, options);
var candidate = LeafFunctionInliner.TryCreateCandidate(
0x800AF180u,
translation.LinearIr,
translation.Instructions,
new LeafInliningPolicy(),
out var rejection);
Assert.Equal(LeafInlineRejection.None, rejection);
Assert.NotNull(candidate);
Assert.Equal(8, candidate!.GuestInstructionCount);
Assert.NotNull(candidate.Blocks);
Assert.Equal(4, candidate.Blocks!.Count);
Assert.Equal(2, candidate.Blocks.Count(block => block.Instructions[^1] is IrReturn));
// Every block leaves through an explicit terminator, so the spliced copy
// does not depend on where the caller places these blocks.
Assert.All(
candidate.Blocks,
block => Assert.True(block.Instructions[^1] is IrReturn or IrJump or IrBranch));
// Turning the shape off puts the callee straight back outside the
// admissible set.
LeafFunctionInliner.TryCreateCandidate(
0x800AF180u,
translation.LinearIr,
translation.Instructions,
new LeafInliningPolicy(AllowAcyclicMultiBlockCallees: false),
out var withoutMultiBlock);
Assert.Equal(LeafInlineRejection.ControlFlow, withoutMultiBlock);
}
/// <summary>
/// <c>EGG::Math::Sqrt</c> looked like a multi-block leaf from a profile, but
/// it saves the link register and calls out, so no relaxation of the block
/// shape can admit it. Pinned so a future rule change has to notice.
/// </summary>
[Fact]
public void EggMathSqrtIsNotALeafAndStaysRefused()
{
var translator = BuildTranslator();
var options = TranslationOptions.Default with
{
AllowUnsupportedInstructions = true
};
var translation = translator.Translate(0x8022F80Cu, options);
LeafFunctionInliner.TryCreateCandidate(
0x8022F80Cu,
translation.LinearIr,
translation.Instructions,
new LeafInliningPolicy(),
out var rejection);
Assert.Equal(LeafInlineRejection.TouchesLinkRegister, rejection);
}
private static MtxFuncSpec Spec(uint address, string name, ReturnKind ret, params ParamKind[] paramKinds)
=> new(address, name, ret, paramKinds);
private static void AssertReturnAbiMatches(MtxFuncSpec spec, FunctionAbiClassification classification)
{
if (spec.Return == ReturnKind.Void)
{
var isFloatReturn = classification.ReturnRepresentation is ValueRepresentation prim && prim.IsFloat;
Assert.False(isFloatReturn, $"{spec.Name} expected non-float return, got {classification.ReturnRepresentation}.");
}
else if (spec.Return == ReturnKind.Float)
{
Assert.True(classification.ReturnRepresentation is ValueRepresentation prim && prim.IsFloat,
$"{spec.Name} expected float return, got {classification.ReturnRepresentation}.");
}
else
{
Assert.True(classification.ReturnRepresentation is ValueRepresentation prim && !prim.IsFloat,
$"{spec.Name} expected integer return, got {classification.ReturnRepresentation}.");
}
}
private static IReadOnlyList<FunctionTranslationResult> TranslateWithDependencies(
FunctionTranslator translator,
IEnumerable<(uint Address, string Name)> seeds,
string outputDirectory,
List<string> generatedFiles)
{
const int MaxTranslations = 256;
var translations = new List<FunctionTranslationResult>();
var scheduled = new HashSet<uint>();
var translated = new HashSet<uint>();
var queue = new Queue<(uint Address, string Name)>();
foreach (var spec in seeds)
{
if (scheduled.Add(spec.Address))
{
queue.Enqueue(spec);
}
}
while (queue.Count > 0)
{
if (scheduled.Count > MaxTranslations)
{
throw new InvalidOperationException($"Auto-translation queue exceeded {MaxTranslations} functions. Check for runaway dependency discovery.");
}
var spec = queue.Dequeue();
if (translated.Contains(spec.Address))
{
continue;
}
var preferredName = string.IsNullOrWhiteSpace(spec.Name) ? $"func_{spec.Address:X8}" : spec.Name;
Console.WriteLine($"[translate] {preferredName} 0x{spec.Address:X8}");
var result = translator.Translate(spec.Address, new TranslationOptions(preferredName));
var classification = result.AbiClassification;
Console.WriteLine($" [abi] {classification.Name} -> {classification.ReturnRepresentation}");
var path = Path.Combine(outputDirectory, $"{result.Name}.cpp");
File.WriteAllText(path, result.CxxCode);
generatedFiles.Add(path);
translations.Add(result);
translated.Add(result.EntryPoint);
foreach (var dependency in DiscoverCallTargets(result))
{
if (translated.Contains(dependency))
{
continue;
}
if (scheduled.Add(dependency))
{
queue.Enqueue((dependency, $"func_{dependency:X8}"));
}
}
}
return translations;
}
private static IEnumerable<uint> DiscoverCallTargets(FunctionTranslationResult translation)
{
var discovered = new HashSet<uint>();
foreach (var block in translation.LinearIr.Blocks)
{
foreach (var instruction in block.Instructions)
{
if (instruction is not IrCall call)
{
continue;
}
if (string.IsNullOrWhiteSpace(call.Target) || !call.Target.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (!uint.TryParse(call.Target.Substring(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var address))
{
continue;
}
if (address == translation.EntryPoint)
{
continue;
}
if (discovered.Add(address))
{
yield return address;
}
}
}
}
private static string PrototypeFor(FunctionAbiClassification signature)
{
return $"extern \"C\" void {signature.Name}(CpuContext* ctx);";
}
private static void EmitCall(StringBuilder sb, IReadOnlyList<MtxFuncSpec> specs, string name, string[] args, string? resultVar = null)
{
var spec = specs.Single(s => s.Name == name);
var gprIndex = 3;
var fprIndex = 1;
for (int i = 0; i < spec.Params.Count; i++)
{
var arg = args.Length > i ? args[i] : "0";
if (spec.Params[i] == ParamKind.Fpr)
{
sb.AppendLine($" g_cpu.fpr[{fprIndex}].d = {arg};");
fprIndex++;
}
else
{
sb.AppendLine($" g_cpu.gpr[{gprIndex}] = ToGpr({arg});");
gprIndex++;
}
}
sb.AppendLine($" {name}(&g_cpu);");
if (!string.IsNullOrWhiteSpace(resultVar))
{
sb.AppendLine($" auto {resultVar} = g_cpu.gpr[3];");
}
}
}
@@ -0,0 +1,79 @@
using System.Buffers.Binary;
using Translator.Core.Mods;
using Translator.Core.Parsing.Kamek;
using Xunit;
namespace Translator.Tests;
public class OverlayFunctionBuilderTests
{
[Fact]
public void BuildAndWriteAppliesModuleAndBaseOverlayPatches()
{
var tempRoot = Path.Combine(Path.GetTempPath(), "mkw_overlay_test_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempRoot);
try
{
var baseDir = Path.Combine(tempRoot, "base");
var outDir = Path.Combine(tempRoot, "out");
Directory.CreateDirectory(baseDir);
Directory.CreateDirectory(outDir);
var baseImage = new byte[0x10];
File.WriteAllBytes(Path.Combine(baseDir, "base_text.bin"), baseImage);
const uint moduleBase = 0x81200000u;
var chunk = new KamekChunk(
0,
0,
0,
0x10,
0,
0,
0x30,
new byte[0x10],
[
new KamekCommand(0, 0, KamekCommandId.Write32, false, 0x4, [0xDEADBEEFu]),
new KamekCommand(4, 0, KamekCommandId.BranchLink, true, 0x80001004u, [0x100u]),
new KamekCommand(8, 0, KamekCommandId.Write32, true, 0x80001008u, [0x60000000u])
]);
var manifest = new BaseManifest(
"test",
1,
"RMCP01",
"P",
"",
0,
[
new BaseSectionMetadata(".text", "main.dol", 0x80001000u, 0x80001010u, true, false, "base_text.bin", 0)
],
[
new BaseFunctionRangeMetadata(0x80001000u, 0x80001010u, "func_80001000", ".text", 0, "test", ["Executable"])
],
"ranges.json");
var plan = KamekPatchPlanner.Build(chunk, manifest, moduleBase);
var result = OverlayFunctionBuilder.BuildAndWrite(chunk, manifest, plan, baseDir, outDir);
var moduleImage = File.ReadAllBytes(Path.Combine(outDir, "overlay_images", result.ModuleImageFile));
Assert.Equal(0xDEADBEEFu, BinaryPrimitives.ReadUInt32BigEndian(moduleImage.AsSpan(4, 4)));
var overlay = Assert.Single(result.OverlayFunctions);
var overlayImage = File.ReadAllBytes(Path.Combine(outDir, "overlay_images", overlay.ImageFile));
Assert.Equal(
KamekPpcEncoding.EncodeBranch(0x80001004u, moduleBase + 0x100u, link: true),
BinaryPrimitives.ReadUInt32BigEndian(overlayImage.AsSpan(4, 4)));
Assert.Equal(0x60000000u, BinaryPrimitives.ReadUInt32BigEndian(overlayImage.AsSpan(8, 4)));
Assert.Equal(2, overlay.PatchCount);
Assert.Empty(result.Diagnostics);
}
finally
{
if (Directory.Exists(tempRoot))
{
Directory.Delete(tempRoot, recursive: true);
}
}
}
}
@@ -0,0 +1,155 @@
using Translator.Core.Analysis.Ssa;
using Translator.Core.Analysis.Representation;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
namespace Translator.Tests;
public sealed class PairedFlowCodeGenTests
{
[Fact]
public void MixedScalarAndPairedPredecessorsNormalizeLiveFprOnTheEdge()
{
var function = new IrFunction(
"mixed_ps_merge",
"entry",
new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrBranch("beq", "paired", "scalar")
}),
new IrBasicBlock("paired", new IrInstruction[]
{
new IrCall("f5", "PPC_PsMul", new[]
{
IrValue.Register("f1"), IrValue.Register("f2")
}),
new IrJump("merge")
}),
new IrBasicBlock("scalar", new IrInstruction[]
{
new IrAssign("f5", IrValue.Register("f3")),
new IrJump("merge")
}),
new IrBasicBlock("merge", new IrInstruction[]
{
new IrCall("f6", "PPC_PsNeg", new[] { IrValue.Register("f5") }),
new IrReturn(null)
})
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["f1"] = ValueRepresentation.Float64,
["f2"] = ValueRepresentation.Float64,
["f3"] = ValueRepresentation.Float64,
["f5"] = ValueRepresentation.Float64,
["f6"] = ValueRepresentation.Float64,
["cr0"] = ValueRepresentation.UInt32
});
var signature = new FunctionAbiClassification("mixed_ps_merge", ValueRepresentation.Void);
var code = new CxxLinearCodeGenerator().Emit(
0x80006000, new SsaTransformer().Convert(function), signature, types);
Assert.Contains(
"f5.d = PPC_PsToScalarInline(f5.d);",
code,
StringComparison.Ordinal);
Assert.Contains(
"PPC_PsNegInline(PPC_PsFromScalarInline(f5.d))",
code,
StringComparison.Ordinal);
}
[Fact]
public void FloatingCrCompareExtractsPs0FromPairedOperands()
{
var function = new IrFunction("paired_fcmp", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall("f2", "PPC_PsMul", new[] { IrValue.Register("f3"), IrValue.Register("f4") }),
new IrCall("f5", "PPC_PsAdd", new[] { IrValue.Register("f6"), IrValue.Register("f7") }),
new IrSetCrField(0, IrValue.Register("f2"), IrValue.Register("f5"), false),
new IrReturn(null)
})
});
var types = new RepresentationEnvironment(Enumerable.Range(0, 8)
.ToDictionary(index => $"f{index}", _ => (ValueRepresentation)ValueRepresentation.Float64));
var signature = new FunctionAbiClassification("paired_fcmp", ValueRepresentation.Void);
var code = new CxxLinearCodeGenerator().Emit(
0x80006004, new SsaTransformer().Convert(function), signature, types);
Assert.Contains(
"SetCRFloatResident(cr, 0, PPC_PsToScalarInline(f2.d), PPC_PsToScalarInline(f5.d));",
code, StringComparison.Ordinal);
}
[Fact]
public void FloatingCrCompareLeavesScalarOperandsUnchanged()
{
var function = new IrFunction("scalar_fcmp", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrSetCrField(3, IrValue.Register("f1"), IrValue.Register("f2"), false),
new IrReturn(null)
})
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["f1"] = ValueRepresentation.Float64,
["f2"] = ValueRepresentation.Float64
});
var signature = new FunctionAbiClassification("scalar_fcmp", ValueRepresentation.Void);
var code = new CxxLinearCodeGenerator().Emit(
0x80006004, new SsaTransformer().Convert(function), signature, types);
Assert.Contains("SetCRFloatResident(cr, 3, f1.d, f2.d);", code, StringComparison.Ordinal);
}
[Fact]
public void ExternalEntryFprsStayScalarAcrossBackedge()
{
var function = new IrFunction(
"entry_backedge_ps_state",
"entry",
new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall(string.Empty, "PPC_Fcmp", new[] { IrValue.Imm(0), IrValue.Register("f31"), IrValue.Register("f1") }),
new IrBranch("beq", "loop", "exit")
}),
new IrBasicBlock("loop", new IrInstruction[]
{
new IrCall("f31", "PPC_PsMul", new[] { IrValue.Register("f2"), IrValue.Register("f3") }),
new IrJump("entry")
}),
new IrBasicBlock("exit", new IrInstruction[] { new IrReturn(null) })
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["f1"] = ValueRepresentation.Float64,
["f2"] = ValueRepresentation.Float64,
["f3"] = ValueRepresentation.Float64,
["f31"] = ValueRepresentation.Float64,
["cr0"] = ValueRepresentation.UInt32
});
var signature = new FunctionAbiClassification("entry_backedge_ps_state", ValueRepresentation.Void);
var ssa = new SsaTransformer().Convert(function);
var code = new CxxLinearCodeGenerator().Emit(0x80006008, ssa, signature, types);
// A raw "PPC_Fcmp" IrCall isn't normal decoder output, but residency treats it as a
// safety-net form of fcmpo/fcmpu and fuses it into the resident CR write like IrSetCrField,
// using the operands directly instead of PPC_PsToScalarInline (CxxLinearCodeGenerator.Inlines.cs "PPC_FCMP").
Assert.Contains("SetCRFloatResident(cr, 0, f31.d, f1.d);", code, StringComparison.Ordinal);
Assert.DoesNotContain("SetCRFloatResident(cr, 0, PPC_PsToScalarInline(f31.d)", code, StringComparison.Ordinal);
}
}
@@ -0,0 +1,539 @@
using Translator.Core.Disassembly;
using Translator.Core.Ir;
using Translator.Core.Lifting;
using Xunit;
namespace Translator.Tests;
public class PairedSingleLifterTests
{
private static PpcRegisterOperand Fpr(int index) => new($"f{index}", index);
private static PpcRegisterOperand Gpr(int index) => new($"r{index}", index);
[Fact]
public void LiftsPsSum0UsingPpcHelperOperandOrder()
{
var instruction = PpcInstruction.Synthetic(
0x80000000,
0,
"ps_sum0",
new PpcOperand[] { Fpr(1), Fpr(2), Fpr(3), Fpr(4) });
var ir = Assert.Single(new PpcLifter().Lift(new[] { instruction })).Ir;
var call = Assert.IsType<IrCall>(Assert.Single(ir));
Assert.Equal("f1", call.Destination);
Assert.Equal("PPC_PsSum0", call.Target);
Assert.Equal(new[] { "f2", "f4", "f3" }, call.Arguments.Select(a => a.RegisterName));
}
[Fact]
public void LiftsPsSelWithControlInSecondArgument()
{
var instruction = PpcInstruction.Synthetic(
0x80000000,
0,
"ps_sel",
new PpcOperand[] { Fpr(1), Fpr(2), Fpr(3), Fpr(4) });
var ir = Assert.Single(new PpcLifter().Lift(new[] { instruction })).Ir;
var call = Assert.IsType<IrCall>(Assert.Single(ir));
Assert.Equal("PPC_PsSel", call.Target);
Assert.Equal(new[] { "f3", "f2", "f4" }, call.Arguments.Select(a => a.RegisterName));
}
[Fact]
public void LiftsRawOpcodeAliasForPsMul()
{
var instruction = PpcInstruction.Synthetic(
0x80000000,
0x106100B2u,
"opc_4_50",
System.Array.Empty<PpcOperand>());
var ir = Assert.Single(new PpcLifter().Lift(new[] { instruction })).Ir;
var call = Assert.IsType<IrCall>(Assert.Single(ir));
Assert.Equal("PPC_PsMul", call.Target);
Assert.Equal("f3", call.Destination);
Assert.Equal(new[] { "f1", "f2" }, call.Arguments.Select(a => a.RegisterName));
}
[Fact]
public void LiftsPsNegFromExplicitMnemonic()
{
var instruction = PpcInstruction.Synthetic(
0x80000000,
0,
"ps_neg",
new PpcOperand[] { Fpr(4), Fpr(7) });
var ir = Assert.Single(new PpcLifter().Lift(new[] { instruction })).Ir;
var call = Assert.IsType<IrCall>(Assert.Single(ir));
Assert.Equal("f4", call.Destination);
Assert.Equal("PPC_PsNeg", call.Target);
Assert.Equal("f7", Assert.Single(call.Arguments).RegisterName);
}
[Fact]
public void LiftsPsqLoadWithAbsoluteBaseZeroAddress()
{
var instruction = PpcInstruction.Synthetic(
0x80000000,
0,
"psq_l",
new PpcOperand[]
{
Fpr(2),
new PpcDisplacementOperand(0x24, "r0", 0),
new PpcImmediateOperand(1),
new PpcImmediateOperand(3)
});
var ir = Assert.Single(new PpcLifter().Lift(new[] { instruction })).Ir;
Assert.Equal(3, ir.Count);
Assert.IsType<IrComment>(ir[0]);
var assign = Assert.IsType<IrAssign>(ir[1]);
Assert.Equal("f2_psq_ea", assign.Destination);
Assert.Equal(0x24, assign.Value.Constant);
var call = Assert.IsType<IrCall>(ir[2]);
Assert.Equal("PPC_PsqL", call.Target);
Assert.Equal("f2", call.Destination);
Assert.Equal(new long?[] { null, 1, 3 }, call.Arguments.Select(a => a.Constant).ToArray());
}
[Fact]
public void LiftsPsqStoreUpdateFormAndWritesBackBase()
{
var instruction = PpcInstruction.Synthetic(
0x80000000,
0,
"psq_stu",
new PpcOperand[]
{
Fpr(2),
new PpcDisplacementOperand(0x10, "r4", 4),
new PpcImmediateOperand(0),
new PpcImmediateOperand(6)
});
var ir = Assert.Single(new PpcLifter().Lift(new[] { instruction })).Ir;
Assert.Equal(4, ir.Count);
Assert.IsType<IrComment>(ir[0]);
var addr = Assert.IsType<IrBinary>(ir[1]);
Assert.Equal("r4_psq_ea", addr.Destination);
var store = Assert.IsType<IrCall>(ir[2]);
Assert.Equal("PPC_PsqSt", store.Target);
Assert.Equal("r4_psq_ea", store.Arguments[0].RegisterName);
Assert.Equal("f2", store.Arguments[1].RegisterName);
var writeback = Assert.IsType<IrAssign>(ir[3]);
Assert.Equal("r4", writeback.Destination);
Assert.Equal("r4_psq_ea", writeback.Value.RegisterName);
}
[Fact]
public void RejectsInvalidPsqUpdateWithR0Base()
{
var instruction = PpcInstruction.Synthetic(
0x80000000,
0,
"psq_lu",
new PpcOperand[]
{
Fpr(1),
new PpcDisplacementOperand(4, "r0", 0),
new PpcImmediateOperand(0),
new PpcImmediateOperand(0)
});
Assert.Throws<InvalidOperationException>(() => new PpcLifter().Lift(new[] { instruction }));
}
[Fact]
public void LiftsPsCompareUsingCrFieldImmediate()
{
var instruction = PpcInstruction.Synthetic(
0x80000000,
0,
"ps_cmpu1",
new PpcOperand[]
{
new PpcConditionRegisterOperand("cr7", 28),
Fpr(2),
Fpr(3)
});
var ir = Assert.Single(new PpcLifter().Lift(new[] { instruction })).Ir;
var call = Assert.IsType<IrCall>(Assert.Single(ir));
Assert.Equal("PPC_PsCmpu1", call.Target);
Assert.Equal(7, call.Arguments[0].Constant);
Assert.Equal("f2", call.Arguments[1].RegisterName);
Assert.Equal("f3", call.Arguments[2].RegisterName);
}
[Fact]
public void LiftsPsSelAliasAsPsNegWhenRawXformMatches()
{
var raw = (4u << 21) | (7u << 11) | (40u << 1);
var instruction = PpcInstruction.Synthetic(
0x80000000,
raw,
"ps_sel",
new PpcOperand[] { Fpr(4), Fpr(1), Fpr(2), Fpr(3) });
var ir = Assert.Single(new PpcLifter().Lift(new[] { instruction })).Ir;
var call = Assert.IsType<IrCall>(Assert.Single(ir));
Assert.Equal("PPC_PsNeg", call.Target);
Assert.Equal("f4", call.Destination);
Assert.Equal("f7", Assert.Single(call.Arguments).RegisterName);
}
[Fact]
public void LiftsRawOpcodeVariantsForMaddsAndIndexedLoadStore()
{
var maddsRaw = (3u << 21) | (1u << 16) | (2u << 11) | (4u << 6);
var madds = PpcInstruction.Synthetic(0x80000000, maddsRaw, "opc_4_28", System.Array.Empty<PpcOperand>());
var maddsIr = Assert.Single(new PpcLifter().Lift(new[] { madds })).Ir;
var maddsCall = Assert.IsType<IrCall>(Assert.Single(maddsIr));
Assert.Equal("PPC_PsMadds0", maddsCall.Target);
Assert.Equal("f3", maddsCall.Destination);
Assert.Equal(new[] { "f1", "f4", "f2" }, maddsCall.Arguments.Select(a => a.RegisterName));
var loadRaw = (3u << 21) | (4u << 16) | (5u << 11) | (1u << 10) | (6u << 7);
var load = PpcInstruction.Synthetic(0x80000004, loadRaw, "opc_4_76", System.Array.Empty<PpcOperand>());
var loadIr = Assert.Single(new PpcLifter().Lift(new[] { load })).Ir;
Assert.Equal(3, loadIr.Count);
var loadCall = Assert.IsType<IrCall>(loadIr[1]);
Assert.Equal("PPC_PsqL", loadCall.Target);
Assert.Equal("f3", loadCall.Destination);
Assert.Equal(1, loadCall.Arguments[1].Constant);
Assert.Equal(6, loadCall.Arguments[2].Constant);
var loadWriteback = Assert.IsType<IrAssign>(loadIr[2]);
Assert.Equal("r4", loadWriteback.Destination);
var storeRaw = (2u << 21) | (4u << 16) | (5u << 11) | (1u << 10) | (3u << 7);
var store = PpcInstruction.Synthetic(0x80000008, storeRaw, "opc_4_78", System.Array.Empty<PpcOperand>());
var storeIr = Assert.Single(new PpcLifter().Lift(new[] { store })).Ir;
Assert.Equal(3, storeIr.Count);
var storeCall = Assert.IsType<IrCall>(storeIr[1]);
Assert.Equal("PPC_PsqSt", storeCall.Target);
Assert.Equal("f2", storeCall.Arguments[1].RegisterName);
Assert.Equal(1, storeCall.Arguments[2].Constant);
Assert.Equal(3, storeCall.Arguments[3].Constant);
var storeWriteback = Assert.IsType<IrAssign>(storeIr[2]);
Assert.Equal("r4", storeWriteback.Destination);
}
[Fact]
public void LiftsMergeAndScalarEstimateHelpers()
{
var merge = PpcInstruction.Synthetic(
0x80000000,
0,
"ps_merge10",
new PpcOperand[] { Fpr(1), Fpr(2), Fpr(3) });
var mergeIr = Assert.Single(new PpcLifter().Lift(new[] { merge })).Ir;
var mergeCall = Assert.IsType<IrCall>(Assert.Single(mergeIr));
Assert.Equal("PPC_PsMerge10", mergeCall.Target);
var res = PpcInstruction.Synthetic(
0x80000004,
0,
"ps_rsqrte",
new PpcOperand[] { Fpr(4), Fpr(5) });
var resIr = Assert.Single(new PpcLifter().Lift(new[] { res })).Ir;
var resCall = Assert.IsType<IrCall>(Assert.Single(resIr));
Assert.Equal("PPC_PsRsqrte", resCall.Target);
Assert.Equal("f4", resCall.Destination);
var nabs = PpcInstruction.Synthetic(
0x80000008,
0,
"ps_nabs",
new PpcOperand[] { Fpr(6), Fpr(7) });
var nabsIr = Assert.Single(new PpcLifter().Lift(new[] { nabs })).Ir;
var nabsCall = Assert.IsType<IrCall>(Assert.Single(nabsIr));
Assert.Equal("PPC_PsNabs", nabsCall.Target);
Assert.Equal("f6", nabsCall.Destination);
}
[Fact]
public void KeepsPairedMoveDistinctFromScalarFmr()
{
var move = PpcInstruction.Synthetic(
0x80000000,
0,
"ps_mr",
new PpcOperand[] { Fpr(2), Fpr(7) });
var moveIr = Assert.Single(new PpcLifter().Lift(new[] { move })).Ir;
var moveCall = Assert.IsType<IrCall>(Assert.Single(moveIr));
Assert.Equal("PPC_PsMr", moveCall.Target);
Assert.Equal("f2", moveCall.Destination);
Assert.Equal("f7", Assert.Single(moveCall.Arguments).RegisterName);
}
[Fact]
public void LiftsDotVariantsForPairedSingleArithmetic()
{
var div = PpcInstruction.Synthetic(
0x7FFFFFFC,
0,
"ps_div.",
new PpcOperand[] { Fpr(1), Fpr(2), Fpr(3) });
var divIr = Assert.Single(new PpcLifter().Lift(new[] { div })).Ir;
var divCall = Assert.IsType<IrCall>(Assert.Single(divIr));
Assert.Equal("PPC_PsDiv", divCall.Target);
Assert.Equal(new[] { "f2", "f3" }, divCall.Arguments.Select(a => a.RegisterName));
var madds0 = PpcInstruction.Synthetic(
0x80000000,
0,
"ps_madds0.",
new PpcOperand[] { Fpr(1), Fpr(2), Fpr(3), Fpr(4) });
var madds0Ir = Assert.Single(new PpcLifter().Lift(new[] { madds0 })).Ir;
Assert.Equal("PPC_PsMadds0", Assert.IsType<IrCall>(Assert.Single(madds0Ir)).Target);
var madds1 = PpcInstruction.Synthetic(
0x80000004,
0,
"ps_madds1.",
new PpcOperand[] { Fpr(1), Fpr(2), Fpr(3), Fpr(4) });
var madds1Ir = Assert.Single(new PpcLifter().Lift(new[] { madds1 })).Ir;
var madds1Call = Assert.IsType<IrCall>(Assert.Single(madds1Ir));
Assert.Equal("PPC_PsMadds1", madds1Call.Target);
Assert.Equal(new[] { "f2", "f3", "f4" }, madds1Call.Arguments.Select(a => a.RegisterName));
var sum0 = PpcInstruction.Synthetic(
0x80000008,
0,
"ps_sum0.",
new PpcOperand[] { Fpr(5), Fpr(6), Fpr(7), Fpr(8) });
var sum0Ir = Assert.Single(new PpcLifter().Lift(new[] { sum0 })).Ir;
var sum0Call = Assert.IsType<IrCall>(Assert.Single(sum0Ir));
Assert.Equal("PPC_PsSum0", sum0Call.Target);
Assert.Equal(new[] { "f6", "f8", "f7" }, sum0Call.Arguments.Select(a => a.RegisterName));
var sum1 = PpcInstruction.Synthetic(
0x8000000C,
0,
"ps_sum1.",
new PpcOperand[] { Fpr(5), Fpr(6), Fpr(7), Fpr(8) });
var sum1Ir = Assert.Single(new PpcLifter().Lift(new[] { sum1 })).Ir;
var sum1Call = Assert.IsType<IrCall>(Assert.Single(sum1Ir));
Assert.Equal("PPC_PsSum1", sum1Call.Target);
Assert.Equal(new[] { "f6", "f8", "f7" }, sum1Call.Arguments.Select(a => a.RegisterName));
var muls0 = PpcInstruction.Synthetic(
0x80000008,
0,
"ps_muls0.",
new PpcOperand[] { Fpr(9), Fpr(10), Fpr(11) });
var muls0Ir = Assert.Single(new PpcLifter().Lift(new[] { muls0 })).Ir;
Assert.Equal("PPC_PsMuls0", Assert.IsType<IrCall>(Assert.Single(muls0Ir)).Target);
var muls1 = PpcInstruction.Synthetic(
0x8000000C,
0,
"ps_muls1.",
new PpcOperand[] { Fpr(12), Fpr(13), Fpr(14) });
var muls1Ir = Assert.Single(new PpcLifter().Lift(new[] { muls1 })).Ir;
Assert.Equal("PPC_PsMuls1", Assert.IsType<IrCall>(Assert.Single(muls1Ir)).Target);
}
[Fact]
public void LiftsExplicitUnaryMoveAndCompareVariants()
{
var res = PpcInstruction.Synthetic(
0x80000000,
0,
"ps_res",
new PpcOperand[] { Fpr(1), Fpr(2) });
var resIr = Assert.Single(new PpcLifter().Lift(new[] { res })).Ir;
Assert.Equal("PPC_PsRes", Assert.IsType<IrCall>(Assert.Single(resIr)).Target);
var abs = PpcInstruction.Synthetic(
0x80000004,
0,
"ps_abs.",
new PpcOperand[] { Fpr(3), Fpr(4) });
var absIr = Assert.Single(new PpcLifter().Lift(new[] { abs })).Ir;
Assert.Equal("PPC_PsAbs", Assert.IsType<IrCall>(Assert.Single(absIr)).Target);
var mr = PpcInstruction.Synthetic(
0x80000008,
0,
"ps_mr",
new PpcOperand[] { Fpr(5), Fpr(6) });
var mrIr = Assert.Single(new PpcLifter().Lift(new[] { mr })).Ir;
var mrCall = Assert.IsType<IrCall>(Assert.Single(mrIr));
Assert.Equal("PPC_PsMr", mrCall.Target);
Assert.Equal("f5", mrCall.Destination);
Assert.Equal("f6", Assert.Single(mrCall.Arguments).RegisterName);
var mrDot = PpcInstruction.Synthetic(
0x8000000A,
0,
"ps_mr.",
new PpcOperand[] { Fpr(5), Fpr(6) });
Assert.Equal(
"PPC_PsMr",
Assert.IsType<IrCall>(Assert.Single(Assert.Single(new PpcLifter().Lift(new[] { mrDot })).Ir)).Target);
var cmpo0 = PpcInstruction.Synthetic(
0x8000000C,
0,
"ps_cmpo0.",
new PpcOperand[] { new PpcConditionRegisterOperand("cr5", 20), Fpr(7), Fpr(8) });
var cmpo0Ir = Assert.Single(new PpcLifter().Lift(new[] { cmpo0 })).Ir;
var cmpo0Call = Assert.IsType<IrCall>(Assert.Single(cmpo0Ir));
Assert.Equal("PPC_PsCmpo0", cmpo0Call.Target);
Assert.Equal(5, cmpo0Call.Arguments[0].Constant);
var cmpu0 = PpcInstruction.Synthetic(
0x80000010,
0,
"ps_cmpu0",
new PpcOperand[] { new PpcConditionRegisterOperand("cr3", 12), Fpr(9), Fpr(10) });
var cmpu0Ir = Assert.Single(new PpcLifter().Lift(new[] { cmpu0 })).Ir;
var cmpu0Call = Assert.IsType<IrCall>(Assert.Single(cmpu0Ir));
Assert.Equal("PPC_PsCmpu0", cmpu0Call.Target);
Assert.Equal(3, cmpu0Call.Arguments[0].Constant);
var cmpo1 = PpcInstruction.Synthetic(
0x80000014,
0,
"ps_cmpo1",
new PpcOperand[] { new PpcConditionRegisterOperand("cr2", 8), Fpr(11), Fpr(12) });
var cmpo1Ir = Assert.Single(new PpcLifter().Lift(new[] { cmpo1 })).Ir;
var cmpo1Call = Assert.IsType<IrCall>(Assert.Single(cmpo1Ir));
Assert.Equal("PPC_PsCmpo1", cmpo1Call.Target);
Assert.Equal(2, cmpo1Call.Arguments[0].Constant);
var cmpu1 = PpcInstruction.Synthetic(
0x80000018,
0,
"ps_cmpu1.",
new PpcOperand[] { new PpcConditionRegisterOperand("cr1", 4), Fpr(13), Fpr(14) });
var cmpu1Ir = Assert.Single(new PpcLifter().Lift(new[] { cmpu1 })).Ir;
var cmpu1Call = Assert.IsType<IrCall>(Assert.Single(cmpu1Ir));
Assert.Equal("PPC_PsCmpu1", cmpu1Call.Target);
Assert.Equal(1, cmpu1Call.Arguments[0].Constant);
}
[Fact]
public void LiftsRawOpc4AliasesForCompareMoveAndIndexedMemory()
{
var compare = PpcInstruction.Synthetic(
0x80000000,
(1u << 16) | (2u << 11),
"opc_4_0",
System.Array.Empty<PpcOperand>());
var compareIr = Assert.Single(new PpcLifter().Lift(new[] { compare })).Ir;
var compareSetCr = Assert.IsType<IrSetCrField>(Assert.Single(compareIr));
Assert.Equal(0, compareSetCr.FieldIndex);
Assert.Equal("f1", compareSetCr.Left.RegisterName);
Assert.Equal("f2", compareSetCr.Right.RegisterName);
var move = PpcInstruction.Synthetic(
0x80000004,
(4u << 21) | (7u << 11),
"opc_4_4",
System.Array.Empty<PpcOperand>());
var moveIr = Assert.Single(new PpcLifter().Lift(new[] { move })).Ir;
var moveCall = Assert.IsType<IrCall>(Assert.Single(moveIr));
Assert.Equal("PPC_PsMr", moveCall.Target);
Assert.Equal("f4", moveCall.Destination);
Assert.Equal("f7", Assert.Single(moveCall.Arguments).RegisterName);
var neg = PpcInstruction.Synthetic(
0x80000008,
(5u << 21) | (8u << 11),
"opc_4_5",
System.Array.Empty<PpcOperand>());
var negIr = Assert.Single(new PpcLifter().Lift(new[] { neg })).Ir;
var negCall = Assert.IsType<IrCall>(Assert.Single(negIr));
Assert.Equal("PPC_PsNabs", negCall.Target);
Assert.Equal("f5", negCall.Destination);
Assert.Equal("f8", Assert.Single(negCall.Arguments).RegisterName);
var load = PpcInstruction.Synthetic(
0x8000000C,
(3u << 21) | (4u << 16) | (5u << 11) | (1u << 10) | (6u << 7),
"opc_4_12",
System.Array.Empty<PpcOperand>());
var loadIr = Assert.Single(new PpcLifter().Lift(new[] { load })).Ir;
Assert.Equal(2, loadIr.Count);
var loadCall = Assert.IsType<IrCall>(loadIr[1]);
Assert.Equal("PPC_PsqL", loadCall.Target);
Assert.Equal("f3", loadCall.Destination);
Assert.Equal(1, loadCall.Arguments[1].Constant);
Assert.Equal(6, loadCall.Arguments[2].Constant);
var store = PpcInstruction.Synthetic(
0x80000010,
(2u << 21) | (4u << 16) | (5u << 11) | (1u << 10) | (3u << 7),
"opc_4_14",
System.Array.Empty<PpcOperand>());
var storeIr = Assert.Single(new PpcLifter().Lift(new[] { store })).Ir;
Assert.Equal(2, storeIr.Count);
var storeCall = Assert.IsType<IrCall>(storeIr[1]);
Assert.Equal("PPC_PsqSt", storeCall.Target);
Assert.Equal("f2", storeCall.Arguments[1].RegisterName);
Assert.Equal(1, storeCall.Arguments[2].Constant);
Assert.Equal(3, storeCall.Arguments[3].Constant);
}
[Fact]
public void RejectsMalformedAndUnknownRawOpc4Variants()
{
var malformed = PpcInstruction.Synthetic(
0x80000000,
0,
"opc_4_bad",
System.Array.Empty<PpcOperand>());
Assert.Throws<NotImplementedException>(() => new PpcLifter().Lift(new[] { malformed }));
var unknown = PpcInstruction.Synthetic(
0x80000004,
0,
"opc_4_999",
System.Array.Empty<PpcOperand>());
Assert.Throws<NotImplementedException>(() => new PpcLifter().Lift(new[] { unknown }));
}
[Theory]
[InlineData("opc_4_20", "call", "PPC_PsSum0")]
[InlineData("opc_4_22", "call", "PPC_PsSum1")]
[InlineData("opc_4_24", "call", "PPC_PsMuls0")]
[InlineData("opc_4_26", "call", "PPC_PsMuls1")]
[InlineData("opc_4_30", "call", "PPC_PsMadds1")]
[InlineData("opc_4_36", "call", "PPC_PsDiv")]
[InlineData("opc_4_40", "call", "PPC_PsSub")]
[InlineData("opc_4_42", "call", "PPC_PsAdd")]
[InlineData("opc_4_44", "call", "PPC_PsAbs")]
[InlineData("opc_4_46", "call", "PPC_PsSel")]
[InlineData("opc_4_48", "call", "PPC_PsRes")]
[InlineData("opc_4_52", "call", "PPC_PsRsqrte")]
[InlineData("opc_4_56", "call", "PPC_PsMsub")]
[InlineData("opc_4_58", "call", "PPC_PsMadd")]
[InlineData("opc_4_60", "call", "PPC_PsNmsub")]
[InlineData("opc_4_62", "call", "PPC_PsNmadd")]
public void LiftsAdditionalRawOpc4ArithmeticVariants(string mnemonic, string kind, string opOrTarget)
{
var raw = (3u << 21) | (1u << 16) | (2u << 11) | (4u << 6);
var instruction = PpcInstruction.Synthetic(0x80000000, raw, mnemonic, System.Array.Empty<PpcOperand>());
var ir = Assert.Single(new PpcLifter().Lift(new[] { instruction })).Ir;
if (kind == "call")
{
var call = Assert.IsType<IrCall>(Assert.Single(ir));
Assert.Equal(opOrTarget, call.Target);
Assert.Equal("f3", call.Destination);
}
else
{
var binary = Assert.IsType<IrBinary>(Assert.Single(ir));
Assert.Equal(opOrTarget, binary.Op);
Assert.Equal("f3", binary.Destination);
}
}
}
@@ -0,0 +1,278 @@
using Translator.Core.Disassembly;
using Xunit;
namespace Translator.Tests;
public class PpcDecoderAdditionalTests
{
private static PpcInstruction Decode(uint word, uint address = 0x80000000u) => PpcDecoder.Decode(address, word);
private static uint EncodeDForm(uint primary, uint rsOrRd, uint ra, int imm)
=> (primary << 26) | (rsOrRd << 21) | (ra << 16) | (uint)(ushort)imm;
private static uint EncodeBc(uint bo, uint bi, int branchDelta, bool aa = false, bool lk = false)
=> (16u << 26) | (bo << 21) | (bi << 16) | ((((uint)branchDelta >> 2) & 0x3FFFu) << 2) | ((aa ? 1u : 0u) << 1) | (lk ? 1u : 0u);
private static uint EncodeB(int branchDelta, bool aa = false, bool lk = false)
=> (18u << 26) | ((((uint)branchDelta >> 2) & 0x00FFFFFFu) << 2) | ((aa ? 1u : 0u) << 1) | (lk ? 1u : 0u);
private static uint EncodeRotate(uint primary, uint rs, uint ra, uint shOrRb, uint mb, uint me, bool rc = false)
=> (primary << 26) | (rs << 21) | (ra << 16) | (shOrRb << 11) | (mb << 6) | (me << 1) | (rc ? 1u : 0u);
private static uint EncodePs(uint frt, uint fra, uint frb, uint frc, uint xo, bool rc = false)
=> (4u << 26) | (frt << 21) | (fra << 16) | (frb << 11) | (frc << 6) | (xo << 1) | (rc ? 1u : 0u);
private static uint EncodePsCmp(uint xo, uint crfD, uint fra, uint frb)
=> (4u << 26) | (crfD << 23) | (fra << 16) | (frb << 11) | (xo << 1);
private static uint EncodePrimary63(uint frt, uint fra, uint frb, uint frc, uint xo, bool rc = false)
=> (63u << 26) | (frt << 21) | (fra << 16) | (frb << 11) | (frc << 6) | (xo << 1) | (rc ? 1u : 0u);
private static uint EncodePrimary59(uint frt, uint fra, uint frb, uint frc, uint xo, bool rc = false)
=> (59u << 26) | (frt << 21) | (fra << 16) | (frb << 11) | (frc << 6) | (xo << 1) | (rc ? 1u : 0u);
[Fact]
public void DecodesImmediateBranchAndRotatePrimaries()
{
var twi = Decode(EncodeDForm(3, 31, 4, -2));
Assert.Equal("twi", twi.Mnemonic);
Assert.Collection(
twi.Operands,
op => Assert.Equal(31, Assert.IsType<PpcImmediateOperand>(op).Value),
op => Assert.Equal("r4", Assert.IsType<PpcRegisterOperand>(op).Name),
op => Assert.Equal(-2, Assert.IsType<PpcImmediateOperand>(op).Value));
var mulli = Decode(EncodeDForm(7, 3, 4, -2));
Assert.Equal("mulli", mulli.Mnemonic);
Assert.Collection(
mulli.Operands,
op => Assert.Equal("r3", Assert.IsType<PpcRegisterOperand>(op).Name),
op => Assert.Equal("r4", Assert.IsType<PpcRegisterOperand>(op).Name),
op => Assert.Equal(-2, Assert.IsType<PpcImmediateOperand>(op).Value));
var subfic = Decode(EncodeDForm(8, 5, 6, 0x1234));
Assert.Equal("subfic", subfic.Mnemonic);
var cmplwiCr0 = Decode(EncodeDForm(10, 0, 7, 0x55AA));
Assert.Equal("cmplwi", cmplwiCr0.Mnemonic);
Assert.Collection(
cmplwiCr0.Operands,
op => Assert.Equal("r7", Assert.IsType<PpcRegisterOperand>(op).Name),
op => Assert.Equal(0x55AA, Assert.IsType<PpcImmediateOperand>(op).Value));
var addic = Decode(EncodeDForm(12, 8, 9, -4));
Assert.Equal("addic", addic.Mnemonic);
Assert.False(addic.IsConditionalBranch);
var addicDot = Decode(EncodeDForm(13, 8, 9, -4));
Assert.Equal("addic.", addicDot.Mnemonic);
Assert.True(addicDot.IsConditionalBranch);
Assert.Equal("li", Decode(EncodeDForm(14, 10, 0, -1)).Mnemonic);
Assert.Equal("addi", Decode(EncodeDForm(14, 10, 11, 4)).Mnemonic);
Assert.Equal("lis", Decode(EncodeDForm(15, 12, 0, 0x1234)).Mnemonic);
Assert.Equal("addis", Decode(EncodeDForm(15, 12, 13, 0x1234)).Mnemonic);
var genericBc = Decode(EncodeBc(0, 5, 0x20, aa: true, lk: true), address: 0x81234560);
Assert.Equal("bcl", genericBc.Mnemonic);
Assert.True(genericBc.IsCall);
Assert.True(genericBc.IsConditionalBranch);
Assert.Collection(
genericBc.Operands,
op => Assert.Equal(0, Assert.IsType<PpcImmediateOperand>(op).Value),
op => Assert.Equal(5, Assert.IsType<PpcImmediateOperand>(op).Value),
op => Assert.Equal("cr1", Assert.IsType<PpcConditionRegisterOperand>(op).Name),
op => Assert.Equal(0x20u, Assert.IsType<PpcBranchTargetOperand>(op).TargetAddress));
Assert.Equal("bdnz", Decode(EncodeBc(16, 0, 0x10)).Mnemonic);
Assert.Equal("bdzl", Decode(EncodeBc(18, 0, 0x10, lk: true)).Mnemonic);
var absoluteBl = Decode(EncodeB(0x40, aa: true, lk: true), address: 0x80001000);
Assert.Equal("bl", absoluteBl.Mnemonic);
Assert.True(absoluteBl.IsCall);
Assert.Equal(0x40u, Assert.IsType<PpcBranchTargetOperand>(Assert.Single(absoluteBl.Operands)).TargetAddress);
var sc = Decode(17u << 26);
Assert.Equal("sc", sc.Mnemonic);
Assert.Empty(sc.Operands);
Assert.Equal("rlwimi.", Decode(EncodeRotate(20, 3, 4, 5, 6, 7, rc: true)).Mnemonic);
Assert.Equal("rlwinm", Decode(EncodeRotate(21, 8, 9, 10, 11, 12)).Mnemonic);
Assert.Equal("rlwnm.", Decode(EncodeRotate(23, 13, 14, 15, 16, 17, rc: true)).Mnemonic);
}
[Fact]
public void DecodesPrimaryFourPairedSingleVariants()
{
Assert.Equal("ps_div.", Decode(EncodePs(1, 2, 3, 0, 18, rc: true)).Mnemonic);
Assert.Equal("ps_sub", Decode(EncodePs(5, 6, 7, 0, 20)).Mnemonic);
Assert.Equal("ps_add.", Decode(EncodePs(9, 10, 11, 0, 21, rc: true)).Mnemonic);
Assert.Equal("ps_res", Decode(EncodePs(13, 14, 15, 0, 24)).Mnemonic);
Assert.Equal("ps_rsqrte.", Decode(EncodePs(17, 18, 19, 0, 26, rc: true)).Mnemonic);
Assert.Equal("ps_merge00", Decode(EncodePs(1, 2, 3, 0, 528)).Mnemonic);
Assert.Equal("ps_merge01.", Decode(EncodePs(4, 5, 6, 0, 560, rc: true)).Mnemonic);
Assert.Equal("ps_merge10", Decode(EncodePs(7, 8, 9, 0, 592)).Mnemonic);
Assert.Equal("ps_merge11.", Decode(EncodePs(10, 11, 12, 0, 624, rc: true)).Mnemonic);
Assert.Equal("ps_mr", Decode(EncodePs(13, 0, 14, 0, 72)).Mnemonic);
Assert.Equal("ps_neg.", Decode(EncodePs(15, 0, 16, 0, 40, rc: true)).Mnemonic);
Assert.Equal("ps_abs", Decode(EncodePs(17, 0, 18, 0, 264)).Mnemonic);
Assert.Equal("ps_nabs.", Decode(EncodePs(19, 0, 20, 0, 136, rc: true)).Mnemonic);
var cmpu0 = Decode(EncodePsCmp(0, 3, 4, 5));
Assert.Equal("ps_cmpu0", cmpu0.Mnemonic);
Assert.True(cmpu0.IsConditionalBranch);
Assert.Equal("cr3", Assert.IsType<PpcConditionRegisterOperand>(cmpu0.Operands[0]).Name);
Assert.Equal("ps_cmpo0", Decode(EncodePsCmp(32, 2, 6, 7)).Mnemonic);
Assert.Equal("ps_cmpu1", Decode(EncodePsCmp(64, 1, 8, 9)).Mnemonic);
Assert.Equal("ps_cmpo1", Decode(EncodePsCmp(96, 7, 10, 11)).Mnemonic);
var dcbzL = Decode(EncodePs(0, 12, 13, 0, 1014));
Assert.Equal("dcbz_l", dcbzL.Mnemonic);
Assert.Collection(
dcbzL.Operands,
op => Assert.Equal("r12", Assert.IsType<PpcRegisterOperand>(op).Name),
op => Assert.Equal("r13", Assert.IsType<PpcRegisterOperand>(op).Name));
var psqLx = Decode(EncodePs(1, 2, 3, 0, 6) | (1u << 10) | (4u << 7));
Assert.Equal("psq_lx", psqLx.Mnemonic);
Assert.Collection(
psqLx.Operands,
op => Assert.Equal("f1", Assert.IsType<PpcRegisterOperand>(op).Name),
op => Assert.Equal("r2", Assert.IsType<PpcRegisterOperand>(op).Name),
op => Assert.Equal("r3", Assert.IsType<PpcRegisterOperand>(op).Name),
op => Assert.Equal(1, Assert.IsType<PpcImmediateOperand>(op).Value),
op => Assert.Equal(4, Assert.IsType<PpcImmediateOperand>(op).Value));
Assert.Equal("psq_stx", Decode(EncodePs(4, 5, 6, 0, 7)).Mnemonic);
Assert.Equal("psq_lux", Decode(EncodePs(7, 8, 9, 0, 38)).Mnemonic);
Assert.Equal("psq_stux", Decode(EncodePs(10, 11, 12, 0, 39)).Mnemonic);
Assert.Equal("opc_4_17", Decode(EncodePs(1, 2, 3, 0, 17)).Mnemonic);
}
[Fact]
public void DecodesLogicalDFormAndPsqMemoryPrimaries()
{
var nop = Decode(24u << 26);
Assert.Equal("nop", nop.Mnemonic);
Assert.Collection(
nop.Operands,
op => Assert.Equal("r0", Assert.IsType<PpcRegisterOperand>(op).Name),
op => Assert.Equal("r0", Assert.IsType<PpcRegisterOperand>(op).Name),
op => Assert.Equal(0, Assert.IsType<PpcImmediateOperand>(op).Value));
Assert.Equal("oris", Decode(EncodeDForm(25, 3, 4, 0x22)).Mnemonic);
Assert.Equal("xori", Decode(EncodeDForm(26, 5, 6, 0x33)).Mnemonic);
Assert.Equal("xoris", Decode(EncodeDForm(27, 7, 8, 0x44)).Mnemonic);
var andi = Decode(EncodeDForm(28, 9, 10, 0x55));
Assert.Equal("andi.", andi.Mnemonic);
Assert.True(andi.IsConditionalBranch);
var andis = Decode(EncodeDForm(29, 11, 12, 0x1234));
Assert.Equal("andis.", andis.Mnemonic);
Assert.True(andis.IsConditionalBranch);
Assert.Collection(
andis.Operands,
op => Assert.Equal("r12", Assert.IsType<PpcRegisterOperand>(op).Name),
op => Assert.Equal("r11", Assert.IsType<PpcRegisterOperand>(op).Name),
op => Assert.Equal(0x1234, Assert.IsType<PpcImmediateOperand>(op).Value));
Assert.Equal("lhz", Decode(EncodeDForm(40, 11, 12, -8)).Mnemonic);
Assert.Equal("lhzu", Decode(EncodeDForm(41, 13, 14, 4)).Mnemonic);
Assert.Equal("lha", Decode(EncodeDForm(42, 15, 16, 6)).Mnemonic);
Assert.Equal("lhau", Decode(EncodeDForm(43, 16, 17, -6)).Mnemonic);
Assert.Equal("sth", Decode(EncodeDForm(44, 17, 18, 8)).Mnemonic);
Assert.Equal("sthu", Decode(EncodeDForm(45, 18, 19, -10)).Mnemonic);
Assert.Equal("lmw", Decode(EncodeDForm(46, 19, 20, 12)).Mnemonic);
Assert.Equal("stmw", Decode(EncodeDForm(47, 21, 22, 16)).Mnemonic);
Assert.Equal("lfs", Decode(EncodeDForm(48, 1, 23, 20)).Mnemonic);
Assert.Equal("lfsu", Decode(EncodeDForm(49, 2, 24, 24)).Mnemonic);
Assert.Equal("lfd", Decode(EncodeDForm(50, 3, 25, 28)).Mnemonic);
Assert.Equal("lfdu", Decode(EncodeDForm(51, 4, 26, 32)).Mnemonic);
Assert.Equal("stfs", Decode(EncodeDForm(52, 5, 27, 36)).Mnemonic);
Assert.Equal("stfsu", Decode(EncodeDForm(53, 6, 28, 40)).Mnemonic);
Assert.Equal("stfd", Decode(EncodeDForm(54, 7, 29, 44)).Mnemonic);
Assert.Equal("stfdu", Decode(EncodeDForm(55, 8, 30, 48)).Mnemonic);
var psqLu = Decode(EncodeDForm(57, 9, 31, 0x5AB) | (1u << 15) | (3u << 12));
Assert.Equal("psq_lu", psqLu.Mnemonic);
Assert.Collection(
psqLu.Operands,
op => Assert.Equal("f9", Assert.IsType<PpcRegisterOperand>(op).Name),
op =>
{
var disp = Assert.IsType<PpcDisplacementOperand>(op);
Assert.Equal(0x5AB, disp.Offset);
Assert.Equal("r31", disp.BaseRegister);
},
op => Assert.Equal(1, Assert.IsType<PpcImmediateOperand>(op).Value),
op => Assert.Equal(3, Assert.IsType<PpcImmediateOperand>(op).Value));
var psqStu = Decode(EncodeDForm(61, 10, 30, -1) | (1u << 15) | (7u << 12));
Assert.Equal("psq_stu", psqStu.Mnemonic);
Assert.Equal(-1, Assert.IsType<PpcDisplacementOperand>(psqStu.Operands[1]).Offset);
}
[Fact]
public void DecodesPrimary63FloatingFormsAndFallback()
{
var fcmpu = Decode(EncodePrimary63(0, 2, 3, 0, 0));
Assert.Equal("fcmpu", fcmpu.Mnemonic);
Assert.True(fcmpu.IsConditionalBranch);
Assert.Equal("fcmpo", Decode(EncodePrimary63(0, 4, 5, 0, 32)).Mnemonic);
Assert.Equal("fctiw.", Decode(EncodePrimary63(6, 0, 7, 0, 14, rc: true)).Mnemonic);
Assert.Equal("fctiwz.", Decode(EncodePrimary63(6, 0, 7, 0, 15, rc: true)).Mnemonic);
Assert.Equal("fdiv", Decode(EncodePrimary63(8, 9, 10, 0, 18)).Mnemonic);
Assert.Equal("fsub.", Decode(EncodePrimary63(11, 12, 13, 0, 20, rc: true)).Mnemonic);
Assert.Equal("fadd", Decode(EncodePrimary63(14, 15, 16, 0, 21)).Mnemonic);
Assert.Equal("frsp", Decode(EncodePrimary63(14, 0, 16, 0, 12)).Mnemonic);
Assert.Equal("fsel.", Decode(EncodePrimary63(14, 15, 16, 17, 23, rc: true)).Mnemonic);
Assert.Equal("fres", Decode(EncodePrimary63(14, 0, 16, 0, 24)).Mnemonic);
Assert.Equal("fmul.", Decode(EncodePrimary63(17, 18, 19, 0, 25, rc: true)).Mnemonic);
Assert.Equal("frsqrte", Decode(EncodePrimary63(17, 0, 19, 0, 26)).Mnemonic);
Assert.Equal("fmsub", Decode(EncodePrimary63(17, 18, 19, 20, 28)).Mnemonic);
Assert.Equal("fmadd", Decode(EncodePrimary63(17, 18, 19, 20, 29)).Mnemonic);
Assert.Equal("fnmsub", Decode(EncodePrimary63(17, 18, 19, 20, 30)).Mnemonic);
Assert.Equal("fnmadd", Decode(EncodePrimary63(17, 18, 19, 20, 31)).Mnemonic);
Assert.Equal("mtfsb1", Decode(EncodePrimary63(17, 0, 0, 0, 38)).Mnemonic);
Assert.Equal("mtfsb0", Decode(EncodePrimary63(18, 0, 0, 0, 70)).Mnemonic);
var mtfsfi = Decode((63u << 26) | (3u << 23) | (0xCu << 12) | (134u << 1));
Assert.Equal("mtfsfi", mtfsfi.Mnemonic);
Assert.Collection(
mtfsfi.Operands,
op => Assert.Equal(3, Assert.IsType<PpcImmediateOperand>(op).Value),
op => Assert.Equal(0xC, Assert.IsType<PpcImmediateOperand>(op).Value));
Assert.Equal("fneg", Decode(EncodePrimary63(21, 0, 22, 0, 40)).Mnemonic);
Assert.Equal("fmr.", Decode(EncodePrimary63(23, 0, 24, 0, 72, rc: true)).Mnemonic);
Assert.Equal("fnabs", Decode(EncodePrimary63(25, 0, 26, 0, 136)).Mnemonic);
Assert.Equal("fabs", Decode(EncodePrimary63(25, 0, 26, 0, 264)).Mnemonic);
Assert.Equal("mffs", Decode(EncodePrimary63(27, 0, 0, 0, 583)).Mnemonic);
var mtfsf = Decode(EncodePrimary63(0, 0, 7, 0, 711) | (0xAAu << 17));
Assert.Equal("mtfsf", mtfsf.Mnemonic);
Assert.Collection(
mtfsf.Operands,
op => Assert.Equal(0xAA, Assert.IsType<PpcImmediateOperand>(op).Value),
op => Assert.Equal("f7", Assert.IsType<PpcRegisterOperand>(op).Name));
Assert.Equal("fp_27", Decode(EncodePrimary63(1, 2, 3, 0, 27)).Mnemonic);
}
[Fact]
public void DecodesPrimaryFiftyNineSinglePrecisionForms()
{
Assert.Equal("fdivs", Decode(EncodePrimary59(1, 2, 3, 4, 18)).Mnemonic);
Assert.Equal("fsubs.", Decode(EncodePrimary59(1, 2, 3, 4, 20, rc: true)).Mnemonic);
Assert.Equal("fadds", Decode(EncodePrimary59(1, 2, 3, 4, 21)).Mnemonic);
Assert.Equal("fres", Decode(EncodePrimary59(1, 2, 3, 4, 24)).Mnemonic);
Assert.Equal("fmuls.", Decode(EncodePrimary59(1, 2, 3, 4, 25, rc: true)).Mnemonic);
Assert.Equal("fmsubs", Decode(EncodePrimary59(1, 2, 3, 4, 28)).Mnemonic);
Assert.Equal("fmadds", Decode(EncodePrimary59(1, 2, 3, 4, 29)).Mnemonic);
Assert.Equal("fnmsubs", Decode(EncodePrimary59(1, 2, 3, 4, 30)).Mnemonic);
Assert.Equal("fnmadds", Decode(EncodePrimary59(1, 2, 3, 4, 31)).Mnemonic);
Assert.Equal("opc_59", Decode(EncodePrimary59(1, 2, 3, 4, 27)).Mnemonic);
}
}
@@ -0,0 +1,136 @@
using System;
using Translator.Core.Disassembly;
using Xunit;
namespace Translator.Tests;
public class PpcDecoderPrimary19Tests
{
private static uint EncodePrimary19(uint field0, uint field1, uint field2, uint xo, bool lk = false)
=> (19u << 26) | (field0 << 21) | (field1 << 16) | (field2 << 11) | (xo << 1) | (lk ? 1u : 0u);
private static PpcInstruction Decode(uint field0, uint field1, uint field2, uint xo, bool lk = false)
=> PpcDecoder.Decode(0x80000000, EncodePrimary19(field0, field1, field2, xo, lk));
[Fact]
public void DecodesBclrVariantsWithCorrectFlags()
{
var blr = Decode(20, 0, 0, 16);
Assert.Equal("blr", blr.Mnemonic);
Assert.True(blr.IsReturn);
Assert.False(blr.IsCall);
Assert.False(blr.IsConditionalBranch);
Assert.Empty(blr.Operands);
var blrl = Decode(20, 0, 0, 16, lk: true);
Assert.Equal("blrl", blrl.Mnemonic);
Assert.False(blrl.IsReturn);
Assert.True(blrl.IsCall);
Assert.False(blrl.IsConditionalBranch);
Assert.Empty(blrl.Operands);
var beqlr = Decode(12, 6, 0, 16);
Assert.Equal("beqlr", beqlr.Mnemonic);
Assert.True(beqlr.IsReturn);
Assert.True(beqlr.IsConditionalBranch);
Assert.Equal("cr1", Assert.IsType<PpcConditionRegisterOperand>(Assert.Single(beqlr.Operands)).Name);
var bnslrl = Decode(4, 7, 0, 16, lk: true);
Assert.Equal("bnslrl", bnslrl.Mnemonic);
Assert.False(bnslrl.IsReturn);
Assert.True(bnslrl.IsCall);
Assert.True(bnslrl.IsConditionalBranch);
Assert.Equal("cr1", Assert.IsType<PpcConditionRegisterOperand>(Assert.Single(bnslrl.Operands)).Name);
}
[Fact]
public void DecodesBcctrVariantsAndCrFieldOperands()
{
var bctr = Decode(20, 0, 0, 528);
Assert.Equal("bctr", bctr.Mnemonic);
Assert.True(bctr.IsReturn);
Assert.False(bctr.IsCall);
Assert.False(bctr.IsConditionalBranch);
Assert.Empty(bctr.Operands);
var bctrl = Decode(20, 0, 0, 528, lk: true);
Assert.Equal("bctrl", bctrl.Mnemonic);
Assert.False(bctrl.IsReturn);
Assert.True(bctrl.IsCall);
Assert.False(bctrl.IsConditionalBranch);
Assert.Empty(bctrl.Operands);
var bcctr = Decode(4, 9, 0, 528);
Assert.Equal("bcctr", bcctr.Mnemonic);
Assert.False(bcctr.IsReturn);
Assert.False(bcctr.IsCall);
Assert.True(bcctr.IsConditionalBranch);
Assert.Equal("cr2", Assert.IsType<PpcConditionRegisterOperand>(Assert.Single(bcctr.Operands)).Name);
}
[Fact]
public void DecodesSystemAndCrMoveForms()
{
var rfi = Decode(0, 0, 0, 50);
Assert.Equal("rfi", rfi.Mnemonic);
Assert.True(rfi.IsReturn);
var isync = Decode(0, 0, 0, 150);
Assert.Equal("isync", isync.Mnemonic);
Assert.False(isync.IsReturn);
var mcrf = Decode(12, 4, 0, 0);
Assert.Equal("mcrf", mcrf.Mnemonic);
Assert.Collection(
mcrf.Operands,
op => Assert.Equal("cr3", Assert.IsType<PpcConditionRegisterOperand>(op).Name),
op => Assert.Equal("cr1", Assert.IsType<PpcConditionRegisterOperand>(op).Name));
}
[Theory]
[InlineData(33u, "crnor")]
[InlineData(129u, "crandc")]
[InlineData(193u, "crxor")]
[InlineData(225u, "crnand")]
[InlineData(257u, "crand")]
[InlineData(289u, "creqv")]
[InlineData(417u, "crorc")]
[InlineData(449u, "cror")]
public void DecodesCrLogicalOps(uint xo, string expectedMnemonic)
{
var instruction = Decode(1, 2, 3, xo);
Assert.Equal(expectedMnemonic, instruction.Mnemonic);
Assert.Collection(
instruction.Operands,
op =>
{
var cr = Assert.IsType<PpcConditionRegisterOperand>(op);
Assert.Equal("crb1", cr.Name);
Assert.Equal(1, cr.BitIndex);
},
op =>
{
var cr = Assert.IsType<PpcConditionRegisterOperand>(op);
Assert.Equal("crb2", cr.Name);
Assert.Equal(2, cr.BitIndex);
},
op =>
{
var cr = Assert.IsType<PpcConditionRegisterOperand>(op);
Assert.Equal("crb3", cr.Name);
Assert.Equal(3, cr.BitIndex);
});
}
[Fact]
public void UnknownPrimary19XoFallsBackToUnk19()
{
var instruction = Decode(0, 0, 0, 511);
Assert.Equal("unk19", instruction.Mnemonic);
Assert.Empty(instruction.Operands);
Assert.False(instruction.IsReturn);
Assert.False(instruction.IsCall);
}
}
@@ -0,0 +1,290 @@
using System;
using Translator.Core.Disassembly;
using Xunit;
namespace Translator.Tests;
public class PpcDecoderPrimary31Tests
{
private static uint EncodeXoForm(uint rs, uint ra, uint rb, uint xo, bool rc = false, bool oe = false)
=> (31u << 26) | (rs << 21) | (ra << 16) | (rb << 11) | ((oe ? 1u : 0u) << 10) | (xo << 1) | (rc ? 1u : 0u);
private static uint EncodeXForm(uint rs, uint ra, uint rb, uint xo10, bool rc = false)
=> (31u << 26) | (rs << 21) | (ra << 16) | (rb << 11) | (xo10 << 1) | (rc ? 1u : 0u);
private static uint EncodeSprWord(uint rs, uint spr, uint xo)
{
var sprLo = spr & 0x1F;
var sprHi = (spr >> 5) & 0x1F;
return (31u << 26) | (rs << 21) | (sprLo << 16) | (sprHi << 11) | (xo << 1);
}
private static PpcInstruction Decode(uint word) => PpcDecoder.Decode(0x80000000, word);
private static void AssertGpr(PpcOperand operand, string expected)
=> Assert.Equal(expected, Assert.IsType<PpcRegisterOperand>(operand).Name);
private static void AssertFpr(PpcOperand operand, string expected)
=> Assert.Equal(expected, Assert.IsType<PpcRegisterOperand>(operand).Name);
[Fact]
public void DecodesXoFormArithmeticAndCompareMnemonics()
{
var cmpw = Decode((31u << 26) | (2u << 23) | (3u << 16) | (5u << 11));
Assert.Equal("cmpw", cmpw.Mnemonic);
Assert.True(cmpw.IsConditionalBranch);
Assert.Collection(
cmpw.Operands,
op => Assert.Equal("cr2", Assert.IsType<PpcConditionRegisterOperand>(op).Name),
op => AssertGpr(op, "r3"),
op => AssertGpr(op, "r5"));
var invalidCmp = Decode((31u << 26) | (1u << 21));
Assert.Equal("invalid_cmp", invalidCmp.Mnemonic);
var tw = Decode(EncodeXoForm(31, 4, 5, 4));
Assert.Equal("tw", tw.Mnemonic);
Assert.Collection(
tw.Operands,
op => Assert.Equal(31, Assert.IsType<PpcImmediateOperand>(op).Value),
op => AssertGpr(op, "r4"),
op => AssertGpr(op, "r5"));
var addc = Decode(EncodeXoForm(3, 4, 5, 10, rc: true, oe: true));
Assert.Equal("addco.", addc.Mnemonic);
Assert.Collection(addc.Operands, op => AssertGpr(op, "r3"), op => AssertGpr(op, "r4"), op => AssertGpr(op, "r5"));
var subfe = Decode(EncodeXoForm(6, 7, 8, 136, oe: true));
Assert.Equal("subfeo", subfe.Mnemonic);
var addze = Decode(EncodeXoForm(9, 10, 0, 202, rc: true));
Assert.Equal("addze.", addze.Mnemonic);
Assert.Collection(addze.Operands, op => AssertGpr(op, "r9"), op => AssertGpr(op, "r10"));
var addme = Decode(EncodeXoForm(9, 10, 0, 234, rc: true));
Assert.Equal("addme.", addme.Mnemonic);
Assert.Collection(addme.Operands, op => AssertGpr(op, "r9"), op => AssertGpr(op, "r10"));
var subfme = Decode(EncodeXoForm(9, 10, 0, 232, rc: true));
Assert.Equal("subfme.", subfme.Mnemonic);
Assert.Collection(subfme.Operands, op => AssertGpr(op, "r9"), op => AssertGpr(op, "r10"));
var neg = Decode(EncodeXoForm(11, 12, 0, 104, rc: true));
Assert.Equal("neg.", neg.Mnemonic);
var mullw = Decode(EncodeXoForm(13, 14, 15, 235, oe: true));
Assert.Equal("mullwo", mullw.Mnemonic);
var add = Decode(EncodeXoForm(16, 17, 18, 266, rc: true));
Assert.Equal("add.", add.Mnemonic);
var divwu = Decode(EncodeXoForm(19, 20, 21, 459, oe: true));
Assert.Equal("divwuo", divwu.Mnemonic);
var divw = Decode(EncodeXoForm(22, 23, 24, 491, rc: true));
Assert.Equal("divw.", divw.Mnemonic);
}
[Fact]
public void DecodesSprAndLogicalRegisterForms()
{
var mfcr = Decode(EncodeXoForm(4, 0, 0, 19));
Assert.Equal("mfcr", mfcr.Mnemonic);
AssertGpr(Assert.Single(mfcr.Operands), "r4");
var and = Decode(EncodeXoForm(5, 6, 7, 28, rc: true));
Assert.Equal("and.", and.Mnemonic);
Assert.Collection(and.Operands, op => AssertGpr(op, "r6"), op => AssertGpr(op, "r5"), op => AssertGpr(op, "r7"));
var andc = Decode(EncodeXoForm(5, 6, 7, 60, rc: true));
Assert.Equal("andc.", andc.Mnemonic);
var mfmsr = Decode(EncodeXoForm(8, 0, 0, 83));
Assert.Equal("mfmsr", mfmsr.Mnemonic);
AssertGpr(Assert.Single(mfmsr.Operands), "r8");
var mtmsr = Decode(EncodeXoForm(9, 0, 0, 146));
Assert.Equal("mtmsr", mtmsr.Mnemonic);
AssertGpr(Assert.Single(mtmsr.Operands), "r9");
var mtcrf = Decode((31u << 26) | (10u << 21) | (0x80u << 12) | (144u << 1));
Assert.Equal("mtcrf", mtcrf.Mnemonic);
Assert.Collection(
mtcrf.Operands,
op => Assert.Equal(0x80, Assert.IsType<PpcImmediateOperand>(op).Value),
op => AssertGpr(op, "r10"));
var nor = Decode(EncodeXoForm(10, 11, 12, 124, rc: true));
Assert.Equal("nor.", nor.Mnemonic);
var nand = Decode(EncodeXoForm(13, 14, 15, 476));
Assert.Equal("nand", nand.Mnemonic);
var eqv = Decode(EncodeXoForm(13, 14, 15, 284, rc: true));
Assert.Equal("eqv.", eqv.Mnemonic);
var xor = Decode(EncodeXoForm(16, 17, 18, 316, rc: true));
Assert.Equal("xor.", xor.Mnemonic);
var mflr = Decode(EncodeSprWord(3, 8, 339));
Assert.Equal("mflr", mflr.Mnemonic);
AssertGpr(Assert.Single(mflr.Operands), "r3");
var mfctr = Decode(EncodeSprWord(4, 9, 339));
Assert.Equal("mfctr", mfctr.Mnemonic);
var mfxer = Decode(EncodeSprWord(5, 1, 339));
Assert.Equal("mfxer", mfxer.Mnemonic);
var mfspr = Decode(EncodeSprWord(6, 912, 339));
Assert.Equal("mfspr", mfspr.Mnemonic);
Assert.Collection(
mfspr.Operands,
op => AssertGpr(op, "r6"),
op => Assert.Equal(912, Assert.IsType<PpcImmediateOperand>(op).Value));
var mtlr = Decode(EncodeSprWord(7, 8, 467));
Assert.Equal("mtlr", mtlr.Mnemonic);
AssertGpr(Assert.Single(mtlr.Operands), "r7");
var mtctr = Decode(EncodeSprWord(8, 9, 467));
Assert.Equal("mtctr", mtctr.Mnemonic);
var mtxer = Decode(EncodeSprWord(9, 1, 467));
Assert.Equal("mtxer", mtxer.Mnemonic);
var mtspr = Decode(EncodeSprWord(10, 913, 467));
Assert.Equal("mtspr", mtspr.Mnemonic);
Assert.Collection(
mtspr.Operands,
op => Assert.Equal(913, Assert.IsType<PpcImmediateOperand>(op).Value),
op => AssertGpr(op, "r10"));
var mtsr = Decode(EncodeXoForm(11, 3, 0, 210));
Assert.Equal("mtsr", mtsr.Mnemonic);
Assert.Collection(
mtsr.Operands,
op => Assert.Equal(3, Assert.IsType<PpcImmediateOperand>(op).Value),
op => AssertGpr(op, "r11"));
Assert.Equal("mtsrin", Decode(EncodeXForm(11, 0, 12, 242)).Mnemonic);
}
[Fact]
public void DecodesIndexedMemoryAndByteReverseForms()
{
Assert.Equal("lwzx", Decode(EncodeXForm(3, 4, 5, 23)).Mnemonic);
Assert.Equal("lwzux", Decode(EncodeXForm(6, 7, 8, 55)).Mnemonic);
Assert.Equal("lbzx", Decode(EncodeXForm(9, 10, 11, 87)).Mnemonic);
Assert.Equal("stwx", Decode(EncodeXForm(12, 13, 14, 151)).Mnemonic);
Assert.Equal("stwux", Decode(EncodeXForm(15, 16, 17, 183)).Mnemonic);
Assert.Equal("stbx", Decode(EncodeXForm(18, 19, 20, 215)).Mnemonic);
Assert.Equal("stbux", Decode(EncodeXForm(21, 22, 23, 247)).Mnemonic);
Assert.Equal("lhzx", Decode(EncodeXForm(24, 25, 26, 279)).Mnemonic);
Assert.Equal("lhzux", Decode(EncodeXForm(24, 25, 26, 311)).Mnemonic);
Assert.Equal("lhax", Decode(EncodeXForm(24, 25, 26, 343)).Mnemonic);
Assert.Equal("lhaux", Decode(EncodeXForm(24, 25, 26, 375)).Mnemonic);
Assert.Equal("sthx", Decode(EncodeXForm(27, 28, 29, 407)).Mnemonic);
Assert.Equal("sthux", Decode(EncodeXForm(27, 28, 29, 439)).Mnemonic);
var stwcx = Decode(EncodeXoForm(3, 4, 5, 498));
Assert.Equal("stwcx.", stwcx.Mnemonic);
Assert.True(stwcx.IsConditionalBranch);
Assert.Equal("lwbrx", Decode(EncodeXForm(3, 4, 5, 534)).Mnemonic);
Assert.Equal("stwbrx", Decode(EncodeXForm(6, 7, 8, 662)).Mnemonic);
Assert.Equal("lhbrx", Decode(EncodeXForm(9, 10, 11, 790)).Mnemonic);
Assert.Equal("sthbrx", Decode(EncodeXForm(12, 13, 14, 918)).Mnemonic);
var stfiwx = Decode(EncodeXForm(2, 3, 4, 983));
Assert.Equal("stfiwx", stfiwx.Mnemonic);
Assert.Collection(stfiwx.Operands, op => AssertFpr(op, "f2"), op => AssertGpr(op, "r3"), op => AssertGpr(op, "r4"));
}
[Fact]
public void DecodesShiftFloatingAndCacheForms()
{
Assert.Equal("slw.", Decode(EncodeXForm(5, 6, 7, 24, rc: true)).Mnemonic);
Assert.Equal("dcbst", Decode(EncodeXForm(0, 6, 7, 54)).Mnemonic);
Assert.Equal("dcbf", Decode(EncodeXForm(0, 6, 7, 86)).Mnemonic);
Assert.Equal("dcbtst", Decode(EncodeXForm(0, 6, 7, 246)).Mnemonic);
Assert.Equal("dcbt", Decode(EncodeXForm(0, 6, 7, 278)).Mnemonic);
Assert.Equal("tlbie", Decode(EncodeXForm(0, 0, 7, 306)).Mnemonic);
Assert.Equal("dcbi", Decode(EncodeXForm(0, 6, 7, 470)).Mnemonic);
Assert.Equal("mcrxr", Decode(EncodeXForm(28, 0, 0, 512)).Mnemonic);
Assert.Equal("srw.", Decode(EncodeXForm(8, 9, 10, 536, rc: true)).Mnemonic);
Assert.Equal("tlbsync", Decode(EncodeXForm(0, 0, 0, 566)).Mnemonic);
var mfsr = Decode(EncodeXForm(8, 9, 0, 595));
Assert.Equal("mfsr", mfsr.Mnemonic);
Assert.Collection(
mfsr.Operands,
op => AssertGpr(op, "r8"),
op => Assert.Equal(9, Assert.IsType<PpcImmediateOperand>(op).Value));
Assert.Equal("mfsrin", Decode(EncodeXForm(8, 0, 9, 659)).Mnemonic);
Assert.Equal("sync", Decode(EncodeXForm(0, 0, 0, 598)).Mnemonic);
Assert.Equal("sraw.", Decode(EncodeXForm(11, 12, 13, 792, rc: true)).Mnemonic);
var srawi = Decode(EncodeXForm(14, 15, 7, 824, rc: true));
Assert.Equal("srawi.", srawi.Mnemonic);
Assert.Collection(
srawi.Operands,
op => AssertGpr(op, "r15"),
op => AssertGpr(op, "r14"),
op => Assert.Equal(7, Assert.IsType<PpcImmediateOperand>(op).Value));
var lfsx = Decode(EncodeXForm(1, 2, 3, 535));
Assert.Equal("lfsx", lfsx.Mnemonic);
Assert.Collection(lfsx.Operands, op => AssertFpr(op, "f1"), op => AssertGpr(op, "r2"), op => AssertGpr(op, "r3"));
Assert.Equal("lfsux", Decode(EncodeXForm(1, 2, 3, 567)).Mnemonic);
var lfdx = Decode(EncodeXForm(4, 5, 6, 599));
Assert.Equal("lfdx", lfdx.Mnemonic);
Assert.Equal("lfdux", Decode(EncodeXForm(4, 5, 6, 631)).Mnemonic);
var stfsx = Decode(EncodeXForm(7, 8, 9, 663));
Assert.Equal("stfsx", stfsx.Mnemonic);
Assert.Equal("stfsux", Decode(EncodeXForm(7, 8, 9, 695)).Mnemonic);
var stfdx = Decode(EncodeXForm(10, 11, 12, 727));
Assert.Equal("stfdx", stfdx.Mnemonic);
Assert.Equal("dcba", Decode(EncodeXForm(0, 11, 12, 758)).Mnemonic);
Assert.Equal("stfdux", Decode(EncodeXForm(10, 11, 12, 759)).Mnemonic);
Assert.Equal("eieio", Decode(EncodeXForm(0, 0, 0, 854)).Mnemonic);
Assert.Equal("mftb", Decode(EncodeSprWord(13, 268, 371)).Mnemonic);
Assert.Equal("mftbu", Decode(EncodeSprWord(14, 269, 371)).Mnemonic);
var icbi = Decode(EncodeXForm(0, 13, 14, 982));
Assert.Equal("icbi", icbi.Mnemonic);
Assert.Collection(icbi.Operands, op => AssertGpr(op, "r13"), op => AssertGpr(op, "r14"));
var dcbz = Decode(EncodeXForm(0, 15, 16, 1014));
Assert.Equal("dcbz", dcbz.Mnemonic);
var extsh = Decode(EncodeXForm(17, 18, 0, 922, rc: true));
Assert.Equal("extsh.", extsh.Mnemonic);
Assert.Collection(extsh.Operands, op => AssertGpr(op, "r18"), op => AssertGpr(op, "r17"));
var extsb = Decode(EncodeXForm(19, 20, 0, 954, rc: true));
Assert.Equal("extsb.", extsb.Mnemonic);
Assert.Collection(extsb.Operands, op => AssertGpr(op, "r20"), op => AssertGpr(op, "r19"));
}
[Fact]
public void FallsBackToXoMnemonicForUnknownCodes()
{
var instruction = Decode(EncodeXForm(1, 2, 3, 321));
Assert.Equal("xo_321", instruction.Mnemonic);
Assert.Empty(instruction.Operands);
Assert.False(instruction.IsConditionalBranch);
}
}
@@ -0,0 +1,201 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Translator.Core.Disassembly;
using Translator.Core.Loading;
using Translator.Core.Parsing.Dol;
using Translator.Core.Parsing.Rel;
using Xunit;
namespace Translator.Tests;
public class PpcDisassemblerTests
{
private static ProgramImage LoadImage()
{
var root = ProjectPaths.FindRepositoryRoot();
var assets = Path.Combine(root, "assets");
var builder = new ProgramImageBuilder();
return builder.Build(Path.Combine(assets, "main.dol"), Path.Combine(assets, "StaticR.rel"));
}
[Fact]
public void DisassemblesKnownFunctionWithStableInstructionCount()
{
var image = LoadImage();
using var disassembler = new PpcDisassembler();
var instructions = disassembler.DisassembleFunction(image, 0x800060A4, maxInstructions: 128);
Assert.NotEmpty(instructions);
Assert.True(instructions.Last().IsReturn || instructions.Last().IsUnconditionalBranch || instructions.Last().IsConditionalBranch,
"Function should terminate with a control-transfer instruction");
Assert.Equal(0x800060A4u, instructions[0].Address);
// Regression guard: reachable-walk should pull in the whole control-flow region (was ~90 after CFG-aware disasm).
Assert.InRange(instructions.Count, 60, 120);
}
[Fact]
public void ThrowsWhenLargeFunctionExceedsLegacyInstructionBudget()
{
var image = LoadImage();
using var disassembler = new PpcDisassembler();
var ex = Assert.Throws<PpcDisassemblyLimitExceededException>(
() => disassembler.DisassembleFunction(image, 0x80821E14, maxInstructions: 4096, maxBytes: 0x8000));
Assert.Contains("0x80821E14", ex.Message);
Assert.Equal(0x80821E14u, ex.EntryPoint);
Assert.Equal(4096, ex.MaxInstructions);
Assert.Equal(0x8000, ex.MaxBytes);
Assert.True(ex.DecodedInstructions >= 4096);
Assert.True(ex.BlockedAddress >= ex.EntryPoint);
}
[Fact]
public void DecodesCmpwFromXo0()
{
var instruction = PpcDecoder.Decode(0x80000000, 0x7C032800);
Assert.Equal("cmpw", instruction.Mnemonic);
Assert.Collection(
instruction.Operands,
op => Assert.Equal("cr0", Assert.IsType<PpcConditionRegisterOperand>(op).Name),
op => Assert.Equal("r3", Assert.IsType<PpcRegisterOperand>(op).Name),
op => Assert.Equal("r5", Assert.IsType<PpcRegisterOperand>(op).Name));
}
[Fact]
public void JumpTableTargetsIgnoreKnownExternalFunctionEntries()
{
const uint entry = 0x80000000;
const uint table = 0x80000100;
const uint case0 = entry + 0x1C;
const uint case1 = entry + 0x24;
var image = TranslatorCppTestHarness.CreateImage(
(entry + 0x00, 0x28030001), // cmplwi r3, 1
(entry + 0x04, 0x5463103A), // slwi r3, r3, 2
(entry + 0x08, 0x3D808000), // lis r12, 0x8000
(entry + 0x0C, 0x398C0100), // addi r12, r12, 0x100
(entry + 0x10, 0x7C0C182E), // lwzx r0, r12, r3
(entry + 0x14, 0x7C0903A6), // mtctr r0
(entry + 0x18, 0x4E800420), // bctr
(case0 + 0x00, 0x38600007), // li r3, 7
(case0 + 0x04, 0x4E800020), // blr
(case1 + 0x00, 0x38600009), // li r3, 9
(case1 + 0x04, 0x4E800020), // blr
(table + 0x00, case0),
(table + 0x04, case1));
using var disassembler = new PpcDisassembler();
var instructions = disassembler.DisassembleFunction(
image,
entry,
maxInstructions: 32,
maxBytes: 0x80,
knownFunctionEntryPoints: new HashSet<uint> { entry, case1 });
Assert.Contains(instructions, ins => ins.Address == case0);
Assert.Contains(instructions, ins => ins.Address == case1);
}
[Fact]
public void JumpTableCaseBranchCanTargetKnownLocalContinuation()
{
const uint entry = 0x80000000;
const uint table = 0x80000100;
const uint case0 = entry + 0x1C;
const uint case1 = entry + 0x28;
const uint common = entry + 0x34;
var image = TranslatorCppTestHarness.CreateImage(
(entry + 0x00, 0x28030002), // cmplwi r3, 2
(entry + 0x04, 0x5463103A), // slwi r3, r3, 2
(entry + 0x08, 0x3D808000), // lis r12, 0x8000
(entry + 0x0C, 0x398C0100), // addi r12, r12, 0x100
(entry + 0x10, 0x7C0C182E), // lwzx r0, r12, r3
(entry + 0x14, 0x7C0903A6), // mtctr r0
(entry + 0x18, 0x4E800420), // bctr
(case0 + 0x00, 0x38600007), // li r3, 7
(case0 + 0x04, 0x48000014), // b common
(case1 + 0x00, 0x38600009), // li r3, 9
(case1 + 0x04, 0x48000008), // b common
(common + 0x00, 0x4E800020), // blr
(table + 0x00, case0),
(table + 0x04, case1),
(table + 0x08, common));
using var disassembler = new PpcDisassembler();
var instructions = disassembler.DisassembleFunction(
image,
entry,
maxInstructions: 32,
maxBytes: 0x80,
knownFunctionEntryPoints: new HashSet<uint> { entry, case0, case1, common });
Assert.Contains(instructions, ins => ins.Address == common);
}
[Fact]
public void ConditionalBranchesStillFollowKnownFunctionEntryTargets()
{
const uint entry = 0x80001000;
const uint branchTarget = 0x80001014;
var image = TranslatorCppTestHarness.CreateImage(
(entry + 0x00, 0x2C030000), // cmpwi r3, 0
(entry + 0x04, EncodeBc(12, 2, 0x10)), // beq branchTarget
(entry + 0x08, 0x38800001), // li r4, 1
(entry + 0x0C, 0x4E800020), // blr
(branchTarget + 0x00, 0x38800002), // li r4, 2
(branchTarget + 0x04, 0x4E800020)); // blr
using var disassembler = new PpcDisassembler();
var instructions = disassembler.DisassembleFunction(
image,
entry,
maxInstructions: 16,
maxBytes: 0x40,
knownFunctionEntryPoints: new HashSet<uint> { entry, branchTarget });
Assert.Contains(instructions, ins => ins.Address == branchTarget);
}
[Fact]
public void DisassemblerThrowsWhenEntryPointIsOutsideLoadedImage()
{
var image = new ProgramImage(
new byte[8],
AddressRange.FromStartAndSize(MemoryLayout.RamBase, 8),
AddressRange.FromStartAndSize(MemoryLayout.RamBase, 8),
AddressRange.FromStartAndSize(MemoryLayout.RamBase, 0),
"test");
using var disassembler = new PpcDisassembler();
Assert.Throws<ArgumentOutOfRangeException>(
() => disassembler.DisassembleFunction(image, MemoryLayout.RamBase + 0x100));
}
[Fact]
public void LimitExceededExceptionExposesAllRecordedProperties()
{
var ex = new PpcDisassemblyLimitExceededException(
entryPoint: 0x800060A4,
maxInstructions: 32,
maxBytes: 0x100,
decodedInstructions: 32,
blockedAddress: 0x80006100);
Assert.Equal(0x800060A4u, ex.EntryPoint);
Assert.Equal(32, ex.MaxInstructions);
Assert.Equal(0x100, ex.MaxBytes);
Assert.Equal(32, ex.DecodedInstructions);
Assert.Equal(0x80006100u, ex.BlockedAddress);
Assert.Contains("0x80006100", ex.Message);
}
private static uint EncodeBc(uint bo, uint bi, int branchDelta, bool aa = false, bool lk = false)
=> (16u << 26) | (bo << 21) | (bi << 16) | ((((uint)branchDelta >> 2) & 0x3FFFu) << 2) | ((aa ? 1u : 0u) << 1) | (lk ? 1u : 0u);
}
@@ -0,0 +1,390 @@
using System;
using System.Linq;
using Translator.Core.Disassembly;
using Translator.Core.Ir;
using Translator.Core.Lifting;
using Xunit;
namespace Translator.Tests;
public class PpcLifterAdditionalCoverageTests
{
private static PpcRegisterOperand Gpr(int index) => new($"r{index}", index);
private static PpcInstruction Instruction(uint raw, string mnemonic, params PpcOperand[] operands)
=> PpcInstruction.Synthetic(0x80000000, raw, mnemonic, operands);
[Fact]
public void LiftsAdditionalNonDotArithmeticAndLogicalForms()
{
var lifter = new PpcLifter();
var orc = Instruction(0, "orc", Gpr(3), Gpr(4), Gpr(5));
var orcIr = Assert.Single(lifter.Lift(new[] { orc })).Ir;
Assert.Equal("orc", Assert.IsType<IrBinary>(Assert.Single(orcIr)).Op);
var addc = Instruction(0, "addc", Gpr(6), Gpr(6), Gpr(7));
var addcIr = Assert.Single(lifter.Lift(new[] { addc })).Ir;
Assert.Equal("r6_addc_left", Assert.IsType<IrAssign>(addcIr[0]).Destination);
Assert.DoesNotContain(addcIr, ins => ins is IrSetCrField);
var subfc = Instruction(0, "subfc", Gpr(8), Gpr(9), Gpr(8));
var subfcIr = Assert.Single(lifter.Lift(new[] { subfc })).Ir;
Assert.Equal("r8_subfc_min", Assert.IsType<IrAssign>(subfcIr[0]).Destination);
Assert.DoesNotContain(subfcIr, ins => ins is IrSetCrField);
var extsb = Instruction(0, "extsb", Gpr(10), Gpr(11));
var extsbBinary = Assert.IsType<IrBinary>(Assert.Single(Assert.Single(lifter.Lift(new[] { extsb })).Ir));
Assert.Equal("sext", extsbBinary.Op);
Assert.Equal(8, extsbBinary.Right.Constant);
var adde = Instruction(0, "adde", Gpr(12), Gpr(12), Gpr(13));
var addeIr = Assert.Single(lifter.Lift(new[] { adde })).Ir;
Assert.Equal("PPC_GetCarry", Assert.IsType<IrCall>(addeIr[1]).Target);
Assert.DoesNotContain(addeIr, ins => ins is IrSetCrField);
var subfze = Instruction(0, "subfze", Gpr(14), Gpr(14));
var subfzeIr = Assert.Single(lifter.Lift(new[] { subfze })).Ir;
Assert.Equal("r14_not", Assert.IsType<IrBinary>(subfzeIr[1]).Destination);
Assert.DoesNotContain(subfzeIr, ins => ins is IrSetCrField);
var cmpwTwoOperand = Instruction(0, "cmpw", Gpr(15), Gpr(16));
var cmpwIr = Assert.Single(lifter.Lift(new[] { cmpwTwoOperand })).Ir;
var cmpwSetCr = Assert.IsType<IrSetCrField>(Assert.Single(cmpwIr));
Assert.Equal(0, cmpwSetCr.FieldIndex);
Assert.False(cmpwSetCr.IsUnsigned);
Assert.Equal("r15", cmpwSetCr.Left.RegisterName);
Assert.Equal("r16", cmpwSetCr.Right.RegisterName);
var nand = Instruction(0, "nand", Gpr(17), Gpr(18), Gpr(19));
Assert.Equal("nand", Assert.IsType<IrBinary>(Assert.Single(Assert.Single(lifter.Lift(new[] { nand })).Ir)).Op);
var eqv = Instruction(0, "eqv.", Gpr(17), Gpr(18), Gpr(19));
var eqvIr = Assert.Single(lifter.Lift(new[] { eqv })).Ir;
Assert.Equal("eqv", Assert.IsType<IrBinary>(eqvIr[0]).Op);
Assert.IsType<IrSetCrField>(eqvIr[1]);
var extsh = Instruction(0, "extsh", Gpr(20), Gpr(21));
var extshBinary = Assert.IsType<IrBinary>(Assert.Single(Assert.Single(lifter.Lift(new[] { extsh })).Ir));
Assert.Equal("sext", extshBinary.Op);
Assert.Equal(16, extshBinary.Right.Constant);
var divw = Instruction(0, "divw", Gpr(22), Gpr(23), Gpr(24));
Assert.Equal("div", Assert.IsType<IrBinary>(Assert.Single(Assert.Single(lifter.Lift(new[] { divw })).Ir)).Op);
var slwi = Instruction(0, "slwi", Gpr(25), Gpr(26), new PpcImmediateOperand(3));
Assert.Equal("shl", Assert.IsType<IrBinary>(Assert.Single(Assert.Single(lifter.Lift(new[] { slwi })).Ir)).Op);
var addic = Instruction(0, "addic", Gpr(27), Gpr(27), new PpcImmediateOperand(4));
var addicIr = Assert.Single(lifter.Lift(new[] { addic })).Ir;
Assert.Equal("r27_addic_src", Assert.IsType<IrAssign>(addicIr[0]).Destination);
Assert.DoesNotContain(addicIr, ins => ins is IrSetCrField);
}
[Fact]
public void LiftsAdditionalMemoryAndControlFormsAndRejectsInvalidUpdateBases()
{
var lifter = new PpcLifter();
var dcbzL = Instruction(0, "dcbz_l", Gpr(3), Gpr(4));
var dcbzLIr = Assert.Single(lifter.Lift(new[] { dcbzL })).Ir;
Assert.Equal("r4_addr_dcbz", Assert.IsType<IrBinary>(dcbzLIr[0]).Destination);
Assert.Equal("memset_zero_32", Assert.IsType<IrCall>(dcbzLIr[2]).Target);
var rfi = Instruction(0, "rfi");
Assert.IsType<IrReturn>(Assert.Single(Assert.Single(lifter.Lift(new[] { rfi })).Ir));
var lfdu = Instruction(0, "lfdu", new PpcRegisterOperand("f1", 1), new PpcDisplacementOperand(8, "r2", 2));
var lfduIr = Assert.Single(lifter.Lift(new[] { lfdu })).Ir;
Assert.Equal("r2_addr", Assert.IsType<IrBinary>(lfduIr[0]).Destination);
Assert.Equal("f1", Assert.IsType<IrLoad>(lfduIr[1]).Destination);
Assert.Equal("r2", Assert.IsType<IrAssign>(lfduIr[2]).Destination);
var invalidLfdu = Instruction(0, "lfdu", new PpcRegisterOperand("f1", 1), new PpcDisplacementOperand(8, "r0", 0));
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { invalidLfdu }));
var invalidLfsu = Instruction(0, "lfsu", new PpcRegisterOperand("f2", 2), new PpcDisplacementOperand(4, "r0", 0));
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { invalidLfsu }));
var invalidLhzu = Instruction(0, "lhzu", Gpr(3), new PpcDisplacementOperand(2, "r0", 0));
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { invalidLhzu }));
var invalidStbu = Instruction(0, "stbu", Gpr(4), new PpcDisplacementOperand(1, "r0", 0));
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { invalidStbu }));
var invalidSthu = Instruction(0, "sthu", Gpr(5), new PpcDisplacementOperand(2, "r0", 0));
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { invalidSthu }));
}
[Fact]
public void LiftsLegacyOpcodeAliasesAndR0StoreForms()
{
var lifter = new PpcLifter();
var andis = Instruction((3u << 21) | (4u << 16) | 0x1234u, "opc_29");
var andisIr = Assert.Single(lifter.Lift(new[] { andis })).Ir;
var andisBinary = Assert.IsType<IrBinary>(andisIr[0]);
Assert.Equal("r4", andisBinary.Destination);
Assert.Equal("and", andisBinary.Op);
Assert.IsType<IrSetCrField>(andisIr[1]);
var legacySthu = Instruction((5u << 21) | (6u << 16) | 0x0002u, "opc_45");
var legacySthuIr = Assert.Single(lifter.Lift(new[] { legacySthu })).Ir;
Assert.Equal("r6_sthu_ea", Assert.IsType<IrBinary>(legacySthuIr[0]).Destination);
Assert.Equal("r6", Assert.IsType<IrAssign>(legacySthuIr[2]).Destination);
var stwR0 = Instruction(0, "stw", Gpr(7), new PpcDisplacementOperand(12, "r0", 0));
var stwR0Ir = Assert.Single(lifter.Lift(new[] { stwR0 })).Ir;
Assert.Collection(
stwR0Ir,
ins =>
{
var assign = Assert.IsType<IrAssign>(ins);
Assert.Equal("r7_ea", assign.Destination);
Assert.Equal(12, assign.Value.Constant);
},
ins =>
{
var store = Assert.IsType<IrStore>(ins);
Assert.Equal("r7_ea", store.Address.Base);
});
}
[Fact]
public void LiftsAdditionalIndexedFloatingAndRotateMaskForms()
{
var lifter = new PpcLifter();
var bgtlr = Instruction(0, "bgtlr");
var bgtlrBranch = Assert.IsType<IrBranch>(Assert.Single(Assert.Single(lifter.Lift(new[] { bgtlr })).Ir));
Assert.Equal("bgt", bgtlrBranch.Condition);
Assert.Equal("return", bgtlrBranch.TrueLabel);
var sc = Instruction(0, "sc");
Assert.Equal("OSSystemCall", Assert.IsType<IrCall>(Assert.Single(Assert.Single(lifter.Lift(new[] { sc })).Ir)).Target);
var tw = Instruction((31u << 21) | (4u << 16) | (5u << 11), "tw");
var trap = Assert.IsType<IrCall>(Assert.Single(Assert.Single(lifter.Lift(new[] { tw })).Ir));
Assert.Equal("PPC_TrapWord", trap.Target);
Assert.Equal(31, trap.Arguments[0].Constant);
Assert.Equal("r4", trap.Arguments[1].RegisterName);
Assert.Equal("r5", trap.Arguments[2].RegisterName);
var twi = Instruction((12u << 21) | (6u << 16) | 0xFFFEu, "twi");
var immediateTrap = Assert.IsType<IrCall>(Assert.Single(Assert.Single(lifter.Lift(new[] { twi })).Ir));
Assert.Equal("PPC_TrapWord", immediateTrap.Target);
Assert.Equal(12, immediateTrap.Arguments[0].Constant);
Assert.Equal("r6", immediateTrap.Arguments[1].RegisterName);
Assert.Equal(-2, immediateTrap.Arguments[2].Constant);
var addis = Instruction(0, "addis", Gpr(3), Gpr(0), new PpcImmediateOperand(0x1234));
var addisBinary = Assert.IsType<IrBinary>(Assert.Single(Assert.Single(lifter.Lift(new[] { addis })).Ir));
Assert.Equal("add", addisBinary.Op);
Assert.Equal(0x12340000, addisBinary.Right.Constant);
Assert.Equal(0, addisBinary.Left.Constant);
var lfdx = Instruction((1u << 21) | (2u << 16) | (3u << 11), "lfdx");
var lfdxIr = Assert.Single(lifter.Lift(new[] { lfdx })).Ir;
Assert.Equal("addr_lfdx_80000000_loc", Assert.IsType<IrBinary>(lfdxIr[0]).Destination);
Assert.Equal("f1", Assert.IsType<IrLoad>(lfdxIr[1]).Destination);
var lfsx = Instruction((4u << 21) | (5u << 16) | (6u << 11), "lfsx");
var lfsxIr = Assert.Single(lifter.Lift(new[] { lfsx })).Ir;
Assert.Equal("f4", Assert.IsType<IrLoad>(lfsxIr[1]).Destination);
var lfsux = Instruction((7u << 21) | (8u << 16) | (9u << 11), "lfsux");
var lfsuxIr = Assert.Single(lifter.Lift(new[] { lfsux })).Ir;
Assert.Equal("addr_lfsux_80000000_loc", Assert.IsType<IrBinary>(lfsuxIr[0]).Destination);
Assert.Equal("r8", Assert.IsType<IrAssign>(lfsuxIr[2]).Destination);
var badLfsux = Instruction((7u << 21) | (0u << 16) | (9u << 11), "lfsux");
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { badLfsux }));
var lfdux = Instruction((7u << 21) | (8u << 16) | (9u << 11), "lfdux");
var lfduxIr = Assert.Single(lifter.Lift(new[] { lfdux })).Ir;
Assert.Equal("addr_lfdux_80000000_loc", Assert.IsType<IrBinary>(lfduxIr[0]).Destination);
Assert.Equal("f7", Assert.IsType<IrLoad>(lfduxIr[1]).Destination);
Assert.Equal("r8", Assert.IsType<IrAssign>(lfduxIr[2]).Destination);
var badLfdux = Instruction((7u << 21) | (0u << 16) | (9u << 11), "lfdux");
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { badLfdux }));
var stfdx = Instruction((10u << 21) | (11u << 16) | (12u << 11), "stfdx");
var stfdxStore = Assert.IsType<IrStore>(Assert.Single(Assert.Single(lifter.Lift(new[] { stfdx })).Ir.Skip(1)));
Assert.Equal("f10", stfdxStore.Source.RegisterName);
var stfsx = Instruction((13u << 21) | (14u << 16) | (15u << 11), "stfsx");
var stfsxStore = Assert.IsType<IrStore>(Assert.Single(Assert.Single(lifter.Lift(new[] { stfsx })).Ir.Skip(1)));
Assert.Equal("f13", stfsxStore.Source.RegisterName);
var stfsux = Instruction((16u << 21) | (17u << 16) | (18u << 11), "stfsux");
var stfsuxIr = Assert.Single(lifter.Lift(new[] { stfsux })).Ir;
Assert.Equal("r17", Assert.IsType<IrBinary>(stfsuxIr[0]).Destination);
Assert.Equal("f16", Assert.IsType<IrStore>(stfsuxIr[1]).Source.RegisterName);
var badStfsux = Instruction((16u << 21) | (0u << 16) | (18u << 11), "stfsux");
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { badStfsux }));
var stfdux = Instruction((16u << 21) | (17u << 16) | (18u << 11), "stfdux");
var stfduxIr = Assert.Single(lifter.Lift(new[] { stfdux })).Ir;
Assert.Equal("addr_stfdux_80000000_loc", Assert.IsType<IrBinary>(stfduxIr[0]).Destination);
Assert.Equal("f16", Assert.IsType<IrStore>(stfduxIr[1]).Source.RegisterName);
Assert.Equal("r17", Assert.IsType<IrAssign>(stfduxIr[2]).Destination);
var badStfdux = Instruction((16u << 21) | (0u << 16) | (18u << 11), "stfdux");
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { badStfdux }));
var sthux = Instruction((19u << 21) | (20u << 16) | (21u << 11), "sthux");
var sthuxIr = Assert.Single(lifter.Lift(new[] { sthux })).Ir;
Assert.Equal("addr_sthux_80000000_loc", Assert.IsType<IrBinary>(sthuxIr[0]).Destination);
Assert.Equal("r20", Assert.IsType<IrAssign>(sthuxIr[2]).Destination);
var badSthux = Instruction((19u << 21) | (0u << 16) | (21u << 11), "sthux");
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { badSthux }));
var stfiwx = Instruction((22u << 21) | (23u << 16) | (24u << 11), "stfiwx");
var stfiwxCall = Assert.IsType<IrCall>(Assert.Single(Assert.Single(lifter.Lift(new[] { stfiwx })).Ir.Skip(1)));
Assert.Equal("PPC_Stfiwx", stfiwxCall.Target);
Assert.Equal("f22", stfiwxCall.Arguments[1].RegisterName);
var clrlwi = Instruction(0, "clrlwi", Gpr(25), Gpr(26), new PpcImmediateOperand(8));
var clrlwiIr = Assert.Single(lifter.Lift(new[] { clrlwi })).Ir;
Assert.Single(clrlwiIr);
Assert.Equal("and", Assert.IsType<IrBinary>(clrlwiIr[0]).Op);
var rlwinm = Instruction(0, "rlwinm", Gpr(27), Gpr(28), new PpcImmediateOperand(5), new PpcImmediateOperand(3), new PpcImmediateOperand(7));
var rlwinmIr = Assert.Single(lifter.Lift(new[] { rlwinm })).Ir;
Assert.Contains(rlwinmIr, ins => ins is IrBinary binary && binary.Destination == "r27_rot");
var rlwimi = Instruction(0, "rlwimi", Gpr(29), Gpr(30), new PpcImmediateOperand(4), new PpcImmediateOperand(8), new PpcImmediateOperand(12));
var rlwimiIr = Assert.Single(lifter.Lift(new[] { rlwimi })).Ir;
Assert.Contains(rlwimiIr, ins => ins is IrBinary binary && binary.Destination == "r29_mdest");
var rlwnm = Instruction(0, "rlwnm", Gpr(31), Gpr(1), Gpr(2), new PpcImmediateOperand(4), new PpcImmediateOperand(9));
var rlwnmIr = Assert.Single(lifter.Lift(new[] { rlwnm })).Ir;
Assert.Contains(rlwnmIr, ins => ins is IrBinary binary && binary.Destination == "r31_rot");
Assert.Contains(rlwnmIr, ins => ins is IrBinary binary && binary.Destination == "r31" && binary.Op == "and");
}
[Fact]
public void LiftsAdditionalSprShiftAndControlDotForms()
{
var lifter = new PpcLifter();
var orcDot = Instruction(0, "orc.", Gpr(3), Gpr(4), Gpr(5));
Assert.Contains(Assert.Single(lifter.Lift(new[] { orcDot })).Ir, ins => ins is IrSetCrField);
var divwuDot = Instruction(0, "divwu.", Gpr(6), Gpr(7), Gpr(8));
Assert.Contains(Assert.Single(lifter.Lift(new[] { divwuDot })).Ir, ins => ins is IrSetCrField);
var rlwnmDot = Instruction(0, "rlwnm.", Gpr(9), Gpr(10), Gpr(11), new PpcImmediateOperand(3), new PpcImmediateOperand(9));
Assert.Contains(Assert.Single(lifter.Lift(new[] { rlwnmDot })).Ir, ins => ins is IrSetCrField);
var mfsprHid0 = Instruction(0, "mfspr", Gpr(12), new PpcImmediateOperand(1008));
Assert.Equal("hid0", Assert.IsType<IrAssign>(Assert.Single(Assert.Single(lifter.Lift(new[] { mfsprHid0 })).Ir)).Value.RegisterName);
var mfsprFallback = Instruction(0, "mfspr", Gpr(13), new PpcImmediateOperand(31));
var mfsprFallbackIr = Assert.Single(lifter.Lift(new[] { mfsprFallback })).Ir;
Assert.IsType<IrComment>(mfsprFallbackIr[0]);
Assert.Equal("PPC_ReadSpr", Assert.IsType<IrCall>(mfsprFallbackIr[1]).Target);
var mtsprHid1 = Instruction(0, "mtspr", new PpcImmediateOperand(1009), Gpr(14));
Assert.Equal("hid1", Assert.IsType<IrAssign>(Assert.Single(Assert.Single(lifter.Lift(new[] { mtsprHid1 })).Ir)).Destination);
var mtsprFallback = Instruction(0, "mtspr", new PpcImmediateOperand(16), Gpr(15));
var mtsprFallbackIr = Assert.Single(lifter.Lift(new[] { mtsprFallback })).Ir;
Assert.IsType<IrComment>(mtsprFallbackIr[0]);
Assert.Equal("PPC_WriteSpr", Assert.IsType<IrCall>(mtsprFallbackIr[1]).Target);
var mtsr = Instruction(0, "mtsr", new PpcImmediateOperand(3), Gpr(16));
Assert.IsType<IrComment>(Assert.Single(Assert.Single(lifter.Lift(new[] { mtsr })).Ir));
var mtsrin = Instruction(0, "mtsrin", Gpr(16), Gpr(17));
Assert.IsType<IrComment>(Assert.Single(Assert.Single(lifter.Lift(new[] { mtsrin })).Ir));
var mfsr = Instruction(0, "mfsr", Gpr(17), new PpcImmediateOperand(3));
Assert.Equal(0, Assert.IsType<IrAssign>(Assert.Single(Assert.Single(lifter.Lift(new[] { mfsr })).Ir)).Value.Constant);
var mfsrin = Instruction(0, "mfsrin", Gpr(18), Gpr(19));
Assert.Equal(0, Assert.IsType<IrAssign>(Assert.Single(Assert.Single(lifter.Lift(new[] { mfsrin })).Ir)).Value.Constant);
var mulhwuDot = Instruction(0, "mulhwu.", Gpr(18), Gpr(19), Gpr(20));
Assert.Contains(Assert.Single(lifter.Lift(new[] { mulhwuDot })).Ir, ins => ins is IrSetCrField);
var mulhwDot = Instruction(0, "mulhw.", Gpr(21), Gpr(22), Gpr(23));
Assert.Contains(Assert.Single(lifter.Lift(new[] { mulhwDot })).Ir, ins => ins is IrSetCrField);
var bdz = Instruction(0, "bdz", new PpcImmediateOperand(unchecked((int)0x80000010)));
Assert.Equal("bdz", Assert.IsType<IrBranch>(Assert.Single(Assert.Single(lifter.Lift(new[] { bdz })).Ir.Skip(1))).Condition);
var srwi = Instruction(0, "srwi", Gpr(24), Gpr(25), new PpcImmediateOperand(5));
Assert.Equal("shr", Assert.IsType<IrBinary>(Assert.Single(Assert.Single(lifter.Lift(new[] { srwi })).Ir)).Op);
var slwDot = Instruction(0, "slw.", Gpr(26), Gpr(27), Gpr(28));
Assert.Contains(Assert.Single(lifter.Lift(new[] { slwDot })).Ir, ins => ins is IrSetCrField);
var andisDot = Instruction(0, "andis.", Gpr(29), Gpr(30), new PpcImmediateOperand(0x1234));
var andisDotIr = Assert.Single(lifter.Lift(new[] { andisDot })).Ir;
var andisDotBinary = Assert.IsType<IrBinary>(andisDotIr[0]);
Assert.Equal(0x12340000, andisDotBinary.Right.Constant);
Assert.Contains(andisDotIr, ins => ins is IrSetCrField);
var andcDot = Instruction(0, "andc.", Gpr(31), Gpr(1), Gpr(2));
Assert.Contains(Assert.Single(lifter.Lift(new[] { andcDot })).Ir, ins => ins is IrSetCrField);
var xorDot = Instruction(0, "xor.", Gpr(3), Gpr(4), Gpr(5));
Assert.Contains(Assert.Single(lifter.Lift(new[] { xorDot })).Ir, ins => ins is IrSetCrField);
var bltlr = Instruction(0, "bltlr");
Assert.Equal("blt", Assert.IsType<IrBranch>(Assert.Single(Assert.Single(lifter.Lift(new[] { bltlr })).Ir)).Condition);
var cntlzwDot = Instruction(0, "cntlzw.", Gpr(6), Gpr(7));
Assert.Contains(Assert.Single(lifter.Lift(new[] { cntlzwDot })).Ir, ins => ins is IrSetCrField);
var invalidLwzu = Instruction(0, "lwzu", Gpr(8), new PpcDisplacementOperand(4, "r0", 0));
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { invalidLwzu }));
var bclrl = Instruction((12u << 21) | (8u << 16), "bclrl", new PpcConditionRegisterOperand("cr2", 8));
var bclrlBranch = Assert.IsType<IrBranch>(Assert.Single(Assert.Single(lifter.Lift(new[] { bclrl })).Ir));
Assert.Contains("call_lr_80000000_80000004", bclrlBranch.TrueLabel, StringComparison.Ordinal);
Assert.Equal("raw", bclrlBranch.Condition);
Assert.Contains("GetCRBit(ctx, 2, 0)", bclrlBranch.ConditionRegister, StringComparison.Ordinal);
}
[Fact]
public void LiftsAdditionalIndexedHalfwordAndStoreUpdateForms()
{
var lifter = new PpcLifter();
var stfdu = Instruction(0, "stfdu", new PpcRegisterOperand("f1", 1), new PpcDisplacementOperand(8, "r2", 2));
var stfduIr = Assert.Single(lifter.Lift(new[] { stfdu })).Ir;
Assert.Equal("r2", Assert.IsType<IrBinary>(stfduIr[0]).Destination);
Assert.Equal("f1", Assert.IsType<IrStore>(stfduIr[1]).Source.RegisterName);
var stfsu = Instruction(0, "stfsu", new PpcRegisterOperand("f3", 3), new PpcDisplacementOperand(4, "r4", 4));
var stfsuIr = Assert.Single(lifter.Lift(new[] { stfsu })).Ir;
Assert.Equal("r4", Assert.IsType<IrBinary>(stfsuIr[0]).Destination);
Assert.Equal("f3", Assert.IsType<IrStore>(stfsuIr[1]).Source.RegisterName);
var invalidStwux = Instruction(0, "stwux", Gpr(5), Gpr(0), Gpr(6));
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { invalidStwux }));
var invalidLwzux = Instruction(0, "lwzux", Gpr(7), Gpr(0), Gpr(8));
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { invalidLwzux }));
var lhzx = Instruction((9u << 21) | (10u << 16) | (11u << 11), "lhzx", Gpr(9), Gpr(10), Gpr(11));
var lhzxIr = Assert.Single(lifter.Lift(new[] { lhzx })).Ir;
Assert.Equal("r9", Assert.IsType<IrLoad>(lhzxIr[1]).Destination);
var lhaux = Instruction((12u << 21) | (13u << 16) | (14u << 11), "lhaux", Gpr(12), Gpr(13), Gpr(14));
var lhauxIr = Assert.Single(lifter.Lift(new[] { lhaux })).Ir;
Assert.Equal("r12", Assert.IsType<IrLoad>(lhauxIr[1]).Destination);
Assert.Equal("r13", Assert.IsType<IrAssign>(lhauxIr[4]).Destination);
var invalidLhaux = Instruction((12u << 21) | (0u << 16) | (14u << 11), "lhaux", Gpr(12), Gpr(0), Gpr(14));
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { invalidLhaux }));
var lhzux = Instruction((15u << 21) | (16u << 16) | (17u << 11), "lhzux", Gpr(15), Gpr(16), Gpr(17));
var lhzuxIr = Assert.Single(lifter.Lift(new[] { lhzux })).Ir;
Assert.Equal("r15", Assert.IsType<IrLoad>(lhzuxIr[1]).Destination);
Assert.Equal("r16", Assert.IsType<IrAssign>(lhzuxIr[2]).Destination);
var invalidLhzux = Instruction((15u << 21) | (0u << 16) | (17u << 11), "lhzux", Gpr(15), Gpr(0), Gpr(17));
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { invalidLhzux }));
}
}
@@ -0,0 +1,678 @@
using System;
using System.Linq;
using System.Reflection;
using Translator.Core.Disassembly;
using Translator.Core.Ir;
using Translator.Core.Lifting;
using Xunit;
namespace Translator.Tests;
public class PpcLifterCoverageTests
{
private static PpcRegisterOperand Gpr(int index) => new($"r{index}", index);
private static readonly Type LifterType = typeof(PpcLifter);
private static PpcInstruction Instruction(uint raw, string mnemonic, params PpcOperand[] operands)
=> PpcInstruction.Synthetic(0x80000000, raw, mnemonic, operands);
private static T InvokePrivate<T>(string name, params object?[] args)
{
var method = LifterType.GetMethod(name, BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(method);
return (T)method!.Invoke(null, args)!;
}
[Fact]
public void NormalizesCrBitStyleConditionRegistersDuringCompare()
{
var instruction = Instruction(
0x7C032800,
"cmpw",
new PpcOperand[]
{
new PpcConditionRegisterOperand("cr28", 28),
Gpr(3),
Gpr(5)
});
var ir = Assert.Single(new PpcLifter().Lift(new[] { instruction })).Ir;
var setCr = Assert.IsType<IrSetCrField>(Assert.Single(ir));
Assert.Equal(7, setCr.FieldIndex);
Assert.Equal("r3", setCr.Left.RegisterName);
Assert.Equal("r5", setCr.Right.RegisterName);
}
[Fact]
public void LiftsXoriDotAndBarrierInstructions()
{
var lifter = new PpcLifter();
var nop = Instruction(0, "nop");
Assert.Empty(Assert.Single(lifter.Lift(new[] { nop })).Ir);
var xori = Instruction(0, "xori.", Gpr(4), Gpr(5), new PpcImmediateOperand(-1));
var xoriIr = Assert.Single(lifter.Lift(new[] { xori })).Ir;
var xoriBinary = Assert.IsType<IrBinary>(xoriIr[0]);
Assert.Equal("r4", xoriBinary.Destination);
Assert.Equal("xor", xoriBinary.Op);
Assert.Equal(0xFFFF, xoriBinary.Right.Constant);
Assert.IsType<IrSetCrField>(xoriIr[1]);
var sync = Instruction(0, "sync");
var syncIr = Assert.Single(lifter.Lift(new[] { sync })).Ir;
Assert.Contains("sync", Assert.IsType<IrComment>(Assert.Single(syncIr)).Text, StringComparison.Ordinal);
}
[Fact]
public void LiftsAliasingCarryInstructions()
{
var lifter = new PpcLifter();
var addc = Instruction(0, "addc.", Gpr(3), Gpr(3), Gpr(4));
var addcIr = Assert.Single(lifter.Lift(new[] { addc })).Ir;
var addcTemp = Assert.IsType<IrAssign>(addcIr[0]);
Assert.Equal("r3_addc_left", addcTemp.Destination);
Assert.Equal("r3", addcTemp.Value.RegisterName);
Assert.Equal("add", Assert.IsType<IrBinary>(addcIr[1]).Op);
Assert.Equal("PPC_UpdateCarryAdd", Assert.IsType<IrCall>(addcIr[2]).Target);
Assert.IsType<IrSetCrField>(addcIr[3]);
var subfc = Instruction(0, "subfc.", Gpr(5), Gpr(1), Gpr(5));
var subfcIr = Assert.Single(lifter.Lift(new[] { subfc })).Ir;
var subfcTemp = Assert.IsType<IrAssign>(subfcIr[0]);
Assert.Equal("r5_subfc_min", subfcTemp.Destination);
Assert.Equal("r5", subfcTemp.Value.RegisterName);
Assert.Equal("sub", Assert.IsType<IrBinary>(subfcIr[1]).Op);
Assert.Equal("PPC_UpdateCarrySub", Assert.IsType<IrCall>(subfcIr[2]).Target);
Assert.IsType<IrSetCrField>(subfcIr[3]);
var lmw = Instruction(0, "lmw", Gpr(30), new PpcDisplacementOperand(8, "r4", 4));
var lmwIr = Assert.Single(lifter.Lift(new[] { lmw })).Ir;
Assert.Collection(
lmwIr,
op => Assert.Equal("r30", Assert.IsType<IrLoad>(op).Destination),
op => Assert.Equal("r31", Assert.IsType<IrLoad>(op).Destination));
}
[Fact]
public void LiftsIndexedByteUpdateFormsAndRejectsZeroBase()
{
var lifter = new PpcLifter();
var lbzux = Instruction((3u << 21) | (4u << 16) | (5u << 11), "lbzux");
var lbzuxIr = Assert.Single(lifter.Lift(new[] { lbzux })).Ir;
Assert.Equal(3, lbzuxIr.Count);
Assert.Equal("addr_lbzux_80000000_loc", Assert.IsType<IrBinary>(lbzuxIr[0]).Destination);
Assert.Equal("r3", Assert.IsType<IrLoad>(lbzuxIr[1]).Destination);
Assert.Equal("r4", Assert.IsType<IrAssign>(lbzuxIr[2]).Destination);
var stbux = Instruction((2u << 21) | (4u << 16) | (5u << 11), "stbux");
var stbuxIr = Assert.Single(lifter.Lift(new[] { stbux })).Ir;
Assert.Equal(3, stbuxIr.Count);
Assert.Equal("addr_stbux_80000000_loc", Assert.IsType<IrBinary>(stbuxIr[0]).Destination);
Assert.IsType<IrStore>(stbuxIr[1]);
Assert.Equal("r4", Assert.IsType<IrAssign>(stbuxIr[2]).Destination);
var invalidLbzux = Instruction((1u << 21) | (0u << 16) | (5u << 11), "lbzux");
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { invalidLbzux }));
var invalidStbux = Instruction((1u << 21) | (0u << 16) | (5u << 11), "stbux");
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { invalidStbux }));
}
[Fact]
public void LiftsChainedCarryAndZeroingVariants()
{
var lifter = new PpcLifter();
var adde = Instruction(0, "adde.", Gpr(7), Gpr(7), Gpr(8));
var addeIr = Assert.Single(lifter.Lift(new[] { adde })).Ir;
Assert.Equal("r7_adde_left", Assert.IsType<IrAssign>(addeIr[0]).Destination);
Assert.Equal("PPC_GetCarry", Assert.IsType<IrCall>(addeIr[1]).Target);
Assert.Equal("add", Assert.IsType<IrBinary>(addeIr[2]).Op);
Assert.Equal("add", Assert.IsType<IrBinary>(addeIr[3]).Op);
Assert.Equal("PPC_UpdateCarryAdd", Assert.IsType<IrCall>(addeIr[4]).Target);
Assert.IsType<IrSetCrField>(addeIr[5]);
var addze = Instruction(0, "addze.", Gpr(9), Gpr(9));
var addzeIr = Assert.Single(lifter.Lift(new[] { addze })).Ir;
Assert.Equal("r9_addze_src", Assert.IsType<IrAssign>(addzeIr[0]).Destination);
Assert.Equal("PPC_GetCarry", Assert.IsType<IrCall>(addzeIr[1]).Target);
Assert.Equal("PPC_UpdateCarryAdd", Assert.IsType<IrCall>(addzeIr[3]).Target);
Assert.IsType<IrSetCrField>(addzeIr[4]);
var addme = Instruction(0, "addme.", Gpr(11), Gpr(11));
var addmeIr = Assert.Single(lifter.Lift(new[] { addme })).Ir;
Assert.Equal("r11_addme_src", Assert.IsType<IrAssign>(addmeIr[0]).Destination);
Assert.Equal("PPC_GetCarry", Assert.IsType<IrCall>(addmeIr[1]).Target);
Assert.Equal(-1, Assert.IsType<IrBinary>(addmeIr[2]).Right.Constant);
Assert.Equal("PPC_UpdateCarryAdd", Assert.IsType<IrCall>(addmeIr[4]).Target);
Assert.IsType<IrSetCrField>(addmeIr[5]);
var subfze = Instruction(0, "subfze.", Gpr(10), Gpr(10));
var subfzeIr = Assert.Single(lifter.Lift(new[] { subfze })).Ir;
Assert.Equal("r10_subfze_src", Assert.IsType<IrAssign>(subfzeIr[0]).Destination);
Assert.Equal("not", Assert.IsType<IrBinary>(subfzeIr[1]).Op);
Assert.Equal("PPC_GetCarry", Assert.IsType<IrCall>(subfzeIr[2]).Target);
Assert.Equal("PPC_UpdateCarryAdd", Assert.IsType<IrCall>(subfzeIr[4]).Target);
Assert.IsType<IrSetCrField>(subfzeIr[5]);
var subfme = Instruction(0, "subfme.", Gpr(12), Gpr(12));
var subfmeIr = Assert.Single(lifter.Lift(new[] { subfme })).Ir;
Assert.Equal("r12_subfme_src", Assert.IsType<IrAssign>(subfmeIr[0]).Destination);
Assert.Equal("not", Assert.IsType<IrBinary>(subfmeIr[1]).Op);
Assert.Equal("PPC_GetCarry", Assert.IsType<IrCall>(subfmeIr[2]).Target);
Assert.Equal(-1, Assert.IsType<IrBinary>(subfmeIr[3]).Right.Constant);
Assert.Equal("PPC_UpdateCarryAdd", Assert.IsType<IrCall>(subfmeIr[5]).Target);
Assert.IsType<IrSetCrField>(subfmeIr[6]);
}
[Fact]
public void LiftsBranchCtrCrAndMaskedRegisterForms()
{
var lifter = new PpcLifter();
var bcctr = Instruction((4u << 21) | (9u << 16), "bcctr");
var bcctrIr = Assert.Single(lifter.Lift(new[] { bcctr })).Ir;
var bcctrBranch = Assert.IsType<IrBranch>(Assert.Single(bcctrIr));
Assert.Equal("raw", bcctrBranch.Condition);
Assert.Equal("indirect_ctr_80000000", bcctrBranch.TrueLabel);
Assert.Equal("0x80000004", bcctrBranch.FalseLabel);
Assert.Contains("GetCRBit(ctx, 2, 1)", bcctrBranch.ConditionRegister, StringComparison.Ordinal);
var bctr = PpcInstruction.Synthetic(
0x80000010,
0,
"bctr",
Array.Empty<PpcOperand>(),
branchTargets: new[] { 0x80000030u, 0x80000020u, 0x80000030u });
var bctrIr = Assert.Single(lifter.Lift(new[] { bctr })).Ir;
var jumpTable = Assert.IsType<IrJumpTable>(Assert.Single(bctrIr));
Assert.Equal("ctr", jumpTable.Selector);
Assert.Equal(new uint[] { 0x80000030u, 0x80000020u }, jumpTable.Cases.Select(c => c.TargetAddress));
var directBctr = Instruction(0, "bctr");
var directBctrIr = Assert.Single(lifter.Lift(new[] { directBctr })).Ir;
Assert.Equal("ctr", Assert.IsType<IrIndirectJump>(Assert.Single(directBctrIr)).Target.RegisterName);
var crclr = Instruction(0, "crclr", new PpcConditionRegisterOperand("cr7eq", 30));
var crclrIr = Assert.Single(lifter.Lift(new[] { crclr })).Ir;
var crclrCall = Assert.IsType<IrCall>(Assert.Single(crclrIr));
Assert.Equal("PPC_CrSetBit", crclrCall.Target);
Assert.Equal(new long?[] { 30, 0 }, crclrCall.Arguments.Select(a => a.Constant).ToArray());
var cror = Instruction(
0,
"cror",
new PpcOperand[]
{
new PpcConditionRegisterOperand("crb1", 1),
new PpcConditionRegisterOperand("crb2", 2),
new PpcConditionRegisterOperand("crb3", 3)
});
var crorIr = Assert.Single(lifter.Lift(new[] { cror })).Ir;
var crorCall = Assert.IsType<IrCall>(Assert.Single(crorIr));
Assert.Equal("PPC_CrLogical", crorCall.Target);
Assert.Equal(new long?[] { 7, 1, 2, 3 }, crorCall.Arguments.Select(a => a.Constant).ToArray());
var mtcrfNoOp = Instruction(0, "mtcrf", new PpcImmediateOperand(0), Gpr(3));
var mtcrfNoOpIr = Assert.Single(lifter.Lift(new[] { mtcrfNoOp })).Ir;
Assert.Contains("no-op", Assert.IsType<IrComment>(Assert.Single(mtcrfNoOpIr)).Text, StringComparison.Ordinal);
var mtcrfMasked = Instruction(0, "mtcrf", new PpcImmediateOperand(0x80), Gpr(4));
var mtcrfMaskedIr = Assert.Single(lifter.Lift(new[] { mtcrfMasked })).Ir;
Assert.Equal(3, mtcrfMaskedIr.Count);
var maskedSrc = Assert.IsType<IrBinary>(mtcrfMaskedIr[0]);
Assert.Equal("r4_mtcrf_src", maskedSrc.Destination);
Assert.Equal(unchecked((int)0xF0000000), maskedSrc.Right.Constant);
Assert.Equal("cr", Assert.IsType<IrBinary>(mtcrfMaskedIr[2]).Destination);
}
[Fact]
public void LiftsComparisonShiftAndUpdateMemoryForms()
{
var lifter = new PpcLifter();
var cmplwi = Instruction(0, "cmplwi", Gpr(6), new PpcImmediateOperand(0x1234));
var cmplwiIr = Assert.Single(lifter.Lift(new[] { cmplwi })).Ir;
var cmplwiSetCr = Assert.IsType<IrSetCrField>(Assert.Single(cmplwiIr));
Assert.True(cmplwiSetCr.IsUnsigned);
Assert.Equal("r6", cmplwiSetCr.Left.RegisterName);
Assert.Equal(0x1234, cmplwiSetCr.Right.Constant);
var cmpwi = Instruction(0, "cmpwi", new PpcConditionRegisterOperand("cr2", 8), Gpr(6), new PpcImmediateOperand(-1));
var cmpwiIr = Assert.Single(lifter.Lift(new[] { cmpwi })).Ir;
var cmpwiSetCr = Assert.IsType<IrSetCrField>(Assert.Single(cmpwiIr));
Assert.Equal(2, cmpwiSetCr.FieldIndex);
Assert.False(cmpwiSetCr.IsUnsigned);
Assert.Equal("r6", cmpwiSetCr.Left.RegisterName);
Assert.Equal(-1, cmpwiSetCr.Right.Constant);
var sraw = Instruction(0, "sraw.", Gpr(11), Gpr(12), Gpr(13));
var srawIr = Assert.Single(lifter.Lift(new[] { sraw })).Ir;
Assert.Equal("PPC_UpdateCarryShiftRight", Assert.IsType<IrCall>(srawIr[0]).Target);
Assert.Equal("r13", Assert.IsType<IrCall>(srawIr[0]).Arguments[1].RegisterName);
Assert.Equal("ppc_sraw", Assert.IsType<IrBinary>(srawIr[1]).Op);
Assert.IsType<IrSetCrField>(srawIr[2]);
var srw = Instruction(0, "srw.", Gpr(7), Gpr(8), Gpr(9));
var srwIr = Assert.Single(lifter.Lift(new[] { srw })).Ir;
var srwBinary = Assert.IsType<IrBinary>(srwIr[0]);
Assert.Equal("ppc_srw", srwBinary.Op);
Assert.Equal("r9", srwBinary.Right.RegisterName);
Assert.IsType<IrSetCrField>(srwIr[1]);
var slw = Instruction(0, "slw.", Gpr(10), Gpr(11), Gpr(12));
var slwIr = Assert.Single(lifter.Lift(new[] { slw })).Ir;
var slwBinary = Assert.IsType<IrBinary>(slwIr[0]);
Assert.Equal("ppc_slw", slwBinary.Op);
Assert.Equal("r12", slwBinary.Right.RegisterName);
Assert.IsType<IrSetCrField>(slwIr[1]);
var lhau = Instruction((3u << 21) | (4u << 16) | 0x0010u, "lhau");
var lhauIr = Assert.Single(lifter.Lift(new[] { lhau })).Ir;
Assert.Equal("r4", Assert.IsType<IrBinary>(lhauIr[0]).Destination);
Assert.Equal("r3", Assert.IsType<IrLoad>(lhauIr[1]).Destination);
Assert.Equal("sar", Assert.IsType<IrBinary>(lhauIr[3]).Op);
var invalidLhau = Instruction((3u << 21) | (0u << 16) | 0x0010u, "lhau");
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { invalidLhau }));
var stbu = Instruction(0, "stbu", Gpr(7), new PpcDisplacementOperand(4, "r8", 8));
var stbuIr = Assert.Single(lifter.Lift(new[] { stbu })).Ir;
Assert.Equal("r8_stbu_ea", Assert.IsType<IrBinary>(stbuIr[0]).Destination);
Assert.IsType<IrStore>(stbuIr[1]);
Assert.Equal("r8", Assert.IsType<IrAssign>(stbuIr[2]).Destination);
var sthu = Instruction(0, "sthu", Gpr(7), new PpcDisplacementOperand(6, "r8", 8));
var sthuIr = Assert.Single(lifter.Lift(new[] { sthu })).Ir;
Assert.Equal("r8_sthu_ea", Assert.IsType<IrBinary>(sthuIr[0]).Destination);
Assert.IsType<IrStore>(sthuIr[1]);
Assert.Equal("r8", Assert.IsType<IrAssign>(sthuIr[2]).Destination);
var addic = Instruction(0, "addic.", Gpr(15), Gpr(15), new PpcImmediateOperand(4));
var addicIr = Assert.Single(lifter.Lift(new[] { addic })).Ir;
Assert.Equal("r15_addic_src", Assert.IsType<IrAssign>(addicIr[0]).Destination);
Assert.Equal("PPC_UpdateCarryAdd", Assert.IsType<IrCall>(addicIr[2]).Target);
Assert.IsType<IrSetCrField>(addicIr[3]);
var subfe = Instruction(0, "subfe.", Gpr(16), Gpr(1), Gpr(16));
var subfeIr = Assert.Single(lifter.Lift(new[] { subfe })).Ir;
Assert.Equal("r16_subfe_rb", Assert.IsType<IrAssign>(subfeIr[0]).Destination);
Assert.Equal("not", Assert.IsType<IrBinary>(subfeIr[1]).Op);
Assert.Equal("PPC_GetCarry", Assert.IsType<IrCall>(subfeIr[2]).Target);
Assert.Equal("PPC_UpdateCarryAdd", Assert.IsType<IrCall>(subfeIr[5]).Target);
Assert.IsType<IrSetCrField>(subfeIr[6]);
}
[Fact]
public void LiftsMiscUnaryStatusAndLogicalForms()
{
var lifter = new PpcLifter();
var neg = Instruction(0, "neg.", Gpr(3), Gpr(4));
var negIr = Assert.Single(lifter.Lift(new[] { neg })).Ir;
var negBinary = Assert.IsType<IrBinary>(negIr[0]);
Assert.Equal("sub", negBinary.Op);
Assert.Equal(0, negBinary.Left.Constant);
Assert.Equal("r4", negBinary.Right.RegisterName);
Assert.IsType<IrSetCrField>(negIr[1]);
var nor = Instruction(0, "nor", Gpr(5), Gpr(6), Gpr(7));
var norIr = Assert.Single(lifter.Lift(new[] { nor })).Ir;
Assert.Equal("nor", Assert.IsType<IrBinary>(Assert.Single(norIr)).Op);
var nand = Instruction(0, "nand.", Gpr(8), Gpr(9), Gpr(10));
var nandIr = Assert.Single(lifter.Lift(new[] { nand })).Ir;
Assert.Equal("nand", Assert.IsType<IrBinary>(nandIr[0]).Op);
Assert.IsType<IrSetCrField>(nandIr[1]);
var mfdar = Instruction(0, "mfdar", Gpr(11));
Assert.Equal(0, Assert.IsType<IrAssign>(Assert.Single(Assert.Single(lifter.Lift(new[] { mfdar })).Ir)).Value.Constant);
var mfmsr = Instruction(0, "mfmsr", Gpr(12));
Assert.Equal("msr", Assert.IsType<IrAssign>(Assert.Single(Assert.Single(lifter.Lift(new[] { mfmsr })).Ir)).Value.RegisterName);
var mfpvr = Instruction(0, "mfpvr", Gpr(13));
Assert.Equal(0, Assert.IsType<IrAssign>(Assert.Single(Assert.Single(lifter.Lift(new[] { mfpvr })).Ir)).Value.Constant);
var mfdsisr = Instruction(0, "mfdsisr", Gpr(14));
Assert.Equal(0, Assert.IsType<IrAssign>(Assert.Single(Assert.Single(lifter.Lift(new[] { mfdsisr })).Ir)).Value.Constant);
var blelr = Instruction(0, "blelr");
var blelrBranch = Assert.IsType<IrBranch>(Assert.Single(Assert.Single(lifter.Lift(new[] { blelr })).Ir));
Assert.Equal("ble", blelrBranch.Condition);
Assert.Equal("return", blelrBranch.TrueLabel);
}
[Fact]
public void LiftsUpdateLoadFormsAndCacheNoOps()
{
var lifter = new PpcLifter();
var dcbzR0 = Instruction(0, "dcbz", Gpr(0), Gpr(5));
var dcbzR0Ir = Assert.Single(lifter.Lift(new[] { dcbzR0 })).Ir;
Assert.Equal("r5_addr_dcbz", Assert.IsType<IrAssign>(dcbzR0Ir[0]).Destination);
Assert.Equal("memset_zero_32", Assert.IsType<IrCall>(dcbzR0Ir[2]).Target);
var dcbz = Instruction(0, "dcbz_l", Gpr(4), Gpr(5));
var dcbzIr = Assert.Single(lifter.Lift(new[] { dcbz })).Ir;
Assert.Equal("add", Assert.IsType<IrBinary>(dcbzIr[0]).Op);
Assert.Equal("and", Assert.IsType<IrBinary>(dcbzIr[1]).Op);
foreach (var mnemonic in new[] { "icbi", "dcbi", "dcbf" })
{
var ins = Instruction(0, mnemonic);
var ir = Assert.Single(lifter.Lift(new[] { ins })).Ir;
Assert.Contains(mnemonic, Assert.IsType<IrComment>(Assert.Single(ir)).Text, StringComparison.Ordinal);
}
var lfsu = Instruction(0, "lfsu", Gpr(1), new PpcDisplacementOperand(4, "r3", 3));
var lfsuIr = Assert.Single(lifter.Lift(new[] { lfsu })).Ir;
Assert.Equal("r3", Assert.IsType<IrBinary>(lfsuIr[0]).Destination);
Assert.Equal("r1", Assert.IsType<IrLoad>(lfsuIr[1]).Destination);
var invalidLfsu = Instruction(0, "lfsu", Gpr(1), new PpcDisplacementOperand(4, "r0", 0));
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { invalidLfsu }));
var lhzu = Instruction(0, "lhzu", Gpr(2), new PpcDisplacementOperand(6, "r4", 4));
var lhzuIr = Assert.Single(lifter.Lift(new[] { lhzu })).Ir;
Assert.Equal("r4", Assert.IsType<IrBinary>(lhzuIr[0]).Destination);
Assert.Equal("r2", Assert.IsType<IrLoad>(lhzuIr[1]).Destination);
var invalidLhzu = Instruction(0, "lhzu", Gpr(2), new PpcDisplacementOperand(6, "r0", 0));
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { invalidLhzu }));
var lbzu = Instruction(0, "lbzu", Gpr(3), new PpcDisplacementOperand(8, "r5", 5));
var lbzuIr = Assert.Single(lifter.Lift(new[] { lbzu })).Ir;
Assert.Equal("r5", Assert.IsType<IrBinary>(lbzuIr[0]).Destination);
Assert.Equal("r3", Assert.IsType<IrLoad>(lbzuIr[1]).Destination);
var invalidLbzu = Instruction(0, "lbzu", Gpr(3), new PpcDisplacementOperand(8, "r0", 0));
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { invalidLbzu }));
var stwu = Instruction(0, "stwu", Gpr(6), new PpcDisplacementOperand(0x10, "r7", 7));
var stwuIr = Assert.Single(lifter.Lift(new[] { stwu })).Ir;
Assert.IsType<IrStore>(stwuIr[0]);
Assert.Equal("r7", Assert.IsType<IrBinary>(stwuIr[1]).Destination);
var invalidStwu = Instruction(0, "stwu", Gpr(6), new PpcDisplacementOperand(0x10, "r0", 0));
Assert.Throws<InvalidOperationException>(() => lifter.Lift(new[] { invalidStwu }));
}
[Fact]
public void LiftsControlRegisterTableAndDivisionForms()
{
var lifter = new PpcLifter();
var cmplw = Instruction(0, "cmplw", new PpcConditionRegisterOperand("cr4", 16), Gpr(8), Gpr(9));
var cmplwIr = Assert.Single(lifter.Lift(new[] { cmplw })).Ir;
var cmplwSetCr = Assert.IsType<IrSetCrField>(Assert.Single(cmplwIr));
Assert.Equal(4, cmplwSetCr.FieldIndex);
Assert.True(cmplwSetCr.IsUnsigned);
Assert.Equal("r8", cmplwSetCr.Left.RegisterName);
Assert.Equal("r9", cmplwSetCr.Right.RegisterName);
foreach (var mnemonic in new[] { "mticcr", "mttbu", "mttbl", "mtdar", "mtdsisr" })
{
var ins = Instruction(0, mnemonic, Gpr(3));
Assert.Throws<NotImplementedException>(() => lifter.Lift(new[] { ins }));
}
var mfibatu = Instruction(0, "mfibatu", Gpr(4), Gpr(5));
Assert.Equal(0, Assert.IsType<IrAssign>(Assert.Single(Assert.Single(lifter.Lift(new[] { mfibatu })).Ir)).Value.Constant);
foreach (var mnemonic in new[] { "mtibatu", "mtibatl", "mtdbatu", "mtdbatl" })
{
var ins = Instruction(0, mnemonic, Gpr(4), Gpr(5));
Assert.Throws<NotImplementedException>(() => lifter.Lift(new[] { ins }));
}
foreach (var mnemonic in new[] { "mfibatl", "mfdbatu", "mfdbatl" })
{
var ins = Instruction(0, mnemonic, Gpr(4), Gpr(5));
var ir = Assert.Single(lifter.Lift(new[] { ins })).Ir;
Assert.Equal(0, Assert.IsType<IrAssign>(Assert.Single(ir)).Value.Constant);
}
var mtxer = Instruction(0, "mtxer", Gpr(6));
Assert.Equal("xer", Assert.IsType<IrAssign>(Assert.Single(Assert.Single(lifter.Lift(new[] { mtxer })).Ir)).Destination);
var mfxer = Instruction(0, "mfxer", Gpr(7));
Assert.Equal("xer", Assert.IsType<IrAssign>(Assert.Single(Assert.Single(lifter.Lift(new[] { mfxer })).Ir)).Value.RegisterName);
var divw = Instruction(0, "divw.", Gpr(10), Gpr(11), Gpr(12));
var divwIr = Assert.Single(lifter.Lift(new[] { divw })).Ir;
Assert.Equal("div", Assert.IsType<IrBinary>(divwIr[0]).Op);
Assert.IsType<IrSetCrField>(divwIr[1]);
}
[Fact]
public void LiftsExtraCrShiftAndUnsupportedFallbackForms()
{
var lifter = new PpcLifter();
var srw = Instruction(0, "srw.", Gpr(3), Gpr(4), Gpr(5));
var srwIr = Assert.Single(lifter.Lift(new[] { srw })).Ir;
Assert.Equal("r3", Assert.IsType<IrBinary>(srwIr[0]).Destination);
Assert.Equal("ppc_srw", Assert.IsType<IrBinary>(srwIr[0]).Op);
Assert.IsType<IrSetCrField>(srwIr[1]);
var divwu = Instruction(0, "divwu.", Gpr(6), Gpr(7), Gpr(8));
var divwuIr = Assert.Single(lifter.Lift(new[] { divwu })).Ir;
Assert.Equal("divu", Assert.IsType<IrBinary>(divwuIr[0]).Op);
Assert.IsType<IrSetCrField>(divwuIr[1]);
var extsb = Instruction(0, "extsb.", Gpr(9), Gpr(10));
var extsbIr = Assert.Single(lifter.Lift(new[] { extsb })).Ir;
var extsbBinary = Assert.IsType<IrBinary>(extsbIr[0]);
Assert.Equal("sext", extsbBinary.Op);
Assert.Equal(8, extsbBinary.Right.Constant);
Assert.IsType<IrSetCrField>(extsbIr[1]);
var mcrf = Instruction(0, "mcrf", new PpcConditionRegisterOperand("cr7", 28), new PpcConditionRegisterOperand("cr2", 8));
var mcrfCall = Assert.IsType<IrCall>(Assert.Single(Assert.Single(lifter.Lift(new[] { mcrf })).Ir));
Assert.Equal("PPC_Mcrf", mcrfCall.Target);
Assert.Equal(new long?[] { 7, 2 }, mcrfCall.Arguments.Select(a => a.Constant).ToArray());
var mcrxr = Instruction(0, "mcrxr", new PpcConditionRegisterOperand("cr7", 28));
var mcrxrCall = Assert.IsType<IrCall>(Assert.Single(Assert.Single(lifter.Lift(new[] { mcrxr })).Ir));
Assert.Equal("PPC_Mcrxr", mcrxrCall.Target);
Assert.Equal(7, Assert.Single(mcrxrCall.Arguments).Constant);
var crxor = Instruction(
0,
"crxor",
new PpcConditionRegisterOperand("cr7eq", 30),
new PpcConditionRegisterOperand("crb2", 2),
new PpcConditionRegisterOperand("31", 31));
var crxorCall = Assert.IsType<IrCall>(Assert.Single(Assert.Single(lifter.Lift(new[] { crxor })).Ir));
Assert.Equal("PPC_CrLogical", crxorCall.Target);
Assert.Equal(new long?[] { 2, 30, 2, 31 }, crxorCall.Arguments.Select(a => a.Constant).ToArray());
var slwi = Instruction(0, "slwi.", Gpr(11), Gpr(12), new PpcImmediateOperand(5));
var slwiIr = Assert.Single(lifter.Lift(new[] { slwi })).Ir;
Assert.Equal("shl", Assert.IsType<IrBinary>(slwiIr[0]).Op);
Assert.IsType<IrSetCrField>(slwiIr[1]);
var unknown = Instruction(0x12345678, "totally_unknown", Gpr(3));
var undefined = Assert.IsType<IrUndefined>(Assert.Single(Assert.Single(lifter.Lift(new[] { unknown }, allowUnsupported: true)).Ir));
Assert.Contains("totally_unknown", undefined.Disassembly, StringComparison.Ordinal);
Assert.Equal(0x12345678u, undefined.RawInstruction);
var unsupported = Instruction(0, "mticcr", Gpr(3));
var unsupportedUndefined = Assert.IsType<IrUndefined>(Assert.Single(Assert.Single(lifter.Lift(new[] { unsupported }, allowUnsupported: true)).Ir));
Assert.Contains("UNIMPLEMENTED", unsupportedUndefined.Reason ?? string.Empty, StringComparison.Ordinal);
}
[Fact]
public void PrivateHelpersParseDisplacementsCrBitsAndBranchConditions()
{
Assert.Equal((0, "r3"), InvokePrivate<(int offset, string @base)>("ParseDisplacement", "r3"));
Assert.Equal((0x1234, "0"), InvokePrivate<(int offset, string @base)>("ParseDisplacement", "0x1234"));
Assert.Equal((0, "r4"), InvokePrivate<(int offset, string @base)>("ParseDisplacement", "(r4)"));
Assert.Throws<TargetInvocationException>(() => InvokePrivate<(int offset, string @base)>("ParseDisplacement", "broken("));
Assert.Equal(30, InvokePrivate<int>("ParseCrBitIndex", "cr7eq"));
Assert.Equal(2, InvokePrivate<int>("ParseCrBitIndex", "crb2"));
Assert.Equal(25, InvokePrivate<int>("ParseCrBitIndex", "cr6gt"));
Assert.Equal(31, InvokePrivate<int>("ParseCrBitIndex", "31"));
Assert.Equal(0, InvokePrivate<int>("ParseCrBitIndex", "mystery"));
Assert.Equal(7, InvokePrivate<int>("ParseCrFieldName", "crf7"));
Assert.Equal(3, InvokePrivate<int>("ParseCrFieldName", "cr3"));
Assert.Equal(0, InvokePrivate<int>("ParseCrFieldName", "bogus"));
var absoluteLoad = InvokePrivate<IReadOnlyList<IrInstruction>>("DFormLoad", "r5", "0", 12, 4);
Assert.Equal("r5_ea", Assert.IsType<IrAssign>(absoluteLoad[0]).Destination);
Assert.Equal("r5", Assert.IsType<IrLoad>(absoluteLoad[1]).Destination);
var registerLoad = InvokePrivate<IReadOnlyList<IrInstruction>>("DFormLoad", "r6", "r7", 16, 2);
var registerLoadIr = Assert.Single(registerLoad);
var load = Assert.IsType<IrLoad>(registerLoadIr);
Assert.Equal("r6", load.Destination);
Assert.Equal("r7", load.Address.Base);
Assert.Equal(16, load.Address.Offset);
}
[Fact]
public void PrivateCrLogicalHelperMapsAllRemainingOpcodes()
{
foreach (var (mnemonic, opcode) in new[]
{
("crnor", 0L),
("crandc", 1L),
("crnand", 3L),
("crand", 4L),
("creqv", 5L),
("crorc", 6L)
})
{
var ir = Assert.Single(InvokePrivate<IReadOnlyList<IrInstruction>>("LiftCrLogical", mnemonic, "cr7eq", "crb2", "31"));
var call = Assert.IsType<IrCall>(ir);
Assert.Equal("PPC_CrLogical", call.Target);
Assert.Equal(opcode, call.Arguments[0].Constant);
Assert.Equal(30, call.Arguments[1].Constant);
Assert.Equal(2, call.Arguments[2].Constant);
Assert.Equal(31, call.Arguments[3].Constant);
}
}
[Fact]
public void LiftsLinkRegisterBranchFamiliesAndGenericBcFallbacks()
{
var lifter = new PpcLifter();
var beqlrl = Instruction(0, "beqlrl", new PpcConditionRegisterOperand("cr3", 12));
var beqlrlBranch = Assert.IsType<IrBranch>(Assert.Single(Assert.Single(lifter.Lift(new[] { beqlrl })).Ir));
Assert.Equal("beq", beqlrlBranch.Condition);
Assert.Equal("cr3", beqlrlBranch.ConditionRegister);
Assert.Contains("call_lr_80000000_80000004", beqlrlBranch.TrueLabel, StringComparison.Ordinal);
var bgtlrl = Instruction(0, "bgtlrl");
var bgtlrlBranch = Assert.IsType<IrBranch>(Assert.Single(Assert.Single(lifter.Lift(new[] { bgtlrl })).Ir));
Assert.Equal("bgt", bgtlrlBranch.Condition);
Assert.Equal("cr0", bgtlrlBranch.ConditionRegister);
var bnelrCr6 = Instruction(0, "bnelr", new PpcConditionRegisterOperand("cr6", 24));
var bnelrCr6Branch = Assert.IsType<IrBranch>(Assert.Single(Assert.Single(lifter.Lift(new[] { bnelrCr6 })).Ir));
Assert.Equal("bne", bnelrCr6Branch.Condition);
Assert.Equal("return", bnelrCr6Branch.TrueLabel);
Assert.Equal("cr6", bnelrCr6Branch.ConditionRegister);
var bnelrCr7 = Instruction(0, "bnelr", new PpcConditionRegisterOperand("cr7", 28));
var bnelrCr7Branch = Assert.IsType<IrBranch>(Assert.Single(Assert.Single(lifter.Lift(new[] { bnelrCr7 })).Ir));
Assert.Equal("bne", bnelrCr7Branch.Condition);
Assert.Equal("cr7", bnelrCr7Branch.ConditionRegister);
var bcImmediate = Instruction(
0,
"bc",
new PpcImmediateOperand(4),
new PpcImmediateOperand(3),
new PpcImmediateOperand(unchecked((int)0x80000020)));
var bcImmediateBranch = Assert.IsType<IrBranch>(Assert.Single(Assert.Single(lifter.Lift(new[] { bcImmediate })).Ir));
Assert.Equal("raw", bcImmediateBranch.Condition);
Assert.Contains("GetCRBit(ctx, 0, 3)", bcImmediateBranch.ConditionRegister, StringComparison.Ordinal);
var bcFallback = PpcInstruction.Synthetic(
0x80000000,
0,
"bc",
new PpcOperand[] { new PpcConditionRegisterOperand("cr4", 16) },
new[] { 0x80000010u },
isConditional: true);
var bcFallbackBranch = Assert.IsType<IrBranch>(Assert.Single(Assert.Single(lifter.Lift(new[] { bcFallback })).Ir));
Assert.Equal("bc", bcFallbackBranch.Condition);
Assert.Equal("cr4", bcFallbackBranch.ConditionRegister);
var ctrBc = Assert.Single(lifter.Lift(new[] { Instruction(0, "bc", new PpcImmediateOperand(0), new PpcImmediateOperand(0), new PpcImmediateOperand(4)) })).Ir;
Assert.Equal("ctr", Assert.IsType<IrBinary>(ctrBc[0]).Destination);
Assert.Equal("raw", Assert.IsType<IrBranch>(ctrBc[1]).Condition);
var bclrReturn = Instruction((20u << 21), "bclr");
var bclrReturnBranch = Assert.IsType<IrBranch>(Assert.Single(Assert.Single(lifter.Lift(new[] { bclrReturn })).Ir));
Assert.Equal("raw", bclrReturnBranch.Condition);
Assert.Equal("return", bclrReturnBranch.TrueLabel);
var bclrCr = Instruction((12u << 21) | (2u << 16), "bclr");
var bclrCrBranch = Assert.IsType<IrBranch>(Assert.Single(Assert.Single(lifter.Lift(new[] { bclrCr })).Ir));
Assert.Equal("raw", bclrCrBranch.Condition);
Assert.Equal("return", bclrCrBranch.TrueLabel);
var bclrCtr = Instruction((16u << 21), "bclr");
var bclrCtrIr = Assert.Single(lifter.Lift(new[] { bclrCtr })).Ir;
Assert.Equal("ctr", Assert.IsType<IrBinary>(bclrCtrIr[0]).Destination);
Assert.Equal("raw", Assert.IsType<IrBranch>(bclrCtrIr[1]).Condition);
var combinedBclr = Assert.Single(lifter.Lift(new[] { Instruction(0, "bclr") })).Ir;
Assert.Equal("ctr", Assert.IsType<IrBinary>(combinedBclr[0]).Destination);
Assert.Equal("raw", Assert.IsType<IrBranch>(combinedBclr[1]).Condition);
}
[Fact]
public void LiftsCacheHintsByteReverseAndRawXoFallbacks()
{
var lifter = new PpcLifter();
foreach (var mnemonic in new[] { "dcbst", "xo_54", "xo_470", "dcbt", "xo_278", "dcbtst", "xo_246", "dcba", "xo_758", "tlbie", "xo_306", "tlbsync", "xo_566" })
{
var ir = Assert.Single(lifter.Lift(new[] { Instruction(0, mnemonic) })).Ir;
Assert.IsType<IrComment>(Assert.Single(ir));
}
var stwbrx = Assert.Single(lifter.Lift(new[] { Instruction((3u << 21) | (4u << 16) | (5u << 11), "stwbrx") })).Ir;
Assert.Equal("PPC_StoreWordByteReverse", Assert.IsType<IrCall>(stwbrx[1]).Target);
var lwbrx = Assert.Single(lifter.Lift(new[] { Instruction((6u << 21) | (7u << 16) | (8u << 11), "lwbrx") })).Ir;
Assert.Equal("PPC_LoadWordByteReverse", Assert.IsType<IrCall>(lwbrx[1]).Target);
var lhbrx = Assert.Single(lifter.Lift(new[] { Instruction((9u << 21) | (10u << 16) | (11u << 11), "lhbrx") })).Ir;
Assert.Equal("PPC_LoadHalfwordByteReverse", Assert.IsType<IrCall>(lhbrx[1]).Target);
var sthbrx = Assert.Single(lifter.Lift(new[] { Instruction((12u << 21) | (13u << 16) | (14u << 11), "sthbrx") })).Ir;
Assert.Equal("PPC_StoreHalfwordByteReverse", Assert.IsType<IrCall>(sthbrx[1]).Target);
var xoNand = Assert.Single(lifter.Lift(new[] { Instruction((4u << 21) | (5u << 16) | (6u << 11) | 1u, "xo_476") })).Ir;
Assert.Equal("nand", Assert.IsType<IrBinary>(xoNand[0]).Op);
Assert.IsType<IrSetCrField>(xoNand[1]);
var xoMulhwu = Assert.Single(lifter.Lift(new[] { Instruction((7u << 21) | (8u << 16) | (9u << 11), "xo_11") })).Ir;
Assert.Equal("mulhwu", Assert.IsType<IrBinary>(Assert.Single(xoMulhwu)).Op);
var xoStwx = Assert.Single(lifter.Lift(new[] { Instruction((10u << 21) | (11u << 16) | (12u << 11), "xo_151") })).Ir;
Assert.Equal(4, Assert.IsType<IrStore>(xoStwx[1]).SizeBytes);
var xoStbx = Assert.Single(lifter.Lift(new[] { Instruction((13u << 21) | (14u << 16) | (15u << 11), "xo_215") })).Ir;
Assert.Equal(1, Assert.IsType<IrStore>(xoStbx[1]).SizeBytes);
var xoAddze = Assert.Single(lifter.Lift(new[] { Instruction((16u << 21) | (17u << 16) | 1u, "xo_202") })).Ir;
Assert.Equal("PPC_GetCarry", Assert.IsType<IrCall>(xoAddze[0]).Target);
Assert.IsType<IrSetCrField>(xoAddze[^1]);
var xoCmp = Assert.Single(lifter.Lift(new[] { Instruction((3u << 23) | (18u << 16) | (19u << 11), "xo_0") })).Ir;
var xoCmpSetCr = Assert.IsType<IrSetCrField>(Assert.Single(xoCmp));
Assert.Equal(3, xoCmpSetCr.FieldIndex);
Assert.Equal("r18", xoCmpSetCr.Left.RegisterName);
Assert.Equal("r19", xoCmpSetCr.Right.RegisterName);
var xoMfsr = Instruction((20u << 21), "xo_595");
Assert.Throws<NotImplementedException>(() => lifter.Lift(new[] { xoMfsr }));
}
}
@@ -0,0 +1,225 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Translator.Core.Disassembly;
using Translator.Core.Ir;
using Translator.Core.Lifting;
using Xunit;
namespace Translator.Tests;
public class PpcLifterHelperCoverageTests
{
private static readonly Type LifterType = typeof(PpcLifter);
private static T InvokePrivate<T>(string name, params object?[] args)
{
var method = LifterType.GetMethod(name, BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(method);
return (T)method!.Invoke(null, args)!;
}
private static PpcRegisterOperand Gpr(int index) => new($"r{index}", index);
[Fact]
public void HelperMethods_ReadParseAndNormalizeRegisters()
{
var instruction = PpcInstruction.Synthetic(0x80000000, 0x12345678, "addi", new PpcOperand[] { Gpr(3), Gpr(4), new PpcImmediateOperand(1) });
Assert.Equal(0x12345678u, InvokePrivate<uint>("ReadRawInstruction", instruction));
// PpcInstruction stores the raw big-endian word itself, so an instruction
// can no longer be missing its encoding; a zero word still reads back.
var zeroWord = new PpcInstruction(
0x80000000,
0u,
"nop",
Array.Empty<PpcOperand>(),
Array.Empty<uint>(),
isReturn: false,
isCall: false,
isConditionalBranch: false);
Assert.Equal(0u, InvokePrivate<uint>("ReadRawInstruction", zeroWord));
Assert.Equal(0, InvokePrivate<int>("ParseCrFieldIndex", string.Empty));
Assert.Equal(0, InvokePrivate<int>("ParseCrFieldIndex", "cr"));
Assert.Equal(7, InvokePrivate<int>("ParseCrFieldIndex", "cr7_3"));
Assert.Equal(3, InvokePrivate<int>("ParseCrFieldIndex", "cr3"));
Assert.Equal(0, InvokePrivate<int>("ParseCrFieldIndex", "bad"));
var zeroBase = InvokePrivate<IrValue>("BaseOrZero", "r0");
Assert.Equal("const", zeroBase.Kind);
Assert.Equal(0, zeroBase.Constant);
var regBase = InvokePrivate<IrValue>("BaseOrZero", "r5");
Assert.Equal("register", regBase.Kind);
Assert.Equal("r5", regBase.RegisterName);
Assert.Equal(31, InvokePrivate<int>("ParseRegisterNumber", "r31"));
var badRegister = Assert.Throws<TargetInvocationException>(() => InvokePrivate<int>("ParseRegisterNumber", "ctr"));
Assert.IsType<FormatException>(badRegister.InnerException);
}
[Fact]
public void HelperMethods_EmitDFormAndPairedSingleMemoryAccesses()
{
var zeroBasedStore = InvokePrivate<IReadOnlyList<IrInstruction>>("DFormStore", "r0", 24, IrValue.Register("r7"), 4);
Assert.Collection(
zeroBasedStore,
ins =>
{
var assign = Assert.IsType<IrAssign>(ins);
Assert.Equal("r7_ea", assign.Destination);
Assert.Equal(24, assign.Value.Constant);
},
ins =>
{
var store = Assert.IsType<IrStore>(ins);
Assert.Equal("r7_ea", store.Address.Base);
Assert.Equal("r7", store.Source.RegisterName);
Assert.Equal(4, store.SizeBytes);
});
var normalStore = InvokePrivate<IReadOnlyList<IrInstruction>>("DFormStore", "r4", -8, IrValue.Register("r6"), 8);
var storeIns = Assert.IsType<IrStore>(Assert.Single(normalStore));
Assert.Equal("r4", storeIns.Address.Base);
Assert.Equal(-8, storeIns.Address.Offset);
Assert.Equal(8, storeIns.SizeBytes);
var zeroBasedLoad = InvokePrivate<IReadOnlyList<IrInstruction>>("EmitPairedSingleLoad", "f1", "r0", 32, 1, 5, false, 0x80001000u);
Assert.Collection(
zeroBasedLoad,
ins => Assert.Contains("psq_load", Assert.IsType<IrComment>(ins).Text, StringComparison.Ordinal),
ins =>
{
var assign = Assert.IsType<IrAssign>(ins);
Assert.Equal("f1_psq_ea", assign.Destination);
Assert.Equal(32, assign.Value.Constant);
},
ins =>
{
var call = Assert.IsType<IrCall>(ins);
Assert.Equal("PPC_PsqL", call.Target);
Assert.Equal("f1_psq_ea", call.Arguments[0].RegisterName);
});
var updatingLoad = InvokePrivate<IReadOnlyList<IrInstruction>>("EmitPairedSingleLoad", "f2", "r9", 12, 0, 3, true, 0x80001004u);
Assert.Equal("r9_psq_addr", Assert.IsType<IrBinary>(updatingLoad[1]).Destination);
Assert.Equal("PPC_PsqL", Assert.IsType<IrCall>(updatingLoad[2]).Target);
Assert.Equal("r9", Assert.IsType<IrAssign>(updatingLoad[3]).Destination);
var zeroOffsetLoad = InvokePrivate<IReadOnlyList<IrInstruction>>("EmitPairedSingleLoad", "f3", "r8", 0, 0, 2, false, 0x80001008u);
var directLoadCall = Assert.IsType<IrCall>(zeroOffsetLoad[1]);
Assert.Equal("r8", directLoadCall.Arguments[0].RegisterName);
var invalidLoad = Assert.Throws<TargetInvocationException>(() =>
InvokePrivate<IReadOnlyList<IrInstruction>>("EmitPairedSingleLoad", "f4", "r0", 4, 0, 0, true, 0x8000100Cu));
Assert.IsType<InvalidOperationException>(invalidLoad.InnerException);
var zeroBasedStorePs = InvokePrivate<IReadOnlyList<IrInstruction>>("EmitPairedSingleStore", "f5", "r0", 48, 1, 6, false, 0x80001010u);
Assert.Collection(
zeroBasedStorePs,
ins => Assert.Contains("psq_store", Assert.IsType<IrComment>(ins).Text, StringComparison.Ordinal),
ins =>
{
var assign = Assert.IsType<IrAssign>(ins);
Assert.Equal("f5_psq_ea", assign.Destination);
Assert.Equal(48, assign.Value.Constant);
},
ins =>
{
var call = Assert.IsType<IrCall>(ins);
Assert.Equal("PPC_PsqSt", call.Target);
Assert.Equal("f5", call.Arguments[1].RegisterName);
});
var updatingStore = InvokePrivate<IReadOnlyList<IrInstruction>>("EmitPairedSingleStore", "f6", "r10", 16, 0, 7, true, 0x80001014u);
Assert.Equal("r10_psq_ea", Assert.IsType<IrBinary>(updatingStore[1]).Destination);
Assert.Equal("PPC_PsqSt", Assert.IsType<IrCall>(updatingStore[2]).Target);
Assert.Equal("r10", Assert.IsType<IrAssign>(updatingStore[3]).Destination);
var zeroOffsetStore = InvokePrivate<IReadOnlyList<IrInstruction>>("EmitPairedSingleStore", "f7", "r11", 0, 0, 1, false, 0x80001018u);
var directStoreCall = Assert.IsType<IrCall>(zeroOffsetStore[1]);
Assert.Equal("r11", directStoreCall.Arguments[0].RegisterName);
var invalidStore = Assert.Throws<TargetInvocationException>(() =>
InvokePrivate<IReadOnlyList<IrInstruction>>("EmitPairedSingleStore", "f8", "r0", 4, 0, 0, true, 0x8000101Cu));
Assert.IsType<InvalidOperationException>(invalidStore.InnerException);
}
[Fact]
public void HelperMethods_ParseCrBitsBuildTargetLabelsAndLiftUndefined()
{
Assert.Equal(0, InvokePrivate<int>("ParseCrBitIndex", string.Empty));
Assert.Equal(30, InvokePrivate<int>("ParseCrBitIndex", "cr7eq"));
Assert.Equal(31, InvokePrivate<int>("ParseCrBitIndex", "crb40"));
Assert.Equal(13, InvokePrivate<int>("ParseCrBitIndex", "cr3gt"));
Assert.Equal(29, InvokePrivate<int>("ParseCrBitIndex", "29"));
Assert.Equal(0, InvokePrivate<int>("ParseCrBitIndex", "bogus"));
Assert.Equal(0, InvokePrivate<int>("ParseCrFieldName", string.Empty));
Assert.Equal(7, InvokePrivate<int>("ParseCrFieldName", "crf7"));
Assert.Equal(3, InvokePrivate<int>("ParseCrFieldName", "cr3"));
Assert.Equal(7, InvokePrivate<int>("ParseCrFieldName", "cr99"));
Assert.Equal(0, InvokePrivate<int>("ParseCrFieldName", "bogus"));
var noTarget = PpcInstruction.Synthetic(0x80002000, 0x48000000, "b", Array.Empty<PpcOperand>());
var noTargetEx = Assert.Throws<TargetInvocationException>(() =>
InvokePrivate<string>("TargetLabel", noTarget, new HashSet<uint>(), true));
Assert.IsType<InvalidOperationException>(noTargetEx.InnerException);
var branch = PpcInstruction.Synthetic(0x80002010, 0x48000000, "b", Array.Empty<PpcOperand>(), branchTargets: new[] { 0x80003000u });
Assert.Equal("0x80002014", InvokePrivate<string>("TargetLabel", branch, new HashSet<uint>(), true));
Assert.Equal("0x80003000", InvokePrivate<string>("TargetLabel", branch, new HashSet<uint>(), false));
Assert.Equal("0x80003000", InvokePrivate<string>("TargetLabel", branch, new HashSet<uint> { 0x80003000u }, true));
var withOperands = PpcInstruction.Synthetic(0x80003000, 0x38630001, "addi", new PpcOperand[] { Gpr(3), Gpr(3), new PpcImmediateOperand(1) });
var undefinedWithOperands = Assert.IsType<IrUndefined>(Assert.Single(InvokePrivate<IReadOnlyList<IrInstruction>>("LiftUndefined", withOperands, "unsupported")));
Assert.Equal("addi r3, r3, 1", undefinedWithOperands.Disassembly);
Assert.Equal("unsupported", undefinedWithOperands.Reason);
var withoutOperands = PpcInstruction.Synthetic(0x80003004, 0x60000000, "nop", Array.Empty<PpcOperand>());
var undefinedWithoutOperands = Assert.IsType<IrUndefined>(Assert.Single(InvokePrivate<IReadOnlyList<IrInstruction>>("LiftUndefined", withoutOperands, "no-op")));
Assert.Equal("nop", undefinedWithoutOperands.Disassembly);
}
[Fact]
public void HelperMethods_LiftCrLogicalAndIndexedPairedSingles()
{
var creqv = Assert.IsType<IrCall>(Assert.Single(InvokePrivate<IReadOnlyList<IrInstruction>>("LiftCrLogical", "creqv", "crb1", "crb2", "crb3")));
Assert.Equal("PPC_CrLogical", creqv.Target);
Assert.Equal(new long?[] { 5, 1, 2, 3 }, new[] { creqv.Arguments[0].Constant, creqv.Arguments[1].Constant, creqv.Arguments[2].Constant, creqv.Arguments[3].Constant });
var unknownLogical = Assert.IsType<IrCall>(Assert.Single(InvokePrivate<IReadOnlyList<IrInstruction>>("LiftCrLogical", "unknown", "cr0lt", "cr0gt", "cr0eq")));
Assert.Equal(0, unknownLogical.Arguments[0].Constant);
var indexedLoad = PpcInstruction.Synthetic(0x80004000, 0, "psq_lx", Array.Empty<PpcOperand>());
var indexedLoadOps = InvokePrivate<IReadOnlyList<IrInstruction>>("LiftPairedSingleIndexedLoad", indexedLoad, (3u << 21) | (4u << 16) | (5u << 11) | (1u << 10) | (2u << 7), false);
Assert.Equal("addr_psqx_80004000_loc", Assert.IsType<IrBinary>(indexedLoadOps[0]).Destination);
Assert.Equal("PPC_PsqL", Assert.IsType<IrCall>(indexedLoadOps[1]).Target);
var indexedLoadZeroBase = InvokePrivate<IReadOnlyList<IrInstruction>>("LiftPairedSingleIndexedLoad", indexedLoad, (3u << 21) | (0u << 16) | (5u << 11), false);
Assert.Equal("addr_psqx_80004000_loc", Assert.IsType<IrAssign>(indexedLoadZeroBase[0]).Destination);
var indexedLoadUpdate = InvokePrivate<IReadOnlyList<IrInstruction>>("LiftPairedSingleIndexedLoad", indexedLoad, (3u << 21) | (4u << 16) | (5u << 11), true);
Assert.Equal("r4", Assert.IsType<IrAssign>(indexedLoadUpdate[2]).Destination);
var badIndexedLoad = Assert.Throws<TargetInvocationException>(() =>
InvokePrivate<IReadOnlyList<IrInstruction>>("LiftPairedSingleIndexedLoad", indexedLoad, (3u << 21) | (0u << 16) | (5u << 11), true));
Assert.IsType<InvalidOperationException>(badIndexedLoad.InnerException);
var indexedStore = PpcInstruction.Synthetic(0x80004004, 0, "psq_stx", Array.Empty<PpcOperand>());
var indexedStoreOps = InvokePrivate<IReadOnlyList<IrInstruction>>("LiftPairedSingleIndexedStore", indexedStore, (6u << 21) | (7u << 16) | (8u << 11) | (1u << 10) | (3u << 7), false);
Assert.Equal("addr_psqx_80004004_loc", Assert.IsType<IrBinary>(indexedStoreOps[0]).Destination);
Assert.Equal("PPC_PsqSt", Assert.IsType<IrCall>(indexedStoreOps[1]).Target);
var indexedStoreZeroBase = InvokePrivate<IReadOnlyList<IrInstruction>>("LiftPairedSingleIndexedStore", indexedStore, (6u << 21) | (0u << 16) | (8u << 11), false);
Assert.Equal("addr_psqx_80004004_loc", Assert.IsType<IrAssign>(indexedStoreZeroBase[0]).Destination);
var indexedStoreUpdate = InvokePrivate<IReadOnlyList<IrInstruction>>("LiftPairedSingleIndexedStore", indexedStore, (6u << 21) | (7u << 16) | (8u << 11), true);
Assert.Equal("r7", Assert.IsType<IrAssign>(indexedStoreUpdate[2]).Destination);
var badIndexedStore = Assert.Throws<TargetInvocationException>(() =>
InvokePrivate<IReadOnlyList<IrInstruction>>("LiftPairedSingleIndexedStore", indexedStore, (6u << 21) | (0u << 16) | (8u << 11), true));
Assert.IsType<InvalidOperationException>(badIndexedStore.InnerException);
}
}
@@ -0,0 +1,70 @@
using System;
using System.Linq;
using Translator.Core.Disassembly;
using Translator.Core.Ir;
using Translator.Core.Lifting;
using Xunit;
namespace Translator.Tests;
public class PpcLifterTests
{
[Fact]
public void LiftsMficcrIntoRegisterRead()
{
var lifter = new PpcLifter();
var instruction = PpcInstruction.Synthetic(
0x80000000,
0,
"mficcr",
new PpcOperand[] { new PpcRegisterOperand("r4", 4) });
var lifted = lifter.Lift(new[] { instruction });
var ir = Assert.Single(Assert.Single(lifted).Ir);
var assign = Assert.IsType<IrAssign>(ir);
Assert.Equal("r4", assign.Destination);
Assert.Equal("iccr", assign.Value.RegisterName);
}
[Fact]
public void LiftsCmpwWithBothOperands()
{
var lifter = new PpcLifter();
var instruction = PpcInstruction.Synthetic(
0x80000000,
0x7C032800,
"cmpw",
new PpcOperand[]
{
new PpcConditionRegisterOperand("cr0", 0),
new PpcRegisterOperand("r3", 3),
new PpcRegisterOperand("r5", 5)
});
var lifted = lifter.Lift(new[] { instruction });
var sequence = Assert.Single(lifted).Ir;
var setCr = Assert.IsType<IrSetCrField>(Assert.Single(sequence));
Assert.Equal(0, setCr.FieldIndex);
Assert.False(setCr.IsUnsigned);
Assert.Equal("r3", setCr.Left.RegisterName);
Assert.Equal("r5", setCr.Right.RegisterName);
}
[Fact]
public void UnsupportedInstructionProducesCommentWhenAllowed()
{
var lifter = new PpcLifter();
var instruction = PpcInstruction.Synthetic(
0x801D1FD4,
0x7C00FD2C,
"xo_662",
Array.Empty<PpcOperand>());
var lifted = lifter.Lift(new[] { instruction }, allowUnsupported: true);
var irList = Assert.Single(lifted).Ir;
Assert.NotEmpty(irList);
}
}
@@ -0,0 +1,44 @@
using Translator.Core.Disassembly;
using Xunit;
namespace Translator.Tests;
public class PpcOperandTests
{
private sealed record CustomOperand(string Text) : PpcOperand
{
public override string ToOperandString() => Text;
public override string ToString() => base.ToString();
}
[Fact]
public void FormatsImmediateOperandsInDecimalAndHex()
{
Assert.Equal("42", new PpcImmediateOperand(42).ToOperandString());
Assert.Equal("0x2A", new PpcImmediateOperand(42, PreferHex: true).ToOperandString());
Assert.Equal("-0x2A", new PpcImmediateOperand(-42, PreferHex: true).ToOperandString());
}
[Fact]
public void FormatsDisplacementOperands()
{
Assert.Equal("0(r3)", new PpcDisplacementOperand(0, "r3", 3).ToOperandString());
Assert.Equal("0x20(r4)", new PpcDisplacementOperand(0x20, "r4", 4).ToOperandString());
Assert.Equal("-0x20(r5)", new PpcDisplacementOperand(-0x20, "r5", 5).ToOperandString());
}
[Fact]
public void FormatsBranchAndConditionOperands()
{
Assert.Equal("0x8000ABCD", new PpcBranchTargetOperand(0x8000ABCD).ToOperandString());
Assert.Equal("cr7", new PpcConditionRegisterOperand("cr7", 28).ToOperandString());
Assert.Equal("r12", new PpcRegisterOperand("r12", 12).ToOperandString());
}
[Fact]
public void BaseOperandToStringDelegatesToTypedFormatter()
{
PpcOperand operand = new CustomOperand("custom");
Assert.Equal("custom", operand.ToString());
}
}
@@ -0,0 +1,686 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using Translator.Core.Loading;
using Xunit;
namespace Translator.Tests;
public class PpcRuntimeHelperTests
{
private static (int ExitCode, string Output) RunProcess(string fileName, string arguments, TimeSpan? timeout = null)
{
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
RedirectStandardError = true,
RedirectStandardOutput = true,
UseShellExecute = false
}
};
process.Start();
var stdoutTask = process.StandardOutput.ReadToEndAsync();
var stderrTask = process.StandardError.ReadToEndAsync();
var exitedInTime = timeout.HasValue
? process.WaitForExit((int)timeout.Value.TotalMilliseconds)
: process.WaitForExit(60000);
if (!exitedInTime)
{
try
{
process.Kill(entireProcessTree: true);
}
catch
{
// ignored
}
process.WaitForExit();
var staleOutput = stdoutTask.GetAwaiter().GetResult() + stderrTask.GetAwaiter().GetResult();
throw new TimeoutException($"Process '{fileName}' timed out. Output before kill:\n{staleOutput}");
}
var output = stdoutTask.GetAwaiter().GetResult() + stderrTask.GetAwaiter().GetResult();
return (process.ExitCode, output);
}
private static string CppCompiler()
{
var envCompiler = Environment.GetEnvironmentVariable("CXX");
if (!string.IsNullOrWhiteSpace(envCompiler) && ExecutableExists(envCompiler))
{
return envCompiler;
}
foreach (var candidate in new[] { "g++", "clang++" })
{
if (ExecutableExists(candidate))
{
return candidate;
}
}
throw new FileNotFoundException("No C++ compiler found. Set CXX or install g++/clang++ on PATH.");
}
private static bool ExecutableExists(string fileName)
{
if (Path.IsPathFullyQualified(fileName))
{
return File.Exists(fileName);
}
var path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
var extensions = OperatingSystem.IsWindows()
? (Environment.GetEnvironmentVariable("PATHEXT") ?? ".EXE;.BAT;.CMD").Split(';', StringSplitOptions.RemoveEmptyEntries)
: new[] { string.Empty };
foreach (var directory in path.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
{
foreach (var extension in extensions)
{
var candidate = Path.Combine(directory, fileName);
if (!candidate.EndsWith(extension, StringComparison.OrdinalIgnoreCase))
{
candidate += extension;
}
if (File.Exists(candidate))
{
return true;
}
}
}
return false;
}
private static string RunnerPath(string runnerBasePath)
{
if (OperatingSystem.IsWindows())
{
var exePath = runnerBasePath + ".exe";
if (File.Exists(exePath))
{
return exePath;
}
}
return runnerBasePath;
}
[Fact]
public void FselAndPsSelMatchDolphinForNaNAndSignedZeroControls()
{
var root = ProjectPaths.FindRepositoryRoot();
var tempRoot = Path.Combine(root, "test_output", "ppc_runtime_helpers");
Directory.CreateDirectory(tempRoot);
var harness = new StringBuilder();
harness.AppendLine("#include <cmath>");
harness.AppendLine("#include <cstdint>");
harness.AppendLine("#include <cstring>");
harness.AppendLine("#include <iostream>");
harness.AppendLine("#include <limits>");
harness.AppendLine("#include \"ppc_runtime.h\"");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_Write8(uint8_t) {}");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_Write16(uint16_t) {}");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_Write32(uint32_t) {}");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_WriteFloat(float) {}");
harness.AppendLine("extern \"C\" void DumpRecentPcTrace(size_t) {}");
harness.AppendLine("int main() {");
harness.AppendLine(" const double nan = std::numeric_limits<double>::quiet_NaN();");
harness.AppendLine(" if (PPC_Fsel(nan, 11.0, 22.0) != 11.0) return 1;");
harness.AppendLine(" if (PPC_Fsel(-1.0, 11.0, 22.0) != 11.0) return 2;");
harness.AppendLine(" if (PPC_Fsel(-0.0, 11.0, 22.0) != 22.0) return 3;");
harness.AppendLine(" if (PPC_Fsel(0.0, 11.0, 22.0) != 22.0) return 4;");
harness.AppendLine(" PPC_FPR lhs{}; lhs.paired.ps0 = 10.0f; lhs.paired.ps1 = 20.0f;");
harness.AppendLine(" PPC_FPR rhs{}; rhs.paired.ps0 = -10.0f; rhs.paired.ps1 = -20.0f;");
harness.AppendLine(" PPC_FPR control{}; control.paired.ps0 = std::numeric_limits<float>::quiet_NaN(); control.paired.ps1 = -0.0f;");
harness.AppendLine(" PPC_FPR out{}; out.d = PPC_PsSel(lhs.d, control.d, rhs.d);");
harness.AppendLine(" if (out.paired.ps0 != rhs.paired.ps0) return 5;");
harness.AppendLine(" if (out.paired.ps1 != lhs.paired.ps1) return 6;");
harness.AppendLine(" control.paired.ps0 = 1.0f; control.paired.ps1 = -1.0f;");
harness.AppendLine(" out.d = PPC_PsSel(lhs.d, control.d, rhs.d);");
harness.AppendLine(" if (out.paired.ps0 != lhs.paired.ps0) return 7;");
harness.AppendLine(" if (out.paired.ps1 != rhs.paired.ps1) return 8;");
harness.AppendLine(" std::cout << \"ok\";");
harness.AppendLine(" return 0;");
harness.AppendLine("}");
var harnessPath = Path.Combine(tempRoot, "harness_ppc_runtime_helpers.cpp");
File.WriteAllText(harnessPath, harness.ToString());
var runnerBasePath = Path.Combine(tempRoot, "runner_ppc_runtime_helpers");
var args = TranslatorCppTestHarness.BuildCompileArguments(
root,
tempRoot,
Array.Empty<string>(),
harnessPath,
runnerBasePath,
includeDataSections: false);
var compiler = CppCompiler();
var (compileExitCode, compileOutput) = RunProcess(compiler, args, TimeSpan.FromSeconds(60));
Assert.True(compileExitCode == 0, $"{compiler} failed: {compileOutput}");
var runnerPath = RunnerPath(runnerBasePath);
var (runExitCode, runOutput) = RunProcess(runnerPath, "", TimeSpan.FromSeconds(10));
Assert.True(runExitCode == 0, $"Runner failed ({runExitCode}). Output:\n{runOutput}");
Assert.Equal("ok", runOutput.Trim());
}
[Fact]
public void InlinePsqFloatFastPathPreservesGuestLaneOrder()
{
var root = ProjectPaths.FindRepositoryRoot();
var tempRoot = Path.Combine(root, "test_output", "ppc_runtime_psq_lane_order");
Directory.CreateDirectory(tempRoot);
var harness = new StringBuilder();
harness.AppendLine("#include <cstdint>");
harness.AppendLine("#include <cstring>");
harness.AppendLine("#include <iostream>");
harness.AppendLine("#include \"memory.h\"");
harness.AppendLine("#include \"ppc_runtime.h\"");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_Write8(uint8_t) {}");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_Write16(uint16_t) {}");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_Write32(uint32_t) {}");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_WriteFloat(float) {}");
harness.AppendLine("extern \"C\" void DumpRecentPcTrace(size_t) {}");
harness.AppendLine("static uint32_t Bits(float value) { uint32_t raw = 0; std::memcpy(&raw, &value, sizeof(raw)); return raw; }");
harness.AppendLine("static uint64_t DBits(double value) { uint64_t raw = 0; std::memcpy(&raw, &value, sizeof(raw)); return raw; }");
harness.AppendLine("template <uint32_t GQR> static int CheckKnown(CpuContext& ctx, uint8_t* range, uint32_t base, double source, uint32_t slot) {");
harness.AppendLine(" ctx.gqr[0] = GQR; const uint32_t addr = base + 256 + slot * 96;");
harness.AppendLine(" Memory::Write64(addr, 0x81FE1234A5C35A7Eull + slot);");
harness.AppendLine(" const double pair = PPC_PsqLInline<0u, 0u>(&ctx, addr);");
harness.AppendLine(" if (DBits(pair) != DBits(PPC_PsqLKnownInline<0u, 0u, GQR>(&ctx, addr))) return 1;");
harness.AppendLine(" if (DBits(pair) != DBits(PPC_PsqLKnownStackInline<0u, 0u, GQR>(&ctx, addr))) return 2;");
harness.AppendLine(" if (DBits(pair) != DBits(PPC_PsqLKnownResolvedInline<0u, 0u, GQR>(&ctx, range, addr - base, addr))) return 3;");
harness.AppendLine(" if (DBits(pair) != DBits(PPC_PsqLKnownResolvedInline<0u, 0u, GQR>(&ctx, nullptr, 0, addr))) return 9;");
harness.AppendLine(" const double single = PPC_PsqLInline<1u, 0u>(&ctx, addr);");
harness.AppendLine(" if (DBits(single) != DBits(PPC_PsqLKnownInline<1u, 0u, GQR>(&ctx, addr))) return 4;");
harness.AppendLine(" if (DBits(single) != DBits(PPC_PsqLKnownStackInline<1u, 0u, GQR>(&ctx, addr))) return 5;");
harness.AppendLine(" if (DBits(single) != DBits(PPC_PsqLKnownResolvedInline<1u, 0u, GQR>(&ctx, range, addr - base, addr))) return 6;");
harness.AppendLine(" if (DBits(single) != DBits(PPC_PsqLKnownResolvedInline<1u, 0u, GQR>(&ctx, nullptr, 0, addr))) return 10;");
harness.AppendLine(" const uint32_t a = addr + 16, b = addr + 32, c = addr + 48, d = addr + 64, e = addr + 80;");
harness.AppendLine(" Memory::Write64(a, ~0ull); Memory::Write64(b, ~0ull); Memory::Write64(c, ~0ull); Memory::Write64(d, ~0ull); Memory::Write64(e, ~0ull);");
harness.AppendLine(" PPC_PsqStInline<0u, 0u>(&ctx, a, source);");
harness.AppendLine(" PPC_PsqStKnownInline<0u, 0u, GQR>(&ctx, b, source);");
harness.AppendLine(" PPC_PsqStKnownStackInline<0u, 0u, GQR>(&ctx, c, source);");
harness.AppendLine(" PPC_PsqStKnownResolvedInline<0u, 0u, GQR>(&ctx, range, d - base, d, source);");
harness.AppendLine(" PPC_PsqStKnownResolvedInline<0u, 0u, GQR>(&ctx, nullptr, 0, e, source);");
harness.AppendLine(" if (Memory::Read64(a) != Memory::Read64(b) || Memory::Read64(a) != Memory::Read64(c) || Memory::Read64(a) != Memory::Read64(d) || Memory::Read64(a) != Memory::Read64(e)) return 7;");
harness.AppendLine(" Memory::Write64(a, ~0ull); Memory::Write64(b, ~0ull); Memory::Write64(c, ~0ull); Memory::Write64(d, ~0ull); Memory::Write64(e, ~0ull);");
harness.AppendLine(" PPC_PsqStInline<1u, 0u>(&ctx, a, source);");
harness.AppendLine(" PPC_PsqStKnownInline<1u, 0u, GQR>(&ctx, b, source);");
harness.AppendLine(" PPC_PsqStKnownStackInline<1u, 0u, GQR>(&ctx, c, source);");
harness.AppendLine(" PPC_PsqStKnownResolvedInline<1u, 0u, GQR>(&ctx, range, d - base, d, source);");
harness.AppendLine(" PPC_PsqStKnownResolvedInline<1u, 0u, GQR>(&ctx, nullptr, 0, e, source);");
harness.AppendLine(" if (Memory::Read64(a) != Memory::Read64(b) || Memory::Read64(a) != Memory::Read64(c) || Memory::Read64(a) != Memory::Read64(d) || Memory::Read64(a) != Memory::Read64(e)) return 8;");
harness.AppendLine(" return 0;");
harness.AppendLine("}");
harness.AppendLine("int main() {");
harness.AppendLine(" Memory::Init(Memory::Config::WiiDefaults());");
harness.AppendLine(" CpuContext ctx{};");
harness.AppendLine(" ctx.gpr[1] = 0x817FF000u;");
harness.AppendLine(" ctx.gqr[0] = 0;");
harness.AppendLine(" CpuContextScope scope(&ctx);");
harness.AppendLine(" const auto RefLoadPair = [](uint64_t v) { return (static_cast<uint64_t>(PpcLoadPsqFloatBitsInline(static_cast<uint32_t>(v >> 32))) << 32) | PpcLoadPsqFloatBitsInline(static_cast<uint32_t>(v)); };");
harness.AppendLine(" const auto RefStorePair = [](uint64_t v) { return (static_cast<uint64_t>(PpcStorePsqFloatBitsInline(static_cast<uint32_t>(v >> 32))) << 32) | PpcStorePsqFloatBitsInline(static_cast<uint32_t>(v)); };");
harness.AppendLine(" const uint32_t floatEdges[] = { 0u, 0x80000000u, 1u, 0x007FFFFFu, 0x00800000u, 0x7F7FFFFFu, 0x7F800000u, 0x7F800001u, 0x7FC00000u, 0xFF800001u }; ");
harness.AppendLine(" for (uint32_t hi : floatEdges) for (uint32_t lo : floatEdges) { const uint64_t v = (static_cast<uint64_t>(hi) << 32) | lo; if (PpcLoadPairPsqFloatBitsPackedInline(v) != RefLoadPair(v)) return 90; if (PpcStorePairPsqFloatBitsPackedInline(v) != RefStorePair(v)) return 91; }");
harness.AppendLine(" uint32_t pairState = 0x7E57A11Du; for (uint32_t i = 0; i < 1000000u; ++i) { pairState = pairState * 1664525u + 1013904223u; const uint64_t hi = pairState; pairState = pairState * 1664525u + 1013904223u; const uint64_t v = (hi << 32) | pairState; if (PpcLoadPairPsqFloatBitsPackedInline(v) != RefLoadPair(v)) return 92; if (PpcStorePairPsqFloatBitsPackedInline(v) != RefStorePair(v)) return 93; }");
harness.AppendLine(" constexpr uint32_t base = 0x80010000u;");
harness.AppendLine(" Memory::Write32(base, Bits(12.5f));");
harness.AppendLine(" Memory::Write32(base + 4, Bits(-7.25f));");
harness.AppendLine(" auto* range = MemoryInline::ResolveRangeHost(base, 0, 2048, true, true);");
harness.AppendLine(" if (!range) return 20;");
harness.AppendLine(" PPC_FPR loaded{};");
harness.AppendLine(" loaded.d = PPC_PsqLInline<0u, 0u>(base);");
harness.AppendLine(" if (DBits(loaded.d) != DBits(PPC_PsqLResolvedInline<0u, 0u>(&ctx, range, 0, base))) return 21;");
harness.AppendLine(" if (loaded.paired.ps0 != 12.5f) return 1;");
harness.AppendLine(" if (loaded.paired.ps1 != -7.25f) return 2;");
harness.AppendLine(" PPC_FPR source{};");
harness.AppendLine(" source.paired.ps0 = 3.5f;");
harness.AppendLine(" source.paired.ps1 = -9.75f;");
harness.AppendLine(" PPC_PsqStInline<0u, 0u>(base + 8, source.d);");
harness.AppendLine(" if (Memory::Read32(base + 8) != Bits(3.5f)) return 3;");
harness.AppendLine(" if (Memory::Read32(base + 12) != Bits(-9.75f)) return 4;");
harness.AppendLine(" PPC_PsqStResolvedInline<0u, 0u>(&ctx, range, 24, base + 24, source.d);");
harness.AppendLine(" if (Memory::Read32(base + 24) != Bits(3.5f) || Memory::Read32(base + 28) != Bits(-9.75f)) return 22;");
harness.AppendLine(" Memory::Write32(base + 16, 0xDEADBEEFu);");
harness.AppendLine(" PPC_PsqStInline<1u, 0u>(base + 16, source.d);");
harness.AppendLine(" if (Memory::Read32(base + 16) != Bits(3.5f)) return 5;");
harness.AppendLine(" Memory::Write32(base + 20, Bits(22.0f));");
harness.AppendLine(" loaded.d = PPC_PsqLInline<1u, 0u>(base + 20);");
harness.AppendLine(" if (loaded.paired.ps0 != 22.0f) return 6;");
harness.AppendLine(" if (loaded.paired.ps1 != 1.0f) return 7;");
harness.AppendLine(" const uint32_t gqrs[] = { 0x00040004u, 0x00050005u, 0x00060006u, 0x00070007u, 0x3D043D04u };");
harness.AppendLine(" for (uint32_t n = 0; n < 5; ++n) {");
harness.AppendLine(" ctx.gqr[0] = gqrs[n];");
harness.AppendLine(" const uint32_t input = base + 40 + n * 8;");
harness.AppendLine(" Memory::Write64(input, 0x81FE1234A5C35A7Eu + n);");
harness.AppendLine(" const double ordinary = PPC_PsqLInline<0u, 0u>(&ctx, input);");
harness.AppendLine(" const double resolved = PPC_PsqLResolvedInline<0u, 0u>(&ctx, range, 40 + n * 8, input);");
harness.AppendLine(" if (DBits(ordinary) != DBits(resolved)) return 30 + n;");
harness.AppendLine(" const uint32_t a = base + 88 + n * 8; const uint32_t b = base + 128 + n * 8;");
harness.AppendLine(" Memory::Write64(a, 0xCCCCCCCCCCCCCCCCull); Memory::Write64(b, 0xCCCCCCCCCCCCCCCCull);");
harness.AppendLine(" PPC_PsqStInline<0u, 0u>(&ctx, a, source.d);");
harness.AppendLine(" PPC_PsqStResolvedInline<0u, 0u>(&ctx, range, 128 + n * 8, b, source.d);");
harness.AppendLine(" if (Memory::Read64(a) != Memory::Read64(b)) return 40 + n;");
harness.AppendLine(" }");
harness.AppendLine(" if (const int e = CheckKnown<0x00000000u>(ctx, range, base, source.d, 0)) return 100 + e;");
harness.AppendLine(" if (const int e = CheckKnown<0x00040004u>(ctx, range, base, source.d, 1)) return 110 + e;");
harness.AppendLine(" if (const int e = CheckKnown<0x00050005u>(ctx, range, base, source.d, 2)) return 120 + e;");
harness.AppendLine(" if (const int e = CheckKnown<0x00060006u>(ctx, range, base, source.d, 3)) return 130 + e;");
harness.AppendLine(" if (const int e = CheckKnown<0x00070007u>(ctx, range, base, source.d, 4)) return 140 + e;");
harness.AppendLine(" if (const int e = CheckKnown<0x3D043D04u>(ctx, range, base, source.d, 5)) return 150 + e;");
harness.AppendLine(" if (const int e = CheckKnown<0x05050505u>(ctx, range, base, source.d, 6)) return 160 + e;");
harness.AppendLine(" if (const int e = CheckKnown<0x21062106u>(ctx, range, base, source.d, 7)) return 170 + e;");
harness.AppendLine(" if (const int e = CheckKnown<0x0C070C07u>(ctx, range, base, source.d, 8)) return 180 + e;");
harness.AppendLine(" std::cout << \"ok\";");
harness.AppendLine(" return 0;");
harness.AppendLine("}");
var harnessPath = Path.Combine(tempRoot, "harness_psq_lane_order.cpp");
File.WriteAllText(harnessPath, harness.ToString());
var runnerBasePath = Path.Combine(tempRoot, "runner_psq_lane_order");
var args = TranslatorCppTestHarness.BuildCompileArguments(
root,
tempRoot,
Array.Empty<string>(),
harnessPath,
runnerBasePath,
includeDataSections: false);
var compiler = CppCompiler();
var (compileExitCode, compileOutput) = RunProcess(compiler, args, TimeSpan.FromSeconds(60));
Assert.True(compileExitCode == 0, $"{compiler} failed: {compileOutput}");
var runnerPath = RunnerPath(runnerBasePath);
var (runExitCode, runOutput) = RunProcess(runnerPath, "", TimeSpan.FromSeconds(10));
Assert.True(runExitCode == 0, $"Runner failed ({runExitCode}). Output:\n{runOutput}");
Assert.Equal("ok", runOutput.Trim());
}
[Fact]
public void KnownU8Scale61PairedStoreMatchesExactQuantizationOnFastAndColdPaths()
{
var root = ProjectPaths.FindRepositoryRoot();
var tempRoot = Path.Combine(root, "test_output", "ppc_runtime_psq_u8_scale61");
Directory.CreateDirectory(tempRoot);
var harness = new StringBuilder();
harness.AppendLine("#include <cmath>");
harness.AppendLine("#include <cstdint>");
harness.AppendLine("#include <cstring>");
harness.AppendLine("#include <iostream>");
harness.AppendLine("#include <limits>");
harness.AppendLine("#include \"memory.h\"");
harness.AppendLine("#include \"ppc_runtime.h\"");
harness.AppendLine("static uint16_t g_fifo16 = 0; static uint32_t g_fifo16Count = 0;");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_Write8(uint8_t) {}");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_Write16(uint16_t value) { g_fifo16 = value; ++g_fifo16Count; }");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_Write32(uint32_t) {}");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_WriteFloat(float) {}");
harness.AppendLine("extern \"C\" void DumpRecentPcTrace(size_t) {}");
harness.AppendLine("static uint16_t Reference(double value) {");
harness.AppendLine(" PPC_FPR fpr{}; fpr.d = value;");
harness.AppendLine(" return static_cast<uint16_t>((static_cast<uint16_t>(PpcQuantizePsqU8Scale61Inline(fpr.paired.ps0)) << 8) | PpcQuantizePsqU8Scale61Inline(fpr.paired.ps1));");
harness.AppendLine("}");
harness.AppendLine("static float FromBits(uint32_t bits) { float value; std::memcpy(&value, &bits, sizeof(value)); return value; }");
harness.AppendLine("int main() {");
harness.AppendLine(" const float edges[] = { -std::numeric_limits<float>::infinity(), -2040.0f, -1.0f, -0.0f, 0.0f,");
harness.AppendLine(" std::nextafter(0.0f, 1.0f), 7.999f, 8.0f, 2039.0f, std::nextafter(2040.0f, 0.0f), 2040.0f,");
harness.AppendLine(" std::numeric_limits<float>::infinity(), std::numeric_limits<float>::quiet_NaN(), FromBits(0x7F800001u) };");
harness.AppendLine(" for (float ps0 : edges) for (float ps1 : edges) {");
harness.AppendLine(" PPC_FPR pair{}; pair.paired.ps0 = ps0; pair.paired.ps1 = ps1;");
harness.AppendLine(" if (PpcQuantizePairPsqU8Scale61PackedInline(pair.d) != Reference(pair.d)) return 1;");
harness.AppendLine(" }");
harness.AppendLine(" uint32_t state = 0xC001D00Du;");
harness.AppendLine(" for (uint32_t i = 0; i < 1000000u; ++i) {");
harness.AppendLine(" state = state * 1664525u + 1013904223u; const uint32_t a = state;");
harness.AppendLine(" state = state * 1664525u + 1013904223u; const uint32_t b = state;");
harness.AppendLine(" PPC_FPR pair{}; pair.paired.ps0 = FromBits(a); pair.paired.ps1 = FromBits(b);");
harness.AppendLine(" if (PpcQuantizePairPsqU8Scale61PackedInline(pair.d) != Reference(pair.d)) return 2;");
harness.AppendLine(" }");
harness.AppendLine(" Memory::Init(Memory::Config::WiiDefaults()); CpuContext ctx{}; CpuContextScope scope(&ctx);");
harness.AppendLine(" constexpr uint32_t base = 0x80010000u; PPC_FPR pair{}; pair.paired.ps0 = 2039.0f; pair.paired.ps1 = 15.9f;");
harness.AppendLine(" const uint16_t expected = Reference(pair.d);");
harness.AppendLine(" PPC_PsqStKnownInline<0u, 6u, 0x3D043D04u>(&ctx, base, pair.d);");
harness.AppendLine(" if (Memory::Read16(base) != expected) return 3;");
harness.AppendLine(" const uint32_t page = base >> MemoryInline::kPageShift;");
harness.AppendLine(" const uintptr_t savedBias = MemoryInline::g_fullWritablePageBias[page];");
harness.AppendLine(" if (savedBias == 0) return 4;");
harness.AppendLine(" MemoryInline::g_fullWritablePageBias[page] = 0;");
harness.AppendLine(" PPC_PsqStKnownInline<0u, 6u, 0x3D043D04u>(&ctx, base + 2, pair.d);");
harness.AppendLine(" MemoryInline::g_fullWritablePageBias[page] = savedBias;");
harness.AppendLine(" if (Memory::Read16(base + 2) != expected) return 5;");
harness.AppendLine(" uint8_t* range = MemoryInline::ResolveRangeHost(base, 0, 64, false, true);");
harness.AppendLine(" if (!range) return 6;");
harness.AppendLine(" PPC_PsqStKnownResolvedInline<0u, 6u, 0x3D043D04u>(&ctx, range, 4, base + 4, pair.d);");
harness.AppendLine(" if (Memory::Read16(base + 4) != expected) return 7;");
harness.AppendLine(" PPC_PsqStKnownStackInline<0u, 6u, 0x3D043D04u>(&ctx, base + 6, pair.d);");
harness.AppendLine(" if (Memory::Read16(base + 6) != expected) return 8;");
harness.AppendLine(" PPC_FPR exceptional{}; exceptional.paired.ps0 = std::numeric_limits<float>::quiet_NaN(); exceptional.paired.ps1 = std::numeric_limits<float>::infinity();");
harness.AppendLine(" PPC_PsqStKnownInline<0u, 6u, 0x3D043D04u>(&ctx, 0xCC008000u, exceptional.d);");
harness.AppendLine(" if (g_fifo16Count != 1 || g_fifo16 != Reference(exceptional.d)) return 9;");
harness.AppendLine(" std::cout << \"ok\"; return 0;");
harness.AppendLine("}");
var harnessPath = Path.Combine(tempRoot, "harness_psq_u8_scale61.cpp");
File.WriteAllText(harnessPath, harness.ToString());
var runnerBasePath = Path.Combine(tempRoot, "runner_psq_u8_scale61");
var args = TranslatorCppTestHarness.BuildCompileArguments(
root,
tempRoot,
Array.Empty<string>(),
harnessPath,
runnerBasePath,
includeDataSections: false);
var compiler = CppCompiler();
var (compileExitCode, compileOutput) = RunProcess(compiler, args, TimeSpan.FromSeconds(60));
Assert.True(compileExitCode == 0, $"{compiler} failed: {compileOutput}");
var runnerPath = RunnerPath(runnerBasePath);
var (runExitCode, runOutput) = RunProcess(runnerPath, "", TimeSpan.FromSeconds(10));
Assert.True(runExitCode == 0, $"Runner failed ({runExitCode}). Output:\n{runOutput}");
Assert.Equal("ok", runOutput.Trim());
}
[Fact]
public void InlinePairedSingleArithmeticPreservesLaneOrder()
{
var root = ProjectPaths.FindRepositoryRoot();
var tempRoot = Path.Combine(root, "test_output", "ppc_runtime_ps_arithmetic");
Directory.CreateDirectory(tempRoot);
var harness = new StringBuilder();
harness.AppendLine("#include <cmath>");
harness.AppendLine("#include <cstdint>");
harness.AppendLine("#include <iostream>");
harness.AppendLine("#include <limits>");
harness.AppendLine("#include \"ppc_runtime.h\"");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_Write8(uint8_t) {}");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_Write16(uint16_t) {}");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_Write32(uint32_t) {}");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_WriteFloat(float) {}");
harness.AppendLine("extern \"C\" void DumpRecentPcTrace(size_t) {}");
harness.AppendLine("static bool Near(float a, float b) { return std::fabs(a - b) < 0.0001f; }");
harness.AppendLine("static int Check(PPC_FPR f, float ps0, float ps1, int code) {");
harness.AppendLine(" if (!Near(f.paired.ps0, ps0)) return code;");
harness.AppendLine(" if (!Near(f.paired.ps1, ps1)) return code + 1;");
harness.AppendLine(" return 0;");
harness.AppendLine("}");
harness.AppendLine("static uint64_t Bits(double value) {");
harness.AppendLine(" uint64_t bits = 0;");
harness.AppendLine(" std::memcpy(&bits, &value, sizeof(bits));");
harness.AppendLine(" return bits;");
harness.AppendLine("}");
harness.AppendLine("static double FromBits(uint64_t bits) {");
harness.AppendLine(" double value = 0.0;");
harness.AppendLine(" std::memcpy(&value, &bits, sizeof(value));");
harness.AppendLine(" return value;");
harness.AppendLine("}");
harness.AppendLine("static float FloatFromBits(uint32_t bits) {");
harness.AppendLine(" float value = 0.0f;");
harness.AppendLine(" std::memcpy(&value, &bits, sizeof(value));");
harness.AppendLine(" return value;");
harness.AppendLine("}");
harness.AppendLine("int main() {");
harness.AppendLine(" PPC_FPR a{}; a.paired.ps0 = 2.0f; a.paired.ps1 = -4.0f;");
harness.AppendLine(" PPC_FPR b{}; b.paired.ps0 = 8.0f; b.paired.ps1 = 0.5f;");
harness.AppendLine(" PPC_FPR c{}; c.paired.ps0 = 3.0f; c.paired.ps1 = 10.0f;");
harness.AppendLine(" PPC_FPR out{};");
harness.AppendLine(" out.d = PPC_PsAddInline(a.d, b.d); if (int rc = Check(out, 10.0f, -3.5f, 1)) return rc;");
harness.AppendLine(" out.d = PPC_PsSubInline(a.d, b.d); if (int rc = Check(out, -6.0f, -4.5f, 3)) return rc;");
harness.AppendLine(" out.d = PPC_PsDivInline(a.d, b.d); if (int rc = Check(out, 0.25f, -8.0f, 21)) return rc;");
harness.AppendLine(" out.d = PPC_PsMulInline(a.d, b.d); if (int rc = Check(out, 16.0f, -2.0f, 5)) return rc;");
harness.AppendLine(" out.d = PPC_PsMaddInline(a.d, b.d, c.d); if (int rc = Check(out, 19.0f, 8.0f, 7)) return rc;");
harness.AppendLine(" out.d = PPC_PsMsubInline(a.d, b.d, c.d); if (int rc = Check(out, 13.0f, -12.0f, 9)) return rc;");
harness.AppendLine(" out.d = PPC_PsNmsubInline(a.d, b.d, c.d); if (int rc = Check(out, -13.0f, 12.0f, 11)) return rc;");
harness.AppendLine(" out.d = PPC_PsMuls0Inline(a.d, b.d); if (int rc = Check(out, 16.0f, -32.0f, 13)) return rc;");
harness.AppendLine(" out.d = PPC_PsMuls1Inline(a.d, b.d); if (int rc = Check(out, 1.0f, -2.0f, 15)) return rc;");
harness.AppendLine(" if (PPC_Divwu(0x12345678u, 0u) != 0u) return 17;");
harness.AppendLine(" if (PPC_Divw(123, 0) != 0) return 18;");
harness.AppendLine(" if (PPC_Divw(-123, 0) != -1) return 19;");
harness.AppendLine(" if (PPC_Divw(INT32_MIN, -1) != -1) return 20;");
harness.AppendLine(" // Exact Gekko estimate bits, including interpolation and exponent parity.");
harness.AppendLine(" if (Bits(PPC_Frsqrte(1.0)) != 0x3FEFFE8000000000ULL) return 23;");
harness.AppendLine(" if (Bits(PPC_Frsqrte(2.0)) != 0x3FE69FA000000000ULL) return 24;");
harness.AppendLine(" if (Bits(PPC_Frsqrte(4.0)) != 0x3FDFFE8000000000ULL) return 25;");
harness.AppendLine(" if (Bits(PPC_Frsqrte(3.14159265358979323846)) != 0x3FE20DD740000000ULL) return 26;");
harness.AppendLine(" // Subnormal normalization must extend the encoded exponent below zero.");
harness.AppendLine(" if (Bits(PPC_Frsqrte(FromBits(0x0000000000000001ULL))) != 0x617FFE8000000000ULL) return 27;");
harness.AppendLine(" if (Bits(PPC_Frsqrte(FromBits(0x000FFFFFFFFFFFFFULL))) != 0x5FE000082C000000ULL) return 28;");
harness.AppendLine(" if (Bits(PPC_Frsqrte(FromBits(0x0010000000000000ULL))) != 0x5FDFFE8000000000ULL) return 29;");
harness.AppendLine(" // These values drive the architectural zero/invalid/NaN paths; the helper's");
harness.AppendLine(" // contract is result bits, while instruction emission owns FPSCR effects.");
harness.AppendLine(" if (Bits(PPC_Frsqrte(0.0)) != 0x7FF0000000000000ULL) return 30;");
harness.AppendLine(" if (Bits(PPC_Frsqrte(-0.0)) != 0xFFF0000000000000ULL) return 31;");
harness.AppendLine(" if (Bits(PPC_Frsqrte(std::numeric_limits<double>::infinity())) != 0x0000000000000000ULL) return 32;");
harness.AppendLine(" if (Bits(PPC_Frsqrte(-std::numeric_limits<double>::infinity())) != 0x7FF8000000000000ULL) return 33;");
harness.AppendLine(" if (Bits(PPC_Frsqrte(-4.0)) != 0x7FF8000000000000ULL) return 34;");
harness.AppendLine(" if (Bits(PPC_Frsqrte(FromBits(0x7FF0000000000001ULL))) != 0x7FF8000000000001ULL) return 35;");
harness.AppendLine(" if (Bits(PPC_Frsqrte(FromBits(0xFFF0000000000123ULL))) != 0xFFF8000000000123ULL) return 36;");
harness.AppendLine(" if (Bits(PPC_Frsqrte(FromBits(0x7FF8000000001234ULL))) != 0x7FF8000000001234ULL) return 37;");
harness.AppendLine(" // Paired multiply-add has one float32 rounding point, not a rounded");
harness.AppendLine(" // float multiply followed by a second rounded add.");
harness.AppendLine(" PPC_FPR edgeA{}; edgeA.paired.ps0 = FloatFromBits(0x42480000u); edgeA.paired.ps1 = edgeA.paired.ps0;");
harness.AppendLine(" PPC_FPR edgeC{}; edgeC.paired.ps0 = FloatFromBits(0xBC88CC38u); edgeC.paired.ps1 = edgeC.paired.ps0;");
harness.AppendLine(" PPC_FPR edgeB{}; edgeB.paired.ps0 = FloatFromBits(0x1B1C72A0u); edgeB.paired.ps1 = edgeB.paired.ps0;");
harness.AppendLine(" out.d = PPC_PsMaddInline(edgeA.d, edgeC.d, edgeB.d);");
harness.AppendLine(" if (PpcBitCastToU32Inline(out.paired.ps0) != 0xBF55BF17u || PpcBitCastToU32Inline(out.paired.ps1) != 0xBF55BF17u) return 38;");
harness.AppendLine(" if (PpcBitCastToU32Inline(static_cast<float>(PpcFmaddsInline(edgeA.paired.ps0, edgeC.paired.ps0, edgeB.paired.ps0))) != 0xBF55BF17u) return 39;");
harness.AppendLine(" // Double fused arithmetic must retain the product error cancelled by B.");
harness.AppendLine(" const double fmaA = 1.0 + 0x1p-27;");
harness.AppendLine(" const double fmaC = 1.0 - 0x1p-27;");
harness.AppendLine(" if (Bits(PpcFmaddInline(fmaA, fmaC, -1.0)) != 0xBC90000000000000ULL) return 40;");
harness.AppendLine(" // Exact Broadway reciprocal estimates, including lane replication.");
harness.AppendLine(" PPC_FPR estimateIn{}; estimateIn.paired.ps0 = 1.0f; estimateIn.paired.ps1 = 2.0f;");
harness.AppendLine(" if (Bits(PPC_Fres(estimateIn.d)) != 0x3F7FF8003F7FF800ULL) return 41;");
harness.AppendLine(" if (Bits(PPC_PsRes(estimateIn.d)) != 0x3F7FF8003EFFF800ULL) return 42;");
harness.AppendLine(" estimateIn.paired.ps0 = 1.0f; estimateIn.paired.ps1 = 4.0f;");
harness.AppendLine(" if (Bits(PPC_PsRsqrte(estimateIn.d)) != 0x3F7FF4003EFFF400ULL) return 43;");
harness.AppendLine(" estimateIn.paired.ps0 = -0.0f; estimateIn.paired.ps1 = -4.0f;");
harness.AppendLine(" if (Bits(PPC_PsRsqrte(estimateIn.d)) != 0xFF8000007FC00000ULL) return 44;");
harness.AppendLine(" // stfs narrows the FPR payload with Gekko's bit conversion, not a host cast.");
harness.AppendLine(" if (MemoryInline::ConvertPpcDoubleToSingleBits(1.0 + 0x1.8p-23) != 0x3F800001u) return 45;");
harness.AppendLine(" if (MemoryInline::ConvertPpcDoubleToSingleBits(1.0 + 0x1.8p-24) != 0x3F800000u) return 46;");
harness.AppendLine(" // FPSCR[NI] flushes single subnormal results while preserving their sign.");
harness.AppendLine(" CpuContext niCpu{}; niCpu.fpscr = 4u;");
harness.AppendLine(" { CpuContextScope niScope(&niCpu);");
harness.AppendLine(" if (PpcBitCastToU32Inline(PpcForceSingleValueInline(0x1p-149)) != 0u) return 47;");
harness.AppendLine(" if (PpcBitCastToU32Inline(PpcForceSingleValueInline(-0x1p-149)) != 0x80000000u) return 48;");
harness.AppendLine(" }");
harness.AppendLine(" std::cout << \"ok\";");
harness.AppendLine(" return 0;");
harness.AppendLine("}");
var harnessPath = Path.Combine(tempRoot, "harness_ps_arithmetic.cpp");
File.WriteAllText(harnessPath, harness.ToString());
var runnerBasePath = Path.Combine(tempRoot, "runner_ps_arithmetic");
var args = TranslatorCppTestHarness.BuildCompileArguments(
root,
tempRoot,
Array.Empty<string>(),
harnessPath,
runnerBasePath,
includeDataSections: false);
var compiler = CppCompiler();
var (compileExitCode, compileOutput) = RunProcess(compiler, args, TimeSpan.FromSeconds(60));
Assert.True(compileExitCode == 0, $"{compiler} failed: {compileOutput}");
var runnerPath = RunnerPath(runnerBasePath);
var (runExitCode, runOutput) = RunProcess(runnerPath, "", TimeSpan.FromSeconds(10));
Assert.True(runExitCode == 0, $"Runner failed ({runExitCode}). Output:\n{runOutput}");
Assert.Equal("ok", runOutput.Trim());
}
[Fact]
public void NonvolatileFprGuardPreservesPackedPairedAndScalarFprs()
{
var root = ProjectPaths.FindRepositoryRoot();
var tempRoot = Path.Combine(root, "test_output", "ppc_nonvolatile_fpr_guard");
Directory.CreateDirectory(tempRoot);
var harness = new StringBuilder();
harness.AppendLine("#include <cstdint>");
harness.AppendLine("#include <iostream>");
harness.AppendLine("#include \"abi_bridge.h\"");
harness.AppendLine("extern \"C\" void ClobberNonvolatileFprs(CpuContext* ctx) {");
harness.AppendLine(" ctx->fpr[14].raw = 0x0102030405060708ull;");
harness.AppendLine(" ctx->fpr[31].raw = 0x8877665544332211ull;");
harness.AppendLine("}");
harness.AppendLine("int main() {");
harness.AppendLine(" CpuContext ctx{};");
harness.AppendLine(" PPC_FPR saved14{}; saved14.paired.ps0 = 12.5f; saved14.paired.ps1 = -7.25f;");
harness.AppendLine(" PPC_FPR saved31{}; saved31.d = 3.141592653589793;");
harness.AppendLine(" PPC_FPR volatile13{}; volatile13.paired.ps0 = 1.0f; volatile13.paired.ps1 = 2.0f;");
harness.AppendLine(" ctx.fpr[14] = saved14;");
harness.AppendLine(" ctx.fpr[31] = saved31;");
harness.AppendLine(" ctx.fpr[13] = volatile13;");
harness.AppendLine(" {");
harness.AppendLine(" PpcNonvolatileFprGuard guard(&ctx);");
harness.AppendLine(" ctx.fpr[14].paired.ps0 = 1000.0f;");
harness.AppendLine(" ctx.fpr[14].paired.ps1 = 2000.0f;");
harness.AppendLine(" ctx.fpr[31].raw = 0x123456789ABCDEF0ull;");
harness.AppendLine(" ctx.fpr[13].paired.ps0 = 9.0f;");
harness.AppendLine(" ctx.fpr[13].paired.ps1 = 10.0f;");
harness.AppendLine(" }");
harness.AppendLine(" if (ctx.fpr[14].raw != saved14.raw) return 1;");
harness.AppendLine(" if (ctx.fpr[31].raw != saved31.raw) return 2;");
harness.AppendLine(" if (ctx.fpr[13].raw == volatile13.raw) return 3;");
harness.AppendLine(" {");
harness.AppendLine(" PpcNonvolatileFprGuard guard(&ctx, 1u << 31);");
harness.AppendLine(" ctx.fpr[14].raw = 0x1111111111111111ull;");
harness.AppendLine(" ctx.fpr[31].raw = 0x2222222222222222ull;");
harness.AppendLine(" }");
harness.AppendLine(" if (ctx.fpr[14].raw == saved14.raw) return 4;");
harness.AppendLine(" if (ctx.fpr[31].raw != saved31.raw) return 5;");
harness.AppendLine(" ctx.fpr[14] = saved14;");
harness.AppendLine(" TranslatedFunctionInfo info{};");
harness.AppendLine(" info.entryPoint = reinterpret_cast<void*>(&ClobberNonvolatileFprs);");
harness.AppendLine(" info.nonvolatileFprWriteMask = 1u << 31;");
harness.AppendLine(" if (!TryDispatchResolvedCpuTarget(&info, &ctx)) return 6;");
harness.AppendLine(" if (ctx.fpr[14].raw == saved14.raw) return 7;");
harness.AppendLine(" if (ctx.fpr[31].raw != saved31.raw) return 8;");
harness.AppendLine(" ctx.fpr[14] = saved14;");
harness.AppendLine(" ctx.fpr[31] = saved31;");
harness.AppendLine(" info.nonvolatileFprWriteMask = kPpcAllNonvolatileFprMask;");
harness.AppendLine(" if (!TryDispatchResolvedCpuTarget(&info, &ctx)) return 9;");
harness.AppendLine(" if (ctx.fpr[14].raw != saved14.raw) return 10;");
harness.AppendLine(" if (ctx.fpr[31].raw != saved31.raw) return 11;");
harness.AppendLine(" info.nonvolatileFprWriteMask = 0;");
harness.AppendLine(" if (!TryDispatchResolvedCpuTarget(&info, &ctx)) return 12;");
harness.AppendLine(" if (ctx.fpr[14].raw == saved14.raw) return 13;");
harness.AppendLine(" if (ctx.fpr[31].raw == saved31.raw) return 14;");
harness.AppendLine(" std::cout << \"ok\";");
harness.AppendLine(" return 0;");
harness.AppendLine("}");
var harnessPath = Path.Combine(tempRoot, "harness_nonvolatile_fpr_guard.cpp");
File.WriteAllText(harnessPath, harness.ToString());
var runnerBasePath = Path.Combine(tempRoot, "runner_nonvolatile_fpr_guard");
var args =
"-std=c++17 " +
$"\"{harnessPath}\" " +
$"-I\"{Path.Combine(root, "runtime", "include")}\" " +
$"-o \"{runnerBasePath}\"";
var compiler = CppCompiler();
var (compileExitCode, compileOutput) = RunProcess(compiler, args, TimeSpan.FromSeconds(60));
Assert.True(compileExitCode == 0, $"{compiler} failed: {compileOutput}");
var runnerPath = RunnerPath(runnerBasePath);
var (runExitCode, runOutput) = RunProcess(runnerPath, "", TimeSpan.FromSeconds(10));
Assert.True(runExitCode == 0, $"Runner failed ({runExitCode}). Output:\n{runOutput}");
Assert.Equal("ok", runOutput.Trim());
}
[Fact]
public void DirectCpuDispatchSeesModOverrideRegisteredAfterFirstBaseCall()
{
var root = ProjectPaths.FindRepositoryRoot();
var tempRoot = Path.Combine(root, "test_output", "ppc_direct_mod_override_refresh");
Directory.CreateDirectory(tempRoot);
var harness = new StringBuilder();
harness.AppendLine("#include <cstdint>");
harness.AppendLine("#include <iostream>");
harness.AppendLine("#include <utility>");
harness.AppendLine("#include \"abi_bridge.h\"");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_Write8(uint8_t) {}");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_Write16(uint16_t) {}");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_Write32(uint32_t) {}");
harness.AppendLine("extern \"C\" void GX_HLE_FIFO_WriteFloat(float) {}");
harness.AppendLine("extern \"C\" void BaseTranslated(CpuContext* ctx) { ctx->gpr[3] = 0xBACE0001u; }");
harness.AppendLine("extern \"C\" void ModTranslated(CpuContext* ctx) { ctx->gpr[3] = 0xC0DE0002u; }");
harness.AppendLine("template <> struct KnownTranslatedCpuCall<0x81234568u> {");
harness.AppendLine(" static constexpr bool kAvailable = true;");
harness.AppendLine(" static constexpr uint32_t kNonvolatileFprWriteMask = 0;");
harness.AppendLine(" static constexpr bool kMayBeOverridden = true;");
harness.AppendLine(" static constexpr void (*Entry)(CpuContext*) = &BaseTranslated;");
harness.AppendLine("};");
harness.AppendLine("static void Register(uint32_t priority, FunctionKind kind, const char* name, void (*entry)(CpuContext*)) {");
harness.AppendLine(" TranslatedFunctionInfo info{};");
harness.AppendLine(" info.address = 0x81234568u;");
harness.AppendLine(" info.name = name;");
harness.AppendLine(" info.kind = kind;");
harness.AppendLine(" info.priority = priority;");
harness.AppendLine(" info.entryPoint = reinterpret_cast<void*>(entry);");
harness.AppendLine(" info.nonvolatileFprWriteMask = 0;");
harness.AppendLine(" TranslatedFunctionRegistry::Register(std::move(info));");
harness.AppendLine("}");
harness.AppendLine("int main() {");
harness.AppendLine(" CpuContext ctx{};");
harness.AppendLine(" Register(0, FunctionKind::BaseTranslated, \"base\", &BaseTranslated);");
harness.AppendLine(" InvokeDirectCpu<0x81234568u>(&ctx);");
harness.AppendLine(" if (ctx.gpr[3] != 0xBACE0001u) return 1;");
harness.AppendLine(" Register(100, FunctionKind::ModTranslated, \"mod\", &ModTranslated);");
harness.AppendLine(" ctx.gpr[3] = 0;");
harness.AppendLine(" InvokeDirectCpu<0x81234568u>(&ctx);");
harness.AppendLine(" if (ctx.gpr[3] != 0xC0DE0002u) return 2;");
harness.AppendLine(" TranslatedFunctionRegistry::Finalize();");
harness.AppendLine(" ctx.gpr[3] = 0;");
harness.AppendLine(" InvokeDirectCpu<0x81234568u>(&ctx);");
harness.AppendLine(" if (ctx.gpr[3] != 0xC0DE0002u) return 3;");
harness.AppendLine(" std::cout << \"ok\";");
harness.AppendLine(" return 0;");
harness.AppendLine("}");
var harnessPath = Path.Combine(tempRoot, "harness_direct_mod_override_refresh.cpp");
File.WriteAllText(harnessPath, harness.ToString());
var runnerBasePath = Path.Combine(tempRoot, "runner_direct_mod_override_refresh");
var args = TranslatorCppTestHarness.BuildCompileArguments(
root,
tempRoot,
Array.Empty<string>(),
harnessPath,
runnerBasePath,
includeDataSections: false);
var compiler = CppCompiler();
var (compileExitCode, compileOutput) = RunProcess(compiler, args, TimeSpan.FromSeconds(60));
Assert.True(compileExitCode == 0, $"{compiler} failed: {compileOutput}");
var runnerPath = RunnerPath(runnerBasePath);
var (runExitCode, runOutput) = RunProcess(runnerPath, "", TimeSpan.FromSeconds(10));
Assert.True(runExitCode == 0, $"Runner failed ({runExitCode}). Output:\n{runOutput}");
Assert.Contains("ok", runOutput);
}
}
@@ -0,0 +1,162 @@
using Translator.Core.Disassembly;
using Xunit;
namespace Translator.Tests;
public sealed class PpcWordAnalysisTests
{
[Fact]
public void FieldsExposeSplitSprAndImmediateForms()
{
var fields = new PpcWordFields(0x7D8903A6u); // mtctr r12
Assert.Equal(31, fields.PrimaryOpcode);
Assert.Equal(12, fields.GprField0);
Assert.Equal(9, fields.Spr);
Assert.Equal(467, fields.ExtendedOpcode);
var immediateFields = new PpcWordFields(0x398CFFF0u); // addi r12,r12,-0x10
Assert.Equal(-16, immediateFields.SignedImmediate16);
Assert.Equal(0xFFF0u, immediateFields.UnsignedImmediate16);
}
[Fact]
public void ImmediateAndRegisterPatternsPreserveArchitecturalFieldOrder()
{
Assert.True(PpcInstructionPatterns.TryGetLis(0x3D80807Eu, out var lisDestination, out var high));
Assert.Equal(12, lisDestination);
Assert.Equal(0x807Eu, high);
Assert.True(PpcInstructionPatterns.TryGetAddi(0x398CFFF0u, out var addiDestination, out var addiSource, out var addiImmediate));
Assert.Equal(12, addiDestination);
Assert.Equal(12, addiSource);
Assert.Equal(-16, addiImmediate);
Assert.True(PpcInstructionPatterns.TryGetOri(0x618C3064u, out var oriSource, out var oriDestination, out var oriImmediate));
Assert.Equal(12, oriSource);
Assert.Equal(12, oriDestination);
Assert.Equal(0x3064u, oriImmediate);
var oris = EncodeImmediate(25, source: 12, destination: 9, immediate: 0x1234);
Assert.True(PpcInstructionPatterns.TryGetOris(oris, out var orisSource, out var orisDestination, out var orisImmediate));
Assert.Equal(12, orisSource);
Assert.Equal(9, orisDestination);
Assert.Equal(0x1234u, orisImmediate);
var orWord = EncodeXForm(source: 12, destination: 9, otherSource: 12, extendedOpcode: 444);
Assert.True(PpcInstructionPatterns.TryGetOr(orWord, out var source, out var destination, out var otherSource));
Assert.Equal(12, source);
Assert.Equal(9, destination);
Assert.Equal(12, otherSource);
var rlwinm = EncodeRlwinm(source: 3, destination: 4, shift: 0, maskBegin: 16, maskEnd: 31);
Assert.True(PpcInstructionPatterns.TryGetRlwinm(rlwinm, out var rotateSource, out var rotateDestination, out var shift, out var maskBegin, out var maskEnd));
Assert.Equal(3, rotateSource);
Assert.Equal(4, rotateDestination);
Assert.Equal(0, shift);
Assert.Equal(16, maskBegin);
Assert.Equal(31, maskEnd);
}
[Fact]
public void SprPatternsRecognizeOnlyTheRequestedSpecialRegister()
{
Assert.True(PpcInstructionPatterns.TryGetMtspr(0x7D8903A6u, 9, out var ctrSource));
Assert.Equal(12, ctrSource);
Assert.False(PpcInstructionPatterns.TryGetMtspr(0x7D8903A6u, 8, out _));
Assert.True(PpcInstructionPatterns.TryGetMtspr(0x7C0803A6u, 8, out var lrSource));
Assert.Equal(0, lrSource);
}
[Fact]
public void BranchPatternsPreserveLinkAbsoluteAndConditionalDistinctions()
{
const uint address = 0x80000100u;
Assert.True(PpcControlFlow.IsRelativeUnlinkedBranch(0x48000008u));
Assert.True(PpcControlFlow.TryDecodeRelativeBranchTarget(address, 0x48000008u, out var forward));
Assert.Equal(0x80000108u, forward);
Assert.True(PpcControlFlow.IsRelativeLinkedBranch(0x48000001u));
Assert.True(PpcControlFlow.TryDecodeRelativeBranchLinkTarget(address, 0x48000001u, out var linked));
Assert.Equal(address, linked);
Assert.False(PpcControlFlow.IsRelativeUnlinkedBranch(0x48000002u)); // absolute b
Assert.False(PpcControlFlow.TryDecodeRelativeBranchTarget(address, 0x48000001u, out _)); // bl
Assert.True(PpcControlFlow.TryDecodeConditionalRelativeBranchTarget(address, 0x4182FFFCu, out var conditional));
Assert.Equal(0x800000FCu, conditional);
Assert.True(PpcControlFlow.MayChangeControlFlow(0x4182FFFCu));
Assert.True(PpcControlFlow.MayChangeControlFlow(0x48000000u));
Assert.True(PpcControlFlow.MayChangeControlFlow(0x4E800020u));
Assert.False(PpcControlFlow.MayChangeControlFlow(0x398CFFF0u));
}
[Fact]
public void ConservativeWritePolicyUsesTheArchitecturalDestinationRegister()
{
// ori RA, RS, UIMM writes RA. Two of the three scanners used to key this
// on RS, so they evicted the source and kept a stale value in the
// register the instruction actually overwrote.
var ori = EncodeImmediate(24, source: 3, destination: 4, immediate: 1);
Assert.True(PpcRegisterEffects.MayWriteGpr(ori, 4));
Assert.False(PpcRegisterEffects.MayWriteGpr(ori, 3));
// addi RT, RA, SIMM writes RT, which this encoder puts in the source field.
var addi = EncodeImmediate(14, source: 5, destination: 6, immediate: 8);
Assert.True(PpcRegisterEffects.MayWriteGpr(addi, 5));
Assert.False(PpcRegisterEffects.MayWriteGpr(addi, 6));
// lwzu RT, d(RA) writes both RT and the RA base.
var lwzu = EncodeImmediate(33, source: 7, destination: 8, immediate: 4);
Assert.True(PpcRegisterEffects.MayWriteGpr(lwzu, 7));
Assert.True(PpcRegisterEffects.MayWriteGpr(lwzu, 8));
// stwu RS, d(RA) writes the RA base back; stw writes no register.
var stwu = EncodeImmediate(37, source: 1, destination: 1, immediate: 0xFFF0);
Assert.True(PpcRegisterEffects.MayWriteGpr(stwu, 1));
var stw = EncodeImmediate(36, source: 9, destination: 10, immediate: 0);
Assert.False(PpcRegisterEffects.MayWriteGpr(stw, 9));
Assert.False(PpcRegisterEffects.MayWriteGpr(stw, 10));
// lfd writes an FPR, so no GPR dies; lfdu still writes its RA base.
var lfd = EncodeImmediate(50, source: 11, destination: 12, immediate: 0);
Assert.False(PpcRegisterEffects.MayWriteGpr(lfd, 11));
Assert.False(PpcRegisterEffects.MayWriteGpr(lfd, 12));
var lfdu = EncodeImmediate(51, source: 11, destination: 12, immediate: 8);
Assert.True(PpcRegisterEffects.MayWriteGpr(lfdu, 12));
// cmpi writes a CR field, not a GPR.
var cmpi = EncodeImmediate(11, source: 0, destination: 13, immediate: 0);
Assert.False(PpcRegisterEffects.MayWriteGpr(cmpi, 0));
Assert.False(PpcRegisterEffects.MayWriteGpr(cmpi, 13));
// X-form is treated as clobbering either candidate: `add r5,r5,r6` and
// `or r5,r6,r6` share a primary opcode but not a destination field.
var xform = EncodeImmediate(31, source: 14, destination: 15, immediate: 0);
Assert.True(PpcRegisterEffects.MayWriteGpr(xform, 14));
Assert.True(PpcRegisterEffects.MayWriteGpr(xform, 15));
}
private static uint EncodeImmediate(int opcode, int source, int destination, int immediate) =>
((uint)opcode << 26) |
((uint)source << 21) |
((uint)destination << 16) |
(uint)(immediate & 0xFFFF);
private static uint EncodeXForm(int source, int destination, int otherSource, int extendedOpcode) =>
(31u << 26) |
((uint)source << 21) |
((uint)destination << 16) |
((uint)otherSource << 11) |
((uint)extendedOpcode << 1);
private static uint EncodeRlwinm(int source, int destination, int shift, int maskBegin, int maskEnd) =>
(21u << 26) |
((uint)source << 21) |
((uint)destination << 16) |
((uint)shift << 11) |
((uint)maskBegin << 6) |
((uint)maskEnd << 1);
}
@@ -0,0 +1,14 @@
namespace Translator.Tests;
public sealed class ProductionTranslationPolicyTests
{
[Fact]
public void ProductionProjectIsStrict()
{
var repositoryRoot = ProjectPathsForTests.FindRepositoryRoot();
var productionProject = File.ReadAllText(
Path.Combine(repositoryRoot, "projects", "mkwii", "recomp.yml"));
Assert.Contains("allow_unsupported_instructions: false", productionProject, StringComparison.Ordinal);
Assert.DoesNotContain("allow_unsupported_instructions: true", productionProject, StringComparison.Ordinal);
}
}
@@ -0,0 +1,427 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Translator.Core.Analysis;
using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis.Ssa;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
public class RegisterResidencyCodeGenTests
{
private static string Emit(
IrFunction function,
RepresentationEnvironment? types = null,
bool stateFree = false,
uint entryPoint = 0x80001000u,
IReadOnlyDictionary<uint, GuestAbiContract>? guestAbiContracts = null)
{
var contract = GuestAbiContractAnalyzer.Analyze(function);
return new CxxLinearCodeGenerator().Emit(
entryPoint,
new SsaTransformer().Convert(function),
new FunctionAbiClassification(function.Name, ValueRepresentation.Void),
types ?? new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
guestAbiContracts: guestAbiContracts,
emitStateFreeLeafVariant: stateFree,
stateFreeAbiContracts: stateFree
? new Dictionary<uint, GuestAbiContract> { [entryPoint] = contract }
: null,
stateFreeCallSymbols: stateFree
? new Dictionary<uint, string> { [entryPoint] = $"{function.Name}_native" }
: null);
}
/// <summary>
/// A synthetic callee contract. Everything not named here is empty, which is
/// what makes the narrowing observable.
/// </summary>
private static GuestAbiContract NarrowContract(
uint gprRead = 0,
uint gprWrite = 0,
uint fprRead = 0,
uint fprWrite = 0,
byte crRead = 0,
byte crWrite = 0,
bool readsCtr = false,
bool writesCtr = false,
GuestCallBoundaryFlags flags = GuestCallBoundaryFlags.None) =>
new(
GprReadBeforeWriteMask: gprRead,
GprPossibleWriteMask: gprWrite,
GprReturnMask: 0,
FprReadBeforeWriteMask: fprRead,
FprPossibleWriteMask: fprWrite,
FprReturnMask: 0,
CrReadBeforeWriteMask: crRead,
CrPossibleWriteMask: crWrite,
ReadsXerBeforeWrite: false,
MayWriteXer: false,
ReadsCtrBeforeWrite: readsCtr,
MayWriteCtr: writesCtr,
ReadsLrBeforeWrite: false,
MayWriteLr: false,
BoundaryFlags: flags,
DirectCallTargets: Array.Empty<uint>());
private static IrFunction CallerAcross(string name, string target) =>
new(name, "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r4"), "add"),
new IrBinary("r5", IrValue.Register("r5"), IrValue.Register("r6"), "add"),
new IrCall(string.Empty, target, Array.Empty<IrValue>()),
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r5"), "add"),
new IrReturn(null)
})
});
private static string FunctionBody(string code, string name)
{
var declaration = $"extern \"C\" void {name}(CpuContext* MKW_RESTRICT ctx)";
var start = code.IndexOf(declaration, StringComparison.Ordinal);
Assert.True(start >= 0, $"missing definition of {name}");
var open = code.IndexOf('{', start);
var depth = 0;
for (var index = open; index < code.Length; ++index)
{
if (code[index] == '{') ++depth;
else if (code[index] == '}' && --depth == 0) return code[open..(index + 1)];
}
throw new InvalidOperationException("unterminated body");
}
[Fact]
public void ResidentBodyLoadsAtEntryAndWritesBackAtReturn()
{
var function = new IrFunction("resident_add", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r4"), "add"),
new IrReturn(null)
})
});
var code = Emit(function);
Assert.Contains("uint32_t r3 = ctx->gpr[3];", code, StringComparison.Ordinal);
Assert.Contains("uint32_t r4 = ctx->gpr[4];", code, StringComparison.Ordinal);
Assert.Contains(" r3 = (r3 + r4);", code, StringComparison.Ordinal);
// r4 is never written, so it is not part of the write-back set.
Assert.Contains(" ctx->gpr[3] = r3;", code, StringComparison.Ordinal);
Assert.DoesNotContain("ctx->gpr[4] = r4;", code, StringComparison.Ordinal);
Assert.DoesNotContain("ctx->gpr[3] = (", code, StringComparison.Ordinal);
}
[Fact]
public void GuestCallIsBracketedByFlushAndReload()
{
var function = new IrFunction("resident_call", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r4"), "add"),
new IrCall(string.Empty, "0x80002000", Array.Empty<IrValue>()),
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r4"), "add"),
new IrReturn(null)
})
});
var body = FunctionBody(Emit(function), "resident_call");
var flush = body.IndexOf(" ctx->gpr[3] = r3;", StringComparison.Ordinal);
var call = body.IndexOf("InvokeDirectCpu<0x80002000u>(ctx);", StringComparison.Ordinal);
var reload = body.IndexOf(" r3 = ctx->gpr[3];", StringComparison.Ordinal);
Assert.True(flush > 0 && call > flush && reload > call,
"the call must be preceded by a flush and followed by a reload");
// A register that is only read still has to be reloaded: the callee may
// clobber it. It must not be flushed, because it was never modified.
Assert.Contains(" r4 = ctx->gpr[4];", body, StringComparison.Ordinal);
Assert.DoesNotContain("ctx->gpr[4] = r4;", body, StringComparison.Ordinal);
}
[Fact]
public void IndirectJumpFlushesWithoutWritingLocalsBack()
{
var function = new IrFunction("resident_tail", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r4"), "add"),
new IrIndirectJump(IrValue.Register("r3"))
})
});
var body = FunctionBody(Emit(function), "resident_tail");
var flush = body.IndexOf("ctx->gpr[3] = r3;", StringComparison.Ordinal);
var jump = body.IndexOf("InvokeIndirectJump(r3, ctx);", StringComparison.Ordinal);
Assert.True(flush > 0 && jump > flush);
// Exactly one publication: nothing may be written back after the tail
// dispatch, because the callee owns the architectural state from there.
Assert.Equal(
1,
body.Split("ctx->gpr[3] = r3;", StringSplitOptions.None).Length - 1);
}
[Fact]
public void ConditionRegisterAndCountRegisterBecomeLocals()
{
var function = new IrFunction("resident_loop", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrAssign("ctr", IrValue.Register("r3")),
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(0), false),
new IrBranch("bdnz", "entry", "exit", "ctr")
}),
new IrBasicBlock("exit", new IrInstruction[] { new IrReturn(null) })
});
var code = Emit(function);
Assert.Contains("uint32_t cr = ctx->cr;", code, StringComparison.Ordinal);
Assert.Contains("uint32_t ctr = ctx->ctr;", code, StringComparison.Ordinal);
Assert.Contains("uint32_t xer = ctx->xer;", code, StringComparison.Ordinal);
Assert.Contains("SetCRResident(cr, xer, 0,", code, StringComparison.Ordinal);
Assert.Contains("if ((ctr != 0))", code, StringComparison.Ordinal);
Assert.Contains(" ctx->cr = cr;", code, StringComparison.Ordinal);
Assert.Contains(" ctx->ctr = ctr;", code, StringComparison.Ordinal);
Assert.DoesNotContain("SetCR(ctx,", code, StringComparison.Ordinal);
}
[Fact]
public void RawBranchConditionTextIsRoutedThroughTheResidentCondition()
{
// PpcLifter emits the general bc/bcctr condition as raw C++ text. It is
// the only register access that bypasses the expression layer, so a
// resident body must rewrite it or silently read a stale CR/CTR.
var function = new IrFunction("resident_raw_condition", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrBranch(
"raw",
"entry",
"exit",
"(((((ctx->ctr != 0)) ^ false)) && ((GetCRBit(ctx, 0, 2) == true)))")
}),
new IrBasicBlock("exit", new IrInstruction[] { new IrReturn(null) })
});
var code = Emit(function);
Assert.Contains("(((ctr != 0)) ^ false)", code, StringComparison.Ordinal);
Assert.Contains("GetCRBitResident(cr, 0, 2)", code, StringComparison.Ordinal);
Assert.DoesNotContain("ctx->ctr !=", code, StringComparison.Ordinal);
Assert.DoesNotContain("GetCRBit(ctx,", code, StringComparison.Ordinal);
}
[Fact]
public void FloatRegistersUseValueLocalsAndPreservePairedLanes()
{
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["f1"] = ValueRepresentation.Float64,
["f2"] = ValueRepresentation.Float64
});
var function = new IrFunction("resident_float", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall("f1", "PPC_PsAdd", new[] { IrValue.Register("f1"), IrValue.Register("f2") }),
new IrReturn(null)
})
});
var code = Emit(function, types);
Assert.Contains("PPC_FPR f1 = ctx->fpr[1];", code, StringComparison.Ordinal);
Assert.Contains("PPC_FPR f2 = ctx->fpr[2];", code, StringComparison.Ordinal);
Assert.Contains("PpcSetPairedFprInline(f1,", code, StringComparison.Ordinal);
// The whole union is written back so PS1 survives the boundary.
Assert.Contains(" ctx->fpr[1] = f1;", code, StringComparison.Ordinal);
Assert.DoesNotContain("ctx->fpr[2] = f2;", code, StringComparison.Ordinal);
}
[Fact]
public void StateFreeVariantIsUnchangedWhileThePublicEntryBecomesResident()
{
var function = new IrFunction("resident_state_free", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r4"), "add"),
new IrReturn(null)
})
});
var code = Emit(function, stateFree: true);
// The explicit-state clone is derived from the legacy cached body, so it
// keeps the cached form while the public CpuContext entry is resident.
var start = code.IndexOf("resident_state_free_native(", StringComparison.Ordinal);
Assert.True(start >= 0);
var variant = code[start..code.IndexOf("RECOMP_STATE_FREE_ABI", start, StringComparison.Ordinal)];
Assert.DoesNotContain("ctx->gpr[3] = r3;", variant, StringComparison.Ordinal);
var publicBody = FunctionBody(code, "resident_state_free");
Assert.Contains("uint32_t r3 = ctx->gpr[3];", publicBody, StringComparison.Ordinal);
Assert.Contains(" r3 = (r3 + r4);", publicBody, StringComparison.Ordinal);
Assert.DoesNotContain("cached_r3", publicBody, StringComparison.Ordinal);
}
[Fact]
public void BoundaryNarrowingSyncsOnlyTheContractIntersectionAtADirectCall()
{
// The callee reads r4 before writing it and can only write r3, so the
// boundary needs r3 (it is in this frame's write set and in the callee
// write set), r4 (the callee reads it), and nothing else on the flush;
// the reload is limited to r3.
var contracts = new Dictionary<uint, GuestAbiContract>
{
[0x80002000u] = NarrowContract(gprRead: 1u << 4, gprWrite: 1u << 3)
};
var body = FunctionBody(
Emit(
CallerAcross("narrowed_call", "0x80002000"),
guestAbiContracts: contracts),
"narrowed_call");
var call = body.IndexOf("InvokeDirectCpu<0x80002000u>(ctx);", StringComparison.Ordinal);
Assert.True(call > 0);
var beforeCall = body[..call];
var afterCall = body[call..];
Assert.Contains("ctx->gpr[3] = r3;", beforeCall, StringComparison.Ordinal);
// r5 is written by this frame but the callee neither reads nor writes it.
Assert.DoesNotContain("ctx->gpr[5] = r5;", beforeCall, StringComparison.Ordinal);
Assert.Contains(" r3 = ctx->gpr[3];", afterCall, StringComparison.Ordinal);
Assert.DoesNotContain("r4 = ctx->gpr[4];", afterCall, StringComparison.Ordinal);
Assert.DoesNotContain("r5 = ctx->gpr[5];", afterCall, StringComparison.Ordinal);
}
[Fact]
public void BoundaryNarrowingFlushesRegistersTheCalleeOnlyMayWrite()
{
// A register the callee may write has to be flushed even though the
// callee never reads it: the write is only possible, so on the paths
// where it does not happen the reload after the call would otherwise
// load a stale architectural value back into the local.
var contracts = new Dictionary<uint, GuestAbiContract>
{
[0x80002000u] = NarrowContract(gprWrite: (1u << 3) | (1u << 5))
};
var body = FunctionBody(
Emit(
CallerAcross("narrowed_possible_write", "0x80002000"),
guestAbiContracts: contracts),
"narrowed_possible_write");
var call = body.IndexOf("InvokeDirectCpu<0x80002000u>(ctx);", StringComparison.Ordinal);
var beforeCall = body[..call];
var afterCall = body[call..];
Assert.Contains("ctx->gpr[3] = r3;", beforeCall, StringComparison.Ordinal);
Assert.Contains("ctx->gpr[5] = r5;", beforeCall, StringComparison.Ordinal);
Assert.Contains(" r3 = ctx->gpr[3];", afterCall, StringComparison.Ordinal);
Assert.Contains(" r5 = ctx->gpr[5];", afterCall, StringComparison.Ordinal);
Assert.DoesNotContain("r4 = ctx->gpr[4];", afterCall, StringComparison.Ordinal);
}
[Fact]
public void BoundaryNarrowingSyncsConditionAndCountRegistersOnAnyIntersectingField()
{
// The contract describes CR per field while the local is the packed
// register, so a single intersecting field forces the whole sync. CTR is
// synced whenever the contract cannot prove the callee leaves it alone.
var function = new IrFunction("narrowed_special", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrAssign("ctr", IrValue.Register("r3")),
new IrSetCrField(2, IrValue.Register("r3"), IrValue.Imm(0), false),
new IrCall(string.Empty, "0x80002000", Array.Empty<IrValue>()),
new IrBranch("bdnz", "entry", "exit", "ctr")
}),
new IrBasicBlock("exit", new IrInstruction[] { new IrReturn(null) })
});
string BodyFor(GuestAbiContract contract) => FunctionBody(
Emit(
function,
guestAbiContracts: new Dictionary<uint, GuestAbiContract> { [0x80002000u] = contract }),
"narrowed_special");
var touched = BodyFor(NarrowContract(crRead: 1 << 5, crWrite: 1 << 1, writesCtr: true));
Assert.Contains("ctx->cr = cr;", touched, StringComparison.Ordinal);
Assert.Contains(" cr = ctx->cr;", touched, StringComparison.Ordinal);
Assert.Contains("ctx->ctr = ctr;", touched, StringComparison.Ordinal);
Assert.Contains(" ctr = ctx->ctr;", touched, StringComparison.Ordinal);
var untouched = BodyFor(NarrowContract(gprWrite: 1u << 3));
var call = untouched.IndexOf("InvokeDirectCpu<0x80002000u>(ctx);", StringComparison.Ordinal);
// The exit flush still publishes CR/CTR; only the call boundary drops it.
Assert.DoesNotContain("ctx->cr = cr;", untouched[..call], StringComparison.Ordinal);
Assert.DoesNotContain("ctx->ctr = ctr;", untouched[..call], StringComparison.Ordinal);
Assert.DoesNotContain(" cr = ctx->cr;", untouched[call..], StringComparison.Ordinal);
Assert.DoesNotContain(" ctr = ctx->ctr;", untouched[call..], StringComparison.Ordinal);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void BoundaryNarrowingFallsBackToAFullSyncWithoutAPreciseContract(bool haveContract)
{
// A missing contract and a full-synchronization fence are the same thing
// here: neither proves anything about the callee's register usage.
var contracts = haveContract
? new Dictionary<uint, GuestAbiContract>
{
[0x80002000u] = NarrowContract(
gprWrite: 1u << 3,
flags: GuestCallBoundaryFlags.RequiresCompleteContext)
}
: new Dictionary<uint, GuestAbiContract>();
var caller = CallerAcross("narrowed_fence", "0x80002000");
var narrowed = FunctionBody(
Emit(caller, guestAbiContracts: contracts),
"narrowed_fence");
// Full synchronization, asserted by shape: r5 is flushed even though the
// callee is declared unable to write it, and r4 is reloaded even though
// this frame only reads it. Narrowing would have dropped both.
var call = narrowed.IndexOf("InvokeDirectCpu<0x80002000u>(ctx);", StringComparison.Ordinal);
Assert.Contains("ctx->gpr[5] = r5;", narrowed[..call], StringComparison.Ordinal);
Assert.Contains(" r4 = ctx->gpr[4];", narrowed[call..], StringComparison.Ordinal);
}
[Fact]
public void BoundaryNarrowingDoesNotApplyToIndirectCalls()
{
var function = new IrFunction("narrowed_indirect", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r4"), "add"),
new IrIndirectCall(string.Empty, IrValue.Register("r12"), Array.Empty<IrValue>()),
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r4"), "add"),
new IrReturn(null)
})
});
var narrowed = FunctionBody(Emit(function), "narrowed_indirect");
// There is no callee contract to narrow against, so the boundary stays
// full: r4 is reloaded after the call even though this frame only reads it.
Assert.Contains(" r4 = ctx->gpr[4];", narrowed, StringComparison.Ordinal);
}
}
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using Translator.Core.Analysis;
using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis.Ssa;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
public class ResolvedMemoryLeafCacheCodeGenTests
{
[Fact]
public void AddressBasesUsedOnlyByResolvedOperationsStayCached()
{
var function = new IrFunction(
"resolved_memory_register_cache",
"entry",
new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrResolveGuestMemoryRange(
"range", IrValue.Register("r13"), 0, 64,
NeedsReadAccess: true, NeedsWriteAccess: false),
new IrResolvedLoad(
"r3", "range", new IrAddress("r2", 4), 4, 4),
new IrReturn(IrValue.Register("r3"))
})
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["r2"] = ValueRepresentation.UInt32,
["r3"] = ValueRepresentation.UInt32,
["r13"] = ValueRepresentation.UInt32
});
// Register residency owns every primary body and achieves the property
// under test (an address base used only by resolved operations promoted
// once to a local) directly at emission time, under its own plain-named
// locals.
var code = new CxxLinearCodeGenerator().Emit(
0x80006202,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("resolved_memory_register_cache", ValueRepresentation.UInt32),
types);
Assert.Contains("uint32_t r2 = ctx->gpr[2];", code, StringComparison.Ordinal);
Assert.Contains("uint32_t r13 = ctx->gpr[13];", code, StringComparison.Ordinal);
Assert.Contains("MemoryInline::ResolveRangeHost(r13", code, StringComparison.Ordinal);
Assert.Contains("MemoryInline::ReadResolved32(range, 4u, (r2 + 4))", code, StringComparison.Ordinal);
}
}
@@ -0,0 +1,37 @@
using Translator.Core.Mods.Mkwii;
using Translator.Core.Parsing.Kamek;
namespace Translator.Tests;
public class RetroWfcBootstrapSuppressorTests
{
[Fact]
public void SuppressRemovesOnlyConfiguredLegacyBootstrapBranch()
{
var bootstrap = Command(KamekCommandId.Branch, 0x800ED6E8u, 0x0003437Cu);
var other = Command(KamekCommandId.Write32, 0x800EE3A0u, 0x2C030000u);
var chunk = Chunk([bootstrap, other]);
var result = RetroWfcBootstrapSuppressor.Suppress(chunk, 0x800ED6E8u);
Assert.Equal([other], result.Commands);
Assert.Equal(2, chunk.Commands.Count);
}
[Fact]
public void SuppressRejectsStaleOrIncorrectProfileAddress()
{
var chunk = Chunk([Command(KamekCommandId.Branch, 0x800ED6E8u, 0x0003437Cu)]);
var error = Assert.Throws<InvalidDataException>(
() => RetroWfcBootstrapSuppressor.Suppress(chunk, 0x800ED6ECu));
Assert.Contains("0x800ED6EC", error.Message, StringComparison.Ordinal);
}
private static KamekCommand Command(KamekCommandId id, uint address, uint argument) =>
new(0, 0, id, true, address, [argument]);
private static KamekChunk Chunk(IReadOnlyList<KamekCommand> commands) =>
new(0, 0, 0, 4, 0, 0, 4, [0, 0, 0, 0], commands);
}
@@ -0,0 +1,99 @@
using System.Buffers.Binary;
using System.Text;
using Translator.Core.Mods;
using Translator.Core.Mods.Mkwii;
using Xunit;
namespace Translator.Tests;
public class RetroWfcPayloadLoweringTests
{
[Fact]
public void SharedPayloadDescriptorDrivesRelocationAndStaticLowering()
{
const uint moduleBase = 0x81700000u;
const uint payloadModuleOffset = 0x2000u;
var payload = BuildSharedPayloadFixture();
var result = RetroWfcPayload.Parse(payload, TestManifest(), moduleBase, payloadModuleOffset, "fixture");
Assert.Equal("RMCPD00", result.Summary.Game);
Assert.Equal(3u, result.Summary.FormatVersion);
Assert.Equal(moduleBase + payloadModuleOffset + 0x1E4u, result.Summary.InitializationTargetAddress);
Assert.Equal(moduleBase + payloadModuleOffset + 0x210u, ReadU32(result.RelocatedImage, 0x1B4));
Assert.Equal(moduleBase + payloadModuleOffset + 0x214u, ReadU32(result.RelocatedImage, 0x220));
var callback = Assert.Single(result.Summary.InitializationCallbacks);
Assert.Equal(moduleBase + payloadModuleOffset + 0x208u, callback.TargetAddress);
var hook = Assert.Single(result.LoweringPlan.ExecutableHooks);
Assert.Equal(moduleBase + payloadModuleOffset + 0x200u, hook.TargetAddress);
Assert.Equal("moduleFunction", hook.TargetKind);
var pointer = Assert.Single(result.LoweringPlan.StaticPointers);
Assert.Equal(moduleBase + payloadModuleOffset + 0x204u, pointer.TargetAddress);
Assert.Equal("moduleFunction", pointer.TargetKind);
}
private static BaseManifest TestManifest() =>
new(
"test",
1,
"RMCP01",
"P",
"",
0,
[
new BaseSectionMetadata(".text", "main.dol", 0x80001000u, 0x80001020u, true, false, "base_text.bin", 0),
new BaseSectionMetadata(".data", "main.dol", 0x80002000u, 0x80002020u, false, true, "base_data.bin", 0)
],
[
new BaseFunctionRangeMetadata(0x80001000u, 0x80001020u, "func_80001000", ".text", 0, "test", ["Executable"])
],
"ranges.json");
private static byte[] BuildSharedPayloadFixture()
{
var payload = new byte[0x240];
Encoding.ASCII.GetBytes("WWFC/Payload").CopyTo(payload, 0);
WriteU32(payload, 0x0C, (uint)payload.Length);
WriteU32(payload, 0x130, 3);
WriteU32(payload, 0x134, 1);
Encoding.ASCII.GetBytes("RMCPD00").CopyTo(payload, 0x138);
WriteU32(payload, 0x144, 0x00010000);
WriteU32(payload, 0x148, 0x1B4);
WriteU32(payload, 0x14C, 0x1B8);
WriteU32(payload, 0x150, 0x1B8);
WriteU32(payload, 0x154, 0x1BC);
WriteU32(payload, 0x158, 0x1BC);
WriteU32(payload, 0x15C, 0x1DC);
WriteU32(payload, 0x1A0, 0x1A4);
WriteU32(payload, 0x1A4, 1);
WriteU32(payload, 0x1A8, 0x1E4);
WriteU32(payload, 0x1AC, 0x1E4);
WriteU32(payload, 0x1B0, 0x230);
WriteU32(payload, 0x1B4, 0x210);
WriteU32(payload, 0x1B8, 0x220);
payload[0x1BC + 1] = 3;
WriteU32(payload, 0x1BC + 4, 0x80001000);
WriteU32(payload, 0x1BC + 8, 0x200);
payload[0x1CC + 1] = 6;
WriteU32(payload, 0x1CC + 4, 0x80002000);
WriteU32(payload, 0x1CC + 8, 0x204);
WriteU32(payload, 0x1DC, 0xFFFFFFFF);
WriteU32(payload, 0x1E0, 0x208);
WriteU32(payload, 0x220, 0x214);
return payload;
}
private static uint ReadU32(byte[] image, int offset) =>
BinaryPrimitives.ReadUInt32BigEndian(image.AsSpan(offset, 4));
private static void WriteU32(byte[] image, int offset, uint value) =>
BinaryPrimitives.WriteUInt32BigEndian(image.AsSpan(offset, 4), value);
}
@@ -0,0 +1,135 @@
using Translator.Cli.Configuration;
using Xunit;
namespace Translator.Tests;
public sealed class RiivolutionProfileConfigTests
{
[Fact]
public void ParsesRiivolutionSectionAndNormalizesPackRelativeXmlPath()
{
var root = CreateTempRoot();
try
{
var projectPath = WriteProject(
root,
" riivolution:",
" xml: .\\xml\\RetroRewind6.xml",
" options:",
" - section: Retro Rewind",
" option: Pack",
" choice: 1",
" - option: Extra");
var profile = TranslationProjectConfig.Load(projectPath).Profiles["retro-rewind"];
Assert.NotNull(profile.Riivolution);
Assert.Equal("xml/RetroRewind6.xml", profile.Riivolution!.Xml);
Assert.Collection(
profile.Riivolution.Options,
option => Assert.Equal(new ProjectRiivolutionOption("Retro Rewind", "Pack", 1u), option),
option => Assert.Equal(new ProjectRiivolutionOption(string.Empty, "Extra", 0u), option));
}
finally
{
Directory.Delete(root, recursive: true);
}
}
[Fact]
public void ProfileWithoutRiivolutionLeavesItUnset()
{
var root = CreateTempRoot();
try
{
var projectPath = WriteProject(root);
Assert.Null(TranslationProjectConfig.Load(projectPath).Profiles["retro-rewind"].Riivolution);
}
finally
{
Directory.Delete(root, recursive: true);
}
}
[Fact]
public void RiivolutionWithoutXmlIsRejected()
{
var root = CreateTempRoot();
try
{
var projectPath = WriteProject(
root,
" riivolution:",
" options:",
" - option: Pack",
" choice: 1");
var error = Assert.Throws<InvalidDataException>(() => TranslationProjectConfig.Load(projectPath));
Assert.Contains("profiles.retro-rewind.riivolution.xml", error.Message, StringComparison.Ordinal);
}
finally
{
Directory.Delete(root, recursive: true);
}
}
[Fact]
public void RiivolutionOptionWithoutOptionNameIsRejected()
{
var root = CreateTempRoot();
try
{
var projectPath = WriteProject(
root,
" riivolution:",
" xml: xml/RetroRewind6.xml",
" options:",
" - section: Retro Rewind",
" choice: 1");
var error = Assert.Throws<InvalidDataException>(() => TranslationProjectConfig.Load(projectPath));
Assert.Contains("riivolution.options[0].option", error.Message, StringComparison.Ordinal);
}
finally
{
Directory.Delete(root, recursive: true);
}
}
private static string WriteProject(string root, params string[] profileLines)
{
File.WriteAllBytes(Path.Combine(root, "main.dol"), new byte[0x100]);
var lines = new List<string>
{
"schema_version: 1",
"workspace_root: .",
"",
"project:",
" id: riivolution-test",
"",
"inputs:",
" dol:",
" path: main.dol",
"",
"translation:",
" entry_points:",
" - 0x80001000",
"",
"profiles:",
" retro-rewind:",
" enabled: true",
};
lines.AddRange(profileLines);
var path = Path.Combine(root, "recomp.yml");
File.WriteAllLines(path, lines);
return path;
}
private static string CreateTempRoot()
{
var root = Path.Combine(Path.GetTempPath(), $"translator-riivolution-{Guid.NewGuid():N}");
Directory.CreateDirectory(root);
return root;
}
}
@@ -0,0 +1,102 @@
using System;
using System.IO;
using Translator.Core.Analysis;
using Xunit;
namespace Translator.Tests;
public sealed class RuntimeNativeGuestEffectAnalyzerTests
{
[Fact]
public void TypedStubDerivesArgumentsAndReturnWithoutContextFence()
{
using var fixture = new SourceFixture("""
extern "C" uint32_t Plain(uint32_t first, float second) { return first; }
PPC_NATIVE_OVERRIDE(80001234, Plain, uint32_t, (uint32_t first, float second), (first, second));
""");
var effects = RuntimeNativeGuestEffectAnalyzer.AnalyzeDirectory(fixture.Directory);
var contract = effects.Contracts[0x80001234u];
Assert.Equal(1u << 3, contract.GprReadBeforeWriteMask);
Assert.Equal(1u << 1, contract.FprReadBeforeWriteMask);
Assert.Equal(1u << 3, contract.GprPossibleWriteMask);
Assert.False(contract.HasFullSynchronizationFence);
Assert.Contains(0x80001234u, effects.PreciseContracts);
}
[Fact]
public void DirectContextNativeDerivesConstantRegisterReadsAndWrites()
{
using var fixture = new SourceFixture("""
extern "C" void Matrix(CpuContext* ctx) {
const auto a = ctx->gpr[3];
const auto b = ctx->gpr[4];
ctx->gpr[6] = a + b;
}
REGISTER_NATIVE_FUNCTION(0x80005678, Matrix);
""");
var contract = RuntimeNativeGuestEffectAnalyzer.AnalyzeDirectory(fixture.Directory).Contracts[0x80005678u];
Assert.Equal((1u << 3) | (1u << 4), contract.GprReadBeforeWriteMask);
Assert.Equal(1u << 6, contract.GprPossibleWriteMask);
Assert.False(contract.HasFullSynchronizationFence);
}
[Fact]
public void EscapedContextAndSchedulerCallsRemainExplicitFullBoundaries()
{
using var fixture = new SourceFixture("""
extern "C" void Sleep(CpuContext* ctx) {
Fiber::YieldToScheduler(ctx);
}
REGISTER_NATIVE_FUNCTION(0x80009ABC, Sleep);
""");
var contract = RuntimeNativeGuestEffectAnalyzer.AnalyzeDirectory(fixture.Directory).Contracts[0x80009ABCu];
Assert.True(contract.HasFullSynchronizationFence);
Assert.True((contract.BoundaryFlags & GuestCallBoundaryFlags.CanSuspend) != 0);
Assert.Contains(0x80009ABCu,
RuntimeNativeGuestEffectAnalyzer.AnalyzeDirectory(fixture.Directory).ConservativeContracts);
}
[Fact]
public void TypedStubReachingForAmbientContextIsAFullFence()
{
// A typed stub's contract comes from its host signature alone. This one
// is shaped exactly like 0x801AAD7C (__OSGetSystemTime): declared to
// return a single uint32_t, but it publishes a 64-bit result by storing
// r3 and r4 through the ambient context. Deriving "writes r3" from the
// signature would let a narrowed call boundary skip reloading r4.
using var fixture = new SourceFixture("""
extern "C" uint32_t GetSystemTime(uint32_t low, uint32_t high) {
const uint64_t now = ReadSystemTime();
if (CpuContext* ctx = CurrentCpuContext()) {
ctx->gpr[3] = static_cast<uint32_t>(now >> 32);
ctx->gpr[4] = static_cast<uint32_t>(now);
}
return static_cast<uint32_t>(now >> 32);
}
PPC_NATIVE_OVERRIDE(801AAD7C, GetSystemTime, uint32_t, (uint32_t low, uint32_t high), (low, high));
""");
var effects = RuntimeNativeGuestEffectAnalyzer.AnalyzeDirectory(fixture.Directory);
var contract = effects.Contracts[0x801AAD7Cu];
Assert.True(contract.HasFullSynchronizationFence);
Assert.Equal(uint.MaxValue, contract.GprPossibleWriteMask);
Assert.Contains(0x801AAD7Cu, effects.ConservativeContracts);
}
private sealed class SourceFixture : IDisposable
{
public SourceFixture(string source)
{
Directory = Path.Combine(Path.GetTempPath(), $"mkw-native-effects-{Guid.NewGuid():N}");
System.IO.Directory.CreateDirectory(Directory);
File.WriteAllText(Path.Combine(Directory, "fixture.cpp"), source);
}
public string Directory { get; }
public void Dispose() => System.IO.Directory.Delete(Directory, recursive: true);
}
}
@@ -0,0 +1,83 @@
using Translator.Core;
using Translator.Core.CodeGen;
using Xunit;
namespace Translator.Tests;
public sealed class RuntimeNativeIndexTests
{
[Fact]
public void BuildCapturesEveryRuntimeRegistrationKind()
{
var directory = Path.Combine(Path.GetTempPath(), $"mkw-native-kinds-{Guid.NewGuid():N}");
Directory.CreateDirectory(directory);
try
{
File.WriteAllText(Path.Combine(directory, "registrations.cpp"), """
REGISTER_NATIVE_FUNCTION(0x80000010, Direct);
REGISTER_NATIVE_FUNCTION_AS(0x80000020, Aliased, "alias");
REGISTER_TRANSLATED_FUNCTION(0x80000030, Translated);
PPC_NATIVE_OVERRIDE_VOID(80000040, Stub, (void), ());
GX_FATAL_STUB(80000050, "Fatal")
// REGISTER_NATIVE_FUNCTION(0x80000060, CommentedOut);
""");
var registrations = RuntimeNativeIndexBuilder.Build(directory).Registrations;
Assert.Equal(5, registrations.Length);
Assert.Contains(registrations, static entry =>
entry.Address == 0x80000010u && entry.ExcludesBaseTranslation);
Assert.Contains(registrations, static entry =>
entry.Address == 0x80000020u && !entry.ExcludesBaseTranslation);
Assert.Contains(registrations, static entry =>
entry.Address == 0x80000030u && entry.IsTranslatedOverride);
Assert.Contains(registrations, static entry =>
entry.Address == 0x80000040u && entry.Symbol == "Stub");
Assert.Contains(registrations, static entry =>
entry.Address == 0x80000050u && entry.Symbol == "GX_FATAL_STUB_80000050");
Assert.DoesNotContain(registrations, static entry => entry.Address == 0x80000060u);
}
finally
{
if (Directory.Exists(directory))
Directory.Delete(directory, recursive: true);
}
}
[Fact]
public void BuildSharesTypedAbiAndEffectDataWithoutCreatingSidecarFiles()
{
var directory = Path.Combine(Path.GetTempPath(), $"mkw-native-index-{Guid.NewGuid():N}");
Directory.CreateDirectory(directory);
var sourcePath = Path.Combine(directory, "fixture.cpp");
try
{
File.WriteAllText(sourcePath, """
extern "C" void Typed(float value, uint32_t count) { (void)value; (void)count; }
PPC_NATIVE_OVERRIDE_VOID(80001234, Typed, (float value, uint32_t count), (value, count));
""");
var index = RuntimeNativeIndexBuilder.Build(directory);
Assert.Single(index.Registrations);
Assert.Single(index.VoidStubAbis);
Assert.Single(index.Effects);
Assert.True(index.ToGuestEffectSet().Contracts.ContainsKey(0x80001234u));
var provider = RuntimeNativeFunctionAbiProvider.FromIndex(
index, new HashSet<uint> { 0x80001234u });
Assert.True(provider.TryGetGuestFunctionAbi("func_80001234", out var abi));
Assert.Contains("f1", abi.ArgumentRegisters);
Assert.Contains("r3", abi.ArgumentRegisters);
Assert.Contains("f1", abi.ScalarFloatArgumentRegisters);
var outOfScope = RuntimeNativeFunctionAbiProvider.FromIndex(index, new HashSet<uint>());
Assert.False(outOfScope.TryGetGuestFunctionAbi("func_80001234", out _));
Assert.Equal([sourcePath], Directory.GetFiles(directory));
}
finally
{
if (Directory.Exists(directory))
Directory.Delete(directory, recursive: true);
}
}
}
@@ -0,0 +1,265 @@
using System;
using System.Collections.Generic;
using System.Linq;
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;
public class ScalarPairedBoundaryCodeGenTests
{
[Theory]
[InlineData("add", true)]
[InlineData("sub", true)]
[InlineData("mul", true)]
[InlineData("fdiv", true)]
[InlineData("fcmp", true)]
[InlineData("fabs", false)]
[InlineData("fneg", false)]
[InlineData("frsp", false)]
[InlineData("fsqrt", false)]
[InlineData("fctiw", false)]
[InlineData("fctiwz", false)]
public void NormalizesPairedOperandsForEveryScalarFloatOperator(
string operation,
bool consumesRightOperand)
{
var code = Emit(
new IrCall("f2", "PPC_PsSum0", new[]
{
IrValue.Register("f5"), IrValue.Register("f6"), IrValue.Register("f7")
}),
new IrCall("f3", "PPC_PsSum0", new[]
{
IrValue.Register("f8"), IrValue.Register("f9"), IrValue.Register("f10")
}),
new IrBinary(
"f4",
IrValue.Register("f2"),
consumesRightOperand ? IrValue.Register("f3") : IrValue.Imm(0),
operation));
Assert.Contains("PPC_PsToScalarInline(f2.d)", code, StringComparison.Ordinal);
if (consumesRightOperand)
Assert.Contains("PPC_PsToScalarInline(f3.d)", code, StringComparison.Ordinal);
}
[Theory]
[InlineData("PPC_PsFromScalar", 1)]
[InlineData("PPC_Fadds", 2)]
[InlineData("PPC_Fsubs", 2)]
[InlineData("PPC_Fmuls", 2)]
[InlineData("PPC_Fdivs", 2)]
[InlineData("PPC_Fsqrt", 1)]
[InlineData("PPC_Frsqrte", 1)]
[InlineData("PPC_Fmadd", 3)]
[InlineData("PPC_Fmsub", 3)]
[InlineData("PPC_Fnmadd", 3)]
[InlineData("PPC_Fnmsub", 3)]
public void NormalizesEveryPairedScalarHelperOperand(string target, int argumentCount)
{
var arguments = Enumerable.Repeat(IrValue.Register("f1"), argumentCount).ToArray();
var code = Emit(
new IrCall("f1", "PPC_PsMul", new[]
{
IrValue.Register("f5"), IrValue.Register("f6")
}),
new IrCall("f2", target, arguments));
const string conversion = "PPC_PsToScalarInline(f1.d)";
Assert.Equal(
argumentCount,
code.Split(conversion, StringSplitOptions.None).Length - 1);
}
[Fact]
public void PreservesScalarDoubleInputForFrsqrte()
{
var code = Emit(new IrCall("f2", "PPC_Frsqrte", new[] { IrValue.Register("f1") }));
Assert.Contains("PPC_Frsqrte(f1.d)", code, StringComparison.Ordinal);
Assert.DoesNotContain("PPC_PsFromScalarInline(f1.d)", code, StringComparison.Ordinal);
}
[Fact]
public void PreservesPairedArgumentsAtUnprovenIndirectCallBoundary()
{
var code = Emit(
new IrCall("f1", "PPC_PsMul", new[]
{
IrValue.Register("f5"), IrValue.Register("f6")
}),
new IrIndirectCall(string.Empty, IrValue.Register("ctr"), new[]
{
IrValue.Register("r3"), IrValue.Register("f1")
}),
new IrCall("f2", "PPC_PsNeg", new[] { IrValue.Register("f1") }));
var call = code.IndexOf("InvokeIndirectCpu(ctr, ctx);", StringComparison.Ordinal);
Assert.True(call >= 0, code);
Assert.DoesNotContain(
"f1.d = PPC_PsToScalarInline(f1.d);",
code[..call],
StringComparison.Ordinal);
Assert.Contains(
"PPC_PsNegInline(PPC_PsFromScalarInline(f1.d))",
code[call..],
StringComparison.Ordinal);
}
[Fact]
public void PreservesUntouchedPairedPayloadAcrossIndirectCall()
{
var code = Emit(
new IrCall("f2", "PPC_PsMul", new[]
{
IrValue.Register("f5"), IrValue.Register("f6")
}),
new IrIndirectCall(string.Empty, IrValue.Register("ctr"), new[]
{
IrValue.Register("r3")
}),
new IrCall("f3", "PPC_PsNeg", new[] { IrValue.Register("f2") }));
var call = code.IndexOf("InvokeIndirectCpu(ctr, ctx);", StringComparison.Ordinal);
Assert.True(call >= 0, code);
Assert.Contains(
"PPC_PsNegInline(f2.d)",
code[call..],
StringComparison.Ordinal);
Assert.DoesNotContain(
"PPC_PsFromScalarInline(f2.d)",
code[call..],
StringComparison.Ordinal);
}
[Fact]
public void UnprovenIndirectF1RemainsConventionalScalarForMixedConsumers()
{
var code = Emit(
new IrIndirectCall(string.Empty, IrValue.Register("ctr"), new[]
{
IrValue.Register("r3")
}),
new IrSetCrField(0, IrValue.Register("f1"), IrValue.Register("f14"), false),
new IrCall("f2", "PPC_PsNeg", new[] { IrValue.Register("f1") }));
var call = code.IndexOf("InvokeIndirectCpu(ctr, ctx);", StringComparison.Ordinal);
Assert.True(call >= 0, code);
Assert.Contains(
"SetCRFloatResident(cr, 0, f1.d, f14.d);",
code[call..],
StringComparison.Ordinal);
Assert.Contains(
"PPC_PsNegInline(PPC_PsFromScalarInline(f1.d))",
code[call..],
StringComparison.Ordinal);
}
[Fact]
public void UnprovenDirectF1RemainsConventionalScalarForMixedConsumers()
{
var code = Emit(
new IrCall(string.Empty, "func_80002000", Array.Empty<IrValue>()),
new IrSetCrField(0, IrValue.Register("f1"), IrValue.Register("f14"), false),
new IrCall("f2", "PPC_PsNeg", new[] { IrValue.Register("f1") }));
var call = code.IndexOf("InvokeDirectCpu<0x80002000u>(ctx);", StringComparison.Ordinal);
Assert.True(call >= 0, code);
Assert.Contains(
"SetCRFloatResident(cr, 0, f1.d, f14.d);",
code[call..],
StringComparison.Ordinal);
Assert.Contains(
"PPC_PsNegInline(PPC_PsFromScalarInline(f1.d))",
code[call..],
StringComparison.Ordinal);
}
[Fact]
public void PacksImmediateForPairedConsumer()
{
var code = Emit(new IrCall("f2", "PPC_PsNeg", new[] { IrValue.Imm(7) }));
Assert.Contains(
"PPC_PsNegInline(PPC_PsFromScalarInline(7))",
code,
StringComparison.Ordinal);
}
[Fact]
public void PairedMoveCopiesBothArchitecturalLanes()
{
var code = Emit(new IrCall("f2", "PPC_PsMr", new[] { IrValue.Register("f7") }));
Assert.Contains(
"PpcSetPairedFprInline(f2, f7.d);",
code,
StringComparison.Ordinal);
Assert.DoesNotContain("f2.d = f7.d", code, StringComparison.Ordinal);
}
[Fact]
public void EmitsArchitecturallyAccurateScalarFloatHelpers()
{
var code = Emit(
new IrCall("f2", "PPC_Fmuls", new[]
{
IrValue.Register("f3"), IrValue.Register("f4")
}),
new IrCall("f5", "PPC_Fmadd", new[]
{
IrValue.Register("f6"), IrValue.Register("f7"), IrValue.Register("f8")
}),
new IrCall("f9", "PPC_Fmsub", new[]
{
IrValue.Register("f10"), IrValue.Register("f11"), IrValue.Register("f12")
}),
new IrCall("f13", "PPC_Fnmadd", new[]
{
IrValue.Register("f14"), IrValue.Register("f15"), IrValue.Register("f16")
}),
new IrCall("f17", "PPC_Fnmsub", new[]
{
IrValue.Register("f18"), IrValue.Register("f19"), IrValue.Register("f20")
}),
new IrBinary(
"f21",
IrValue.Register("f22"),
IrValue.Imm(0),
"frsp"));
Assert.Contains("PpcFmulsInline(", code, StringComparison.Ordinal);
Assert.Contains("PpcFmaddInline(", code, StringComparison.Ordinal);
Assert.Contains("PpcFmsubInline(", code, StringComparison.Ordinal);
Assert.Contains("PpcFnmaddInline(", code, StringComparison.Ordinal);
Assert.Contains("PpcFnmsubInline(", code, StringComparison.Ordinal);
Assert.Contains("PpcForceSingleValueInline(", code, StringComparison.Ordinal);
}
private static string Emit(params IrInstruction[] instructions)
{
var body = instructions.Concat(new[] { new IrReturn(null) }).ToArray();
var function = new IrFunction(
"scalar_paired_boundary",
"entry",
new[] { new IrBasicBlock("entry", body) });
var types = Enumerable.Range(0, 32).ToDictionary(
index => $"f{index}",
_ => (ValueRepresentation)ValueRepresentation.Float64,
StringComparer.OrdinalIgnoreCase);
types["r3"] = ValueRepresentation.UInt32;
types["ctr"] = ValueRepresentation.UInt32;
return new CxxLinearCodeGenerator().Emit(
0x80006200,
new SsaTransformer().Convert(function),
new FunctionAbiClassification(
"scalar_paired_boundary",
ValueRepresentation.Void),
new RepresentationEnvironment(types));
}
}
@@ -0,0 +1,180 @@
using System;
using System.IO;
using Translator.Core;
using Translator.Core.CodeGen;
using Translator.Core.Parsing.Dol;
using Translator.Core.Parsing.Rel;
using Xunit;
namespace Translator.Tests;
public class SdaAndGeneratorTests
{
[Fact]
public void RuntimeConfigGeneratorEmitsConfiguredConstants()
{
const uint entry = 0x80006000;
var dol = SyntheticDolFactory.Create(
entry,
sections:
[
SyntheticDolFactory.Text(0, entry, 0x60000000)
]);
var tempDir = Path.Combine(Path.GetTempPath(), "mkw_runtime_config_tests");
Directory.CreateDirectory(tempDir);
var output = Path.Combine(tempDir, "RuntimeConfig.generated.h");
RuntimeConfigGenerator.GenerateConfigHeader(0x8038F780u, 0x8038E9C8u, output);
var text = File.ReadAllText(output);
Assert.Contains("constexpr uint32_t SDA1_BASE = 0x8038F780u;", text);
Assert.Contains("constexpr uint32_t SDA2_BASE = 0x8038E9C8u;", text);
}
[Fact]
public void RuntimeConfigGeneratorDoesNotRewriteIdenticalContent()
{
const uint entry = 0x80006000;
var dol = SyntheticDolFactory.Create(
entry,
sections:
[
SyntheticDolFactory.Text(0, entry, 0x60000000)
]);
var tempDir = Path.Combine(Path.GetTempPath(), "mkw_runtime_config_write_tests");
Directory.CreateDirectory(tempDir);
var output = Path.Combine(tempDir, "RuntimeConfig.generated.h");
RuntimeConfigGenerator.GenerateConfigHeader(0x8038F780u, 0x8038E9C8u, output);
var sentinel = new DateTime(2001, 1, 1, 0, 0, 0, DateTimeKind.Utc);
File.SetLastWriteTimeUtc(output, sentinel);
RuntimeConfigGenerator.GenerateConfigHeader(0x8038F780u, 0x8038E9C8u, output);
Assert.Equal(sentinel, File.GetLastWriteTimeUtc(output));
}
[Fact]
public void DataSectionGeneratorEmbedsDolAndRelSections()
{
var dol = SyntheticDolFactory.Create(
0x80004000,
bssAddress: 0x80009000,
bssSize: 0x40,
sections:
[
SyntheticDolFactory.Text(0, 0x80004000, 0x60000000),
SyntheticDolFactory.Data(5, 0x80008000, 0xAA, 0xBB, 0xCC)
]);
var rel = new RelImage(new byte[] { 0x11, 0x22, 0x33 }, 0x80510000);
var tempDir = Path.Combine(Path.GetTempPath(), "mkw_data_section_tests");
Directory.CreateDirectory(tempDir);
var output = Path.Combine(tempDir, "data_sections_init_test.cpp");
DataSectionGenerator.Generate(dol, rel, output);
var text = File.ReadAllText(output);
Assert.Contains("kData__data", text);
Assert.Contains("kData_rel_module", text);
Assert.Contains("InitializeDataSections", text);
Assert.Contains("g_dataInitialized", text);
Assert.Contains("0x80008000", text);
Assert.Contains("0x80510000", text);
}
[Fact]
public void DataSectionGeneratorDoesNotEmitRuntimeBssClearAfterInitializedSections()
{
var dol = SyntheticDolFactory.Create(
0x80004000,
bssAddress: 0x80008000,
bssSize: 0x100,
sections:
[
SyntheticDolFactory.Data(5, 0x80008020, 0x41, 0x20, 0x00, 0x00)
]);
var tempDir = Path.Combine(Path.GetTempPath(), "mkw_data_section_bss_overlap_tests");
Directory.CreateDirectory(tempDir);
var output = Path.Combine(tempDir, "data_sections_init_overlap_test.cpp");
DataSectionGenerator.Generate(dol, rel: null, output);
var text = File.ReadAllText(output);
Assert.Contains("0x80008020", text);
Assert.Contains("BSS section @ 0x80008000 (256 bytes) - memory already zero-initialized", text);
Assert.DoesNotContain("std::memset", text);
}
[Fact]
public void DataSectionGeneratorCanWriteStagedBlobsWithFinalIncbinReferences()
{
var dol = SyntheticDolFactory.Create(
0x80004000,
sections:
[
SyntheticDolFactory.Data(5, 0x80008000, 0xAA, 0xBB, 0xCC)
]);
var tempDir = Path.Combine(Path.GetTempPath(), $"mkw_data_section_staging_{Guid.NewGuid():N}");
try
{
var stagedDirectory = Path.Combine(tempDir, "staging");
var finalBlobDirectory = Path.Combine(tempDir, "published", "data_sections_init_blobs");
var output = Path.Combine(stagedDirectory, "data_sections_init.cpp");
DataSectionGenerator.Generate(
dol,
rel: null,
output,
blobReferenceDirectory: finalBlobDirectory);
var assembly = File.ReadAllText(Path.Combine(stagedDirectory, "data_sections_init_blobs.S"));
var expectedReference = Path.GetFullPath(Path.Combine(finalBlobDirectory, "_data.bin")).Replace('\\', '/');
Assert.Contains($".incbin \"{expectedReference}\"", assembly, StringComparison.Ordinal);
Assert.True(File.Exists(Path.Combine(stagedDirectory, "data_sections_init_blobs", "_data.bin")));
Assert.False(File.Exists(Path.Combine(finalBlobDirectory, "_data.bin")));
}
finally
{
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, recursive: true);
}
}
}
[Fact]
public void DataSectionGeneratorChangesAssemblyWhenSameSizeBlobContentChanges()
{
var tempDir = Path.Combine(Path.GetTempPath(), $"mkw_data_section_hash_{Guid.NewGuid():N}");
try
{
var output = Path.Combine(tempDir, "data_sections_init.cpp");
var first = SyntheticDolFactory.Create(
0x80004000,
sections: [SyntheticDolFactory.Data(5, 0x80008000, 1, 2, 3, 4)]);
DataSectionGenerator.Generate(first, rel: null, output);
var assemblyPath = Path.Combine(tempDir, "data_sections_init_blobs.S");
var before = File.ReadAllText(assemblyPath);
var second = SyntheticDolFactory.Create(
0x80004000,
sections: [SyntheticDolFactory.Data(5, 0x80008000, 1, 2, 3, 5)]);
DataSectionGenerator.Generate(second, rel: null, output);
var after = File.ReadAllText(assemblyPath);
Assert.NotEqual(before, after);
Assert.Contains("sha256=", after, StringComparison.Ordinal);
}
finally
{
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, recursive: true);
}
}
}
}
@@ -0,0 +1,247 @@
using System.Collections.Generic;
using System.Linq;
using Translator.Core.Analysis.Ssa;
using Translator.Core.Ir;
using Xunit;
namespace Translator.Tests;
public class SsaTests
{
[Fact]
public void InsertsPhiAndRenamesUses()
{
var blocks = new List<IrBasicBlock>
{
new("entry", new List<IrInstruction>
{
new IrAssign("r3", IrValue.Imm(5)),
new IrBranch("bne", "then", "else")
}),
new("then", new List<IrInstruction>
{
new IrAssign("r3", IrValue.Imm(1)),
new IrJump("merge")
}),
new("else", new List<IrInstruction>
{
new IrAssign("r3", IrValue.Imm(2)),
new IrJump("merge")
}),
new("merge", new List<IrInstruction>
{
new IrBinary("r4", IrValue.Register("r3"), IrValue.Imm(0), "add"),
new IrReturn(IrValue.Register("r4"))
})
};
var func = new IrFunction("test", "entry", blocks);
var ssa = new SsaTransformer().Convert(func);
ssa.ValidateUseDef();
var merge = ssa.Function.Blocks.Single(b => b.Label == "merge");
Assert.IsType<IrPhi>(merge.Instructions[0]);
var phi = (IrPhi)merge.Instructions[0];
Assert.Equal(2, phi.Sources.Count);
Assert.Contains("then", phi.Sources.Keys);
Assert.Contains("else", phi.Sources.Keys);
// All registers should be versioned.
Assert.All(ssa.Function.Blocks.SelectMany(b => b.Instructions), ins =>
{
switch (ins)
{
case IrAssign a:
Assert.Contains("_", a.Destination);
break;
case IrBinary b:
Assert.Contains("_", b.Destination);
Assert.Contains("_", b.Left.RegisterName ?? string.Empty);
break;
}
});
}
[Fact]
public void FloatPhiAllowsImplicitLiveIn()
{
var blocks = new List<IrBasicBlock>
{
new("entry", new List<IrInstruction>
{
new IrBranch("bne", "left", "right")
}),
new("left", new List<IrInstruction>
{
new IrAssign("f1", IrValue.Imm(0)),
new IrJump("merge")
}),
new("right", new List<IrInstruction>
{
new IrJump("merge")
}),
new("merge", new List<IrInstruction>
{
new IrReturn(IrValue.Register("f1"))
})
};
var func = new IrFunction("float_phi", "entry", blocks);
var ssa = new SsaTransformer().Convert(func);
ssa.ValidateUseDef();
var merge = ssa.Function.Blocks.Single(b => b.Label == "merge");
var phi = Assert.IsType<IrPhi>(merge.Instructions[0]);
Assert.True(phi.Sources.ContainsKey("left"));
Assert.True(phi.Sources.ContainsKey("right"));
}
[Fact]
public void ConditionRegisterPhiAllowsImplicitLiveIn()
{
var blocks = new List<IrBasicBlock>
{
new("entry", new List<IrInstruction>
{
new IrBranch("bne", "left", "right")
}),
new("left", new List<IrInstruction>
{
new IrBinary("cr1", IrValue.Register("r3"), IrValue.Imm(0), "sub"),
new IrJump("merge")
}),
new("right", new List<IrInstruction>
{
new IrJump("merge")
}),
new("merge", new List<IrInstruction>
{
new IrBranch("eq", "exit_true", "exit_false", "cr1")
}),
new("exit_true", new List<IrInstruction>
{
new IrReturn(null)
}),
new("exit_false", new List<IrInstruction>
{
new IrReturn(null)
})
};
var func = new IrFunction("cr_phi", "entry", blocks);
var ssa = new SsaTransformer().Convert(func);
ssa.ValidateUseDef();
var merge = ssa.Function.Blocks.Single(b => b.Label == "merge");
var phi = Assert.IsType<IrPhi>(merge.Instructions[0]);
Assert.True(phi.Sources.ContainsKey("left"));
Assert.True(phi.Sources.ContainsKey("right"));
}
[Theory]
[InlineData("gqr0")]
[InlineData("gqr7")]
[InlineData("hid0")]
[InlineData("hid1")]
[InlineData("hid2")]
[InlineData("srr0")]
[InlineData("srr1")]
public void ContextBackedSpecialRegisterPhiAllowsImplicitLiveIn(string register)
{
var function = new IrFunction("special_phi", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrBranch("bne", "written", "live_in")
}),
new IrBasicBlock("written", new IrInstruction[]
{
new IrAssign(register, IrValue.Imm(1)),
new IrJump("merge")
}),
new IrBasicBlock("live_in", new IrInstruction[]
{
new IrJump("merge")
}),
new IrBasicBlock("merge", new IrInstruction[]
{
new IrReturn(IrValue.Register(register))
})
});
var ssa = new SsaTransformer().Convert(function);
ssa.ValidateUseDef();
var phi = Assert.IsType<IrPhi>(
ssa.Function.Blocks.Single(block => block.Label == "merge").Instructions[0]);
Assert.Equal(2, phi.Sources.Count);
Assert.Contains("written", phi.Sources.Keys);
Assert.Contains("live_in", phi.Sources.Keys);
}
[Fact]
public void SetCrOperandsAreRenamed()
{
var blocks = new List<IrBasicBlock>
{
new("entry", new List<IrInstruction>
{
new IrAssign("r3", IrValue.Imm(1)),
new IrAssign("r4", IrValue.Imm(2)),
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Register("r4"), true),
new IrReturn(IrValue.Register("r3"))
})
};
var func = new IrFunction("set_cr", "entry", blocks);
var ssa = new SsaTransformer().Convert(func);
var block = ssa.Function.Blocks.Single();
var setCr = Assert.IsType<IrSetCrField>(block.Instructions[2]);
Assert.NotNull(setCr.Left.RegisterName);
Assert.NotNull(setCr.Right.RegisterName);
Assert.Contains('_', setCr.Left.RegisterName!);
Assert.Contains('_', setCr.Right.RegisterName!);
}
[Fact]
public void SiblingDefinitionsReceiveGloballyUniqueVersions()
{
var function = new IrFunction("sibling_versions", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrBranch("bne", "left", "right")
}),
new IrBasicBlock("left", new IrInstruction[]
{
new IrAssign("f1", IrValue.Imm(1)),
new IrJump("merge")
}),
new IrBasicBlock("right", new IrInstruction[]
{
new IrAssign("f1", IrValue.Imm(2)),
new IrJump("merge")
}),
new IrBasicBlock("merge", new IrInstruction[]
{
new IrReturn(IrValue.Register("f1"))
})
});
var ssa = new SsaTransformer().Convert(function);
ssa.ValidateUseDef();
var leftDefinition = Assert.IsType<IrAssign>(
ssa.Function.Blocks.Single(block => block.Label == "left").Instructions[0]).Destination;
var rightDefinition = Assert.IsType<IrAssign>(
ssa.Function.Blocks.Single(block => block.Label == "right").Instructions[0]).Destination;
var phi = Assert.IsType<IrPhi>(
ssa.Function.Blocks.Single(block => block.Label == "merge").Instructions[0]);
Assert.NotEqual(leftDefinition, rightDefinition);
Assert.Equal(leftDefinition, phi.Sources["left"]);
Assert.Equal(rightDefinition, phi.Sources["right"]);
}
}
@@ -0,0 +1,66 @@
using Translator.Core.Analysis;
using Translator.Core.Ir;
namespace Translator.Tests;
public sealed class StackAddressFactsTests
{
[Fact]
public void TracksFramePointerCopiesAndSubtractions()
{
var function = new IrFunction("stack_facts", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrBinary("r11", IrValue.Register("r1"), IrValue.Imm(64), "add"),
new IrAssign("save_area", IrValue.Register("r11")),
new IrBinary("slot", IrValue.Register("save_area"), IrValue.Imm(8), "sub")
})
});
var facts = StackAddressFacts.Build(function);
Assert.True(facts.TryResolve(new IrAddress("r11", -28), out var frameOffset));
Assert.Equal(36, frameOffset);
Assert.True(facts.TryResolve(new IrAddress("slot", 0), out var slotOffset));
Assert.Equal(56, slotOffset);
Assert.True(facts.ContainsTemporary("r11"));
Assert.True(facts.ContainsTemporary("save_area"));
}
[Fact]
public void TreatsEveryR1VersionAsTheCurrentStackRoot()
{
var facts = StackAddressFacts.Build(new IrFunction("r1_versions", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrAssign("r1_7", IrValue.Register("r4"))
})
}));
Assert.True(facts.TryResolve(new IrAddress("r1_7", 12), out var offset));
Assert.Equal(12, offset);
}
[Fact]
public void DoesNotAddCfgOrCommutedExpressionSemantics()
{
var facts = StackAddressFacts.Build(new IrFunction("simple_only", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrBinary("commuted", IrValue.Imm(8), IrValue.Register("r1"), "add"),
new IrPhi("joined", new Dictionary<string, string>
{
["entry"] = "r1",
["other"] = "r1"
})
})
}));
Assert.False(facts.ContainsTemporary("commuted"));
Assert.False(facts.ContainsTemporary("joined"));
Assert.False(facts.TryResolve(new IrAddress("commuted", 0), out _));
}
}
@@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using Translator.Core.Analysis;
using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis.Ssa;
using Translator.Core.CodeGen;
using Translator.Core.Ir;
using Translator.Core.Representation;
using Xunit;
namespace Translator.Tests;
/// <summary>
/// Pins that a resident call's fast arm carries no CpuContext traffic, while the
/// <c>InvokeDirectCpu</c> arm (used when a mod overlay replaces the callee) keeps
/// its own flush/reload pair.
/// </summary>
public class StateFreeResidentMarshallingTests
{
private const uint CallerAddress = 0x80001000u;
private const uint CalleeAddress = 0x80002000u;
private static GuestAbiContract Contract(
uint gprRead = 0,
uint gprWrite = 0,
byte crRead = 0,
byte crWrite = 0,
bool readsXer = false,
bool writesXer = false) =>
new(
GprReadBeforeWriteMask: gprRead,
GprPossibleWriteMask: gprWrite,
GprReturnMask: 0,
FprReadBeforeWriteMask: 0,
FprPossibleWriteMask: 0,
FprReturnMask: 0,
CrReadBeforeWriteMask: crRead,
CrPossibleWriteMask: crWrite,
ReadsXerBeforeWrite: readsXer,
MayWriteXer: writesXer,
ReadsCtrBeforeWrite: false,
MayWriteCtr: false,
ReadsLrBeforeWrite: false,
MayWriteLr: false,
BoundaryFlags: GuestCallBoundaryFlags.None,
DirectCallTargets: Array.Empty<uint>());
private static string Emit(IrFunction function, GuestAbiContract calleeContract)
{
return new CxxLinearCodeGenerator().Emit(
CallerAddress,
new SsaTransformer().Convert(function),
new FunctionAbiClassification(function.Name, ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
stateFreeAbiContracts: new Dictionary<uint, GuestAbiContract> { [CalleeAddress] = calleeContract },
stateFreeCallSymbols: new Dictionary<uint, string> { [CalleeAddress] = "callee_native" });
}
private static IrFunction Caller() =>
new("state_free_caller", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r4"), "add"),
new IrCall(string.Empty, "0x80002000", Array.Empty<IrValue>()),
new IrBinary("r3", IrValue.Register("r3"), IrValue.Register("r4"), "add"),
new IrReturn(null)
})
});
[Fact]
public void FastArmPassesResidentLocalsAndWritesResultsBackIntoThem()
{
var code = Emit(Caller(), Contract(gprRead: 1u << 3, gprWrite: 1u << 3));
var fastArm = code.IndexOf("callee_native(r3)", StringComparison.Ordinal);
Assert.True(fastArm > 0, $"expected the fast arm to pass the resident local:\n{code}");
Assert.DoesNotContain("callee_native(ctx->gpr[3])", code, StringComparison.Ordinal);
// One output packs into a bare uint64_t, unpacked straight into the local.
var resultStore = code.IndexOf(" r3 = static_cast<uint32_t>(state_free_result_80002000_", StringComparison.Ordinal);
Assert.True(resultStore > fastArm, $"expected the result to land in the local:\n{code}");
}
[Fact]
public void SlowArmKeepsItsOwnCompleteFlushAndReload()
{
var code = Emit(Caller(), Contract(gprRead: 1u << 3, gprWrite: 1u << 3));
var elseArm = code.IndexOf("} else {", StringComparison.Ordinal);
var call = code.IndexOf("InvokeDirectCpu<0x80002000u>(ctx);", StringComparison.Ordinal);
var flush = code.IndexOf(" ctx->gpr[3] = r3;", StringComparison.Ordinal);
var reload = code.IndexOf(" r3 = ctx->gpr[3];", StringComparison.Ordinal);
Assert.True(elseArm > 0 && flush > elseArm && call > flush && reload > call,
$"the InvokeDirectCpu arm must publish and re-read the locals itself:\n{code}");
}
/// <summary>
/// The flush that used to bracket the whole call site is gone; only the two
/// registers an HLE interrupt handler reads out of the architectural copy
/// survive, and only when this frame actually wrote them.
/// </summary>
[Fact]
public void FastArmEmitsNoBoundaryFlushOrReload()
{
var code = Emit(Caller(), Contract(gprRead: 1u << 3, gprWrite: 1u << 3));
var fastArm = code.IndexOf("if (MkwStateFreeAbiEnabled(", StringComparison.Ordinal);
Assert.True(fastArm > 0);
// Nothing between the last body statement and the guard.
var beforeGuard = code[..fastArm];
Assert.DoesNotContain("ctx->gpr[3] = r3;", beforeGuard, StringComparison.Ordinal);
Assert.DoesNotContain("ctx->gpr[4] = r4;", beforeGuard, StringComparison.Ordinal);
// r4 is only read by this frame and is not part of the callee interface,
// so it is neither passed nor reloaded on the fast arm.
Assert.DoesNotContain("callee_native(r3, r4)", code, StringComparison.Ordinal);
}
[Fact]
public void CrAndXerInterfaceRegistersUseTheResidentLocals()
{
var contract = Contract(gprRead: 1u << 3, crRead: 0x80, crWrite: 0x80, readsXer: true);
var function = new IrFunction("state_free_cr_caller", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrSetCrField(0, IrValue.Register("r3"), IrValue.Imm(0), true),
new IrCall(string.Empty, "0x80002000", Array.Empty<IrValue>()),
new IrReturn(null)
})
});
var code = Emit(function, contract);
Assert.Contains("callee_native(r3, cr, xer)", code, StringComparison.Ordinal);
Assert.Contains(" cr = static_cast<uint32_t>(state_free_result_80002000_", code, StringComparison.Ordinal);
// XER is published for the interrupt-handler invariant, never reloaded.
Assert.DoesNotContain(" xer = ctx->xer;\n r3", code, StringComparison.Ordinal);
}
}
@@ -0,0 +1,94 @@
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using Translator.Core.Parsing.Dol;
namespace Translator.Tests;
internal static class SyntheticDolFactory
{
private const int HeaderSize = 0x100;
internal readonly record struct SectionSpec(bool IsText, int Slot, uint Address, byte[] Data);
public static SectionSpec Text(int slot, uint address, params uint[] words) =>
new(true, slot, address, WordsToBytes(words));
public static SectionSpec Data(int slot, uint address, params byte[] data) =>
new(false, slot, address, data);
public static DolFile Create(uint entryPoint, uint bssAddress = 0, uint bssSize = 0, params SectionSpec[] sections)
=> DolFile.Load(new MemoryStream(CreateBytes(entryPoint, bssAddress, bssSize, sections), writable: false));
public static byte[] CreateBytes(uint entryPoint, uint bssAddress = 0, uint bssSize = 0, params SectionSpec[] sections)
{
var textOffsets = new uint[7];
var dataOffsets = new uint[11];
var textAddresses = new uint[7];
var dataAddresses = new uint[11];
var textSizes = new uint[7];
var dataSizes = new uint[11];
var blobs = new List<(uint Offset, byte[] Data)>();
var nextOffset = (uint)HeaderSize;
foreach (var section in sections)
{
if (section.IsText)
{
textOffsets[section.Slot] = nextOffset;
textAddresses[section.Slot] = section.Address;
textSizes[section.Slot] = (uint)section.Data.Length;
}
else
{
dataOffsets[section.Slot] = nextOffset;
dataAddresses[section.Slot] = section.Address;
dataSizes[section.Slot] = (uint)section.Data.Length;
}
blobs.Add((nextOffset, section.Data));
nextOffset += (uint)section.Data.Length;
}
var image = new byte[nextOffset];
WriteArray(image, 0x00, textOffsets);
WriteArray(image, 0x1C, dataOffsets);
WriteArray(image, 0x48, textAddresses);
WriteArray(image, 0x64, dataAddresses);
WriteArray(image, 0x90, textSizes);
WriteArray(image, 0xAC, dataSizes);
WriteU32(image, 0xD8, bssAddress);
WriteU32(image, 0xDC, bssSize);
WriteU32(image, 0xE0, entryPoint);
foreach (var (offset, data) in blobs)
{
data.CopyTo(image.AsSpan((int)offset));
}
return image;
}
private static byte[] WordsToBytes(params uint[] words)
{
var bytes = new byte[words.Length * 4];
for (var i = 0; i < words.Length; i++)
{
BinaryPrimitives.WriteUInt32BigEndian(bytes.AsSpan(i * 4, 4), words[i]);
}
return bytes;
}
private static void WriteArray(byte[] image, int offset, IReadOnlyList<uint> values)
{
for (var i = 0; i < values.Count; i++)
{
WriteU32(image, offset + (i * 4), values[i]);
}
}
private static void WriteU32(byte[] image, int offset, uint value) =>
BinaryPrimitives.WriteUInt32BigEndian(image.AsSpan(offset, 4), value);
}
@@ -0,0 +1,159 @@
using Translator.Core.IO;
using Xunit;
namespace Translator.Tests;
public sealed class TransactionalDirectoryOutputTests
{
[Fact]
public void SuccessfulGenerationPreservesUnchangedTimestampsAndReconcilesMembership()
{
var root = CreateTempRoot();
try
{
var output = Path.Combine(root, "generated-mod");
Directory.CreateDirectory(Path.Combine(output, "cpp"));
var unchangedPath = Path.Combine(output, "cpp", "unchanged.cpp");
File.WriteAllText(unchangedPath, "same");
File.WriteAllText(Path.Combine(output, "cpp", "changed.cpp"), "old");
File.WriteAllText(Path.Combine(output, "cpp", "removed.cpp"), "stale");
var originalTimestamp = new DateTime(2001, 2, 3, 4, 5, 6, DateTimeKind.Utc);
File.SetLastWriteTimeUtc(unchangedPath, originalTimestamp);
var outcome = TransactionalDirectoryOutput.GenerateAndPublish(
output,
staging =>
{
Assert.Empty(Directory.EnumerateFileSystemEntries(staging));
Directory.CreateDirectory(Path.Combine(staging, "cpp"));
File.WriteAllText(Path.Combine(staging, "cpp", "unchanged.cpp"), "same");
File.WriteAllText(Path.Combine(staging, "cpp", "changed.cpp"), "new");
File.WriteAllText(Path.Combine(staging, "cpp", "added.cpp"), "added");
return 0;
});
Assert.Equal(0, outcome.ExitCode);
Assert.Empty(outcome.Warnings);
var result = Assert.IsType<TransactionalDirectoryOutput.PublishResult>(outcome.Publication);
Assert.Equal(1, result.AddedFiles);
Assert.Equal(1, result.UpdatedFiles);
Assert.Equal(1, result.RemovedFiles);
Assert.Equal(1, result.UnchangedFiles);
Assert.Equal(originalTimestamp, File.GetLastWriteTimeUtc(Path.Combine(output, "cpp", "unchanged.cpp")));
Assert.Equal("new", File.ReadAllText(Path.Combine(output, "cpp", "changed.cpp")));
Assert.Equal("added", File.ReadAllText(Path.Combine(output, "cpp", "added.cpp")));
Assert.False(File.Exists(Path.Combine(output, "cpp", "removed.cpp")));
}
finally
{
Directory.Delete(root, recursive: true);
}
}
[Fact]
public void FailedGenerationLeavesLastKnownGoodOutputUntouched()
{
var root = CreateTempRoot();
try
{
var output = Path.Combine(root, "generated-mod");
Directory.CreateDirectory(output);
var livePath = Path.Combine(output, "mod_manifest.json");
File.WriteAllText(livePath, "last-known-good");
var originalTimestamp = new DateTime(2002, 3, 4, 5, 6, 7, DateTimeKind.Utc);
File.SetLastWriteTimeUtc(livePath, originalTimestamp);
var outcome = TransactionalDirectoryOutput.GenerateAndPublish(
output,
staging =>
{
File.WriteAllText(Path.Combine(staging, "mod_manifest.json"), "incomplete");
File.WriteAllText(Path.Combine(staging, "stale-discovery.cpp"), "must not publish");
return 3;
});
Assert.Equal(3, outcome.ExitCode);
Assert.Null(outcome.Publication);
Assert.Equal("last-known-good", File.ReadAllText(livePath));
Assert.Equal(originalTimestamp, File.GetLastWriteTimeUtc(livePath));
Assert.False(File.Exists(Path.Combine(output, "stale-discovery.cpp")));
}
finally
{
Directory.Delete(root, recursive: true);
}
}
[Fact]
public void CleanupFailureDoesNotMaskProducerFailure()
{
var root = CreateTempRoot();
FileStream? lockedStagingFile = null;
try
{
var output = Path.Combine(root, "generated-mod");
var error = Assert.Throws<InvalidOperationException>(() =>
TransactionalDirectoryOutput.GenerateAndPublish(
output,
staging =>
{
lockedStagingFile = new FileStream(
Path.Combine(staging, "locked.cpp"),
FileMode.Create,
FileAccess.ReadWrite,
FileShare.None);
throw new InvalidOperationException("producer failure");
}));
Assert.Equal("producer failure", error.Message);
Assert.False(Directory.Exists(output));
}
finally
{
lockedStagingFile?.Dispose();
Directory.Delete(root, recursive: true);
}
}
[Fact]
public void FailedGenerationExposesStagingDiagnosticsBeforeCleanup()
{
var root = CreateTempRoot();
try
{
var output = Path.Combine(root, "generated-mod");
string? capturedReport = null;
var capturedExitCode = 0;
var outcome = TransactionalDirectoryOutput.GenerateAndPublish(
output,
staging =>
{
File.WriteAllText(Path.Combine(staging, "runtime_patch_slots.txt"), "two executable writes");
return 2;
},
(staging, producerExitCode) =>
{
capturedExitCode = producerExitCode;
capturedReport = File.ReadAllText(Path.Combine(staging, "runtime_patch_slots.txt"));
});
Assert.Equal(2, outcome.ExitCode);
Assert.Null(outcome.Publication);
Assert.Equal(2, capturedExitCode);
Assert.Equal("two executable writes", capturedReport);
Assert.False(Directory.Exists(output));
}
finally
{
Directory.Delete(root, recursive: true);
}
}
private static string CreateTempRoot()
{
var root = Path.Combine(Path.GetTempPath(), $"translator-transactional-output-{Guid.NewGuid():N}");
Directory.CreateDirectory(root);
return root;
}
}
@@ -0,0 +1,210 @@
using System.Reflection;
using System.Threading.Tasks;
using Translator.Cli.Configuration;
using Translator.Core.Mods;
using Xunit;
namespace Translator.Tests;
public sealed class TranslateRecursiveRuntimeConfigTests
{
[Fact]
public void UnsupportedOpcodeFailsStrictTranslation()
{
var root = Path.Combine(Path.GetTempPath(), $"translator-unsupported-opcode-{Guid.NewGuid():N}");
Directory.CreateDirectory(root);
try
{
const uint entry = 0x80001000u;
File.WriteAllBytes(
Path.Combine(root, "main.dol"),
SyntheticDolFactory.CreateBytes(
entry,
sections: [SyntheticDolFactory.Text(
0,
entry,
0x04000000u, // unassigned primary opcode; deliberately unsupported by the lifter
0x4E800020u)])); // blr
var projectPath = Path.Combine(root, "recomp.yml");
File.WriteAllText(
projectPath,
"""
schema_version: 1
project:
id: unsupported-opcode-test
memory:
sda_base: 0x80002000
sda2_base: 0x80003000
inputs:
dol:
path: main.dol
translation:
entry_points: [0x80001000]
output:
root: generated
""");
Assert.False(TranslationProjectConfig.Load(projectPath).Translation.AllowUnsupportedInstructions);
var strictError = Assert.Throws<TargetInvocationException>(() => InvokeCli(
"translate-recursive",
$"0x{entry:X8}",
"--project", projectPath,
"--outdir", Path.Combine(root, "strict", "functions"),
"--output-metadata", Path.Combine(root, "strict", "metadata.json"),
"--threads", "1"));
Assert.Contains("UNIMPLEMENTED", strictError.InnerException?.ToString(), StringComparison.Ordinal);
}
finally
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}
[Fact]
public void RepeatedRunsReuseUnchangedOutputsAndPruneStaleFiles()
{
var root = Path.Combine(Path.GetTempPath(), $"translator-recursive-prune-{Guid.NewGuid():N}");
Directory.CreateDirectory(root);
try
{
const uint entry = 0x80001000u;
File.WriteAllBytes(
Path.Combine(root, "main.dol"),
SyntheticDolFactory.CreateBytes(
entry,
sections: [SyntheticDolFactory.Text(
0,
entry,
0x60000000u, // nop
0x60000000u, // nop
0x4E800020u)])); // blr
var projectPath = Path.Combine(root, "recomp.yml");
File.WriteAllText(
projectPath,
"""
schema_version: 1
project:
id: recursive-prune-test
display_name: Recursive Prune Test
memory:
sda_base: 0x80002000
sda2_base: 0x80003000
inputs:
dol:
path: main.dol
translation:
entry_points: [0x80001000]
output:
root: generated
""");
var stagedFunctions = Path.Combine(root, "staging", "functions");
var stagedOutputMetadata = Path.Combine(root, "staging", "functions_metadata.json");
var exitCode = InvokeCli(
"translate-recursive",
$"0x{entry:X8}",
"--project",
projectPath,
"--outdir",
stagedFunctions,
"--output-metadata",
stagedOutputMetadata,
"--threads",
"1");
Assert.Equal(0, exitCode);
var firstMetadata = BaseTranslationOutputMetadataFile.Read(stagedOutputMetadata);
Assert.Null(firstMetadata.TranslationIdentityHash);
Assert.Equal(0, firstMetadata.Quality.UnsupportedInstructionCount);
Assert.Equal(0, firstMetadata.Quality.InvalidSsaFunctionCount);
Assert.Single(firstMetadata.Functions);
Assert.Single(Directory.GetFiles(stagedFunctions, "*.cpp"));
var generatedPath = Directory.GetFiles(stagedFunctions, "*.cpp").Single();
var unchangedTimestamp = new DateTime(2004, 5, 6, 7, 8, 10, DateTimeKind.Utc);
File.SetLastWriteTimeUtc(generatedPath, unchangedTimestamp);
// A populated output tree is not an input to recursive discovery.
// This stale filename used to become a synthetic function boundary
// and truncate the second translation at entry + 4.
File.WriteAllText(Path.Combine(stagedFunctions, $"func_{entry + 4:X8}.cpp"), "stale output hint");
var metadataTimestamp = new DateTime(2004, 5, 6, 7, 8, 12, DateTimeKind.Utc);
File.SetLastWriteTimeUtc(stagedOutputMetadata, metadataTimestamp);
var secondExitCode = InvokeCli(
"translate-recursive",
$"0x{entry:X8}",
"--project",
projectPath,
"--outdir",
stagedFunctions,
"--output-metadata",
stagedOutputMetadata,
"--threads",
"1");
Assert.Equal(0, secondExitCode);
Assert.Equal(unchangedTimestamp, File.GetLastWriteTimeUtc(generatedPath));
Assert.Equal(metadataTimestamp, File.GetLastWriteTimeUtc(stagedOutputMetadata));
var stalePath = Path.Combine(stagedFunctions, "func_80001004.cpp");
var unlistedPath = Path.Combine(stagedFunctions, "unlisted.cpp");
File.WriteAllText(stalePath, "// stale");
File.WriteAllText(unlistedPath, "// not translator-owned");
BaseTranslationOutputMetadataFile.WriteIfChangedAtomic(
stagedOutputMetadata,
BaseTranslationOutputMetadata.Create(
firstMetadata.Functions.Concat([
new BaseTranslationFunctionMetadata(
"func_80001004.cpp",
8,
new string('e', 64),
0x80001004u,
[])
]),
TranslationQualityMetadata.Clean));
var pruneExitCode = InvokeCli(
"translate-recursive",
$"0x{entry:X8}",
"--project",
projectPath,
"--outdir",
stagedFunctions,
"--output-metadata",
stagedOutputMetadata,
"--prune-stale",
"--threads",
"1");
Assert.Equal(0, pruneExitCode);
Assert.False(File.Exists(stalePath));
Assert.True(File.Exists(unlistedPath));
}
finally
{
if (Directory.Exists(root))
{
Directory.Delete(root, recursive: true);
}
}
}
private static int InvokeCli(params string[] args)
{
var entryPoint = typeof(TranslationProjectConfig).Assembly.EntryPoint;
Assert.NotNull(entryPoint);
var result = entryPoint!.Invoke(null, new object?[] { args });
return result switch
{
int exitCode => exitCode,
Task<int> intTask => intTask.GetAwaiter().GetResult(),
Task task => CompleteTask(task),
null => 0,
_ => throw new InvalidOperationException($"Unexpected CLI entry point result type: {result.GetType().FullName}")
};
}
private static int CompleteTask(Task task)
{
task.GetAwaiter().GetResult();
return 0;
}
}
@@ -0,0 +1,374 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Translator.Core.Build;
using Translator.Core.Mods;
using Xunit;
namespace Translator.Tests;
public sealed class TranslatedBuildShardEmitterTests
{
[Fact]
public void RemainingWeightPartitionDoesNotCreateOversizedTailBin()
{
var weights = Enumerable.Repeat(300L, 10).ToArray();
var first = TranslatedBuildShardEmitter.PartitionWeightedIndices(weights, 4);
var second = TranslatedBuildShardEmitter.PartitionWeightedIndices(weights, 4);
Assert.Equal(new[] { 2, 3, 2, 3 }, first.Select(static group => group.Count));
Assert.Equal(
first.Select(static group => string.Join(",", group)),
second.Select(static group => string.Join(",", group)));
Assert.All(first, group => Assert.InRange(group.Sum(index => weights[index]), 600, 900));
}
[Fact]
public void EmitsDeterministicSmallTraitShardsWithoutCodeMap()
{
var root = Path.Combine(Path.GetTempPath(), $"translator-build-shards-{Guid.NewGuid():N}");
var functions = Path.Combine(root, "functions");
var native = Path.Combine(root, "runtime", "src");
var mod = Path.Combine(root, "mod", "cpp");
var output = Path.Combine(root, "out");
Directory.CreateDirectory(functions);
Directory.CreateDirectory(native);
Directory.CreateDirectory(mod);
try
{
const string caller = """
#include "abi_bridge.h"
extern "C" void func_80001000(CpuContext* ctx) {
InvokeDirectCpu<0x80002000u>(ctx);
}
// RECOMP_REGISTRATION base 0x80001000 func_80001000 preserves=true fpr_mask=0x00000000
""";
const string target = """
#include "abi_bridge.h"
extern "C" void func_80002000(CpuContext*) {}
// RECOMP_REGISTRATION base 0x80002000 func_80002000 preserves=true fpr_mask=0x00000000
""";
const string runtimeOverridden = """
#include "abi_bridge.h"
extern "C" void func_80003000(CpuContext*) {}
// RECOMP_REGISTRATION base 0x80003000 func_80003000 preserves=true fpr_mask=0x00000000
""";
const string nativeOverridden = """
#include "abi_bridge.h"
extern "C" void func_80004000(CpuContext*) {}
// RECOMP_REGISTRATION base 0x80004000 func_80004000 preserves=true fpr_mask=0x00000000
""";
Write(Path.Combine(functions, "func_80001000.cpp"), caller);
Write(Path.Combine(functions, "func_80002000.cpp"), target);
Write(Path.Combine(functions, "func_80003000.cpp"), runtimeOverridden);
Write(Path.Combine(functions, "func_80004000.cpp"), nativeOverridden);
Write(Path.Combine(native, "override.cpp"),
"REGISTER_TRANSLATED_FUNCTION(0x80003000, RuntimeOverride_80003000);\n" +
"REGISTER_NATIVE_FUNCTION(0x80004000, NativeOverride_80004000);\n");
const string modSource = """
#include "abi_bridge.h"
extern "C" void rr_80002000(CpuContext*) {}
// RECOMP_REGISTRATION mod 0x80002000 rr_80002000 "rr_80002000" preserves=false fpr_mask=0x00004000 priority=100 module_id=7
""";
Write(Path.Combine(mod, "rr_80002000.cpp"), modSource);
var metadataPath = Path.Combine(root, "base_output.json");
BaseTranslationOutputMetadataFile.WriteIfChangedAtomic(metadataPath,
BaseTranslationOutputMetadata.Create([
Metadata("func_80001000.cpp", 0x80001000u, caller),
Metadata("func_80002000.cpp", 0x80002000u, target),
Metadata("func_80003000.cpp", 0x80003000u, runtimeOverridden),
Metadata("func_80004000.cpp", 0x80004000u, nativeOverridden)
], TranslationQualityMetadata.Clean, "identity"));
var manifestPath = Path.Combine(root, "base_manifest.json");
Write(manifestPath, """
{"Functions":[
{"Start":2147487744,"End":2147487748,"Name":"func_80001000"},
{"Start":2147491840,"End":2147491844,"Name":"func_80002000"},
{"Start":2147495936,"End":2147495940,"Name":"func_80003000"},
{"Start":2147500032,"End":2147500036,"Name":"func_80004000"}
]}
""");
var resolvedPath = Path.Combine(root, "mod", "resolved_dispatch_profile.json");
Write(resolvedPath, """
{"Entries":[
{"Address":2147487744,"Symbol":"func_80001000","Name":"func_80001000","Kind":"base","Priority":0,"DirectCallAvailable":true,"PreservesNonvolatileFprs":true,"NonvolatileFprWriteMask":0,"MustRemainDynamicallyDispatchable":false},
{"Address":2147491840,"Symbol":"rr_80002000","Name":"rr_80002000","Kind":"rr","Priority":100,"DirectCallAvailable":true,"PreservesNonvolatileFprs":false,"NonvolatileFprWriteMask":16384,"MustRemainDynamicallyDispatchable":true}
]}
""");
var options = new TranslatedBuildShardOptions(
metadataPath, functions, output, native,
resolvedPath, mod, BaseShardCount: 2, ModShardCount: 1, RegistrationShardCount: 1);
var first = TranslatedBuildShardEmitter.Emit(options);
Assert.Equal(1, first.SharedBaseFunctionCount);
Assert.Equal(1, first.ProfileSensitiveTargetCount);
Assert.Equal(1, first.ProfileSensitiveCallerCount);
Assert.Equal(1, first.ModFunctionCount);
Assert.False(File.Exists(Path.Combine(root, "mod", "code.map")));
var shard = Directory.GetFiles(Path.Combine(output, "base_portable_sensitive"), "*.cpp").Single();
var shardText = File.ReadAllText(shard);
Assert.Contains("#include \"abi_bridge.h\"", shardText);
Assert.DoesNotContain("#include \"" + Path.GetFullPath(Path.Combine(functions, "func_80001000.cpp")).Replace('\\', '/'), shardText);
Assert.Contains("MKW_STATIC_TRANSLATED_CALL(0x80002000u, func_80002000, ctx);", shardText);
Assert.Contains("ApplyRuntimeCallOptions(Target, Context)", shardText);
var portableBaseTraits = Directory.GetFiles(Path.Combine(output, "base_portable_sensitive"), "*_traits.h").Single();
var portableRetroTraits = Directory.GetFiles(Path.Combine(output, "retro_portable_sensitive"), "*_traits.h").Single();
Assert.Contains("MKW_TRANSLATED_TRAIT(80002000, func_80002000,", File.ReadAllText(portableBaseTraits));
Assert.Contains("MKW_TRANSLATED_TRAIT(80002000, rr_80002000,", File.ReadAllText(portableRetroTraits));
var baseDispatch = Directory.GetFiles(Path.Combine(output, "base_dispatch"), "*.cpp").Single();
var baseDispatchText = File.ReadAllText(baseDispatch);
Assert.Contains("const StaticIndirectDispatchSegment kSegments[256]", baseDispatchText);
Assert.Contains("{0x80001000u, &func_80001000, 0x00000000u, false}", baseDispatchText);
Assert.Contains("{0x80002000u, &func_80002000, 0x00000000u, false}", baseDispatchText);
Assert.Contains("{0x80003000u, &RuntimeOverride_80003000, 0xFFFFC000u, false}", baseDispatchText);
Assert.DoesNotContain("0x80004000u", baseDispatchText);
var retroDispatch = Directory.GetFiles(Path.Combine(output, "retro_rewind_dispatch"), "*.cpp").Single();
var retroDispatchText = File.ReadAllText(retroDispatch);
Assert.Contains("{0x80002000u, &rr_80002000, 0x00004000u, false}", retroDispatchText);
Assert.DoesNotContain("{0x80002000u, &func_80002000", retroDispatchText);
// A function claimed by a native registration is excluded from the
// translated graph entirely rather than emitted and then overridden.
Assert.DoesNotContain(
"func_80003000",
Directory.GetFiles(output, "*.cpp", SearchOption.AllDirectories)
.Select(File.ReadAllText)
.Aggregate(string.Concat));
var timestamps = Directory.GetFiles(output, "*", SearchOption.AllDirectories)
.ToDictionary(Path.GetFullPath, File.GetLastWriteTimeUtc, StringComparer.OrdinalIgnoreCase);
TranslatedBuildShardEmitter.Emit(options);
foreach (var (path, timestamp) in timestamps)
Assert.Equal(timestamp, File.GetLastWriteTimeUtc(path));
}
finally
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}
[Fact]
public void BindsDirectCallsToTheSelectedProfileWinner()
{
var root = Path.Combine(Path.GetTempPath(), $"translator-same-tu-calls-{Guid.NewGuid():N}");
var functions = Path.Combine(root, "functions");
var native = Path.Combine(root, "runtime", "src");
var output = Path.Combine(root, "out");
Directory.CreateDirectory(functions);
Directory.CreateDirectory(native);
try
{
// 0x80001000 shares a shard with both of its targets: 0x80002000 has
// the same winner in every profile, 0x80003000 writes nonvolatile FPRs.
const string commonCaller = """
#include "abi_bridge.h"
extern "C" void func_80001000(CpuContext* ctx) {
InvokeDirectCpu<0x80002000u>(ctx);
InvokeDirectCpu<0x80003000u>(ctx);
}
// RECOMP_REGISTRATION base 0x80001000 func_80001000 preserves=true fpr_mask=0x00000000
""";
const string invariantTarget = """
#include "abi_bridge.h"
extern "C" void func_80002000(CpuContext*) {}
// RECOMP_REGISTRATION base 0x80002000 func_80002000 preserves=true fpr_mask=0x00000000
""";
const string fprWritingTarget = """
#include "abi_bridge.h"
extern "C" void func_80003000(CpuContext*) {}
// RECOMP_REGISTRATION base 0x80003000 func_80003000 preserves=false fpr_mask=0x00004000
""";
// 0x80004000 and 0x80005000 both call the profile-sensitive winner at
// 0x80005000, so they land in the product-specific partition.
const string sharedCaller = """
#include "abi_bridge.h"
extern "C" void func_80004000(CpuContext* ctx) {
InvokeDirectCpu<0x80005000u>(ctx);
InvokeDirectCpu<0x80002000u>(ctx);
}
// RECOMP_REGISTRATION base 0x80004000 func_80004000 preserves=true fpr_mask=0x00000000
""";
const string profileSensitiveTarget = """
#include "abi_bridge.h"
extern "C" void func_80005000(CpuContext* ctx) {
InvokeDirectCpu<0x80005000u>(ctx);
}
// RECOMP_REGISTRATION base 0x80005000 func_80005000 preserves=true fpr_mask=0x00000000
""";
Write(Path.Combine(functions, "func_80001000.cpp"), commonCaller);
Write(Path.Combine(functions, "func_80002000.cpp"), invariantTarget);
Write(Path.Combine(functions, "func_80003000.cpp"), fprWritingTarget);
Write(Path.Combine(functions, "func_80004000.cpp"), sharedCaller);
Write(Path.Combine(functions, "func_80005000.cpp"), profileSensitiveTarget);
var metadataPath = Path.Combine(root, "base_output.json");
BaseTranslationOutputMetadataFile.WriteIfChangedAtomic(metadataPath,
BaseTranslationOutputMetadata.Create([
Metadata("func_80001000.cpp", 0x80001000u, commonCaller),
Metadata("func_80002000.cpp", 0x80002000u, invariantTarget),
Metadata("func_80003000.cpp", 0x80003000u, fprWritingTarget),
Metadata("func_80004000.cpp", 0x80004000u, sharedCaller),
Metadata("func_80005000.cpp", 0x80005000u, profileSensitiveTarget)
], TranslationQualityMetadata.Clean, "identity"));
var manifestPath = Path.Combine(root, "base_manifest.json");
Write(manifestPath, """
{"Functions":[
{"Start":2147487744,"End":2147487748,"Name":"func_80001000"},
{"Start":2147491840,"End":2147491844,"Name":"func_80002000"},
{"Start":2147495936,"End":2147495940,"Name":"func_80003000"},
{"Start":2147500032,"End":2147500036,"Name":"func_80004000"},
{"Start":2147504128,"End":2147504132,"Name":"func_80005000"}
]}
""");
// Retro Rewind resolves 0x80005000 to its own translation, so that
// winner is not profile-invariant even inside one shard.
var resolvedPath = Path.Combine(root, "resolved_dispatch_profile.json");
Write(resolvedPath, """
{"Entries":[
{"Address":2147504128,"Symbol":"rr_80005000","Name":"rr_80005000","Kind":"rr","Priority":100,"DirectCallAvailable":true,"PreservesNonvolatileFprs":true,"NonvolatileFprWriteMask":0,"MustRemainDynamicallyDispatchable":false}
]}
""");
var options = new TranslatedBuildShardOptions(
metadataPath, functions, output, native,
resolvedPath, BaseShardCount: 1, ModShardCount: 1, RegistrationShardCount: 1);
TranslatedBuildShardEmitter.Emit(options);
var commonShard = File.ReadAllText(
Directory.GetFiles(Path.Combine(output, "base_common"), "*.cpp").Single());
// Same shard, same winner in both profiles: call the definition in
// this translation unit so the host optimizer can still inline it.
Assert.Contains("MKW_STATIC_TRANSLATED_CALL(0x80002000u, func_80002000, ctx);", commonShard);
// A nonvolatile-FPR writer keeps the generic dynamically guarded call.
Assert.Contains("InvokeDirectCpu<0x80003000u>(ctx);", commonShard);
var baseSpecific = Directory.GetFiles(Path.Combine(output, "base_portable_sensitive"), "*.cpp")
.Select(File.ReadAllText)
.Aggregate(string.Concat);
Assert.Contains("MKW_STATIC_TRANSLATED_CALL(0x80005000u, func_80005000, ctx);", baseSpecific);
Assert.Contains("MKW_STATIC_TRANSLATED_CALL(0x80002000u, func_80002000, ctx);", baseSpecific);
// The lowering choice participates in shard identity, so a rebuild is
// byte-identical and file identities are stable.
var timestamps = Directory.GetFiles(output, "*", SearchOption.AllDirectories)
.ToDictionary(Path.GetFullPath, File.GetLastWriteTimeUtc, StringComparer.OrdinalIgnoreCase);
TranslatedBuildShardEmitter.Emit(options);
foreach (var (path, timestamp) in timestamps)
Assert.Equal(timestamp, File.GetLastWriteTimeUtc(path));
}
finally
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}
[Fact]
public void FrozenBaseCommonBoundariesKeepUnchangedShardIdentityAcrossAFunctionBodyChange()
{
var root = Path.Combine(Path.GetTempPath(), $"translator-frozen-shards-{Guid.NewGuid():N}");
var functions = Path.Combine(root, "functions");
var native = Path.Combine(root, "runtime", "src");
var output = Path.Combine(root, "out");
Directory.CreateDirectory(functions);
Directory.CreateDirectory(native);
try
{
// Six equally weighted profile-neutral functions pack 3/3 across two
// base_common shards.
var addresses = new uint[] { 0x80001000, 0x80002000, 0x80003000, 0x80004000, 0x80005000, 0x80006000 };
static string Body(uint address, string filler) =>
"#include \"abi_bridge.h\"\n" +
$"extern \"C\" void func_{address:X8}(CpuContext*) {{}}\n" +
$"// {filler}\n" +
$"// RECOMP_REGISTRATION base 0x{address:X8} func_{address:X8} preserves=true fpr_mask=0x00000000\n";
var metadataPath = Path.Combine(root, "base_output.json");
var manifestPath = Path.Combine(root, "base_manifest.json");
void WriteInputs(string lastFunctionFiller)
{
var entries = new List<BaseTranslationFunctionMetadata>();
foreach (var address in addresses)
{
var filler = address == addresses[^1] ? lastFunctionFiller : "";
var source = Body(address, filler);
var relative = $"func_{address:X8}.cpp";
Write(Path.Combine(functions, relative), source);
entries.Add(Metadata(relative, address, source));
}
BaseTranslationOutputMetadataFile.WriteIfChangedAtomic(
metadataPath, BaseTranslationOutputMetadata.Create(
entries, TranslationQualityMetadata.Clean, "identity"));
Write(manifestPath,
"{\"Functions\":[" +
string.Join(",", addresses.Select(address =>
$"{{\"Start\":{address},\"End\":{address + 4},\"Name\":\"func_{address:X8}\"}}")) +
"]}");
}
string[] BaseCommonShards() =>
Directory.GetFiles(Path.Combine(output, "base_common"), "*.cpp")
.Select(static path => Path.GetFileName(path)!)
.Order(StringComparer.Ordinal)
.ToArray();
var options = new TranslatedBuildShardOptions(
metadataPath, functions, output, native,
BaseShardCount: 2, ModShardCount: 1, RegistrationShardCount: 1);
WriteInputs("");
var first = TranslatedBuildShardEmitter.Emit(options);
Assert.False(first.BaseCommonBoundariesReused);
Assert.NotNull(first.BaseCommonShardMapPath);
var mapPath = first.BaseCommonShardMapPath!;
Assert.Equal("base_common_shard_map.json", Path.GetFileName(mapPath));
Assert.True(File.Exists(mapPath));
Assert.NotNull(first.BaseCommonBalance);
Assert.Equal(2, first.BaseCommonBalance!.ShardCount);
var frozenMap = File.ReadAllText(mapPath);
Assert.Contains("\"0x80001000\"", frozenMap);
Assert.Contains("\"0x80004000\"", frozenMap);
var before = BaseCommonShards();
Assert.Equal(2, before.Length);
// Only the last function's body changes, and it changes enough to
// move every greedy cut point behind it. Frozen membership keeps the
// shard that owns the first three functions byte-identical, so its
// content-addressed name - and its object file - survive.
WriteInputs(new string('x', 4096));
var second = TranslatedBuildShardEmitter.Emit(options);
Assert.True(second.BaseCommonBoundariesReused);
Assert.Equal(frozenMap, File.ReadAllText(mapPath));
var after = BaseCommonShards();
Assert.Equal(2, after.Length);
Assert.Single(before.Intersect(after, StringComparer.Ordinal));
// Discarding the recorded table is the only thing that moves a boundary.
File.Delete(mapPath);
var repacked = TranslatedBuildShardEmitter.Emit(options);
Assert.False(repacked.BaseCommonBoundariesReused);
var repackedMap = File.ReadAllText(mapPath);
Assert.NotEqual(frozenMap, repackedMap);
Assert.Contains("\"0x80006000\"", repackedMap);
Assert.Empty(after.Intersect(BaseCommonShards(), StringComparer.Ordinal));
}
finally
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}
private static BaseTranslationFunctionMetadata Metadata(string path, uint address, string content)
{
var bytes = Encoding.UTF8.GetBytes(content);
return new BaseTranslationFunctionMetadata(
path, bytes.Length, Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(), address, []);
}
private static void Write(string path, string content)
{
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, content, new UTF8Encoding(false));
}
}
@@ -0,0 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Translator.Core\Translator.Core.csproj" />
<ProjectReference Include="..\..\src\Translator.Cli\Translator.Cli.csproj" />
</ItemGroup>
<!--
The default suite is intentionally binary-free and host-compiler-free. These
files need either MKWii game data or a working host C++ toolchain, so they are
excluded until they are moved into a separate asset-backed integration project.
Every excluded file still compiles against this repository; the ones that did
not (they referenced a REL base-address constant that does not exist here) were
deleted rather than left as permanently broken source.
-->
<ItemGroup>
<Compile Remove="MtxIntegrationTests.cs" />
<Compile Remove="CodeGenInlineCoverageTests.cs" />
<Compile Remove="CodeGenTests.cs" />
<Compile Remove="DolFileTests.cs" />
<Compile Remove="BasicBlockTests.cs" />
<Compile Remove="PpcDisassemblerTests.cs" />
<Compile Remove="PpcRuntimeHelperTests.cs" />
</ItemGroup>
</Project>
@@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Translator.Core.Loading;
namespace Translator.Tests;
internal static class TranslatorCppTestHarness
{
private static readonly string[] LeanRuntimeSources =
{
Path.Combine("runtime", "src", "abi_bridge.cpp"),
Path.Combine("runtime", "src", "fpu_helpers.cpp"),
Path.Combine("runtime", "src", "memory.cpp"),
Path.Combine("runtime", "src", "ppc_helpers.cpp"),
};
public static string BuildCompileArguments(
string repoRoot,
string tempRoot,
IEnumerable<string> generatedFiles,
string harnessPath,
string outputPath,
bool includeDataSections = true)
{
var supportSource = EnsureSupportSource(tempRoot);
var runtimeSources = LeanRuntimeSources.Select(path => Path.Combine(repoRoot, path));
var args = new StringBuilder();
args.Append("-std=c++17 ");
args.Append("-D_CRT_SECURE_NO_WARNINGS ");
args.Append("-march=x86-64-v3 ");
if (!RuntimeHeadersDefineRestrictMacro(repoRoot))
{
// Fallback for the window between an emitter change using MKW_RESTRICT and the runtime
// headers defining it. Stops firing once headers catch up, so it can't hide a real regression.
args.Append("-DMKW_RESTRICT=__restrict ");
}
args.Append("-Wno-error=unused-but-set-variable ");
args.Append(string.Join(' ', runtimeSources.Select(Quote))).Append(' ');
args.Append(Quote(supportSource)).Append(' ');
args.Append(string.Join(' ', generatedFiles.Select(Quote))).Append(' ');
if (includeDataSections)
{
var dataSectionsInit = Path.Combine(repoRoot, "generated", "data_sections_init.cpp");
if (File.Exists(dataSectionsInit))
{
args.Append(Quote(dataSectionsInit)).Append(' ');
}
var dataSectionsBlobs = Path.Combine(repoRoot, "generated", "data_sections_init_blobs.S");
if (File.Exists(dataSectionsBlobs))
{
args.Append(Quote(dataSectionsBlobs)).Append(' ');
}
}
args.Append(Quote(harnessPath)).Append(' ');
args.Append($"-I{Quote(Path.Combine(repoRoot, "runtime", "include"))} ");
args.Append($"-I{Quote(Path.Combine(repoRoot, "generated"))} ");
args.Append($"-o {Quote(outputPath)}");
return args.ToString();
}
private static bool RuntimeHeadersDefineRestrictMacro(string repoRoot)
{
var includeDirectory = Path.Combine(repoRoot, "runtime", "include");
if (!Directory.Exists(includeDirectory))
{
return false;
}
foreach (var header in Directory.EnumerateFiles(includeDirectory, "*.h", SearchOption.AllDirectories))
{
if (File.ReadAllText(header).Contains("define MKW_RESTRICT", StringComparison.Ordinal))
{
return true;
}
}
return false;
}
public static ProgramImage CreateImage(params (uint Address, uint Value)[] words)
{
var maxOffset = 0;
foreach (var (address, _) in words)
{
var offset = checked((int)(address - MemoryLayout.RamBase));
maxOffset = Math.Max(maxOffset, offset + 4);
}
var memory = new byte[Math.Max(maxOffset, 4)];
foreach (var (address, value) in words)
{
var offset = checked((int)(address - MemoryLayout.RamBase));
memory[offset + 0] = (byte)(value >> 24);
memory[offset + 1] = (byte)(value >> 16);
memory[offset + 2] = (byte)(value >> 8);
memory[offset + 3] = (byte)value;
}
return new ProgramImage(
memory,
new AddressRange(MemoryLayout.RamBase, checked(MemoryLayout.RamBase + (uint)memory.Length)),
new AddressRange(MemoryLayout.RamBase, checked(MemoryLayout.RamBase + (uint)memory.Length)),
new AddressRange(0, 0),
sha256: "test");
}
private static string EnsureSupportSource(string tempRoot)
{
Directory.CreateDirectory(tempRoot);
var path = Path.Combine(tempRoot, "translator_test_support.cpp");
const string contents = """
#include "system_bridge.h"
#include "ppc_runtime.h"
#include "recomp_mod_loader.h"
#include <csetjmp>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <ostream>
#include <string_view>
bool g_suppressSehReporting = false;
thread_local jmp_buf* g_sehJumpTarget = nullptr;
thread_local uint32_t g_sehLastExceptionCode = 0;
thread_local uintptr_t g_sehLastExceptionAddress = 0;
thread_local uintptr_t g_sehLastAccessedAddress = 0;
thread_local uint32_t g_sehLastAccessType = 0;
void DebugTrackPcImpl(uint32_t) {}
void DebugPumpAurora() {}
#if defined(__GNUC__) || defined(__clang__)
extern "C" void DumpRecentPcTrace(size_t) __attribute__((weak));
void MarkFatalErrorReported() __attribute__((weak));
#endif
extern "C" void DumpRecentPcTrace(size_t) {}
void MarkFatalErrorReported() {}
extern "C" void DumpHostStackTraceForRuntimeHelper() {}
extern "C" void OS_HLE_ProcessAlarms(int) {}
void ShowRuntimeFatalPopup(std::string_view, std::string_view) noexcept
{
std::abort();
}
namespace RecompMod {
std::atomic<bool> g_executableWriteGuardEnabled{false};
std::atomic<uint8_t> g_executableWriteGuardPages[kExecutableWriteGuardPageCount]{};
std::atomic<uint8_t> g_executableWriteGuardCoarsePages[kExecutableWriteGuardCoarsePageCount]{};
std::atomic<uint8_t> g_executableWriteGuardMidPages[kExecutableWriteGuardMidPageCount]{};
ScopedTranslatedExecutionAddress::ScopedTranslatedExecutionAddress(uint32_t) noexcept {}
ScopedTranslatedExecutionAddress::~ScopedTranslatedExecutionAddress() noexcept {}
uint32_t CurrentTranslatedExecutionAddress() noexcept { return 0; }
bool HandleExecutableWrite(uint32_t, size_t, uint64_t) { return false; }
void CheckExecutableWrite(uint32_t, size_t, uint64_t) {}
}
RuntimeOptions SystemBridge::ParseCommandLine(int, char**) { return {}; }
void SystemBridge::Initialize(const RuntimeOptions&) {}
void SystemBridge::DumpRegisteredFunctions() {}
void SystemBridge::DumpMemoryLayout() {}
void SystemBridge::PrintUsage() {}
void SystemBridge::DumpCpuState(const CpuContext*) {}
void SystemBridge::DumpCpuState(std::ostream&, const CpuContext*) {}
""";
if (!File.Exists(path) || !string.Equals(File.ReadAllText(path), contents, StringComparison.Ordinal))
{
File.WriteAllText(path, contents);
}
return path;
}
private static string Quote(string path) => $"\"{path}\"";
}
@@ -0,0 +1,47 @@
#include <cstdint>
#include <cstdio>
#if defined(__clang__) && defined(_WIN64)
#define INTERNAL_CALL __regcall
#else
#define INTERNAL_CALL
#endif
struct Result15 {
uint32_t r0, r1, r3, r4, r5, r6, r7, r8;
uint32_t r9, r10, r11, r12, r30, r31, cr;
};
extern "C" __declspec(noinline) Result15 INTERNAL_CALL callee(
uint32_t r1, uint32_t r3, uint32_t r4, uint32_t r5,
uint32_t r6, uint32_t r7, uint32_t r8, uint32_t r9,
uint32_t r10, uint32_t r11, uint32_t r12, uint32_t r30,
uint32_t r31, uint32_t cr, uint32_t xer, uint32_t lr) {
return {lr, r1, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r30, r31, cr ^ xer};
}
extern "C" __declspec(noinline) Result15 INTERNAL_CALL recurse(
uint32_t depth, uint32_t pointer, uint32_t salt) {
if (depth == 0) {
return callee(0x80398be8u, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
30, pointer, 0x44000088u, salt, 0x80124d08u);
}
auto nested = recurse(depth - 1, pointer, salt);
return callee(nested.r1, nested.r3, nested.r4, nested.r5, nested.r6,
nested.r7, nested.r8, nested.r9, nested.r10, nested.r11,
nested.r12, nested.r30, nested.r31, nested.cr, salt,
nested.r0);
}
int main() {
constexpr uint32_t expected = 0x802f12ccu;
for (uint32_t i = 0; i < 1000000; ++i) {
const auto result = recurse(4, expected, i);
if (result.r31 != expected) {
std::printf("mismatch iteration=%u expected=%08X actual=%08X\n", i, expected, result.r31);
return 1;
}
}
std::puts("statefree regcall+sret proof passed");
return 0;
}