mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 01:00:14 +02:00
translator: add project entry observers for MKW VR
This commit is contained in:
1 parent
8b607ccbb9
commit
8c8e4561e9
15 files changed
+398
-25
No files matched your search
@@ -34,6 +34,19 @@ translation:
|
|||||||
#just a precaution, but this should never be needed for MKWii
|
#just a precaution, but this should never be needed for MKWii
|
||||||
allow_unsupported_instructions: false
|
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:
|
runtime:
|
||||||
native_registration_root: runtime/src
|
native_registration_root: runtime/src
|
||||||
native_abi_directories:
|
native_abi_directories:
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ the translator does not guess them.
|
|||||||
- `translation.function_map.path` - optional symbol map used as the discovery oracle.
|
- `translation.function_map.path` - optional symbol map used as the discovery oracle.
|
||||||
- `translation.allow_unsupported_instructions` - off by default; enabling it emits runtime traps
|
- `translation.allow_unsupported_instructions` - off by default; enabling it emits runtime traps
|
||||||
instead of failing, and such a build can never ship.
|
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.
|
Relative paths resolve from `workspace_root`, which itself resolves from the manifest directory.
|
||||||
|
|
||||||
|
|||||||
@@ -121,8 +121,14 @@ var modPatchProfiles = new Lazy<IReadOnlyList<Translator.Cli.Configuration.Proje
|
|||||||
var modPatchedAddresses = new Lazy<IReadOnlySet<uint>>(() =>
|
var modPatchedAddresses = new Lazy<IReadOnlySet<uint>>(() =>
|
||||||
BuildModPatchedAddresses(modPatchProfiles.Value));
|
BuildModPatchedAddresses(modPatchProfiles.Value));
|
||||||
var leafInliningBlockedTargets = new Lazy<IReadOnlySet<uint>>(() =>
|
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
|
// 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
|
// canonical graph the emission wave lowers is already inlined and every
|
||||||
@@ -417,6 +423,7 @@ int RunTranslateRecursive(string[] argsTail)
|
|||||||
outDir,
|
outDir,
|
||||||
RequireProject().Translation.EntryPoints
|
RequireProject().Translation.EntryPoints
|
||||||
.Append(startAddr)
|
.Append(startAddr)
|
||||||
|
.Concat(RequireProject().Translation.EntryObserver?.EntryPoints ?? new HashSet<uint>())
|
||||||
.Concat(functionMap?.Addresses ?? Array.Empty<uint>()),
|
.Concat(functionMap?.Addresses ?? Array.Empty<uint>()),
|
||||||
includeGeneratedHistory: false);
|
includeGeneratedHistory: false);
|
||||||
Console.WriteLine($"[translator] Loaded {knownBaseFunctionEntryPoints.Count:N0} known base function start(s) for tail-call boundary detection.");
|
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;
|
var residentTranslationExclusions = baseTranslationExclusions.Value;
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"[translator] Leaf inlining: {leafInliningBlockedTargets.Value.Count:N0} address(es) are not " +
|
$"[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)>();
|
var queue = new Queue<(uint addr, int depth)>();
|
||||||
queue.Enqueue((startAddr, 0));
|
queue.Enqueue((startAddr, 0));
|
||||||
visited.Add(startAddr);
|
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
|
// 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
|
// 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
|
// failing the run: unlike a walked call target, nothing proves the map
|
||||||
@@ -730,18 +750,28 @@ int RunTranslateRecursive(string[] argsTail)
|
|||||||
allowUnsupportedInstructions);
|
allowUnsupportedInstructions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The call-graph walk from the entry point, then everything the configured map
|
// The call-graph walk from the executable roots, then everything the configured
|
||||||
// knows about that the walk never reached. Those two sources replace the
|
// map knows about that the walk never reached. Those two sources replace the
|
||||||
// constructor, vtable, function-pointer and adjacent-prologue scans, which
|
// constructor, vtable, function-pointer and adjacent-prologue scans, which
|
||||||
// guessed at exactly this set - and the hand-maintained force-translate
|
// guessed at exactly this set - and the hand-maintained force-translate
|
||||||
// list that existed to paper over the addresses they still missed.
|
// list that existed to paper over the addresses they still missed.
|
||||||
ProcessQueue();
|
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;
|
var walkReachable = count;
|
||||||
ProcessSpeculativeSeeds();
|
ProcessSpeculativeSeeds();
|
||||||
if (functionMap is not null)
|
if (functionMap is not null)
|
||||||
{
|
{
|
||||||
Console.WriteLine(
|
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.");
|
$"{count - walkReachable:N0} added speculatively from the map.");
|
||||||
if (speculativeSkips.Count != 0)
|
if (speculativeSkips.Count != 0)
|
||||||
{
|
{
|
||||||
@@ -878,7 +908,8 @@ int RunTranslateRecursive(string[] argsTail)
|
|||||||
// rather than retaining every descendant dependency forever.
|
// rather than retaining every descendant dependency forever.
|
||||||
var interproceduralGuestAbi = GuestAbiInterproceduralAnalyzer.Analyze(
|
var interproceduralGuestAbi = GuestAbiInterproceduralAnalyzer.Analyze(
|
||||||
canonicalGuestFunctions,
|
canonicalGuestFunctions,
|
||||||
nativeGuestEffects.Contracts);
|
nativeGuestEffects.Contracts,
|
||||||
|
observerEntryPoints);
|
||||||
var guestAbiContracts = interproceduralGuestAbi.Contracts
|
var guestAbiContracts = interproceduralGuestAbi.Contracts
|
||||||
.ToDictionary(static pair => pair.Key, static pair => pair.Value);
|
.ToDictionary(static pair => pair.Key, static pair => pair.Value);
|
||||||
var recursiveComponents = interproceduralGuestAbi.StronglyConnectedComponents
|
var recursiveComponents = interproceduralGuestAbi.StronglyConnectedComponents
|
||||||
@@ -1007,6 +1038,7 @@ int RunTranslateRecursive(string[] argsTail)
|
|||||||
index,
|
index,
|
||||||
eligible = component.All(address =>
|
eligible = component.All(address =>
|
||||||
!residentTranslationExclusions.Contains(address) &&
|
!residentTranslationExclusions.Contains(address) &&
|
||||||
|
residentPolicy.EntryObserver?.EntryPoints.Contains(address) != true &&
|
||||||
!localGuestAbiContracts[address].HasFullSynchronizationFence &&
|
!localGuestAbiContracts[address].HasFullSynchronizationFence &&
|
||||||
GuestStateLivenessAnalyzer.CanDeconstructWithoutContext(canonicalGuestFunctions[address]) &&
|
GuestStateLivenessAnalyzer.CanDeconstructWithoutContext(canonicalGuestFunctions[address]) &&
|
||||||
(!directlyCalledGuestFunctions.Contains(address) ||
|
(!directlyCalledGuestFunctions.Contains(address) ||
|
||||||
@@ -1090,8 +1122,8 @@ int RunTranslateRecursive(string[] argsTail)
|
|||||||
StateFreeAbiContracts: stateFreeAbiContracts,
|
StateFreeAbiContracts: stateFreeAbiContracts,
|
||||||
StateFreeCallSymbols: stateFreeCallSymbols,
|
StateFreeCallSymbols: stateFreeCallSymbols,
|
||||||
StateFreeCallSiteVariants: stateFreeCallSiteVariantsByCaller.GetValueOrDefault(address),
|
StateFreeCallSiteVariants: stateFreeCallSiteVariantsByCaller.GetValueOrDefault(address),
|
||||||
StateFreeEntryVariants: stateFreeEntryVariantsByTarget.GetValueOrDefault(address),
|
StateFreeEntryVariants: stateFreeEntryVariantsByTarget.GetValueOrDefault(address),
|
||||||
ModOverridableCallTargets: modOverridableCallTargets),
|
ModOverridableCallTargets: modOverridableCallTargets),
|
||||||
residentPolicy,
|
residentPolicy,
|
||||||
address);
|
address);
|
||||||
}
|
}
|
||||||
@@ -4019,8 +4051,13 @@ static IReadOnlySet<uint> BuildModPatchedAddresses(
|
|||||||
static TranslationOptions WithProjectCodegenPolicy(
|
static TranslationOptions WithProjectCodegenPolicy(
|
||||||
TranslationOptions options,
|
TranslationOptions options,
|
||||||
ProjectTranslation translation,
|
ProjectTranslation translation,
|
||||||
uint entryPoint) =>
|
uint entryPoint)
|
||||||
options with
|
{
|
||||||
|
var fullContextCallTargets = options.FullContextCallTargets?.ToHashSet() ?? new HashSet<uint>();
|
||||||
|
if (translation.EntryObserver is { } observer)
|
||||||
|
fullContextCallTargets.UnionWith(observer.EntryPoints);
|
||||||
|
|
||||||
|
return options with
|
||||||
{
|
{
|
||||||
AllowUnsupportedInstructions = translation.AllowUnsupportedInstructions,
|
AllowUnsupportedInstructions = translation.AllowUnsupportedInstructions,
|
||||||
// Leaf ABI spill elision is structurally self-guarding: it requires a
|
// 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
|
// leaf, so a partial mod chunk that only carries one half of an ABI
|
||||||
// prologue is rejected rather than mis-transformed.
|
// prologue is rejected rather than mis-transformed.
|
||||||
EnableLeafAbiSpillElision = true,
|
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>
|
/// <summary>
|
||||||
/// One option carried by one command. <paramref name="Value"/> is the metavariable
|
/// One option carried by one command. <paramref name="Value"/> is the metavariable
|
||||||
|
|||||||
@@ -97,7 +97,8 @@ internal sealed class TranslationProjectConfig
|
|||||||
var translation = new ProjectTranslation(
|
var translation = new ProjectTranslation(
|
||||||
entryPoints,
|
entryPoints,
|
||||||
functionMapPath,
|
functionMapPath,
|
||||||
dto.Translation?.AllowUnsupportedInstructions ?? false);
|
dto.Translation?.AllowUnsupportedInstructions ?? false,
|
||||||
|
ResolveEntryObserver(dto.Translation?.EntryObserver));
|
||||||
|
|
||||||
var abiDirectories = (dto.Runtime?.NativeAbiDirectories ?? [])
|
var abiDirectories = (dto.Runtime?.NativeAbiDirectories ?? [])
|
||||||
.Select(pathValue => ResolvePath(workspaceRoot, pathValue))
|
.Select(pathValue => ResolvePath(workspaceRoot, pathValue))
|
||||||
@@ -262,6 +263,43 @@ internal sealed class TranslationProjectConfig
|
|||||||
private static string Require(string? value, string field) =>
|
private static string Require(string? value, string field) =>
|
||||||
!string.IsNullOrWhiteSpace(value) ? value : throw new InvalidDataException($"Translation project requires {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:
|
// 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
|
// 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.
|
// 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 List<string>? EntryPoints { get; init; }
|
||||||
public FunctionMapDto? FunctionMap { get; init; }
|
public FunctionMapDto? FunctionMap { get; init; }
|
||||||
public bool? AllowUnsupportedInstructions { 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
|
private sealed class FunctionMapDto
|
||||||
@@ -472,7 +518,12 @@ internal sealed record ProjectInputs(ProjectBinaryInput Dol, ProjectRelInput? Re
|
|||||||
internal sealed record ProjectTranslation(
|
internal sealed record ProjectTranslation(
|
||||||
IReadOnlyList<uint> EntryPoints,
|
IReadOnlyList<uint> EntryPoints,
|
||||||
string? FunctionMapPath,
|
string? FunctionMapPath,
|
||||||
bool AllowUnsupportedInstructions);
|
bool AllowUnsupportedInstructions,
|
||||||
|
ProjectEntryObserver? EntryObserver);
|
||||||
|
internal sealed record ProjectEntryObserver(
|
||||||
|
string Header,
|
||||||
|
string Symbol,
|
||||||
|
IReadOnlySet<uint> EntryPoints);
|
||||||
internal sealed record ProjectRuntime(
|
internal sealed record ProjectRuntime(
|
||||||
IReadOnlyList<string> NativeAbiDirectories,
|
IReadOnlyList<string> NativeAbiDirectories,
|
||||||
string NativeRegistrationRoot);
|
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>
|
/// <summary>
|
||||||
/// Successor count (0, 1, or 2) and labels for <paramref name="block"/>; count-plus-out-params
|
/// Successor count (0, 1, or 2) and labels for <paramref name="block"/>; count-plus-out-params
|
||||||
/// keeps the CFG walk allocation-free.
|
/// keeps the CFG walk allocation-free.
|
||||||
|
|||||||
@@ -18,9 +18,13 @@ public static class GuestAbiInterproceduralAnalyzer
|
|||||||
{
|
{
|
||||||
public static GuestAbiInterproceduralResult Analyze(
|
public static GuestAbiInterproceduralResult Analyze(
|
||||||
IReadOnlyDictionary<uint, IrFunction> functions,
|
IReadOnlyDictionary<uint, IrFunction> functions,
|
||||||
IReadOnlyDictionary<uint, GuestAbiContract>? externalContracts = null)
|
IReadOnlyDictionary<uint, GuestAbiContract>? externalContracts = null,
|
||||||
|
IEnumerable<uint>? contextObservingEntryPoints = null)
|
||||||
{
|
{
|
||||||
externalContracts ??= new Dictionary<uint, GuestAbiContract>();
|
externalContracts ??= new Dictionary<uint, GuestAbiContract>();
|
||||||
|
var fullContextSet = (contextObservingEntryPoints ?? Array.Empty<uint>())
|
||||||
|
.Where(functions.ContainsKey)
|
||||||
|
.ToHashSet();
|
||||||
var addresses = functions.Keys.OrderBy(static address => address).ToArray();
|
var addresses = functions.Keys.OrderBy(static address => address).ToArray();
|
||||||
|
|
||||||
// GuestAbiContractAnalyzer.Analyze is pure and the round's contract snapshot is frozen, so
|
// GuestAbiContractAnalyzer.Analyze is pure and the round's contract snapshot is frozen, so
|
||||||
@@ -40,7 +44,9 @@ public static class GuestAbiInterproceduralAnalyzer
|
|||||||
0,
|
0,
|
||||||
addresses.Length,
|
addresses.Length,
|
||||||
parallelOptions,
|
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);
|
var contracts = new Dictionary<uint, GuestAbiContract>(addresses.Length);
|
||||||
for (var index = 0; index < addresses.Length; ++index) contracts.Add(addresses[index], current[index]);
|
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 =>
|
Parallel.For(0, worklistCount, parallelOptions, position =>
|
||||||
{
|
{
|
||||||
var ordinal = worklist[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;
|
roundResults[position] = Equivalent(current[ordinal], next) ? null : next;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -124,6 +132,13 @@ public static class GuestAbiInterproceduralAnalyzer
|
|||||||
return new GuestAbiInterproceduralResult(contracts, BuildComponents(functions, contracts));
|
return new GuestAbiInterproceduralResult(contracts, BuildComponents(functions, contracts));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static GuestAbiContract AddFullContextEntryEffects(
|
||||||
|
GuestAbiContract contract,
|
||||||
|
bool requiresFullContext) =>
|
||||||
|
requiresFullContext
|
||||||
|
? GuestAbiContractAnalyzer.WithReadOnlyContextObserver(contract)
|
||||||
|
: contract;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ordinals of functions this one calls directly, from raw IR rather than
|
/// Ordinals of functions this one calls directly, from raw IR rather than
|
||||||
/// <see cref="GuestAbiContract.DirectCallTargets"/>, which omits inline save/restore thunks
|
/// <see cref="GuestAbiContract.DirectCallTargets"/>, which omits inline save/restore thunks
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ public sealed partial class CxxLinearCodeGenerator
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
private static ResidencyBoundarySync ResolveDirectCallBoundarySync(
|
private static ResidencyBoundarySync ResolveDirectCallBoundarySync(
|
||||||
uint target,
|
uint target,
|
||||||
|
|||||||
@@ -100,9 +100,21 @@ public sealed partial class CxxLinearCodeGenerator
|
|||||||
bool gqrConstantsRequireRuntimeGuard = false,
|
bool gqrConstantsRequireRuntimeGuard = false,
|
||||||
bool enableLeafAbiSpillElision = false,
|
bool enableLeafAbiSpillElision = false,
|
||||||
IReadOnlySet<uint>? modOverridableCallTargets = null,
|
IReadOnlySet<uint>? modOverridableCallTargets = null,
|
||||||
bool enableGpuFifoBurstCoalescing = true)
|
IReadOnlySet<uint>? fullContextCallTargets = null,
|
||||||
|
bool enableGpuFifoBurstCoalescing = true,
|
||||||
|
string? entryObserverHeader = null,
|
||||||
|
string? entryObserverSymbol = null)
|
||||||
{
|
{
|
||||||
modOverridableCallTargets ??= new HashSet<uint>();
|
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>();
|
nonReturningCallTargets ??= new HashSet<uint>();
|
||||||
lrContinuationCallTargets ??= new HashSet<uint>();
|
lrContinuationCallTargets ??= new HashSet<uint>();
|
||||||
guestAbiContracts ??= new Dictionary<uint, GuestAbiContract>();
|
guestAbiContracts ??= new Dictionary<uint, GuestAbiContract>();
|
||||||
@@ -149,6 +161,10 @@ public sealed partial class CxxLinearCodeGenerator
|
|||||||
// tuple for every instruction of every emission of this body.
|
// tuple for every instruction of every emission of this body.
|
||||||
var suppressedInstructionMasks = BuildSuppressedInstructionMasks(func, flagElision);
|
var suppressedInstructionMasks = BuildSuppressedInstructionMasks(func, flagElision);
|
||||||
var cxxName = SanitizeIdentifier(signature.Name);
|
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 pairedFlow = ComputePairedFlowStates(func, cfg, _guestAbiProvider);
|
||||||
var pairedIn = pairedFlow.In;
|
var pairedIn = pairedFlow.In;
|
||||||
var pairedOut = pairedFlow.Out;
|
var pairedOut = pairedFlow.Out;
|
||||||
@@ -661,6 +677,21 @@ public sealed partial class CxxLinearCodeGenerator
|
|||||||
{
|
{
|
||||||
code = ApplyGuardedGqrFunctionVersioning(code, implementationName, gqrEntryGuards);
|
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(
|
return new CxxEmissionResult(
|
||||||
code,
|
code,
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ public sealed record TranslationOptions(
|
|||||||
bool GqrConstantsRequireRuntimeGuard = false,
|
bool GqrConstantsRequireRuntimeGuard = false,
|
||||||
bool EnableLeafAbiSpillElision = false,
|
bool EnableLeafAbiSpillElision = false,
|
||||||
IReadOnlySet<uint>? ModOverridableCallTargets = null,
|
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 GenerateCxx = true,
|
||||||
bool DiscoveryOnly = false,
|
bool DiscoveryOnly = false,
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -70,7 +77,15 @@ public sealed record TranslationOptions(
|
|||||||
/// acyclic and every exit is a plain return. Emergency opt-out; the
|
/// acyclic and every exit is a plain return. Emergency opt-out; the
|
||||||
/// straight-line shape keeps working with this off.
|
/// straight-line shape keeps working with this off.
|
||||||
/// </summary>
|
/// </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();
|
public static TranslationOptions Default { get; } = new();
|
||||||
}
|
}
|
||||||
@@ -746,7 +761,10 @@ public sealed class FunctionTranslator
|
|||||||
gqrCalleeWriteMasks: options.GqrCalleeWriteMasks,
|
gqrCalleeWriteMasks: options.GqrCalleeWriteMasks,
|
||||||
gqrConstantsRequireRuntimeGuard: options.GqrConstantsRequireRuntimeGuard,
|
gqrConstantsRequireRuntimeGuard: options.GqrConstantsRequireRuntimeGuard,
|
||||||
enableLeafAbiSpillElision: options.EnableLeafAbiSpillElision,
|
enableLeafAbiSpillElision: options.EnableLeafAbiSpillElision,
|
||||||
modOverridableCallTargets: options.ModOverridableCallTargets);
|
modOverridableCallTargets: options.ModOverridableCallTargets,
|
||||||
|
fullContextCallTargets: options.FullContextCallTargets,
|
||||||
|
entryObserverHeader: options.EntryObserverHeader,
|
||||||
|
entryObserverSymbol: options.EntryObserverSymbol);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IrTracePpc CreateTrace(PpcInstruction instruction)
|
private static IrTracePpc CreateTrace(PpcInstruction instruction)
|
||||||
|
|||||||
@@ -38,7 +38,10 @@ internal static class CxxLinearCodeGeneratorEmitExtensions
|
|||||||
bool gqrConstantsRequireRuntimeGuard = false,
|
bool gqrConstantsRequireRuntimeGuard = false,
|
||||||
bool enableLeafAbiSpillElision = false,
|
bool enableLeafAbiSpillElision = false,
|
||||||
IReadOnlySet<uint>? modOverridableCallTargets = null,
|
IReadOnlySet<uint>? modOverridableCallTargets = null,
|
||||||
bool enableGpuFifoBurstCoalescing = true) =>
|
IReadOnlySet<uint>? fullContextCallTargets = null,
|
||||||
|
bool enableGpuFifoBurstCoalescing = true,
|
||||||
|
string? entryObserverHeader = null,
|
||||||
|
string? entryObserverSymbol = null) =>
|
||||||
generator.EmitWithFacts(
|
generator.EmitWithFacts(
|
||||||
entryPoint: entryPoint,
|
entryPoint: entryPoint,
|
||||||
ssa: ssa,
|
ssa: ssa,
|
||||||
@@ -63,5 +66,8 @@ internal static class CxxLinearCodeGeneratorEmitExtensions
|
|||||||
gqrConstantsRequireRuntimeGuard: gqrConstantsRequireRuntimeGuard,
|
gqrConstantsRequireRuntimeGuard: gqrConstantsRequireRuntimeGuard,
|
||||||
enableLeafAbiSpillElision: enableLeafAbiSpillElision,
|
enableLeafAbiSpillElision: enableLeafAbiSpillElision,
|
||||||
modOverridableCallTargets: modOverridableCallTargets,
|
modOverridableCallTargets: modOverridableCallTargets,
|
||||||
enableGpuFifoBurstCoalescing: enableGpuFifoBurstCoalescing).Code;
|
fullContextCallTargets: fullContextCallTargets,
|
||||||
|
enableGpuFifoBurstCoalescing: enableGpuFifoBurstCoalescing,
|
||||||
|
entryObserverHeader: entryObserverHeader,
|
||||||
|
entryObserverSymbol: entryObserverSymbol).Code;
|
||||||
}
|
}
|
||||||
@@ -110,6 +110,10 @@ public sealed class GenericProjectTests
|
|||||||
translation:
|
translation:
|
||||||
entry_points: [0x80001000]
|
entry_points: [0x80001000]
|
||||||
allow_unsupported_instructions: true
|
allow_unsupported_instructions: true
|
||||||
|
entry_observer:
|
||||||
|
header: instrumentation/entry_observer.h
|
||||||
|
symbol: ObserveEntry
|
||||||
|
entry_points: [0x80001000]
|
||||||
output:
|
output:
|
||||||
root: out
|
root: out
|
||||||
""");
|
""");
|
||||||
@@ -117,6 +121,10 @@ public sealed class GenericProjectTests
|
|||||||
var project = TranslationProjectConfig.Load(projectPath);
|
var project = TranslationProjectConfig.Load(projectPath);
|
||||||
Assert.Null(project.Inputs.Rel);
|
Assert.Null(project.Inputs.Rel);
|
||||||
Assert.Equal(entry, Assert.Single(project.Translation.EntryPoints));
|
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 dol = DolFile.Load(project.Inputs.Dol.Path);
|
||||||
var image = new ProgramImageBuilder().Build(dol, ramBase: project.Memory.Base, ramSize: project.Memory.Size);
|
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(
|
private static string Emit(
|
||||||
IrFunction function,
|
IrFunction function,
|
||||||
IReadOnlyDictionary<uint, byte>? gqrCalleeWriteMasks = null) =>
|
IReadOnlyDictionary<uint, byte>? gqrCalleeWriteMasks = null,
|
||||||
|
IReadOnlyDictionary<string, uint>? gqrEntryConstants = null,
|
||||||
|
IReadOnlySet<uint>? fullContextCallTargets = null) =>
|
||||||
new CxxLinearCodeGenerator().Emit(
|
new CxxLinearCodeGenerator().Emit(
|
||||||
0x80001000u,
|
0x80001000u,
|
||||||
new SsaTransformer().Convert(function),
|
new SsaTransformer().Convert(function),
|
||||||
new FunctionAbiClassification(function.Name, ValueRepresentation.Void),
|
new FunctionAbiClassification(function.Name, ValueRepresentation.Void),
|
||||||
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
|
new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
|
||||||
gqrCalleeWriteMasks: gqrCalleeWriteMasks);
|
gqrEntryConstants: gqrEntryConstants,
|
||||||
|
gqrCalleeWriteMasks: gqrCalleeWriteMasks,
|
||||||
|
fullContextCallTargets: fullContextCallTargets);
|
||||||
|
|
||||||
private static IrCall PsqLoad(int index, string address = "r4") =>
|
private static IrCall PsqLoad(int index, string address = "r4") =>
|
||||||
new("f1", "PPC_PsqL", [IrValue.Register(address), IrValue.Imm(0), IrValue.Imm(index)]);
|
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];"));
|
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]
|
[Fact]
|
||||||
public void RuntimeHelperCallsDoNotForceAReload()
|
public void RuntimeHelperCallsDoNotForceAReload()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -84,6 +84,35 @@ public sealed class GuestAbiInterproceduralAnalyzerTests
|
|||||||
result.Contracts[second].GprReadBeforeWriteMask & ((1u << 3) | (1u << 4)));
|
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]
|
[Fact]
|
||||||
public void DeepCallGraphDoesNotConsumeTheNativeStack()
|
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,
|
RepresentationEnvironment? types = null,
|
||||||
bool stateFree = false,
|
bool stateFree = false,
|
||||||
uint entryPoint = 0x80001000u,
|
uint entryPoint = 0x80001000u,
|
||||||
IReadOnlyDictionary<uint, GuestAbiContract>? guestAbiContracts = null)
|
IReadOnlyDictionary<uint, GuestAbiContract>? guestAbiContracts = null,
|
||||||
|
IReadOnlySet<uint>? fullContextCallTargets = null)
|
||||||
{
|
{
|
||||||
var contract = GuestAbiContractAnalyzer.Analyze(function);
|
var contract = GuestAbiContractAnalyzer.Analyze(function);
|
||||||
return new CxxLinearCodeGenerator().Emit(
|
return new CxxLinearCodeGenerator().Emit(
|
||||||
@@ -27,6 +28,7 @@ public class RegisterResidencyCodeGenTests
|
|||||||
new FunctionAbiClassification(function.Name, ValueRepresentation.Void),
|
new FunctionAbiClassification(function.Name, ValueRepresentation.Void),
|
||||||
types ?? new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
|
types ?? new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>()),
|
||||||
guestAbiContracts: guestAbiContracts,
|
guestAbiContracts: guestAbiContracts,
|
||||||
|
fullContextCallTargets: fullContextCallTargets,
|
||||||
emitStateFreeLeafVariant: stateFree,
|
emitStateFreeLeafVariant: stateFree,
|
||||||
stateFreeAbiContracts: stateFree
|
stateFreeAbiContracts: stateFree
|
||||||
? new Dictionary<uint, GuestAbiContract> { [entryPoint] = contract }
|
? new Dictionary<uint, GuestAbiContract> { [entryPoint] = contract }
|
||||||
@@ -308,6 +310,71 @@ public class RegisterResidencyCodeGenTests
|
|||||||
Assert.DoesNotContain("r5 = ctx->gpr[5];", afterCall, StringComparison.Ordinal);
|
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]
|
[Fact]
|
||||||
public void BoundaryNarrowingFlushesRegistersTheCalleeOnlyMayWrite()
|
public void BoundaryNarrowingFlushesRegistersTheCalleeOnlyMayWrite()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user