translator: add project entry observers for MKW VR

This commit is contained in:
iChris4 committed 2026-09-03 03:59:42 +02:00
1 parent 8b607ccbb9
commit 8c8e4561e9
15 files changed
+398 -25

No files matched your search

+13
View File
@@ -34,6 +34,19 @@ translation:
#just a precaution, but this should never be needed for MKWii
allow_unsupported_instructions: false
# PAL RMCP01 entry observers used by the OpenXR presentation policy. Keeping
# these in the game manifest prevents the generic translator from acquiring a
# Mario Kart runtime dependency merely because another title shares an address.
# The callback contract is read-only: (uint32_t, const CpuContext*).
entry_observer:
header: vr/mkw_vr_instrumentation.h
symbol: MkwVRObserveTranslatedFunctionEntry
entry_points:
- 0x80553C50 # RaceScene::OnEnter
- 0x805549B0 # RaceScene::OnExit
- 0x805A21D0 # RaceCamera::Update
- 0x805B1CD8 # ScnMgrRace::Draw
runtime:
native_registration_root: runtime/src
native_abi_directories:
+3
View File
@@ -56,6 +56,9 @@ the translator does not guess them.
- `translation.function_map.path` - optional symbol map used as the discovery oracle.
- `translation.allow_unsupported_instructions` - off by default; enabling it emits runtime traps
instead of failing, and such a build can never ship.
- `translation.entry_observer` - optional header, C symbol, and entry-point list for a read-only
host observer. Its callback must accept `(uint32_t, const CpuContext*)`; the translator materializes
the complete guest context before every direct or transitive path that can reach it.
Relative paths resolve from `workspace_root`, which itself resolves from the manifest directory.
+60 -11
View File
@@ -121,8 +121,14 @@ var modPatchProfiles = new Lazy<IReadOnlyList<Translator.Cli.Configuration.Proje
var modPatchedAddresses = new Lazy<IReadOnlySet<uint>>(() =>
BuildModPatchedAddresses(modPatchProfiles.Value));
var leafInliningBlockedTargets = new Lazy<IReadOnlySet<uint>>(() =>
BuildLeafInliningBlockedTargets(
modPatchedAddresses.Value, runtimeNativeGuestEffects.Value, baseTranslationExclusions.Value));
{
var blocked = BuildLeafInliningBlockedTargets(
modPatchedAddresses.Value, runtimeNativeGuestEffects.Value, baseTranslationExclusions.Value)
.ToHashSet();
if (project?.Translation.EntryObserver is { } observer)
blocked.UnionWith(observer.EntryPoints);
return blocked;
});
// Leaf inlining is a front-end decision: the splice happens before SSA, so the
// canonical graph the emission wave lowers is already inlined and every
@@ -417,6 +423,7 @@ int RunTranslateRecursive(string[] argsTail)
outDir,
RequireProject().Translation.EntryPoints
.Append(startAddr)
.Concat(RequireProject().Translation.EntryObserver?.EntryPoints ?? new HashSet<uint>())
.Concat(functionMap?.Addresses ?? Array.Empty<uint>()),
includeGeneratedHistory: false);
Console.WriteLine($"[translator] Loaded {knownBaseFunctionEntryPoints.Count:N0} known base function start(s) for tail-call boundary detection.");
@@ -434,12 +441,25 @@ int RunTranslateRecursive(string[] argsTail)
var residentTranslationExclusions = baseTranslationExclusions.Value;
Console.WriteLine(
$"[translator] Leaf inlining: {leafInliningBlockedTargets.Value.Count:N0} address(es) are not " +
"inlinable (native, excluded from translation, or mod-patched).");
"inlinable (native, excluded from translation, mod-patched, or observed).");
var queue = new Queue<(uint addr, int depth)>();
queue.Enqueue((startAddr, 0));
visited.Add(startAddr);
// Observers are an executable project contract, not advisory function-map
// metadata. Translate every configured target as a required root so a bad
// address fails the build instead of being silently dropped as a
// speculative map entry.
var observerEntryPoints =
RequireProject().Translation.EntryObserver?.EntryPoints ?? new HashSet<uint>();
foreach (var observerEntryPoint in observerEntryPoints.Order())
{
gqrUnknownEntryRoots.Add(observerEntryPoint);
if (visited.Add(observerEntryPoint))
queue.Enqueue((observerEntryPoint, 0));
}
// Map entries the call-graph walk never reaches. They are translated, but
// on a decode or lift failure they are skipped with a count instead of
// failing the run: unlike a walked call target, nothing proves the map
@@ -730,18 +750,28 @@ int RunTranslateRecursive(string[] argsTail)
allowUnsupportedInstructions);
}
// The call-graph walk from the entry point, then everything the configured map
// knows about that the walk never reached. Those two sources replace the
// The call-graph walk from the executable roots, then everything the configured
// map knows about that the walk never reached. Those two sources replace the
// constructor, vtable, function-pointer and adjacent-prologue scans, which
// guessed at exactly this set - and the hand-maintained force-translate
// list that existed to paper over the addresses they still missed.
ProcessQueue();
var missingObserverEntries = observerEntryPoints
.Where(address => !translated.Contains(address))
.Order()
.ToArray();
if (missingObserverEntries.Length != 0)
{
throw new InvalidDataException(
"Required entry observer target(s) were not translated: " +
string.Join(", ", missingObserverEntries.Select(static address => $"0x{address:X8}")));
}
var walkReachable = count;
ProcessSpeculativeSeeds();
if (functionMap is not null)
{
Console.WriteLine(
$"[translator] Function starts: {walkReachable:N0} reached by the call-graph walk, " +
$"[translator] Function starts: {walkReachable:N0} required/root-reachable, " +
$"{count - walkReachable:N0} added speculatively from the map.");
if (speculativeSkips.Count != 0)
{
@@ -878,7 +908,8 @@ int RunTranslateRecursive(string[] argsTail)
// rather than retaining every descendant dependency forever.
var interproceduralGuestAbi = GuestAbiInterproceduralAnalyzer.Analyze(
canonicalGuestFunctions,
nativeGuestEffects.Contracts);
nativeGuestEffects.Contracts,
observerEntryPoints);
var guestAbiContracts = interproceduralGuestAbi.Contracts
.ToDictionary(static pair => pair.Key, static pair => pair.Value);
var recursiveComponents = interproceduralGuestAbi.StronglyConnectedComponents
@@ -1007,6 +1038,7 @@ int RunTranslateRecursive(string[] argsTail)
index,
eligible = component.All(address =>
!residentTranslationExclusions.Contains(address) &&
residentPolicy.EntryObserver?.EntryPoints.Contains(address) != true &&
!localGuestAbiContracts[address].HasFullSynchronizationFence &&
GuestStateLivenessAnalyzer.CanDeconstructWithoutContext(canonicalGuestFunctions[address]) &&
(!directlyCalledGuestFunctions.Contains(address) ||
@@ -1090,8 +1122,8 @@ int RunTranslateRecursive(string[] argsTail)
StateFreeAbiContracts: stateFreeAbiContracts,
StateFreeCallSymbols: stateFreeCallSymbols,
StateFreeCallSiteVariants: stateFreeCallSiteVariantsByCaller.GetValueOrDefault(address),
StateFreeEntryVariants: stateFreeEntryVariantsByTarget.GetValueOrDefault(address),
ModOverridableCallTargets: modOverridableCallTargets),
StateFreeEntryVariants: stateFreeEntryVariantsByTarget.GetValueOrDefault(address),
ModOverridableCallTargets: modOverridableCallTargets),
residentPolicy,
address);
}
@@ -4019,8 +4051,13 @@ static IReadOnlySet<uint> BuildModPatchedAddresses(
static TranslationOptions WithProjectCodegenPolicy(
TranslationOptions options,
ProjectTranslation translation,
uint entryPoint) =>
options with
uint entryPoint)
{
var fullContextCallTargets = options.FullContextCallTargets?.ToHashSet() ?? new HashSet<uint>();
if (translation.EntryObserver is { } observer)
fullContextCallTargets.UnionWith(observer.EntryPoints);
return options with
{
AllowUnsupportedInstructions = translation.AllowUnsupportedInstructions,
// Leaf ABI spill elision is structurally self-guarding: it requires a
@@ -4028,7 +4065,19 @@ static TranslationOptions WithProjectCodegenPolicy(
// leaf, so a partial mod chunk that only carries one half of an ABI
// prologue is rejected rather than mis-transformed.
EnableLeafAbiSpillElision = true,
EntryObserverHeader = translation.EntryObserver?.EntryPoints.Contains(entryPoint) == true
? translation.EntryObserver.Header
: null,
EntryObserverSymbol = translation.EntryObserver?.EntryPoints.Contains(entryPoint) == true
? translation.EntryObserver.Symbol
: null,
// Read-only observer callbacks can inspect fields outside the guest
// callee's inferred ABI. Apply the full materialization boundary here,
// rather than only on the base path, so continuations, overlays, modules,
// and payloads cannot expose stale resident CpuContext state either.
FullContextCallTargets = fullContextCallTargets.Count == 0 ? null : fullContextCallTargets,
};
}
/// <summary>
/// One option carried by one command. <paramref name="Value"/> is the metavariable
@@ -97,7 +97,8 @@ internal sealed class TranslationProjectConfig
var translation = new ProjectTranslation(
entryPoints,
functionMapPath,
dto.Translation?.AllowUnsupportedInstructions ?? false);
dto.Translation?.AllowUnsupportedInstructions ?? false,
ResolveEntryObserver(dto.Translation?.EntryObserver));
var abiDirectories = (dto.Runtime?.NativeAbiDirectories ?? [])
.Select(pathValue => ResolvePath(workspaceRoot, pathValue))
@@ -262,6 +263,43 @@ internal sealed class TranslationProjectConfig
private static string Require(string? value, string field) =>
!string.IsNullOrWhiteSpace(value) ? value : throw new InvalidDataException($"Translation project requires {field}.");
private static ProjectEntryObserver? ResolveEntryObserver(EntryObserverDto? dto)
{
if (dto is null)
{
return null;
}
var header = Require(dto.Header, "translation.entry_observer.header")
.Trim().Replace('\\', '/');
if (header.StartsWith("/", StringComparison.Ordinal) ||
header.Split('/').Any(static component => component is "" or "." or "..") ||
header.Any(static ch => !(char.IsAsciiLetterOrDigit(ch) || ch is '_' or '-' or '.' or '/')))
{
throw new InvalidDataException(
"translation.entry_observer.header must be a safe relative include path.");
}
var symbol = Require(dto.Symbol, "translation.entry_observer.symbol").Trim();
if (!(char.IsAsciiLetter(symbol[0]) || symbol[0] == '_') ||
symbol.Skip(1).Any(static ch => !(char.IsAsciiLetterOrDigit(ch) || ch == '_')))
{
throw new InvalidDataException(
"translation.entry_observer.symbol must be a C identifier.");
}
var entryPoints = (dto.EntryPoints ?? [])
.Select((value, index) => ParseUInt32(
value, null, $"translation.entry_observer.entry_points[{index}]"))
.ToHashSet();
if (entryPoints.Count == 0)
{
throw new InvalidDataException(
"translation.entry_observer requires at least one entry point.");
}
return new ProjectEntryObserver(header, symbol, entryPoints);
}
// Riivolution declarations are verbatim distribution metadata, not paths on this machine:
// the XML lives inside the pack the runtime mounts, so it stays a forward-slashed
// pack-relative string and is never resolved against the workspace root.
@@ -405,6 +443,14 @@ internal sealed class TranslationProjectConfig
public List<string>? EntryPoints { get; init; }
public FunctionMapDto? FunctionMap { get; init; }
public bool? AllowUnsupportedInstructions { get; init; }
public EntryObserverDto? EntryObserver { get; init; }
}
private sealed class EntryObserverDto
{
public string? Header { get; init; }
public string? Symbol { get; init; }
public List<string>? EntryPoints { get; init; }
}
private sealed class FunctionMapDto
@@ -472,7 +518,12 @@ internal sealed record ProjectInputs(ProjectBinaryInput Dol, ProjectRelInput? Re
internal sealed record ProjectTranslation(
IReadOnlyList<uint> EntryPoints,
string? FunctionMapPath,
bool AllowUnsupportedInstructions);
bool AllowUnsupportedInstructions,
ProjectEntryObserver? EntryObserver);
internal sealed record ProjectEntryObserver(
string Header,
string Symbol,
IReadOnlySet<uint> EntryPoints);
internal sealed record ProjectRuntime(
IReadOnlyList<string> NativeAbiDirectories,
string NativeRegistrationRoot);
@@ -227,6 +227,26 @@ public static class GuestAbiContractAnalyzer
};
}
/// <summary>
/// Adds the architectural reads performed by a const host-side entry
/// observer. The translated body's write effects remain unchanged; the
/// fence forces complete materialization and propagates through callers.
/// </summary>
public static GuestAbiContract WithReadOnlyContextObserver(GuestAbiContract contract) =>
contract with
{
GprReadBeforeWriteMask = uint.MaxValue,
FprReadBeforeWriteMask = uint.MaxValue,
CrReadBeforeWriteMask = byte.MaxValue,
ReadsXerBeforeWrite = true,
ReadsCtrBeforeWrite = true,
ReadsLrBeforeWrite = true,
ReadsFpscrBeforeWrite = true,
GqrReadBeforeWriteMask = byte.MaxValue,
HidReadBeforeWriteMask = byte.MaxValue,
BoundaryFlags = contract.BoundaryFlags | GuestCallBoundaryFlags.RequiresCompleteContext,
};
/// <summary>
/// Successor count (0, 1, or 2) and labels for <paramref name="block"/>; count-plus-out-params
/// keeps the CFG walk allocation-free.
@@ -18,9 +18,13 @@ public static class GuestAbiInterproceduralAnalyzer
{
public static GuestAbiInterproceduralResult Analyze(
IReadOnlyDictionary<uint, IrFunction> functions,
IReadOnlyDictionary<uint, GuestAbiContract>? externalContracts = null)
IReadOnlyDictionary<uint, GuestAbiContract>? externalContracts = null,
IEnumerable<uint>? contextObservingEntryPoints = null)
{
externalContracts ??= new Dictionary<uint, GuestAbiContract>();
var fullContextSet = (contextObservingEntryPoints ?? Array.Empty<uint>())
.Where(functions.ContainsKey)
.ToHashSet();
var addresses = functions.Keys.OrderBy(static address => address).ToArray();
// GuestAbiContractAnalyzer.Analyze is pure and the round's contract snapshot is frozen, so
@@ -40,7 +44,9 @@ public static class GuestAbiInterproceduralAnalyzer
0,
addresses.Length,
parallelOptions,
index => current[index] = GuestAbiContractAnalyzer.Analyze(bodies[index]));
index => current[index] = AddFullContextEntryEffects(
GuestAbiContractAnalyzer.Analyze(bodies[index]),
fullContextSet.Contains(addresses[index])));
var contracts = new Dictionary<uint, GuestAbiContract>(addresses.Length);
for (var index = 0; index < addresses.Length; ++index) contracts.Add(addresses[index], current[index]);
@@ -95,7 +101,9 @@ public static class GuestAbiInterproceduralAnalyzer
Parallel.For(0, worklistCount, parallelOptions, position =>
{
var ordinal = worklist[position];
var next = GuestAbiContractAnalyzer.Analyze(bodies[ordinal], visible);
var next = AddFullContextEntryEffects(
GuestAbiContractAnalyzer.Analyze(bodies[ordinal], visible),
fullContextSet.Contains(addresses[ordinal]));
roundResults[position] = Equivalent(current[ordinal], next) ? null : next;
});
@@ -124,6 +132,13 @@ public static class GuestAbiInterproceduralAnalyzer
return new GuestAbiInterproceduralResult(contracts, BuildComponents(functions, contracts));
}
private static GuestAbiContract AddFullContextEntryEffects(
GuestAbiContract contract,
bool requiresFullContext) =>
requiresFullContext
? GuestAbiContractAnalyzer.WithReadOnlyContextObserver(contract)
: contract;
/// <summary>
/// Ordinals of functions this one calls directly, from raw IR rather than
/// <see cref="GuestAbiContract.DirectCallTargets"/>, which omits inline save/restore thunks
@@ -354,7 +354,7 @@ public sealed partial class CxxLinearCodeGenerator
}
/// <summary>
/// Resolves the boundary sync set for a direct guest call, falling back to <see cref="ResidencyBoundarySync.Full"/> when narrowing is disabled, residency is inactive, the callee has no contract or a full-sync fence, or a mod could replace the callee with a re-translation this body's contract doesn't describe.
/// Resolves the boundary sync set for a direct guest call, falling back to <see cref="ResidencyBoundarySync.Full"/> when narrowing is disabled, residency is inactive, the callee has no contract or a full-sync fence, or host-side behavior (such as a mod replacement or entry observer) can access state outside the guest contract.
/// </summary>
private static ResidencyBoundarySync ResolveDirectCallBoundarySync(
uint target,
@@ -100,9 +100,21 @@ public sealed partial class CxxLinearCodeGenerator
bool gqrConstantsRequireRuntimeGuard = false,
bool enableLeafAbiSpillElision = false,
IReadOnlySet<uint>? modOverridableCallTargets = null,
bool enableGpuFifoBurstCoalescing = true)
IReadOnlySet<uint>? fullContextCallTargets = null,
bool enableGpuFifoBurstCoalescing = true,
string? entryObserverHeader = null,
string? entryObserverSymbol = null)
{
modOverridableCallTargets ??= new HashSet<uint>();
if (fullContextCallTargets is { Count: > 0 })
{
// The configured observer is read-only, but it can inspect fields
// outside the inferred guest ABI. Reuse the conservative boundary
// machinery so all resident state is materialized before entry.
var completeContextTargets = modOverridableCallTargets.ToHashSet();
completeContextTargets.UnionWith(fullContextCallTargets);
modOverridableCallTargets = completeContextTargets;
}
nonReturningCallTargets ??= new HashSet<uint>();
lrContinuationCallTargets ??= new HashSet<uint>();
guestAbiContracts ??= new Dictionary<uint, GuestAbiContract>();
@@ -149,6 +161,10 @@ public sealed partial class CxxLinearCodeGenerator
// tuple for every instruction of every emission of this body.
var suppressedInstructionMasks = BuildSuppressedInstructionMasks(func, flagElision);
var cxxName = SanitizeIdentifier(signature.Name);
var emitEntryObserver = !string.IsNullOrWhiteSpace(entryObserverHeader) &&
!string.IsNullOrWhiteSpace(entryObserverSymbol);
if (emitEntryObserver)
guestAbiContract = GuestAbiContractAnalyzer.WithReadOnlyContextObserver(guestAbiContract);
var pairedFlow = ComputePairedFlowStates(func, cfg, _guestAbiProvider);
var pairedIn = pairedFlow.In;
var pairedOut = pairedFlow.Out;
@@ -661,6 +677,21 @@ public sealed partial class CxxLinearCodeGenerator
{
code = ApplyGuardedGqrFunctionVersioning(code, implementationName, gqrEntryGuards);
}
if (emitEntryObserver)
{
var declaration = FunctionDefinitionSignature(implementationName);
var declarationStart = code.IndexOf(declaration, StringComparison.Ordinal);
if (declarationStart < 0)
throw new InvalidOperationException($"Entry observer could not find the function body for 0x{entryPoint:X8}.");
var openBrace = code.IndexOf('{', declarationStart + declaration.Length);
if (openBrace < 0)
throw new InvalidOperationException($"Entry observer found no function body for 0x{entryPoint:X8}.");
// The explicit const conversion makes the manifest's read-only
// callback contract a compile-time requirement for every project.
var observer = $"{Environment.NewLine} {entryObserverSymbol}(0x{entryPoint:X8}u, static_cast<const CpuContext*>(ctx));";
code = $"#include \"{entryObserverHeader}\"" + Environment.NewLine +
code.Insert(openBrace + 1, observer);
}
return new CxxEmissionResult(
code,
@@ -49,6 +49,13 @@ public sealed record TranslationOptions(
bool GqrConstantsRequireRuntimeGuard = false,
bool EnableLeafAbiSpillElision = false,
IReadOnlySet<uint>? ModOverridableCallTargets = null,
/// <summary>
/// Direct-call targets whose read-only host-side entry observer may inspect
/// any field in CpuContext, independently of the translated guest ABI.
/// Callers materialize all resident architectural state before these
/// boundaries. Observer APIs must accept a const CpuContext pointer.
/// </summary>
IReadOnlySet<uint>? FullContextCallTargets = null,
bool GenerateCxx = true,
bool DiscoveryOnly = false,
/// <summary>
@@ -70,7 +77,15 @@ public sealed record TranslationOptions(
/// acyclic and every exit is a plain return. Emergency opt-out; the
/// straight-line shape keeps working with this off.
/// </summary>
bool LeafInliningAllowMultiBlockCallees = true)
bool LeafInliningAllowMultiBlockCallees = true,
/// <summary>
/// Optional project-configured observer inserted at this function's public
/// CpuContext entry. The CLI excludes observed functions from leaf inlining
/// and state-free ABI lowering, and marks calls to them as full-context
/// boundaries, so no direct-call optimization can bypass or starve it.
/// </summary>
string? EntryObserverHeader = null,
string? EntryObserverSymbol = null)
{
public static TranslationOptions Default { get; } = new();
}
@@ -746,7 +761,10 @@ public sealed class FunctionTranslator
gqrCalleeWriteMasks: options.GqrCalleeWriteMasks,
gqrConstantsRequireRuntimeGuard: options.GqrConstantsRequireRuntimeGuard,
enableLeafAbiSpillElision: options.EnableLeafAbiSpillElision,
modOverridableCallTargets: options.ModOverridableCallTargets);
modOverridableCallTargets: options.ModOverridableCallTargets,
fullContextCallTargets: options.FullContextCallTargets,
entryObserverHeader: options.EntryObserverHeader,
entryObserverSymbol: options.EntryObserverSymbol);
}
private static IrTracePpc CreateTrace(PpcInstruction instruction)
@@ -38,7 +38,10 @@ internal static class CxxLinearCodeGeneratorEmitExtensions
bool gqrConstantsRequireRuntimeGuard = false,
bool enableLeafAbiSpillElision = false,
IReadOnlySet<uint>? modOverridableCallTargets = null,
bool enableGpuFifoBurstCoalescing = true) =>
IReadOnlySet<uint>? fullContextCallTargets = null,
bool enableGpuFifoBurstCoalescing = true,
string? entryObserverHeader = null,
string? entryObserverSymbol = null) =>
generator.EmitWithFacts(
entryPoint: entryPoint,
ssa: ssa,
@@ -63,5 +66,8 @@ internal static class CxxLinearCodeGeneratorEmitExtensions
gqrConstantsRequireRuntimeGuard: gqrConstantsRequireRuntimeGuard,
enableLeafAbiSpillElision: enableLeafAbiSpillElision,
modOverridableCallTargets: modOverridableCallTargets,
enableGpuFifoBurstCoalescing: enableGpuFifoBurstCoalescing).Code;
fullContextCallTargets: fullContextCallTargets,
enableGpuFifoBurstCoalescing: enableGpuFifoBurstCoalescing,
entryObserverHeader: entryObserverHeader,
entryObserverSymbol: entryObserverSymbol).Code;
}
@@ -110,6 +110,10 @@ public sealed class GenericProjectTests
translation:
entry_points: [0x80001000]
allow_unsupported_instructions: true
entry_observer:
header: instrumentation/entry_observer.h
symbol: ObserveEntry
entry_points: [0x80001000]
output:
root: out
""");
@@ -117,6 +121,10 @@ public sealed class GenericProjectTests
var project = TranslationProjectConfig.Load(projectPath);
Assert.Null(project.Inputs.Rel);
Assert.Equal(entry, Assert.Single(project.Translation.EntryPoints));
var entryObserver = Assert.IsType<ProjectEntryObserver>(project.Translation.EntryObserver);
Assert.Equal("instrumentation/entry_observer.h", entryObserver.Header);
Assert.Equal("ObserveEntry", entryObserver.Symbol);
Assert.Contains(entry, entryObserver.EntryPoints);
var dol = DolFile.Load(project.Inputs.Dol.Path);
var image = new ProgramImageBuilder().Build(dol, ramBase: project.Memory.Base, ramSize: project.Memory.Size);
@@ -19,13 +19,17 @@ public class GqrPrologueHoistingCodeGenTests
{
private static string Emit(
IrFunction function,
IReadOnlyDictionary<uint, byte>? gqrCalleeWriteMasks = null) =>
IReadOnlyDictionary<uint, byte>? gqrCalleeWriteMasks = null,
IReadOnlyDictionary<string, uint>? gqrEntryConstants = null,
IReadOnlySet<uint>? fullContextCallTargets = null) =>
new CxxLinearCodeGenerator().Emit(
0x80001000u,
new SsaTransformer().Convert(function),
new FunctionAbiClassification(function.Name, ValueRepresentation.Void),
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
gqrCalleeWriteMasks: gqrCalleeWriteMasks);
gqrEntryConstants: gqrEntryConstants,
gqrCalleeWriteMasks: gqrCalleeWriteMasks,
fullContextCallTargets: fullContextCallTargets);
private static IrCall PsqLoad(int index, string address = "r4") =>
new("f1", "PPC_PsqL", [IrValue.Register(address), IrValue.Imm(0), IrValue.Imm(index)]);
@@ -141,6 +145,22 @@ public class GqrPrologueHoistingCodeGenTests
Assert.Equal(2, CountOccurrences(code, "mkw_gqr3 = ctx->gqr[3];"));
}
[Fact]
public void ReadOnlyContextObserverDoesNotInventGqrWrites()
{
var code = Emit(
Function("observed_call",
new IrCall(string.Empty, "func_80002000", []),
PsqLoad(3),
new IrReturn(null)),
gqrCalleeWriteMasks: new Dictionary<uint, byte> { [0x80002000u] = 0 },
gqrEntryConstants: new Dictionary<string, uint> { ["gqr3"] = 0x00070007u },
fullContextCallTargets: new HashSet<uint> { 0x80002000u });
Assert.DoesNotContain("mkw_gqr3", code, StringComparison.Ordinal);
Assert.Contains("PPC_PsqLKnownInline<0u, 3u, 0x00070007u>", code, StringComparison.Ordinal);
}
[Fact]
public void RuntimeHelperCallsDoNotForceAReload()
{
@@ -84,6 +84,35 @@ public sealed class GuestAbiInterproceduralAnalyzerTests
result.Contracts[second].GprReadBeforeWriteMask & ((1u << 3) | (1u << 4)));
}
[Fact]
public void FullContextEntryEffectsPropagateThroughAllCallers()
{
const uint root = 0x80001000u;
const uint middle = 0x80002000u;
const uint observed = 0x80003000u;
var functions = new Dictionary<uint, IrFunction>
{
[root] = Function("root",
new IrCall("lr", $"0x{middle:X8}", Array.Empty<IrValue>()),
new IrReturn(null)),
[middle] = Function("middle",
new IrCall("lr", $"0x{observed:X8}", Array.Empty<IrValue>()),
new IrReturn(null)),
[observed] = Function("observed", new IrReturn(null))
};
var result = GuestAbiInterproceduralAnalyzer.Analyze(
functions,
contextObservingEntryPoints: new[] { observed });
Assert.All(new[] { root, middle, observed },
address => Assert.True(result.Contracts[address].HasFullSynchronizationFence));
Assert.Equal(uint.MaxValue, result.Contracts[observed].GprReadBeforeWriteMask);
Assert.Equal(0u, result.Contracts[observed].GprPossibleWriteMask);
Assert.Equal(byte.MaxValue, result.Contracts[observed].GqrReadBeforeWriteMask);
Assert.Equal((byte)0, result.Contracts[observed].GqrPossibleWriteMask);
}
[Fact]
public void DeepCallGraphDoesNotConsumeTheNativeStack()
{
@@ -0,0 +1,43 @@
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 MkwVrObserverCodeGenTests
{
[Fact]
public void EmitsOnlyWhenProjectConfiguresAnObserver()
{
var function = new IrFunction(
"race_scene_enter",
"entry",
new[]
{
new IrBasicBlock("entry", new IrInstruction[] { new IrReturn(null) })
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>());
var signature = new FunctionAbiClassification("race_scene_enter", ValueRepresentation.Void);
var ssa = new SsaTransformer().Convert(function);
var observed = new CxxLinearCodeGenerator().EmitWithFacts(
0x80553C50u, ssa, signature, types,
entryObserverHeader: "vr/mkw_vr_instrumentation.h",
entryObserverSymbol: "MkwVRObserveTranslatedFunctionEntry");
var ordinary = new CxxLinearCodeGenerator().Emit(0x80553C50u, ssa, signature, types);
Assert.Contains("#include \"vr/mkw_vr_instrumentation.h\"", observed.Code);
Assert.Contains(
"MkwVRObserveTranslatedFunctionEntry(0x80553C50u, static_cast<const CpuContext*>(ctx));",
observed.Code);
Assert.True(observed.GuestAbiContract.HasFullSynchronizationFence);
Assert.Equal(uint.MaxValue, observed.GuestAbiContract.GprReadBeforeWriteMask);
Assert.Equal(0u, observed.GuestAbiContract.GprPossibleWriteMask);
Assert.Contains("fence=1", observed.GuestAbiMarker);
Assert.DoesNotContain("MkwVRObserveTranslatedFunctionEntry", ordinary);
}
}
@@ -18,7 +18,8 @@ public class RegisterResidencyCodeGenTests
RepresentationEnvironment? types = null,
bool stateFree = false,
uint entryPoint = 0x80001000u,
IReadOnlyDictionary<uint, GuestAbiContract>? guestAbiContracts = null)
IReadOnlyDictionary<uint, GuestAbiContract>? guestAbiContracts = null,
IReadOnlySet<uint>? fullContextCallTargets = null)
{
var contract = GuestAbiContractAnalyzer.Analyze(function);
return new CxxLinearCodeGenerator().Emit(
@@ -27,6 +28,7 @@ public class RegisterResidencyCodeGenTests
new FunctionAbiClassification(function.Name, ValueRepresentation.Void),
types ?? new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
guestAbiContracts: guestAbiContracts,
fullContextCallTargets: fullContextCallTargets,
emitStateFreeLeafVariant: stateFree,
stateFreeAbiContracts: stateFree
? new Dictionary<uint, GuestAbiContract> { [entryPoint] = contract }
@@ -308,6 +310,71 @@ public class RegisterResidencyCodeGenTests
Assert.DoesNotContain("r5 = ctx->gpr[5];", afterCall, StringComparison.Ordinal);
}
[Fact]
public void ContextObservingTargetForcesAFullDirectCallBoundary()
{
var target = 0x80002000u;
var contracts = new Dictionary<uint, GuestAbiContract>
{
[target] = NarrowContract(gprRead: 1u << 4, gprWrite: 1u << 3)
};
var body = FunctionBody(
Emit(
CallerAcross("observed_call", $"0x{target:X8}"),
guestAbiContracts: contracts,
fullContextCallTargets: new HashSet<uint> { target }),
"observed_call");
var call = body.IndexOf($"InvokeDirectCpu<0x{target:X8}u>(ctx);", StringComparison.Ordinal);
Assert.True(call > 0);
// r5 is outside the guest callee's contract, but a host observer may
// inspect it through CpuContext.
Assert.Contains("ctx->gpr[5] = r5;", body[..call], StringComparison.Ordinal);
Assert.Contains(" r4 = ctx->gpr[4];", body[call..], StringComparison.Ordinal);
}
[Fact]
public void ContextObserverForcesFullBoundariesThroughIntermediateCallers()
{
const uint root = 0x80001000u;
const uint middle = 0x80002000u;
const uint observed = 0x80003000u;
var rootFunction = CallerAcross("transitive_observed_call", $"0x{middle:X8}");
var functions = new Dictionary<uint, IrFunction>
{
[root] = rootFunction,
[middle] = new IrFunction("middle", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[]
{
new IrCall(string.Empty, $"0x{observed:X8}", Array.Empty<IrValue>()),
new IrReturn(null)
})
}),
[observed] = new IrFunction("observed", "entry", new[]
{
new IrBasicBlock("entry", new IrInstruction[] { new IrReturn(null) })
})
};
var contracts = GuestAbiInterproceduralAnalyzer.Analyze(
functions,
contextObservingEntryPoints: new[] { observed }).Contracts;
var body = FunctionBody(
Emit(
rootFunction,
entryPoint: root,
guestAbiContracts: contracts,
fullContextCallTargets: new HashSet<uint> { observed }),
"transitive_observed_call");
var call = body.IndexOf($"InvokeDirectCpu<0x{middle:X8}u>(ctx);", StringComparison.Ordinal);
Assert.True(contracts[middle].HasFullSynchronizationFence);
Assert.True(call > 0);
Assert.Contains("ctx->gpr[5] = r5;", body[..call], StringComparison.Ordinal);
Assert.Contains(" r4 = ctx->gpr[4];", body[call..], StringComparison.Ordinal);
}
[Fact]
public void BoundaryNarrowingFlushesRegistersTheCalleeOnlyMayWrite()
{