mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 07:00:30 +02:00
init
This commit is contained in:
commit
ec226e8348
1099 files changed
+400981
No files matched your search
@@ -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->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;
|
||||
}
|
||||
Reference in new issue
Block a user