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
|
||||
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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user