mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 08:00:25 +02:00
init
This commit is contained in:
commit
ec226e8348
1099 files changed
+400981
No files matched your search
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Runs an indexed operation with the same sequential behavior used by the
|
||||
/// translator's parallel stages. Keeping the sequential path explicit makes
|
||||
/// single-threaded runs deterministic and avoids AggregateException wrapping.
|
||||
/// </summary>
|
||||
internal static class IndexedParallel
|
||||
{
|
||||
internal static void For(int count, ParallelOptions parallelOptions, Action<int> body)
|
||||
{
|
||||
if (count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (parallelOptions.MaxDegreeOfParallelism <= 1 || count == 1)
|
||||
{
|
||||
for (var index = 0; index < count; index++)
|
||||
{
|
||||
body(index);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Parallel.For(0, count, parallelOptions, body);
|
||||
}
|
||||
catch (AggregateException ex)
|
||||
{
|
||||
// Preserve the recursive translator's historical behavior: a
|
||||
// single worker failure is reported as that failure, while a
|
||||
// genuinely multi-failure pass remains aggregate-shaped.
|
||||
throw ex.InnerExceptions.Count == 1 ? ex.InnerExceptions[0] : ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Translator.Core.Disassembly;
|
||||
using Translator.Core.Translation;
|
||||
|
||||
/// <summary>One function a mod translation wave has to emit.</summary>
|
||||
sealed record ModTranslationWork(
|
||||
uint Address,
|
||||
TranslationOptions Options,
|
||||
string OutputPath);
|
||||
|
||||
/// <summary>
|
||||
/// The outcome of one work item: either a translation or the message explaining
|
||||
/// why that address could not be translated. Failures are values, not
|
||||
/// exceptions, so one bad address cannot abandon the rest of its wave.
|
||||
/// </summary>
|
||||
sealed record ModTranslationAttempt(
|
||||
ModTranslationWork Work,
|
||||
FunctionTranslationResult? Result,
|
||||
string? Error);
|
||||
|
||||
/// <summary>
|
||||
/// Translates one wave of mod work items in parallel and returns the attempts in
|
||||
/// a deterministic (Address, OutputPath) order, so the emitted bundle never
|
||||
/// depends on thread count or completion order.
|
||||
/// </summary>
|
||||
static class ModTranslationWaveRunner
|
||||
{
|
||||
internal static ModTranslationAttempt[] Translate(
|
||||
Func<FunctionTranslator> translatorFactory,
|
||||
IReadOnlyList<ModTranslationWork> wave,
|
||||
ParallelOptions parallelOptions)
|
||||
{
|
||||
var attempts = new ModTranslationAttempt[wave.Count];
|
||||
void TranslateAt(int index)
|
||||
{
|
||||
var work = wave[index];
|
||||
try
|
||||
{
|
||||
attempts[index] = new ModTranslationAttempt(
|
||||
work,
|
||||
translatorFactory().Translate(work.Address, work.Options),
|
||||
null);
|
||||
}
|
||||
catch (Exception ex) when (ex is InvalidOperationException or ArgumentOutOfRangeException or PpcDisassemblyLimitExceededException)
|
||||
{
|
||||
attempts[index] = new ModTranslationAttempt(
|
||||
work,
|
||||
null,
|
||||
$"0x{work.Address:X8} {work.Options.PreferredName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
IndexedParallel.For(wave.Count, parallelOptions, TranslateAt);
|
||||
|
||||
return attempts
|
||||
.OrderBy(attempt => attempt.Work.Address)
|
||||
.ThenBy(attempt => attempt.Work.OutputPath, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Translator.Tests")]
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Small FIFO primitive shared by the recursive and mod wave schedulers.
|
||||
/// Traversal-specific policy (depth, visited sets, retries, and failure
|
||||
/// handling) intentionally remains with each caller.
|
||||
/// </summary>
|
||||
internal static class QueueBatch
|
||||
{
|
||||
internal static List<T> Dequeue<T>(
|
||||
Queue<T> queue,
|
||||
int maximumCount,
|
||||
Func<T, bool>? skip = null)
|
||||
{
|
||||
if (maximumCount <= 0)
|
||||
{
|
||||
return new List<T>();
|
||||
}
|
||||
|
||||
var batch = new List<T>(Math.Min(queue.Count, maximumCount));
|
||||
while (queue.Count > 0 && batch.Count < maximumCount)
|
||||
{
|
||||
var item = queue.Dequeue();
|
||||
if (skip is not null && skip(item))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
batch.Add(item);
|
||||
}
|
||||
|
||||
return batch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,506 @@
|
||||
using System.Globalization;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
using Translator.Core.Loading;
|
||||
|
||||
namespace Translator.Cli.Configuration;
|
||||
|
||||
internal sealed class TranslationProjectConfig
|
||||
{
|
||||
private TranslationProjectConfig(
|
||||
string sourcePath,
|
||||
string workspaceRoot,
|
||||
ProjectIdentity identity,
|
||||
ProjectMemory memory,
|
||||
ProjectInputs inputs,
|
||||
ProjectTranslation translation,
|
||||
ProjectRuntime runtime,
|
||||
ProjectOutput output,
|
||||
IReadOnlyDictionary<string, ProjectProfile> profiles)
|
||||
{
|
||||
SourcePath = sourcePath;
|
||||
WorkspaceRoot = workspaceRoot;
|
||||
Identity = identity;
|
||||
Memory = memory;
|
||||
Inputs = inputs;
|
||||
Translation = translation;
|
||||
Runtime = runtime;
|
||||
Output = output;
|
||||
Profiles = profiles;
|
||||
}
|
||||
|
||||
public string SourcePath { get; }
|
||||
public string WorkspaceRoot { get; }
|
||||
public ProjectIdentity Identity { get; }
|
||||
public ProjectMemory Memory { get; }
|
||||
public ProjectInputs Inputs { get; }
|
||||
public ProjectTranslation Translation { get; }
|
||||
public ProjectRuntime Runtime { get; }
|
||||
public ProjectOutput Output { get; }
|
||||
public IReadOnlyDictionary<string, ProjectProfile> Profiles { get; }
|
||||
|
||||
public static TranslationProjectConfig Load(string path)
|
||||
{
|
||||
var sourcePath = Path.GetFullPath(path);
|
||||
if (!File.Exists(sourcePath))
|
||||
{
|
||||
throw new FileNotFoundException("Translation project manifest not found.", sourcePath);
|
||||
}
|
||||
|
||||
var deserializer = new DeserializerBuilder()
|
||||
.WithNamingConvention(UnderscoredNamingConvention.Instance)
|
||||
.Build();
|
||||
var dto = deserializer.Deserialize<ProjectDto>(File.ReadAllText(sourcePath))
|
||||
?? throw new InvalidDataException($"Translation project manifest is empty: {sourcePath}");
|
||||
if (dto.SchemaVersion != 1)
|
||||
{
|
||||
throw new InvalidDataException($"Unsupported translation project schema_version {dto.SchemaVersion}; expected 1.");
|
||||
}
|
||||
|
||||
var manifestDirectory = Path.GetDirectoryName(sourcePath)!;
|
||||
var workspaceRoot = ResolvePath(manifestDirectory, dto.WorkspaceRoot ?? ".");
|
||||
var identity = new ProjectIdentity(
|
||||
Require(dto.Project?.Id, "project.id"),
|
||||
dto.Project?.DisplayName ?? dto.Project?.Id!,
|
||||
dto.Project?.GameId,
|
||||
dto.Project?.Region,
|
||||
dto.Project?.BaseManifestFormat ?? "recomp-base-manifest",
|
||||
dto.Project?.BaseManifestStem ?? "base");
|
||||
var memory = new ProjectMemory(
|
||||
ParseUInt32(dto.Memory?.Base, MemoryLayout.RamBase, "memory.base"),
|
||||
ParseInt32(dto.Memory?.Size, MemoryLayout.RamSize, "memory.size"),
|
||||
ParseOptionalUInt32(dto.Memory?.SdaBase, "memory.sda_base"),
|
||||
ParseOptionalUInt32(dto.Memory?.Sda2Base, "memory.sda2_base"));
|
||||
|
||||
var dolDto = dto.Inputs?.Dol ?? throw new InvalidDataException("Translation project requires inputs.dol.");
|
||||
var dol = ResolveInput(workspaceRoot, dolDto, "inputs.dol");
|
||||
ProjectRelInput? rel = null;
|
||||
if (dto.Inputs?.Rel is { Path.Length: > 0 } relDto)
|
||||
{
|
||||
var relInput = ResolveInput(workspaceRoot, relDto, "inputs.rel");
|
||||
rel = new ProjectRelInput(
|
||||
relInput.Path,
|
||||
relInput.Sha256,
|
||||
ParseUInt32(relDto.LoadAddress, null, "inputs.rel.load_address"));
|
||||
}
|
||||
|
||||
var entryPoints = (dto.Translation?.EntryPoints ?? [])
|
||||
.Select((value, index) => ParseUInt32(value, null, $"translation.entry_points[{index}]"))
|
||||
.ToArray();
|
||||
string? functionMapPath = null;
|
||||
if (dto.Translation?.FunctionMap is { } functionMapDto)
|
||||
{
|
||||
functionMapPath = ResolvePath(
|
||||
workspaceRoot,
|
||||
Require(functionMapDto.Path, "translation.function_map.path"));
|
||||
}
|
||||
var translation = new ProjectTranslation(
|
||||
entryPoints,
|
||||
functionMapPath,
|
||||
dto.Translation?.AllowUnsupportedInstructions ?? false);
|
||||
|
||||
var abiDirectories = (dto.Runtime?.NativeAbiDirectories ?? [])
|
||||
.Select(pathValue => ResolvePath(workspaceRoot, pathValue))
|
||||
.ToArray();
|
||||
var runtime = new ProjectRuntime(
|
||||
abiDirectories,
|
||||
ResolvePath(workspaceRoot, dto.Runtime?.NativeRegistrationRoot ?? "runtime/src"));
|
||||
var outputRoot = ResolvePath(workspaceRoot, dto.Output?.Root ?? "generated");
|
||||
var output = new ProjectOutput(
|
||||
outputRoot,
|
||||
ResolveOutput(outputRoot, dto.Output?.Functions, "functions"),
|
||||
ResolveOutput(outputRoot, dto.Output?.RuntimeConfig, "RuntimeConfig.h"),
|
||||
ResolveOutput(outputRoot, dto.Output?.DataInitializer, "data_sections_init.cpp"),
|
||||
ResolveOutput(outputRoot, dto.Output?.BaseManifest, Path.Combine("base", "base_manifest.json")));
|
||||
|
||||
var profiles = new Dictionary<string, ProjectProfile>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var (name, profileDto) in dto.Profiles ?? new Dictionary<string, ProfileDto>())
|
||||
{
|
||||
profiles[name] = new ProjectProfile(
|
||||
name,
|
||||
profileDto.Enabled ?? true,
|
||||
ResolveOptionalPath(workspaceRoot, profileDto.CodePul),
|
||||
ResolveOptionalPath(workspaceRoot, profileDto.ModRoot),
|
||||
profileDto.Region,
|
||||
ParseOptionalUInt32(profileDto.ModuleGuestBase, $"profiles.{name}.module_guest_base"),
|
||||
ParseOptionalUInt32(profileDto.ModuleLinkBase, $"profiles.{name}.module_link_base"),
|
||||
ResolveOptionalPath(workspaceRoot, profileDto.Output),
|
||||
profileDto.EnableRetroWfc ?? false,
|
||||
profileDto.RetroWfcPayload,
|
||||
ParseOptionalUInt32(
|
||||
profileDto.RetroWfcLegacyBootstrapHook,
|
||||
$"profiles.{name}.retro_wfc_legacy_bootstrap_hook"),
|
||||
profileDto.RequiresGameId,
|
||||
NormalizeHash(profileDto.RequiresDolSha256),
|
||||
ResolveRiivolution(profileDto.Riivolution, name));
|
||||
}
|
||||
|
||||
var config = new TranslationProjectConfig(
|
||||
sourcePath,
|
||||
workspaceRoot,
|
||||
identity,
|
||||
memory,
|
||||
new ProjectInputs(dol, rel),
|
||||
translation,
|
||||
runtime,
|
||||
output,
|
||||
profiles);
|
||||
config.ValidateInputs();
|
||||
return config;
|
||||
}
|
||||
|
||||
public ProjectProfile? GetProfile(string? name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name) || string.Equals(name, "base", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Profiles.TryGetValue(name, out var profile)
|
||||
? profile
|
||||
: throw new InvalidDataException($"Project '{Identity.Id}' has no profile named '{name}'.");
|
||||
}
|
||||
|
||||
public void ValidateProfile(ProjectProfile profile)
|
||||
{
|
||||
if (!profile.Enabled)
|
||||
{
|
||||
throw new InvalidDataException($"Profile '{profile.Name}' is disabled.");
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(profile.RequiresGameId) &&
|
||||
!string.Equals(profile.RequiresGameId, Identity.GameId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Profile '{profile.Name}' requires game ID {profile.RequiresGameId}, but project is {Identity.GameId ?? "unspecified"}.");
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(profile.RequiresDolSha256) &&
|
||||
!string.Equals(profile.RequiresDolSha256, FileSha256(Inputs.Dol.Path), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidDataException($"Profile '{profile.Name}' is not compatible with this DOL hash.");
|
||||
}
|
||||
if (profile.EnableRetroWfc && string.IsNullOrWhiteSpace(profile.RetroWfcPayload))
|
||||
{
|
||||
throw new InvalidDataException($"Profile '{profile.Name}' enables Retro WFC but has no payload.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The pinned Small Data Area bases (r13/r2), or a diagnostic naming which manifest keys are missing.</summary>
|
||||
public (uint Sda1Base, uint Sda2Base) RequireSdaBases()
|
||||
{
|
||||
if (Memory.SdaBase is { } sda1 && Memory.Sda2Base is { } sda2)
|
||||
{
|
||||
return (sda1, sda2);
|
||||
}
|
||||
|
||||
var missing = Memory.SdaBase is null
|
||||
? Memory.Sda2Base is null ? "memory.sda_base and memory.sda2_base" : "memory.sda_base"
|
||||
: "memory.sda2_base";
|
||||
throw new InvalidDataException(
|
||||
$"Translation project '{Identity.Id}' is missing {missing} in {SourcePath}. " +
|
||||
"These are the r13/r2 Small Data Area bases the boot code installs (_SDA_BASE_ and _SDA2_BASE_); " +
|
||||
"read them from the lis/ori pair in __init_registers of the pinned DOL and add them as hex strings " +
|
||||
"under the manifest's memory: section.");
|
||||
}
|
||||
|
||||
private void ValidateInputs()
|
||||
{
|
||||
ValidateInput(Inputs.Dol, "DOL");
|
||||
if (Inputs.Rel is not null)
|
||||
{
|
||||
ValidateInput(Inputs.Rel, "REL");
|
||||
}
|
||||
if (Translation.FunctionMapPath is { } functionMapPath && !File.Exists(functionMapPath))
|
||||
{
|
||||
throw new FileNotFoundException("Configured function map was not found.", functionMapPath);
|
||||
}
|
||||
if (Memory.Size <= 0)
|
||||
{
|
||||
throw new InvalidDataException("memory.size must be positive.");
|
||||
}
|
||||
}
|
||||
|
||||
private static ProjectBinaryInput ResolveInput(string root, BinaryInputDto input, string field)
|
||||
{
|
||||
var path = ResolvePath(root, Require(input.Path, $"{field}.path"));
|
||||
return new ProjectBinaryInput(path, NormalizeHash(input.Sha256));
|
||||
}
|
||||
|
||||
private static void ValidateInput(ProjectBinaryInput input, string label)
|
||||
{
|
||||
if (!File.Exists(input.Path))
|
||||
{
|
||||
throw new FileNotFoundException($"Configured {label} input was not found.", input.Path);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(input.Sha256))
|
||||
{
|
||||
var actual = FileSha256(input.Path);
|
||||
if (!string.Equals(actual, input.Sha256, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidDataException($"Configured {label} SHA-256 does not match {input.Path}. Expected {input.Sha256}, got {actual}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string FileSha256(string path) =>
|
||||
ChecksumUtilities.Sha256HexOfFile(path);
|
||||
|
||||
private static string? NormalizeHash(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim().ToLowerInvariant();
|
||||
|
||||
private static string ResolveOutput(string outputRoot, string? value, string fallback) =>
|
||||
ResolvePath(outputRoot, string.IsNullOrWhiteSpace(value) ? fallback : value);
|
||||
|
||||
private static string? ResolveOptionalPath(string root, string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? null : ResolvePath(root, value);
|
||||
|
||||
private static string ResolvePath(string root, string value)
|
||||
{
|
||||
var expanded = Environment.ExpandEnvironmentVariables(value);
|
||||
return Path.GetFullPath(Path.IsPathRooted(expanded) ? expanded : Path.Combine(root, expanded));
|
||||
}
|
||||
|
||||
private static string Require(string? value, string field) =>
|
||||
!string.IsNullOrWhiteSpace(value) ? value : throw new InvalidDataException($"Translation project requires {field}.");
|
||||
|
||||
// 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.
|
||||
private static ProjectRiivolution? ResolveRiivolution(RiivolutionDto? dto, string profileName)
|
||||
{
|
||||
if (dto is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var xml = NormalizePackRelativePath(dto.Xml);
|
||||
if (xml.Length == 0)
|
||||
{
|
||||
throw new InvalidDataException($"Translation project requires profiles.{profileName}.riivolution.xml.");
|
||||
}
|
||||
|
||||
var entries = dto.Options ?? [];
|
||||
var options = new List<ProjectRiivolutionOption>(entries.Count);
|
||||
for (var index = 0; index < entries.Count; index++)
|
||||
{
|
||||
var field = $"profiles.{profileName}.riivolution.options[{index}]";
|
||||
var option = entries[index].Option?.Trim() ?? string.Empty;
|
||||
if (option.Length == 0)
|
||||
{
|
||||
throw new InvalidDataException($"Translation project requires {field}.option.");
|
||||
}
|
||||
options.Add(new ProjectRiivolutionOption(
|
||||
entries[index].Section?.Trim() ?? string.Empty,
|
||||
option,
|
||||
ParseUInt32(entries[index].Choice, 0u, $"{field}.choice")));
|
||||
}
|
||||
|
||||
return new ProjectRiivolution(xml, options);
|
||||
}
|
||||
|
||||
private static string NormalizePackRelativePath(string? value)
|
||||
{
|
||||
var text = (value ?? string.Empty).Trim().Replace('\\', '/');
|
||||
while (true)
|
||||
{
|
||||
if (text.StartsWith("./", StringComparison.Ordinal))
|
||||
{
|
||||
text = text[2..];
|
||||
continue;
|
||||
}
|
||||
if (text.StartsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
text = text[1..];
|
||||
continue;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
private static uint ParseUInt32(string? value, uint? fallback, string field)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return fallback ?? throw new InvalidDataException($"Translation project requires {field}.");
|
||||
}
|
||||
var text = value.Trim();
|
||||
var style = NumberStyles.Integer;
|
||||
if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
text = text[2..];
|
||||
style = NumberStyles.HexNumber;
|
||||
}
|
||||
return uint.TryParse(text, style, CultureInfo.InvariantCulture, out var result)
|
||||
? result
|
||||
: throw new InvalidDataException($"Invalid unsigned integer '{value}' for {field}.");
|
||||
}
|
||||
|
||||
private static uint? ParseOptionalUInt32(string? value, string field) =>
|
||||
string.IsNullOrWhiteSpace(value) ? null : ParseUInt32(value, null, field);
|
||||
|
||||
private static int ParseInt32(string? value, int fallback, string field)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
var parsed = ParseUInt32(value, null, field);
|
||||
return parsed <= int.MaxValue
|
||||
? (int)parsed
|
||||
: throw new InvalidDataException($"{field} exceeds Int32 range.");
|
||||
}
|
||||
|
||||
private sealed class ProjectDto
|
||||
{
|
||||
public int SchemaVersion { get; init; }
|
||||
public string? WorkspaceRoot { get; init; }
|
||||
public IdentityDto? Project { get; init; }
|
||||
public MemoryDto? Memory { get; init; }
|
||||
public InputsDto? Inputs { get; init; }
|
||||
public TranslationDto? Translation { get; init; }
|
||||
public RuntimeDto? Runtime { get; init; }
|
||||
public OutputDto? Output { get; init; }
|
||||
public Dictionary<string, ProfileDto>? Profiles { get; init; }
|
||||
}
|
||||
|
||||
private sealed class IdentityDto
|
||||
{
|
||||
public string? Id { get; init; }
|
||||
public string? DisplayName { get; init; }
|
||||
public string? GameId { get; init; }
|
||||
public string? Region { get; init; }
|
||||
public string? BaseManifestFormat { get; init; }
|
||||
public string? BaseManifestStem { get; init; }
|
||||
}
|
||||
|
||||
private sealed class MemoryDto
|
||||
{
|
||||
public string? Base { get; init; }
|
||||
public string? Size { get; init; }
|
||||
public string? SdaBase { get; init; }
|
||||
public string? Sda2Base { get; init; }
|
||||
}
|
||||
|
||||
private sealed class InputsDto
|
||||
{
|
||||
public BinaryInputDto? Dol { get; init; }
|
||||
public RelInputDto? Rel { get; init; }
|
||||
}
|
||||
|
||||
private class BinaryInputDto
|
||||
{
|
||||
public string Path { get; init; } = string.Empty;
|
||||
public string? Sha256 { get; init; }
|
||||
}
|
||||
|
||||
private sealed class RelInputDto : BinaryInputDto
|
||||
{
|
||||
public string? LoadAddress { get; init; }
|
||||
}
|
||||
|
||||
// Translation behaviour (state-free ABI, register residency, leaf inlining, flat guest memory) is
|
||||
// unconditional; only inputs are declared here. function_map is one: every function start in the
|
||||
// image, which the recursive pass seeds from.
|
||||
private sealed class TranslationDto
|
||||
{
|
||||
public List<string>? EntryPoints { get; init; }
|
||||
public FunctionMapDto? FunctionMap { get; init; }
|
||||
public bool? AllowUnsupportedInstructions { get; init; }
|
||||
}
|
||||
|
||||
private sealed class FunctionMapDto
|
||||
{
|
||||
public string? Path { get; init; }
|
||||
}
|
||||
|
||||
private sealed class RuntimeDto
|
||||
{
|
||||
public List<string>? NativeAbiDirectories { get; init; }
|
||||
public string? NativeRegistrationRoot { get; init; }
|
||||
}
|
||||
|
||||
private sealed class OutputDto
|
||||
{
|
||||
public string? Root { get; init; }
|
||||
public string? Functions { get; init; }
|
||||
public string? RuntimeConfig { get; init; }
|
||||
public string? DataInitializer { get; init; }
|
||||
public string? BaseManifest { get; init; }
|
||||
}
|
||||
|
||||
private sealed class ProfileDto
|
||||
{
|
||||
public bool? Enabled { get; init; }
|
||||
public string? CodePul { get; init; }
|
||||
public string? ModRoot { get; init; }
|
||||
public string? Region { get; init; }
|
||||
public string? ModuleGuestBase { get; init; }
|
||||
public string? ModuleLinkBase { get; init; }
|
||||
public string? Output { get; init; }
|
||||
public bool? EnableRetroWfc { get; init; }
|
||||
public string? RetroWfcPayload { get; init; }
|
||||
public string? RetroWfcLegacyBootstrapHook { get; init; }
|
||||
public string? RequiresGameId { get; init; }
|
||||
public string? RequiresDolSha256 { get; init; }
|
||||
public RiivolutionDto? Riivolution { get; init; }
|
||||
}
|
||||
|
||||
private sealed class RiivolutionDto
|
||||
{
|
||||
public string? Xml { get; init; }
|
||||
public List<RiivolutionOptionDto>? Options { get; init; }
|
||||
}
|
||||
|
||||
private sealed class RiivolutionOptionDto
|
||||
{
|
||||
public string? Section { get; init; }
|
||||
public string? Option { get; init; }
|
||||
public string? Choice { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record ProjectIdentity(
|
||||
string Id,
|
||||
string DisplayName,
|
||||
string? GameId,
|
||||
string? Region,
|
||||
string BaseManifestFormat,
|
||||
string BaseManifestStem);
|
||||
internal sealed record ProjectMemory(uint Base, int Size, uint? SdaBase = null, uint? Sda2Base = null);
|
||||
internal record ProjectBinaryInput(string Path, string? Sha256);
|
||||
internal sealed record ProjectRelInput(string Path, string? Sha256, uint LoadAddress) : ProjectBinaryInput(Path, Sha256);
|
||||
internal sealed record ProjectInputs(ProjectBinaryInput Dol, ProjectRelInput? Rel);
|
||||
internal sealed record ProjectTranslation(
|
||||
IReadOnlyList<uint> EntryPoints,
|
||||
string? FunctionMapPath,
|
||||
bool AllowUnsupportedInstructions);
|
||||
internal sealed record ProjectRuntime(
|
||||
IReadOnlyList<string> NativeAbiDirectories,
|
||||
string NativeRegistrationRoot);
|
||||
internal sealed record ProjectOutput(
|
||||
string Root,
|
||||
string Functions,
|
||||
string RuntimeConfig,
|
||||
string DataInitializer,
|
||||
string BaseManifest);
|
||||
internal sealed record ProjectProfile(
|
||||
string Name,
|
||||
bool Enabled,
|
||||
string? CodePul,
|
||||
string? ModRoot,
|
||||
string? Region,
|
||||
uint? ModuleGuestBase,
|
||||
uint? ModuleLinkBase,
|
||||
string? Output,
|
||||
bool EnableRetroWfc,
|
||||
string? RetroWfcPayload,
|
||||
uint? RetroWfcLegacyBootstrapHook,
|
||||
string? RequiresGameId,
|
||||
string? RequiresDolSha256,
|
||||
ProjectRiivolution? Riivolution);
|
||||
internal sealed record ProjectRiivolution(
|
||||
string Xml,
|
||||
IReadOnlyList<ProjectRiivolutionOption> Options);
|
||||
internal sealed record ProjectRiivolutionOption(
|
||||
string Section,
|
||||
string Option,
|
||||
uint Choice);
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
|
||||
<VersionPrefix>0.5.0</VersionPrefix>
|
||||
<Company>WiiCompiled</Company>
|
||||
<Product>Mario Kart Wii Static Translator</Product>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Translator.Core\Translator.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="YamlDotNet" Version="15.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using Translator.Core.Disassembly;
|
||||
|
||||
namespace Translator.Core.Analysis.BasicBlocks;
|
||||
|
||||
public sealed class BasicBlock
|
||||
{
|
||||
public BasicBlock(uint startAddress, IReadOnlyList<PpcInstruction> instructions)
|
||||
{
|
||||
StartAddress = startAddress;
|
||||
Instructions = instructions;
|
||||
Successors = new List<uint>();
|
||||
Predecessors = new List<uint>();
|
||||
}
|
||||
|
||||
public uint StartAddress { get; }
|
||||
public IReadOnlyList<PpcInstruction> Instructions { get; }
|
||||
public List<uint> Successors { get; }
|
||||
public List<uint> Predecessors { get; }
|
||||
|
||||
public PpcInstruction Terminator => Instructions[^1];
|
||||
|
||||
public override string ToString() => $"BB 0x{StartAddress:X8} (len {Instructions.Count})";
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Translator.Core.Disassembly;
|
||||
|
||||
namespace Translator.Core.Analysis.BasicBlocks;
|
||||
|
||||
/// <summary>
|
||||
/// Slices a linear instruction list into basic blocks and wires up successor/predecessor links.
|
||||
/// </summary>
|
||||
public static class BasicBlockBuilder
|
||||
{
|
||||
public static IReadOnlyList<BasicBlock> Build(
|
||||
IReadOnlyList<PpcInstruction> instructions,
|
||||
IEnumerable<uint>? extraLeaders = null)
|
||||
{
|
||||
if (instructions.Count == 0)
|
||||
{
|
||||
return Array.Empty<BasicBlock>();
|
||||
}
|
||||
|
||||
var leaders = new HashSet<uint> { instructions[0].Address };
|
||||
if (extraLeaders is not null)
|
||||
{
|
||||
// Only the requested leaders need a containment structure; building
|
||||
// one over every instruction address was the expensive half.
|
||||
HashSet<uint>? requested = null;
|
||||
foreach (var leader in extraLeaders)
|
||||
{
|
||||
(requested ??= new HashSet<uint>()).Add(leader);
|
||||
}
|
||||
|
||||
if (requested is not null)
|
||||
{
|
||||
foreach (var instruction in instructions)
|
||||
{
|
||||
if (requested.Contains(instruction.Address))
|
||||
{
|
||||
leaders.Add(instruction.Address);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < instructions.Count; i++)
|
||||
{
|
||||
var ins = instructions[i];
|
||||
if (i > 0)
|
||||
{
|
||||
var previous = instructions[i - 1];
|
||||
if (previous.IsReturn ||
|
||||
previous.IsUnconditionalBranch ||
|
||||
previous.EndAddress != ins.Address)
|
||||
{
|
||||
leaders.Add(ins.Address);
|
||||
}
|
||||
}
|
||||
|
||||
if (ins.IsConditionalBranch)
|
||||
{
|
||||
foreach (var target in ins.BranchTargets)
|
||||
{
|
||||
leaders.Add(target);
|
||||
}
|
||||
|
||||
leaders.Add(ins.EndAddress); // fallthrough
|
||||
}
|
||||
else if (ins.IsUnconditionalBranch)
|
||||
{
|
||||
foreach (var target in ins.BranchTargets)
|
||||
{
|
||||
leaders.Add(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sortedLeaders = new uint[leaders.Count];
|
||||
leaders.CopyTo(sortedLeaders);
|
||||
Array.Sort(sortedLeaders);
|
||||
|
||||
// One pass over the instructions instead of a full rescan per leader.
|
||||
// Each instruction lands in the block of the greatest leader that is not
|
||||
// above it, which is exactly the half-open range the old filter used.
|
||||
var buckets = new List<PpcInstruction>?[sortedLeaders.Length];
|
||||
foreach (var instruction in instructions)
|
||||
{
|
||||
var address = instruction.Address;
|
||||
var index = Array.BinarySearch(sortedLeaders, address);
|
||||
if (index < 0)
|
||||
{
|
||||
index = ~index - 1;
|
||||
}
|
||||
|
||||
if (index < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// The final block's exclusive end was uint.MaxValue.
|
||||
if (index == sortedLeaders.Length - 1 && address == uint.MaxValue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
(buckets[index] ??= new List<PpcInstruction>()).Add(instruction);
|
||||
}
|
||||
|
||||
var blocks = new List<BasicBlock>();
|
||||
for (var i = 0; i < sortedLeaders.Length; i++)
|
||||
{
|
||||
var blockInstructions = buckets[i];
|
||||
if (blockInstructions is null || blockInstructions.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var block = new BasicBlock(sortedLeaders[i], blockInstructions);
|
||||
blocks.Add(block);
|
||||
}
|
||||
|
||||
// Wire successors/predecessors.
|
||||
var lookup = blocks.ToDictionary(b => b.StartAddress, b => b);
|
||||
foreach (var block in blocks)
|
||||
{
|
||||
var term = block.Terminator;
|
||||
if (term.IsConditionalBranch)
|
||||
{
|
||||
foreach (var target in term.BranchTargets)
|
||||
{
|
||||
AddEdge(block, lookup, target);
|
||||
}
|
||||
|
||||
AddEdge(block, lookup, term.EndAddress);
|
||||
}
|
||||
else if (term.IsUnconditionalBranch || term.IsCall)
|
||||
{
|
||||
foreach (var target in term.BranchTargets)
|
||||
{
|
||||
AddEdge(block, lookup, target);
|
||||
}
|
||||
}
|
||||
else if (!term.IsReturn)
|
||||
{
|
||||
// Fallthrough
|
||||
AddEdge(block, lookup, term.EndAddress);
|
||||
}
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private static void AddEdge(BasicBlock from, IDictionary<uint, BasicBlock> lookup, uint toAddress)
|
||||
{
|
||||
if (!lookup.TryGetValue(toAddress, out var to))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!from.Successors.Contains(toAddress))
|
||||
{
|
||||
from.Successors.Add(toAddress);
|
||||
}
|
||||
|
||||
if (!to.Predecessors.Contains(from.StartAddress))
|
||||
{
|
||||
to.Predecessors.Add(from.StartAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,569 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Translator.Core.Ir;
|
||||
|
||||
namespace Translator.Core.Analysis;
|
||||
|
||||
[Flags]
|
||||
public enum GuestCallBoundaryFlags
|
||||
{
|
||||
None = 0,
|
||||
RequiresCompleteContext = 1 << 0,
|
||||
CanSuspend = 1 << 1,
|
||||
CanSwitchThreads = 1 << 2,
|
||||
InvokesGuestCode = 1 << 3
|
||||
}
|
||||
|
||||
public sealed record GuestAbiContract(
|
||||
uint GprReadBeforeWriteMask,
|
||||
uint GprPossibleWriteMask,
|
||||
uint GprReturnMask,
|
||||
uint FprReadBeforeWriteMask,
|
||||
uint FprPossibleWriteMask,
|
||||
uint FprReturnMask,
|
||||
byte CrReadBeforeWriteMask,
|
||||
byte CrPossibleWriteMask,
|
||||
bool ReadsXerBeforeWrite,
|
||||
bool MayWriteXer,
|
||||
bool ReadsCtrBeforeWrite,
|
||||
bool MayWriteCtr,
|
||||
bool ReadsLrBeforeWrite,
|
||||
bool MayWriteLr,
|
||||
GuestCallBoundaryFlags BoundaryFlags,
|
||||
IReadOnlyList<uint> DirectCallTargets)
|
||||
{
|
||||
public bool ReadsFpscrBeforeWrite { get; init; }
|
||||
public bool MayWriteFpscr { get; init; }
|
||||
public byte GqrReadBeforeWriteMask { get; init; }
|
||||
public byte GqrPossibleWriteMask { get; init; }
|
||||
public byte HidReadBeforeWriteMask { get; init; }
|
||||
public byte HidPossibleWriteMask { get; init; }
|
||||
public uint GprDefiniteWriteMask { get; init; }
|
||||
public uint FprDefiniteWriteMask { get; init; }
|
||||
public byte CrDefiniteWriteMask { get; init; }
|
||||
public bool DefinitelyWritesXer { get; init; }
|
||||
public bool DefinitelyWritesCtr { get; init; }
|
||||
public bool DefinitelyWritesLr { get; init; }
|
||||
public bool DefinitelyWritesFpscr { get; init; }
|
||||
public byte GqrDefiniteWriteMask { get; init; }
|
||||
public byte HidDefiniteWriteMask { get; init; }
|
||||
|
||||
public bool HasFullSynchronizationFence =>
|
||||
(BoundaryFlags & GuestCallBoundaryFlags.RequiresCompleteContext) != 0;
|
||||
}
|
||||
|
||||
public static class GuestAbiContractAnalyzer
|
||||
{
|
||||
private readonly record struct RegisterSet(
|
||||
uint Gpr, uint Fpr, byte Cr, bool Xer, bool Ctr, bool Lr, bool Fpscr, byte Gqr, byte Hid)
|
||||
{
|
||||
public static RegisterSet Empty => default;
|
||||
public RegisterSet Union(RegisterSet other) =>
|
||||
new(Gpr | other.Gpr, Fpr | other.Fpr, (byte)(Cr | other.Cr),
|
||||
Xer || other.Xer, Ctr || other.Ctr, Lr || other.Lr,
|
||||
Fpscr || other.Fpscr, (byte)(Gqr | other.Gqr), (byte)(Hid | other.Hid));
|
||||
public RegisterSet Intersect(RegisterSet other) =>
|
||||
new(Gpr & other.Gpr, Fpr & other.Fpr, (byte)(Cr & other.Cr),
|
||||
Xer && other.Xer, Ctr && other.Ctr, Lr && other.Lr,
|
||||
Fpscr && other.Fpscr, (byte)(Gqr & other.Gqr), (byte)(Hid & other.Hid));
|
||||
public RegisterSet Except(RegisterSet other) =>
|
||||
new(Gpr & ~other.Gpr, Fpr & ~other.Fpr, (byte)(Cr & ~other.Cr),
|
||||
Xer && !other.Xer, Ctr && !other.Ctr, Lr && !other.Lr,
|
||||
Fpscr && !other.Fpscr, (byte)(Gqr & ~other.Gqr), (byte)(Hid & ~other.Hid));
|
||||
}
|
||||
|
||||
public static GuestAbiContract Analyze(
|
||||
IrFunction function,
|
||||
IReadOnlyDictionary<uint, GuestAbiContract>? calleeContracts = null)
|
||||
{
|
||||
// Block labels are replaced by ordinals for the duration of the
|
||||
// analysis: the fixpoint below runs on every function several times per
|
||||
// interprocedural round, and string-keyed dictionaries dominated it.
|
||||
var blockList = function.Blocks;
|
||||
var blockCount = blockList.Count;
|
||||
var ordinals = new Dictionary<string, int>(blockCount, StringComparer.Ordinal);
|
||||
for (var index = 0; index < blockCount; ++index)
|
||||
{
|
||||
ordinals.Add(blockList[index].Label, index);
|
||||
}
|
||||
|
||||
var predecessors = new List<int>[blockCount];
|
||||
for (var index = 0; index < blockCount; ++index) predecessors[index] = new List<int>();
|
||||
for (var index = 0; index < blockCount; ++index)
|
||||
{
|
||||
var successorCount = Successors(blockList[index], out var first, out var second);
|
||||
if (successorCount >= 1 && ordinals.TryGetValue(first, out var firstOrdinal))
|
||||
predecessors[firstOrdinal].Add(index);
|
||||
if (successorCount == 2 && ordinals.TryGetValue(second, out var secondOrdinal))
|
||||
predecessors[secondOrdinal].Add(index);
|
||||
}
|
||||
|
||||
var definiteWritesByBlock = new RegisterSet[blockCount];
|
||||
for (var index = 0; index < blockCount; ++index)
|
||||
{
|
||||
var instructions = blockList[index].Instructions;
|
||||
var set = RegisterSet.Empty;
|
||||
for (var position = 0; position < instructions.Count; ++position)
|
||||
{
|
||||
Writes(instructions[position], calleeContracts, out _, out var definiteWrite);
|
||||
set = set.Union(definiteWrite);
|
||||
}
|
||||
definiteWritesByBlock[index] = set;
|
||||
}
|
||||
|
||||
var definiteIn = new RegisterSet[blockCount];
|
||||
var definiteOut = new RegisterSet[blockCount];
|
||||
|
||||
bool changed;
|
||||
do
|
||||
{
|
||||
changed = false;
|
||||
for (var index = 0; index < blockCount; ++index)
|
||||
{
|
||||
var blockPredecessors = predecessors[index];
|
||||
RegisterSet incoming;
|
||||
if (blockList[index].Label == function.EntryLabel || blockPredecessors.Count == 0)
|
||||
{
|
||||
incoming = RegisterSet.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
incoming = definiteOut[blockPredecessors[0]];
|
||||
for (var position = 1; position < blockPredecessors.Count; ++position)
|
||||
incoming = incoming.Intersect(definiteOut[blockPredecessors[position]]);
|
||||
}
|
||||
|
||||
var outgoing = incoming.Union(definiteWritesByBlock[index]);
|
||||
if (incoming != definiteIn[index] || outgoing != definiteOut[index])
|
||||
{
|
||||
definiteIn[index] = incoming;
|
||||
definiteOut[index] = outgoing;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
} while (changed);
|
||||
|
||||
var readBeforeWrite = RegisterSet.Empty;
|
||||
var possibleWrites = RegisterSet.Empty;
|
||||
var directTargets = new SortedSet<uint>();
|
||||
var boundaryFlags = GuestCallBoundaryFlags.None;
|
||||
for (var index = 0; index < blockCount; ++index)
|
||||
{
|
||||
var written = definiteIn[index];
|
||||
var instructions = blockList[index].Instructions;
|
||||
for (var position = 0; position < instructions.Count; ++position)
|
||||
{
|
||||
var instruction = instructions[position];
|
||||
readBeforeWrite = readBeforeWrite.Union(Reads(instruction, calleeContracts).Except(written));
|
||||
Writes(instruction, calleeContracts, out var possibleInstructionWrites, out var definiteInstructionWrites);
|
||||
possibleWrites = possibleWrites.Union(possibleInstructionWrites);
|
||||
written = written.Union(definiteInstructionWrites);
|
||||
if (instruction is IrCall call && GuestTargetParser.TryParseAddress(call.Target, out var target))
|
||||
{
|
||||
if (!TryGetInlineThunk(target, out _, out _))
|
||||
directTargets.Add(target);
|
||||
if (calleeContracts?.TryGetValue(target, out var callee) == true)
|
||||
boundaryFlags |= callee.BoundaryFlags;
|
||||
}
|
||||
else if (instruction is IrCall helperCall)
|
||||
{
|
||||
boundaryFlags |= GuestHelperEffectCatalog.Analyze(helperCall).BoundaryFlags;
|
||||
}
|
||||
if (instruction is IrIndirectCall or IrIndirectJump or IrUndefined)
|
||||
{
|
||||
boundaryFlags |= GuestCallBoundaryFlags.RequiresCompleteContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var definiteWrites = RegisterSet.Empty;
|
||||
var sawExitBlock = false;
|
||||
for (var index = 0; index < blockCount; ++index)
|
||||
{
|
||||
var successorCount = Successors(blockList[index], out var first, out var second);
|
||||
if ((successorCount >= 1 && ordinals.ContainsKey(first)) ||
|
||||
(successorCount == 2 && ordinals.ContainsKey(second)))
|
||||
continue;
|
||||
definiteWrites = sawExitBlock ? definiteWrites.Intersect(definiteOut[index]) : definiteOut[index];
|
||||
sawExitBlock = true;
|
||||
}
|
||||
|
||||
const uint gprReturnMask = (1u << 3) | (1u << 4);
|
||||
const uint fprReturnMask = 1u << 1;
|
||||
return new GuestAbiContract(
|
||||
readBeforeWrite.Gpr,
|
||||
possibleWrites.Gpr,
|
||||
possibleWrites.Gpr & gprReturnMask,
|
||||
readBeforeWrite.Fpr,
|
||||
possibleWrites.Fpr,
|
||||
possibleWrites.Fpr & fprReturnMask,
|
||||
readBeforeWrite.Cr,
|
||||
possibleWrites.Cr,
|
||||
readBeforeWrite.Xer,
|
||||
possibleWrites.Xer,
|
||||
readBeforeWrite.Ctr,
|
||||
possibleWrites.Ctr,
|
||||
readBeforeWrite.Lr,
|
||||
possibleWrites.Lr,
|
||||
boundaryFlags,
|
||||
directTargets.ToArray())
|
||||
{
|
||||
ReadsFpscrBeforeWrite = readBeforeWrite.Fpscr,
|
||||
MayWriteFpscr = possibleWrites.Fpscr,
|
||||
GqrReadBeforeWriteMask = readBeforeWrite.Gqr,
|
||||
GqrPossibleWriteMask = possibleWrites.Gqr,
|
||||
HidReadBeforeWriteMask = readBeforeWrite.Hid,
|
||||
HidPossibleWriteMask = possibleWrites.Hid,
|
||||
GprDefiniteWriteMask = definiteWrites.Gpr,
|
||||
FprDefiniteWriteMask = definiteWrites.Fpr,
|
||||
CrDefiniteWriteMask = definiteWrites.Cr,
|
||||
DefinitelyWritesXer = definiteWrites.Xer,
|
||||
DefinitelyWritesCtr = definiteWrites.Ctr,
|
||||
DefinitelyWritesLr = definiteWrites.Lr,
|
||||
DefinitelyWritesFpscr = definiteWrites.Fpscr,
|
||||
GqrDefiniteWriteMask = definiteWrites.Gqr,
|
||||
HidDefiniteWriteMask = definiteWrites.Hid
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Successor count (0, 1, or 2) and labels for <paramref name="block"/>; count-plus-out-params
|
||||
/// keeps the CFG walk allocation-free.
|
||||
/// </summary>
|
||||
private static int Successors(IrBasicBlock block, out string first, out string second)
|
||||
{
|
||||
var instructions = block.Instructions;
|
||||
var terminator = instructions.Count == 0 ? null : instructions[instructions.Count - 1];
|
||||
if (terminator is IrBranch branch)
|
||||
{
|
||||
first = branch.TrueLabel;
|
||||
second = branch.FalseLabel;
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (terminator is IrJump jump)
|
||||
{
|
||||
first = jump.TargetLabel;
|
||||
second = string.Empty;
|
||||
return 1;
|
||||
}
|
||||
|
||||
first = string.Empty;
|
||||
second = string.Empty;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static RegisterSet Reads(
|
||||
IrInstruction instruction,
|
||||
IReadOnlyDictionary<uint, GuestAbiContract>? calleeContracts)
|
||||
{
|
||||
var result = RegisterSet.Empty;
|
||||
switch (instruction)
|
||||
{
|
||||
case IrAssign value: AddValue(ref result, value.Value); break;
|
||||
case IrBinary value: AddValue(ref result, value.Left); AddValue(ref result, value.Right); break;
|
||||
case IrLoad value: Add(ref result, value.Address.Base); break;
|
||||
case IrStore value: Add(ref result, value.Address.Base); AddValue(ref result, value.Source); break;
|
||||
case IrResolveGuestMemoryRange value: AddValue(ref result, value.Base); break;
|
||||
case IrResolvedLoad value: Add(ref result, value.OriginalAddress.Base); break;
|
||||
case IrResolvedStore value: Add(ref result, value.OriginalAddress.Base); AddValue(ref result, value.Source); break;
|
||||
case IrResolvedPsqLoad value:
|
||||
AddValue(ref result, value.OriginalAddress);
|
||||
if (value.KnownGqr is null || value.GuardKnownGqr) AddGqr(ref result, value.I);
|
||||
break;
|
||||
case IrResolvedPsqStore value:
|
||||
AddValue(ref result, value.OriginalAddress); AddValue(ref result, value.Source);
|
||||
if (value.KnownGqr is null || value.GuardKnownGqr) AddGqr(ref result, value.I);
|
||||
break;
|
||||
case IrResolvedLoadPair value:
|
||||
Add(ref result, value.FirstOriginalAddress.Base);
|
||||
Add(ref result, value.SecondOriginalAddress.Base);
|
||||
break;
|
||||
case IrResolvedStorePair value:
|
||||
Add(ref result, value.FirstOriginalAddress.Base);
|
||||
Add(ref result, value.SecondOriginalAddress.Base);
|
||||
AddValue(ref result, value.FirstSource);
|
||||
AddValue(ref result, value.SecondSource);
|
||||
break;
|
||||
case IrCall value:
|
||||
if (GuestTargetParser.TryParseAddress(value.Target, out var directTarget) &&
|
||||
calleeContracts?.TryGetValue(directTarget, out var callee) == true)
|
||||
{
|
||||
// A decoded call carries the volatile ABI registers as generic IR arguments;
|
||||
// once the callee has a contract, only state it actually consumes is a real read.
|
||||
result = result.Union(ContractReads(callee));
|
||||
}
|
||||
else if (GuestTargetParser.TryParseAddress(value.Target, out var readThunkAddress) &&
|
||||
TryGetInlineThunk(readThunkAddress, out var readThunk, out var readStart) &&
|
||||
readThunk is InlineThunkKind.SaveGpr or InlineThunkKind.SaveFpr)
|
||||
{
|
||||
AddRegisterRangeToTop(ref result, readThunk == InlineThunkKind.SaveGpr, readStart);
|
||||
}
|
||||
else if (!GuestTargetParser.TryParseAddress(value.Target, out _))
|
||||
{
|
||||
foreach (var argument in value.Arguments) AddValue(ref result, argument);
|
||||
result = result.Union(HiddenReads(GuestHelperEffectCatalog.Analyze(value)));
|
||||
}
|
||||
else
|
||||
{
|
||||
// An unresolved direct/native target has no stronger
|
||||
// contract, so its explicitly modelled ABI arguments remain
|
||||
// observable inputs.
|
||||
foreach (var argument in value.Arguments) AddValue(ref result, argument);
|
||||
}
|
||||
break;
|
||||
case IrIndirectCall value:
|
||||
AddValue(ref result, value.Target);
|
||||
foreach (var argument in value.Arguments) AddValue(ref result, argument);
|
||||
result = result.Union(FullState);
|
||||
break;
|
||||
case IrIndirectJump value:
|
||||
AddValue(ref result, value.Target);
|
||||
result = result.Union(FullState);
|
||||
break;
|
||||
case IrSetCrField value:
|
||||
AddValue(ref result, value.Left);
|
||||
AddValue(ref result, value.Right);
|
||||
// Integer comparisons copy XER.SO into the CR field.
|
||||
result = result with { Xer = true };
|
||||
break;
|
||||
case IrPhi value: foreach (var source in value.Sources.Values) Add(ref result, source); break;
|
||||
case IrBranch value:
|
||||
Add(ref result, value.ConditionRegister);
|
||||
result = result.Union(SpecialRegistersInRawExpression(value.ConditionRegister));
|
||||
break;
|
||||
case IrJumpTable value: Add(ref result, value.Selector); break;
|
||||
case IrReturn { Value: { } value }: AddValue(ref result, value); break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Classifies both write flavours of an instruction in one pass: <paramref name="definite"/> is
|
||||
/// guaranteed, <paramref name="possible"/> is what it may write. Only call cases differ.
|
||||
/// </summary>
|
||||
private static void Writes(
|
||||
IrInstruction instruction,
|
||||
IReadOnlyDictionary<uint, GuestAbiContract>? calleeContracts,
|
||||
out RegisterSet possible,
|
||||
out RegisterSet definite)
|
||||
{
|
||||
var shared = RegisterSet.Empty;
|
||||
var possibleOnly = RegisterSet.Empty;
|
||||
var definiteOnly = RegisterSet.Empty;
|
||||
switch (instruction)
|
||||
{
|
||||
case IrAssign value: Add(ref shared, value.Destination); break;
|
||||
case IrBinary value: Add(ref shared, value.Destination); break;
|
||||
case IrLoad value: Add(ref shared, value.Destination); break;
|
||||
case IrResolveGuestMemoryRange value: Add(ref shared, value.Destination); break;
|
||||
case IrResolvedLoad value: Add(ref shared, value.Destination); break;
|
||||
case IrResolvedPsqLoad value: Add(ref shared, value.Destination); break;
|
||||
case IrResolvedLoadPair value:
|
||||
Add(ref shared, value.FirstDestination);
|
||||
Add(ref shared, value.SecondDestination);
|
||||
break;
|
||||
case IrCall value:
|
||||
var helperEffect = !GuestTargetParser.TryParseAddress(value.Target, out _)
|
||||
? GuestHelperEffectCatalog.Analyze(value)
|
||||
: null;
|
||||
var destination = Register(value.Destination);
|
||||
// CR helpers return the complete packed CR only because that is
|
||||
// the runtime helper ABI. Architecturally they change the fields
|
||||
// described by the helper contract, not all eight fields.
|
||||
if (!(destination.Cr == byte.MaxValue && helperEffect?.CrWriteMask != 0))
|
||||
shared = shared.Union(destination);
|
||||
if (GuestTargetParser.TryParseAddress(value.Target, out var directTarget) &&
|
||||
calleeContracts?.TryGetValue(directTarget, out var callee) == true)
|
||||
{
|
||||
possibleOnly = ContractWrites(callee);
|
||||
definiteOnly = ContractDefiniteWrites(callee);
|
||||
}
|
||||
else if (GuestTargetParser.TryParseAddress(value.Target, out var writeThunkAddress) &&
|
||||
TryGetInlineThunk(writeThunkAddress, out var writeThunk, out var writeStart) &&
|
||||
writeThunk is InlineThunkKind.RestGpr or InlineThunkKind.RestFpr)
|
||||
{
|
||||
AddRegisterRangeToTop(ref shared, writeThunk == InlineThunkKind.RestGpr, writeStart);
|
||||
}
|
||||
else if (helperEffect is not null)
|
||||
{
|
||||
// Hidden helper state is a possible write only: the definite
|
||||
// flavour deliberately ignores non-guest call targets.
|
||||
possibleOnly = HiddenWrites(helperEffect);
|
||||
}
|
||||
break;
|
||||
case IrIndirectCall value:
|
||||
Add(ref shared, value.Destination);
|
||||
shared = shared.Union(FullState);
|
||||
break;
|
||||
case IrSetCrField value:
|
||||
shared = shared with { Cr = (byte)(shared.Cr | (1 << value.FieldIndex)) };
|
||||
break;
|
||||
case IrPhi value: Add(ref shared, value.Destination); break;
|
||||
case IrUndefined:
|
||||
shared = FullState;
|
||||
break;
|
||||
}
|
||||
|
||||
possible = shared.Union(possibleOnly);
|
||||
definite = shared.Union(definiteOnly);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unions <c>rN..r31</c> or <c>fN..f31</c>, the register window an inline
|
||||
/// save/restore thunk touches.
|
||||
/// </summary>
|
||||
private static void AddRegisterRangeToTop(ref RegisterSet set, bool general, int startRegister)
|
||||
{
|
||||
var mask = uint.MaxValue << startRegister;
|
||||
set = general
|
||||
? set with { Gpr = set.Gpr | mask }
|
||||
: set with { Fpr = set.Fpr | mask };
|
||||
}
|
||||
|
||||
private static void Add(ref RegisterSet set, string? name) => set = set.Union(Register(name));
|
||||
|
||||
private static void AddValue(ref RegisterSet set, IrValue value)
|
||||
{
|
||||
if (value.Kind == "register") Add(ref set, value.RegisterName);
|
||||
}
|
||||
|
||||
private static void AddGqr(ref RegisterSet set, uint index)
|
||||
{
|
||||
// Mirrors Register("gqr{index}"): only the eight architectural
|
||||
// quantization registers exist, anything else names nothing.
|
||||
if (index <= 7) set = set with { Gqr = (byte)(set.Gqr | (1 << (int)index)) };
|
||||
}
|
||||
|
||||
private static RegisterSet HiddenReads(GuestHelperEffect effect) =>
|
||||
new(effect.GprReadMask, effect.FprReadMask, effect.CrReadMask,
|
||||
effect.ReadsXer, effect.ReadsCtr, effect.ReadsLr, false, 0, 0);
|
||||
|
||||
private static RegisterSet HiddenWrites(GuestHelperEffect effect) =>
|
||||
new(effect.GprWriteMask, effect.FprWriteMask, effect.CrWriteMask,
|
||||
effect.WritesXer, effect.WritesCtr, effect.WritesLr, false, 0, 0);
|
||||
|
||||
private static RegisterSet ContractReads(GuestAbiContract contract) =>
|
||||
new(contract.GprReadBeforeWriteMask, contract.FprReadBeforeWriteMask,
|
||||
contract.CrReadBeforeWriteMask, contract.ReadsXerBeforeWrite,
|
||||
contract.ReadsCtrBeforeWrite, contract.ReadsLrBeforeWrite,
|
||||
contract.ReadsFpscrBeforeWrite, contract.GqrReadBeforeWriteMask,
|
||||
contract.HidReadBeforeWriteMask);
|
||||
|
||||
private static RegisterSet ContractWrites(GuestAbiContract contract) =>
|
||||
new(contract.GprPossibleWriteMask, contract.FprPossibleWriteMask,
|
||||
contract.CrPossibleWriteMask, contract.MayWriteXer,
|
||||
contract.MayWriteCtr, contract.MayWriteLr,
|
||||
contract.MayWriteFpscr, contract.GqrPossibleWriteMask,
|
||||
contract.HidPossibleWriteMask);
|
||||
|
||||
private static RegisterSet ContractDefiniteWrites(GuestAbiContract contract) =>
|
||||
new(contract.GprDefiniteWriteMask, contract.FprDefiniteWriteMask,
|
||||
contract.CrDefiniteWriteMask, contract.DefinitelyWritesXer,
|
||||
contract.DefinitelyWritesCtr, contract.DefinitelyWritesLr,
|
||||
contract.DefinitelyWritesFpscr, contract.GqrDefiniteWriteMask,
|
||||
contract.HidDefiniteWriteMask);
|
||||
|
||||
private static RegisterSet FullState =>
|
||||
new(uint.MaxValue, uint.MaxValue, byte.MaxValue, true, true, true, true, byte.MaxValue, byte.MaxValue);
|
||||
|
||||
private static RegisterSet Register(string? name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name)) return RegisterSet.Empty;
|
||||
// Classification runs on a span: an SSA name such as "r3_12" used to
|
||||
// cost two substrings and a LINQ digit scan before anything was decided.
|
||||
var baseName = SsaBaseName(name.AsSpan());
|
||||
if (baseName.Length >= 2 && baseName[0] == 'r' && int.TryParse(baseName[1..], out var gpr) && gpr is >= 0 and < 32)
|
||||
return RegisterSet.Empty with { Gpr = 1u << gpr };
|
||||
if (baseName.Length >= 2 && baseName[0] == 'f' && int.TryParse(baseName[1..], out var fpr) && fpr is >= 0 and < 32)
|
||||
return RegisterSet.Empty with { Fpr = 1u << fpr };
|
||||
if (baseName.Length == 3 && baseName.StartsWith("cr", StringComparison.OrdinalIgnoreCase) &&
|
||||
baseName[2] is >= '0' and <= '7')
|
||||
return RegisterSet.Empty with { Cr = (byte)(1 << (baseName[2] - '0')) };
|
||||
if (baseName.Length is >= 4 and <= 5 && baseName.StartsWith("crb", StringComparison.OrdinalIgnoreCase) &&
|
||||
int.TryParse(baseName[3..], out var crBit) && crBit is >= 0 and < 32)
|
||||
return RegisterSet.Empty with { Cr = (byte)(1 << (crBit / 4)) };
|
||||
if (baseName.Equals("cr", StringComparison.OrdinalIgnoreCase))
|
||||
return RegisterSet.Empty with { Cr = byte.MaxValue };
|
||||
if (baseName.Equals("xer", StringComparison.OrdinalIgnoreCase))
|
||||
return RegisterSet.Empty with { Xer = true };
|
||||
if (baseName.Equals("ctr", StringComparison.OrdinalIgnoreCase))
|
||||
return RegisterSet.Empty with { Ctr = true };
|
||||
if (baseName.Equals("lr", StringComparison.OrdinalIgnoreCase))
|
||||
return RegisterSet.Empty with { Lr = true };
|
||||
if (baseName.Equals("fpscr", StringComparison.OrdinalIgnoreCase))
|
||||
return RegisterSet.Empty with { Fpscr = true };
|
||||
if (baseName.Length == 4 && baseName.StartsWith("gqr", StringComparison.OrdinalIgnoreCase) &&
|
||||
baseName[3] is >= '0' and <= '7')
|
||||
return RegisterSet.Empty with { Gqr = (byte)(1 << (baseName[3] - '0')) };
|
||||
if (baseName.Length == 4 && baseName.StartsWith("hid", StringComparison.OrdinalIgnoreCase) &&
|
||||
baseName[3] is >= '0' and <= '2')
|
||||
return RegisterSet.Empty with { Hid = (byte)(1 << (baseName[3] - '0')) };
|
||||
return RegisterSet.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops a trailing SSA version suffix: everything after the first
|
||||
/// underscore is removed when it consists solely of digits.
|
||||
/// </summary>
|
||||
private static ReadOnlySpan<char> SsaBaseName(ReadOnlySpan<char> name)
|
||||
{
|
||||
var underscore = name.IndexOf('_');
|
||||
if (underscore <= 0) return name;
|
||||
for (var index = underscore + 1; index < name.Length; ++index)
|
||||
{
|
||||
if (!char.IsDigit(name[index])) return name;
|
||||
}
|
||||
|
||||
return name[..underscore];
|
||||
}
|
||||
|
||||
private static RegisterSet SpecialRegistersInRawExpression(string expression) =>
|
||||
new(
|
||||
0,
|
||||
0,
|
||||
expression.Contains("ctx->cr", StringComparison.Ordinal) ||
|
||||
expression.Contains("GetCRBit(ctx", StringComparison.Ordinal) ? byte.MaxValue : (byte)0,
|
||||
expression.Contains("ctx->xer", StringComparison.Ordinal),
|
||||
expression.Contains("ctx->ctr", StringComparison.Ordinal),
|
||||
expression.Contains("ctx->lr", StringComparison.Ordinal),
|
||||
expression.Contains("ctx->fpscr", StringComparison.Ordinal),
|
||||
0,
|
||||
(byte)((expression.Contains("ctx->hid0", StringComparison.Ordinal) ? 1 : 0) |
|
||||
(expression.Contains("ctx->hid1", StringComparison.Ordinal) ? 2 : 0) |
|
||||
(expression.Contains("ctx->hid2", StringComparison.Ordinal) ? 4 : 0)));
|
||||
|
||||
private enum InlineThunkKind { SaveGpr, RestGpr, SaveFpr, RestFpr }
|
||||
|
||||
private static bool TryGetInlineThunk(uint address, out InlineThunkKind kind, out int startRegister)
|
||||
{
|
||||
var thunks = GuestSaveRestoreThunks.Current;
|
||||
if (TryRange(address, thunks.SaveGpr, out startRegister)) { kind = InlineThunkKind.SaveGpr; return true; }
|
||||
if (TryRange(address, thunks.RestGpr, out startRegister)) { kind = InlineThunkKind.RestGpr; return true; }
|
||||
if (TryRange(address, thunks.SaveFpr, out startRegister)) { kind = InlineThunkKind.SaveFpr; return true; }
|
||||
if (TryRange(address, thunks.RestFpr, out startRegister)) { kind = InlineThunkKind.RestFpr; return true; }
|
||||
kind = default;
|
||||
startRegister = -1;
|
||||
return false;
|
||||
|
||||
static bool TryRange(uint candidate, GuestSaveRestoreThunkRange? range, out int start)
|
||||
{
|
||||
if (range is null)
|
||||
{
|
||||
start = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
var (baseAddress, firstRegister, lastStartRegister) = range;
|
||||
var lastAddress = baseAddress + checked((uint)((lastStartRegister - firstRegister) * 4));
|
||||
if (candidate < baseAddress || candidate > lastAddress || ((candidate - baseAddress) & 3u) != 0)
|
||||
{
|
||||
start = -1;
|
||||
return false;
|
||||
}
|
||||
start = firstRegister + checked((int)((candidate - baseAddress) / 4));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Translator.Core.Ir;
|
||||
|
||||
namespace Translator.Core.Analysis;
|
||||
|
||||
public sealed record GuestAbiInterproceduralResult(
|
||||
IReadOnlyDictionary<uint, GuestAbiContract> Contracts,
|
||||
IReadOnlyList<IReadOnlyList<uint>> StronglyConnectedComponents);
|
||||
|
||||
/// <summary>
|
||||
/// Computes architectural effects in guest execution order, so a callee read is suppressed once
|
||||
/// the caller already defined that register, unlike a transitive union of descendant masks.
|
||||
/// </summary>
|
||||
public static class GuestAbiInterproceduralAnalyzer
|
||||
{
|
||||
public static GuestAbiInterproceduralResult Analyze(
|
||||
IReadOnlyDictionary<uint, IrFunction> functions,
|
||||
IReadOnlyDictionary<uint, GuestAbiContract>? externalContracts = null)
|
||||
{
|
||||
externalContracts ??= new Dictionary<uint, GuestAbiContract>();
|
||||
var addresses = functions.Keys.OrderBy(static address => address).ToArray();
|
||||
|
||||
// GuestAbiContractAnalyzer.Analyze is pure and the round's contract snapshot is frozen, so
|
||||
// per-function analyses within a round can run on any thread. Determinism comes from
|
||||
// applying results serially in ascending-ordinal order, matching the old single-threaded loop.
|
||||
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount };
|
||||
|
||||
// Resolving the IR by ordinal once keeps the caller-supplied dictionary
|
||||
// off every parallel path: only this loop and BuildComponents touch it.
|
||||
var bodies = new IrFunction[addresses.Length];
|
||||
for (var index = 0; index < addresses.Length; ++index) bodies[index] = functions[addresses[index]];
|
||||
|
||||
// Mirror of `contracts` keyed by ordinal. The fixpoint compares against
|
||||
// it from the parallel phase, where the dictionary must stay untouched.
|
||||
var current = new GuestAbiContract[addresses.Length];
|
||||
Parallel.For(
|
||||
0,
|
||||
addresses.Length,
|
||||
parallelOptions,
|
||||
index => current[index] = GuestAbiContractAnalyzer.Analyze(bodies[index]));
|
||||
|
||||
var contracts = new Dictionary<uint, GuestAbiContract>(addresses.Length);
|
||||
for (var index = 0; index < addresses.Length; ++index) contracts.Add(addresses[index], current[index]);
|
||||
|
||||
var ordinals = new Dictionary<uint, int>(addresses.Length);
|
||||
for (var index = 0; index < addresses.Length; ++index) ordinals.Add(addresses[index], index);
|
||||
|
||||
// Callers of each function. A function whose callees all kept their contracts reproduces
|
||||
// the same contract and can be skipped for the round; that's the only liberty taken, since
|
||||
// rounds still see a snapshot fixed at round start, preserving the original fixed point.
|
||||
// The edge scan itself is a read-only IR walk, collected in parallel per ordinal then
|
||||
// stitched serially in ascending caller order to match the single-threaded result.
|
||||
var directCallees = new int[addresses.Length][];
|
||||
Parallel.For(
|
||||
0,
|
||||
addresses.Length,
|
||||
parallelOptions,
|
||||
static () => new HashSet<int>(),
|
||||
(index, _, scratch) =>
|
||||
{
|
||||
scratch.Clear();
|
||||
CollectDirectCallees(bodies[index], ordinals, scratch);
|
||||
directCallees[index] = scratch.ToArray();
|
||||
return scratch;
|
||||
},
|
||||
static _ => { });
|
||||
|
||||
var callers = new List<int>?[addresses.Length];
|
||||
for (var index = 0; index < addresses.Length; ++index)
|
||||
{
|
||||
foreach (var callee in directCallees[index]) (callers[callee] ??= new List<int>()).Add(index);
|
||||
}
|
||||
|
||||
var visible = new Dictionary<uint, GuestAbiContract>(externalContracts);
|
||||
foreach (var (address, contract) in contracts) visible[address] = contract;
|
||||
|
||||
var pending = new bool[addresses.Length];
|
||||
var nextRound = new bool[addresses.Length];
|
||||
Array.Fill(pending, true);
|
||||
// Ascending ordinals of the functions this round re-analyzes, plus the
|
||||
// slot each one writes its result into. Both are reused every round.
|
||||
var worklist = new int[addresses.Length];
|
||||
var roundResults = new GuestAbiContract?[addresses.Length];
|
||||
while (true)
|
||||
{
|
||||
var worklistCount = 0;
|
||||
for (var index = 0; index < addresses.Length; ++index)
|
||||
{
|
||||
if (pending[index]) worklist[worklistCount++] = index;
|
||||
}
|
||||
|
||||
Parallel.For(0, worklistCount, parallelOptions, position =>
|
||||
{
|
||||
var ordinal = worklist[position];
|
||||
var next = GuestAbiContractAnalyzer.Analyze(bodies[ordinal], visible);
|
||||
roundResults[position] = Equivalent(current[ordinal], next) ? null : next;
|
||||
});
|
||||
|
||||
Array.Clear(nextRound);
|
||||
var updated = false;
|
||||
for (var position = 0; position < worklistCount; ++position)
|
||||
{
|
||||
var contract = roundResults[position];
|
||||
if (contract is null) continue;
|
||||
updated = true;
|
||||
var ordinal = worklist[position];
|
||||
var address = addresses[ordinal];
|
||||
current[ordinal] = contract;
|
||||
contracts[address] = contract;
|
||||
visible[address] = contract;
|
||||
var callerList = callers[ordinal];
|
||||
if (callerList is null) continue;
|
||||
foreach (var caller in callerList) nextRound[caller] = true;
|
||||
}
|
||||
|
||||
if (!updated) break;
|
||||
|
||||
(pending, nextRound) = (nextRound, pending);
|
||||
}
|
||||
|
||||
return new GuestAbiInterproceduralResult(contracts, BuildComponents(functions, contracts));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ordinals of functions this one calls directly, from raw IR rather than
|
||||
/// <see cref="GuestAbiContract.DirectCallTargets"/>, which omits inline save/restore thunks
|
||||
/// that the analyzer still consults when they happen to have a contract.
|
||||
/// </summary>
|
||||
private static void CollectDirectCallees(
|
||||
IrFunction function,
|
||||
Dictionary<uint, int> ordinals,
|
||||
HashSet<int> result)
|
||||
{
|
||||
foreach (var block in function.Blocks)
|
||||
{
|
||||
foreach (var instruction in block.Instructions)
|
||||
{
|
||||
if (instruction is IrCall call &&
|
||||
GuestTargetParser.TryParseAddress(call.Target, out var target) &&
|
||||
ordinals.TryGetValue(target, out var ordinal))
|
||||
{
|
||||
result.Add(ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool Equivalent(GuestAbiContract left, GuestAbiContract right) =>
|
||||
left.GprReadBeforeWriteMask == right.GprReadBeforeWriteMask &&
|
||||
left.GprPossibleWriteMask == right.GprPossibleWriteMask &&
|
||||
left.GprReturnMask == right.GprReturnMask &&
|
||||
left.FprReadBeforeWriteMask == right.FprReadBeforeWriteMask &&
|
||||
left.FprPossibleWriteMask == right.FprPossibleWriteMask &&
|
||||
left.FprReturnMask == right.FprReturnMask &&
|
||||
left.CrReadBeforeWriteMask == right.CrReadBeforeWriteMask &&
|
||||
left.CrPossibleWriteMask == right.CrPossibleWriteMask &&
|
||||
left.ReadsXerBeforeWrite == right.ReadsXerBeforeWrite &&
|
||||
left.MayWriteXer == right.MayWriteXer &&
|
||||
left.ReadsCtrBeforeWrite == right.ReadsCtrBeforeWrite &&
|
||||
left.MayWriteCtr == right.MayWriteCtr &&
|
||||
left.ReadsLrBeforeWrite == right.ReadsLrBeforeWrite &&
|
||||
left.MayWriteLr == right.MayWriteLr &&
|
||||
left.ReadsFpscrBeforeWrite == right.ReadsFpscrBeforeWrite &&
|
||||
left.MayWriteFpscr == right.MayWriteFpscr &&
|
||||
left.GqrReadBeforeWriteMask == right.GqrReadBeforeWriteMask &&
|
||||
left.GqrPossibleWriteMask == right.GqrPossibleWriteMask &&
|
||||
left.HidReadBeforeWriteMask == right.HidReadBeforeWriteMask &&
|
||||
left.HidPossibleWriteMask == right.HidPossibleWriteMask &&
|
||||
left.GprDefiniteWriteMask == right.GprDefiniteWriteMask &&
|
||||
left.FprDefiniteWriteMask == right.FprDefiniteWriteMask &&
|
||||
left.CrDefiniteWriteMask == right.CrDefiniteWriteMask &&
|
||||
left.DefinitelyWritesXer == right.DefinitelyWritesXer &&
|
||||
left.DefinitelyWritesCtr == right.DefinitelyWritesCtr &&
|
||||
left.DefinitelyWritesLr == right.DefinitelyWritesLr &&
|
||||
left.DefinitelyWritesFpscr == right.DefinitelyWritesFpscr &&
|
||||
left.GqrDefiniteWriteMask == right.GqrDefiniteWriteMask &&
|
||||
left.HidDefiniteWriteMask == right.HidDefiniteWriteMask &&
|
||||
left.BoundaryFlags == right.BoundaryFlags &&
|
||||
SameTargets(left.DirectCallTargets, right.DirectCallTargets);
|
||||
|
||||
private static bool SameTargets(IReadOnlyList<uint> left, IReadOnlyList<uint> right)
|
||||
{
|
||||
if (ReferenceEquals(left, right)) return true;
|
||||
if (left.Count != right.Count) return false;
|
||||
for (var index = 0; index < left.Count; ++index)
|
||||
{
|
||||
if (left[index] != right[index]) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<IReadOnlyList<uint>> BuildComponents(
|
||||
IReadOnlyDictionary<uint, IrFunction> functions,
|
||||
IReadOnlyDictionary<uint, GuestAbiContract> contracts)
|
||||
{
|
||||
// Iterative Kosaraju, not recursive Tarjan: production titles have call chains deep enough
|
||||
// to exhaust the native .NET stack before a recursive Tarjan discovers a component.
|
||||
var addresses = functions.Keys.OrderBy(static address => address).ToArray();
|
||||
var adjacency = new Dictionary<uint, uint[]>(addresses.Length);
|
||||
var reverse = addresses.ToDictionary(static address => address, static _ => new List<uint>());
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
var targets = contracts[address].DirectCallTargets
|
||||
.Where(functions.ContainsKey)
|
||||
.Distinct()
|
||||
.OrderBy(static target => target)
|
||||
.ToArray();
|
||||
adjacency[address] = targets;
|
||||
foreach (var target in targets) reverse[target].Add(address);
|
||||
}
|
||||
|
||||
var visited = new HashSet<uint>();
|
||||
var finishOrder = new List<uint>(addresses.Length);
|
||||
foreach (var root in addresses)
|
||||
{
|
||||
if (!visited.Add(root)) continue;
|
||||
var traversal = new Stack<(uint Address, int NextTarget)>();
|
||||
traversal.Push((root, 0));
|
||||
while (traversal.Count != 0)
|
||||
{
|
||||
var (address, nextTarget) = traversal.Pop();
|
||||
var targets = adjacency[address];
|
||||
if (nextTarget < targets.Length)
|
||||
{
|
||||
traversal.Push((address, nextTarget + 1));
|
||||
var target = targets[nextTarget];
|
||||
if (visited.Add(target)) traversal.Push((target, 0));
|
||||
continue;
|
||||
}
|
||||
|
||||
finishOrder.Add(address);
|
||||
}
|
||||
}
|
||||
|
||||
visited.Clear();
|
||||
var result = new List<IReadOnlyList<uint>>();
|
||||
for (var orderIndex = finishOrder.Count - 1; orderIndex >= 0; --orderIndex)
|
||||
{
|
||||
var root = finishOrder[orderIndex];
|
||||
if (!visited.Add(root)) continue;
|
||||
var component = new List<uint>();
|
||||
var traversal = new Stack<uint>();
|
||||
traversal.Push(root);
|
||||
while (traversal.Count != 0)
|
||||
{
|
||||
var address = traversal.Pop();
|
||||
component.Add(address);
|
||||
foreach (var caller in reverse[address])
|
||||
if (visited.Add(caller)) traversal.Push(caller);
|
||||
}
|
||||
|
||||
component.Sort();
|
||||
result.Add(component);
|
||||
}
|
||||
|
||||
result.Sort(static (left, right) => left[0].CompareTo(right[0]));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Translator.Core.Analysis.Ssa;
|
||||
using Translator.Core.Ir;
|
||||
|
||||
namespace Translator.Core.Analysis;
|
||||
|
||||
/// <summary>
|
||||
/// The eight CR fields and the XER carry bit, tracked separately from <see cref="GuestStateMask"/> for
|
||||
/// sub-register granularity. Compare and carry read/modify/write helpers touch XER only to preserve
|
||||
/// bits they don't own, so that must not count as a carry read or every carry update would look live.
|
||||
/// </summary>
|
||||
public readonly record struct GuestFlagState(byte CrFields, bool Carry)
|
||||
{
|
||||
public static GuestFlagState None => default;
|
||||
|
||||
/// <summary>Everything live. The conservative answer for any unknown boundary.</summary>
|
||||
public static GuestFlagState All => new(byte.MaxValue, true);
|
||||
|
||||
public GuestFlagState Union(GuestFlagState other) =>
|
||||
new((byte)(CrFields | other.CrFields), Carry || other.Carry);
|
||||
|
||||
public GuestFlagState Except(GuestFlagState other) =>
|
||||
new((byte)(CrFields & ~other.CrFields), Carry && !other.Carry);
|
||||
|
||||
public bool HasCrField(int field) => (CrFields & (1 << field)) != 0;
|
||||
|
||||
public static GuestFlagState Cr(byte fields) => new(fields, false);
|
||||
|
||||
public static GuestFlagState CrField(int field) => new((byte)(1 << field), false);
|
||||
|
||||
public static GuestFlagState CarryOnly => new(0, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What the flag liveness transfer needs at a call site. A target missing from
|
||||
/// <see cref="CalleeContracts"/>, or listed in <see cref="OpaqueCallTargets"/> (a mod may replace
|
||||
/// it, so the published contract may not match what actually runs), is a complete flag boundary.
|
||||
/// </summary>
|
||||
public sealed record GuestFlagLivenessContext(
|
||||
IReadOnlyDictionary<uint, GuestAbiContract> CalleeContracts,
|
||||
IReadOnlySet<uint> OpaqueCallTargets)
|
||||
{
|
||||
public static GuestFlagLivenessContext Empty { get; } = new(
|
||||
new Dictionary<uint, GuestAbiContract>(), new HashSet<uint>());
|
||||
}
|
||||
|
||||
public sealed record GuestFlagLivenessResult(
|
||||
IReadOnlyDictionary<string, GuestFlagState> LiveIn,
|
||||
IReadOnlyDictionary<string, GuestFlagState> LiveOut);
|
||||
|
||||
/// <summary>
|
||||
/// Backward per-CR-field and XER.CA liveness over the emitted IR. Reads are collected aggressively
|
||||
/// while kills are limited to provably total writes (<see cref="IrSetCrField"/>, a full <c>xer</c>
|
||||
/// def), so a call never kills and a callee that may rewrite a flag can't erase a caller-side update.
|
||||
/// </summary>
|
||||
public static class GuestFlagLivenessAnalyzer
|
||||
{
|
||||
/// <summary>
|
||||
/// Helpers whose only carry effect is overwriting XER.CA; the surrounding-bit read to
|
||||
/// preserve them doesn't count as a carry read.
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> CarryProducingHelpers = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"PPC_UpdateCarryAdd", "PPC_UpdateCarrySub", "PPC_UpdateCarryShiftRight"
|
||||
};
|
||||
|
||||
private static readonly Regex GetCrBitRegex = new(
|
||||
@"GetCRBit\(\s*ctx\s*,\s*(?<field>\d+)\s*,\s*(?<bit>\d+)\s*\)",
|
||||
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
|
||||
public static bool IsCarryProducingHelper(string target) => CarryProducingHelpers.Contains(target);
|
||||
|
||||
public static GuestFlagLivenessResult Analyze(
|
||||
IrFunction function,
|
||||
IrCfg cfg,
|
||||
GuestFlagLivenessContext context,
|
||||
GuestFlagState exitLive)
|
||||
{
|
||||
var liveIn = function.Blocks.ToDictionary(
|
||||
block => block.Label, _ => GuestFlagState.None, StringComparer.OrdinalIgnoreCase);
|
||||
var liveOut = function.Blocks.ToDictionary(
|
||||
block => block.Label, _ => GuestFlagState.None, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
bool changed;
|
||||
do
|
||||
{
|
||||
changed = false;
|
||||
for (var blockIndex = function.Blocks.Count - 1; blockIndex >= 0; --blockIndex)
|
||||
{
|
||||
var block = function.Blocks[blockIndex];
|
||||
var successors = cfg.Successors(block.Label);
|
||||
var outgoing = exitLive;
|
||||
if (successors.Count != 0)
|
||||
{
|
||||
outgoing = GuestFlagState.None;
|
||||
for (var successorIndex = 0; successorIndex < successors.Count; ++successorIndex)
|
||||
{
|
||||
outgoing = outgoing.Union(
|
||||
liveIn.TryGetValue(successors[successorIndex], out var successorIn)
|
||||
? successorIn
|
||||
: exitLive);
|
||||
}
|
||||
}
|
||||
|
||||
var incoming = outgoing;
|
||||
for (var index = block.Instructions.Count - 1; index >= 0; --index)
|
||||
{
|
||||
incoming = Transfer(block.Instructions[index], incoming, context);
|
||||
}
|
||||
|
||||
if (outgoing != liveOut[block.Label] || incoming != liveIn[block.Label])
|
||||
{
|
||||
liveOut[block.Label] = outgoing;
|
||||
liveIn[block.Label] = incoming;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
} while (changed);
|
||||
|
||||
return new GuestFlagLivenessResult(liveIn, liveOut);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Backward transfer for one instruction: <c>(live \ kills) ∪ reads</c>.
|
||||
/// </summary>
|
||||
public static GuestFlagState Transfer(
|
||||
IrInstruction instruction,
|
||||
GuestFlagState live,
|
||||
GuestFlagLivenessContext context) =>
|
||||
live.Except(Kills(instruction)).Union(Reads(instruction, context));
|
||||
|
||||
public static GuestFlagState Kills(IrInstruction instruction)
|
||||
{
|
||||
if (instruction is IrSetCrField setCr)
|
||||
{
|
||||
return GuestFlagState.CrField(setCr.FieldIndex & 7);
|
||||
}
|
||||
|
||||
// A definition of the whole XER register replaces the carry bit. That
|
||||
// covers mtxer/mtspr as well as the carry read/modify/write helpers,
|
||||
// whose IR destination is xer.
|
||||
foreach (var definition in IrRegisterDataFlow.Definitions(instruction))
|
||||
{
|
||||
if (RegisterNameUtils.HardwareBase(definition.AsSpan()).Equals("xer", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GuestFlagState.CarryOnly;
|
||||
}
|
||||
}
|
||||
|
||||
return GuestFlagState.None;
|
||||
}
|
||||
|
||||
public static GuestFlagState Reads(IrInstruction instruction, GuestFlagLivenessContext context)
|
||||
{
|
||||
switch (instruction)
|
||||
{
|
||||
case IrSetCrField:
|
||||
// Reads XER.SO only, and no CR field: the compare overwrites the
|
||||
// complete field from its two operands.
|
||||
return GuestFlagState.None;
|
||||
|
||||
case IrBranch branch:
|
||||
return GuestFlagState.Cr(BranchCrFields(branch));
|
||||
|
||||
case IrIndirectCall:
|
||||
case IrIndirectJump:
|
||||
case IrUndefined:
|
||||
return GuestFlagState.All;
|
||||
|
||||
case IrCall call:
|
||||
return CallReads(call, context);
|
||||
|
||||
default:
|
||||
{
|
||||
var result = GuestFlagState.None;
|
||||
foreach (var use in IrRegisterDataFlow.Uses(instruction))
|
||||
{
|
||||
result = result.Union(RegisterFlags(use));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static GuestFlagState CallReads(IrCall call, GuestFlagLivenessContext context)
|
||||
{
|
||||
if (GuestTargetParser.TryParseAddress(call.Target, out var target))
|
||||
{
|
||||
if (context.OpaqueCallTargets.Contains(target) ||
|
||||
!context.CalleeContracts.TryGetValue(target, out var contract))
|
||||
{
|
||||
return GuestFlagState.All;
|
||||
}
|
||||
|
||||
return contract.HasFullSynchronizationFence
|
||||
? GuestFlagState.All
|
||||
: new GuestFlagState(contract.CrReadBeforeWriteMask, contract.ReadsXerBeforeWrite);
|
||||
}
|
||||
|
||||
// Non-address targets are either runtime helpers with a cataloged
|
||||
// effect, or guest symbols the emitter calls directly. The catalog
|
||||
// classifies anything it does not recognize as a complete boundary.
|
||||
var effect = GuestHelperEffectCatalog.Analyze(call);
|
||||
var readsCarry = effect.ReadsXer && !CarryProducingHelpers.Contains(call.Target);
|
||||
var result = new GuestFlagState(effect.CrReadMask, readsCarry);
|
||||
foreach (var argument in call.Arguments)
|
||||
{
|
||||
if (argument.Kind == "register" && argument.RegisterName is { } name)
|
||||
{
|
||||
result = result.Union(RegisterFlags(name));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CR fields a branch condition observes. The lifter emits general <c>bc</c>/<c>bcctr</c> forms
|
||||
/// as raw C++ text rather than a register operand, so the text must be inspected; anything that
|
||||
/// can't be parsed conservatively reports every field.
|
||||
/// </summary>
|
||||
public static byte BranchCrFields(IrBranch branch)
|
||||
{
|
||||
var conditionRegister = branch.ConditionRegister;
|
||||
if (string.IsNullOrEmpty(conditionRegister))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (conditionRegister.Contains("GetCRBit", StringComparison.Ordinal) ||
|
||||
conditionRegister.Contains("ctx->cr", StringComparison.Ordinal) ||
|
||||
conditionRegister.Contains("cached_cr", StringComparison.Ordinal))
|
||||
{
|
||||
if (conditionRegister.Contains("ctx->cr", StringComparison.Ordinal) ||
|
||||
conditionRegister.Contains("cached_cr", StringComparison.Ordinal))
|
||||
{
|
||||
return byte.MaxValue;
|
||||
}
|
||||
|
||||
byte mask = 0;
|
||||
var matches = GetCrBitRegex.Matches(conditionRegister);
|
||||
if (matches.Count != CountOccurrences(conditionRegister, "GetCRBit"))
|
||||
{
|
||||
return byte.MaxValue;
|
||||
}
|
||||
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
mask |= (byte)(1 << (int.Parse(match.Groups["field"].Value) & 7));
|
||||
}
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
return RegisterFlags(conditionRegister).CrFields;
|
||||
}
|
||||
|
||||
private static int CountOccurrences(string text, string value)
|
||||
{
|
||||
var count = 0;
|
||||
var index = text.IndexOf(value, StringComparison.Ordinal);
|
||||
while (index >= 0)
|
||||
{
|
||||
++count;
|
||||
index = text.IndexOf(value, index + value.Length, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private static GuestFlagState RegisterFlags(string? name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return GuestFlagState.None;
|
||||
}
|
||||
|
||||
var baseName = RegisterNameUtils.HardwareBase(name.AsSpan());
|
||||
if (baseName.Length == 3 && baseName.StartsWith("cr", StringComparison.OrdinalIgnoreCase) &&
|
||||
baseName[2] is >= '0' and <= '7')
|
||||
{
|
||||
return GuestFlagState.CrField(baseName[2] - '0');
|
||||
}
|
||||
|
||||
if (baseName.Length is >= 4 and <= 5 && baseName.StartsWith("crb", StringComparison.OrdinalIgnoreCase) &&
|
||||
int.TryParse(baseName[3..], out var crBit) && crBit is >= 0 and < 32)
|
||||
{
|
||||
return GuestFlagState.CrField(crBit / 4);
|
||||
}
|
||||
|
||||
if (baseName.Equals("cr", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GuestFlagState.Cr(byte.MaxValue);
|
||||
}
|
||||
|
||||
if (baseName.Equals("xer", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GuestFlagState.CarryOnly;
|
||||
}
|
||||
|
||||
return GuestFlagState.None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using Translator.Core.Ir;
|
||||
|
||||
namespace Translator.Core.Analysis;
|
||||
|
||||
public sealed record GuestHelperEffect(
|
||||
uint GprReadMask = 0,
|
||||
uint GprWriteMask = 0,
|
||||
uint FprReadMask = 0,
|
||||
uint FprWriteMask = 0,
|
||||
byte CrReadMask = 0,
|
||||
byte CrWriteMask = 0,
|
||||
bool ReadsXer = false,
|
||||
bool WritesXer = false,
|
||||
bool ReadsCtr = false,
|
||||
bool WritesCtr = false,
|
||||
bool ReadsLr = false,
|
||||
bool WritesLr = false,
|
||||
GuestCallBoundaryFlags BoundaryFlags = GuestCallBoundaryFlags.None);
|
||||
|
||||
/// <summary>
|
||||
/// Architectural effects hidden behind non-guest helper calls, reached implicitly through CpuContext
|
||||
/// (explicit IrCall args/destinations are handled by the ordinary IR analyzer). Unknown helpers default
|
||||
/// to complete-context boundaries so a new runtime helper can't silently invalidate resident ownership.
|
||||
/// </summary>
|
||||
public static class GuestHelperEffectCatalog
|
||||
{
|
||||
// The effects below are constants, so one shared instance per distinct
|
||||
// effect is handed out instead of allocating a record per call site.
|
||||
// GuestHelperEffect is an immutable record: every member is init-only.
|
||||
private static readonly GuestHelperEffect NoEffect = new();
|
||||
private static readonly GuestHelperEffect CarryUpdate = new(ReadsXer: true, WritesXer: true);
|
||||
private static readonly GuestHelperEffect CarryRead = new(ReadsXer: true);
|
||||
private static readonly GuestHelperEffect UnboundedStringLoad = new(GprWriteMask: uint.MaxValue, ReadsXer: true);
|
||||
private static readonly GuestHelperEffect UnboundedStringStore = new(GprReadMask: uint.MaxValue, ReadsXer: true);
|
||||
private static readonly GuestHelperEffect ConditionalStore =
|
||||
new(CrReadMask: byte.MaxValue, CrWriteMask: 1, ReadsXer: true);
|
||||
private static readonly GuestHelperEffect CompleteBoundary = CreateComplete(GuestCallBoundaryFlags.None);
|
||||
private static readonly GuestHelperEffect SystemCallBoundary = CreateComplete(
|
||||
GuestCallBoundaryFlags.CanSuspend | GuestCallBoundaryFlags.CanSwitchThreads |
|
||||
GuestCallBoundaryFlags.InvokesGuestCode);
|
||||
|
||||
/// <summary>Helpers whose effect is derived from a constant argument.</summary>
|
||||
private enum ArgumentDependentHelper
|
||||
{
|
||||
LoadStringImmediate,
|
||||
StoreStringImmediate,
|
||||
MoveConditionFromXer,
|
||||
MoveConditionField,
|
||||
MoveConditionFromFpscr,
|
||||
SetConditionBit,
|
||||
ConditionLogical,
|
||||
CompareIntoConditionField,
|
||||
ReadSpr,
|
||||
WriteSpr
|
||||
}
|
||||
|
||||
private static readonly FrozenDictionary<string, ArgumentDependentHelper> ArgumentDependentHelpers =
|
||||
new Dictionary<string, ArgumentDependentHelper>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["PPC_Lswi"] = ArgumentDependentHelper.LoadStringImmediate,
|
||||
["PPC_Stswi"] = ArgumentDependentHelper.StoreStringImmediate,
|
||||
["PPC_Mcrxr"] = ArgumentDependentHelper.MoveConditionFromXer,
|
||||
["PPC_Mcrf"] = ArgumentDependentHelper.MoveConditionField,
|
||||
["PPC_Mcrfs"] = ArgumentDependentHelper.MoveConditionFromFpscr,
|
||||
["PPC_CrSetBit"] = ArgumentDependentHelper.SetConditionBit,
|
||||
["PPC_CrLogical"] = ArgumentDependentHelper.ConditionLogical,
|
||||
["PPC_Fcmp"] = ArgumentDependentHelper.CompareIntoConditionField,
|
||||
["PPC_PsCmpo0"] = ArgumentDependentHelper.CompareIntoConditionField,
|
||||
["PPC_PsCmpo1"] = ArgumentDependentHelper.CompareIntoConditionField,
|
||||
["PPC_PsCmpu0"] = ArgumentDependentHelper.CompareIntoConditionField,
|
||||
["PPC_PsCmpu1"] = ArgumentDependentHelper.CompareIntoConditionField,
|
||||
["PPC_ReadSpr"] = ArgumentDependentHelper.ReadSpr,
|
||||
["PPC_WriteSpr"] = ArgumentDependentHelper.WriteSpr
|
||||
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static readonly string[] PureHelpers =
|
||||
{
|
||||
"memset_zero_32", "PPC_Cntlzw", "PPC_Eciwx", "PPC_Ecowx", "PPC_Lwarx",
|
||||
"PPC_LoadHalfwordByteReverse", "PPC_LoadWordByteReverse",
|
||||
"PPC_StoreHalfwordByteReverse", "PPC_StoreWordByteReverse", "PPC_Stfiwx",
|
||||
"PPC_Fadds", "PPC_Fdivs", "PPC_Fmadd", "PPC_Fmadds", "PPC_Fmsub", "PPC_Fmsubs",
|
||||
"PPC_Fmuls", "PPC_Fnmadd", "PPC_Fnmadds", "PPC_Fnmsub", "PPC_Fnmsubs",
|
||||
"PPC_Fres", "PPC_Frsqrte", "PPC_Fsel", "PPC_Fsqrt", "PPC_Fsubs",
|
||||
"PPC_PsAbs", "PPC_PsAdd", "PPC_PsDiv", "PPC_PsMadd", "PPC_PsMadds0",
|
||||
"PPC_PsMadds1", "PPC_PsMerge00", "PPC_PsMerge01", "PPC_PsMerge10",
|
||||
"PPC_PsMerge11", "PPC_PsMsub", "PPC_PsMul", "PPC_PsMuls0", "PPC_PsMuls1",
|
||||
"PPC_PsNabs", "PPC_PsNeg", "PPC_PsNmadd", "PPC_PsNmsub", "PPC_PsRes",
|
||||
"PPC_PsRsqrte", "PPC_PsSel", "PPC_PsSub", "PPC_PsSum0", "PPC_PsSum1",
|
||||
"PPC_Mftb", "PPC_Mftbu", "PPC_TrapWord"
|
||||
};
|
||||
|
||||
private static readonly string[] XerReadWriteHelpers =
|
||||
{
|
||||
"PPC_UpdateCarryAdd", "PPC_UpdateCarrySub", "PPC_UpdateCarryShiftRight",
|
||||
"PPC_Addco", "PPC_Addeo", "PPC_Addmeo", "PPC_Addo", "PPC_Addzeo",
|
||||
"PPC_Subfco", "PPC_Subfeo", "PPC_Subfmeo", "PPC_Subfo", "PPC_Subfzeo",
|
||||
"PPC_Divwo", "PPC_Divwuo", "PPC_Mullwo", "PPC_Nego"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Helpers that implicitly touch FPSCR or GQR state, which stays authoritative in CpuContext
|
||||
/// so no GPR sync is needed; kept as explicit entries so they don't default to unknown
|
||||
/// full-context fences.
|
||||
/// </summary>
|
||||
private static readonly string[] FpscrAndGqrHelpers =
|
||||
{
|
||||
"PPC_Mffs", "PPC_Mtfsb0", "PPC_Mtfsb1", "PPC_Mtfsf", "PPC_Mtfsfi", "PPC_PsqL", "PPC_PsqSt"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Helpers whose effect doesn't depend on arguments, as one hash lookup instead of the linear
|
||||
/// case-insensitive chain this used to walk per call site. Declared after the name tables above
|
||||
/// since static field initializers run in textual order and this one reads them.
|
||||
/// </summary>
|
||||
private static readonly FrozenDictionary<string, GuestHelperEffect> ConstantEffectHelpers =
|
||||
BuildConstantEffectHelpers();
|
||||
|
||||
private static FrozenDictionary<string, GuestHelperEffect> BuildConstantEffectHelpers()
|
||||
{
|
||||
var entries = new Dictionary<string, GuestHelperEffect>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var helper in PureHelpers) entries.Add(helper, NoEffect);
|
||||
foreach (var helper in XerReadWriteHelpers) entries.Add(helper, CarryUpdate);
|
||||
foreach (var helper in FpscrAndGqrHelpers) entries.Add(helper, NoEffect);
|
||||
entries.Add("PPC_GetCarry", CarryRead);
|
||||
entries.Add("OSSystemCall", SystemCallBoundary);
|
||||
entries.Add("PPC_Lswx", UnboundedStringLoad);
|
||||
entries.Add("PPC_Stswx", UnboundedStringStore);
|
||||
entries.Add("PPC_Stwcx", ConditionalStore);
|
||||
return entries.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static GuestHelperEffect Analyze(IrCall call)
|
||||
{
|
||||
// The prefixed families are matched first, exactly as before: a name is
|
||||
// only looked up once it is known not to be a known-GQR paired form.
|
||||
if (call.Target.StartsWith("PPC_PsqLKnown_", StringComparison.OrdinalIgnoreCase) ||
|
||||
call.Target.StartsWith("PPC_PsqLKnownGuarded_", StringComparison.OrdinalIgnoreCase) ||
|
||||
call.Target.StartsWith("PPC_PsqStKnown_", StringComparison.OrdinalIgnoreCase) ||
|
||||
call.Target.StartsWith("PPC_PsqStKnownGuarded_", StringComparison.OrdinalIgnoreCase))
|
||||
return NoEffect;
|
||||
|
||||
if (ConstantEffectHelpers.TryGetValue(call.Target, out var constantEffect)) return constantEffect;
|
||||
|
||||
if (ArgumentDependentHelpers.TryGetValue(call.Target, out var helper))
|
||||
{
|
||||
return helper switch
|
||||
{
|
||||
ArgumentDependentHelper.LoadStringImmediate =>
|
||||
new GuestHelperEffect(GprWriteMask: StringRegisterMask(call, countArgument: 2)),
|
||||
ArgumentDependentHelper.StoreStringImmediate =>
|
||||
new GuestHelperEffect(GprReadMask: StringRegisterMask(call, countArgument: 2)),
|
||||
ArgumentDependentHelper.MoveConditionFromXer =>
|
||||
new GuestHelperEffect(
|
||||
CrReadMask: byte.MaxValue, CrWriteMask: FieldMask(call, 0),
|
||||
ReadsXer: true, WritesXer: true),
|
||||
ArgumentDependentHelper.MoveConditionField =>
|
||||
new GuestHelperEffect(CrReadMask: byte.MaxValue, CrWriteMask: FieldMask(call, 0)),
|
||||
ArgumentDependentHelper.MoveConditionFromFpscr =>
|
||||
new GuestHelperEffect(CrReadMask: byte.MaxValue, CrWriteMask: FieldMask(call, 0)),
|
||||
ArgumentDependentHelper.SetConditionBit =>
|
||||
new GuestHelperEffect(CrReadMask: byte.MaxValue, CrWriteMask: BitFieldMask(call, 0)),
|
||||
ArgumentDependentHelper.ConditionLogical =>
|
||||
new GuestHelperEffect(
|
||||
// Updates one field but preserves the other seven via a packed-CR
|
||||
// read/modify/write, so resident CR ownership needs the full packed value first.
|
||||
CrReadMask: byte.MaxValue,
|
||||
CrWriteMask: BitFieldMask(call, 1)),
|
||||
ArgumentDependentHelper.CompareIntoConditionField =>
|
||||
new GuestHelperEffect(CrReadMask: byte.MaxValue, CrWriteMask: FieldMask(call, 0)),
|
||||
ArgumentDependentHelper.ReadSpr => SprEffect(call, write: false),
|
||||
ArgumentDependentHelper.WriteSpr => SprEffect(call, write: true),
|
||||
_ => Complete()
|
||||
};
|
||||
}
|
||||
|
||||
return Complete();
|
||||
}
|
||||
|
||||
private static GuestHelperEffect SprEffect(IrCall call, bool write)
|
||||
{
|
||||
if (call.Arguments.Count == 0 || call.Arguments[0].Constant is not long spr)
|
||||
return Complete();
|
||||
return spr switch
|
||||
{
|
||||
1 => new GuestHelperEffect(ReadsXer: !write, WritesXer: write),
|
||||
8 => new GuestHelperEffect(ReadsLr: !write, WritesLr: write),
|
||||
9 => new GuestHelperEffect(ReadsCtr: !write, WritesCtr: write),
|
||||
_ => NoEffect
|
||||
};
|
||||
}
|
||||
|
||||
private static uint StringRegisterMask(IrCall call, int countArgument)
|
||||
{
|
||||
if (call.Arguments.Count <= countArgument || call.Arguments[0].Constant is not long startValue ||
|
||||
call.Arguments[countArgument].Constant is not long countValue)
|
||||
return uint.MaxValue;
|
||||
var start = (int)(startValue & 31);
|
||||
var count = (int)(countValue == 0 ? 32 : countValue);
|
||||
var registers = Math.Min(32, (count + 3) / 4);
|
||||
uint mask = 0;
|
||||
for (var index = 0; index < registers; ++index) mask |= 1u << ((start + index) & 31);
|
||||
return mask;
|
||||
}
|
||||
|
||||
private static byte FieldMask(IrCall call, int argument) =>
|
||||
call.Arguments.Count > argument && call.Arguments[argument].Constant is long field
|
||||
? (byte)(1 << ((int)field & 7))
|
||||
: byte.MaxValue;
|
||||
|
||||
private static byte BitFieldMask(IrCall call, int argument) =>
|
||||
call.Arguments.Count > argument && call.Arguments[argument].Constant is long bit
|
||||
? (byte)(1 << (((int)bit & 31) / 4))
|
||||
: byte.MaxValue;
|
||||
|
||||
private static GuestHelperEffect Complete(GuestCallBoundaryFlags extra = GuestCallBoundaryFlags.None) =>
|
||||
extra == GuestCallBoundaryFlags.None ? CompleteBoundary : CreateComplete(extra);
|
||||
|
||||
private static GuestHelperEffect CreateComplete(GuestCallBoundaryFlags extra) =>
|
||||
new(uint.MaxValue, uint.MaxValue, uint.MaxValue, uint.MaxValue, byte.MaxValue, byte.MaxValue,
|
||||
true, true, true, true, true, true, extra | GuestCallBoundaryFlags.RequiresCompleteContext);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Translator.Core.Loading;
|
||||
|
||||
namespace Translator.Core.Analysis;
|
||||
|
||||
/// <summary>
|
||||
/// One CodeWarrior save/restore thunk family: a run of one-instruction entry
|
||||
/// points, four bytes apart, where entry <c>k</c> saves or restores registers
|
||||
/// <c>FirstRegister + k</c> upwards.
|
||||
/// </summary>
|
||||
public sealed record GuestSaveRestoreThunkRange(uint BaseAddress, int FirstRegister, int LastRegister);
|
||||
|
||||
/// <summary>
|
||||
/// The four CodeWarrior save/restore thunk ranges, derived once from the function map's symbol names
|
||||
/// (<c>_save_gpr_14</c>, <c>_rest_gpr_14</c>, <c>_save_fpr_23</c>, <c>_rest_fpr_23</c> ...) and published
|
||||
/// here instead of threaded as a parameter through the three passes that recognize these thunks.
|
||||
/// </summary>
|
||||
public sealed class GuestSaveRestoreThunks
|
||||
{
|
||||
public static GuestSaveRestoreThunks None { get; } = new(null, null, null, null);
|
||||
|
||||
/// <summary>
|
||||
/// Ranges in effect for this process. Set once from the loaded project;
|
||||
/// <see cref="None"/> until then, which makes every thunk query answer "no",
|
||||
/// exactly as it does for a project that configures no function map.
|
||||
/// </summary>
|
||||
public static GuestSaveRestoreThunks Current { get; set; } = None;
|
||||
|
||||
private GuestSaveRestoreThunks(
|
||||
GuestSaveRestoreThunkRange? saveGpr,
|
||||
GuestSaveRestoreThunkRange? restGpr,
|
||||
GuestSaveRestoreThunkRange? saveFpr,
|
||||
GuestSaveRestoreThunkRange? restFpr)
|
||||
{
|
||||
SaveGpr = saveGpr;
|
||||
RestGpr = restGpr;
|
||||
SaveFpr = saveFpr;
|
||||
RestFpr = restFpr;
|
||||
}
|
||||
|
||||
public GuestSaveRestoreThunkRange? SaveGpr { get; }
|
||||
public GuestSaveRestoreThunkRange? RestGpr { get; }
|
||||
public GuestSaveRestoreThunkRange? SaveFpr { get; }
|
||||
public GuestSaveRestoreThunkRange? RestFpr { get; }
|
||||
|
||||
public bool IsEmpty => SaveGpr is null && RestGpr is null && SaveFpr is null && RestFpr is null;
|
||||
|
||||
public static GuestSaveRestoreThunks FromFunctionMap(FunctionMap map) =>
|
||||
new(
|
||||
ResolveRange(map, "_save_gpr_"),
|
||||
ResolveRange(map, "_rest_gpr_"),
|
||||
ResolveRange(map, "_save_fpr_"),
|
||||
ResolveRange(map, "_rest_fpr_"));
|
||||
|
||||
/// <summary>
|
||||
/// Recovers (base, first, last register) from the family's named entries, validating that the
|
||||
/// run is exactly four bytes per register step since only the family's extremes are guaranteed named.
|
||||
/// </summary>
|
||||
private static GuestSaveRestoreThunkRange? ResolveRange(FunctionMap map, string prefix)
|
||||
{
|
||||
var family = map.NumberedFamily(prefix);
|
||||
if (family.Count < 2) return null;
|
||||
|
||||
var first = family.Keys.Min();
|
||||
var last = family.Keys.Max();
|
||||
var baseAddress = family[first];
|
||||
var expectedEnd = baseAddress + checked((uint)((last - first) * 4));
|
||||
if (family[last] != expectedEnd)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Function map {map.SourcePath} describes '{prefix}' thunks at 0x{baseAddress:X8}..0x{family[last]:X8} " +
|
||||
$"for registers {first}..{last}, which is not a four-byte-per-register run.");
|
||||
}
|
||||
|
||||
foreach (var (register, address) in family)
|
||||
{
|
||||
var expected = baseAddress + checked((uint)((register - first) * 4));
|
||||
if (address != expected)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Function map {map.SourcePath} places '{prefix}{register}' at 0x{address:X8}; " +
|
||||
$"the surrounding run requires 0x{expected:X8}.");
|
||||
}
|
||||
}
|
||||
|
||||
return new GuestSaveRestoreThunkRange(baseAddress, first, last);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Translator.Core.Ir;
|
||||
|
||||
namespace Translator.Core.Analysis;
|
||||
|
||||
/// <summary>
|
||||
/// Architectural state that can remain as ordinary SSA values between known translated call
|
||||
/// sites. Memory is deliberately absent, it is an independently observable side effect, not part of the register file.
|
||||
/// </summary>
|
||||
public readonly record struct GuestStateMask(
|
||||
uint Gpr,
|
||||
uint Fpr,
|
||||
byte Cr,
|
||||
bool Xer,
|
||||
bool Ctr,
|
||||
bool Lr,
|
||||
bool Fpscr,
|
||||
byte Gqr,
|
||||
byte Hid)
|
||||
{
|
||||
public static GuestStateMask Empty => default;
|
||||
|
||||
public bool IsEmpty =>
|
||||
Gpr == 0 && Fpr == 0 && Cr == 0 && !Xer && !Ctr && !Lr && !Fpscr && Gqr == 0 && Hid == 0;
|
||||
|
||||
public GuestStateMask Union(GuestStateMask other) =>
|
||||
new(Gpr | other.Gpr, Fpr | other.Fpr, (byte)(Cr | other.Cr),
|
||||
Xer || other.Xer, Ctr || other.Ctr, Lr || other.Lr,
|
||||
Fpscr || other.Fpscr, (byte)(Gqr | other.Gqr), (byte)(Hid | other.Hid));
|
||||
|
||||
public GuestStateMask Intersect(GuestStateMask other) =>
|
||||
new(Gpr & other.Gpr, Fpr & other.Fpr, (byte)(Cr & other.Cr),
|
||||
Xer && other.Xer, Ctr && other.Ctr, Lr && other.Lr,
|
||||
Fpscr && other.Fpscr, (byte)(Gqr & other.Gqr), (byte)(Hid & other.Hid));
|
||||
|
||||
public GuestStateMask Except(GuestStateMask other) =>
|
||||
new(Gpr & ~other.Gpr, Fpr & ~other.Fpr, (byte)(Cr & ~other.Cr),
|
||||
Xer && !other.Xer, Ctr && !other.Ctr, Lr && !other.Lr,
|
||||
Fpscr && !other.Fpscr, (byte)(Gqr & ~other.Gqr), (byte)(Hid & ~other.Hid));
|
||||
|
||||
public static GuestStateMask FromContractReads(GuestAbiContract contract) =>
|
||||
new(contract.GprReadBeforeWriteMask, contract.FprReadBeforeWriteMask,
|
||||
contract.CrReadBeforeWriteMask, contract.ReadsXerBeforeWrite,
|
||||
contract.ReadsCtrBeforeWrite, contract.ReadsLrBeforeWrite,
|
||||
contract.ReadsFpscrBeforeWrite, contract.GqrReadBeforeWriteMask,
|
||||
contract.HidReadBeforeWriteMask);
|
||||
|
||||
public static GuestStateMask FromContractWrites(GuestAbiContract contract) =>
|
||||
new(contract.GprPossibleWriteMask, contract.FprPossibleWriteMask,
|
||||
contract.CrPossibleWriteMask, contract.MayWriteXer,
|
||||
contract.MayWriteCtr, contract.MayWriteLr,
|
||||
contract.MayWriteFpscr, contract.GqrPossibleWriteMask,
|
||||
contract.HidPossibleWriteMask);
|
||||
|
||||
public static GuestStateMask FromContractDefiniteWrites(GuestAbiContract contract) =>
|
||||
new(contract.GprDefiniteWriteMask, contract.FprDefiniteWriteMask,
|
||||
contract.CrDefiniteWriteMask, contract.DefinitelyWritesXer,
|
||||
contract.DefinitelyWritesCtr, contract.DefinitelyWritesLr,
|
||||
contract.DefinitelyWritesFpscr, contract.GqrDefiniteWriteMask,
|
||||
contract.HidDefiniteWriteMask);
|
||||
}
|
||||
|
||||
public sealed record GuestCallSiteStateContract(
|
||||
string BlockLabel,
|
||||
int InstructionIndex,
|
||||
int CallOrdinal,
|
||||
uint Target,
|
||||
GuestStateMask Inputs,
|
||||
GuestStateMask Outputs,
|
||||
GuestStateMask LiveAfter);
|
||||
|
||||
/// <summary>
|
||||
/// Stable identity for a direct call across codegen rewrites that remove unrelated instructions.
|
||||
/// The ordinal counts same-target calls in a block, so it survives rewrites that InstructionIndex does not.
|
||||
/// </summary>
|
||||
public readonly record struct GuestStateFreeCallSiteKey(
|
||||
string BlockLabel,
|
||||
uint Target,
|
||||
int CallOrdinal);
|
||||
|
||||
/// <summary>
|
||||
/// A compact native entry specialized for the state consumed after one or more
|
||||
/// equivalent direct call sites.
|
||||
/// </summary>
|
||||
public sealed record GuestStateFreeCallVariant(
|
||||
uint Target,
|
||||
string Symbol,
|
||||
GuestAbiContract Contract);
|
||||
|
||||
public sealed record GuestStateLivenessResult(
|
||||
IReadOnlyDictionary<string, GuestStateMask> BlockLiveIn,
|
||||
IReadOnlyDictionary<string, GuestStateMask> BlockLiveOut,
|
||||
IReadOnlyList<GuestCallSiteStateContract> DirectCalls);
|
||||
|
||||
/// <summary>
|
||||
/// Backward architectural-state liveness. A call kills the values it writes, introduces only its
|
||||
/// proven inputs, and reports only values live after that exact call site.
|
||||
/// </summary>
|
||||
public static class GuestStateLivenessAnalyzer
|
||||
{
|
||||
/// <summary>
|
||||
/// A public translated-function boundary materializes CpuContext, so every value the function
|
||||
/// may change is observable there. Guest ABI return masks describe source-language return
|
||||
/// values only, not a license to discard other instruction-level side effects.
|
||||
/// </summary>
|
||||
public static GuestStateMask MaterializedContextExit(GuestAbiContract contract) =>
|
||||
GuestStateMask.FromContractWrites(contract);
|
||||
|
||||
public static uint RequiredStateFreeGprInputs(GuestAbiContract contract, uint demandedOutputMask)
|
||||
{
|
||||
var outputs = contract.GprPossibleWriteMask & demandedOutputMask;
|
||||
return contract.GprReadBeforeWriteMask | (outputs & ~contract.GprDefiniteWriteMask);
|
||||
}
|
||||
|
||||
public static bool CanDeconstructWithoutContext(IrFunction function)
|
||||
{
|
||||
foreach (var instruction in function.Blocks.SelectMany(static block => block.Instructions))
|
||||
{
|
||||
// Unknown control-flow targets have no static call-site contract,
|
||||
// so the dispatcher must observe a materialized CpuContext.
|
||||
if (instruction is IrIndirectCall or IrIndirectJump or IrJumpTable or IrUndefined)
|
||||
return false;
|
||||
|
||||
foreach (var name in IrRegisterDataFlow.Uses(instruction))
|
||||
{
|
||||
if (IsSupervisorRegister(name)) return false;
|
||||
}
|
||||
|
||||
foreach (var name in IrRegisterDataFlow.Definitions(instruction))
|
||||
{
|
||||
if (IsSupervisorRegister(name)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Supervisor-level state the state-free calling convention cannot carry.
|
||||
/// </summary>
|
||||
private static bool IsSupervisorRegister(string name)
|
||||
{
|
||||
var baseName = RegisterNameUtils.HardwareBase(name.AsSpan());
|
||||
return baseName.Equals("msr", StringComparison.Ordinal) ||
|
||||
baseName.Equals("dar", StringComparison.Ordinal) ||
|
||||
baseName.Equals("dsisr", StringComparison.Ordinal) ||
|
||||
baseName.Equals("iccr", StringComparison.Ordinal) ||
|
||||
baseName.Equals("tbr", StringComparison.Ordinal) ||
|
||||
baseName.Equals("tbl", StringComparison.Ordinal) ||
|
||||
baseName.Equals("tbu", StringComparison.Ordinal) ||
|
||||
baseName.Equals("srr0", StringComparison.Ordinal) ||
|
||||
baseName.Equals("srr1", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public static GuestStateLivenessResult Analyze(
|
||||
IrFunction function,
|
||||
IReadOnlyDictionary<uint, GuestAbiContract> calleeContracts,
|
||||
GuestStateMask exitLive)
|
||||
{
|
||||
var blockList = function.Blocks;
|
||||
var blockCount = blockList.Count;
|
||||
var blocks = blockList.ToDictionary(block => block.Label, StringComparer.Ordinal);
|
||||
// Successor labels are resolved once into per-block arrays; the fixpoint
|
||||
// below re-reads them on every round and cannot afford the projection.
|
||||
var successors = new string[blockCount][];
|
||||
for (var index = 0; index < blockCount; ++index)
|
||||
{
|
||||
var successorCount = Successors(
|
||||
blockList[index],
|
||||
index + 1 < blockCount ? blockList[index + 1].Label : null,
|
||||
out var first,
|
||||
out var second);
|
||||
var keepFirst = successorCount >= 1 && blocks.ContainsKey(first);
|
||||
var keepSecond = successorCount == 2 && blocks.ContainsKey(second);
|
||||
successors[index] =
|
||||
keepFirst && keepSecond ? new[] { first, second }
|
||||
: keepFirst ? new[] { first }
|
||||
: keepSecond ? new[] { second }
|
||||
: Array.Empty<string>();
|
||||
}
|
||||
|
||||
var liveIn = blockList.ToDictionary(block => block.Label, _ => GuestStateMask.Empty, StringComparer.Ordinal);
|
||||
var liveOut = blockList.ToDictionary(block => block.Label, _ => GuestStateMask.Empty, StringComparer.Ordinal);
|
||||
|
||||
bool changed;
|
||||
do
|
||||
{
|
||||
changed = false;
|
||||
for (var blockIndex = blockCount - 1; blockIndex >= 0; --blockIndex)
|
||||
{
|
||||
var block = blockList[blockIndex];
|
||||
var blockSuccessors = successors[blockIndex];
|
||||
var outgoing = exitLive;
|
||||
if (blockSuccessors.Length != 0)
|
||||
{
|
||||
outgoing = GuestStateMask.Empty;
|
||||
foreach (var label in blockSuccessors) outgoing = outgoing.Union(liveIn[label]);
|
||||
}
|
||||
|
||||
var incoming = TransferBlock(block, outgoing, calleeContracts, calls: null);
|
||||
if (outgoing != liveOut[block.Label] || incoming != liveIn[block.Label])
|
||||
{
|
||||
liveOut[block.Label] = outgoing;
|
||||
liveIn[block.Label] = incoming;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
} while (changed);
|
||||
|
||||
var calls = new List<GuestCallSiteStateContract>();
|
||||
foreach (var block in function.Blocks)
|
||||
TransferBlock(block, liveOut[block.Label], calleeContracts, calls);
|
||||
calls.Sort(static (left, right) =>
|
||||
{
|
||||
var block = string.CompareOrdinal(left.BlockLabel, right.BlockLabel);
|
||||
return block != 0 ? block : left.InstructionIndex.CompareTo(right.InstructionIndex);
|
||||
});
|
||||
var ordinals = new Dictionary<(string Block, uint Target), int>();
|
||||
for (var index = 0; index < calls.Count; ++index)
|
||||
{
|
||||
var call = calls[index];
|
||||
var key = (call.BlockLabel, call.Target);
|
||||
var ordinal = ordinals.GetValueOrDefault(key);
|
||||
calls[index] = call with { CallOrdinal = ordinal };
|
||||
ordinals[key] = ordinal + 1;
|
||||
}
|
||||
return new GuestStateLivenessResult(liveIn, liveOut, calls);
|
||||
}
|
||||
|
||||
private static GuestStateMask TransferBlock(
|
||||
IrBasicBlock block,
|
||||
GuestStateMask live,
|
||||
IReadOnlyDictionary<uint, GuestAbiContract> calleeContracts,
|
||||
List<GuestCallSiteStateContract>? calls)
|
||||
{
|
||||
for (var index = block.Instructions.Count - 1; index >= 0; --index)
|
||||
{
|
||||
var instruction = block.Instructions[index];
|
||||
if (instruction is IrCall call &&
|
||||
GuestTargetParser.TryParseAddress(call.Target, out var target) &&
|
||||
calleeContracts.TryGetValue(target, out var contract))
|
||||
{
|
||||
var inputs = GuestStateMask.FromContractReads(contract);
|
||||
// A no-destination call is a lowered PPC tail branch, which forwards the caller's
|
||||
// incoming LR to the eventual blr even if the callee's ABI summary omits it.
|
||||
if (string.IsNullOrWhiteSpace(call.Destination))
|
||||
inputs = inputs.Union(GuestStateMask.Empty with { Lr = true });
|
||||
var possibleWrites = GuestStateMask.FromContractWrites(contract);
|
||||
var definiteWrites = GuestStateMask.FromContractDefiniteWrites(contract);
|
||||
var liveAfter = live;
|
||||
calls?.Add(new GuestCallSiteStateContract(
|
||||
block.Label, index, 0, target, inputs, liveAfter.Intersect(possibleWrites), liveAfter));
|
||||
live = liveAfter.Except(definiteWrites).Union(inputs);
|
||||
// The IR call destination (normally LR) is an instruction-level
|
||||
// definition in addition to the callee's architectural effects.
|
||||
var destination = Register(call.Destination);
|
||||
live = live.Except(destination);
|
||||
continue;
|
||||
}
|
||||
|
||||
live = live.Except(Writes(instruction)).Union(Reads(instruction));
|
||||
}
|
||||
return live;
|
||||
}
|
||||
|
||||
private static GuestStateMask Reads(IrInstruction instruction)
|
||||
{
|
||||
var result = GuestStateMask.Empty;
|
||||
switch (instruction)
|
||||
{
|
||||
case IrAssign value: AddValue(ref result, value.Value); break;
|
||||
case IrBinary value: AddValue(ref result, value.Left); AddValue(ref result, value.Right); break;
|
||||
case IrLoad value: Add(ref result, value.Address.Base); break;
|
||||
case IrStore value: Add(ref result, value.Address.Base); AddValue(ref result, value.Source); break;
|
||||
case IrResolveGuestMemoryRange value: AddValue(ref result, value.Base); break;
|
||||
case IrResolvedLoad value: Add(ref result, value.OriginalAddress.Base); break;
|
||||
case IrResolvedStore value: Add(ref result, value.OriginalAddress.Base); AddValue(ref result, value.Source); break;
|
||||
case IrResolvedPsqLoad value:
|
||||
AddValue(ref result, value.OriginalAddress);
|
||||
if (value.KnownGqr is null || value.GuardKnownGqr) AddGqr(ref result, value.I);
|
||||
break;
|
||||
case IrResolvedPsqStore value:
|
||||
AddValue(ref result, value.OriginalAddress); AddValue(ref result, value.Source);
|
||||
if (value.KnownGqr is null || value.GuardKnownGqr) AddGqr(ref result, value.I);
|
||||
break;
|
||||
case IrResolvedLoadPair value:
|
||||
Add(ref result, value.FirstOriginalAddress.Base);
|
||||
Add(ref result, value.SecondOriginalAddress.Base);
|
||||
break;
|
||||
case IrResolvedStorePair value:
|
||||
Add(ref result, value.FirstOriginalAddress.Base); Add(ref result, value.SecondOriginalAddress.Base);
|
||||
AddValue(ref result, value.FirstSource); AddValue(ref result, value.SecondSource);
|
||||
break;
|
||||
case IrCall value: foreach (var argument in value.Arguments) AddValue(ref result, argument); break;
|
||||
case IrIndirectCall value:
|
||||
AddValue(ref result, value.Target);
|
||||
foreach (var argument in value.Arguments) AddValue(ref result, argument);
|
||||
result = result.Union(FullState);
|
||||
break;
|
||||
case IrIndirectJump value: AddValue(ref result, value.Target); result = result.Union(FullState); break;
|
||||
case IrSetCrField value:
|
||||
AddValue(ref result, value.Left);
|
||||
AddValue(ref result, value.Right);
|
||||
// Same as unioning Register("xer").
|
||||
result = result with { Xer = true };
|
||||
break;
|
||||
case IrPhi value: foreach (var source in value.Sources.Values) Add(ref result, source); break;
|
||||
case IrBranch value: Add(ref result, value.ConditionRegister); break;
|
||||
case IrJumpTable value: Add(ref result, value.Selector); break;
|
||||
case IrReturn { Value: { } value }: AddValue(ref result, value); break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static GuestStateMask Writes(IrInstruction instruction)
|
||||
{
|
||||
var result = GuestStateMask.Empty;
|
||||
switch (instruction)
|
||||
{
|
||||
case IrAssign value: Add(ref result, value.Destination); break;
|
||||
case IrBinary value: Add(ref result, value.Destination); break;
|
||||
case IrLoad value: Add(ref result, value.Destination); break;
|
||||
case IrResolveGuestMemoryRange value: Add(ref result, value.Destination); break;
|
||||
case IrResolvedLoad value: Add(ref result, value.Destination); break;
|
||||
case IrResolvedPsqLoad value: Add(ref result, value.Destination); break;
|
||||
case IrResolvedLoadPair value:
|
||||
Add(ref result, value.FirstDestination);
|
||||
Add(ref result, value.SecondDestination);
|
||||
break;
|
||||
case IrCall value: Add(ref result, value.Destination); break;
|
||||
case IrIndirectCall value: Add(ref result, value.Destination); result = result.Union(FullState); break;
|
||||
case IrSetCrField value:
|
||||
result = result with { Cr = (byte)(result.Cr | (1 << value.FieldIndex)) };
|
||||
break;
|
||||
case IrPhi value: Add(ref result, value.Destination); break;
|
||||
case IrUndefined: result = FullState; break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void Add(ref GuestStateMask mask, string? name) => mask = mask.Union(Register(name));
|
||||
|
||||
private static void AddValue(ref GuestStateMask mask, IrValue value)
|
||||
{
|
||||
if (value.Kind == "register") Add(ref mask, value.RegisterName);
|
||||
}
|
||||
|
||||
private static void AddGqr(ref GuestStateMask mask, uint index)
|
||||
{
|
||||
// Mirrors Register("gqr{index}"): only the eight architectural
|
||||
// quantization registers exist, anything else names nothing.
|
||||
if (index <= 7) mask = mask with { Gqr = (byte)(mask.Gqr | (1 << (int)index)) };
|
||||
}
|
||||
|
||||
private static GuestStateMask Register(string? name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name)) return GuestStateMask.Empty;
|
||||
var baseName = RegisterNameUtils.HardwareBase(name.AsSpan());
|
||||
if (baseName.Length >= 2 && baseName[0] == 'r' && int.TryParse(baseName[1..], out var gpr) && gpr is >= 0 and < 32)
|
||||
return GuestStateMask.Empty with { Gpr = 1u << gpr };
|
||||
if (baseName.Length >= 2 && baseName[0] == 'f' && int.TryParse(baseName[1..], out var fpr) && fpr is >= 0 and < 32)
|
||||
return GuestStateMask.Empty with { Fpr = 1u << fpr };
|
||||
if (baseName.Length == 3 && baseName.StartsWith("cr", StringComparison.OrdinalIgnoreCase) && baseName[2] is >= '0' and <= '7')
|
||||
return GuestStateMask.Empty with { Cr = (byte)(1 << (baseName[2] - '0')) };
|
||||
if (baseName.Length is >= 4 and <= 5 && baseName.StartsWith("crb", StringComparison.OrdinalIgnoreCase) &&
|
||||
int.TryParse(baseName[3..], out var crBit) && crBit is >= 0 and < 32)
|
||||
return GuestStateMask.Empty with { Cr = (byte)(1 << (crBit / 4)) };
|
||||
if (baseName.Equals("cr", StringComparison.OrdinalIgnoreCase)) return GuestStateMask.Empty with { Cr = byte.MaxValue };
|
||||
if (baseName.Equals("xer", StringComparison.OrdinalIgnoreCase)) return GuestStateMask.Empty with { Xer = true };
|
||||
if (baseName.Equals("ctr", StringComparison.OrdinalIgnoreCase)) return GuestStateMask.Empty with { Ctr = true };
|
||||
if (baseName.Equals("lr", StringComparison.OrdinalIgnoreCase)) return GuestStateMask.Empty with { Lr = true };
|
||||
if (baseName.Equals("fpscr", StringComparison.OrdinalIgnoreCase)) return GuestStateMask.Empty with { Fpscr = true };
|
||||
if (baseName.Length == 4 && baseName.StartsWith("gqr", StringComparison.OrdinalIgnoreCase) &&
|
||||
baseName[3] is >= '0' and <= '7')
|
||||
return GuestStateMask.Empty with { Gqr = (byte)(1 << (baseName[3] - '0')) };
|
||||
if (baseName.Length == 4 && baseName.StartsWith("hid", StringComparison.OrdinalIgnoreCase) &&
|
||||
baseName[3] is >= '0' and <= '2')
|
||||
return GuestStateMask.Empty with { Hid = (byte)(1 << (baseName[3] - '0')) };
|
||||
return GuestStateMask.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of syntactic successors of <paramref name="block"/>
|
||||
/// (0, 1 or 2) and their labels, without allocating an iterator.
|
||||
/// </summary>
|
||||
private static int Successors(
|
||||
IrBasicBlock block,
|
||||
string? fallthroughLabel,
|
||||
out string first,
|
||||
out string second)
|
||||
{
|
||||
var instructions = block.Instructions;
|
||||
var terminator = instructions.Count == 0 ? null : instructions[instructions.Count - 1];
|
||||
if (terminator is IrBranch branch)
|
||||
{
|
||||
first = branch.TrueLabel;
|
||||
second = branch.FalseLabel;
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (terminator is IrJump jump)
|
||||
{
|
||||
first = jump.TargetLabel;
|
||||
second = string.Empty;
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (terminator is not (IrReturn or IrUndefined or IrIndirectJump or IrJumpTable) &&
|
||||
fallthroughLabel is not null)
|
||||
{
|
||||
// The canonical IR deliberately leaves ordinary sequential PPC
|
||||
// flow implicit. Omitting this edge makes later-block inputs look
|
||||
// dead at entry and is unsound for state deconstruction.
|
||||
first = fallthroughLabel;
|
||||
second = string.Empty;
|
||||
return 1;
|
||||
}
|
||||
|
||||
first = string.Empty;
|
||||
second = string.Empty;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static GuestStateMask FullState =>
|
||||
new(uint.MaxValue, uint.MaxValue, byte.MaxValue, true, true, true, true, byte.MaxValue, byte.MaxValue);
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Translator.Core;
|
||||
using Translator.Core.Analysis.Ssa;
|
||||
using Translator.Core.Ir;
|
||||
using Translator.Core.Representation;
|
||||
|
||||
namespace Translator.Core.Analysis.Representation;
|
||||
|
||||
public sealed record FunctionAbiClassification(
|
||||
string Name,
|
||||
ValueRepresentation ReturnRepresentation);
|
||||
|
||||
public sealed class FunctionAbiClassifier
|
||||
{
|
||||
private static readonly string[] AbiIntegerReturnOrder = { "r3", "r4" };
|
||||
private static readonly string[] AbiFloatReturnOrder = { "f1" };
|
||||
|
||||
public FunctionAbiClassification Classify(
|
||||
string functionName,
|
||||
IrFunction function,
|
||||
RepresentationEnvironment representations) =>
|
||||
new(functionName, InferReturnRepresentation(function, representations));
|
||||
|
||||
private static ValueRepresentation InferReturnRepresentation(IrFunction function, RepresentationEnvironment types)
|
||||
{
|
||||
var explicitReturnRepresentations = function.Blocks
|
||||
.SelectMany(b => b.Instructions)
|
||||
.OfType<IrReturn>()
|
||||
.Where(r => r.Value != null)
|
||||
.Select(r => InferReturnRepresentationFromValue(r.Value!, types))
|
||||
.Where(t => t != null)
|
||||
.Cast<ValueRepresentation>()
|
||||
.ToList();
|
||||
|
||||
if (explicitReturnRepresentations.Count > 0)
|
||||
{
|
||||
return MergeReturnRepresentations(explicitReturnRepresentations);
|
||||
}
|
||||
|
||||
var cfg = IrCfg.Build(function);
|
||||
var inferredReturnRepresentations = new List<ValueRepresentation>();
|
||||
foreach (var block in function.Blocks)
|
||||
{
|
||||
if (block.Instructions.LastOrDefault() is not IrReturn)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
inferredReturnRepresentations.AddRange(CollectAbiReturnRepresentationsNearExit(block.Label, cfg, types, new HashSet<string>(StringComparer.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
return inferredReturnRepresentations.Count == 0
|
||||
? ValueRepresentation.Void
|
||||
: MergeReturnRepresentations(inferredReturnRepresentations);
|
||||
}
|
||||
|
||||
private static ValueRepresentation? InferReturnRepresentationFromValue(IrValue val, RepresentationEnvironment types)
|
||||
{
|
||||
if (val.Kind == "register" && val.RegisterName != null)
|
||||
{
|
||||
return types.Get(Base(val.RegisterName));
|
||||
}
|
||||
|
||||
return ValueRepresentation.Int32;
|
||||
}
|
||||
|
||||
private static IEnumerable<ValueRepresentation> CollectAbiReturnRepresentationsNearExit(
|
||||
string label,
|
||||
IrCfg cfg,
|
||||
RepresentationEnvironment types,
|
||||
HashSet<string> visited)
|
||||
{
|
||||
if (!visited.Add(label) || !cfg.Blocks.TryGetValue(label, out var block))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var localType = FindRecentAbiReturnRepresentation(block, types);
|
||||
if (localType != null)
|
||||
{
|
||||
yield return localType;
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var pred in cfg.Predecessors(label))
|
||||
{
|
||||
foreach (var predType in CollectAbiReturnRepresentationsNearExit(pred, cfg, types, visited))
|
||||
{
|
||||
yield return predType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ValueRepresentation? FindRecentAbiReturnRepresentation(IrBasicBlock block, RepresentationEnvironment types)
|
||||
{
|
||||
var limit = block.Instructions.Count;
|
||||
var terminator = limit > 0 ? block.Instructions[limit - 1] : null;
|
||||
var isDirectReturnBlock = terminator is IrReturn;
|
||||
if (isDirectReturnBlock)
|
||||
{
|
||||
limit--;
|
||||
}
|
||||
|
||||
for (var i = limit - 1; i >= 0; i--)
|
||||
{
|
||||
if (!TryGetDefinedRegister(block.Instructions[i], out var definedRegister))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var baseName = Base(definedRegister);
|
||||
if (AbiFloatReturnOrder.Contains(baseName, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!CanFloatDefineImplicitReturn(block, i, limit, terminator))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return types.Get(baseName);
|
||||
}
|
||||
|
||||
if (AbiIntegerReturnOrder.Contains(baseName, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return types.Get(baseName);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool CanFloatDefineImplicitReturn(IrBasicBlock block, int candidateIndex, int limit, IrInstruction? terminator)
|
||||
{
|
||||
if (terminator is not (IrReturn or IrJump))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var epilogueLimit = terminator is IrJump ? Math.Max(candidateIndex + 1, limit - 1) : limit;
|
||||
for (var i = candidateIndex + 1; i < epilogueLimit; i++)
|
||||
{
|
||||
if (!IsDirectReturnFloatEpilogueInstruction(block.Instructions[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsDirectReturnFloatEpilogueInstruction(IrInstruction instruction)
|
||||
{
|
||||
return instruction switch
|
||||
{
|
||||
IrAssign => true,
|
||||
IrBinary => true,
|
||||
IrLoad load when string.Equals(Base(load.Address.Base), "r1", StringComparison.OrdinalIgnoreCase) => true,
|
||||
IrComment => true,
|
||||
IrTracePpc => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryGetDefinedRegister(IrInstruction ins, out string register)
|
||||
{
|
||||
register = string.Empty;
|
||||
switch (ins)
|
||||
{
|
||||
case IrAssign a when !string.IsNullOrWhiteSpace(a.Destination):
|
||||
register = a.Destination;
|
||||
return true;
|
||||
case IrBinary b when !string.IsNullOrWhiteSpace(b.Destination):
|
||||
register = b.Destination;
|
||||
return true;
|
||||
case IrLoad l when !string.IsNullOrWhiteSpace(l.Destination):
|
||||
register = l.Destination;
|
||||
return true;
|
||||
case IrCall c when !string.IsNullOrWhiteSpace(c.Destination):
|
||||
register = c.Destination;
|
||||
return true;
|
||||
case IrIndirectCall ic when !string.IsNullOrWhiteSpace(ic.Destination):
|
||||
register = ic.Destination;
|
||||
return true;
|
||||
case IrPhi p when !string.IsNullOrWhiteSpace(p.Destination):
|
||||
register = p.Destination;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static ValueRepresentation MergeReturnRepresentations(IReadOnlyList<ValueRepresentation> returnTypes)
|
||||
{
|
||||
var distinct = returnTypes.Distinct().ToList();
|
||||
if (distinct.Count == 1)
|
||||
{
|
||||
return distinct[0];
|
||||
}
|
||||
|
||||
var floatCandidate = distinct.OfType<ValueRepresentation>().FirstOrDefault(t => t.IsFloat);
|
||||
var nonFloatCandidate = distinct.OfType<ValueRepresentation>().FirstOrDefault(t => !t.IsFloat);
|
||||
|
||||
if (floatCandidate != null && nonFloatCandidate == null)
|
||||
{
|
||||
return floatCandidate;
|
||||
}
|
||||
|
||||
if (nonFloatCandidate != null && floatCandidate == null)
|
||||
{
|
||||
return nonFloatCandidate;
|
||||
}
|
||||
|
||||
return ValueRepresentation.Int32;
|
||||
}
|
||||
|
||||
private static string Base(string name) => RegisterNameUtils.StripNumericSuffix(name);
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Translator.Core.Analysis;
|
||||
|
||||
public sealed record RuntimeNativeGuestEffectSet(
|
||||
IReadOnlyDictionary<uint, GuestAbiContract> Contracts,
|
||||
IReadOnlySet<uint> PreciseContracts,
|
||||
IReadOnlySet<uint> ConservativeContracts);
|
||||
|
||||
/// <summary>
|
||||
/// Builds architectural effect contracts for every literal native registration. Typed stubs
|
||||
/// derive their contract from the host signature; CpuContext natives are precise only when the
|
||||
/// whole body reduces to direct, constant-index member access, otherwise they get a full-context contract.
|
||||
/// </summary>
|
||||
public static class RuntimeNativeGuestEffectAnalyzer
|
||||
{
|
||||
private static readonly Regex StubRegex = GeneratedMarkers.NativeOverrideSignaturePattern();
|
||||
private static readonly Regex RegistrationRegex = GeneratedMarkers.NativeFunctionRegistrationPattern();
|
||||
private static readonly Regex FatalRegex = GeneratedMarkers.FatalStubPattern();
|
||||
|
||||
public static RuntimeNativeGuestEffectSet AnalyzeDirectory(string directory)
|
||||
=> AnalyzeSources(NativeSourceParsing.ReadDirectory(directory));
|
||||
|
||||
internal static RuntimeNativeGuestEffectSet AnalyzeSources(
|
||||
IReadOnlyList<NativeSourceFile> sources)
|
||||
{
|
||||
var contracts = new Dictionary<uint, GuestAbiContract>();
|
||||
var precise = new HashSet<uint>();
|
||||
var conservative = new HashSet<uint>();
|
||||
|
||||
foreach (var sourceFile in sources)
|
||||
{
|
||||
var source = sourceFile.Content;
|
||||
var stubAddresses = new HashSet<uint>();
|
||||
foreach (Match match in StubRegex.Matches(source))
|
||||
{
|
||||
var address = ParseAddress(match.Groups["address"].Value);
|
||||
stubAddresses.Add(address);
|
||||
var symbol = match.Groups["symbol"].Value;
|
||||
var tail = match.Groups["tail"].Value;
|
||||
var isVoid = match.Groups["void"].Success;
|
||||
if (TryParseStubTail(tail, isVoid, out var returnType, out var arguments) &&
|
||||
TryAnalyzeRegisteredFunction(source, symbol, returnType, arguments, out var contract))
|
||||
{
|
||||
Add(address, contract, !contract.HasFullSynchronizationFence);
|
||||
}
|
||||
else
|
||||
{
|
||||
Add(address, CompleteContextContract(), false);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Match match in RegistrationRegex.Matches(source))
|
||||
{
|
||||
var address = ParseAddress(match.Groups["address"].Value);
|
||||
if (stubAddresses.Contains(address))
|
||||
continue;
|
||||
var symbol = match.Groups["symbol"].Value;
|
||||
if (TryExtractFunction(source, symbol, out var definition) &&
|
||||
TryAnalyzeRegisteredFunction(source, symbol, definition.ReturnRepresentation, definition.Arguments, out var contract))
|
||||
{
|
||||
Add(address, contract, !contract.HasFullSynchronizationFence);
|
||||
}
|
||||
else
|
||||
{
|
||||
Add(address, CompleteContextContract(), false);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Match match in FatalRegex.Matches(source))
|
||||
Add(ParseAddress(match.Groups["address"].Value), CompleteContextContract(), false);
|
||||
}
|
||||
|
||||
return new RuntimeNativeGuestEffectSet(contracts, precise, conservative);
|
||||
|
||||
void Add(uint address, GuestAbiContract contract, bool isPrecise)
|
||||
{
|
||||
// Multiple registrations for one address are safe only at the
|
||||
// intersection of their precision. A conservative contender wins.
|
||||
if (contracts.TryGetValue(address, out var previous))
|
||||
{
|
||||
contract = MergeAlternativeContracts(previous, contract);
|
||||
isPrecise = precise.Contains(address) && isPrecise && !contract.HasFullSynchronizationFence;
|
||||
}
|
||||
contracts[address] = contract;
|
||||
if (isPrecise)
|
||||
{
|
||||
precise.Add(address);
|
||||
conservative.Remove(address);
|
||||
}
|
||||
else
|
||||
{
|
||||
precise.Remove(address);
|
||||
conservative.Add(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record FunctionDefinition(string ReturnRepresentation, string Arguments, string Body, string ContextName);
|
||||
|
||||
private static bool TryAnalyzeRegisteredFunction(
|
||||
string source,
|
||||
string symbol,
|
||||
string returnType,
|
||||
string arguments,
|
||||
out GuestAbiContract contract)
|
||||
{
|
||||
var argumentList = NativeSourceParsing.SplitArguments(arguments).ToArray();
|
||||
var contextArgument = argumentList
|
||||
.Select(argument => Regex.Match(argument, @"\bCpuContext\s*\*\s*(?<name>[A-Za-z_][A-Za-z0-9_]*)"))
|
||||
.FirstOrDefault(static match => match.Success);
|
||||
if (contextArgument is not null && contextArgument.Success)
|
||||
{
|
||||
if (!TryExtractFunction(source, symbol, out var definition))
|
||||
{
|
||||
contract = CompleteContextContract();
|
||||
return false;
|
||||
}
|
||||
contract = AnalyzeContextBody(definition.Body, contextArgument.Groups["name"].Value);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryExtractFunction(source, symbol, out var typedDefinition))
|
||||
{
|
||||
contract = CompleteContextContract();
|
||||
return false;
|
||||
}
|
||||
var typedBoundaryFlags = AnalyzeBoundaryFlags(typedDefinition.Body);
|
||||
if ((typedBoundaryFlags & GuestCallBoundaryFlags.RequiresCompleteContext) != 0)
|
||||
{
|
||||
contract = CompleteContextContract(typedBoundaryFlags);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint gprRead = 0;
|
||||
uint fprRead = 0;
|
||||
var gpr = 3;
|
||||
var fpr = 1;
|
||||
foreach (var argument in argumentList)
|
||||
{
|
||||
var trimmed = argument.Trim();
|
||||
if (trimmed.Length == 0 || trimmed.Equals("void", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
if (NativeSourceParsing.IsFloatingPointValueArgument(trimmed))
|
||||
{
|
||||
if (fpr <= 13) fprRead |= 1u << fpr;
|
||||
++fpr;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (gpr <= 10) gprRead |= 1u << gpr;
|
||||
else gprRead |= 1u << 1; // stack argument area
|
||||
++gpr;
|
||||
}
|
||||
}
|
||||
|
||||
var normalizedReturn = returnType.Trim();
|
||||
var returnsVoid = Regex.IsMatch(normalizedReturn, @"\bvoid\s*$", RegexOptions.CultureInvariant);
|
||||
var returnsFloat = !returnsVoid && Regex.IsMatch(normalizedReturn, @"\b(float|double)\s*$", RegexOptions.CultureInvariant);
|
||||
var gprWrite = !returnsVoid && !returnsFloat ? 1u << 3 : 0u;
|
||||
var fprWrite = returnsFloat ? 1u << 1 : 0u;
|
||||
contract = new GuestAbiContract(
|
||||
gprRead, gprWrite, gprWrite,
|
||||
fprRead, fprWrite, fprWrite,
|
||||
0, 0, false, false, false, false, false, false,
|
||||
typedBoundaryFlags, Array.Empty<uint>());
|
||||
return true;
|
||||
}
|
||||
|
||||
private static GuestAbiContract AnalyzeContextBody(string body, string contextName)
|
||||
{
|
||||
uint gprRead = 0, gprWrite = 0, fprRead = 0, fprWrite = 0;
|
||||
byte crRead = 0, crWrite = 0;
|
||||
var xerRead = false; var xerWrite = false;
|
||||
var ctrRead = false; var ctrWrite = false;
|
||||
var lrRead = false; var lrWrite = false;
|
||||
var flags = GuestCallBoundaryFlags.None;
|
||||
var escapedName = Regex.Escape(contextName);
|
||||
|
||||
AnalyzeIndexed("gpr", 32, (index, read, write) =>
|
||||
{
|
||||
if (read) gprRead |= 1u << index;
|
||||
if (write) gprWrite |= 1u << index;
|
||||
});
|
||||
AnalyzeIndexed("fpr", 32, (index, read, write) =>
|
||||
{
|
||||
if (read) fprRead |= 1u << index;
|
||||
if (write) fprWrite |= 1u << index;
|
||||
});
|
||||
AnalyzeScalar("cr", (read, write) => { if (read) crRead = byte.MaxValue; if (write) crWrite = byte.MaxValue; });
|
||||
AnalyzeScalar("xer", (read, write) => { xerRead |= read; xerWrite |= write; });
|
||||
AnalyzeScalar("ctr", (read, write) => { ctrRead |= read; ctrWrite |= write; });
|
||||
AnalyzeScalar("lr", (read, write) => { lrRead |= read; lrWrite |= write; });
|
||||
|
||||
if (Regex.IsMatch(body, $@"\b{escapedName}\s*->\s*(gpr|fpr)\s*\[\s*[^0-9\s]", RegexOptions.CultureInvariant) ||
|
||||
Regex.IsMatch(body, $@"\b{escapedName}\s*->\s*(?!gpr\b|fpr\b|cr\b|xer\b|ctr\b|lr\b)", RegexOptions.CultureInvariant))
|
||||
flags |= GuestCallBoundaryFlags.RequiresCompleteContext;
|
||||
|
||||
// Passing or aliasing the context hides arbitrary architectural access.
|
||||
if (Regex.IsMatch(body, $@"(?:\(|,)\s*{escapedName}\s*(?:,|\))", RegexOptions.CultureInvariant) ||
|
||||
Regex.IsMatch(body, $@"[=&]\s*{escapedName}\s*;", RegexOptions.CultureInvariant))
|
||||
flags |= GuestCallBoundaryFlags.RequiresCompleteContext;
|
||||
|
||||
flags |= AnalyzeBoundaryFlags(body);
|
||||
|
||||
if ((flags & GuestCallBoundaryFlags.RequiresCompleteContext) != 0)
|
||||
return CompleteContextContract(flags);
|
||||
|
||||
return new GuestAbiContract(
|
||||
gprRead, gprWrite, gprWrite & ((1u << 3) | (1u << 4)),
|
||||
fprRead, fprWrite, fprWrite & (1u << 1),
|
||||
crRead, crWrite, xerRead, xerWrite, ctrRead, ctrWrite, lrRead, lrWrite,
|
||||
flags, Array.Empty<uint>());
|
||||
|
||||
void AnalyzeIndexed(string member, int count, Action<int, bool, bool> add)
|
||||
{
|
||||
var regex = new Regex($@"\b{escapedName}\s*->\s*{member}\s*\[\s*(?<index>[0-9]+)\s*\](?:\s*\.\s*[A-Za-z_][A-Za-z0-9_]*)?", RegexOptions.CultureInvariant);
|
||||
foreach (Match match in regex.Matches(body))
|
||||
{
|
||||
if (!int.TryParse(match.Groups["index"].Value, out var index) || index < 0 || index >= count)
|
||||
{
|
||||
flags |= GuestCallBoundaryFlags.RequiresCompleteContext;
|
||||
continue;
|
||||
}
|
||||
var (read, write) = ClassifyAccess(body, match.Index + match.Length);
|
||||
add(index, read, write);
|
||||
}
|
||||
}
|
||||
|
||||
void AnalyzeScalar(string member, Action<bool, bool> add)
|
||||
{
|
||||
var regex = new Regex($@"\b{escapedName}\s*->\s*{member}\b", RegexOptions.CultureInvariant);
|
||||
foreach (Match match in regex.Matches(body))
|
||||
{
|
||||
var (read, write) = ClassifyAccess(body, match.Index + match.Length);
|
||||
add(read, write);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static GuestCallBoundaryFlags AnalyzeBoundaryFlags(string body)
|
||||
{
|
||||
var flags = GuestCallBoundaryFlags.None;
|
||||
// A typed stub's contract comes from its C++ signature alone, so a body that reaches for
|
||||
// CpuContext can touch architectural state the signature never mentions. Real case:
|
||||
// 0x801AAD7C (__OSGetSystemTime) is `uint32_t (...)` but writes r3 and r4 via
|
||||
// CurrentCpuContext(); a narrowed boundary left r4 stale and wedged RFL's loader.
|
||||
if (Regex.IsMatch(body,
|
||||
@"\b(CurrentCpuContext|TryGetCpuContext|GetPersistentCpuContext|CpuContextScope)\b",
|
||||
RegexOptions.CultureInvariant))
|
||||
flags |= GuestCallBoundaryFlags.RequiresCompleteContext;
|
||||
if (Regex.IsMatch(body, @"\b(InvokeDirectCpu|InvokeIndirectCpu|InvokeIndirectJump|InvokeGuestCallback)\b", RegexOptions.CultureInvariant))
|
||||
flags |= GuestCallBoundaryFlags.InvokesGuestCode | GuestCallBoundaryFlags.RequiresCompleteContext;
|
||||
if (Regex.IsMatch(body, @"\b(YieldToScheduler|SuspendCurrent|SleepCurrent|WaitFor|BlockCurrent)\b", RegexOptions.CultureInvariant))
|
||||
flags |= GuestCallBoundaryFlags.CanSuspend | GuestCallBoundaryFlags.RequiresCompleteContext;
|
||||
if (Regex.IsMatch(body, @"\b(SwitchTo|SelectThread|RunReadyThreads|ScheduleThread)\b", RegexOptions.CultureInvariant))
|
||||
flags |= GuestCallBoundaryFlags.CanSwitchThreads | GuestCallBoundaryFlags.RequiresCompleteContext;
|
||||
return flags;
|
||||
}
|
||||
|
||||
private static (bool Read, bool Write) ClassifyAccess(string body, int end)
|
||||
{
|
||||
var tail = body.AsSpan(end).TrimStart();
|
||||
if (tail.StartsWith("++", StringComparison.Ordinal) || tail.StartsWith("--", StringComparison.Ordinal)) return (true, true);
|
||||
if (tail.StartsWith("+=", StringComparison.Ordinal) || tail.StartsWith("-=", StringComparison.Ordinal) ||
|
||||
tail.StartsWith("*=", StringComparison.Ordinal) || tail.StartsWith("/=", StringComparison.Ordinal) ||
|
||||
tail.StartsWith("|=", StringComparison.Ordinal) || tail.StartsWith("&=", StringComparison.Ordinal) ||
|
||||
tail.StartsWith("^=", StringComparison.Ordinal)) return (true, true);
|
||||
if (tail.StartsWith("=", StringComparison.Ordinal) && !tail.StartsWith("==", StringComparison.Ordinal)) return (false, true);
|
||||
return (true, false);
|
||||
}
|
||||
|
||||
private static bool TryParseStubTail(string tail, bool isVoid, out string returnType, out string arguments)
|
||||
{
|
||||
returnType = "void";
|
||||
arguments = string.Empty;
|
||||
var cursor = 0;
|
||||
if (!isVoid)
|
||||
{
|
||||
var comma = FindTopLevelComma(tail, cursor);
|
||||
if (comma < 0) return false;
|
||||
returnType = tail[..comma].Trim();
|
||||
cursor = comma + 1;
|
||||
}
|
||||
while (cursor < tail.Length && char.IsWhiteSpace(tail[cursor])) ++cursor;
|
||||
if (cursor >= tail.Length || tail[cursor] != '(') return false;
|
||||
var close = FindMatching(tail, cursor, '(', ')');
|
||||
if (close < 0) return false;
|
||||
arguments = tail[(cursor + 1)..close];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryExtractFunction(string source, string symbol, out FunctionDefinition definition)
|
||||
{
|
||||
var regex = new Regex($@"(?:extern\s+\""C\""\s+)?(?<return>[A-Za-z_][A-Za-z0-9_:<>\s*&]*?)\b{Regex.Escape(symbol)}\s*\((?<args>[^)]*)\)\s*(?:noexcept\s*)?\{{", RegexOptions.CultureInvariant);
|
||||
foreach (Match match in regex.Matches(source))
|
||||
{
|
||||
var open = match.Index + match.Length - 1;
|
||||
var close = FindMatching(source, open, '{', '}');
|
||||
if (close < 0) continue;
|
||||
var args = match.Groups["args"].Value;
|
||||
var context = Regex.Match(args, @"\bCpuContext\s*\*\s*(?<name>[A-Za-z_][A-Za-z0-9_]*)");
|
||||
definition = new FunctionDefinition(
|
||||
match.Groups["return"].Value.Trim(),
|
||||
args,
|
||||
source[(open + 1)..close],
|
||||
context.Success ? context.Groups["name"].Value : string.Empty);
|
||||
return true;
|
||||
}
|
||||
definition = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static GuestAbiContract CompleteContextContract(GuestCallBoundaryFlags extra = GuestCallBoundaryFlags.None) =>
|
||||
new(uint.MaxValue, uint.MaxValue, (1u << 3) | (1u << 4),
|
||||
uint.MaxValue, uint.MaxValue, 1u << 1,
|
||||
byte.MaxValue, byte.MaxValue, true, true, true, true, true, true,
|
||||
extra | GuestCallBoundaryFlags.RequiresCompleteContext, Array.Empty<uint>());
|
||||
|
||||
private static GuestAbiContract MergeAlternativeContracts(GuestAbiContract left, GuestAbiContract right) =>
|
||||
new(left.GprReadBeforeWriteMask | right.GprReadBeforeWriteMask,
|
||||
left.GprPossibleWriteMask | right.GprPossibleWriteMask,
|
||||
left.GprReturnMask | right.GprReturnMask,
|
||||
left.FprReadBeforeWriteMask | right.FprReadBeforeWriteMask,
|
||||
left.FprPossibleWriteMask | right.FprPossibleWriteMask,
|
||||
left.FprReturnMask | right.FprReturnMask,
|
||||
(byte)(left.CrReadBeforeWriteMask | right.CrReadBeforeWriteMask),
|
||||
(byte)(left.CrPossibleWriteMask | right.CrPossibleWriteMask),
|
||||
left.ReadsXerBeforeWrite || right.ReadsXerBeforeWrite,
|
||||
left.MayWriteXer || right.MayWriteXer,
|
||||
left.ReadsCtrBeforeWrite || right.ReadsCtrBeforeWrite,
|
||||
left.MayWriteCtr || right.MayWriteCtr,
|
||||
left.ReadsLrBeforeWrite || right.ReadsLrBeforeWrite,
|
||||
left.MayWriteLr || right.MayWriteLr,
|
||||
left.BoundaryFlags | right.BoundaryFlags,
|
||||
Array.Empty<uint>());
|
||||
|
||||
private static int FindTopLevelComma(string text, int start)
|
||||
{
|
||||
var depth = 0;
|
||||
for (var index = start; index < text.Length; ++index)
|
||||
{
|
||||
if (text[index] is '(' or '[' or '<') ++depth;
|
||||
else if (text[index] is ')' or ']' or '>') depth = Math.Max(0, depth - 1);
|
||||
else if (text[index] == ',' && depth == 0) return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static int FindMatching(string text, int open, char openChar, char closeChar)
|
||||
{
|
||||
var depth = 0;
|
||||
for (var index = open; index < text.Length; ++index)
|
||||
{
|
||||
if (text[index] == openChar) ++depth;
|
||||
else if (text[index] == closeChar && --depth == 0) return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static uint ParseAddress(string value) => GuestTargetParser.ParseHexAddress(value);
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,190 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Translator.Core.Ir;
|
||||
|
||||
namespace Translator.Core.Analysis;
|
||||
|
||||
/// <summary>
|
||||
/// Simple, intraprocedural stack-address facts for local codegen and stack helper selection. Scans the
|
||||
/// IR in block order following register copies and register-plus-constant add/sub, treating every SSA
|
||||
/// spelling of r1 as the stack root. No CFG joins or phi analysis; that's the entry-frame/affine passes' job.
|
||||
/// </summary>
|
||||
internal sealed class StackAddressFacts
|
||||
{
|
||||
private readonly Dictionary<string, int> _temporaryOffsets;
|
||||
|
||||
private StackAddressFacts(Dictionary<string, int> temporaryOffsets)
|
||||
{
|
||||
_temporaryOffsets = temporaryOffsets;
|
||||
}
|
||||
|
||||
public static StackAddressFacts Empty { get; } =
|
||||
new(new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
public static StackAddressFacts Build(IrFunction function)
|
||||
{
|
||||
var temporaryOffsets = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var block in function.Blocks)
|
||||
{
|
||||
foreach (var instruction in block.Instructions)
|
||||
{
|
||||
switch (instruction)
|
||||
{
|
||||
// SSA form makes an architectural register destination as stable as a compiler
|
||||
// temporary (e.g. PPC prologues copying r1 to r11 for the save area).
|
||||
case IrAssign assign when
|
||||
TryResolveValue(assign.Value, temporaryOffsets, out var assignOffset):
|
||||
temporaryOffsets[assign.Destination] = assignOffset;
|
||||
break;
|
||||
|
||||
case IrBinary binary when
|
||||
TryResolveBinary(binary, temporaryOffsets, out var binaryOffset):
|
||||
temporaryOffsets[binary.Destination] = binaryOffset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new StackAddressFacts(temporaryOffsets);
|
||||
}
|
||||
|
||||
public bool TryResolve(IrAddress address, out int offset)
|
||||
{
|
||||
if (TryResolveRegister(address.Base, out var baseOffset))
|
||||
{
|
||||
offset = checked(baseOffset + address.Offset);
|
||||
return true;
|
||||
}
|
||||
|
||||
offset = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryResolve(IrValue value, out int offset)
|
||||
{
|
||||
if (value.Kind == "register" &&
|
||||
TryResolveRegister(value.RegisterName, out offset))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
offset = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryResolve(IrBinary binary, out int offset) =>
|
||||
TryResolveBinary(binary, _temporaryOffsets, out offset);
|
||||
|
||||
public bool TryResolveRegister(string? registerName, out int offset)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(registerName))
|
||||
{
|
||||
offset = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GetRegisterBaseName(registerName).Equals("r1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
offset = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
return _temporaryOffsets.TryGetValue(registerName, out offset);
|
||||
}
|
||||
|
||||
public bool ContainsTemporary(string registerName) =>
|
||||
_temporaryOffsets.ContainsKey(registerName);
|
||||
|
||||
private static bool TryResolveBinary(
|
||||
IrBinary binary,
|
||||
IReadOnlyDictionary<string, int> temporaryOffsets,
|
||||
out int offset) =>
|
||||
TryResolveValue(binary.Left, temporaryOffsets, out offset) &&
|
||||
TryGetIntConstant(binary.Right, out var constant) &&
|
||||
TryApplyOffset(binary.Op, offset, constant, out offset);
|
||||
|
||||
private static bool TryResolveValue(
|
||||
IrValue value,
|
||||
IReadOnlyDictionary<string, int> temporaryOffsets,
|
||||
out int offset)
|
||||
{
|
||||
if (value.Kind == "register" &&
|
||||
TryResolveRegister(value.RegisterName, temporaryOffsets, out offset))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
offset = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryResolveRegister(
|
||||
string? registerName,
|
||||
IReadOnlyDictionary<string, int> temporaryOffsets,
|
||||
out int offset)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(registerName))
|
||||
{
|
||||
offset = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GetRegisterBaseName(registerName).Equals("r1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
offset = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
return temporaryOffsets.TryGetValue(registerName, out offset);
|
||||
}
|
||||
|
||||
private static bool TryGetIntConstant(IrValue value, out int result)
|
||||
{
|
||||
if (value.Kind == "const" &&
|
||||
value.Constant is { } constant &&
|
||||
constant >= int.MinValue &&
|
||||
constant <= int.MaxValue)
|
||||
{
|
||||
result = (int)constant;
|
||||
return true;
|
||||
}
|
||||
|
||||
result = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryApplyOffset(string op, int baseOffset, int constant, out int offset)
|
||||
{
|
||||
offset = 0;
|
||||
switch (op)
|
||||
{
|
||||
case "add":
|
||||
offset = checked(baseOffset + constant);
|
||||
return true;
|
||||
case "sub":
|
||||
offset = checked(baseOffset - constant);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetRegisterBaseName(string name)
|
||||
{
|
||||
var underscore = name.IndexOf('_');
|
||||
if (underscore < 0 || underscore + 1 >= name.Length)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
for (var index = underscore + 1; index < name.Length; index++)
|
||||
{
|
||||
if (!char.IsDigit(name[index]))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
return name[..underscore];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using Translator.Core.Mods;
|
||||
|
||||
namespace Translator.Core.Build;
|
||||
|
||||
/// <summary>
|
||||
/// What one prune pass did. Warnings are collected rather than printed: pruning
|
||||
/// runs inside Core, and the CLI owns every byte of console output.
|
||||
/// </summary>
|
||||
public sealed record GeneratedOutputPruneResult(int RemovedFiles, IReadOnlyList<string> Warnings)
|
||||
{
|
||||
public static GeneratedOutputPruneResult Empty { get; } = new(0, Array.Empty<string>());
|
||||
}
|
||||
|
||||
public static class GeneratedOutputPruner
|
||||
{
|
||||
public static GeneratedOutputPruneResult Prune(
|
||||
string baseOutDir,
|
||||
IReadOnlySet<string> emittedPaths,
|
||||
BaseTranslationOutputMetadata? previousOutputMetadata = null)
|
||||
{
|
||||
if (!Directory.Exists(baseOutDir))
|
||||
return GeneratedOutputPruneResult.Empty;
|
||||
|
||||
var comparison = OperatingSystem.IsWindows()
|
||||
? StringComparison.OrdinalIgnoreCase
|
||||
: StringComparison.Ordinal;
|
||||
var root = Path.GetFullPath(baseOutDir)
|
||||
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
var rootPrefix = root + Path.DirectorySeparatorChar;
|
||||
IEnumerable<string> candidates;
|
||||
if (previousOutputMetadata is null)
|
||||
{
|
||||
// Compatibility migration for trees produced before translator-owned
|
||||
// metadata: every generated body under the root is a prune candidate.
|
||||
candidates = Directory.EnumerateFiles(root, "*.cpp", SearchOption.AllDirectories);
|
||||
}
|
||||
else
|
||||
{
|
||||
candidates = previousOutputMetadata.Functions.Select(function =>
|
||||
Path.Combine(root, function.RelativePath.Replace('/', Path.DirectorySeparatorChar)));
|
||||
}
|
||||
|
||||
var removed = 0;
|
||||
var warnings = new List<string>();
|
||||
foreach (var file in candidates.Distinct(
|
||||
OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal))
|
||||
{
|
||||
var fullPath = Path.GetFullPath(file);
|
||||
if (!fullPath.StartsWith(rootPrefix, comparison))
|
||||
{
|
||||
warnings.Add($"refused to prune generated path outside output root: {fullPath}");
|
||||
continue;
|
||||
}
|
||||
if (emittedPaths.Contains(fullPath) || !File.Exists(fullPath))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
File.Delete(fullPath);
|
||||
removed++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
warnings.Add($"failed to prune stale generated file {fullPath}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return new GeneratedOutputPruneResult(removed, warnings);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,211 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Translator.Core.Loading;
|
||||
|
||||
namespace Translator.Core.Build;
|
||||
|
||||
/// <summary>
|
||||
/// One bundled translation unit. Sha256/SourceByteLength are populated by TranslationSourceBundle.Read
|
||||
/// from the already-verified on-disk record, so consumers can validate against external metadata
|
||||
/// without re-hashing; entries built by producers leave them unset.
|
||||
/// </summary>
|
||||
public sealed record TranslationSourceBundleEntry(
|
||||
uint EntryPoint,
|
||||
string VirtualPath,
|
||||
string Source,
|
||||
string? Sha256 = null,
|
||||
long SourceByteLength = -1);
|
||||
|
||||
/// <summary>Versioned deterministic source handoff between lowering and aggregate shard emission.</summary>
|
||||
public sealed class TranslationSourceBundle
|
||||
{
|
||||
private static readonly byte[] Magic = "MKWSRC01"u8.ToArray();
|
||||
public const int FormatVersion = 1;
|
||||
private readonly IReadOnlyList<TranslationSourceBundleEntry> _entries;
|
||||
// A null value marks an address carried by more than one virtual path. Such an
|
||||
// address is ambiguous and must not resolve, exactly as the previous linear
|
||||
// "exactly one match" probe behaved.
|
||||
private readonly Dictionary<uint, TranslationSourceBundleEntry?> _byEntryPoint;
|
||||
|
||||
private TranslationSourceBundle(IReadOnlyList<TranslationSourceBundleEntry> entries)
|
||||
{
|
||||
_entries = entries;
|
||||
_byEntryPoint = new Dictionary<uint, TranslationSourceBundleEntry?>(entries.Count);
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (!_byEntryPoint.TryAdd(entry.EntryPoint, entry)) _byEntryPoint[entry.EntryPoint] = null;
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<TranslationSourceBundleEntry> Entries => _entries;
|
||||
public bool TryGet(uint entryPoint, out TranslationSourceBundleEntry entry)
|
||||
{
|
||||
if (_byEntryPoint.TryGetValue(entryPoint, out var match) && match is not null)
|
||||
{
|
||||
entry = match;
|
||||
return true;
|
||||
}
|
||||
entry = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void Write(string path, IEnumerable<TranslationSourceBundleEntry> entries)
|
||||
{
|
||||
var ordered = entries.OrderBy(static entry => entry.EntryPoint).ThenBy(static entry => entry.VirtualPath, StringComparer.Ordinal).ToArray();
|
||||
if (ordered.Select(static entry => (entry.EntryPoint, entry.VirtualPath)).Distinct().Count() != ordered.Length)
|
||||
throw new InvalidDataException("Translation source bundle contains duplicate address/path identities.");
|
||||
var fullPath = Path.GetFullPath(path);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
|
||||
var temporary = $"{fullPath}.tmp.{Guid.NewGuid():N}";
|
||||
using (var stream = File.Create(temporary))
|
||||
using (var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: false))
|
||||
{
|
||||
writer.Write(Magic);
|
||||
writer.Write(FormatVersion);
|
||||
writer.Write(ordered.Length);
|
||||
foreach (var entry in ordered)
|
||||
{
|
||||
var pathBytes = Encoding.UTF8.GetBytes(entry.VirtualPath.Replace('\\', '/'));
|
||||
var sourceBytes = Encoding.UTF8.GetBytes(entry.Source);
|
||||
writer.Write(entry.EntryPoint);
|
||||
writer.Write(pathBytes.Length);
|
||||
writer.Write(pathBytes);
|
||||
writer.Write(sourceBytes.Length);
|
||||
writer.Write(SHA256.HashData(sourceBytes));
|
||||
writer.Write(sourceBytes);
|
||||
}
|
||||
}
|
||||
File.Move(temporary, fullPath, overwrite: true);
|
||||
}
|
||||
|
||||
public static TranslationSourceBundleWriter CreateWriter(string path, int entryCount) => new(path, entryCount, Magic, FormatVersion);
|
||||
|
||||
public static TranslationSourceBundle Read(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: false);
|
||||
if (!reader.ReadBytes(Magic.Length).SequenceEqual(Magic) || reader.ReadInt32() != FormatVersion)
|
||||
throw new InvalidDataException($"Unsupported translation source bundle '{path}'.");
|
||||
var count = reader.ReadInt32();
|
||||
if (count < 0) throw new InvalidDataException("Negative translation source bundle entry count.");
|
||||
var entries = new List<TranslationSourceBundleEntry>(count);
|
||||
var identities = new HashSet<(uint, string)>();
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var address = reader.ReadUInt32();
|
||||
var virtualPath = Encoding.UTF8.GetString(ReadBoundedBytes(reader, 1 << 20));
|
||||
var sourceLength = reader.ReadInt32();
|
||||
var hash = reader.ReadBytes(32);
|
||||
if (sourceLength < 0) throw new InvalidDataException("Negative translation source length.");
|
||||
var sourceBytes = reader.ReadBytes(sourceLength);
|
||||
if (sourceBytes.Length != sourceLength || !SHA256.HashData(sourceBytes).SequenceEqual(hash))
|
||||
throw new InvalidDataException($"Translation source 0x{address:X8} is truncated or corrupt.");
|
||||
if (!identities.Add((address, virtualPath)))
|
||||
throw new InvalidDataException($"Duplicate translation source 0x{address:X8} '{virtualPath}'.");
|
||||
entries.Add(new TranslationSourceBundleEntry(
|
||||
address,
|
||||
virtualPath,
|
||||
Encoding.UTF8.GetString(sourceBytes),
|
||||
ChecksumUtilities.ToHex(hash),
|
||||
sourceLength));
|
||||
}
|
||||
if (stream.Position != stream.Length) throw new InvalidDataException("Translation source bundle has trailing data.");
|
||||
return new TranslationSourceBundle(entries);
|
||||
}
|
||||
|
||||
private static byte[] ReadBoundedBytes(BinaryReader reader, int maximum)
|
||||
{
|
||||
var length = reader.ReadInt32();
|
||||
if (length < 0 || length > maximum) throw new InvalidDataException("Invalid translation bundle string length.");
|
||||
var bytes = reader.ReadBytes(length);
|
||||
if (bytes.Length != length) throw new EndOfStreamException();
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TranslationSourceBundleWriter : IDisposable
|
||||
{
|
||||
private readonly string _path;
|
||||
private readonly string _temporary;
|
||||
private readonly int _expectedCount;
|
||||
private readonly BinaryWriter _writer;
|
||||
private int _written;
|
||||
private bool _completed;
|
||||
private (uint Address, string Path)? _previous;
|
||||
|
||||
internal TranslationSourceBundleWriter(string path, int entryCount, byte[] magic, int version)
|
||||
{
|
||||
if (entryCount < 0) throw new ArgumentOutOfRangeException(nameof(entryCount));
|
||||
_path = Path.GetFullPath(path);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_path)!);
|
||||
SweepAbandonedTemporaries(_path);
|
||||
_temporary = $"{_path}.tmp.{Guid.NewGuid():N}";
|
||||
_expectedCount = entryCount;
|
||||
_writer = new BinaryWriter(File.Create(_temporary), Encoding.UTF8, leaveOpen: false);
|
||||
_writer.Write(magic);
|
||||
_writer.Write(version);
|
||||
_writer.Write(entryCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A killed run leaves its in-progress temporary behind, and those orphans are
|
||||
/// hundreds of megabytes each. Only the exact "target.tmp." prefix is swept, and a
|
||||
/// file another process still holds open is left alone.
|
||||
/// </summary>
|
||||
private static void SweepAbandonedTemporaries(string targetPath)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(targetPath)!;
|
||||
var prefix = Path.GetFileName(targetPath) + ".tmp.";
|
||||
// The pattern is a Win32 wildcard, so re-check the prefix explicitly and
|
||||
// snapshot the listing before deleting from it.
|
||||
foreach (var candidate in Directory.GetFiles(directory, prefix + "*"))
|
||||
{
|
||||
if (!Path.GetFileName(candidate).StartsWith(prefix, StringComparison.Ordinal)) continue;
|
||||
try
|
||||
{
|
||||
File.Delete(candidate);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(TranslationSourceBundleEntry entry)
|
||||
{
|
||||
if (_completed || _written >= _expectedCount) throw new InvalidOperationException("Translation source bundle writer is complete.");
|
||||
var virtualPath = entry.VirtualPath.Replace('\\', '/');
|
||||
if (_previous is { } previous &&
|
||||
(entry.EntryPoint < previous.Address ||
|
||||
(entry.EntryPoint == previous.Address && string.CompareOrdinal(virtualPath, previous.Path) <= 0)))
|
||||
throw new InvalidDataException("Translation source bundle entries must be written in deterministic address/path order.");
|
||||
var pathBytes = Encoding.UTF8.GetBytes(virtualPath);
|
||||
var sourceBytes = Encoding.UTF8.GetBytes(entry.Source);
|
||||
_writer.Write(entry.EntryPoint);
|
||||
_writer.Write(pathBytes.Length);
|
||||
_writer.Write(pathBytes);
|
||||
_writer.Write(sourceBytes.Length);
|
||||
_writer.Write(SHA256.HashData(sourceBytes));
|
||||
_writer.Write(sourceBytes);
|
||||
_previous = (entry.EntryPoint, virtualPath);
|
||||
_written++;
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
if (_written != _expectedCount)
|
||||
throw new InvalidOperationException($"Translation source bundle expected {_expectedCount} entries but received {_written}.");
|
||||
_writer.Dispose();
|
||||
File.Move(_temporary, _path, overwrite: true);
|
||||
_completed = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_writer.Dispose();
|
||||
if (!_completed && File.Exists(_temporary)) File.Delete(_temporary);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Collections.Generic;
|
||||
using Translator.Core.Analysis;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
/// <summary>
|
||||
/// The facts one emitted state-free interface published, mirroring the <c>// RECOMP_STATE_FREE_ABI ...</c>
|
||||
/// persistence comment so this record avoids re-parsing that text in the same process invocation.
|
||||
/// <see cref="GprInputMask"/> is the native GPR parameter mask, derived from the contract rather than stored on it.
|
||||
/// </summary>
|
||||
public sealed record GuestStateFreeEmissionFacts(
|
||||
uint EntryPoint,
|
||||
string Symbol,
|
||||
GuestAbiContract Contract,
|
||||
uint GprInputMask);
|
||||
|
||||
/// <summary>
|
||||
/// Build registration facts for one emitted function. Present only for the base
|
||||
/// (non-mod) registration form.
|
||||
/// </summary>
|
||||
public sealed record CxxBuildRegistrationFacts(
|
||||
string Symbol,
|
||||
bool PreservesNonvolatileFprs,
|
||||
uint NonvolatileFprWriteMask);
|
||||
|
||||
/// <summary>
|
||||
/// Structured result of emitting one function: the C++ text plus every fact the emitter derived, for same-process
|
||||
/// consumers. The comment markers in <see cref="Code"/> remain the cross-run persistence format for build sharding and dispatch profiles.
|
||||
/// </summary>
|
||||
public sealed record CxxEmissionResult(
|
||||
string Code,
|
||||
GuestAbiContract GuestAbiContract,
|
||||
string GuestAbiMarker,
|
||||
CxxBuildRegistrationFacts? Registration,
|
||||
IReadOnlyList<uint> EmittedDirectCallTargets,
|
||||
IReadOnlyList<uint> EmittedLocalLabelAddresses,
|
||||
GuestStateFreeEmissionFacts? StateFree,
|
||||
IReadOnlyList<GuestStateFreeEmissionFacts> StateFreeEntryVariants);
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Translator.Core.Analysis.Ssa;
|
||||
using Translator.Core.Ir;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
public sealed partial class CxxLinearCodeGenerator
|
||||
{
|
||||
private static IrFunction ElideDeadPureLocalTemps(IrFunction function)
|
||||
{
|
||||
var cfg = IrCfg.Build(function);
|
||||
var liveIn = function.Blocks.ToDictionary(
|
||||
block => block.Label,
|
||||
_ => new HashSet<string>(StringComparer.OrdinalIgnoreCase),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
var liveOut = function.Blocks.ToDictionary(
|
||||
block => block.Label,
|
||||
_ => new HashSet<string>(StringComparer.OrdinalIgnoreCase),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var changed = true;
|
||||
while (changed)
|
||||
{
|
||||
changed = false;
|
||||
for (var blockIndex = function.Blocks.Count - 1; blockIndex >= 0; blockIndex--)
|
||||
{
|
||||
var block = function.Blocks[blockIndex];
|
||||
var nextOut = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var successor in cfg.Successors(block.Label))
|
||||
{
|
||||
if (liveIn.TryGetValue(successor, out var successorIn))
|
||||
{
|
||||
nextOut.UnionWith(successorIn);
|
||||
}
|
||||
}
|
||||
|
||||
var nextIn = new HashSet<string>(nextOut, StringComparer.OrdinalIgnoreCase);
|
||||
for (var insIndex = block.Instructions.Count - 1; insIndex >= 0; insIndex--)
|
||||
{
|
||||
UpdateLivePureLocalTemps(block.Instructions[insIndex], nextIn);
|
||||
}
|
||||
|
||||
if (!nextOut.SetEquals(liveOut[block.Label]))
|
||||
{
|
||||
liveOut[block.Label] = nextOut;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!nextIn.SetEquals(liveIn[block.Label]))
|
||||
{
|
||||
liveIn[block.Label] = nextIn;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var rewrittenBlocks = new IrBasicBlock[function.Blocks.Count];
|
||||
var removedAny = false;
|
||||
|
||||
for (var blockIndex = function.Blocks.Count - 1; blockIndex >= 0; blockIndex--)
|
||||
{
|
||||
var block = function.Blocks[blockIndex];
|
||||
var rewritten = new List<IrInstruction>(block.Instructions.Count);
|
||||
var live = new HashSet<string>(liveOut[block.Label], StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
for (var insIndex = block.Instructions.Count - 1; insIndex >= 0; insIndex--)
|
||||
{
|
||||
var instruction = block.Instructions[insIndex];
|
||||
if (instruction is IrAssign assign && IsDeadPureLocalDestination(assign.Destination, live))
|
||||
{
|
||||
removedAny = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (instruction is IrBinary binary && IsDeadPureLocalDestination(binary.Destination, live))
|
||||
{
|
||||
removedAny = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
rewritten.Add(instruction);
|
||||
UpdateLivePureLocalTemps(instruction, live);
|
||||
}
|
||||
|
||||
rewritten.Reverse();
|
||||
rewrittenBlocks[blockIndex] = removedAny && rewritten.Count != block.Instructions.Count
|
||||
? block with { Instructions = rewritten }
|
||||
: block;
|
||||
}
|
||||
|
||||
return removedAny
|
||||
? function with { Blocks = rewrittenBlocks }
|
||||
: function;
|
||||
}
|
||||
|
||||
private static bool IsDeadPureLocalDestination(string destination, HashSet<string> live)
|
||||
{
|
||||
return !IsCpuRegister(destination) && !live.Contains(destination);
|
||||
}
|
||||
|
||||
private static void UpdateLivePureLocalTemps(IrInstruction instruction, HashSet<string> live)
|
||||
{
|
||||
switch (instruction)
|
||||
{
|
||||
case IrAssign assign:
|
||||
KillIfPureLocal(assign.Destination, live);
|
||||
AddIfPureLocal(assign.Value, live);
|
||||
break;
|
||||
|
||||
case IrBinary binary:
|
||||
KillIfPureLocal(binary.Destination, live);
|
||||
AddIfPureLocal(binary.Left, live);
|
||||
AddIfPureLocal(binary.Right, live);
|
||||
break;
|
||||
|
||||
case IrLoad load:
|
||||
// Preserve all memory reads. They can validate guest addresses or expose
|
||||
// unsupported hardware reads even when the loaded value is dead.
|
||||
KillIfPureLocal(load.Destination, live);
|
||||
AddIfPureLocal(load.Address.Base, live);
|
||||
break;
|
||||
|
||||
case IrStore store:
|
||||
AddIfPureLocal(store.Address.Base, live);
|
||||
AddIfPureLocal(store.Source, live);
|
||||
break;
|
||||
|
||||
case IrCall call:
|
||||
if (!string.IsNullOrWhiteSpace(call.Destination))
|
||||
{
|
||||
KillIfPureLocal(call.Destination, live);
|
||||
}
|
||||
foreach (var arg in call.Arguments)
|
||||
{
|
||||
AddIfPureLocal(arg, live);
|
||||
}
|
||||
break;
|
||||
|
||||
case IrIndirectCall indirectCall:
|
||||
if (!string.IsNullOrWhiteSpace(indirectCall.Destination))
|
||||
{
|
||||
KillIfPureLocal(indirectCall.Destination, live);
|
||||
}
|
||||
AddIfPureLocal(indirectCall.Target, live);
|
||||
foreach (var arg in indirectCall.Arguments)
|
||||
{
|
||||
AddIfPureLocal(arg, live);
|
||||
}
|
||||
break;
|
||||
|
||||
case IrSetCrField setCr:
|
||||
AddIfPureLocal(setCr.Left, live);
|
||||
AddIfPureLocal(setCr.Right, live);
|
||||
break;
|
||||
|
||||
case IrPhi phi:
|
||||
if (!IsCpuRegister(phi.Destination))
|
||||
{
|
||||
live.Remove(phi.Destination);
|
||||
}
|
||||
foreach (var source in phi.Sources.Values)
|
||||
{
|
||||
AddIfPureLocal(source, live);
|
||||
}
|
||||
break;
|
||||
|
||||
case IrBranch branch when IsCpuRegister(branch.ConditionRegister):
|
||||
AddIfPureLocal(branch.ConditionRegister, live);
|
||||
break;
|
||||
|
||||
case IrIndirectJump indirectJump:
|
||||
AddIfPureLocal(indirectJump.Target, live);
|
||||
break;
|
||||
|
||||
case IrJumpTable jumpTable:
|
||||
AddIfPureLocal(jumpTable.Selector, live);
|
||||
break;
|
||||
|
||||
case IrReturn { Value: { } value }:
|
||||
AddIfPureLocal(value, live);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddIfPureLocal(IrValue value, HashSet<string> live)
|
||||
{
|
||||
if (value.Kind == "register" && value.RegisterName != null)
|
||||
{
|
||||
AddIfPureLocal(value.RegisterName, live);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddIfPureLocal(string name, HashSet<string> live)
|
||||
{
|
||||
if (!IsCpuRegister(name))
|
||||
{
|
||||
live.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
private static void KillIfPureLocal(string name, HashSet<string> live)
|
||||
{
|
||||
if (!IsCpuRegister(name))
|
||||
{
|
||||
live.Remove(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,775 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Translator.Core.Analysis.Ssa;
|
||||
using Translator.Core.Ir;
|
||||
using Translator.Core.Analysis.Representation;
|
||||
using Translator.Core.Analysis;
|
||||
using Translator.Core.Representation;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
public sealed partial class CxxLinearCodeGenerator
|
||||
{
|
||||
private static void EmitInstruction(string currentLabel, int directCallOrdinal, IrInstruction ins, StringBuilder sb, int bufferBaseLength, int indent,
|
||||
IrCfg cfg, Dictionary<string, string> labelNames, RepresentationEnvironment types, FunctionAbiClassification signature,
|
||||
Dictionary<string, bool> localPaired, IGuestFunctionAbiProvider guestAbiProvider,
|
||||
IReadOnlyDictionary<string, uint> knownConstants,
|
||||
IReadOnlyDictionary<string, uint> localConstants,
|
||||
LinkedAddressRemap? linkedAddressRemap,
|
||||
IReadOnlySet<uint> nonReturningCallTargets,
|
||||
IReadOnlySet<uint> lrContinuationCallTargets,
|
||||
StackAddressFacts stackFacts,
|
||||
bool inlineGuestThunkStackBase,
|
||||
uint? localFallthroughLr,
|
||||
IReadOnlyDictionary<uint, GuestAbiContract> guestAbiContracts,
|
||||
IReadOnlyDictionary<uint, GuestAbiContract> stateFreeAbiContracts,
|
||||
IReadOnlyDictionary<uint, string> stateFreeCallSymbols,
|
||||
IReadOnlyDictionary<GuestStateFreeCallSiteKey, GuestStateFreeCallVariant> stateFreeCallSiteVariants,
|
||||
IReadOnlySet<uint> modOverridableCallTargets)
|
||||
{
|
||||
if (ins is IrPhi)
|
||||
{
|
||||
return; // Phi nodes are eliminated before codegen.
|
||||
}
|
||||
|
||||
var pad = IndentPad(indent);
|
||||
switch (ins)
|
||||
{
|
||||
case IrAssign assign:
|
||||
{
|
||||
var dest = ToDestination(assign.Destination, types);
|
||||
var src = ToRegisterAssignmentExpression(assign.Destination, assign.Value, types);
|
||||
sb.AppendLine($"{pad}{dest} = {src};");
|
||||
if (IsFloatRegister(assign.Destination))
|
||||
{
|
||||
var paired = false;
|
||||
if (assign.Value.Kind == "register" &&
|
||||
assign.Value.RegisterName != null &&
|
||||
IsFloatRegister(assign.Value.RegisterName))
|
||||
{
|
||||
var srcBase = GetRegisterBaseName(assign.Value.RegisterName);
|
||||
paired = localPaired.TryGetValue(srcBase, out var isPaired) && isPaired;
|
||||
}
|
||||
localPaired[GetRegisterBaseName(assign.Destination)] = paired;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IrBinary bin:
|
||||
{
|
||||
var dest = ToDestination(bin.Destination, types);
|
||||
var scalarFloatOperation = IsFloatRegister(bin.Destination);
|
||||
var left = scalarFloatOperation
|
||||
? ToScalarFloatExpression(bin.Left, types, localPaired)
|
||||
: ToExpression(bin.Left, types);
|
||||
var right = scalarFloatOperation
|
||||
? ToScalarFloatExpression(bin.Right, types, localPaired)
|
||||
: ToExpression(bin.Right, types);
|
||||
var expr = BinaryExpressionWithOperands(bin, left, right);
|
||||
sb.AppendLine($"{pad}{dest} = {expr};");
|
||||
if (IsFloatRegister(bin.Destination))
|
||||
{
|
||||
localPaired[GetRegisterBaseName(bin.Destination)] = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IrLoad load:
|
||||
{
|
||||
var dest = ToDestination(load.Destination, types);
|
||||
var destType = types.Get(load.Destination);
|
||||
var isFloatDest = (destType is ValueRepresentation pt && pt.IsFloat) ||
|
||||
IsFloatRegister(load.Destination);
|
||||
|
||||
if (TryEmitStackLoad(load, dest, isFloatDest, sb, pad, types, stackFacts))
|
||||
{
|
||||
}
|
||||
else if (isFloatDest && load.SizeBytes == 4)
|
||||
{
|
||||
sb.AppendLine($"{pad}{dest} = {GuestLoadHelper(4, isFloat: true)}({RemapAddress(load.Address, types, localConstants, linkedAddressRemap)});");
|
||||
}
|
||||
else if (isFloatDest && load.SizeBytes == 8)
|
||||
{
|
||||
sb.AppendLine($"{pad}{dest} = {GuestLoadHelper(8, isFloat: true)}({RemapAddress(load.Address, types, localConstants, linkedAddressRemap)});");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"{pad}{dest} = {GuestLoadHelper(load.SizeBytes, isFloat: false)}({RemapAddress(load.Address, types, localConstants, linkedAddressRemap)});");
|
||||
}
|
||||
if (IsFloatRegister(load.Destination))
|
||||
{
|
||||
localPaired[GetRegisterBaseName(load.Destination)] = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IrStore store:
|
||||
{
|
||||
var srcExpr = ToExpression(store.Source, types);
|
||||
var isFloatStore = store.Source.Kind == "register" &&
|
||||
store.Source.RegisterName != null &&
|
||||
IsFloatRegister(store.Source.RegisterName);
|
||||
if (isFloatStore &&
|
||||
store.SizeBytes == 4 &&
|
||||
store.Source.RegisterName != null &&
|
||||
localPaired.TryGetValue(GetRegisterBaseName(store.Source.RegisterName), out var sourceIsPaired) &&
|
||||
sourceIsPaired)
|
||||
{
|
||||
srcExpr = $"PPC_PsToScalarInline({srcExpr})";
|
||||
}
|
||||
|
||||
if (TryEmitKnownGpuFifoStore(store, srcExpr, isFloatStore, sb, pad, knownConstants))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// A translate-time-constant address outside the MMIO window (gather-pipe already handled
|
||||
// above) always takes the flat helper's plain-store arm, so the runtime MMIO test is provably dead.
|
||||
var provenRamStore =
|
||||
TryGetKnownEffectiveAddress(store.Address, knownConstants, out var knownStoreAddress) &&
|
||||
IsProvenRamStore(knownStoreAddress, store.SizeBytes);
|
||||
string StoreHelper(int sizeBytes, bool isFloat) => provenRamStore
|
||||
? FlatWriteRamHelper(Bits(sizeBytes), isFloat)
|
||||
: GuestStoreHelper(sizeBytes, isFloat);
|
||||
|
||||
if (TryEmitStackStore(store, srcExpr, isFloatStore, sb, pad, types, stackFacts))
|
||||
{
|
||||
}
|
||||
else if (isFloatStore && store.SizeBytes == 4)
|
||||
{
|
||||
sb.AppendLine($"{pad}{StoreHelper(4, isFloat: true)}({RemapAddress(store.Address, types, localConstants, linkedAddressRemap)}, {srcExpr});");
|
||||
}
|
||||
else if (isFloatStore && store.SizeBytes == 8)
|
||||
{
|
||||
sb.AppendLine($"{pad}{StoreHelper(8, isFloat: true)}({RemapAddress(store.Address, types, localConstants, linkedAddressRemap)}, {srcExpr});");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Memory::Write8/Write16 take uint8_t/uint16_t, so cast to
|
||||
// the exact parameter type instead of widening to
|
||||
// uint32_t and letting the call narrow it again. The
|
||||
// stored bytes are identical either way. Write64 keeps a
|
||||
// 32-bit cast: it has no 64-bit integer producer here.
|
||||
var integerSource = store.SizeBytes switch
|
||||
{
|
||||
4 => srcExpr,
|
||||
1 or 2 => $"static_cast<uint{Bits(store.SizeBytes)}_t>({srcExpr})",
|
||||
_ => $"static_cast<uint32_t>({srcExpr})"
|
||||
};
|
||||
sb.AppendLine($"{pad}{StoreHelper(store.SizeBytes, isFloat: false)}({RemapAddress(store.Address, types, localConstants, linkedAddressRemap)}, {integerSource});");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IrResolveGuestMemoryRange resolve:
|
||||
{
|
||||
var start = RemapAddress(
|
||||
new IrAddress(resolve.Base.RegisterName!, resolve.MinOffset),
|
||||
types, localConstants, linkedAddressRemap);
|
||||
sb.AppendLine($"{pad}{resolve.Destination} = MemoryInline::ResolveRangeHost({start}, 0, {resolve.Length}u, {(resolve.NeedsReadAccess ? "true" : "false")}, {(resolve.NeedsWriteAccess ? "true" : "false")});");
|
||||
}
|
||||
break;
|
||||
case IrResolvedLoad load:
|
||||
{
|
||||
var dest = ToDestination(load.Destination, types);
|
||||
var destType = types.Get(load.Destination);
|
||||
var isFloatDest = (destType is ValueRepresentation pt && pt.IsFloat) || IsFloatRegister(load.Destination);
|
||||
var address = RemapAddress(load.OriginalAddress, types, localConstants, linkedAddressRemap);
|
||||
var helper = isFloatDest ? $"Float{Bits(load.SizeBytes)}" : Bits(load.SizeBytes).ToString();
|
||||
sb.AppendLine($"{pad}{dest} = MemoryInline::ReadResolved{helper}({load.Range}, {load.RangeOffset}u, {address});");
|
||||
if (IsFloatRegister(load.Destination))
|
||||
localPaired[GetRegisterBaseName(load.Destination)] = false;
|
||||
}
|
||||
break;
|
||||
case IrResolvedStore store:
|
||||
{
|
||||
var srcExpr = ToExpression(store.Source, types);
|
||||
var isFloatStore = store.Source.Kind == "register" && store.Source.RegisterName is not null &&
|
||||
IsFloatRegister(store.Source.RegisterName);
|
||||
if (isFloatStore && store.SizeBytes == 4 && store.Source.RegisterName is not null &&
|
||||
localPaired.TryGetValue(GetRegisterBaseName(store.Source.RegisterName), out var sourceIsPaired) && sourceIsPaired)
|
||||
srcExpr = $"PPC_PsToScalarInline({srcExpr})";
|
||||
var address = RemapAddress(store.OriginalAddress, types, localConstants, linkedAddressRemap);
|
||||
var helper = isFloatStore ? $"Float{Bits(store.SizeBytes)}" : Bits(store.SizeBytes).ToString();
|
||||
var value = isFloatStore || store.SizeBytes == 4 ? srcExpr : $"static_cast<uint{Bits(store.SizeBytes)}_t>({srcExpr})";
|
||||
sb.AppendLine($"{pad}MemoryInline::WriteResolved{helper}({store.Range}, {store.RangeOffset}u, {address}, {value});");
|
||||
}
|
||||
break;
|
||||
case IrResolvedPsqLoad load:
|
||||
{
|
||||
var helper = load.KnownGqr.HasValue
|
||||
? "PPC_PsqLKnownResolvedInline"
|
||||
: "PPC_PsqLResolvedInline";
|
||||
var known = load.KnownGqr.HasValue ? $", 0x{load.KnownGqr.Value:X8}u" : string.Empty;
|
||||
var template = $"<{load.W}u, {load.I}u{known}>";
|
||||
var address = ToExpression(load.OriginalAddress, types);
|
||||
var knownCall = $"{helper}{template}(ctx, {load.Range}, {load.RangeOffset}u, {address})";
|
||||
if (load.GuardKnownGqr && load.KnownGqr.HasValue)
|
||||
{
|
||||
var fallback = $"PPC_PsqLResolvedInline<{load.W}u, {load.I}u>(ctx, {load.Range}, {load.RangeOffset}u, {address})";
|
||||
sb.AppendLine($"{pad}if ({GqrEntryGuardName(load.I, load.KnownGqr.Value)}) {{");
|
||||
sb.AppendLine($"{pad} {PairedAssignment(load.Destination, knownCall, types)}");
|
||||
sb.AppendLine($"{pad}}} else {{");
|
||||
sb.AppendLine($"{pad} {PairedAssignment(load.Destination, fallback, types)}");
|
||||
sb.AppendLine($"{pad}}}");
|
||||
}
|
||||
else sb.AppendLine($"{pad}{PairedAssignment(load.Destination, knownCall, types)}");
|
||||
if (IsFloatRegister(load.Destination))
|
||||
localPaired[GetRegisterBaseName(load.Destination)] = true;
|
||||
}
|
||||
break;
|
||||
case IrResolvedPsqStore store:
|
||||
{
|
||||
var helper = store.KnownGqr.HasValue
|
||||
? "PPC_PsqStKnownResolvedInline"
|
||||
: "PPC_PsqStResolvedInline";
|
||||
var known = store.KnownGqr.HasValue ? $", 0x{store.KnownGqr.Value:X8}u" : string.Empty;
|
||||
var template = $"<{store.W}u, {store.I}u{known}>";
|
||||
var value = ToPairedFloatExpression(store.Source, types, localPaired);
|
||||
var address = ToExpression(store.OriginalAddress, types);
|
||||
var knownCall = $"{helper}{template}(ctx, {store.Range}, {store.RangeOffset}u, {address}, {value})";
|
||||
if (store.GuardKnownGqr && store.KnownGqr.HasValue)
|
||||
{
|
||||
sb.AppendLine($"{pad}if ({GqrEntryGuardName(store.I, store.KnownGqr.Value)}) {{");
|
||||
sb.AppendLine($"{pad} {knownCall};");
|
||||
sb.AppendLine($"{pad}}} else {{");
|
||||
sb.AppendLine($"{pad} PPC_PsqStResolvedInline<{store.W}u, {store.I}u>(ctx, {store.Range}, {store.RangeOffset}u, {address}, {value});");
|
||||
sb.AppendLine($"{pad}}}");
|
||||
}
|
||||
else sb.AppendLine($"{pad}{knownCall};");
|
||||
}
|
||||
break;
|
||||
case IrResolvedLoadPair pair:
|
||||
{
|
||||
bool IsFloat(string destination) =>
|
||||
(types.Get(destination) is ValueRepresentation primitive && primitive.IsFloat) || IsFloatRegister(destination);
|
||||
var firstFloat = IsFloat(pair.FirstDestination);
|
||||
var secondFloat = IsFloat(pair.SecondDestination);
|
||||
var firstAddress = RemapAddress(pair.FirstOriginalAddress, types, localConstants, linkedAddressRemap);
|
||||
var secondAddress = RemapAddress(pair.SecondOriginalAddress, types, localConstants, linkedAddressRemap);
|
||||
var pairHelper = pair.ElementSizeBytes == 2
|
||||
? $"MemoryInline::ReadResolvedPair16({pair.Range}, {pair.RangeOffset}u)"
|
||||
: $"MemoryInline::ReadResolvedPair32({pair.Range}, {pair.RangeOffset}u)";
|
||||
string FastValue(string member, bool isFloat) => isFloat ? $"PpcBitCastToFloatInline(resolved_pair.{member})" : $"resolved_pair.{member}";
|
||||
string SlowHelper(bool isFloat) => isFloat ? $"Float{Bits(pair.ElementSizeBytes)}" : Bits(pair.ElementSizeBytes).ToString();
|
||||
var firstMember = pair.Descending ? "second" : "first";
|
||||
var secondMember = pair.Descending ? "first" : "second";
|
||||
var firstOffset = pair.Descending ? pair.RangeOffset + pair.ElementSizeBytes : pair.RangeOffset;
|
||||
var secondOffset = pair.Descending ? pair.RangeOffset : pair.RangeOffset + pair.ElementSizeBytes;
|
||||
sb.AppendLine($"{pad}{{");
|
||||
sb.AppendLine($"{pad} const auto resolved_pair = {pairHelper};");
|
||||
sb.AppendLine($"{pad} if (resolved_pair.valid) {{");
|
||||
sb.AppendLine($"{pad} {ToDestination(pair.FirstDestination, types)} = {FastValue(firstMember, firstFloat)};");
|
||||
sb.AppendLine($"{pad} {ToDestination(pair.SecondDestination, types)} = {FastValue(secondMember, secondFloat)};");
|
||||
sb.AppendLine($"{pad} }} else {{");
|
||||
sb.AppendLine($"{pad} {ToDestination(pair.FirstDestination, types)} = MemoryInline::ReadResolved{SlowHelper(firstFloat)}({pair.Range}, {firstOffset}u, {firstAddress});");
|
||||
sb.AppendLine($"{pad} {ToDestination(pair.SecondDestination, types)} = MemoryInline::ReadResolved{SlowHelper(secondFloat)}({pair.Range}, {secondOffset}u, {secondAddress});");
|
||||
sb.AppendLine($"{pad} }}");
|
||||
sb.AppendLine($"{pad}}}");
|
||||
if (IsFloatRegister(pair.FirstDestination)) localPaired[GetRegisterBaseName(pair.FirstDestination)] = false;
|
||||
if (IsFloatRegister(pair.SecondDestination)) localPaired[GetRegisterBaseName(pair.SecondDestination)] = false;
|
||||
}
|
||||
break;
|
||||
case IrResolvedStorePair pair:
|
||||
{
|
||||
bool IsFloat(IrValue source) => source.RegisterName is { } name && IsFloatRegister(name);
|
||||
string Source(IrValue source)
|
||||
{
|
||||
var expression = ToExpression(source, types);
|
||||
if (pair.ElementSizeBytes == 4 && source.RegisterName is { } name && IsFloatRegister(name) &&
|
||||
localPaired.TryGetValue(GetRegisterBaseName(name), out var paired) && paired)
|
||||
expression = $"PPC_PsToScalarInline({expression})";
|
||||
return expression;
|
||||
}
|
||||
var firstFloat = IsFloat(pair.FirstSource);
|
||||
var secondFloat = IsFloat(pair.SecondSource);
|
||||
var first = Source(pair.FirstSource);
|
||||
var second = Source(pair.SecondSource);
|
||||
string Raw(string value, bool isFloat) => isFloat
|
||||
? $"PpcBitCastToU32Inline(static_cast<float>({value}))"
|
||||
: $"static_cast<uint32_t>({value})";
|
||||
var lowSource = pair.Descending ? second : first;
|
||||
var highSource = pair.Descending ? first : second;
|
||||
var lowFloat = pair.Descending ? secondFloat : firstFloat;
|
||||
var highFloat = pair.Descending ? firstFloat : secondFloat;
|
||||
var packed = pair.ElementSizeBytes == 2
|
||||
? $"((static_cast<uint32_t>(static_cast<uint16_t>({lowSource})) << 16) | static_cast<uint16_t>({highSource}))"
|
||||
: $"((static_cast<uint64_t>({Raw(lowSource, lowFloat)}) << 32) | {Raw(highSource, highFloat)})";
|
||||
var firstAddress = RemapAddress(pair.FirstOriginalAddress, types, localConstants, linkedAddressRemap);
|
||||
var secondAddress = RemapAddress(pair.SecondOriginalAddress, types, localConstants, linkedAddressRemap);
|
||||
var fast = pair.ElementSizeBytes == 2
|
||||
? $"MemoryInline::WriteResolvedPair16({pair.Range}, {pair.RangeOffset}u, {packed})"
|
||||
: $"MemoryInline::WriteResolvedPair32({pair.Range}, {pair.RangeOffset}u, {packed})";
|
||||
string Helper(bool isFloat) => isFloat ? $"Float{Bits(pair.ElementSizeBytes)}" : Bits(pair.ElementSizeBytes).ToString();
|
||||
var firstOffset = pair.Descending ? pair.RangeOffset + pair.ElementSizeBytes : pair.RangeOffset;
|
||||
var secondOffset = pair.Descending ? pair.RangeOffset : pair.RangeOffset + pair.ElementSizeBytes;
|
||||
sb.AppendLine($"{pad}if (!{fast}) {{");
|
||||
sb.AppendLine($"{pad} MemoryInline::WriteResolved{Helper(firstFloat)}({pair.Range}, {firstOffset}u, {firstAddress}, {first});");
|
||||
sb.AppendLine($"{pad} MemoryInline::WriteResolved{Helper(secondFloat)}({pair.Range}, {secondOffset}u, {secondAddress}, {second});");
|
||||
sb.AppendLine($"{pad}}}");
|
||||
}
|
||||
break;
|
||||
case IrCall call:
|
||||
if (TryParseAddress(call.Target, out var addr))
|
||||
{
|
||||
if (TryEmitInlineGuestThunk(addr, sb, pad, types, inlineGuestThunkStackBase))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Guest call boundary: CpuContext becomes authoritative for the callee. Argument normalization
|
||||
// and state-free marshalling both read/write CpuContext directly, so the flush choice below
|
||||
// can't be narrowed when either is present; normalization is built into its own buffer first
|
||||
// so the emit order stays fixed.
|
||||
var argumentNormalization = new StringBuilder();
|
||||
EmitNormalizeGuestCallFloatArguments(
|
||||
call.Target, call.Arguments, argumentNormalization, pad, localPaired, guestAbiProvider);
|
||||
var callSiteKey = new GuestStateFreeCallSiteKey(currentLabel, addr, directCallOrdinal);
|
||||
var hasCallSiteVariant = stateFreeCallSiteVariants.TryGetValue(callSiteKey, out var callSiteVariant);
|
||||
var stateFreeSymbol = hasCallSiteVariant
|
||||
? callSiteVariant!.Symbol
|
||||
: stateFreeCallSymbols.GetValueOrDefault(addr);
|
||||
var stateFreeContract = hasCallSiteVariant
|
||||
? callSiteVariant!.Contract
|
||||
: stateFreeAbiContracts.GetValueOrDefault(addr);
|
||||
var usesStateFreeCall = stateFreeSymbol is not null && stateFreeContract is not null &&
|
||||
IsStateFreeCallSafe(stateFreeContract);
|
||||
var resumesThroughLinkRegister =
|
||||
nonReturningCallTargets.Contains(addr) ||
|
||||
(lrContinuationCallTargets.Contains(addr) && localFallthroughLr.HasValue);
|
||||
// The explicit-state signature carries everything the callee reads/writes, so a resident caller
|
||||
// can pass host registers instead of spilling to CpuContext. Excluded: paired-single arg/return
|
||||
// normalization rewrites ctx->fpr[N] directly, and LR-continuation sites dispatch on ctx->lr
|
||||
// after the callee writes it, so those keep the full boundary.
|
||||
var marshalsStateFreeThroughResidency =
|
||||
usesStateFreeCall &&
|
||||
_activeResidency is not null &&
|
||||
argumentNormalization.Length == 0 &&
|
||||
!resumesThroughLinkRegister &&
|
||||
!IsGuestPairedScalarFloatReturn(guestAbiProvider, call.Target);
|
||||
if (marshalsStateFreeThroughResidency)
|
||||
{
|
||||
EmitResidentStateFreeCall(
|
||||
sb, pad, bufferBaseLength, addr, stateFreeSymbol!, stateFreeContract!,
|
||||
localFallthroughLr, stateFreeAbiContracts, stateFreeCallSymbols);
|
||||
// Emits nothing for this shape (the paired-scalar return
|
||||
// case is excluded above); it still owns the f1
|
||||
// representation tag every guest call boundary applies.
|
||||
EmitNormalizeGuestCallFloatReturn(call.Target, sb, pad, localPaired, guestAbiProvider);
|
||||
break;
|
||||
}
|
||||
var boundarySync = usesStateFreeCall || argumentNormalization.Length != 0
|
||||
? ResidencyBoundarySync.Full
|
||||
: ResolveDirectCallBoundarySync(addr, guestAbiContracts, modOverridableCallTargets);
|
||||
if (!boundarySync.IsFull &&
|
||||
IsGuestPairedScalarFloatReturn(guestAbiProvider, call.Target))
|
||||
{
|
||||
// The return normalization below rewrites ctx->fpr[1]
|
||||
// directly, so f1 has to be part of this boundary even if
|
||||
// the callee contract does not mention it.
|
||||
boundarySync = boundarySync.WithFpr(1);
|
||||
}
|
||||
AppendFlush(sb, pad, boundarySync);
|
||||
sb.Append(argumentNormalization);
|
||||
if (usesStateFreeCall)
|
||||
{
|
||||
sb.AppendLine($"{pad}if ({StateFreeAvailabilityCondition(addr, stateFreeAbiContracts, stateFreeCallSymbols)}) {{");
|
||||
if (StateFreeHasOutputs(stateFreeContract!))
|
||||
{
|
||||
// Uniquified by the call site's position in the
|
||||
// translation unit. The body is built in its own
|
||||
// buffer, so the offset of that buffer inside the
|
||||
// unit has to be added back.
|
||||
var stateFreeResult = $"state_free_result_{addr:X8}_{bufferBaseLength + sb.Length:X}";
|
||||
sb.AppendLine($"{pad} const auto {stateFreeResult} = {stateFreeSymbol}({StateFreeCallArguments(stateFreeContract!, localFallthroughLr)});");
|
||||
AppendStateFreeResultStores(sb, pad + " ", addr, stateFreeContract!, stateFreeResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"{pad} {stateFreeSymbol}({StateFreeCallArguments(stateFreeContract!, localFallthroughLr)});");
|
||||
}
|
||||
sb.AppendLine($"{pad}}} else {{");
|
||||
RecordEmittedDirectCallTarget(addr);
|
||||
sb.AppendLine($"{pad} InvokeDirectCpu<0x{addr:X8}u>(ctx);");
|
||||
sb.AppendLine($"{pad}}}");
|
||||
}
|
||||
else
|
||||
{
|
||||
RecordEmittedDirectCallTarget(addr);
|
||||
sb.AppendLine($"{pad}InvokeDirectCpu<0x{addr:X8}u>(ctx);");
|
||||
}
|
||||
if (resumesThroughLinkRegister)
|
||||
{
|
||||
// The callee may resume this body through a local LR
|
||||
// continuation label, so the locals must be refreshed
|
||||
// from the context it just wrote. The exit paths below
|
||||
// return without writing locals back.
|
||||
AppendReload(sb, pad, boundarySync);
|
||||
var fallbackPad = pad;
|
||||
if (localFallthroughLr.HasValue)
|
||||
{
|
||||
sb.AppendLine($"{pad}if (ctx->lr != 0x{localFallthroughLr.Value:X8}u) {{");
|
||||
fallbackPad = IndentPad(indent + 1);
|
||||
}
|
||||
|
||||
EmitLocalLrContinuationDispatch(sb, fallbackPad, labelNames);
|
||||
sb.AppendLine($"{fallbackPad}if (TranslatedFunctionRegistry::FindByAddressPtr(ctx->lr) != nullptr) {{");
|
||||
sb.AppendLine($"{fallbackPad} InvokeIndirectCpu(ctx->lr, ctx);");
|
||||
sb.AppendLine($"{fallbackPad}}}");
|
||||
sb.AppendLine($"{fallbackPad}return;");
|
||||
if (localFallthroughLr.HasValue)
|
||||
{
|
||||
sb.AppendLine($"{pad}}}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
EmitNormalizeGuestCallFloatReturn(call.Target, sb, pad, localPaired, guestAbiProvider);
|
||||
AppendReload(sb, pad, boundarySync);
|
||||
}
|
||||
else if (IsGuestCallTarget(call.Target))
|
||||
{
|
||||
var targetSymbol = FormatSymbol(call.Target);
|
||||
AppendFlush(sb, pad);
|
||||
EmitNormalizeGuestCallFloatArguments(call.Target, call.Arguments, sb, pad, localPaired, guestAbiProvider);
|
||||
sb.AppendLine($"{pad}{targetSymbol}(ctx);");
|
||||
EmitNormalizeGuestCallFloatReturn(call.Target, sb, pad, localPaired, guestAbiProvider);
|
||||
AppendReload(sb, pad);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try inline expansion first for performance
|
||||
if (TryEmitInlinePpc(call, sb, pad, types, localPaired, stackFacts))
|
||||
{
|
||||
// Inline code was emitted - update paired tracking
|
||||
if (!string.IsNullOrWhiteSpace(call.Destination) && IsFloatRegister(call.Destination))
|
||||
{
|
||||
localPaired[GetRegisterBaseName(call.Destination)] = IsPairedProducerTarget(call.Target);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Helper function call (e.g., PPC_Ps*, memory helpers)
|
||||
var isPairedConsumer = IsPairedConsumerTarget(call.Target);
|
||||
var isSingleConsumer = IsSinglePrecisionConsumerTarget(call.Target);
|
||||
var isScalarConsumer = IsScalarFloatConsumerTarget(call.Target);
|
||||
var args = new List<string>(call.Arguments.Count);
|
||||
for (var argIndex = 0; argIndex < call.Arguments.Count; argIndex++)
|
||||
{
|
||||
var argVal = call.Arguments[argIndex];
|
||||
var expr = isPairedConsumer
|
||||
? ToPairedFloatExpression(argVal, types, localPaired)
|
||||
: (isSingleConsumer || isScalarConsumer)
|
||||
? ToScalarFloatExpression(argVal, types, localPaired)
|
||||
: ToExpression(argVal, types);
|
||||
args.Add(expr);
|
||||
}
|
||||
// Helpers can implicitly touch guest state through CpuContext (PPC_Stwcx writes CR0, PPC_Lswi
|
||||
// writes GPRs, OSSystemCall is a full boundary). Resident locals can't observe that, so the
|
||||
// cataloged effect becomes a mini boundary: flush what it may read, reload what it may write.
|
||||
var helperSync = _activeResidency is not null
|
||||
? ResidencyBoundarySync.FromHelperEffect(AnalyzeGuestHelperEffect(call))
|
||||
: null;
|
||||
if (helperSync is not null && !helperSync.IsEmpty)
|
||||
{
|
||||
AppendFlush(sb, pad, helperSync);
|
||||
}
|
||||
var callExpr = $"{FormatSymbol(call.Target)}({string.Join(", ", args)})";
|
||||
if (!string.IsNullOrWhiteSpace(call.Destination))
|
||||
{
|
||||
sb.AppendLine(IsPairedProducerTarget(call.Target)
|
||||
? $"{pad}{PairedAssignment(call.Destination, callExpr, types)}"
|
||||
: $"{pad}{ToDestination(call.Destination, types)} = {callExpr};");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"{pad}{callExpr};");
|
||||
}
|
||||
if (helperSync is not null && !helperSync.IsEmpty)
|
||||
{
|
||||
// An explicit destination carries the helper's return
|
||||
// value in the local; reloading it from CpuContext would
|
||||
// clobber that result with stale state.
|
||||
var reloadSync = ExcludeDestinationFromReload(helperSync, call.Destination);
|
||||
AppendReload(sb, pad, reloadSync);
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(call.Destination) && IsFloatRegister(call.Destination))
|
||||
{
|
||||
localPaired[GetRegisterBaseName(call.Destination)] = IsPairedProducerTarget(call.Target);
|
||||
}
|
||||
break;
|
||||
case IrIndirectCall icall:
|
||||
{
|
||||
var indirectTarget = ToExpression(icall.Target, types);
|
||||
AppendFlush(sb, pad);
|
||||
sb.AppendLine($"{pad}InvokeIndirectCpu({indirectTarget}, ctx);");
|
||||
AppendReload(sb, pad);
|
||||
// f1 is the conventional scalar floating-point return.
|
||||
// Do not retag the other volatile FPRs: a callee that leaves
|
||||
// one untouched also leaves its packed payload untouched.
|
||||
localPaired["f1"] = false;
|
||||
}
|
||||
break;
|
||||
case IrIndirectJump ijump:
|
||||
{
|
||||
var jumpTarget = ToExpression(ijump.Target, types);
|
||||
// Tail dispatch: the callee inherits authoritative context
|
||||
// and this frame never observes registers again, so the
|
||||
// locals must not be written back afterwards.
|
||||
AppendFlush(sb, pad);
|
||||
sb.AppendLine($"{pad}InvokeIndirectJump({jumpTarget}, ctx);");
|
||||
sb.AppendLine($"{pad}return;");
|
||||
}
|
||||
break;
|
||||
case IrComment comment:
|
||||
{
|
||||
// Emit as a C++ comment - these are legitimate comments like nop, mtcrf no-op, psq operations
|
||||
sb.AppendLine($"{pad}// {EscapeForCxxLiteral(comment.Text)}");
|
||||
}
|
||||
break;
|
||||
case IrTracePpc trace:
|
||||
{
|
||||
}
|
||||
break;
|
||||
case IrUndefined undef:
|
||||
{
|
||||
var details = undef.Reason == null
|
||||
? $"{undef.Disassembly}"
|
||||
: $"{undef.Disassembly} | {undef.Reason}";
|
||||
var escaped = EscapeForCxxLiteral(details);
|
||||
sb.AppendLine($"{pad}UNDEFINED(0x{undef.Address:X8}u, 0x{undef.RawInstruction:X8}u, \"{escaped}\");");
|
||||
}
|
||||
break;
|
||||
case IrSetCrField setCr:
|
||||
{
|
||||
var leftExpr = ToExpression(setCr.Left, types);
|
||||
var rightExpr = ToExpression(setCr.Right, types);
|
||||
// The resident helpers take the CR by reference and XER by
|
||||
// value. Both are localized, so the summary-overflow copy
|
||||
// reads a host register instead of reloading CpuContext
|
||||
// after every potentially aliasing guest store.
|
||||
if (IsFloatValue(setCr.Left, types) || IsFloatValue(setCr.Right, types))
|
||||
{
|
||||
leftExpr = ToScalarFloatExpression(setCr.Left, types, localPaired);
|
||||
rightExpr = ToScalarFloatExpression(setCr.Right, types, localPaired);
|
||||
sb.AppendLine(_activeResidency is null
|
||||
? $"{pad}SetCRFloat(ctx, {setCr.FieldIndex}, {leftExpr}, {rightExpr});"
|
||||
: $"{pad}SetCRFloatResident({_activeResidency.Cr(written: true)}, {setCr.FieldIndex}, {leftExpr}, {rightExpr});");
|
||||
}
|
||||
else
|
||||
{
|
||||
var leftCast = setCr.IsUnsigned
|
||||
? $"static_cast<uint32_t>({leftExpr})"
|
||||
: $"static_cast<int32_t>({leftExpr})";
|
||||
var rightCast = setCr.IsUnsigned
|
||||
? $"static_cast<uint32_t>({rightExpr})"
|
||||
: $"static_cast<int32_t>({rightExpr})";
|
||||
sb.AppendLine(_activeResidency is null
|
||||
? $"{pad}SetCR(ctx, {setCr.FieldIndex}, {leftCast}, {rightCast});"
|
||||
: $"{pad}SetCRResident({_activeResidency.Cr(written: true)}, {_activeResidency.Xer(written: false)}, {setCr.FieldIndex}, {leftCast}, {rightCast});");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryEmitStackLoad(
|
||||
IrLoad load,
|
||||
string dest,
|
||||
bool isFloatDest,
|
||||
StringBuilder sb,
|
||||
string pad,
|
||||
RepresentationEnvironment types,
|
||||
StackAddressFacts stackFacts)
|
||||
{
|
||||
if (!stackFacts.TryResolve(load.Address, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var address = Address(load.Address, types);
|
||||
if (isFloatDest && load.SizeBytes == 4)
|
||||
{
|
||||
sb.AppendLine($"{pad}{dest} = {StackLoadHelper(4, isFloat: true)}({address});");
|
||||
return true;
|
||||
}
|
||||
if (isFloatDest && load.SizeBytes == 8)
|
||||
{
|
||||
sb.AppendLine($"{pad}{dest} = {StackLoadHelper(8, isFloat: true)}({address});");
|
||||
return true;
|
||||
}
|
||||
|
||||
sb.AppendLine($"{pad}{dest} = {StackLoadHelper(load.SizeBytes, isFloat: false)}({address});");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryEmitStackStore(
|
||||
IrStore store,
|
||||
string srcExpr,
|
||||
bool isFloatStore,
|
||||
StringBuilder sb,
|
||||
string pad,
|
||||
RepresentationEnvironment types,
|
||||
StackAddressFacts stackFacts)
|
||||
{
|
||||
if (!stackFacts.TryResolve(store.Address, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var address = Address(store.Address, types);
|
||||
if (isFloatStore && store.SizeBytes == 4)
|
||||
{
|
||||
sb.AppendLine($"{pad}{StackStoreHelper(4, isFloat: true)}({address}, {srcExpr});");
|
||||
return true;
|
||||
}
|
||||
if (isFloatStore && store.SizeBytes == 8)
|
||||
{
|
||||
sb.AppendLine($"{pad}{StackStoreHelper(8, isFloat: true)}({address}, {srcExpr});");
|
||||
return true;
|
||||
}
|
||||
|
||||
var bits = Bits(store.SizeBytes);
|
||||
var integerSource = bits == 32 ? srcExpr : $"static_cast<uint{bits}_t>({srcExpr})";
|
||||
sb.AppendLine($"{pad}{StackStoreHelper(store.SizeBytes, isFloat: false)}({address}, {integerSource});");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryEmitKnownGpuFifoStore(
|
||||
IrStore store,
|
||||
string srcExpr,
|
||||
bool isFloatStore,
|
||||
StringBuilder sb,
|
||||
string pad,
|
||||
IReadOnlyDictionary<string, uint> knownConstants)
|
||||
{
|
||||
if (!TryGetKnownGpuFifoStore(store, isFloatStore, knownConstants, out var fifoStore))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isFloatStore)
|
||||
{
|
||||
if (_activeGpuFifoBurstSlot is { } floatSlot)
|
||||
{
|
||||
EmitGpuFifoBurstSlotStore(sb, pad, floatSlot, srcExpr);
|
||||
return true;
|
||||
}
|
||||
|
||||
sb.AppendLine($"{pad}GX_HLE_FIFO_WriteFloat(static_cast<float>({srcExpr}));");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_activeGpuFifoBurstSlot is { } slot)
|
||||
{
|
||||
EmitGpuFifoBurstSlotStore(sb, pad, slot, srcExpr);
|
||||
return true;
|
||||
}
|
||||
|
||||
var helper = fifoStore.ByteLength switch
|
||||
{
|
||||
1 => "GX_HLE_FIFO_Write8",
|
||||
2 => "GX_HLE_FIFO_Write16",
|
||||
4 => "GX_HLE_FIFO_Write32",
|
||||
_ => throw new InvalidOperationException($"Unexpected GPU FIFO width {fifoStore.ByteLength}.")
|
||||
};
|
||||
sb.AppendLine($"{pad}{helper}(static_cast<uint{fifoStore.ByteLength * 8}_t>({srcExpr}));");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void EmitLocalLrContinuationDispatch(
|
||||
StringBuilder sb,
|
||||
string pad,
|
||||
IReadOnlyDictionary<string, string> labelNames)
|
||||
{
|
||||
var localContinuations = labelNames
|
||||
.Where(static pair => TryParseAddress(pair.Key, out _))
|
||||
.Select(static pair =>
|
||||
{
|
||||
TryParseAddress(pair.Key, out var address);
|
||||
return (Address: address, Label: pair.Value);
|
||||
})
|
||||
.OrderBy(static item => item.Address)
|
||||
.ToArray();
|
||||
|
||||
if (localContinuations.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sb.AppendLine($"{pad}switch (ctx->lr) {{");
|
||||
foreach (var (address, label) in localContinuations)
|
||||
{
|
||||
RecordEmittedLocalLabel(label);
|
||||
sb.AppendLine($"{pad}case 0x{address:X8}u:");
|
||||
sb.AppendLine($"{pad} goto {label};");
|
||||
}
|
||||
|
||||
sb.AppendLine($"{pad}default:");
|
||||
sb.AppendLine($"{pad} break;");
|
||||
sb.AppendLine($"{pad}}}");
|
||||
}
|
||||
|
||||
private static bool TryGetKnownEffectiveAddress(
|
||||
IrAddress address,
|
||||
IReadOnlyDictionary<string, uint> knownConstants,
|
||||
out uint effectiveAddress)
|
||||
{
|
||||
if (knownConstants.TryGetValue(address.Base, out var baseAddress))
|
||||
{
|
||||
effectiveAddress = unchecked(baseAddress + (uint)address.Offset);
|
||||
return true;
|
||||
}
|
||||
|
||||
effectiveAddress = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsGpuFifoAddress(uint address) =>
|
||||
address >= 0xCC008000u && address < 0xCC008100u;
|
||||
|
||||
private static void EmitNormalizeGuestCallFloatArguments(
|
||||
string target,
|
||||
IReadOnlyList<IrValue> arguments,
|
||||
StringBuilder sb,
|
||||
string pad,
|
||||
Dictionary<string, bool> localPaired,
|
||||
IGuestFunctionAbiProvider guestAbiProvider)
|
||||
{
|
||||
foreach (var arg in arguments)
|
||||
{
|
||||
if (arg.Kind != "register" || arg.RegisterName == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var baseName = GetRegisterBaseName(arg.RegisterName);
|
||||
if (!localPaired.TryGetValue(baseName, out var isPaired) ||
|
||||
!isPaired ||
|
||||
!IsGuestScalarFloatArgument(guestAbiProvider, target, baseName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var regIndex = ParseFloatRegisterIndex(baseName);
|
||||
sb.AppendLine($"{pad}ctx->fpr[{regIndex}].d = PPC_PsToScalarInline(ctx->fpr[{regIndex}].d);");
|
||||
localPaired[baseName] = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void EmitNormalizeGuestCallFloatReturn(
|
||||
string target,
|
||||
StringBuilder sb,
|
||||
string pad,
|
||||
Dictionary<string, bool> localPaired,
|
||||
IGuestFunctionAbiProvider guestAbiProvider)
|
||||
{
|
||||
if (IsGuestPairedScalarFloatReturn(guestAbiProvider, target))
|
||||
{
|
||||
sb.AppendLine($"{pad}ctx->fpr[1].d = PPC_PsToScalarInline(ctx->fpr[1].d);");
|
||||
}
|
||||
|
||||
// f1 is the conventional scalar floating-point return. Preserve the
|
||||
// representation tags for f2-f13: ABI volatility does not mean that an
|
||||
// untouched packed payload has magically become a scalar value.
|
||||
localPaired["f1"] = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
using System;
|
||||
using Translator.Core.Ir;
|
||||
using Translator.Core.Analysis.Representation;
|
||||
using Translator.Core.Representation;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
public sealed partial class CxxLinearCodeGenerator
|
||||
{
|
||||
private static string BinaryExpressionWithOperands(IrBinary bin, string left, string right)
|
||||
{
|
||||
if (bin.Op == "sext")
|
||||
{
|
||||
var widthBits = bin.Right.Constant.GetValueOrDefault(8);
|
||||
return widthBits switch
|
||||
{
|
||||
16 => $"(static_cast<int32_t>(static_cast<int16_t>({left})))",
|
||||
8 => $"(static_cast<int32_t>(static_cast<int8_t>({left})))",
|
||||
_ => $"(static_cast<int32_t>({left}))",
|
||||
};
|
||||
}
|
||||
|
||||
return bin.Op switch
|
||||
{
|
||||
"add" => $"({left} + {right})",
|
||||
"sub" => $"({left} - {right})",
|
||||
"and" => $"({left} & {right})",
|
||||
"andc" => $"({left} & ~{right})",
|
||||
"or" => $"({left} | {right})",
|
||||
"orc" => $"({left} | ~{right})",
|
||||
"fdiv" => $"({left} / {right})",
|
||||
"nor" => $"~({left} | {right})",
|
||||
"nand" => $"~({left} & {right})",
|
||||
"eqv" => $"~({left} ^ {right})",
|
||||
"xor" => $"({left} ^ {right})",
|
||||
"shl" => $"({left} << {right})",
|
||||
"rotl" => $"PpcRotl32Inline(static_cast<uint32_t>({left}), static_cast<uint32_t>({right}))",
|
||||
"shr" => $"({left} >> {right})",
|
||||
"ppc_slw" => $"PPC_Slw(static_cast<uint32_t>({left}), static_cast<uint32_t>({right}))",
|
||||
"ppc_srw" => $"PPC_Srw(static_cast<uint32_t>({left}), static_cast<uint32_t>({right}))",
|
||||
"ppc_sraw" => $"PPC_Sraw(static_cast<uint32_t>({left}), static_cast<uint32_t>({right}))",
|
||||
"fabs" => $"std::fabs({left})",
|
||||
"fneg" => $"(-({left}))",
|
||||
"frsp" => $"static_cast<double>(PpcForceSingleValueInline({left}))",
|
||||
"frsp_noni" => $"static_cast<double>(static_cast<float>({left}))",
|
||||
"fsqrt" => $"std::sqrt({left})",
|
||||
"fcmp" => $"({left} - {right})",
|
||||
"fctiw" => $"PPC_Fctiw({left})",
|
||||
"fctiwz" => $"PPC_Fctiwz({left})",
|
||||
"fpr_low_word" => $"PPC_FprLowWordInline({left})",
|
||||
"sar" => $"(static_cast<int32_t>({left}) >> {right})",
|
||||
"mul" => $"({left} * {right})",
|
||||
"mulhwu" => $"(static_cast<uint32_t>((static_cast<uint64_t>({left}) * static_cast<uint64_t>({right})) >> 32))",
|
||||
"mulhw" => $"(static_cast<int32_t>((static_cast<int64_t>(static_cast<int32_t>({left})) * static_cast<int64_t>(static_cast<int32_t>({right}))) >> 32))",
|
||||
"divu" => $"PPC_Divwu(static_cast<uint32_t>({left}), static_cast<uint32_t>({right}))",
|
||||
"div" => $"PPC_Divw(static_cast<int32_t>({left}), static_cast<int32_t>({right}))",
|
||||
"sext16" => $"(static_cast<int32_t>(static_cast<int16_t>({left})))",
|
||||
"not" => $"~({left})",
|
||||
"sub_u" => $"(CompareUnsigned(static_cast<uint32_t>({left}), static_cast<uint32_t>({right})))",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(bin.Op), bin.Op, null)
|
||||
};
|
||||
}
|
||||
|
||||
// Guest physical address window: MEM1 at 0x80000000 and MEM2 up to
|
||||
// 0x94000000. A negative IR immediate whose unsigned form lands here is a
|
||||
// code or data address that was only negative because IrValue carries a
|
||||
// signed int.
|
||||
private const uint GuestAddressWindowStart = 0x80000000u;
|
||||
private const uint GuestAddressWindowEnd = 0x94000000u;
|
||||
|
||||
/// <summary>
|
||||
/// Formats an assignment RHS. Negative <c>int</c> immediates that are really guest addresses (return
|
||||
/// addresses, <c>lis/ori</c> constants) print as hex when the destination is an unsigned 32-bit guest
|
||||
/// register, for readability only; small signed immediates like <c>-16</c> stay decimal.
|
||||
/// </summary>
|
||||
private static string ToRegisterAssignmentExpression(
|
||||
string destination,
|
||||
IrValue value,
|
||||
RepresentationEnvironment types)
|
||||
{
|
||||
if (value.Kind == "const" &&
|
||||
value.Constant is { } constant &&
|
||||
constant < 0 &&
|
||||
IsUnsigned32BitGuestRegister(destination))
|
||||
{
|
||||
var unsigned = unchecked((uint)constant);
|
||||
if (unsigned is >= GuestAddressWindowStart and < GuestAddressWindowEnd)
|
||||
{
|
||||
return $"0x{unsigned:X8}u";
|
||||
}
|
||||
}
|
||||
|
||||
return ToExpression(value, types);
|
||||
}
|
||||
|
||||
private static bool IsUnsigned32BitGuestRegister(string name)
|
||||
{
|
||||
if (!IsCpuRegister(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var baseName = BaseRegister(name);
|
||||
if (baseName.Length > 1 && baseName[0] == 'r' &&
|
||||
int.TryParse(baseName.AsSpan(1), out var gpr) && gpr is >= 0 and < 32)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// CR field destinations lower to ordinary locals whose host type comes
|
||||
// from representation classification, so they are deliberately excluded.
|
||||
return baseName.Equals("lr", StringComparison.OrdinalIgnoreCase) ||
|
||||
baseName.Equals("ctr", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string ToExpression(IrValue val, RepresentationEnvironment types)
|
||||
{
|
||||
if (val.Kind == "const" && val.Constant.HasValue)
|
||||
{
|
||||
return val.Constant.Value.ToString();
|
||||
}
|
||||
|
||||
if (val.Kind != "register" || val.RegisterName == null)
|
||||
{
|
||||
return "0";
|
||||
}
|
||||
|
||||
var name = val.RegisterName;
|
||||
if (IsCpuRegister(name))
|
||||
{
|
||||
return RegisterToReadExpression(name, types);
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private static string RegisterToCtxRead(string name, RepresentationEnvironment types)
|
||||
{
|
||||
var baseName = BaseRegisterSpan(name);
|
||||
var residency = _activeResidency;
|
||||
|
||||
if (baseName.Length > 1 && baseName[0] == 'r' && int.TryParse(baseName[1..], out var gpr) && gpr is >= 0 and < 32)
|
||||
{
|
||||
return residency is null ? CtxGprName(gpr) : residency.Gpr(gpr, written: false);
|
||||
}
|
||||
|
||||
if (baseName.Length > 1 && baseName[0] == 'f' && int.TryParse(baseName[1..], out var fpr) && fpr is >= 0 and < 32)
|
||||
{
|
||||
return residency is null ? CtxFprScalarName(fpr) : residency.FprScalar(fpr, written: false);
|
||||
}
|
||||
|
||||
if (baseName.Length == 3 && baseName.StartsWith("cr".AsSpan(), StringComparison.OrdinalIgnoreCase) && char.IsDigit(baseName[2]))
|
||||
{
|
||||
var crField = baseName[2] - '0';
|
||||
if (crField is >= 0 and <= 7)
|
||||
{
|
||||
var shift = (7 - crField) * 4;
|
||||
var crExpr = residency is null ? "ctx->cr" : residency.Cr(written: false);
|
||||
return $"(({crExpr} >> {shift}) & 0xF)";
|
||||
}
|
||||
}
|
||||
|
||||
if (SpanEqualsIgnoreCase(baseName, "lr")) return "ctx->lr";
|
||||
if (SpanEqualsIgnoreCase(baseName, "ctr")) return residency is null ? "ctx->ctr" : residency.Ctr(written: false);
|
||||
if (SpanEqualsIgnoreCase(baseName, "cr")) return residency is null ? "ctx->cr" : residency.Cr(written: false);
|
||||
if (SpanEqualsIgnoreCase(baseName, "xer")) return residency is null ? "ctx->xer" : residency.Xer(written: false);
|
||||
if (SpanEqualsIgnoreCase(baseName, "srr0")) return "ctx->srr0";
|
||||
if (SpanEqualsIgnoreCase(baseName, "srr1")) return "ctx->srr1";
|
||||
if (SpanEqualsIgnoreCase(baseName, "msr")) return "ctx->msr";
|
||||
if (SpanEqualsIgnoreCase(baseName, "hid0")) return "ctx->hid0";
|
||||
if (SpanEqualsIgnoreCase(baseName, "hid1")) return "ctx->hid1";
|
||||
if (SpanEqualsIgnoreCase(baseName, "hid2")) return "ctx->hid2";
|
||||
if (baseName.StartsWith("gqr".AsSpan(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return int.TryParse(baseName[3..], out var gqrIdx) && gqrIdx is >= 0 and < 8
|
||||
? $"ctx->gqr[{gqrIdx}]"
|
||||
: "0";
|
||||
}
|
||||
|
||||
return "0";
|
||||
}
|
||||
|
||||
private static string ToDestination(string name, RepresentationEnvironment types)
|
||||
{
|
||||
if (IsCpuRegister(name))
|
||||
{
|
||||
return RegisterToWriteExpression(name, types);
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private static string RegisterToCtxWrite(string name, RepresentationEnvironment types)
|
||||
{
|
||||
var baseName = BaseRegisterSpan(name);
|
||||
var residency = _activeResidency;
|
||||
|
||||
if (baseName.Length > 1 && baseName[0] == 'r' && int.TryParse(baseName[1..], out var gpr) && gpr is >= 0 and < 32)
|
||||
{
|
||||
return residency is null ? CtxGprName(gpr) : residency.Gpr(gpr, written: true);
|
||||
}
|
||||
|
||||
if (baseName.Length > 1 && baseName[0] == 'f' && int.TryParse(baseName[1..], out var fpr) && fpr is >= 0 and < 32)
|
||||
{
|
||||
return residency is null ? CtxFprScalarName(fpr) : residency.FprScalar(fpr, written: true);
|
||||
}
|
||||
|
||||
if (baseName.Length == 3 && baseName.StartsWith("cr".AsSpan(), StringComparison.OrdinalIgnoreCase) && char.IsDigit(baseName[2]))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
if (SpanEqualsIgnoreCase(baseName, "lr")) return "ctx->lr";
|
||||
if (SpanEqualsIgnoreCase(baseName, "ctr")) return residency is null ? "ctx->ctr" : residency.Ctr(written: true);
|
||||
if (SpanEqualsIgnoreCase(baseName, "cr")) return residency is null ? "ctx->cr" : residency.Cr(written: true);
|
||||
if (SpanEqualsIgnoreCase(baseName, "xer")) return residency is null ? "ctx->xer" : residency.Xer(written: true);
|
||||
if (SpanEqualsIgnoreCase(baseName, "srr0")) return "ctx->srr0";
|
||||
if (SpanEqualsIgnoreCase(baseName, "srr1")) return "ctx->srr1";
|
||||
if (SpanEqualsIgnoreCase(baseName, "msr")) return "ctx->msr";
|
||||
if (SpanEqualsIgnoreCase(baseName, "hid0")) return "ctx->hid0";
|
||||
if (SpanEqualsIgnoreCase(baseName, "hid1")) return "ctx->hid1";
|
||||
if (SpanEqualsIgnoreCase(baseName, "hid2")) return "ctx->hid2";
|
||||
if (baseName.StartsWith("gqr".AsSpan(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return int.TryParse(baseName[3..], out var gqrIdx) && gqrIdx is >= 0 and < 8
|
||||
? $"ctx->gqr[{gqrIdx}]"
|
||||
: name;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private static (int Mask, bool Set) CrFieldConditionTest(string condition)
|
||||
{
|
||||
if (EqualsIgnoreCase(condition, "bne")) return (2, false);
|
||||
if (EqualsIgnoreCase(condition, "beq")) return (2, true);
|
||||
if (EqualsIgnoreCase(condition, "bgt")) return (4, true);
|
||||
if (EqualsIgnoreCase(condition, "ble")) return (4, false);
|
||||
if (EqualsIgnoreCase(condition, "blt")) return (8, true);
|
||||
if (EqualsIgnoreCase(condition, "bge")) return (8, false);
|
||||
if (EqualsIgnoreCase(condition, "bso")) return (1, true);
|
||||
if (EqualsIgnoreCase(condition, "bns")) return (1, false);
|
||||
return (0, false);
|
||||
}
|
||||
|
||||
private static bool EqualsIgnoreCase(string value, string candidate) =>
|
||||
string.Equals(value, candidate, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string ToCondition(string condition, string conditionRegister, RepresentationEnvironment types)
|
||||
{
|
||||
var conditionBase = BaseRegisterSpan(conditionRegister);
|
||||
if (conditionBase.Length == 3 &&
|
||||
conditionBase.StartsWith("cr".AsSpan(), StringComparison.OrdinalIgnoreCase) &&
|
||||
conditionBase[2] is >= '0' and <= '7')
|
||||
{
|
||||
var field = conditionBase[2] - '0';
|
||||
var fieldShift = (7 - field) * 4;
|
||||
var (mask, set) = CrFieldConditionTest(condition);
|
||||
if (mask != 0)
|
||||
{
|
||||
var shiftedMask = mask << fieldShift;
|
||||
var crExpr = _activeResidency is null
|
||||
? "ctx->cr"
|
||||
: _activeResidency.Cr(written: false);
|
||||
return $"(({crExpr} & 0x{shiftedMask:X8}u) {(set ? "!=" : "==")} 0)";
|
||||
}
|
||||
}
|
||||
|
||||
var reg = IsCpuRegister(conditionRegister)
|
||||
? RegisterToReadExpression(conditionRegister, types)
|
||||
: ResidentRawCondition(conditionRegister);
|
||||
|
||||
if (EqualsIgnoreCase(condition, "bdnz")) return $"({reg} != 0)";
|
||||
if (EqualsIgnoreCase(condition, "bdz")) return $"({reg} == 0)";
|
||||
if (EqualsIgnoreCase(condition, "bne")) return $"(({reg} & 0x2) == 0)";
|
||||
if (EqualsIgnoreCase(condition, "beq")) return $"(({reg} & 0x2) != 0)";
|
||||
if (EqualsIgnoreCase(condition, "bgt")) return $"(({reg} & 0x4) != 0)";
|
||||
if (EqualsIgnoreCase(condition, "blt")) return $"(({reg} & 0x8) != 0)";
|
||||
if (EqualsIgnoreCase(condition, "bge")) return $"(({reg} & 0x8) == 0)";
|
||||
if (EqualsIgnoreCase(condition, "ble")) return $"(({reg} & 0x4) == 0)";
|
||||
if (EqualsIgnoreCase(condition, "bso")) return $"(({reg} & 0x1) != 0)";
|
||||
if (EqualsIgnoreCase(condition, "bns")) return $"(({reg} & 0x1) == 0)";
|
||||
return reg;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps the raw branch-condition text <c>PpcLifter</c> builds for <c>bc</c>/<c>bcctr</c> onto resident
|
||||
/// locals. This is the only emitted register access that bypasses the expression layer, so missing a
|
||||
/// rewrite here means reading a stale architectural CR/CTR instead of the live local.
|
||||
/// </summary>
|
||||
private static string ResidentRawCondition(string conditionText)
|
||||
{
|
||||
var residency = _activeResidency;
|
||||
if (residency is null || string.IsNullOrEmpty(conditionText))
|
||||
{
|
||||
return conditionText;
|
||||
}
|
||||
|
||||
var rewritten = conditionText;
|
||||
if (rewritten.Contains("ctx->ctr", StringComparison.Ordinal))
|
||||
{
|
||||
rewritten = rewritten.Replace("ctx->ctr", residency.Ctr(written: false), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
if (rewritten.Contains("GetCRBit(ctx,", StringComparison.Ordinal))
|
||||
{
|
||||
rewritten = rewritten.Replace(
|
||||
"GetCRBit(ctx,",
|
||||
$"GetCRBitResident({residency.Cr(written: false)},",
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
return rewritten;
|
||||
}
|
||||
|
||||
private static string Address(IrAddress addr, RepresentationEnvironment types)
|
||||
{
|
||||
var baseExpr = IsCpuRegister(addr.Base)
|
||||
? RegisterToReadExpression(addr.Base, types)
|
||||
: addr.Base;
|
||||
|
||||
return addr.Offset == 0
|
||||
? baseExpr
|
||||
: $"({baseExpr} + {addr.Offset})";
|
||||
}
|
||||
|
||||
private static string RemapAddress(
|
||||
IrAddress addr,
|
||||
RepresentationEnvironment types,
|
||||
IReadOnlyDictionary<string, uint> localConstants,
|
||||
LinkedAddressRemap? remap)
|
||||
{
|
||||
if (remap is not null &&
|
||||
(localConstants.TryGetValue(addr.Base, out var baseValue) ||
|
||||
localConstants.TryGetValue(GetRegisterBaseName(addr.Base), out baseValue)))
|
||||
{
|
||||
var effective = unchecked(baseValue + (uint)addr.Offset);
|
||||
var linkEnd = checked(remap.LinkBase + remap.CodeSize);
|
||||
if (effective >= remap.LinkBase && effective < linkEnd)
|
||||
{
|
||||
var relocated = checked(remap.GuestBase + (effective - remap.LinkBase));
|
||||
return $"0x{relocated:X8}u";
|
||||
}
|
||||
}
|
||||
|
||||
return Address(addr, types);
|
||||
}
|
||||
|
||||
private static string BaseRegister(string name)
|
||||
{
|
||||
var idx = name.IndexOf('_');
|
||||
return idx < 0 ? name : name[..idx];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocation-free <see cref="BaseRegister"/>. The expression layer only
|
||||
/// inspects the prefix, so the substring it used to build per access was
|
||||
/// pure garbage.
|
||||
/// </summary>
|
||||
private static ReadOnlySpan<char> BaseRegisterSpan(string name)
|
||||
{
|
||||
var idx = name.IndexOf('_');
|
||||
return idx < 0 ? name.AsSpan() : name.AsSpan(0, idx);
|
||||
}
|
||||
|
||||
private static int Bits(int sizeBytes) => sizeBytes switch
|
||||
{
|
||||
1 => 8,
|
||||
2 => 16,
|
||||
8 => 64,
|
||||
_ => 32
|
||||
};
|
||||
|
||||
private static string RegisterToReadExpression(string name, RepresentationEnvironment types)
|
||||
{
|
||||
return RegisterToCtxRead(name, types);
|
||||
}
|
||||
|
||||
private static string RegisterToWriteExpression(string name, RepresentationEnvironment types)
|
||||
{
|
||||
return RegisterToCtxWrite(name, types);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Translator.Core.Analysis;
|
||||
using Translator.Core.Analysis.Ssa;
|
||||
using Translator.Core.Ir;
|
||||
using Translator.Core.Representation;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
public sealed partial class CxxLinearCodeGenerator
|
||||
{
|
||||
private readonly record struct StackAccessInfo(int Offset, int SizeBytes, bool IsStore);
|
||||
|
||||
private static IrFunction ElideFctiwStackLowWordLoads(IrFunction func, RepresentationEnvironment types)
|
||||
{
|
||||
var stackFacts = StackAddressFacts.Build(func);
|
||||
var cfg = IrCfg.Build(func);
|
||||
var changed = false;
|
||||
var nextTemp = 0;
|
||||
var rewrittenBlocks = new List<IrBasicBlock>(func.Blocks.Count);
|
||||
|
||||
foreach (var block in func.Blocks)
|
||||
{
|
||||
var insertedBefore = new Dictionary<int, List<IrInstruction>>();
|
||||
var replacements = new Dictionary<int, IrInstruction>();
|
||||
var removals = new HashSet<int>();
|
||||
var claimedLoads = new HashSet<int>();
|
||||
var convertedFprs = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
for (var index = 0; index < block.Instructions.Count; index++)
|
||||
{
|
||||
var instruction = block.Instructions[index];
|
||||
if (instruction is IrStore store &&
|
||||
store.SizeBytes == 8 &&
|
||||
store.Source.Kind == "register" &&
|
||||
store.Source.RegisterName is { } sourceRegister &&
|
||||
IsFloatRegister(sourceRegister) &&
|
||||
convertedFprs.TryGetValue(GetRegisterBaseName(sourceRegister), out var converted) &&
|
||||
converted &&
|
||||
stackFacts.TryResolve(store.Address, out var storeOffset) &&
|
||||
TryFindFctiwLowWordLoad(block.Instructions, index, stackFacts, storeOffset, claimedLoads, out var loadIndex))
|
||||
{
|
||||
var temp = $"fctiwzword{nextTemp++}";
|
||||
AddInsertion(index, new IrBinary(temp, IrValue.Register(sourceRegister), IrValue.Imm(0), "fpr_low_word"));
|
||||
replacements[loadIndex] = new IrAssign(((IrLoad)block.Instructions[loadIndex]).Destination, IrValue.Register(temp));
|
||||
if (CanRemoveFctiwStackStore(func, cfg, block.Label, loadIndex + 1, stackFacts, storeOffset))
|
||||
{
|
||||
removals.Add(index);
|
||||
}
|
||||
claimedLoads.Add(loadIndex);
|
||||
types.Unify(temp, ValueRepresentation.UInt32);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
UpdateConvertedFprState(instruction, convertedFprs);
|
||||
}
|
||||
|
||||
if (insertedBefore.Count == 0 && replacements.Count == 0 && removals.Count == 0)
|
||||
{
|
||||
rewrittenBlocks.Add(block);
|
||||
continue;
|
||||
}
|
||||
|
||||
var rewrittenInstructions = new List<IrInstruction>(block.Instructions.Count + insertedBefore.Count);
|
||||
for (var index = 0; index < block.Instructions.Count; index++)
|
||||
{
|
||||
if (insertedBefore.TryGetValue(index, out var inserted))
|
||||
{
|
||||
rewrittenInstructions.AddRange(inserted);
|
||||
}
|
||||
|
||||
if (removals.Contains(index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
rewrittenInstructions.Add(replacements.TryGetValue(index, out var replacement)
|
||||
? replacement
|
||||
: block.Instructions[index]);
|
||||
}
|
||||
|
||||
rewrittenBlocks.Add(new IrBasicBlock(block.Label, rewrittenInstructions));
|
||||
|
||||
void AddInsertion(int index, IrInstruction inserted)
|
||||
{
|
||||
if (!insertedBefore.TryGetValue(index, out var list))
|
||||
{
|
||||
list = new List<IrInstruction>();
|
||||
insertedBefore[index] = list;
|
||||
}
|
||||
|
||||
list.Add(inserted);
|
||||
}
|
||||
}
|
||||
|
||||
return changed
|
||||
? new IrFunction(func.Name, func.EntryLabel, rewrittenBlocks)
|
||||
: func;
|
||||
}
|
||||
|
||||
private static bool CanRemoveFctiwStackStore(
|
||||
IrFunction func,
|
||||
IrCfg cfg,
|
||||
string startBlockLabel,
|
||||
int startIndex,
|
||||
StackAddressFacts stackFacts,
|
||||
int storeOffset)
|
||||
{
|
||||
var pending = new Queue<(string BlockLabel, int StartIndex)>();
|
||||
var visited = new HashSet<(string BlockLabel, int StartIndex)>();
|
||||
pending.Enqueue((startBlockLabel, startIndex));
|
||||
|
||||
while (pending.Count > 0)
|
||||
{
|
||||
var (blockLabel, blockStartIndex) = pending.Dequeue();
|
||||
if (!visited.Add((blockLabel, blockStartIndex)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!cfg.Blocks.TryGetValue(blockLabel, out var block))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var overwrittenOnThisPath = false;
|
||||
for (var index = Math.Max(0, blockStartIndex); index < block.Instructions.Count; index++)
|
||||
{
|
||||
var instruction = block.Instructions[index];
|
||||
if (IsGuestMemoryBarrier(instruction))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryGetStackAccessInfo(instruction, stackFacts, out var access) ||
|
||||
!RangesOverlap(storeOffset, 8, access.Offset, access.SizeBytes))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!access.IsStore)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
overwrittenOnThisPath = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (overwrittenOnThisPath)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var successor in cfg.Successors(blockLabel))
|
||||
{
|
||||
pending.Enqueue((successor, 0));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryFindFctiwLowWordLoad(
|
||||
IReadOnlyList<IrInstruction> instructions,
|
||||
int storeIndex,
|
||||
StackAddressFacts stackFacts,
|
||||
int storeOffset,
|
||||
IReadOnlySet<int> claimedLoads,
|
||||
out int loadIndex)
|
||||
{
|
||||
for (var index = storeIndex + 1; index < instructions.Count; index++)
|
||||
{
|
||||
if (!TryGetStackAccessInfo(instructions[index], stackFacts, out var access) ||
|
||||
!RangesOverlap(storeOffset, 8, access.Offset, access.SizeBytes))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!access.IsStore &&
|
||||
access.SizeBytes == 4 &&
|
||||
access.Offset == storeOffset + 4 &&
|
||||
!claimedLoads.Contains(index) &&
|
||||
instructions[index] is IrLoad)
|
||||
{
|
||||
loadIndex = index;
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
loadIndex = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetStackAccessInfo(
|
||||
IrInstruction instruction,
|
||||
StackAddressFacts stackFacts,
|
||||
out StackAccessInfo access)
|
||||
{
|
||||
switch (instruction)
|
||||
{
|
||||
case IrStore store when stackFacts.TryResolve(store.Address, out var storeOffset):
|
||||
access = new StackAccessInfo(storeOffset, store.SizeBytes, true);
|
||||
return true;
|
||||
|
||||
case IrLoad load when stackFacts.TryResolve(load.Address, out var loadOffset):
|
||||
access = new StackAccessInfo(loadOffset, load.SizeBytes, false);
|
||||
return true;
|
||||
|
||||
case IrCall call when TryGetPsqStackAccess(call, stackFacts, out var psq):
|
||||
access = new StackAccessInfo(psq.Offset, psq.SizeBytes, psq.IsStore);
|
||||
return true;
|
||||
|
||||
default:
|
||||
access = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateConvertedFprState(
|
||||
IrInstruction instruction,
|
||||
Dictionary<string, bool> convertedFprs)
|
||||
{
|
||||
switch (instruction)
|
||||
{
|
||||
case IrBinary binary when IsFloatRegister(binary.Destination):
|
||||
convertedFprs[GetRegisterBaseName(binary.Destination)] = IsFctiwOp(binary.Op);
|
||||
break;
|
||||
|
||||
case IrAssign assign when IsFloatRegister(assign.Destination):
|
||||
convertedFprs[GetRegisterBaseName(assign.Destination)] = false;
|
||||
break;
|
||||
|
||||
case IrLoad load when IsFloatRegister(load.Destination):
|
||||
convertedFprs[GetRegisterBaseName(load.Destination)] = false;
|
||||
break;
|
||||
|
||||
case IrCall call:
|
||||
if (!string.IsNullOrWhiteSpace(call.Destination) &&
|
||||
IsFloatRegister(call.Destination))
|
||||
{
|
||||
convertedFprs[GetRegisterBaseName(call.Destination)] = false;
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(call.Destination) &&
|
||||
IsGuestCallTarget(call.Target))
|
||||
{
|
||||
convertedFprs.Clear();
|
||||
}
|
||||
break;
|
||||
|
||||
case IrIndirectCall:
|
||||
convertedFprs.Clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsFctiwOp(string op) =>
|
||||
op.Equals("fctiw", StringComparison.OrdinalIgnoreCase) ||
|
||||
op.Equals("fctiwz", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsGuestMemoryBarrier(IrInstruction instruction) =>
|
||||
instruction is IrIndirectCall ||
|
||||
instruction is IrCall call && IsGuestCallTarget(call.Target);
|
||||
}
|
||||
@@ -0,0 +1,606 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Translator.Core.Analysis;
|
||||
using Translator.Core.Analysis.Representation;
|
||||
using Translator.Core.Analysis.Ssa;
|
||||
using Translator.Core.Ir;
|
||||
using Translator.Core.Representation;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
public sealed partial class CxxLinearCodeGenerator
|
||||
{
|
||||
internal enum FusedComparison
|
||||
{
|
||||
Equal,
|
||||
NotEqual,
|
||||
Less,
|
||||
GreaterOrEqual,
|
||||
Greater,
|
||||
LessOrEqual
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A compare whose only consumer is the branch that terminates the same
|
||||
/// block. The branch emits the C++ comparison directly and the CR field is
|
||||
/// never materialized.
|
||||
/// </summary>
|
||||
internal sealed record FusedCompare(
|
||||
IrValue Left,
|
||||
IrValue Right,
|
||||
bool IsUnsigned,
|
||||
bool IsFloat,
|
||||
FusedComparison Comparison);
|
||||
|
||||
/// <summary>
|
||||
/// Everything the body emitter needs to drop condition-flag work: the
|
||||
/// instructions it must not emit at all, and the branches that replace a
|
||||
/// packed CR test with a direct comparison.
|
||||
/// </summary>
|
||||
internal sealed record FlagElisionPlan(
|
||||
IReadOnlySet<(string Block, int Index)> SuppressedInstructions,
|
||||
IReadOnlyDictionary<string, FusedCompare> FusedBranches)
|
||||
{
|
||||
public static FlagElisionPlan Empty { get; } = new(
|
||||
new HashSet<(string, int)>(),
|
||||
new Dictionary<string, FusedCompare>(StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Materializes <see cref="FlagElisionPlan.SuppressedInstructions"/> as one dense flag array per block,
|
||||
/// replacing a per-emission <c>(string, int)</c> tuple hash lookup for every instruction. Block keys use
|
||||
/// ordinal comparison to match the set's tuple comparer.
|
||||
/// </summary>
|
||||
private static Dictionary<string, bool[]> BuildSuppressedInstructionMasks(
|
||||
IrFunction function,
|
||||
FlagElisionPlan plan)
|
||||
{
|
||||
var masks = new Dictionary<string, bool[]>(StringComparer.Ordinal);
|
||||
if (plan.SuppressedInstructions.Count == 0)
|
||||
{
|
||||
return masks;
|
||||
}
|
||||
|
||||
var blockSizes = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
foreach (var block in function.Blocks)
|
||||
{
|
||||
blockSizes[block.Label] = block.Instructions.Count;
|
||||
}
|
||||
|
||||
foreach (var (blockLabel, index) in plan.SuppressedInstructions)
|
||||
{
|
||||
if (index < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!masks.TryGetValue(blockLabel, out var mask))
|
||||
{
|
||||
var size = blockSizes.TryGetValue(blockLabel, out var known) ? known : index + 1;
|
||||
mask = new bool[Math.Max(size, index + 1)];
|
||||
masks[blockLabel] = mask;
|
||||
}
|
||||
else if (index >= mask.Length)
|
||||
{
|
||||
Array.Resize(ref mask, index + 1);
|
||||
masks[blockLabel] = mask;
|
||||
}
|
||||
|
||||
mask[index] = true;
|
||||
}
|
||||
|
||||
return masks;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Condition-flag elision planner: one backward liveness pass drives compare+branch fusion, dead CR-field
|
||||
/// writes, and dead XER.CA updates. Exit state is <see cref="GuestFlagState.All"/> since a return publishes
|
||||
/// ctx->cr/ctx->xer with no caller info, so the last write on any path to a return is always kept.
|
||||
/// </summary>
|
||||
private static FlagElisionPlan PlanFlagElision(
|
||||
IrFunction function,
|
||||
IrCfg cfg,
|
||||
RepresentationEnvironment types,
|
||||
IReadOnlyDictionary<uint, GuestAbiContract> guestAbiContracts,
|
||||
IReadOnlySet<uint> modOverridableCallTargets,
|
||||
IReadOnlySet<uint> nonReturningCallTargets,
|
||||
IReadOnlySet<uint> lrContinuationCallTargets)
|
||||
{
|
||||
// A body that dispatches on ctx->lr can re-enter any of its own labels,
|
||||
// including positions inside a block. The static CFG does not model
|
||||
// those edges, so no liveness statement derived from it holds and the
|
||||
// whole plan is abandoned for such functions.
|
||||
if (HasOutOfBandLocalEntry(function, nonReturningCallTargets, lrContinuationCallTargets))
|
||||
{
|
||||
return FlagElisionPlan.Empty;
|
||||
}
|
||||
|
||||
var suppressed = new HashSet<(string Block, int Index)>();
|
||||
var fused = new Dictionary<string, FusedCompare>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
{
|
||||
var context = new GuestFlagLivenessContext(guestAbiContracts, modOverridableCallTargets);
|
||||
var liveness = GuestFlagLivenessAnalyzer.Analyze(function, cfg, context, GuestFlagState.All);
|
||||
var fusedCompareSites = new HashSet<(string Block, int Index)>();
|
||||
|
||||
{
|
||||
foreach (var block in function.Blocks)
|
||||
{
|
||||
if (!TryPlanCompareBranchFusion(
|
||||
function, cfg, block, liveness.LiveOut[block.Label], types,
|
||||
out var compare, out var compareBlock, out var compareIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
fused[block.Label] = compare!;
|
||||
fusedCompareSites.Add((compareBlock!, compareIndex));
|
||||
suppressed.Add((compareBlock!, compareIndex));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
foreach (var block in function.Blocks)
|
||||
{
|
||||
var live = liveness.LiveOut[block.Label];
|
||||
for (var index = block.Instructions.Count - 1; index >= 0; --index)
|
||||
{
|
||||
var instruction = block.Instructions[index];
|
||||
if (fusedCompareSites.Contains((block.Label, index)))
|
||||
{
|
||||
// Already removed, but the branch that replaced it
|
||||
// still consumes the value: it kills the field for
|
||||
// everything above, exactly as the compare did.
|
||||
live = live.Except(GuestFlagLivenessAnalyzer.Kills(instruction));
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (instruction)
|
||||
{
|
||||
case IrSetCrField setCr when !live.HasCrField(setCr.FieldIndex & 7):
|
||||
suppressed.Add((block.Label, index));
|
||||
continue;
|
||||
|
||||
case IrCall call when !live.Carry &&
|
||||
GuestFlagLivenessAnalyzer.IsCarryProducingHelper(call.Target) &&
|
||||
IsXerDestination(call.Destination):
|
||||
suppressed.Add((block.Label, index));
|
||||
continue;
|
||||
}
|
||||
|
||||
live = GuestFlagLivenessAnalyzer.Transfer(instruction, live, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
foreach (var block in function.Blocks)
|
||||
{
|
||||
for (var index = 0; index + 1 < block.Instructions.Count; ++index)
|
||||
{
|
||||
if (block.Instructions[index] is not IrAssign { Value.Kind: "const" } assign ||
|
||||
!GetRegisterBaseName(assign.Destination).Equals("lr", StringComparison.OrdinalIgnoreCase) ||
|
||||
block.Instructions[index + 1] is not IrCall call ||
|
||||
!CalleeIgnoresLinkRegister(
|
||||
call, guestAbiContracts, modOverridableCallTargets,
|
||||
nonReturningCallTargets, lrContinuationCallTargets))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
suppressed.Add((block.Label, index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (suppressed.Count == 0 && fused.Count == 0)
|
||||
{
|
||||
return FlagElisionPlan.Empty;
|
||||
}
|
||||
|
||||
return new FlagElisionPlan(suppressed, fused);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when the body has a call whose callee may resume it via <c>EmitLocalLrContinuationDispatch</c>,
|
||||
/// which can jump to any <c>loc_</c> label including ones inside a block, so the static CFG understates edges.
|
||||
/// </summary>
|
||||
private static bool HasOutOfBandLocalEntry(
|
||||
IrFunction function,
|
||||
IReadOnlySet<uint> nonReturningCallTargets,
|
||||
IReadOnlySet<uint> lrContinuationCallTargets)
|
||||
{
|
||||
if (nonReturningCallTargets.Count == 0 && lrContinuationCallTargets.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var block in function.Blocks)
|
||||
{
|
||||
foreach (var instruction in block.Instructions)
|
||||
{
|
||||
if (instruction is IrCall call &&
|
||||
TryParseAddress(call.Target, out var target) &&
|
||||
(nonReturningCallTargets.Contains(target) || lrContinuationCallTargets.Contains(target)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsXerDestination(string? destination) =>
|
||||
!string.IsNullOrWhiteSpace(destination) &&
|
||||
GetRegisterBaseName(destination!).Equals("xer", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// A call whose return address never becomes observable state: translated functions return via the C++
|
||||
/// stack, not by branching to ctx->lr, so the LR store is skipped unless the callee reads LR, the call site
|
||||
/// dispatches on ctx->lr, or the callee escapes the contract model entirely.
|
||||
/// </summary>
|
||||
private static bool CalleeIgnoresLinkRegister(
|
||||
IrCall call,
|
||||
IReadOnlyDictionary<uint, GuestAbiContract> guestAbiContracts,
|
||||
IReadOnlySet<uint> modOverridableCallTargets,
|
||||
IReadOnlySet<uint> nonReturningCallTargets,
|
||||
IReadOnlySet<uint> lrContinuationCallTargets)
|
||||
{
|
||||
if (!TryParseAddress(call.Target, out var target) ||
|
||||
modOverridableCallTargets.Contains(target) ||
|
||||
nonReturningCallTargets.Contains(target) ||
|
||||
lrContinuationCallTargets.Contains(target) ||
|
||||
!guestAbiContracts.TryGetValue(target, out var contract))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const GuestCallBoundaryFlags escaping =
|
||||
GuestCallBoundaryFlags.RequiresCompleteContext |
|
||||
GuestCallBoundaryFlags.CanSuspend |
|
||||
GuestCallBoundaryFlags.CanSwitchThreads |
|
||||
GuestCallBoundaryFlags.InvokesGuestCode;
|
||||
|
||||
return !contract.ReadsLrBeforeWrite && (contract.BoundaryFlags & escaping) == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How many basic blocks back the compare feeding a branch is searched. Compare/branch pairs are adjacent
|
||||
/// in PowerPC, but block builders often split a new block at the branch, so the producer is usually at the
|
||||
/// end of the single predecessor.
|
||||
/// </summary>
|
||||
private const int MaxFusionBlockHops = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Finds the compare feeding a block's terminating branch when the branch is its only consumer, searching
|
||||
/// back through a chain of blocks with exactly one predecessor/successor each and no conditional transfer.
|
||||
/// </summary>
|
||||
private static bool TryPlanCompareBranchFusion(
|
||||
IrFunction function,
|
||||
IrCfg cfg,
|
||||
IrBasicBlock block,
|
||||
GuestFlagState liveOut,
|
||||
RepresentationEnvironment types,
|
||||
out FusedCompare? compare,
|
||||
out string? compareBlock,
|
||||
out int compareIndex)
|
||||
{
|
||||
compare = null;
|
||||
compareBlock = null;
|
||||
compareIndex = -1;
|
||||
if (block.Instructions.Count == 0 ||
|
||||
block.Instructions[^1] is not IrBranch branch ||
|
||||
!TryParseSingleCrBitTest(branch, out var field, out var bit, out var testSet))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// The summary-overflow copy is not reconstructible from the compare
|
||||
// operands, so a branch on it can never be fused.
|
||||
if (bit == CrBit.SummaryOverflow || liveOut.HasCrField(field))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Instructions executed between the compare and the branch. The fused
|
||||
// comparison is evaluated at the branch, so none of them may redefine an
|
||||
// operand.
|
||||
var between = new List<IrInstruction>();
|
||||
var current = block;
|
||||
var startIndex = block.Instructions.Count - 2;
|
||||
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { block.Label };
|
||||
|
||||
for (var hop = 0; hop < MaxFusionBlockHops; ++hop)
|
||||
{
|
||||
for (var index = startIndex; index >= 0; --index)
|
||||
{
|
||||
var instruction = current.Instructions[index];
|
||||
if (instruction is IrSetCrField setCr && (setCr.FieldIndex & 7) == field)
|
||||
{
|
||||
if (between.Any(later => RedefinesFusedOperand(later, setCr)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var candidate = BuildFusedCompare(setCr, bit, testSet, types);
|
||||
// Paired-single representation is normalized on CFG edges by
|
||||
// emitted code that no IR definition describes, so a float
|
||||
// compare is only re-evaluated inside its own block.
|
||||
if (candidate is null || (candidate.IsFloat && hop != 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
compare = candidate;
|
||||
compareBlock = current.Label;
|
||||
compareIndex = index;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!IsFusionTransparent(instruction, field))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
between.Add(instruction);
|
||||
}
|
||||
|
||||
// The function entry is always reachable without executing any
|
||||
// predecessor, so a single recorded predecessor proves nothing there.
|
||||
if (current.Label.Equals(function.EntryLabel, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var predecessors = cfg.Predecessors(current.Label);
|
||||
if (predecessors.Count != 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var predecessorLabel = predecessors[0];
|
||||
if (!visited.Add(predecessorLabel) ||
|
||||
cfg.Successors(predecessorLabel).Count != 1 ||
|
||||
!cfg.Blocks.TryGetValue(predecessorLabel, out var predecessor) ||
|
||||
predecessor.Instructions.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only a plain fallthrough or unconditional goto may sit between the
|
||||
// compare and the branch.
|
||||
var terminator = predecessor.Instructions[^1];
|
||||
if (terminator is IrBranch or IrJumpTable or IrIndirectJump or IrReturn or IrUndefined)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
current = predecessor;
|
||||
startIndex = terminator is IrJump
|
||||
? predecessor.Instructions.Count - 2
|
||||
: predecessor.Instructions.Count - 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private enum CrBit
|
||||
{
|
||||
LessThan,
|
||||
GreaterThan,
|
||||
Equal,
|
||||
SummaryOverflow
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An instruction that may sit between a compare and the branch it feeds. The fused comparison is evaluated
|
||||
/// at the branch, so nothing between them may redefine an operand or observe the CR field; calls are
|
||||
/// excluded outright since they can do both.
|
||||
/// </summary>
|
||||
private static bool IsFusionTransparent(IrInstruction instruction, int field)
|
||||
{
|
||||
switch (instruction)
|
||||
{
|
||||
case IrTracePpc:
|
||||
case IrComment:
|
||||
return true;
|
||||
// A compare into a different field neither reads CR nor touches the
|
||||
// field being fused; the matching field is resolved by the caller.
|
||||
case IrSetCrField other:
|
||||
return (other.FieldIndex & 7) != field;
|
||||
case IrCall:
|
||||
case IrIndirectCall:
|
||||
case IrIndirectJump:
|
||||
case IrJumpTable:
|
||||
case IrUndefined:
|
||||
case IrPhi:
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var name in IrRegisterDataFlow.Definitions(instruction)
|
||||
.Concat(IrRegisterDataFlow.Uses(instruction)))
|
||||
{
|
||||
var baseName = RegisterNameUtils.HardwareBase(name);
|
||||
if (baseName.Equals("cr", StringComparison.OrdinalIgnoreCase) ||
|
||||
(baseName.Length == 3 && baseName.StartsWith("cr", StringComparison.OrdinalIgnoreCase) &&
|
||||
baseName[2] is >= '0' and <= '7' && baseName[2] - '0' == field) ||
|
||||
baseName.StartsWith("crb", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The operand registers a fused comparison re-reads at the branch. Any
|
||||
/// redefinition between the compare and the branch makes the fusion unsound.
|
||||
/// </summary>
|
||||
private static bool RedefinesFusedOperand(IrInstruction instruction, IrSetCrField compare)
|
||||
{
|
||||
foreach (var definition in IrRegisterDataFlow.Definitions(instruction))
|
||||
{
|
||||
var baseName = RegisterNameUtils.HardwareBase(definition);
|
||||
if (MatchesOperand(compare.Left, baseName) || MatchesOperand(compare.Right, baseName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
static bool MatchesOperand(IrValue value, string baseName) =>
|
||||
value.Kind == "register" && value.RegisterName is { } name &&
|
||||
RegisterNameUtils.HardwareBase(name).Equals(baseName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static FusedCompare? BuildFusedCompare(
|
||||
IrSetCrField setCr,
|
||||
CrBit bit,
|
||||
bool testSet,
|
||||
RepresentationEnvironment types)
|
||||
{
|
||||
var comparison = (bit, testSet) switch
|
||||
{
|
||||
(CrBit.Equal, true) => FusedComparison.Equal,
|
||||
(CrBit.Equal, false) => FusedComparison.NotEqual,
|
||||
(CrBit.LessThan, true) => FusedComparison.Less,
|
||||
(CrBit.LessThan, false) => FusedComparison.GreaterOrEqual,
|
||||
(CrBit.GreaterThan, true) => FusedComparison.Greater,
|
||||
(CrBit.GreaterThan, false) => FusedComparison.LessOrEqual,
|
||||
_ => (FusedComparison?)null
|
||||
};
|
||||
|
||||
if (comparison is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var isFloat = IsFloatValue(setCr.Left, types) || IsFloatValue(setCr.Right, types);
|
||||
return new FusedCompare(setCr.Left, setCr.Right, setCr.IsUnsigned, isFloat, comparison.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes the single CR bit a conditional branch tests. Returns false for
|
||||
/// counter-based forms, for multi-bit raw expressions, and for anything the
|
||||
/// lifter produced that this decoder does not recognize exactly.
|
||||
/// </summary>
|
||||
private static bool TryParseSingleCrBitTest(IrBranch branch, out int field, out CrBit bit, out bool testSet)
|
||||
{
|
||||
field = 0;
|
||||
bit = CrBit.SummaryOverflow;
|
||||
testSet = false;
|
||||
|
||||
if (TryParseRawCrBitTest(branch.ConditionRegister, out field, out var rawBit, out testSet))
|
||||
{
|
||||
bit = rawBit;
|
||||
return true;
|
||||
}
|
||||
|
||||
var conditionBase = BaseRegister(branch.ConditionRegister ?? string.Empty);
|
||||
if (conditionBase.Length != 3 ||
|
||||
!conditionBase.StartsWith("cr", StringComparison.OrdinalIgnoreCase) ||
|
||||
conditionBase[2] is < '0' or > '7')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
field = conditionBase[2] - '0';
|
||||
(bit, testSet) = branch.Condition.ToLowerInvariant() switch
|
||||
{
|
||||
"beq" => (CrBit.Equal, true),
|
||||
"bne" => (CrBit.Equal, false),
|
||||
"bgt" => (CrBit.GreaterThan, true),
|
||||
"ble" => (CrBit.GreaterThan, false),
|
||||
"blt" => (CrBit.LessThan, true),
|
||||
"bge" => (CrBit.LessThan, false),
|
||||
_ => (CrBit.SummaryOverflow, false)
|
||||
};
|
||||
|
||||
return bit != CrBit.SummaryOverflow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Matches the exact text <c>PpcLifter.BuildBoConditionExpression</c> emits
|
||||
/// for a <c>bc</c> form that tests one CR bit and ignores CTR. Anything else
|
||||
/// - including a CTR-decrementing form - is rejected rather than guessed at.
|
||||
/// </summary>
|
||||
private static bool TryParseRawCrBitTest(string? conditionText, out int field, out CrBit bit, out bool testSet)
|
||||
{
|
||||
field = 0;
|
||||
bit = CrBit.SummaryOverflow;
|
||||
testSet = false;
|
||||
if (string.IsNullOrEmpty(conditionText))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var match = RawBoConditionRegex.Match(conditionText);
|
||||
if (!match.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
field = int.Parse(match.Groups["field"].Value) & 7;
|
||||
var bitIndex = int.Parse(match.Groups["bit"].Value);
|
||||
if (bitIndex is < 0 or > 3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bit = (CrBit)bitIndex;
|
||||
testSet = match.Groups["polarity"].Value == "true";
|
||||
return true;
|
||||
}
|
||||
|
||||
private static readonly System.Text.RegularExpressions.Regex RawBoConditionRegex = new(
|
||||
@"^\(\(true\) && \(\(GetCRBit\(ctx, (?<field>\d+), (?<bit>\d+)\) == (?<polarity>true|false)\)\)\)$",
|
||||
System.Text.RegularExpressions.RegexOptions.Compiled |
|
||||
System.Text.RegularExpressions.RegexOptions.CultureInvariant);
|
||||
|
||||
/// <summary>
|
||||
/// Formats a fused comparison. Integer forms keep the natural relational
|
||||
/// operator; float forms keep the PowerPC bit semantics instead, where a
|
||||
/// cleared LT bit means "not less than" and is therefore true for NaN.
|
||||
/// </summary>
|
||||
private static string FusedConditionExpression(
|
||||
FusedCompare compare,
|
||||
RepresentationEnvironment types,
|
||||
Dictionary<string, bool> localPaired)
|
||||
{
|
||||
if (compare.IsFloat)
|
||||
{
|
||||
var leftFloat = ToScalarFloatExpression(compare.Left, types, localPaired);
|
||||
var rightFloat = ToScalarFloatExpression(compare.Right, types, localPaired);
|
||||
return compare.Comparison switch
|
||||
{
|
||||
FusedComparison.Equal => $"({leftFloat} == {rightFloat})",
|
||||
FusedComparison.NotEqual => $"(!({leftFloat} == {rightFloat}))",
|
||||
FusedComparison.Less => $"({leftFloat} < {rightFloat})",
|
||||
FusedComparison.GreaterOrEqual => $"(!({leftFloat} < {rightFloat}))",
|
||||
FusedComparison.Greater => $"({leftFloat} > {rightFloat})",
|
||||
_ => $"(!({leftFloat} > {rightFloat}))"
|
||||
};
|
||||
}
|
||||
|
||||
var cast = compare.IsUnsigned ? "uint32_t" : "int32_t";
|
||||
var left = $"static_cast<{cast}>({ToExpression(compare.Left, types)})";
|
||||
var right = $"static_cast<{cast}>({ToExpression(compare.Right, types)})";
|
||||
var op = compare.Comparison switch
|
||||
{
|
||||
FusedComparison.Equal => "==",
|
||||
FusedComparison.NotEqual => "!=",
|
||||
FusedComparison.Less => "<",
|
||||
FusedComparison.GreaterOrEqual => ">=",
|
||||
FusedComparison.Greater => ">",
|
||||
_ => "<="
|
||||
};
|
||||
|
||||
return $"({left} {op} {right})";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
/// <summary>
|
||||
/// Selects the runtime helper family for guest memory accesses. Checked helpers resolve through the 1 MiB page
|
||||
/// table each access; flat helpers use the 4 GiB guest reservation, so an access is just a byte swap with
|
||||
/// MMIO/EFB/executable-write interception moved to host page protections. Both families behave identically.
|
||||
/// </summary>
|
||||
public sealed partial class CxxLinearCodeGenerator
|
||||
{
|
||||
private enum FlatMemoryHelperFamily
|
||||
{
|
||||
Read,
|
||||
Write,
|
||||
WriteRam
|
||||
}
|
||||
|
||||
// Index order is 8, 16, 32, 64 bits. Keep the literal spellings in one
|
||||
// table: the runtime deliberately exposes separate guarded and proven-RAM
|
||||
// write families, while Bits(sizeBytes) supplies the same normalization at
|
||||
// every call site.
|
||||
private static readonly string[] FlatHelpers =
|
||||
{
|
||||
"MemoryInline::FlatRead8", "MemoryInline::FlatRead16",
|
||||
"MemoryInline::FlatRead32", "MemoryInline::FlatRead64",
|
||||
"MemoryInline::FlatReadFloat8", "MemoryInline::FlatReadFloat16",
|
||||
"MemoryInline::FlatReadFloat32", "MemoryInline::FlatReadFloat64",
|
||||
"MemoryInline::FlatWrite8", "MemoryInline::FlatWrite16",
|
||||
"MemoryInline::FlatWrite32", "MemoryInline::FlatWrite64",
|
||||
"MemoryInline::FlatWriteFloat8", "MemoryInline::FlatWriteFloat16",
|
||||
"MemoryInline::FlatWriteFloat32", "MemoryInline::FlatWriteFloat64",
|
||||
"MemoryInline::FlatWriteRam8", "MemoryInline::FlatWriteRam16",
|
||||
"MemoryInline::FlatWriteRam32", "MemoryInline::FlatWriteRam64",
|
||||
"MemoryInline::FlatWriteRamFloat8", "MemoryInline::FlatWriteRamFloat16",
|
||||
"MemoryInline::FlatWriteRamFloat32", "MemoryInline::FlatWriteRamFloat64"
|
||||
};
|
||||
|
||||
private static int FlatHelperIndex(int bits) => bits switch
|
||||
{
|
||||
8 => 0,
|
||||
16 => 1,
|
||||
64 => 3,
|
||||
_ => 2
|
||||
};
|
||||
|
||||
private static string FlatHelper(FlatMemoryHelperFamily family, int bits, bool isFloat)
|
||||
{
|
||||
var familyIndex = (int)family;
|
||||
if ((uint)familyIndex >= 3u)
|
||||
throw new ArgumentOutOfRangeException(nameof(family), family, null);
|
||||
return FlatHelpers[((familyIndex * 2) + (isFloat ? 1 : 0)) * 4 + FlatHelperIndex(bits)];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper spellings are a closed set of widths and families. Selection is
|
||||
/// table-driven but remains allocation-free at each emitted access.
|
||||
/// </summary>
|
||||
private static string FlatReadHelper(int bits, bool isFloat) =>
|
||||
FlatHelper(FlatMemoryHelperFamily.Read, bits, isFloat);
|
||||
|
||||
private static string FlatWriteHelper(int bits, bool isFloat) =>
|
||||
FlatHelper(FlatMemoryHelperFamily.Write, bits, isFloat);
|
||||
|
||||
/// <summary>
|
||||
/// The exact predicate <c>MemoryInline::FlatWriteNeedsPolicy</c> uses:
|
||||
/// 0xCC000000..0xCDFFFFFF. Kept in sync by construction - the two must never
|
||||
/// disagree, because that is what makes the check-free family sound.
|
||||
/// </summary>
|
||||
private static bool FlatWriteNeedsPolicy(uint address) =>
|
||||
(address & 0xFE000000u) == 0xCC000000u;
|
||||
|
||||
/// <summary>
|
||||
/// A translate-time-constant effective address whose complete span sits
|
||||
/// outside the MMIO policy window, and which therefore provably takes the
|
||||
/// plain-store arm of every <c>FlatWrite*</c> helper.
|
||||
/// </summary>
|
||||
private static bool IsProvenRamStore(uint address, int sizeBytes)
|
||||
{
|
||||
if (sizeBytes <= 0 || sizeBytes > 8) return false;
|
||||
var last = unchecked(address + (uint)(sizeBytes - 1));
|
||||
if (last < address) return false; // wraps past the end of the address space
|
||||
return !FlatWriteNeedsPolicy(address) && !FlatWriteNeedsPolicy(last);
|
||||
}
|
||||
|
||||
private static string GuestLoadHelper(int sizeBytes, bool isFloat) =>
|
||||
FlatReadHelper(Bits(sizeBytes), isFloat);
|
||||
|
||||
private static string GuestStoreHelper(int sizeBytes, bool isFloat) =>
|
||||
FlatWriteHelper(Bits(sizeBytes), isFloat);
|
||||
|
||||
/// <summary>
|
||||
/// Stack slots are ordinary guest memory; the checked variants only skipped
|
||||
/// the executable-write guard because a stack frame can never hold code. The
|
||||
/// flat path has no per-access tables at all, so both collapse to one helper.
|
||||
/// </summary>
|
||||
private static string StackLoadHelper(int sizeBytes, bool isFloat) =>
|
||||
FlatReadHelper(Bits(sizeBytes), isFloat);
|
||||
|
||||
/// <summary>
|
||||
/// Check-free flat store family. Selected only where the caller has already
|
||||
/// proven the effective address is ordinary guest RAM; see the contract on
|
||||
/// <c>MemoryInline::FlatWriteRam*</c> in runtime/include/memory_access.h.
|
||||
/// </summary>
|
||||
private static string FlatWriteRamHelper(int bits, bool isFloat) =>
|
||||
FlatHelper(FlatMemoryHelperFamily.WriteRam, bits, isFloat);
|
||||
|
||||
private static string StackStoreHelper(int sizeBytes, bool isFloat)
|
||||
{
|
||||
// A stack slot is `r1 + constant` with the whole chain back to r1
|
||||
// visible in this body (StackAddressFacts). Guest stack frames
|
||||
// live in MEM1/MEM2 and can never be MMIO, so the per-store MMIO
|
||||
// policy test is provably dead here - and 45% of all emitted flat
|
||||
// stores have this shape.
|
||||
return FlatWriteRamHelper(Bits(sizeBytes), isFloat);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Translator.Core.Ir;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
/// <summary>
|
||||
/// Coalesces consecutive statically-known gather-pipe stores into one <c>GX_HLE_FIFO_WriteBurst</c>
|
||||
/// call instead of one GX_HLE_FIFO_Write* per field, avoiding a display-list state re-walk each time.
|
||||
/// </summary>
|
||||
public sealed partial class CxxLinearCodeGenerator
|
||||
{
|
||||
// Below this the saved call overhead does not pay for the buffer traffic
|
||||
// and the extra stack slot.
|
||||
private const int MinimumGpuFifoBurstStores = 4;
|
||||
|
||||
private readonly record struct GpuFifoStoreInfo(bool IsFloat, int ByteLength);
|
||||
|
||||
private sealed record GpuFifoBurstRun(string BufferName, int TotalBytes);
|
||||
|
||||
private sealed record GpuFifoBurstSlot(
|
||||
GpuFifoBurstRun Run,
|
||||
int ByteOffset,
|
||||
GpuFifoStoreInfo Store);
|
||||
|
||||
private sealed class GpuFifoBurstPlan
|
||||
{
|
||||
public static readonly GpuFifoBurstPlan Empty = new(
|
||||
new Dictionary<(string Block, int Index), GpuFifoBurstSlot>(),
|
||||
new Dictionary<(string Block, int Index), GpuFifoBurstRun>(),
|
||||
Array.Empty<GpuFifoBurstRun>());
|
||||
|
||||
private readonly IReadOnlyDictionary<(string Block, int Index), GpuFifoBurstSlot> _slots;
|
||||
private readonly IReadOnlyDictionary<(string Block, int Index), GpuFifoBurstRun> _completions;
|
||||
|
||||
public GpuFifoBurstPlan(
|
||||
IReadOnlyDictionary<(string Block, int Index), GpuFifoBurstSlot> slots,
|
||||
IReadOnlyDictionary<(string Block, int Index), GpuFifoBurstRun> completions,
|
||||
IReadOnlyList<GpuFifoBurstRun> runs)
|
||||
{
|
||||
_slots = slots;
|
||||
_completions = completions;
|
||||
Runs = runs;
|
||||
}
|
||||
|
||||
public IReadOnlyList<GpuFifoBurstRun> Runs { get; }
|
||||
|
||||
public GpuFifoBurstSlot? Slot(string block, int index) =>
|
||||
_slots.Count != 0 && _slots.TryGetValue((block, index), out var slot) ? slot : null;
|
||||
|
||||
public GpuFifoBurstRun? Completion(string block, int index) =>
|
||||
_completions.Count != 0 && _completions.TryGetValue((block, index), out var run) ? run : null;
|
||||
}
|
||||
|
||||
// The store currently being emitted, if it belongs to a burst run; set around its
|
||||
// EmitInstruction call. Thread-static because translation runs Parallel.For over functions.
|
||||
[ThreadStatic]
|
||||
private static GpuFifoBurstSlot? _activeGpuFifoBurstSlot;
|
||||
|
||||
/// <summary>
|
||||
/// Describes a store the emitter can turn into a direct GX_HLE_FIFO_Write* call; shared by the
|
||||
/// burst planner and direct emitter so a rejected store can't leave a hole in a burst buffer.
|
||||
/// </summary>
|
||||
private static bool TryGetKnownGpuFifoStore(
|
||||
IrStore store,
|
||||
bool isFloatStore,
|
||||
IReadOnlyDictionary<string, uint> knownConstants,
|
||||
out GpuFifoStoreInfo info)
|
||||
{
|
||||
if (!TryGetKnownEffectiveAddress(store.Address, knownConstants, out var address) ||
|
||||
!IsGpuFifoAddress(address))
|
||||
{
|
||||
info = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isFloatStore)
|
||||
{
|
||||
if (store.SizeBytes != 4)
|
||||
{
|
||||
info = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
info = new GpuFifoStoreInfo(IsFloat: true, ByteLength: 4);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (store.SizeBytes is not (1 or 2 or 4))
|
||||
{
|
||||
info = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
info = new GpuFifoStoreInfo(IsFloat: false, ByteLength: store.SizeBytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryClassifyKnownGpuFifoStore(
|
||||
IrInstruction instruction,
|
||||
IReadOnlyDictionary<string, uint> knownConstants,
|
||||
out GpuFifoStoreInfo info)
|
||||
{
|
||||
if (instruction is not IrStore store)
|
||||
{
|
||||
info = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var isFloatStore = store.Source.Kind == "register" &&
|
||||
store.Source.RegisterName is not null &&
|
||||
IsFloatRegister(store.Source.RegisterName);
|
||||
return TryGetKnownGpuFifoStore(store, isFloatStore, knownConstants, out info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instructions a burst run may span: pure register/flag computation that can't read the FIFO,
|
||||
/// fault, or observe the deferred write. Anything else (memory access, calls, control transfer) ends the run.
|
||||
/// </summary>
|
||||
private static bool IsGpuFifoBurstTransparent(IrInstruction instruction) =>
|
||||
instruction is IrAssign or IrBinary or IrComment or IrTracePpc or IrPhi or IrSetCrField;
|
||||
|
||||
private static GpuFifoBurstPlan BuildGpuFifoBurstPlan(
|
||||
IrFunction function,
|
||||
IReadOnlyDictionary<string, uint> knownConstants,
|
||||
bool enabled)
|
||||
{
|
||||
if (!enabled) return GpuFifoBurstPlan.Empty;
|
||||
|
||||
var slots = new Dictionary<(string Block, int Index), GpuFifoBurstSlot>();
|
||||
var completions = new Dictionary<(string Block, int Index), GpuFifoBurstRun>();
|
||||
var runs = new List<GpuFifoBurstRun>();
|
||||
|
||||
foreach (var block in function.Blocks)
|
||||
{
|
||||
var instructions = block.Instructions;
|
||||
var index = 0;
|
||||
while (index < instructions.Count)
|
||||
{
|
||||
if (!TryClassifyKnownGpuFifoStore(instructions[index], knownConstants, out var firstStore))
|
||||
{
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var members = new List<(int Index, GpuFifoStoreInfo Store)> { (index, firstStore) };
|
||||
var scan = index + 1;
|
||||
while (scan < instructions.Count)
|
||||
{
|
||||
if (TryClassifyKnownGpuFifoStore(instructions[scan], knownConstants, out var store))
|
||||
{
|
||||
members.Add((scan, store));
|
||||
scan++;
|
||||
continue;
|
||||
}
|
||||
if (IsGpuFifoBurstTransparent(instructions[scan]))
|
||||
{
|
||||
scan++;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (members.Count >= MinimumGpuFifoBurstStores)
|
||||
{
|
||||
var total = members.Sum(static member => member.Store.ByteLength);
|
||||
var run = new GpuFifoBurstRun($"mkw_fifo_burst_{runs.Count}", total);
|
||||
runs.Add(run);
|
||||
var offset = 0;
|
||||
foreach (var (memberIndex, store) in members)
|
||||
{
|
||||
slots[(block.Label, memberIndex)] = new GpuFifoBurstSlot(run, offset, store);
|
||||
offset += store.ByteLength;
|
||||
}
|
||||
completions[(block.Label, members[^1].Index)] = run;
|
||||
}
|
||||
|
||||
// Every store between the run's first member and `scan` is
|
||||
// already accounted for, so no later run can start before it.
|
||||
index = Math.Max(scan, members[^1].Index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return runs.Count == 0
|
||||
? GpuFifoBurstPlan.Empty
|
||||
: new GpuFifoBurstPlan(slots, completions, runs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes one store's value into its slot, big-endian, matching the bytes the corresponding
|
||||
/// GX_HLE_FIFO_Write* call would push. Evaluated once into a temporary so multi-byte members don't re-evaluate per byte.
|
||||
/// </summary>
|
||||
private static void EmitGpuFifoBurstSlotStore(
|
||||
StringBuilder sb,
|
||||
string pad,
|
||||
GpuFifoBurstSlot slot,
|
||||
string srcExpr)
|
||||
{
|
||||
var buffer = slot.Run.BufferName;
|
||||
if (!slot.Store.IsFloat && slot.Store.ByteLength == 1)
|
||||
{
|
||||
sb.AppendLine($"{pad}{buffer}[{slot.ByteOffset}] = static_cast<uint8_t>({srcExpr});");
|
||||
return;
|
||||
}
|
||||
|
||||
var word = slot.Store.IsFloat
|
||||
? $"PpcBitCastToU32Inline(static_cast<float>({srcExpr}))"
|
||||
: $"static_cast<uint32_t>({srcExpr})";
|
||||
sb.AppendLine($"{pad}{{");
|
||||
sb.AppendLine($"{pad} const uint32_t mkw_fifo_word = {word};");
|
||||
for (var byteIndex = 0; byteIndex < slot.Store.ByteLength; byteIndex++)
|
||||
{
|
||||
var shift = (slot.Store.ByteLength - 1 - byteIndex) * 8;
|
||||
var shifted = shift == 0 ? "mkw_fifo_word" : $"(mkw_fifo_word >> {shift})";
|
||||
sb.AppendLine($"{pad} {buffer}[{slot.ByteOffset + byteIndex}] = static_cast<uint8_t>({shifted});");
|
||||
}
|
||||
sb.AppendLine($"{pad}}}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Translator.Core.Analysis;
|
||||
using Translator.Core.Ir;
|
||||
using Translator.Core.Translation;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
/// <summary>
|
||||
/// Hoists GQR0-7 into locals read once per prologue instead of every psq_l/psq_st reload, re-reading
|
||||
/// only after an mtspr to that GQR or a call not proven to leave it alone. Only the generic helpers
|
||||
/// participate; stack forms have no GQR-value overload and <c>Known</c> forms template the value already.
|
||||
/// </summary>
|
||||
public sealed partial class CxxLinearCodeGenerator
|
||||
{
|
||||
// Translation waves run Parallel.For over functions; ambient emitter state
|
||||
// is per-thread, exactly like the active residency.
|
||||
[ThreadStatic]
|
||||
private static IReadOnlySet<uint>? _hoistedGqrIndices;
|
||||
|
||||
internal static string HoistedGqrName(long index) => $"mkw_gqr{index}";
|
||||
|
||||
private static bool IsGqrHoisted(long index) =>
|
||||
_hoistedGqrIndices is { Count: > 0 } && _hoistedGqrIndices.Contains((uint)index);
|
||||
|
||||
/// <summary>
|
||||
/// The GQR indices whose value a generic PSQ helper in this body would read
|
||||
/// out of the context. Guarded known sites count: their fallback arm is the
|
||||
/// generic helper.
|
||||
/// </summary>
|
||||
private static IReadOnlySet<uint> CollectHoistableGqrIndices(
|
||||
IrFunction function,
|
||||
StackAddressFacts stackFacts,
|
||||
bool enabled)
|
||||
{
|
||||
var indices = new HashSet<uint>();
|
||||
if (!enabled) return indices;
|
||||
|
||||
foreach (var call in function.Blocks.SelectMany(static block => block.Instructions).OfType<IrCall>())
|
||||
{
|
||||
if (!TryGetGenericGqrPsqSite(call, out var index, out var addressArgument)) continue;
|
||||
// The stack forms resolve the host pointer through the frame cache
|
||||
// and have no GQR-value overload in the runtime.
|
||||
if (stackFacts.TryResolve(addressArgument, out _)) continue;
|
||||
indices.Add(index);
|
||||
}
|
||||
|
||||
return indices;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True for the PSQ call shapes that emit a helper reading ctx->gqr[I]:
|
||||
/// the plain PPC_PsqL/PPC_PsqSt forms and the guarded known forms, whose
|
||||
/// else-arm is the plain form.
|
||||
/// </summary>
|
||||
private static bool TryGetGenericGqrPsqSite(IrCall call, out uint index, out IrValue addressArgument)
|
||||
{
|
||||
index = 0;
|
||||
addressArgument = null!;
|
||||
var isLoad = call.Target.Equals("PPC_PsqL", StringComparison.OrdinalIgnoreCase) ||
|
||||
call.Target.StartsWith("PPC_PsqLKnownGuarded_", StringComparison.OrdinalIgnoreCase);
|
||||
var isStore = call.Target.Equals("PPC_PsqSt", StringComparison.OrdinalIgnoreCase) ||
|
||||
call.Target.StartsWith("PPC_PsqStKnownGuarded_", StringComparison.OrdinalIgnoreCase);
|
||||
if (!isLoad && !isStore) return false;
|
||||
var widthArgument = isLoad ? 1 : 2;
|
||||
var indexArgument = isLoad ? 2 : 3;
|
||||
if (call.Arguments.Count <= indexArgument) return false;
|
||||
if (call.Arguments[widthArgument].Constant is not (0 or 1)) return false;
|
||||
if (call.Arguments[indexArgument].Constant is not { } rawIndex || rawIndex is < 0 or > 7) return false;
|
||||
index = (uint)rawIndex;
|
||||
addressArgument = call.Arguments[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void EmitHoistedGqrPrologue(
|
||||
StringBuilder body, IReadOnlySet<uint> hoistedGqrIndices)
|
||||
{
|
||||
if (hoistedGqrIndices.Count == 0) return;
|
||||
foreach (var index in hoistedGqrIndices.OrderBy(static value => value))
|
||||
{
|
||||
// maybe_unused: guarded-store sites become `if constexpr` arms under
|
||||
// _gqr_impl versioning, so one instantiation can discard every use.
|
||||
body.AppendLine($" [[maybe_unused]] uint32_t {HoistedGqrName(index)} = ctx->gqr[{index}];");
|
||||
}
|
||||
body.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-reads hoisted locals after anything that can have written a GQR: an mtspr reloads just
|
||||
/// that register, direct guest calls use the interprocedural write mask, everything else reloads all.
|
||||
/// </summary>
|
||||
private static void EmitHoistedGqrReloads(
|
||||
StringBuilder body,
|
||||
string pad,
|
||||
IrInstruction instruction,
|
||||
IReadOnlySet<uint> hoistedGqrIndices,
|
||||
IReadOnlyDictionary<uint, byte>? gqrCalleeWriteMasks)
|
||||
{
|
||||
if (hoistedGqrIndices.Count == 0) return;
|
||||
|
||||
IEnumerable<uint>? reloaded = null;
|
||||
switch (instruction)
|
||||
{
|
||||
case IrAssign assign when GqrConstantPropagation.TryGetGqrKey(assign.Destination) is { } assignKey:
|
||||
reloaded = new[] { (uint)(assignKey[3] - '0') };
|
||||
break;
|
||||
case IrPhi phi when GqrConstantPropagation.TryGetGqrKey(phi.Destination) is { } phiKey:
|
||||
reloaded = new[] { (uint)(phiKey[3] - '0') };
|
||||
break;
|
||||
case IrCall call when GqrConstantPropagation.TryGuestAddress(call.Target, out var target):
|
||||
{
|
||||
var mask = gqrCalleeWriteMasks is not null &&
|
||||
gqrCalleeWriteMasks.TryGetValue(target, out var knownMask)
|
||||
? knownMask
|
||||
: (byte)0xFF;
|
||||
if (mask == 0) return;
|
||||
reloaded = hoistedGqrIndices.Where(index => (mask & (1 << (int)index)) != 0);
|
||||
break;
|
||||
}
|
||||
case IrCall call:
|
||||
if (!HelperCallCanWriteGqrs(call)) return;
|
||||
reloaded = hoistedGqrIndices;
|
||||
break;
|
||||
case IrIndirectCall:
|
||||
reloaded = hoistedGqrIndices;
|
||||
break;
|
||||
}
|
||||
|
||||
if (reloaded is null) return;
|
||||
foreach (var index in reloaded.Where(hoistedGqrIndices.Contains).OrderBy(static value => value))
|
||||
{
|
||||
body.AppendLine($"{pad}{HoistedGqrName(index)} = ctx->gqr[{index}];");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a non-guest helper call can leave a GQR stale. The catalog is authoritative; anything it
|
||||
/// doesn't model exactly reloads. One case it can't model: the generic SPR writer reaches GQR0-7
|
||||
/// through SPR 912-919, so it must not slip through as if it left them alone.
|
||||
/// </summary>
|
||||
private static bool HelperCallCanWriteGqrs(IrCall call)
|
||||
{
|
||||
if (call.Target.Equals("PPC_WriteSpr", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return call.Arguments.Count == 0 ||
|
||||
call.Arguments[0].Constant is not { } spr ||
|
||||
spr is >= 912 and <= 919;
|
||||
}
|
||||
|
||||
const GuestCallBoundaryFlags opaque =
|
||||
GuestCallBoundaryFlags.RequiresCompleteContext |
|
||||
GuestCallBoundaryFlags.CanSuspend |
|
||||
GuestCallBoundaryFlags.CanSwitchThreads |
|
||||
GuestCallBoundaryFlags.InvokesGuestCode;
|
||||
return (AnalyzeGuestHelperEffect(call).BoundaryFlags & opaque) != 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using System.Text;
|
||||
using Translator.Core.Analysis;
|
||||
using Translator.Core.Analysis.Representation;
|
||||
using Translator.Core.Representation;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
public sealed partial class CxxLinearCodeGenerator
|
||||
{
|
||||
private static bool TryEmitInlineGuestThunk(
|
||||
uint address,
|
||||
StringBuilder sb,
|
||||
string pad,
|
||||
RepresentationEnvironment types,
|
||||
bool frameBaseIsStack)
|
||||
{
|
||||
if (!TryGetInlineGuestThunkSpec(address, out var spec))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var frameBase = RegisterToReadExpression("r11", types);
|
||||
var isFloat = spec.Kind is InlineGuestThunkKind.SaveFpr or InlineGuestThunkKind.RestFpr;
|
||||
var isStore = spec.Kind is InlineGuestThunkKind.SaveGpr or InlineGuestThunkKind.SaveFpr;
|
||||
EmitInlineGuestThunkRange(spec, frameBase, sb, pad, types, frameBaseIsStack, isFloat, isStore);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void EmitInlineGuestThunkRange(
|
||||
InlineGuestThunkSpec spec,
|
||||
string frameBase,
|
||||
StringBuilder sb,
|
||||
string pad,
|
||||
RepresentationEnvironment types,
|
||||
bool frameBaseIsStack,
|
||||
bool isFloat,
|
||||
bool isStore)
|
||||
{
|
||||
var bytesPerRegister = isFloat ? 8 : 4;
|
||||
var helperSize = bytesPerRegister;
|
||||
|
||||
if (frameBaseIsStack)
|
||||
{
|
||||
var firstOffset = (spec.StartRegister - 32) * bytesPerRegister;
|
||||
var length = (32 - spec.StartRegister) * bytesPerRegister;
|
||||
sb.AppendLine($"{pad}{{");
|
||||
var needsRead = isStore ? "false" : "true";
|
||||
var needsWrite = isStore ? "true" : "false";
|
||||
sb.AppendLine($"{pad} uint8_t* const guest_thunk_stack = MemoryInline::ResolveRangeHost(({frameBase} + {firstOffset}), 0, {length}u, {needsRead}, {needsWrite});");
|
||||
for (var reg = spec.StartRegister; reg <= 31; reg++)
|
||||
{
|
||||
var offset = (reg - 32) * bytesPerRegister;
|
||||
var rangeOffset = offset - firstOffset;
|
||||
var registerName = isFloat ? FprRegisterName(reg) : GprRegisterName(reg);
|
||||
var value = isStore
|
||||
? RegisterToReadExpression(registerName, types)
|
||||
: RegisterToWriteExpression(registerName, types);
|
||||
if (isStore)
|
||||
{
|
||||
var storedValue = isFloat ? value : $"static_cast<uint32_t>({value})";
|
||||
var helper = isFloat ? "WriteResolvedFloat64" : "WriteResolved32";
|
||||
sb.AppendLine($"{pad} MemoryInline::{helper}(guest_thunk_stack, {rangeOffset}u, ({frameBase} + {offset}), {storedValue});");
|
||||
}
|
||||
else
|
||||
{
|
||||
var helper = isFloat ? "ReadResolvedFloat64" : "ReadResolved32";
|
||||
sb.AppendLine($"{pad} {value} = MemoryInline::{helper}(guest_thunk_stack, {rangeOffset}u, ({frameBase} + {offset}));");
|
||||
}
|
||||
}
|
||||
sb.AppendLine($"{pad}}}");
|
||||
return;
|
||||
}
|
||||
|
||||
for (var reg = spec.StartRegister; reg <= 31; reg++)
|
||||
{
|
||||
var offset = (reg - 32) * bytesPerRegister;
|
||||
var registerName = isFloat ? FprRegisterName(reg) : GprRegisterName(reg);
|
||||
var value = isStore
|
||||
? RegisterToReadExpression(registerName, types)
|
||||
: RegisterToWriteExpression(registerName, types);
|
||||
if (isStore)
|
||||
{
|
||||
var storedValue = isFloat ? value : $"static_cast<uint32_t>({value})";
|
||||
sb.AppendLine($"{pad}{GuestStoreHelper(helperSize, isFloat)}(({frameBase} + {offset}), {storedValue});");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"{pad}{value} = {GuestLoadHelper(helperSize, isFloat)}(({frameBase} + {offset}));");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasLocallyProvenStackFrameBase(
|
||||
IReadOnlyList<Translator.Core.Ir.IrInstruction> instructions,
|
||||
int callIndex,
|
||||
StackAddressFacts stackFacts)
|
||||
{
|
||||
// EABI save/restore thunks implicitly consume r11, so it is not present
|
||||
// in the ordinary argument list. Prove the thunk's frame base from the
|
||||
// nearest local SSA definition. Standard compiler prologues/epilogues
|
||||
// establish r11 immediately before the thunk; anything less direct
|
||||
// deliberately falls back to generic guest memory.
|
||||
for (var index = callIndex - 1; index >= 0; --index)
|
||||
{
|
||||
var destination = instructions[index] switch
|
||||
{
|
||||
Translator.Core.Ir.IrAssign assign => assign.Destination,
|
||||
Translator.Core.Ir.IrBinary binary => binary.Destination,
|
||||
Translator.Core.Ir.IrLoad load => load.Destination,
|
||||
Translator.Core.Ir.IrCall call => call.Destination,
|
||||
Translator.Core.Ir.IrIndirectCall call => call.Destination,
|
||||
Translator.Core.Ir.IrPhi phi => phi.Destination,
|
||||
_ => null
|
||||
};
|
||||
if (destination is null ||
|
||||
!GetRegisterBaseName(destination).Equals("r11", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return stackFacts.ContainsTemporary(destination);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Translator.Core.Analysis;
|
||||
using Translator.Core.Analysis.Ssa;
|
||||
using Translator.Core.Ir;
|
||||
using Translator.Core.Analysis.Representation;
|
||||
using Translator.Core.Representation;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
public sealed partial class CxxLinearCodeGenerator
|
||||
{
|
||||
private enum InlineGuestThunkKind
|
||||
{
|
||||
SaveGpr,
|
||||
RestGpr,
|
||||
SaveFpr,
|
||||
RestFpr,
|
||||
}
|
||||
|
||||
private readonly record struct InlineGuestThunkSpec(
|
||||
InlineGuestThunkKind Kind,
|
||||
int StartRegister,
|
||||
uint Address);
|
||||
|
||||
/// <summary>
|
||||
/// Indentation prefixes. <c>EmitInstruction</c> built one of these per
|
||||
/// emitted instruction; the table is indexed by nesting depth and produces
|
||||
/// exactly the same text.
|
||||
/// </summary>
|
||||
private static readonly string[] IndentPads = CreateIndentPads();
|
||||
|
||||
private static string[] CreateIndentPads()
|
||||
{
|
||||
var pads = new string[10];
|
||||
for (var i = 0; i < pads.Length; i++)
|
||||
{
|
||||
pads[i] = new string(' ', i * 4);
|
||||
}
|
||||
|
||||
return pads;
|
||||
}
|
||||
|
||||
private static string IndentPad(int indent) =>
|
||||
(uint)indent < (uint)IndentPads.Length ? IndentPads[indent] : new string(' ', indent * 4);
|
||||
|
||||
/// <summary>
|
||||
/// Single entry point for helper-effect lookups from the emitter. Not memoized: the catalog
|
||||
/// already resolves a target via one frozen-dictionary probe and hands out shared instances,
|
||||
/// so a call-site memo would just duplicate the catalog's own lookup for no benefit.
|
||||
/// </summary>
|
||||
private static Translator.Core.Analysis.GuestHelperEffect AnalyzeGuestHelperEffect(IrCall call) =>
|
||||
Translator.Core.Analysis.GuestHelperEffectCatalog.Analyze(call);
|
||||
|
||||
private static string SanitizeLabel(string label)
|
||||
{
|
||||
var clean = label.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? label[2..] : label;
|
||||
clean = clean.Replace(":", "_").Replace("-", "_");
|
||||
return $"loc_{clean}";
|
||||
}
|
||||
|
||||
private static string SanitizeIdentifier(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return "_";
|
||||
}
|
||||
|
||||
var trimmed = name.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
||||
? name[2..]
|
||||
: name;
|
||||
|
||||
var sb = new StringBuilder(trimmed.Length + 8);
|
||||
foreach (var ch in trimmed)
|
||||
{
|
||||
sb.Append(char.IsLetterOrDigit(ch) || ch == '_' ? ch : '_');
|
||||
}
|
||||
|
||||
var result = sb.ToString();
|
||||
if (result.Length == 0 || !(char.IsLetter(result[0]) || result[0] == '_'))
|
||||
{
|
||||
result = "func_" + result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string FormatSymbol(string target) => SanitizeIdentifier(target);
|
||||
|
||||
private static bool IsGuestCallTarget(string target)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GuestTargetParser.TryParseAddress(target, out _))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var trimmed = target.Trim();
|
||||
if (trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return trimmed.StartsWith("func_", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool TryParseAddress(string target, out uint address) =>
|
||||
GuestTargetParser.TryParseAddress(target, out address);
|
||||
|
||||
private static bool IsGuestScalarFloatArgument(
|
||||
IGuestFunctionAbiProvider guestAbiProvider,
|
||||
string target,
|
||||
string registerName)
|
||||
{
|
||||
var baseName = GetRegisterBaseName(registerName);
|
||||
if (!IsAbiFloatArgumentRegister(baseName) ||
|
||||
!guestAbiProvider.TryGetGuestFunctionAbi(target, out var abi))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return abi.HasScalarFloatArgument(baseName);
|
||||
}
|
||||
|
||||
private static bool IsGuestPairedScalarFloatReturn(
|
||||
IGuestFunctionAbiProvider guestAbiProvider,
|
||||
string target) =>
|
||||
guestAbiProvider.TryGetGuestFunctionAbi(target, out var abi) &&
|
||||
abi.ReturnsPairedScalarFloat;
|
||||
|
||||
private static bool IsPairedProducerTarget(string target) =>
|
||||
PpcFloatCallSemantics.IsPairedProducerTarget(target);
|
||||
|
||||
private static bool IsPairedConsumerTarget(string target) =>
|
||||
PpcFloatCallSemantics.IsPairedConsumerTarget(target);
|
||||
|
||||
private static bool IsSinglePrecisionConsumerTarget(string target) =>
|
||||
PpcFloatCallSemantics.IsSinglePrecisionConsumerTarget(target);
|
||||
|
||||
private static bool IsScalarFloatConsumerTarget(string target) =>
|
||||
PpcFloatCallSemantics.IsScalarFloatConsumerTarget(target);
|
||||
|
||||
private static bool IsPairedFloatValue(
|
||||
IrValue value,
|
||||
IReadOnlyDictionary<string, bool> localPaired)
|
||||
{
|
||||
if (value is not { Kind: "register", RegisterName: { } register } ||
|
||||
!IsFloatRegister(register))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return localPaired.TryGetValue(GetRegisterBaseName(register), out var paired) && paired;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Materializes the scalar ps0 value when a scalar floating-point consumer
|
||||
/// reads an FPR whose current authoritative representation is a packed pair.
|
||||
/// </summary>
|
||||
private static string ToScalarFloatExpression(
|
||||
IrValue value,
|
||||
RepresentationEnvironment types,
|
||||
IReadOnlyDictionary<string, bool> localPaired)
|
||||
{
|
||||
var expression = ToExpression(value, types);
|
||||
return IsPairedFloatValue(value, localPaired)
|
||||
? $"PPC_PsToScalarInline({expression})"
|
||||
: expression;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Materializes a packed pair when a paired-single consumer reads a scalar FPR.
|
||||
/// </summary>
|
||||
private static string ToPairedFloatExpression(
|
||||
IrValue value,
|
||||
RepresentationEnvironment types,
|
||||
IReadOnlyDictionary<string, bool> localPaired,
|
||||
bool fpscrNiDisabled = false)
|
||||
{
|
||||
var expression = ToExpression(value, types);
|
||||
return IsPairedFloatValue(value, localPaired)
|
||||
? expression
|
||||
: fpscrNiDisabled
|
||||
? $"PPC_PsFromScalarNoNiInline({expression})"
|
||||
: $"PPC_PsFromScalarInline({expression})";
|
||||
}
|
||||
|
||||
private static string FprStorageExpression(string register)
|
||||
{
|
||||
var index = ParseFloatRegisterIndex(register);
|
||||
if (index is < 0 or >= 32)
|
||||
throw new InvalidOperationException($"Invalid FPR register '{register}'.");
|
||||
return _activeResidency is null
|
||||
? $"ctx->fpr[{index}]"
|
||||
: _activeResidency.FprStorage(index, written: true);
|
||||
}
|
||||
|
||||
private static string PairedAssignment(string destination, string expression, RepresentationEnvironment types)
|
||||
{
|
||||
if (IsFloatRegister(destination))
|
||||
return $"PpcSetPairedFprInline({FprStorageExpression(destination)}, {expression});";
|
||||
return $"{ToDestination(destination, types)} = {expression};";
|
||||
}
|
||||
|
||||
private static bool TryGetInlineGuestThunkSpec(uint address, out InlineGuestThunkSpec spec)
|
||||
{
|
||||
var thunks = GuestSaveRestoreThunks.Current;
|
||||
if (TryGetInlineGuestThunkRangeSpec(address, thunks.SaveGpr, InlineGuestThunkKind.SaveGpr, out spec))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryGetInlineGuestThunkRangeSpec(address, thunks.RestGpr, InlineGuestThunkKind.RestGpr, out spec))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryGetInlineGuestThunkRangeSpec(address, thunks.SaveFpr, InlineGuestThunkKind.SaveFpr, out spec))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryGetInlineGuestThunkRangeSpec(address, thunks.RestFpr, InlineGuestThunkKind.RestFpr, out spec))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
spec = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetInlineGuestThunkRangeSpec(
|
||||
uint address,
|
||||
GuestSaveRestoreThunkRange? range,
|
||||
InlineGuestThunkKind kind,
|
||||
out InlineGuestThunkSpec spec)
|
||||
{
|
||||
if (range is null)
|
||||
{
|
||||
spec = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var (baseAddress, startRegister, endRegister) = range;
|
||||
var rangeBytes = checked((uint)((endRegister - startRegister) * 4));
|
||||
if (address < baseAddress || address > (baseAddress + rangeBytes) || ((address - baseAddress) & 0x3) != 0)
|
||||
{
|
||||
spec = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
spec = new InlineGuestThunkSpec(
|
||||
kind,
|
||||
startRegister + (int)((address - baseAddress) / 4),
|
||||
address);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string RequireLabel(string? label, Dictionary<string, string> labelNames, string fromLabel, string functionName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(label))
|
||||
{
|
||||
throw new InvalidOperationException($"Block '{fromLabel}' in '{functionName}' references an empty target label.");
|
||||
}
|
||||
|
||||
if (!labelNames.TryGetValue(label, out var existing) || string.IsNullOrWhiteSpace(existing))
|
||||
{
|
||||
throw new InvalidOperationException($"Block '{fromLabel}' in '{functionName}' references missing target '{label}'.");
|
||||
}
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
private static string EscapeForCxxLiteral(string text) => text
|
||||
.Replace("\\", "\\\\")
|
||||
.Replace("\"", "\\\"");
|
||||
}
|
||||
@@ -0,0 +1,544 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Translator.Core.Analysis;
|
||||
using Translator.Core.Ir;
|
||||
using Translator.Core.Representation;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
public sealed partial class CxxLinearCodeGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to emit inline C++ code for a PPC helper call instead of generating a function call.
|
||||
/// Returns true if inline code was emitted, false if the caller should fall back to function call.
|
||||
/// </summary>
|
||||
private static bool TryEmitInlinePpc(
|
||||
IrCall call,
|
||||
StringBuilder sb,
|
||||
string pad,
|
||||
RepresentationEnvironment types,
|
||||
Dictionary<string, bool> localPaired,
|
||||
StackAddressFacts stackFacts)
|
||||
{
|
||||
var target = call.Target;
|
||||
if (string.IsNullOrWhiteSpace(target))
|
||||
return false;
|
||||
|
||||
// Get destination if any
|
||||
var hasDest = !string.IsNullOrWhiteSpace(call.Destination);
|
||||
var dest = hasDest ? ToDestination(call.Destination!, types) : null;
|
||||
|
||||
void EmitPairedResult(string expression)
|
||||
{
|
||||
if (!hasDest) return;
|
||||
sb.AppendLine($"{pad}{PairedAssignment(call.Destination!, expression, types)}");
|
||||
}
|
||||
|
||||
// Helper to get argument expression
|
||||
string GetArg(int index)
|
||||
{
|
||||
if (index >= call.Arguments.Count)
|
||||
return "0";
|
||||
return ToExpression(call.Arguments[index], types);
|
||||
}
|
||||
|
||||
bool TryGetConstArg(int index, out long value)
|
||||
{
|
||||
value = 0;
|
||||
if (index >= call.Arguments.Count)
|
||||
return false;
|
||||
var argVal = call.Arguments[index];
|
||||
if (argVal.Kind != "const" || !argVal.Constant.HasValue)
|
||||
return false;
|
||||
value = argVal.Constant.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wrap scalar in PsFromScalar if needed for paired consumer
|
||||
string WrapForPaired(int index, bool fpscrNiDisabled = false)
|
||||
{
|
||||
if (index >= call.Arguments.Count)
|
||||
return "0";
|
||||
return ToPairedFloatExpression(
|
||||
call.Arguments[index], types, localPaired, fpscrNiDisabled);
|
||||
}
|
||||
|
||||
// Extract ps0 for scalar consumers if the source is paired
|
||||
string WrapForScalar(int index)
|
||||
{
|
||||
if (index >= call.Arguments.Count)
|
||||
return "0";
|
||||
return ToScalarFloatExpression(call.Arguments[index], types, localPaired);
|
||||
}
|
||||
|
||||
var guardedKnown = target.StartsWith("PPC_PsqLKnownGuarded_", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.StartsWith("PPC_PsqStKnownGuarded_", StringComparison.OrdinalIgnoreCase);
|
||||
var knownPrefix = target.StartsWith("PPC_PsqLKnownGuarded_", StringComparison.OrdinalIgnoreCase)
|
||||
? "PPC_PsqLKnownGuarded_"
|
||||
: target.StartsWith("PPC_PsqStKnownGuarded_", StringComparison.OrdinalIgnoreCase)
|
||||
? "PPC_PsqStKnownGuarded_"
|
||||
: target.StartsWith("PPC_PsqLKnown_", StringComparison.OrdinalIgnoreCase)
|
||||
? "PPC_PsqLKnown_"
|
||||
: target.StartsWith("PPC_PsqStKnown_", StringComparison.OrdinalIgnoreCase)
|
||||
? "PPC_PsqStKnown_"
|
||||
: null;
|
||||
if (knownPrefix != null &&
|
||||
uint.TryParse(target.AsSpan(knownPrefix.Length), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var knownGqr))
|
||||
{
|
||||
var knownLoad = knownPrefix.StartsWith("PPC_PsqL", StringComparison.OrdinalIgnoreCase);
|
||||
var wIndex = knownLoad ? 1 : 2;
|
||||
var iIndex = knownLoad ? 2 : 3;
|
||||
if (TryGetConstArg(wIndex, out var knownW) && TryGetConstArg(iIndex, out var knownI) &&
|
||||
knownW is 0 or 1 && knownI is >= 0 and < 8)
|
||||
{
|
||||
var address = GetArg(0);
|
||||
var stack = stackFacts.TryResolve(call.Arguments[0], out _);
|
||||
// The guard's else-arm is the generic helper, so it takes the
|
||||
// hoisted GQR local exactly like an unguarded generic site.
|
||||
var hoistedFallback = !stack && IsGqrHoisted(knownI);
|
||||
if (knownLoad && hasDest)
|
||||
{
|
||||
var helper = stack
|
||||
? "PPC_PsqLKnownStackInline"
|
||||
: "PPC_PsqLKnownInline";
|
||||
var knownCall = $"{helper}<{knownW}u, {knownI}u, 0x{knownGqr:X8}u>(ctx, {address})";
|
||||
var knownExpression = knownCall;
|
||||
if (guardedKnown)
|
||||
{
|
||||
var fallback = hoistedFallback
|
||||
? $"PPC_PsqLGqrInline<{knownW}u, {knownI}u>(ctx, {HoistedGqrName(knownI)}, {address})"
|
||||
: $"{(stack ? "PPC_PsqLStackInline" : "PPC_PsqLInline")}<{knownW}u, {knownI}u>(ctx, {address})";
|
||||
knownExpression = $"{GqrEntryGuardName((uint)knownI, knownGqr)} ? {knownCall} : {fallback}";
|
||||
}
|
||||
EmitPairedResult(knownExpression);
|
||||
return true;
|
||||
}
|
||||
if (!knownLoad)
|
||||
{
|
||||
var helper = stack
|
||||
? "PPC_PsqStKnownStackInline"
|
||||
: "PPC_PsqStKnownInline";
|
||||
var value = WrapForPaired(1);
|
||||
if (guardedKnown)
|
||||
{
|
||||
var fallback = hoistedFallback
|
||||
? $"PPC_PsqStGqrInline<{knownW}u, {knownI}u>(ctx, {HoistedGqrName(knownI)}, {address}, {value})"
|
||||
: $"{(stack ? "PPC_PsqStStackInline" : "PPC_PsqStInline")}<{knownW}u, {knownI}u>(ctx, {address}, {value})";
|
||||
sb.AppendLine($"{pad}if ({GqrEntryGuardName((uint)knownI, knownGqr)}) {helper}<{knownW}u, {knownI}u, 0x{knownGqr:X8}u>(ctx, {address}, {value});");
|
||||
sb.AppendLine($"{pad}else {fallback};");
|
||||
}
|
||||
else sb.AppendLine($"{pad}{helper}<{knownW}u, {knownI}u, 0x{knownGqr:X8}u>(ctx, {address}, {value});");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (InlinePairedCallSpecs.TryGetValue(target, out var pairedSpec))
|
||||
{
|
||||
if (hasDest)
|
||||
{
|
||||
var arguments = new string[pairedSpec.ArgumentOrder.Length];
|
||||
for (var i = 0; i < arguments.Length; i++)
|
||||
{
|
||||
arguments[i] = WrapForPaired(
|
||||
pairedSpec.ArgumentOrder[i], pairedSpec.NoNiVariant);
|
||||
}
|
||||
|
||||
EmitPairedResult($"{pairedSpec.Helper}({string.Join(", ", arguments)})");
|
||||
}
|
||||
|
||||
// The hand-written arithmetic cases historically returned true
|
||||
// without a destination, suppressing a malformed helper call. Keep
|
||||
// that behavior in the table-driven path.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (InlineScalarCallSpecs.TryGetValue(target, out var scalarSpec))
|
||||
{
|
||||
if (hasDest)
|
||||
{
|
||||
var arguments = new string[scalarSpec.ArgumentOrder.Length];
|
||||
for (var i = 0; i < arguments.Length; i++)
|
||||
{
|
||||
arguments[i] = WrapForScalar(scalarSpec.ArgumentOrder[i]);
|
||||
}
|
||||
|
||||
var expression = scalarSpec.Kind switch
|
||||
{
|
||||
ScalarInlineKind.HelperCall =>
|
||||
$"{scalarSpec.Helper}({string.Join(", ", arguments)})",
|
||||
ScalarInlineKind.RoundedBinary =>
|
||||
$"static_cast<double>({(scalarSpec.NoNiVariant
|
||||
? $"static_cast<float>({arguments[0]} {scalarSpec.Operator} {arguments[1]})"
|
||||
: $"PpcForceSingleValueInline({arguments[0]} {scalarSpec.Operator} {arguments[1]})")})",
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Unknown scalar inline kind '{scalarSpec.Kind}'.")
|
||||
};
|
||||
sb.AppendLine($"{pad}{dest} = {expression};");
|
||||
}
|
||||
|
||||
// Match the old scalar helper cases: a missing destination is a
|
||||
// handled call that emits no statement.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Match and inline specific PPC helpers
|
||||
switch (InlineHelperSwitchKey(target))
|
||||
{
|
||||
// fcmpo/fcmpu lower to IrSetCrField today, but the PPC_Fcmp helper
|
||||
// writes a CR field through the thread-local CpuContext, which a
|
||||
// resident CR local can never observe. Keep a resident expansion as
|
||||
// a safety net in case a lifter path ever produces the helper form.
|
||||
case "PPC_FCMP":
|
||||
if (_activeResidency is null || !TryGetConstArg(0, out var fcmpField))
|
||||
return false;
|
||||
sb.AppendLine($"{pad}SetCRFloatResident({_activeResidency.Cr(written: true)}, {fcmpField & 7}, {WrapForScalar(1)}, {WrapForScalar(2)});");
|
||||
return true;
|
||||
|
||||
case "PPC_CRSETBIT":
|
||||
if (!hasDest)
|
||||
return false;
|
||||
sb.AppendLine($"{pad}{dest} = PpcCrSetBitResident({dest}, static_cast<uint32_t>({GetArg(0)}), static_cast<uint32_t>({GetArg(1)}));");
|
||||
return true;
|
||||
|
||||
case "PPC_CRLOGICAL":
|
||||
if (!hasDest)
|
||||
return false;
|
||||
sb.AppendLine($"{pad}{dest} = PpcCrLogicalResident({dest}, static_cast<uint32_t>({GetArg(0)}), static_cast<uint32_t>({GetArg(1)}), static_cast<uint32_t>({GetArg(2)}), static_cast<uint32_t>({GetArg(3)}));");
|
||||
return true;
|
||||
|
||||
case "PPC_MCRF":
|
||||
if (!hasDest)
|
||||
return false;
|
||||
sb.AppendLine($"{pad}{dest} = PpcMcrfResident({dest}, static_cast<uint32_t>({GetArg(0)}), static_cast<uint32_t>({GetArg(1)}));");
|
||||
return true;
|
||||
|
||||
case "PPC_CNTLZW":
|
||||
if (!hasDest)
|
||||
return false;
|
||||
sb.AppendLine($"{pad}{dest} = PPC_CntlzwInline(static_cast<uint32_t>({GetArg(0)}));");
|
||||
return true;
|
||||
|
||||
case "PPC_GETCARRY":
|
||||
if (!hasDest)
|
||||
return false;
|
||||
{
|
||||
var carrySource = _activeResidency is null
|
||||
? "ctx->xer"
|
||||
: _activeResidency.Xer(written: false);
|
||||
sb.AppendLine($"{pad}{dest} = ({carrySource} >> 29) & 1u;");
|
||||
}
|
||||
return true;
|
||||
|
||||
case "PPC_UPDATECARRYSUB":
|
||||
if (!hasDest)
|
||||
return false;
|
||||
sb.AppendLine($"{pad}{dest} = ({dest} & 0xDFFFFFFFu) | " +
|
||||
$"((static_cast<uint32_t>({GetArg(0)}) >= static_cast<uint32_t>({GetArg(1)}) ? 1u : 0u) << 29);");
|
||||
return true;
|
||||
|
||||
case "PPC_UPDATECARRYADD":
|
||||
if (!hasDest)
|
||||
return false;
|
||||
sb.AppendLine($"{pad}{{");
|
||||
sb.AppendLine($"{pad} const uint64_t ppcCarryWide = static_cast<uint64_t>(static_cast<uint32_t>({GetArg(0)})) + " +
|
||||
$"static_cast<uint64_t>(static_cast<uint32_t>({GetArg(1)})) + " +
|
||||
$"(static_cast<uint64_t>(static_cast<uint32_t>({GetArg(2)})) & 1u);");
|
||||
sb.AppendLine($"{pad} {dest} = ({dest} & 0xDFFFFFFFu) | " +
|
||||
"(static_cast<uint32_t>((ppcCarryWide >> 32) & 1u) << 29);");
|
||||
sb.AppendLine($"{pad}}}");
|
||||
return true;
|
||||
|
||||
case "PPC_UPDATECARRYSHIFTRIGHT":
|
||||
if (!hasDest)
|
||||
return false;
|
||||
sb.AppendLine($"{pad}{{");
|
||||
sb.AppendLine($"{pad} const uint32_t ppcCarryValue = static_cast<uint32_t>({GetArg(0)});");
|
||||
sb.AppendLine($"{pad} const uint32_t ppcCarryShift = static_cast<uint32_t>({GetArg(1)}) & 0x3Fu;");
|
||||
sb.AppendLine($"{pad} const bool ppcCarryNegative = (ppcCarryValue & 0x80000000u) != 0;");
|
||||
sb.AppendLine($"{pad} const uint32_t ppcCarry = ppcCarryShift == 0 ? 0u : " +
|
||||
"(ppcCarryShift >= 32 ? (ppcCarryNegative && ppcCarryValue != 0 ? 1u : 0u) : " +
|
||||
"(ppcCarryNegative && (ppcCarryValue & ((1u << ppcCarryShift) - 1u)) != 0 ? 1u : 0u));");
|
||||
sb.AppendLine($"{pad} {dest} = ({dest} & 0xDFFFFFFFu) | (ppcCarry << 29);");
|
||||
sb.AppendLine($"{pad}}}");
|
||||
return true;
|
||||
|
||||
case "PPC_PSFROMSCALAR":
|
||||
// PPC_PsFromScalar(value): Pack scalar float into both ps0 and ps1
|
||||
if (hasDest)
|
||||
{
|
||||
var arg = WrapForScalar(0);
|
||||
EmitPairedResult($"PPC_PsFromScalarInline({arg})");
|
||||
}
|
||||
return true;
|
||||
|
||||
case "PPC_PSTOSCALAR":
|
||||
// PPC_PsToScalar(value): Extract ps0 as double
|
||||
if (hasDest)
|
||||
{
|
||||
var arg = WrapForScalar(0);
|
||||
sb.AppendLine($"{pad}{dest} = {arg};");
|
||||
}
|
||||
return true;
|
||||
|
||||
case "PPC_PSMR":
|
||||
// ps_mr owns both lanes; do not lower it through the scalar
|
||||
// IrAssign path used by fmr.
|
||||
if (hasDest)
|
||||
EmitPairedResult(GetArg(0));
|
||||
return true;
|
||||
|
||||
case "PPC_PSCMPO0":
|
||||
case "PPC_PSCMPU0":
|
||||
case "PPC_PSCMPO1":
|
||||
case "PPC_PSCMPU1":
|
||||
{
|
||||
// ps_cmpX lifts with an empty destination and the CR field as argument 0, so the
|
||||
// resident CR local is the real destination. Falling through to the PPC_PsCmpX helper
|
||||
// on a missing destination wrote CR through CpuContext instead, invisible to the
|
||||
// resident local, which froze ps_cmp-gated loops and flipped branch decisions.
|
||||
var lane = target.EndsWith("1", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
|
||||
var laneHelper = lane == 0 ? "PpcGetPs0Inline" : "PpcGetPs1Inline";
|
||||
var left = WrapForPaired(1);
|
||||
var right = WrapForPaired(2);
|
||||
var crDestination = hasDest
|
||||
? dest
|
||||
: _activeResidency?.Cr(written: true);
|
||||
if (crDestination is null)
|
||||
return false;
|
||||
sb.AppendLine($"{pad}SetCRFloatResident({crDestination}, static_cast<uint32_t>({GetArg(0)}) & 7u, " +
|
||||
$"{laneHelper}({left}), {laneHelper}({right}));");
|
||||
return true;
|
||||
}
|
||||
|
||||
case "PPC_PSQL":
|
||||
if (hasDest &&
|
||||
TryGetConstArg(1, out var loadW) &&
|
||||
TryGetConstArg(2, out var loadI) &&
|
||||
loadW is 0 or 1 &&
|
||||
loadI is >= 0 and < 8)
|
||||
{
|
||||
var addr = GetArg(0);
|
||||
var stackLoad = stackFacts.TryResolve(call.Arguments[0], out _);
|
||||
EmitPairedResult(!stackLoad && IsGqrHoisted(loadI)
|
||||
? $"PPC_PsqLGqrInline<{loadW}u, {loadI}u>(ctx, {HoistedGqrName(loadI)}, {addr})"
|
||||
: $"{(stackLoad ? "PPC_PsqLStackInline" : "PPC_PsqLInline")}<{loadW}u, {loadI}u>(ctx, {addr})");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case "PPC_PSQST":
|
||||
if (TryGetConstArg(2, out var storeW) &&
|
||||
TryGetConstArg(3, out var storeI) &&
|
||||
storeW is 0 or 1 &&
|
||||
storeI is >= 0 and < 8)
|
||||
{
|
||||
var addr = GetArg(0);
|
||||
var value = WrapForPaired(1);
|
||||
var stackStore = stackFacts.TryResolve(call.Arguments[0], out _);
|
||||
sb.AppendLine(!stackStore && IsGqrHoisted(storeI)
|
||||
? $"{pad}PPC_PsqStGqrInline<{storeW}u, {storeI}u>(ctx, {HoistedGqrName(storeI)}, {addr}, {value});"
|
||||
: $"{pad}{(stackStore ? "PPC_PsqStStackInline" : "PPC_PsqStInline")}<{storeW}u, {storeI}u>(ctx, {addr}, {value});");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
default:
|
||||
// Not handled - fall back to function call
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private enum ScalarInlineKind
|
||||
{
|
||||
HelperCall,
|
||||
RoundedBinary,
|
||||
}
|
||||
|
||||
private readonly record struct PairedInlineSpec(
|
||||
string Helper,
|
||||
bool NoNiVariant,
|
||||
int[] ArgumentOrder);
|
||||
|
||||
private readonly record struct ScalarInlineSpec(
|
||||
ScalarInlineKind Kind,
|
||||
string? Helper,
|
||||
string? Operator,
|
||||
bool NoNiVariant,
|
||||
int[] ArgumentOrder);
|
||||
|
||||
/// <summary>
|
||||
/// Descriptor table for paired arithmetic helpers. NoNI aliases are
|
||||
/// explicit entries so the helper name and scalar-to-paired conversion mode
|
||||
/// cannot drift independently.
|
||||
/// </summary>
|
||||
private static readonly IReadOnlyDictionary<string, PairedInlineSpec> InlinePairedCallSpecs =
|
||||
BuildPairedInlineCallSpecs();
|
||||
|
||||
private static IReadOnlyDictionary<string, PairedInlineSpec> BuildPairedInlineCallSpecs()
|
||||
{
|
||||
var specs = new Dictionary<string, PairedInlineSpec>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static void Add(
|
||||
Dictionary<string, PairedInlineSpec> destination,
|
||||
string target,
|
||||
string helper,
|
||||
bool noNiVariant,
|
||||
params int[] argumentOrder) =>
|
||||
destination.Add(
|
||||
target,
|
||||
new PairedInlineSpec(helper, noNiVariant, argumentOrder));
|
||||
|
||||
static void AddNoNi(
|
||||
Dictionary<string, PairedInlineSpec> destination,
|
||||
string target,
|
||||
string noNiTarget,
|
||||
string helper,
|
||||
string noNiHelper,
|
||||
params int[] argumentOrder)
|
||||
{
|
||||
Add(destination, target, helper, false, argumentOrder);
|
||||
Add(destination, noNiTarget, noNiHelper, true, argumentOrder);
|
||||
}
|
||||
|
||||
Add(specs, "PPC_PSMERGE00", "PPC_PsMerge00Inline", false, 0, 1);
|
||||
Add(specs, "PPC_PSMERGE01", "PPC_PsMerge01Inline", false, 0, 1);
|
||||
Add(specs, "PPC_PSMERGE10", "PPC_PsMerge10Inline", false, 0, 1);
|
||||
Add(specs, "PPC_PSMERGE11", "PPC_PsMerge11Inline", false, 0, 1);
|
||||
|
||||
AddNoNi(
|
||||
specs, "PPC_PSADD", "PPC_PSADDNONI",
|
||||
"PPC_PsAddInline", "PPC_PsAddNoNiInline", 0, 1);
|
||||
AddNoNi(
|
||||
specs, "PPC_PSSUB", "PPC_PSSUBNONI",
|
||||
"PPC_PsSubInline", "PPC_PsSubNoNiInline", 0, 1);
|
||||
Add(specs, "PPC_PSDIV", "PPC_PsDivInline", false, 0, 1);
|
||||
AddNoNi(
|
||||
specs, "PPC_PSMUL", "PPC_PSMULNONI",
|
||||
"PPC_PsMulInline", "PPC_PsMulNoNiInline", 0, 1);
|
||||
|
||||
Add(specs, "PPC_PSNEG", "PPC_PsNegInline", false, 0);
|
||||
Add(specs, "PPC_PSABS", "PPC_PsAbsInline", false, 0);
|
||||
Add(specs, "PPC_PSMULS0", "PPC_PsMuls0Inline", false, 0, 1);
|
||||
Add(specs, "PPC_PSMULS1", "PPC_PsMuls1Inline", false, 0, 1);
|
||||
|
||||
AddNoNi(
|
||||
specs, "PPC_PSMADD", "PPC_PSMADDNONI",
|
||||
"PPC_PsMaddInline", "PPC_PsMaddNoNiInline", 0, 1, 2);
|
||||
AddNoNi(
|
||||
specs, "PPC_PSMSUB", "PPC_PSMSUBNONI",
|
||||
"PPC_PsMsubInline", "PPC_PsMsubNoNiInline", 0, 1, 2);
|
||||
AddNoNi(
|
||||
specs, "PPC_PSNMSUB", "PPC_PSNMSUBNONI",
|
||||
"PPC_PsNmsubInline", "PPC_PsNmsubNoNiInline", 0, 1, 2);
|
||||
Add(specs, "PPC_PSNMADD", "PPC_PsNmaddInline", false, 0, 1, 2);
|
||||
Add(specs, "PPC_PSMADDS0", "PPC_PsMadds0Inline", false, 0, 1, 2);
|
||||
Add(specs, "PPC_PSMADDS1", "PPC_PsMadds1Inline", false, 0, 1, 2);
|
||||
Add(specs, "PPC_PSSUM0", "PPC_PsSum0Inline", false, 0, 1, 2);
|
||||
Add(specs, "PPC_PSSUM1", "PPC_PsSum1Inline", false, 0, 1, 2);
|
||||
|
||||
return specs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Descriptor table for scalar arithmetic helpers. Rounded binary operators
|
||||
/// retain their distinct NI and NoNI expression forms; helper-call entries
|
||||
/// retain the runtime primitive names directly.
|
||||
/// </summary>
|
||||
private static readonly IReadOnlyDictionary<string, ScalarInlineSpec> InlineScalarCallSpecs =
|
||||
BuildScalarInlineCallSpecs();
|
||||
|
||||
private static IReadOnlyDictionary<string, ScalarInlineSpec> BuildScalarInlineCallSpecs()
|
||||
{
|
||||
var specs = new Dictionary<string, ScalarInlineSpec>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
static void AddHelper(
|
||||
Dictionary<string, ScalarInlineSpec> destination,
|
||||
string target,
|
||||
string helper,
|
||||
params int[] argumentOrder) =>
|
||||
destination.Add(
|
||||
target,
|
||||
new ScalarInlineSpec(
|
||||
ScalarInlineKind.HelperCall,
|
||||
helper,
|
||||
Operator: null,
|
||||
NoNiVariant: false,
|
||||
ArgumentOrder: argumentOrder));
|
||||
|
||||
static void AddHelperNoNi(
|
||||
Dictionary<string, ScalarInlineSpec> destination,
|
||||
string target,
|
||||
string noNiTarget,
|
||||
string helper,
|
||||
string noNiHelper,
|
||||
params int[] argumentOrder)
|
||||
{
|
||||
AddHelper(destination, target, helper, argumentOrder);
|
||||
destination.Add(
|
||||
noNiTarget,
|
||||
new ScalarInlineSpec(
|
||||
ScalarInlineKind.HelperCall,
|
||||
noNiHelper,
|
||||
Operator: null,
|
||||
NoNiVariant: true,
|
||||
ArgumentOrder: argumentOrder));
|
||||
}
|
||||
|
||||
static void AddRoundedBinary(
|
||||
Dictionary<string, ScalarInlineSpec> destination,
|
||||
string target,
|
||||
string noNiTarget,
|
||||
string @operator)
|
||||
{
|
||||
destination.Add(
|
||||
target,
|
||||
new ScalarInlineSpec(
|
||||
ScalarInlineKind.RoundedBinary,
|
||||
Helper: null,
|
||||
Operator: @operator,
|
||||
NoNiVariant: false,
|
||||
ArgumentOrder: new[] { 0, 1 }));
|
||||
destination.Add(
|
||||
noNiTarget,
|
||||
new ScalarInlineSpec(
|
||||
ScalarInlineKind.RoundedBinary,
|
||||
Helper: null,
|
||||
Operator: @operator,
|
||||
NoNiVariant: true,
|
||||
ArgumentOrder: new[] { 0, 1 }));
|
||||
}
|
||||
|
||||
AddRoundedBinary(specs, "PPC_FADDS", "PPC_FADDSNONI", "+");
|
||||
AddRoundedBinary(specs, "PPC_FSUBS", "PPC_FSUBSNONI", "-");
|
||||
AddHelperNoNi(
|
||||
specs, "PPC_FMULS", "PPC_FMULSNONI",
|
||||
"PpcFmulsInline", "PpcFmulsNoNiInline", 0, 1);
|
||||
AddRoundedBinary(specs, "PPC_FDIVS", "PPC_FDIVSNONI", "/");
|
||||
|
||||
AddHelper(specs, "PPC_FSQRT", "std::sqrt", 0);
|
||||
AddHelper(specs, "PPC_FMADD", "PpcFmaddInline", 0, 1, 2);
|
||||
AddHelper(specs, "PPC_FMSUB", "PpcFmsubInline", 0, 1, 2);
|
||||
AddHelper(specs, "PPC_FNMADD", "PpcFnmaddInline", 0, 1, 2);
|
||||
AddHelper(specs, "PPC_FNMSUB", "PpcFnmsubInline", 0, 1, 2);
|
||||
|
||||
return specs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Case-insensitive lookup table for the remaining hand-written inline
|
||||
/// cases. Table-driven arithmetic targets are looked up before this switch.
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> InlineExpandedHelperTargets = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"PPC_FCMP", "PPC_CRSETBIT", "PPC_CRLOGICAL", "PPC_MCRF", "PPC_CNTLZW", "PPC_GETCARRY",
|
||||
"PPC_UPDATECARRYSUB", "PPC_UPDATECARRYADD", "PPC_UPDATECARRYSHIFTRIGHT", "PPC_PSFROMSCALAR",
|
||||
"PPC_PSTOSCALAR", "PPC_PSMR", "PPC_PSCMPO0", "PPC_PSCMPU0", "PPC_PSCMPO1", "PPC_PSCMPU1",
|
||||
"PPC_PSQL", "PPC_PSQST"
|
||||
};
|
||||
|
||||
private static string InlineHelperSwitchKey(string target) =>
|
||||
InlineExpandedHelperTargets.TryGetValue(target, out var canonical) ? canonical : string.Empty;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,520 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Translator.Core.Analysis.Ssa;
|
||||
using Translator.Core.Ir;
|
||||
using Translator.Core.Analysis.Representation;
|
||||
using Translator.Core.Translation;
|
||||
using Translator.Core.Representation;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
public sealed partial class CxxLinearCodeGenerator
|
||||
{
|
||||
internal const int MinimumGuardedGqrVersioningSites = 3;
|
||||
|
||||
private readonly record struct GqrEntryGuard(uint Index, uint Value);
|
||||
|
||||
private static string GqrEntryGuardName(uint index, uint value) =>
|
||||
$"gqr_entry_{index}_{value:X8}";
|
||||
|
||||
private static IReadOnlyList<GqrEntryGuard> CollectGqrEntryGuards(IrFunction function)
|
||||
{
|
||||
var guards = new HashSet<GqrEntryGuard>();
|
||||
foreach (var instruction in function.Blocks.SelectMany(static block => block.Instructions))
|
||||
{
|
||||
switch (instruction)
|
||||
{
|
||||
case IrResolvedPsqLoad load when load.GuardKnownGqr && load.KnownGqr.HasValue:
|
||||
guards.Add(new GqrEntryGuard(load.I, load.KnownGqr.Value));
|
||||
break;
|
||||
case IrResolvedPsqStore store when store.GuardKnownGqr && store.KnownGqr.HasValue:
|
||||
guards.Add(new GqrEntryGuard(store.I, store.KnownGqr.Value));
|
||||
break;
|
||||
case IrCall call when TryGetGuardedGqr(call, out var guard):
|
||||
guards.Add(guard);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return guards.OrderBy(static guard => guard.Index).ThenBy(static guard => guard.Value).ToArray();
|
||||
}
|
||||
|
||||
private static bool TryGetGuardedGqr(IrCall call, out GqrEntryGuard guard)
|
||||
{
|
||||
guard = default;
|
||||
const string loadPrefix = "PPC_PsqLKnownGuarded_";
|
||||
const string storePrefix = "PPC_PsqStKnownGuarded_";
|
||||
var isLoad = call.Target.StartsWith(loadPrefix, StringComparison.OrdinalIgnoreCase);
|
||||
var prefix = isLoad ? loadPrefix : call.Target.StartsWith(storePrefix, StringComparison.OrdinalIgnoreCase)
|
||||
? storePrefix : null;
|
||||
var indexArgument = isLoad ? 2 : 3;
|
||||
var index = call.Arguments.Count > indexArgument
|
||||
? call.Arguments[indexArgument].Constant : null;
|
||||
if (prefix is null || call.Arguments.Count <= indexArgument ||
|
||||
index is not (>= 0 and < 8) ||
|
||||
!uint.TryParse(call.Target.AsSpan(prefix.Length), System.Globalization.NumberStyles.HexNumber,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var value))
|
||||
return false;
|
||||
guard = new GqrEntryGuard((uint)index.Value, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool CanVersionGuardedGqrFunction(
|
||||
IrFunction function,
|
||||
IReadOnlyList<GqrEntryGuard> guards)
|
||||
{
|
||||
// Versioning trades cold code size for a branch-free hot body, so it only pays off with enough
|
||||
// repeated PSQ work to amortize the second instantiation. Three sites already outweighs that
|
||||
// cost; a higher threshold missed matrix helpers like PSMTXMultVec/PSMTXConcat that dominate
|
||||
// the paired-single profile but only clear it after inlining.
|
||||
if (guards.Count == 0 ||
|
||||
guards.GroupBy(static guard => guard.Index).Any(static group => group.Count() != 1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var guardedIndices = guards.Select(static guard => guard.Index).ToHashSet();
|
||||
var guardedSites = 0;
|
||||
foreach (var instruction in function.Blocks.SelectMany(static block => block.Instructions))
|
||||
{
|
||||
switch (instruction)
|
||||
{
|
||||
case IrAssign assign when GqrConstantPropagation.TryGetGqrKey(assign.Destination) is { } key &&
|
||||
guardedIndices.Contains((uint)(key[3] - '0')):
|
||||
return false;
|
||||
case IrResolvedPsqLoad load when load.GuardKnownGqr && load.KnownGqr.HasValue:
|
||||
case IrResolvedPsqStore store when store.GuardKnownGqr && store.KnownGqr.HasValue:
|
||||
guardedSites++;
|
||||
break;
|
||||
case IrCall call when TryGetGuardedGqr(call, out _):
|
||||
guardedSites++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return guardedSites >= MinimumGuardedGqrVersioningSites;
|
||||
}
|
||||
|
||||
private static string ApplyGuardedGqrFunctionVersioning(
|
||||
string code,
|
||||
string functionName,
|
||||
IReadOnlyList<GqrEntryGuard> guards)
|
||||
{
|
||||
const string parameters = CpuContextDefinitionParameter;
|
||||
const string callArguments = "ctx";
|
||||
// Match the unqualified definition emitted before leaf-cache rewriting.
|
||||
// The inline qualifier belongs on the rewritten definition below; if
|
||||
// it is present here the cache pass cannot find the function opening.
|
||||
var functionDecl = FunctionDefinitionSignature(functionName);
|
||||
var implementationName = $"{functionName}_gqr_impl";
|
||||
var lines = code.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n');
|
||||
var rewritten = new StringBuilder(code.Length * 2);
|
||||
var pendingFunctionOpen = false;
|
||||
var inFunction = false;
|
||||
var braceDepth = 0;
|
||||
|
||||
foreach (var originalLine in lines)
|
||||
{
|
||||
var line = originalLine;
|
||||
if (!inFunction && string.Equals(line, functionDecl, StringComparison.Ordinal))
|
||||
{
|
||||
rewritten.AppendLine("template <bool gqr_entry_profile>");
|
||||
rewritten.AppendLine($"MKW_PPC_NO_INLINE static void {implementationName}({parameters})");
|
||||
pendingFunctionOpen = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pendingFunctionOpen && string.Equals(line, "{", StringComparison.Ordinal))
|
||||
{
|
||||
rewritten.AppendLine(line);
|
||||
pendingFunctionOpen = false;
|
||||
inFunction = true;
|
||||
braceDepth = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inFunction)
|
||||
{
|
||||
if (guards.Any(guard => line.Contains(
|
||||
$"const bool {GqrEntryGuardName(guard.Index, guard.Value)} =",
|
||||
StringComparison.Ordinal)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var guard in guards)
|
||||
{
|
||||
line = line.Replace(
|
||||
GqrEntryGuardName(guard.Index, guard.Value),
|
||||
"gqr_entry_profile",
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
line = line.Replace(
|
||||
"if (gqr_entry_profile)",
|
||||
"if constexpr (gqr_entry_profile)",
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
rewritten.AppendLine(line);
|
||||
|
||||
if (!inFunction)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
braceDepth += CountChar(line, '{') - CountChar(line, '}');
|
||||
if (braceDepth > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
inFunction = false;
|
||||
var condition = string.Join(" && ", guards.Select(static guard =>
|
||||
$"ctx->gqr[{guard.Index}u] == 0x{guard.Value:X8}u"));
|
||||
rewritten.AppendLine();
|
||||
rewritten.AppendLine(FunctionDefinitionSignature(functionName));
|
||||
rewritten.AppendLine("{");
|
||||
rewritten.AppendLine($" if ({condition})");
|
||||
rewritten.AppendLine($" {implementationName}<true>({callArguments});");
|
||||
rewritten.AppendLine(" else");
|
||||
rewritten.AppendLine($" {implementationName}<false>({callArguments});");
|
||||
rewritten.AppendLine("}");
|
||||
}
|
||||
|
||||
return rewritten.ToString();
|
||||
}
|
||||
|
||||
private sealed record PairedFlowStateMaps(
|
||||
Dictionary<string, HashSet<string>> In,
|
||||
Dictionary<string, HashSet<string>> Out);
|
||||
|
||||
private static HashSet<string> CollectNonRegisterLocals(IrFunction function)
|
||||
{
|
||||
var locals = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
void Add(string name)
|
||||
{
|
||||
// Only add if it's NOT a CPU register that uses direct ctx access.
|
||||
// CR fields need local variables because direct CR field writes are complex.
|
||||
if (!IsCpuRegisterNoLocalNeeded(name))
|
||||
{
|
||||
locals.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var block in function.Blocks)
|
||||
{
|
||||
foreach (var instruction in block.Instructions)
|
||||
{
|
||||
// Range destinations are host pointers declared separately in
|
||||
// the function prologue. IrSetCrField writes CR through a
|
||||
// helper and has no C++ destination expression. Everything
|
||||
// else follows the canonical syntactic IR use/def walker.
|
||||
if (instruction is not (IrResolveGuestMemoryRange or IrSetCrField))
|
||||
{
|
||||
foreach (var definition in IrRegisterDataFlow.Definitions(instruction))
|
||||
{
|
||||
Add(definition);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var use in IrRegisterDataFlow.Uses(instruction))
|
||||
{
|
||||
Add(use);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return locals;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> CollectCallTargets(IrFunction function)
|
||||
{
|
||||
foreach (var block in function.Blocks)
|
||||
{
|
||||
foreach (var ins in block.Instructions)
|
||||
{
|
||||
switch (ins)
|
||||
{
|
||||
case IrCall call when !string.IsNullOrWhiteSpace(call.Target):
|
||||
yield return call.Target;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bit position of a float register in a paired-state mask, or -1 if not a float register.
|
||||
/// </summary>
|
||||
private static int PairedFloatRegisterBit(string baseName) =>
|
||||
ClassifyRegisterBase(RegisterBaseSpan(baseName), out var index) == GuestRegisterClass.Fpr
|
||||
? index
|
||||
: -1;
|
||||
|
||||
private static HashSet<string> MaterializePairedFloatSet(uint mask, StringComparer comparer)
|
||||
{
|
||||
var set = new HashSet<string>(comparer);
|
||||
for (var bit = 0; bit < 32; bit++)
|
||||
{
|
||||
if ((mask & (1u << bit)) != 0)
|
||||
{
|
||||
set.Add(FprRegisterName(bit));
|
||||
}
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
private static PairedFlowStateMaps ComputePairedFlowStates(
|
||||
IrFunction func,
|
||||
IrCfg cfg,
|
||||
IGuestFunctionAbiProvider guestAbiProvider)
|
||||
{
|
||||
var comparer = StringComparer.OrdinalIgnoreCase;
|
||||
// f0..f31 occupy bits 0..31. The fixpoint used to seed a fresh
|
||||
// HashSet<string> holding all 32 register names for every block and to
|
||||
// rebuild two more sets per block per iteration; the mask form has the
|
||||
// same lattice, the same initial state and the same transfer.
|
||||
const uint allFloatRegisters = uint.MaxValue;
|
||||
const uint f1Bit = 1u << 1;
|
||||
var blocks = func.Blocks;
|
||||
var blockCount = blocks.Count;
|
||||
var blockIndices = new Dictionary<string, int>(blockCount, comparer);
|
||||
for (var i = 0; i < blockCount; i++)
|
||||
{
|
||||
blockIndices[blocks[i].Label] = i;
|
||||
}
|
||||
|
||||
var isEntryBlock = new bool[blockCount];
|
||||
var inMasks = new uint[blockCount];
|
||||
var outMasks = new uint[blockCount];
|
||||
// This is a forward must-analysis: non-entry blocks start at top so loop latches
|
||||
// can converge on paired registers that are preserved through the cycle.
|
||||
for (var i = 0; i < blockCount; i++)
|
||||
{
|
||||
isEntryBlock[i] = blocks[i].Label.Equals(func.EntryLabel, StringComparison.OrdinalIgnoreCase);
|
||||
inMasks[i] = isEntryBlock[i] ? 0u : allFloatRegisters;
|
||||
outMasks[i] = isEntryBlock[i] ? 0u : allFloatRegisters;
|
||||
}
|
||||
|
||||
// The CFG is immutable for the whole fixpoint, so the filtered
|
||||
// predecessor lists are built once instead of once per iteration.
|
||||
var predecessors = new List<string>[blockCount];
|
||||
for (var i = 0; i < blockCount; i++)
|
||||
{
|
||||
var label = blocks[i].Label;
|
||||
predecessors[i] = cfg.Predecessors(label)
|
||||
.Where(p => !p.Equals(label, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var changed = true;
|
||||
while (changed)
|
||||
{
|
||||
changed = false;
|
||||
for (var b = 0; b < blockCount; b++)
|
||||
{
|
||||
var block = blocks[b];
|
||||
var preds = predecessors[b];
|
||||
uint newIn;
|
||||
// A translated entry can have CFG backedges, but it also has an external
|
||||
// predecessor whose live FPRs use the scalar CpuContext representation.
|
||||
if (isEntryBlock[b] || preds.Count == 0)
|
||||
{
|
||||
newIn = 0u;
|
||||
}
|
||||
else
|
||||
{
|
||||
newIn = outMasks[blockIndices[preds[0]]];
|
||||
for (var p = 1; p < preds.Count; p++)
|
||||
{
|
||||
newIn &= outMasks[blockIndices[preds[p]]];
|
||||
}
|
||||
}
|
||||
|
||||
var newOut = newIn;
|
||||
foreach (var ins in block.Instructions)
|
||||
{
|
||||
switch (ins)
|
||||
{
|
||||
case IrPhi phi:
|
||||
{
|
||||
var destBit = PairedFloatRegisterBit(phi.Destination);
|
||||
if (destBit < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var allPaired = true;
|
||||
foreach (var (predLabel, src) in phi.Sources)
|
||||
{
|
||||
var srcBit = PairedFloatRegisterBit(src);
|
||||
if (srcBit < 0)
|
||||
{
|
||||
allPaired = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!blockIndices.TryGetValue(predLabel, out var predIndex) ||
|
||||
(outMasks[predIndex] & (1u << srcBit)) == 0)
|
||||
{
|
||||
allPaired = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allPaired)
|
||||
{
|
||||
newOut |= 1u << destBit;
|
||||
}
|
||||
else
|
||||
{
|
||||
newOut &= ~(1u << destBit);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case IrCall call:
|
||||
{
|
||||
if (IsGuestCallTarget(call.Target))
|
||||
{
|
||||
// Normalize proven scalar arguments and treat f1 as the
|
||||
// conventional scalar return. Preserve f2-f13 representation
|
||||
// tags when the callee leaves their payloads untouched.
|
||||
newOut = ClearPairedGuestScalarFloatArguments(
|
||||
call.Target, call.Arguments, newOut, guestAbiProvider);
|
||||
newOut &= ~f1Bit;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(call.Destination))
|
||||
{
|
||||
var destBit = PairedFloatRegisterBit(call.Destination);
|
||||
if (destBit < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (IsPairedProducerTarget(call.Target))
|
||||
{
|
||||
newOut |= 1u << destBit;
|
||||
}
|
||||
else
|
||||
{
|
||||
newOut &= ~(1u << destBit);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case IrIndirectCall icall when !string.IsNullOrWhiteSpace(icall.Destination):
|
||||
{
|
||||
newOut &= ~f1Bit;
|
||||
var destBit = PairedFloatRegisterBit(icall.Destination);
|
||||
if (destBit >= 0)
|
||||
{
|
||||
newOut &= ~(1u << destBit);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case IrIndirectCall icall:
|
||||
newOut &= ~f1Bit;
|
||||
break;
|
||||
case IrAssign assign:
|
||||
{
|
||||
var destBit = PairedFloatRegisterBit(assign.Destination);
|
||||
if (destBit < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (assign.Value.Kind == "register" && assign.Value.RegisterName != null)
|
||||
{
|
||||
var srcBit = PairedFloatRegisterBit(assign.Value.RegisterName);
|
||||
if (srcBit >= 0 && (newOut & (1u << srcBit)) != 0)
|
||||
{
|
||||
newOut |= 1u << destBit;
|
||||
}
|
||||
else
|
||||
{
|
||||
newOut &= ~(1u << destBit);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
newOut &= ~(1u << destBit);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case IrBinary bin:
|
||||
{
|
||||
var destBit = PairedFloatRegisterBit(bin.Destination);
|
||||
if (destBit >= 0)
|
||||
{
|
||||
newOut &= ~(1u << destBit);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case IrLoad load:
|
||||
{
|
||||
var destBit = PairedFloatRegisterBit(load.Destination);
|
||||
if (destBit >= 0)
|
||||
{
|
||||
newOut &= ~(1u << destBit);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newIn != inMasks[b])
|
||||
{
|
||||
inMasks[b] = newIn;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (newOut != outMasks[b])
|
||||
{
|
||||
outMasks[b] = newOut;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var inMap = new Dictionary<string, HashSet<string>>(blockCount, comparer);
|
||||
var outMap = new Dictionary<string, HashSet<string>>(blockCount, comparer);
|
||||
for (var i = 0; i < blockCount; i++)
|
||||
{
|
||||
inMap[blocks[i].Label] = MaterializePairedFloatSet(inMasks[i], comparer);
|
||||
outMap[blocks[i].Label] = MaterializePairedFloatSet(outMasks[i], comparer);
|
||||
}
|
||||
|
||||
return new PairedFlowStateMaps(inMap, outMap);
|
||||
}
|
||||
|
||||
private static uint ClearPairedGuestScalarFloatArguments(
|
||||
string target,
|
||||
IReadOnlyList<IrValue> arguments,
|
||||
uint pairedMask,
|
||||
IGuestFunctionAbiProvider guestAbiProvider)
|
||||
{
|
||||
foreach (var arg in arguments)
|
||||
{
|
||||
if (arg.Kind != "register" || arg.RegisterName == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var bit = PairedFloatRegisterBit(arg.RegisterName);
|
||||
if (bit < 0 || (pairedMask & (1u << bit)) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsGuestScalarFloatArgument(guestAbiProvider, target, GetRegisterBaseName(arg.RegisterName)))
|
||||
{
|
||||
pairedMask &= ~(1u << bit);
|
||||
}
|
||||
}
|
||||
|
||||
return pairedMask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Translator.Core.Analysis;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
public sealed partial class CxxLinearCodeGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Guest register residency for one emitted function body: C++ locals are authoritative between call boundaries (host regs), synced via flush/reload at each one.
|
||||
/// Emitted twice, a recording pass then the real pass, so any register reference outside the recorded set fails loud instead of silently reading stale <c>ctx-></c> state.
|
||||
/// </summary>
|
||||
private sealed class RegisterResidency
|
||||
{
|
||||
private readonly bool _recording;
|
||||
private readonly SortedSet<int> _gprs = new();
|
||||
private readonly SortedSet<int> _fprs = new();
|
||||
private readonly SortedSet<int> _writtenGprs = new();
|
||||
private readonly SortedSet<int> _writtenFprs = new();
|
||||
private bool _cr;
|
||||
private bool _ctr;
|
||||
private bool _xer;
|
||||
private bool _writesCr;
|
||||
private bool _writesCtr;
|
||||
private bool _writesXer;
|
||||
|
||||
private RegisterResidency(bool recording) => _recording = recording;
|
||||
|
||||
public static RegisterResidency CreateRecorder() => new(true);
|
||||
|
||||
public RegisterResidency Snapshot()
|
||||
{
|
||||
var snapshot = new RegisterResidency(false);
|
||||
snapshot._gprs.UnionWith(_gprs);
|
||||
snapshot._fprs.UnionWith(_fprs);
|
||||
snapshot._writtenGprs.UnionWith(_writtenGprs);
|
||||
snapshot._writtenFprs.UnionWith(_writtenFprs);
|
||||
snapshot._cr = _cr;
|
||||
snapshot._ctr = _ctr;
|
||||
snapshot._xer = _xer;
|
||||
snapshot._writesCr = _writesCr;
|
||||
snapshot._writesCtr = _writesCtr;
|
||||
snapshot._writesXer = _writesXer;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
public bool IsEmpty => _gprs.Count == 0 && _fprs.Count == 0 && !_cr && !_ctr && !_xer;
|
||||
|
||||
public static string GprLocal(int index) => GprRegisterName(index);
|
||||
public static string FprLocal(int index) => FprRegisterName(index);
|
||||
public const string CrLocal = "cr";
|
||||
public const string CtrLocal = "ctr";
|
||||
public const string XerLocal = "xer";
|
||||
|
||||
public IEnumerable<string> DeclaredLocalNames
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var gpr in _gprs) yield return GprLocal(gpr);
|
||||
foreach (var fpr in _fprs) yield return FprLocal(fpr);
|
||||
if (_cr) yield return CrLocal;
|
||||
if (_ctr) yield return CtrLocal;
|
||||
if (_xer) yield return XerLocal;
|
||||
}
|
||||
}
|
||||
|
||||
public string Gpr(int index, bool written)
|
||||
{
|
||||
Track(_gprs, _writtenGprs, index, written, "r");
|
||||
return GprLocal(index);
|
||||
}
|
||||
|
||||
/// <summary>Scalar (PS0) view of a localized FPR.</summary>
|
||||
public string FprScalar(int index, bool written)
|
||||
{
|
||||
Track(_fprs, _writtenFprs, index, written, "f");
|
||||
return FprScalarLocalName(index);
|
||||
}
|
||||
|
||||
/// <summary>Whole <c>PPC_FPR</c> value of a localized FPR.</summary>
|
||||
public string FprStorage(int index, bool written)
|
||||
{
|
||||
Track(_fprs, _writtenFprs, index, written, "f");
|
||||
return FprLocal(index);
|
||||
}
|
||||
|
||||
public string Cr(bool written)
|
||||
{
|
||||
if (_recording)
|
||||
{
|
||||
_cr = true;
|
||||
_writesCr |= written;
|
||||
}
|
||||
else if (!_cr || (written && !_writesCr))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Register residency did not localize cr for this function body.");
|
||||
}
|
||||
|
||||
return CrLocal;
|
||||
}
|
||||
|
||||
public string Ctr(bool written)
|
||||
{
|
||||
if (_recording)
|
||||
{
|
||||
_ctr = true;
|
||||
_writesCtr |= written;
|
||||
}
|
||||
else if (!_ctr || (written && !_writesCtr))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Register residency did not localize ctr for this function body.");
|
||||
}
|
||||
|
||||
return CtrLocal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// XER, localized as one value (not just summary-overflow) because the carry helpers
|
||||
/// read/modify/write it and every integer compare reads XER.SO into its CR field.
|
||||
/// </summary>
|
||||
public string Xer(bool written)
|
||||
{
|
||||
if (_recording)
|
||||
{
|
||||
_xer = true;
|
||||
_writesXer |= written;
|
||||
}
|
||||
else if (!_xer || (written && !_writesXer))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Register residency did not localize xer for this function body.");
|
||||
}
|
||||
|
||||
return XerLocal;
|
||||
}
|
||||
|
||||
private void Track(SortedSet<int> referenced, SortedSet<int> written, int index, bool isWrite, string prefix)
|
||||
{
|
||||
if (_recording)
|
||||
{
|
||||
referenced.Add(index);
|
||||
if (isWrite) written.Add(index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!referenced.Contains(index) || (isWrite && !written.Contains(index)))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Register residency did not localize {prefix}{index} for this function body.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Entry loads. Every referenced register is loaded unconditionally:
|
||||
/// liveness-narrowed initialization is a later optimization and reading
|
||||
/// an architectural register is always well defined.
|
||||
/// </summary>
|
||||
public void AppendEntryLoads(StringBuilder sb)
|
||||
{
|
||||
if (_recording) return;
|
||||
foreach (var gpr in _gprs)
|
||||
sb.Append(" uint32_t ").Append(GprLocal(gpr)).Append(" = ctx->gpr[").Append(gpr).AppendLine("];");
|
||||
foreach (var fpr in _fprs)
|
||||
sb.Append(" PPC_FPR ").Append(FprLocal(fpr)).Append(" = ctx->fpr[").Append(fpr).AppendLine("];");
|
||||
if (_cr) sb.AppendLine($" uint32_t {CrLocal} = ctx->cr;");
|
||||
if (_ctr) sb.AppendLine($" uint32_t {CtrLocal} = ctx->ctr;");
|
||||
if (_xer) sb.AppendLine($" uint32_t {XerLocal} = ctx->xer;");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores the possibly-written localized registers back into
|
||||
/// <c>CpuContext</c>. Storing an unmodified register writes the same
|
||||
/// value back, so the conservative write set stays correct.
|
||||
/// </summary>
|
||||
public void AppendFlush(StringBuilder sb, string pad) => AppendFlush(sb, pad, ResidencyBoundarySync.Full);
|
||||
|
||||
/// <summary>
|
||||
/// Contract-narrowed flush. Only the registers the boundary can observe
|
||||
/// or overwrite are published; see <see cref="ResidencyBoundarySync"/>
|
||||
/// for why the write set has to participate in the flush mask.
|
||||
/// </summary>
|
||||
public void AppendFlush(StringBuilder sb, string pad, ResidencyBoundarySync sync)
|
||||
{
|
||||
if (_recording) return;
|
||||
foreach (var gpr in _writtenGprs)
|
||||
if (sync.FlushesGpr(gpr))
|
||||
sb.Append(pad).Append("ctx->gpr[").Append(gpr).Append("] = ").Append(GprLocal(gpr)).AppendLine(";");
|
||||
foreach (var fpr in _writtenFprs)
|
||||
if (sync.FlushesFpr(fpr))
|
||||
sb.Append(pad).Append("ctx->fpr[").Append(fpr).Append("] = ").Append(FprLocal(fpr)).AppendLine(";");
|
||||
if (_writesCr && sync.FlushesCr) sb.Append(pad).AppendLine($"ctx->cr = {CrLocal};");
|
||||
if (_writesCtr && sync.FlushesCtr) sb.Append(pad).AppendLine($"ctx->ctr = {CtrLocal};");
|
||||
if (_writesXer && sync.FlushesXer) sb.Append(pad).AppendLine($"ctx->xer = {XerLocal};");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the full localized set after a boundary returns, unconditionally: a callee can
|
||||
/// change any volatile register and a nonvolatile one too if it restores it itself.
|
||||
/// </summary>
|
||||
public void AppendReload(StringBuilder sb, string pad) => AppendReload(sb, pad, ResidencyBoundarySync.Full);
|
||||
|
||||
/// <summary>Contract-narrowed reload.</summary>
|
||||
public void AppendReload(StringBuilder sb, string pad, ResidencyBoundarySync sync)
|
||||
{
|
||||
if (_recording) return;
|
||||
foreach (var gpr in _gprs)
|
||||
if (sync.ReloadsGpr(gpr))
|
||||
sb.Append(pad).Append(GprLocal(gpr)).Append(" = ctx->gpr[").Append(gpr).AppendLine("];");
|
||||
foreach (var fpr in _fprs)
|
||||
if (sync.ReloadsFpr(fpr))
|
||||
sb.Append(pad).Append(FprLocal(fpr)).Append(" = ctx->fpr[").Append(fpr).AppendLine("];");
|
||||
if (_cr && sync.ReloadsCr) sb.Append(pad).AppendLine($"{CrLocal} = ctx->cr;");
|
||||
if (_ctr && sync.ReloadsCtr) sb.Append(pad).AppendLine($"{CtrLocal} = ctx->ctr;");
|
||||
if (_xer && sync.ReloadsXer) sb.Append(pad).AppendLine($"{XerLocal} = ctx->xer;");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Which localized registers one call boundary must sync. Flush mask is <c>read-before-write | possible-write</c>, not just callee reads, or an unflushed possibly-written register reloads stale.
|
||||
/// Reload mask is the callee's possible-write mask (transitive union, not net effect); safe even for a spill/restore, which just re-reads a value already restored to what this frame flushed.
|
||||
/// Falls back to <see cref="Full"/> without a precise contract (missing, full-fence, indirect, or CpuContext-touching marshalling).
|
||||
/// </summary>
|
||||
internal sealed record ResidencyBoundarySync(
|
||||
uint FlushGprMask,
|
||||
uint FlushFprMask,
|
||||
bool FlushesCr,
|
||||
bool FlushesCtr,
|
||||
uint ReloadGprMask,
|
||||
uint ReloadFprMask,
|
||||
bool ReloadsCr,
|
||||
bool ReloadsCtr,
|
||||
bool FlushesXer = false,
|
||||
bool ReloadsXer = false)
|
||||
{
|
||||
public static ResidencyBoundarySync Full { get; } = new(
|
||||
uint.MaxValue, uint.MaxValue, true, true,
|
||||
uint.MaxValue, uint.MaxValue, true, true,
|
||||
FlushesXer: true, ReloadsXer: true);
|
||||
|
||||
/// <summary>
|
||||
/// Fast arm of a state-free call site (explicit-state ABI, locals carry args/results) still flushes r1 and XER unconditionally, since a guest gather-pipe store can trigger GX HLE mid-call servicing a VI retrace or OS alarm whose handlers read them architecturally.
|
||||
/// Flush-only; the slow arm (<c>InvokeDirectCpu</c>) keeps a full <see cref="Full"/> pair.
|
||||
/// </summary>
|
||||
public static ResidencyBoundarySync StateFreeResidentOuter { get; } = new(
|
||||
FlushGprMask: 1u << 1,
|
||||
FlushFprMask: 0u,
|
||||
FlushesCr: false,
|
||||
FlushesCtr: false,
|
||||
ReloadGprMask: 0u,
|
||||
ReloadFprMask: 0u,
|
||||
ReloadsCr: false,
|
||||
ReloadsCtr: false,
|
||||
FlushesXer: true,
|
||||
ReloadsXer: false);
|
||||
|
||||
public bool FlushesGpr(int index) => (FlushGprMask & (1u << index)) != 0;
|
||||
public bool FlushesFpr(int index) => (FlushFprMask & (1u << index)) != 0;
|
||||
public bool ReloadsGpr(int index) => (ReloadGprMask & (1u << index)) != 0;
|
||||
public bool ReloadsFpr(int index) => (ReloadFprMask & (1u << index)) != 0;
|
||||
|
||||
public bool IsFull =>
|
||||
FlushGprMask == uint.MaxValue && FlushFprMask == uint.MaxValue && FlushesCr && FlushesCtr &&
|
||||
ReloadGprMask == uint.MaxValue && ReloadFprMask == uint.MaxValue && ReloadsCr && ReloadsCtr &&
|
||||
FlushesXer && ReloadsXer;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a register class back to both masks, for call sites that emit their own architectural
|
||||
/// <c>CpuContext</c> traffic (paired-single return normalization) the callee contract omits.
|
||||
/// </summary>
|
||||
public ResidencyBoundarySync WithFpr(int index) => this with
|
||||
{
|
||||
FlushFprMask = FlushFprMask | (1u << index),
|
||||
ReloadFprMask = ReloadFprMask | (1u << index)
|
||||
};
|
||||
|
||||
public bool IsEmpty =>
|
||||
FlushGprMask == 0 && FlushFprMask == 0 && !FlushesCr && !FlushesCtr &&
|
||||
ReloadGprMask == 0 && ReloadFprMask == 0 && !ReloadsCr && !ReloadsCtr &&
|
||||
!FlushesXer && !ReloadsXer;
|
||||
|
||||
/// <summary>
|
||||
/// Boundary set for a non-guest helper's implicit <c>CpuContext</c> effects, from
|
||||
/// <see cref="Translator.Core.Analysis.GuestHelperEffectCatalog"/>. Explicit argument/destination registers
|
||||
/// need no sync; unknown helpers catalog as complete boundaries and arrive as full masks; a pure helper is empty.
|
||||
/// </summary>
|
||||
public static ResidencyBoundarySync FromHelperEffect(Translator.Core.Analysis.GuestHelperEffect effect) => new(
|
||||
FlushGprMask: effect.GprReadMask | effect.GprWriteMask,
|
||||
FlushFprMask: effect.FprReadMask | effect.FprWriteMask,
|
||||
FlushesCr: (effect.CrReadMask | effect.CrWriteMask) != 0,
|
||||
FlushesCtr: effect.ReadsCtr || effect.WritesCtr,
|
||||
ReloadGprMask: effect.GprWriteMask,
|
||||
ReloadFprMask: effect.FprWriteMask,
|
||||
ReloadsCr: effect.CrWriteMask != 0,
|
||||
ReloadsCtr: effect.WritesCtr,
|
||||
FlushesXer: effect.ReadsXer || effect.WritesXer,
|
||||
ReloadsXer: effect.WritesXer);
|
||||
|
||||
/// <summary>
|
||||
/// r1 flushes unconditionally: typed native stubs derive their contract from a host signature and can't see the body re-enter guest code via HLE interrupt delivery, whose callback chains build frames from <c>ctx->gpr[1]</c>.
|
||||
/// A stale r1 once let such a callback overwrite a live frame's save area (return address landed in nw4r::ef's saved r29 at postVRetrace, 0x80210050, as a wild read). Flush-only; caller keeps its stack pointer.
|
||||
/// </summary>
|
||||
private const uint StackPointerFlushMask = 1u << 1;
|
||||
|
||||
public static ResidencyBoundarySync FromCalleeContract(GuestAbiContract contract)
|
||||
{
|
||||
if (contract.HasFullSynchronizationFence) return Full;
|
||||
return new ResidencyBoundarySync(
|
||||
FlushGprMask: contract.GprReadBeforeWriteMask | contract.GprPossibleWriteMask |
|
||||
StackPointerFlushMask,
|
||||
FlushFprMask: contract.FprReadBeforeWriteMask | contract.FprPossibleWriteMask,
|
||||
// The contract tracks CR per field while the local is the whole
|
||||
// packed register, so any intersecting field forces the sync.
|
||||
FlushesCr: (contract.CrReadBeforeWriteMask | contract.CrPossibleWriteMask) != 0,
|
||||
FlushesCtr: contract.ReadsCtrBeforeWrite || contract.MayWriteCtr,
|
||||
ReloadGprMask: contract.GprPossibleWriteMask,
|
||||
ReloadFprMask: contract.FprPossibleWriteMask,
|
||||
ReloadsCr: contract.CrPossibleWriteMask != 0,
|
||||
ReloadsCtr: contract.MayWriteCtr,
|
||||
// Flushed unconditionally for the same reason as r1: interrupt handlers read XER.SO out
|
||||
// of the architectural copy. Reload stays contract-driven so a handler can't clobber it.
|
||||
FlushesXer: true,
|
||||
ReloadsXer: contract.MayWriteXer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ambient residency for the body being emitted. Thread-local, not an explicit parameter, since threading one through 30+ static formatting helpers risks a missed path silently reading stale <c>ctx->gpr[N]</c>; also keeps parallel per-function emission independent.
|
||||
/// </summary>
|
||||
[ThreadStatic]
|
||||
private static RegisterResidency? _activeResidency;
|
||||
|
||||
private static void AppendFlush(StringBuilder sb, string pad)
|
||||
{
|
||||
if (_activeResidency is { } residency) residency.AppendFlush(sb, pad);
|
||||
}
|
||||
|
||||
private static void AppendReload(StringBuilder sb, string pad)
|
||||
{
|
||||
if (_activeResidency is { } residency) residency.AppendReload(sb, pad);
|
||||
}
|
||||
|
||||
private static void AppendFlush(StringBuilder sb, string pad, ResidencyBoundarySync sync)
|
||||
{
|
||||
if (_activeResidency is { } residency) residency.AppendFlush(sb, pad, sync);
|
||||
}
|
||||
|
||||
private static void AppendReload(StringBuilder sb, string pad, ResidencyBoundarySync sync)
|
||||
{
|
||||
if (_activeResidency is { } residency) residency.AppendReload(sb, pad, sync);
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
private static ResidencyBoundarySync ResolveDirectCallBoundarySync(
|
||||
uint target,
|
||||
IReadOnlyDictionary<uint, GuestAbiContract> guestAbiContracts,
|
||||
IReadOnlySet<uint> modOverridableCallTargets)
|
||||
{
|
||||
if (_activeResidency is null ||
|
||||
modOverridableCallTargets.Contains(target) ||
|
||||
!guestAbiContracts.TryGetValue(target, out var contract))
|
||||
{
|
||||
return ResidencyBoundarySync.Full;
|
||||
}
|
||||
|
||||
return ResidencyBoundarySync.FromCalleeContract(contract);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes an explicit helper destination register from a helper-effect
|
||||
/// reload mask. The destination local holds the helper's return value; the
|
||||
/// implicit-context reload must not overwrite it.
|
||||
/// </summary>
|
||||
private static ResidencyBoundarySync ExcludeDestinationFromReload(
|
||||
ResidencyBoundarySync sync,
|
||||
string? destination)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(destination))
|
||||
{
|
||||
return sync;
|
||||
}
|
||||
|
||||
var baseName = GetRegisterBaseName(destination);
|
||||
if (baseName.Length > 1 && baseName[0] is 'r' or 'R' &&
|
||||
int.TryParse(baseName.AsSpan(1), out var gpr) && gpr is >= 0 and < 32)
|
||||
{
|
||||
return sync with { ReloadGprMask = sync.ReloadGprMask & ~(1u << gpr) };
|
||||
}
|
||||
|
||||
if (baseName.Length > 1 && baseName[0] is 'f' or 'F' &&
|
||||
int.TryParse(baseName.AsSpan(1), out var fpr) && fpr is >= 0 and < 32)
|
||||
{
|
||||
return sync with { ReloadFprMask = sync.ReloadFprMask & ~(1u << fpr) };
|
||||
}
|
||||
|
||||
if (baseName.Equals("cr", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return sync with { ReloadsCr = false };
|
||||
}
|
||||
|
||||
if (baseName.Equals("ctr", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return sync with { ReloadsCtr = false };
|
||||
}
|
||||
|
||||
if (baseName.Equals("xer", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return sync with { ReloadsXer = false };
|
||||
}
|
||||
|
||||
return sync;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rejects a residency whose local names would shadow an emitted temporary. A guard against a
|
||||
/// future naming change, not an expected condition, since <see cref="IsCpuRegisterNoLocalNeeded"/>
|
||||
/// already keeps machine register names out of the ordinary local set.
|
||||
/// </summary>
|
||||
private static void RequireResidencyNamesAreFree(
|
||||
RegisterResidency residency,
|
||||
uint entryPoint,
|
||||
IEnumerable<string> emittedLocalNames)
|
||||
{
|
||||
var taken = new HashSet<string>(emittedLocalNames, StringComparer.Ordinal);
|
||||
var collisions = residency.DeclaredLocalNames.Where(taken.Contains).ToArray();
|
||||
if (collisions.Length != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Register residency local name(s) {string.Join(", ", collisions)} collide with an existing " +
|
||||
$"local in 0x{entryPoint:X8}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Translator.Core.Ir;
|
||||
using Translator.Core.Analysis.Representation;
|
||||
using Translator.Core.Representation;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
public sealed partial class CxxLinearCodeGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Machine register classes recognized by the emitter. Exactly one of these
|
||||
/// applies to any base register name, which is what lets the individual
|
||||
/// predicates share a single allocation-free classification pass.
|
||||
/// </summary>
|
||||
private enum GuestRegisterClass
|
||||
{
|
||||
None = 0,
|
||||
Gpr,
|
||||
Fpr,
|
||||
CrField,
|
||||
Special
|
||||
}
|
||||
|
||||
/// <summary>Canonical <c>r0</c>..<c>r31</c> / <c>f0</c>..<c>f31</c> spellings, interned so the
|
||||
/// dataflow passes and residency emitter stop reformatting them per instruction per iteration.</summary>
|
||||
private static readonly string[] GprRegisterNames = CreateRegisterNames("r");
|
||||
private static readonly string[] FprRegisterNames = CreateRegisterNames("f");
|
||||
|
||||
/// <summary>Scalar (PS0) spelling of a resident FPR local: <c>f0.d</c>..<c>f31.d</c>.</summary>
|
||||
private static readonly string[] FprScalarLocalNames = CreateFprScalarLocalNames();
|
||||
|
||||
private static string[] CreateFprScalarLocalNames()
|
||||
{
|
||||
var names = new string[32];
|
||||
for (var index = 0; index < names.Length; index++)
|
||||
{
|
||||
names[index] = "f" + index.ToString(CultureInfo.InvariantCulture) + ".d";
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
/// <summary>Architectural <c>CpuContext</c> spellings used when no residency is active.</summary>
|
||||
private static readonly string[] CtxGprNames = CreateCtxNames("ctx->gpr[", "]");
|
||||
private static readonly string[] CtxFprScalarNames = CreateCtxNames("ctx->fpr[", "].d");
|
||||
|
||||
private static string[] CreateCtxNames(string prefix, string suffix)
|
||||
{
|
||||
var names = new string[32];
|
||||
for (var index = 0; index < names.Length; index++)
|
||||
{
|
||||
names[index] = prefix + index.ToString(CultureInfo.InvariantCulture) + suffix;
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
private static string CtxGprName(int index) =>
|
||||
(uint)index < (uint)CtxGprNames.Length
|
||||
? CtxGprNames[index]
|
||||
: "ctx->gpr[" + index.ToString(CultureInfo.InvariantCulture) + "]";
|
||||
|
||||
private static string CtxFprScalarName(int index) =>
|
||||
(uint)index < (uint)CtxFprScalarNames.Length
|
||||
? CtxFprScalarNames[index]
|
||||
: "ctx->fpr[" + index.ToString(CultureInfo.InvariantCulture) + "].d";
|
||||
|
||||
private static string FprScalarLocalName(int index) =>
|
||||
(uint)index < (uint)FprScalarLocalNames.Length
|
||||
? FprScalarLocalNames[index]
|
||||
: FprRegisterName(index) + ".d";
|
||||
|
||||
private static string[] CreateRegisterNames(string prefix)
|
||||
{
|
||||
var names = new string[32];
|
||||
for (var index = 0; index < names.Length; index++)
|
||||
{
|
||||
names[index] = prefix + index.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
private static string GprRegisterName(int index) =>
|
||||
(uint)index < (uint)GprRegisterNames.Length
|
||||
? GprRegisterNames[index]
|
||||
: "r" + index.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
private static string FprRegisterName(int index) =>
|
||||
(uint)index < (uint)FprRegisterNames.Length
|
||||
? FprRegisterNames[index]
|
||||
: "f" + index.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
private static bool IsAsciiDigit(char value) => (uint)(value - '0') <= 9u;
|
||||
|
||||
/// <summary>
|
||||
/// Base register name as a span over the original string. The SSA renamer
|
||||
/// appends <c>_<version></c>; anything else (including an underscore
|
||||
/// followed by a non-numeric suffix) is already a base name.
|
||||
/// </summary>
|
||||
private static ReadOnlySpan<char> RegisterBaseSpan(string name)
|
||||
{
|
||||
var underscoreIdx = name.IndexOf('_');
|
||||
if (underscoreIdx < 0) return name.AsSpan();
|
||||
|
||||
var suffixStart = underscoreIdx + 1;
|
||||
if (suffixStart >= name.Length) return name.AsSpan();
|
||||
|
||||
for (var i = suffixStart; i < name.Length; i++)
|
||||
{
|
||||
if (!char.IsDigit(name[i])) return name.AsSpan();
|
||||
}
|
||||
|
||||
return name.AsSpan(0, underscoreIdx);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Memo for the substring produced when a name really carries an SSA
|
||||
/// version suffix. The name universe of one function is small, so this
|
||||
/// turns a per-call allocation into a dictionary probe.
|
||||
/// </summary>
|
||||
[ThreadStatic]
|
||||
private static Dictionary<string, string>? _registerBaseNameMemo;
|
||||
|
||||
private static string GetRegisterBaseName(string name)
|
||||
{
|
||||
var baseSpan = RegisterBaseSpan(name);
|
||||
if (baseSpan.Length == name.Length) return name;
|
||||
|
||||
var memo = _registerBaseNameMemo ??= new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
if (memo.TryGetValue(name, out var cached)) return cached;
|
||||
|
||||
// The emitter runs one function at a time per thread; the bound only
|
||||
// exists so a very long translation cannot grow this without limit.
|
||||
if (memo.Count >= 8192) memo.Clear();
|
||||
var baseName = new string(baseSpan);
|
||||
memo[name] = baseName;
|
||||
return baseName;
|
||||
}
|
||||
|
||||
private static bool SpanEqualsIgnoreCase(ReadOnlySpan<char> value, string candidate) =>
|
||||
value.Equals(candidate.AsSpan(), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Classifies a base register name and decodes its index for the indexed classes. GPR/FPR require
|
||||
/// a lowercase prefix; CR fields and special registers match case insensitively.
|
||||
/// </summary>
|
||||
private static GuestRegisterClass ClassifyRegisterBase(ReadOnlySpan<char> baseName, out int index)
|
||||
{
|
||||
index = -1;
|
||||
var length = baseName.Length;
|
||||
if (length is < 2 or > 4) return GuestRegisterClass.None;
|
||||
|
||||
var first = baseName[0];
|
||||
if (length <= 3 && (first == 'r' || first == 'f') && IsAsciiDigit(baseName[1]))
|
||||
{
|
||||
if (length == 2)
|
||||
{
|
||||
index = baseName[1] - '0';
|
||||
return first == 'r' ? GuestRegisterClass.Gpr : GuestRegisterClass.Fpr;
|
||||
}
|
||||
|
||||
if (IsAsciiDigit(baseName[2]))
|
||||
{
|
||||
var value = ((baseName[1] - '0') * 10) + (baseName[2] - '0');
|
||||
if (value >= 32) return GuestRegisterClass.None;
|
||||
index = value;
|
||||
return first == 'r' ? GuestRegisterClass.Gpr : GuestRegisterClass.Fpr;
|
||||
}
|
||||
}
|
||||
|
||||
if (length == 3 &&
|
||||
(first is 'c' or 'C') && (baseName[1] is 'r' or 'R') &&
|
||||
IsAsciiDigit(baseName[2]) && (baseName[2] - '0') <= 7)
|
||||
{
|
||||
index = baseName[2] - '0';
|
||||
return GuestRegisterClass.CrField;
|
||||
}
|
||||
|
||||
switch (length)
|
||||
{
|
||||
case 2:
|
||||
if (SpanEqualsIgnoreCase(baseName, "lr") ||
|
||||
SpanEqualsIgnoreCase(baseName, "cr"))
|
||||
{
|
||||
return GuestRegisterClass.Special;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 3:
|
||||
if (SpanEqualsIgnoreCase(baseName, "ctr") ||
|
||||
SpanEqualsIgnoreCase(baseName, "xer") ||
|
||||
SpanEqualsIgnoreCase(baseName, "msr"))
|
||||
{
|
||||
return GuestRegisterClass.Special;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 4:
|
||||
if (SpanEqualsIgnoreCase(baseName, "srr0") ||
|
||||
SpanEqualsIgnoreCase(baseName, "srr1") ||
|
||||
SpanEqualsIgnoreCase(baseName, "hid0") ||
|
||||
SpanEqualsIgnoreCase(baseName, "hid1") ||
|
||||
SpanEqualsIgnoreCase(baseName, "hid2"))
|
||||
{
|
||||
return GuestRegisterClass.Special;
|
||||
}
|
||||
|
||||
if (SpanEqualsIgnoreCase(baseName[..3], "gqr") &&
|
||||
IsAsciiDigit(baseName[3]) && (baseName[3] - '0') < 8)
|
||||
{
|
||||
index = baseName[3] - '0';
|
||||
return GuestRegisterClass.Special;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return GuestRegisterClass.None;
|
||||
}
|
||||
|
||||
private static GuestRegisterClass ClassifyRegister(string name, out int index) =>
|
||||
ClassifyRegisterBase(RegisterBaseSpan(name), out index);
|
||||
|
||||
private static bool IsFloatRegister(string name) =>
|
||||
ClassifyRegister(name, out _) == GuestRegisterClass.Fpr;
|
||||
|
||||
private static bool IsAbiFloatArgumentRegister(string name) =>
|
||||
ClassifyRegister(name, out var index) == GuestRegisterClass.Fpr &&
|
||||
index is >= 1 and <= 13;
|
||||
|
||||
private static bool IsFloatValue(IrValue value, RepresentationEnvironment types)
|
||||
{
|
||||
if (value.Kind != "register" || string.IsNullOrWhiteSpace(value.RegisterName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsFloatRegister(value.RegisterName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var type = types.Get(value.RegisterName);
|
||||
return type is ValueRepresentation primitive && primitive.IsFloat;
|
||||
}
|
||||
|
||||
private static bool TryParseGprRegisterIndex(string registerName, out int index)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(registerName) &&
|
||||
registerName.StartsWith("r", StringComparison.OrdinalIgnoreCase) &&
|
||||
int.TryParse(RegisterBaseSpan(registerName)[1..], out index) &&
|
||||
index is >= 0 and <= 31)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
index = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryParseFloatRegisterIndex(string registerName, out int index)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(registerName) &&
|
||||
registerName.StartsWith("f", StringComparison.OrdinalIgnoreCase) &&
|
||||
int.TryParse(RegisterBaseSpan(registerName)[1..], out index) &&
|
||||
index is >= 0 and <= 31)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
index = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsCpuRegister(string name) =>
|
||||
ClassifyRegister(name, out _) != GuestRegisterClass.None;
|
||||
|
||||
private static bool IsCpuRegisterNoLocalNeeded(string name) =>
|
||||
ClassifyRegister(name, out _) is GuestRegisterClass.Gpr or GuestRegisterClass.Fpr or GuestRegisterClass.Special;
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Translator.Core.Analysis;
|
||||
using Translator.Core.Ir;
|
||||
using Translator.Core.Representation;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
public sealed partial class CxxLinearCodeGenerator
|
||||
{
|
||||
/// <summary>Identifies one IR instruction by its position in the function.</summary>
|
||||
private readonly record struct InstructionKey(string BlockLabel, int Index);
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="InstructionKey"/> plus a whole-function ordinal, so two
|
||||
/// sites in different blocks can be compared in emission order.
|
||||
/// </summary>
|
||||
private readonly record struct InstructionSite(string BlockLabel, int Index, int Order)
|
||||
{
|
||||
public InstructionKey Key => new(BlockLabel, Index);
|
||||
}
|
||||
|
||||
private sealed record StackAccess(InstructionSite Site, int Offset, int SizeBytes);
|
||||
|
||||
private sealed class FprStackSaveSlot
|
||||
{
|
||||
public List<InstructionSite> DirectStores { get; } = new();
|
||||
public List<InstructionSite> DirectLoads { get; } = new();
|
||||
public List<InstructionSite> PsqStores { get; } = new();
|
||||
public List<InstructionSite> PsqLoads { get; } = new();
|
||||
}
|
||||
|
||||
/// <summary>Replaces a leaf function's private ABI FPR stack spill with a native local. Requires
|
||||
/// one entry save, one epilogue restore, one return, no overlapping stack access, and, for paired
|
||||
/// saves, canonical scalar+PSQ save / PSQ+scalar restore order.</summary>
|
||||
private static IrFunction ElideLeafStackFprSaveRestore(
|
||||
IrFunction func,
|
||||
RepresentationEnvironment types)
|
||||
{
|
||||
if (!FunctionEligibleForLeafRegisterCache(func))
|
||||
return func;
|
||||
|
||||
var stackFacts = StackAddressFacts.Build(func);
|
||||
var stackAccesses = new List<StackAccess>();
|
||||
var slots = new Dictionary<(int Fpr, int Offset), FprStackSaveSlot>();
|
||||
var psqAddressTemps = new Dictionary<InstructionKey, string>();
|
||||
var returnSites = new List<InstructionSite>();
|
||||
var orderedInstructions = new List<(InstructionSite Site, IrInstruction Instruction)>();
|
||||
|
||||
var order = 0;
|
||||
foreach (var block in func.Blocks)
|
||||
{
|
||||
for (var index = 0; index < block.Instructions.Count; ++index, ++order)
|
||||
{
|
||||
var instruction = block.Instructions[index];
|
||||
var site = new InstructionSite(block.Label, index, order);
|
||||
orderedInstructions.Add((site, instruction));
|
||||
switch (instruction)
|
||||
{
|
||||
case IrReturn:
|
||||
returnSites.Add(site);
|
||||
break;
|
||||
case IrStore store:
|
||||
RecordDirectStackStore(store, site);
|
||||
break;
|
||||
case IrLoad load:
|
||||
RecordDirectStackLoad(load, site);
|
||||
break;
|
||||
case IrCall call:
|
||||
RecordPsqStackAccess(call, site);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (returnSites.Count != 1)
|
||||
return func;
|
||||
|
||||
var replacements = new Dictionary<InstructionKey, IrInstruction>();
|
||||
var removals = new HashSet<InstructionKey>();
|
||||
var savedFprs = new SortedSet<int>();
|
||||
var stackTempUseCounts = CountRegisterUses(func);
|
||||
var stackTempDefinitions = CollectStackAddressTemporaryDefinitions(func, stackFacts);
|
||||
var returnSite = returnSites[0];
|
||||
|
||||
foreach (var ((fpr, offset), slot) in slots.OrderBy(pair => pair.Key.Fpr).ThenBy(pair => pair.Key.Offset))
|
||||
{
|
||||
if (slot.DirectStores.Count != 1 || slot.DirectLoads.Count != 1)
|
||||
continue;
|
||||
|
||||
var directStore = slot.DirectStores[0];
|
||||
var directLoad = slot.DirectLoads[0];
|
||||
if (!directStore.BlockLabel.Equals(func.EntryLabel, StringComparison.OrdinalIgnoreCase) ||
|
||||
!directLoad.BlockLabel.Equals(returnSite.BlockLabel, StringComparison.OrdinalIgnoreCase) ||
|
||||
directStore.Order >= directLoad.Order || directLoad.Order >= returnSite.Order ||
|
||||
HasFprDefinitionBefore(fpr, directStore.Order))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var slotSites = new HashSet<InstructionKey> { directStore.Key, directLoad.Key };
|
||||
var accessSize = 8;
|
||||
var hasPairedSave = slot.PsqStores.Count == 1 && slot.PsqLoads.Count == 1;
|
||||
if (hasPairedSave)
|
||||
{
|
||||
var psqStore = slot.PsqStores[0];
|
||||
var psqLoad = slot.PsqLoads[0];
|
||||
if (!psqStore.BlockLabel.Equals(func.EntryLabel, StringComparison.OrdinalIgnoreCase) ||
|
||||
!psqLoad.BlockLabel.Equals(returnSite.BlockLabel, StringComparison.OrdinalIgnoreCase) ||
|
||||
!(directStore.Order < psqStore.Order && psqStore.Order < psqLoad.Order &&
|
||||
psqLoad.Order < directLoad.Order) ||
|
||||
HasFprAccessBetween(fpr, psqLoad.Order, directLoad.Order))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
slotSites.Add(psqStore.Key);
|
||||
slotSites.Add(psqLoad.Key);
|
||||
accessSize = 16;
|
||||
}
|
||||
else if (slot.PsqStores.Count != 0 || slot.PsqLoads.Count != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (HasOverlappingStackAccess(stackAccesses, slotSites, offset, accessSize))
|
||||
continue;
|
||||
|
||||
savedFprs.Add(fpr);
|
||||
removals.Add(directStore.Key);
|
||||
replacements[directLoad.Key] = new IrAssign($"f{fpr}", IrValue.Register(LeafStackSavedFprName(fpr)));
|
||||
|
||||
if (hasPairedSave)
|
||||
{
|
||||
var psqStore = slot.PsqStores[0];
|
||||
var psqLoad = slot.PsqLoads[0];
|
||||
removals.Add(psqStore.Key);
|
||||
removals.Add(psqLoad.Key);
|
||||
RemoveSingleUsePsqAddressTemporary(psqStore.Key);
|
||||
RemoveSingleUsePsqAddressTemporary(psqLoad.Key);
|
||||
}
|
||||
}
|
||||
|
||||
if (savedFprs.Count == 0)
|
||||
return func;
|
||||
|
||||
foreach (var fpr in savedFprs)
|
||||
types.Unify(LeafStackSavedFprName(fpr), ValueRepresentation.Float64);
|
||||
|
||||
var rewrittenBlocks = new List<IrBasicBlock>(func.Blocks.Count);
|
||||
foreach (var block in func.Blocks)
|
||||
{
|
||||
var rewritten = new List<IrInstruction>(block.Instructions.Count + savedFprs.Count);
|
||||
if (block.Label.Equals(func.EntryLabel, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
foreach (var fpr in savedFprs)
|
||||
// This pass runs after SSA construction. Preserve the
|
||||
// explicit live-in version so later LLVM lowering never
|
||||
// mistakes the epilogue's synthetic fN definition for the
|
||||
// entry value being saved.
|
||||
rewritten.Add(new IrAssign(LeafStackSavedFprName(fpr), IrValue.Register($"f{fpr}_0")));
|
||||
}
|
||||
|
||||
for (var index = 0; index < block.Instructions.Count; ++index)
|
||||
{
|
||||
var key = new InstructionKey(block.Label, index);
|
||||
if (removals.Contains(key))
|
||||
continue;
|
||||
rewritten.Add(replacements.TryGetValue(key, out var replacement)
|
||||
? replacement
|
||||
: block.Instructions[index]);
|
||||
}
|
||||
rewrittenBlocks.Add(new IrBasicBlock(block.Label, rewritten));
|
||||
}
|
||||
|
||||
return new IrFunction(func.Name, func.EntryLabel, rewrittenBlocks);
|
||||
|
||||
bool HasFprDefinitionBefore(int fpr, int limit)
|
||||
{
|
||||
var name = $"f{fpr}";
|
||||
return orderedInstructions.Any(item => item.Site.Order < limit &&
|
||||
InstructionDefinesRegister(item.Instruction, name));
|
||||
}
|
||||
|
||||
bool HasFprAccessBetween(int fpr, int after, int before)
|
||||
{
|
||||
var name = $"f{fpr}";
|
||||
return orderedInstructions.Any(item => item.Site.Order > after && item.Site.Order < before &&
|
||||
(InstructionUsesRegister(item.Instruction, name) || InstructionDefinesRegister(item.Instruction, name)));
|
||||
}
|
||||
|
||||
void RemoveSingleUsePsqAddressTemporary(InstructionKey psqKey)
|
||||
{
|
||||
if (psqAddressTemps.TryGetValue(psqKey, out var name) &&
|
||||
stackTempUseCounts.TryGetValue(name, out var useCount) && useCount == 1 &&
|
||||
stackTempDefinitions.TryGetValue(name, out var definition))
|
||||
{
|
||||
removals.Add(definition.Key);
|
||||
}
|
||||
}
|
||||
|
||||
FprStackSaveSlot GetSlot(int fpr, int offset)
|
||||
{
|
||||
var key = (fpr, offset);
|
||||
if (!slots.TryGetValue(key, out var slot))
|
||||
slots[key] = slot = new FprStackSaveSlot();
|
||||
return slot;
|
||||
}
|
||||
|
||||
void RecordDirectStackStore(IrStore store, InstructionSite site)
|
||||
{
|
||||
if (!stackFacts.TryResolve(store.Address, out var offset))
|
||||
return;
|
||||
stackAccesses.Add(new StackAccess(site, offset, store.SizeBytes));
|
||||
if (store.SizeBytes == 8 && store.Source.Kind == "register" &&
|
||||
TryParseNonvolatileFpr(store.Source.RegisterName, out var fpr))
|
||||
{
|
||||
GetSlot(fpr, offset).DirectStores.Add(site);
|
||||
}
|
||||
}
|
||||
|
||||
void RecordDirectStackLoad(IrLoad load, InstructionSite site)
|
||||
{
|
||||
if (!stackFacts.TryResolve(load.Address, out var offset))
|
||||
return;
|
||||
stackAccesses.Add(new StackAccess(site, offset, load.SizeBytes));
|
||||
if (load.SizeBytes == 8 && TryParseNonvolatileFpr(load.Destination, out var fpr))
|
||||
GetSlot(fpr, offset).DirectLoads.Add(site);
|
||||
}
|
||||
|
||||
void RecordPsqStackAccess(IrCall call, InstructionSite site)
|
||||
{
|
||||
if (!TryGetPsqStackAccess(call, stackFacts, out var psq))
|
||||
return;
|
||||
stackAccesses.Add(new StackAccess(site, psq.Offset, psq.SizeBytes));
|
||||
if (!psq.IsSaveRestoreCandidate || psq.SizeBytes != 8 || psq.Offset < 8)
|
||||
return;
|
||||
var slot = GetSlot(psq.Fpr, psq.Offset - 8);
|
||||
if (call.Arguments.Count > 0 && call.Arguments[0].Kind == "register" &&
|
||||
call.Arguments[0].RegisterName is { } temporary)
|
||||
{
|
||||
psqAddressTemps[site.Key] = temporary;
|
||||
}
|
||||
if (psq.IsStore) slot.PsqStores.Add(site); else slot.PsqLoads.Add(site);
|
||||
}
|
||||
}
|
||||
|
||||
private static string LeafStackSavedFprName(int fpr) => $"leaf_stack_saved_f{fpr}_entry";
|
||||
|
||||
private static IrFunction ElideOverwrittenPsqStackLoads(IrFunction func)
|
||||
{
|
||||
var stackFacts = StackAddressFacts.Build(func);
|
||||
var stackTempUseCounts = CountRegisterUses(func);
|
||||
var stackTempDefinitions = CollectStackAddressTemporaryDefinitions(func, stackFacts);
|
||||
var removals = new HashSet<InstructionKey>();
|
||||
|
||||
foreach (var block in func.Blocks)
|
||||
{
|
||||
for (var index = 0; index + 1 < block.Instructions.Count; index++)
|
||||
{
|
||||
if (block.Instructions[index] is not IrCall call ||
|
||||
!TryGetPsqStackAccess(call, stackFacts, out var psq) ||
|
||||
psq.IsStore ||
|
||||
!psq.IsSaveRestoreCandidate ||
|
||||
psq.SizeBytes != 8 ||
|
||||
psq.Offset < 8 ||
|
||||
!TryFindOverwritingScalarFprLoad(block.Instructions, index, stackFacts, psq, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var callKey = new InstructionKey(block.Label, index);
|
||||
removals.Add(callKey);
|
||||
|
||||
if (call.Arguments.Count > 0 &&
|
||||
call.Arguments[0].Kind == "register" &&
|
||||
call.Arguments[0].RegisterName is { } psqTemp &&
|
||||
stackTempUseCounts.TryGetValue(psqTemp, out var useCount) &&
|
||||
useCount == 1 &&
|
||||
stackTempDefinitions.TryGetValue(psqTemp, out var defSite))
|
||||
{
|
||||
removals.Add(defSite.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removals.Count == 0)
|
||||
{
|
||||
return func;
|
||||
}
|
||||
|
||||
var rewrittenBlocks = new List<IrBasicBlock>(func.Blocks.Count);
|
||||
foreach (var block in func.Blocks)
|
||||
{
|
||||
var rewrittenInstructions = new List<IrInstruction>(block.Instructions.Count);
|
||||
for (var index = 0; index < block.Instructions.Count; index++)
|
||||
{
|
||||
if (!removals.Contains(new InstructionKey(block.Label, index)))
|
||||
{
|
||||
rewrittenInstructions.Add(block.Instructions[index]);
|
||||
}
|
||||
}
|
||||
|
||||
rewrittenBlocks.Add(new IrBasicBlock(block.Label, rewrittenInstructions));
|
||||
}
|
||||
|
||||
return new IrFunction(func.Name, func.EntryLabel, rewrittenBlocks);
|
||||
}
|
||||
|
||||
private static bool TryFindOverwritingScalarFprLoad(
|
||||
IReadOnlyList<IrInstruction> instructions,
|
||||
int psqLoadIndex,
|
||||
StackAddressFacts stackFacts,
|
||||
PsqStackAccess psq,
|
||||
out IrLoad load)
|
||||
{
|
||||
for (var index = psqLoadIndex + 1; index < instructions.Count; index++)
|
||||
{
|
||||
var instruction = instructions[index];
|
||||
switch (instruction)
|
||||
{
|
||||
case IrComment:
|
||||
case IrTracePpc:
|
||||
continue;
|
||||
|
||||
case IrLoad candidate
|
||||
when candidate.SizeBytes == 8 &&
|
||||
TryParseNonvolatileFpr(candidate.Destination, out var loadFpr) &&
|
||||
loadFpr == psq.Fpr &&
|
||||
stackFacts.TryResolve(candidate.Address, out var loadOffset) &&
|
||||
loadOffset == psq.Offset - 8:
|
||||
load = candidate;
|
||||
return true;
|
||||
|
||||
case IrCall:
|
||||
case IrIndirectCall:
|
||||
case IrBranch:
|
||||
case IrJump:
|
||||
case IrIndirectJump:
|
||||
case IrJumpTable:
|
||||
case IrReturn:
|
||||
load = default!;
|
||||
return false;
|
||||
|
||||
default:
|
||||
if (InstructionUsesRegister(instruction, $"f{psq.Fpr}") ||
|
||||
InstructionDefinesRegister(instruction, $"f{psq.Fpr}"))
|
||||
{
|
||||
load = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
load = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool InstructionUsesRegister(IrInstruction instruction, string registerName)
|
||||
{
|
||||
var baseName = GetRegisterBaseName(registerName);
|
||||
return IrRegisterDataFlow.Uses(instruction).Any(name =>
|
||||
GetRegisterBaseName(name).Equals(baseName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool InstructionDefinesRegister(IrInstruction instruction, string registerName)
|
||||
{
|
||||
var baseName = GetRegisterBaseName(registerName);
|
||||
return IrRegisterDataFlow.Definitions(instruction).Any(name =>
|
||||
GetRegisterBaseName(name).Equals(baseName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static Dictionary<string, InstructionSite> CollectStackAddressTemporaryDefinitions(
|
||||
IrFunction func,
|
||||
StackAddressFacts stackFacts)
|
||||
{
|
||||
var definitions = new Dictionary<string, InstructionSite>(StringComparer.OrdinalIgnoreCase);
|
||||
var order = 0;
|
||||
foreach (var block in func.Blocks)
|
||||
{
|
||||
for (var index = 0; index < block.Instructions.Count; index++, order++)
|
||||
{
|
||||
var instruction = block.Instructions[index];
|
||||
string? destination = instruction switch
|
||||
{
|
||||
IrAssign assign => assign.Destination,
|
||||
IrBinary binary => binary.Destination,
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (destination != null && stackFacts.ContainsTemporary(destination))
|
||||
{
|
||||
definitions[destination] = new InstructionSite(block.Label, index, order);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
private static Dictionary<string, int> CountRegisterUses(IrFunction func)
|
||||
{
|
||||
var counts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var block in func.Blocks)
|
||||
{
|
||||
foreach (var instruction in block.Instructions)
|
||||
{
|
||||
// Keep every occurrence: stack-temp elision relies on a
|
||||
// true use count, not a set of referenced names.
|
||||
foreach (var name in IrRegisterDataFlow.Uses(instruction))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
counts[name] = counts.TryGetValue(name, out var count) ? count + 1 : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return counts;
|
||||
}
|
||||
|
||||
private sealed record PsqStackAccess(
|
||||
bool IsStore,
|
||||
bool IsSaveRestoreCandidate,
|
||||
int Fpr,
|
||||
int Offset,
|
||||
int SizeBytes);
|
||||
|
||||
private static bool TryGetPsqStackAccess(
|
||||
IrCall call,
|
||||
StackAddressFacts stackFacts,
|
||||
out PsqStackAccess access)
|
||||
{
|
||||
access = default!;
|
||||
var isStore = string.Equals(call.Target, "PPC_PsqSt", StringComparison.Ordinal);
|
||||
var isLoad = string.Equals(call.Target, "PPC_PsqL", StringComparison.Ordinal);
|
||||
if (!isStore && !isLoad)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (call.Arguments.Count < (isStore ? 4 : 3) ||
|
||||
!stackFacts.TryResolve(call.Arguments[0], out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var widthIndex = isStore ? 2 : 1;
|
||||
var quantIndex = isStore ? 3 : 2;
|
||||
var width = TryGetIntConstant(call.Arguments[widthIndex], out var widthValue)
|
||||
? widthValue
|
||||
: -1;
|
||||
var quant = TryGetIntConstant(call.Arguments[quantIndex], out var quantValue)
|
||||
? quantValue
|
||||
: -1;
|
||||
var sizeBytes = width == 1 ? 4 : 8;
|
||||
var candidate = width == 0 && quant == 0;
|
||||
var fpr = -1;
|
||||
|
||||
if (candidate)
|
||||
{
|
||||
if (isStore)
|
||||
{
|
||||
candidate = call.Arguments[1].Kind == "register" &&
|
||||
TryParseNonvolatileFpr(call.Arguments[1].RegisterName, out fpr);
|
||||
}
|
||||
else
|
||||
{
|
||||
candidate = TryParseNonvolatileFpr(call.Destination, out fpr);
|
||||
}
|
||||
}
|
||||
|
||||
access = new PsqStackAccess(isStore, candidate, fpr, offset, sizeBytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryGetIntConstant(IrValue value, out int result)
|
||||
{
|
||||
if (value.Kind == "const" &&
|
||||
value.Constant is { } constant &&
|
||||
constant >= int.MinValue &&
|
||||
constant <= int.MaxValue)
|
||||
{
|
||||
result = (int)constant;
|
||||
return true;
|
||||
}
|
||||
|
||||
result = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryParseNonvolatileFpr(string? registerName, out int fpr)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(registerName) &&
|
||||
TryParseFloatRegisterIndex(GetRegisterBaseName(registerName), out fpr) &&
|
||||
fpr is >= 14 and <= 31)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
fpr = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasOverlappingStackAccess(
|
||||
IReadOnlyList<StackAccess> accesses,
|
||||
IReadOnlySet<InstructionKey> allowedSites,
|
||||
int offset,
|
||||
int sizeBytes)
|
||||
{
|
||||
foreach (var access in accesses)
|
||||
{
|
||||
if (allowedSites.Contains(access.Site.Key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (RangesOverlap(offset, sizeBytes, access.Offset, access.SizeBytes))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool RangesOverlap(int leftOffset, int leftSize, int rightOffset, int rightSize)
|
||||
{
|
||||
var leftEnd = leftOffset + leftSize;
|
||||
var rightEnd = rightOffset + rightSize;
|
||||
return leftOffset < rightEnd && rightOffset < leftEnd;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,914 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Translator.Core.Analysis;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
public sealed partial class CxxLinearCodeGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Every pattern the state-free rewriter uses, built once. The static <c>Regex.*</c> helpers
|
||||
/// cache only 15 patterns by default; this rewriter has more than that and used to build a
|
||||
/// fresh pattern per register, thrashing the cache and re-parsing on every call.
|
||||
/// </summary>
|
||||
private static class StateFreePatterns
|
||||
{
|
||||
private const RegexOptions Options = RegexOptions.CultureInvariant | RegexOptions.Compiled;
|
||||
|
||||
public static readonly Regex DifferentialBlock = new(
|
||||
@"(?s)\s*// MKW_STATE_FREE_DIFFERENTIAL_BEGIN.*?// MKW_STATE_FREE_DIFFERENTIAL_END\s*", Options);
|
||||
|
||||
public static readonly Regex CtxGqrAccess = new(@"ctx->gqr\[(\d+)u?\]", Options);
|
||||
|
||||
public static readonly Regex PsqInlineGqrOperand = new(
|
||||
@"PPC_Psq(?:L|St)(?:Stack|Resolved)?Inline<\d+u,\s*(\d+)u>\(ctx,", Options);
|
||||
|
||||
public static readonly Regex CachedGprFlush = new(
|
||||
@"(?m)^\s*ctx->gpr\[\d+\]\s*=\s*cached_r\d+;\s*\r?\n?", Options);
|
||||
|
||||
public static readonly Regex CachedSpecialFlush = new(
|
||||
@"(?m)^\s*ctx->(?:cr|xer|ctr|lr)\s*=\s*cached_(?:cr|xer|ctr|lr);\s*\r?\n?", Options);
|
||||
|
||||
public static readonly Regex CachedFprFlush = new(
|
||||
@"(?m)^\s*ctx->fpr\[\d+\]\s*=\s*cached_f\d+;\s*\r?\n?", Options);
|
||||
|
||||
public static readonly Regex LrStore = new(@"(?m)^\s*ctx->lr\s*=\s*[^;]+;\s*\r?\n?", Options);
|
||||
|
||||
public static readonly Regex PsqLInline = new(
|
||||
@"PPC_PsqLInline<(?<w>\d+)u,\s*(?<i>\d+)u>\(ctx,\s*", Options);
|
||||
|
||||
public static readonly Regex PsqLStackInline = new(
|
||||
@"PPC_PsqLStackInline<(?<w>\d+)u,\s*(?<i>\d+)u>\(ctx,\s*", Options);
|
||||
|
||||
public static readonly Regex PsqStInline = new(
|
||||
@"PPC_PsqStInline<(?<w>\d+)u,\s*(?<i>\d+)u>\(ctx,\s*", Options);
|
||||
|
||||
public static readonly Regex PsqStStackInline = new(
|
||||
@"PPC_PsqStStackInline<(?<w>\d+)u,\s*(?<i>\d+)u>\(ctx,\s*", Options);
|
||||
|
||||
public static readonly Regex PsqLResolvedInline = new(
|
||||
@"PPC_PsqLResolvedInline<(?<w>\d+)u,\s*(?<i>\d+)u>\(ctx,\s*", Options);
|
||||
|
||||
public static readonly Regex PsqStResolvedInline = new(
|
||||
@"PPC_PsqStResolvedInline<(?<w>\d+)u,\s*(?<i>\d+)u>\(ctx,\s*", Options);
|
||||
|
||||
public static readonly Regex PsqKnownInline = new(
|
||||
@"(PPC_Psq(?:L|St)Known(?:Resolved|Stack)?Inline<[^>]+>)\(ctx,\s*", Options);
|
||||
|
||||
public static readonly Regex BareReturn = new(@"\breturn\s*;", Options);
|
||||
|
||||
public static readonly Regex AggregateReturn = new(@"\breturn\s*\{", Options);
|
||||
|
||||
public static readonly Regex ContextWord = new(@"\bctx\b", Options);
|
||||
|
||||
public static readonly Regex BoundaryDirectCall = new(
|
||||
@"(?m)^(?<pad>\s*)InvokeDirectCpu<0x(?<address>[0-9A-Fa-f]{8})u>\(ctx\);\s*$", Options);
|
||||
|
||||
public static readonly Regex BoundaryDirectCallAddress = new(
|
||||
@"\bInvokeDirectCpu<0x([0-9A-Fa-f]{8})u>\(ctx\);", Options);
|
||||
|
||||
/// <summary>
|
||||
/// <c>uint32_t cached_<name></c> declarations, found in a single scan (the old
|
||||
/// per-register pattern ending in <c>\b</c> matched the same complete identifiers).
|
||||
/// </summary>
|
||||
public static readonly Regex CachedIntDeclaration = new(@"\buint32_t\s+(cached_\w+)\b", Options);
|
||||
|
||||
public static readonly Regex CachedFprDeclaration = new(@"\bPPC_FPR\s+(cached_\w+)\b", Options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Names of the <c>cached_*</c> locals declared in one body, collected in a
|
||||
/// single pass. See <see cref="StateFreePatterns.CachedIntDeclaration"/>.
|
||||
/// </summary>
|
||||
private static HashSet<string> CollectStateFreeCachedDeclarations(string body)
|
||||
{
|
||||
var declared = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (Match match in StateFreePatterns.CachedIntDeclaration.Matches(body))
|
||||
{
|
||||
declared.Add(match.Groups[1].Value);
|
||||
}
|
||||
|
||||
foreach (Match match in StateFreePatterns.CachedFprDeclaration.Matches(body))
|
||||
{
|
||||
declared.Add(match.Groups[1].Value);
|
||||
}
|
||||
|
||||
return declared;
|
||||
}
|
||||
|
||||
private static bool IsStateFreeCallSafe(GuestAbiContract contract) =>
|
||||
!contract.HasFullSynchronizationFence;
|
||||
|
||||
private static bool StateFreeHasOutputs(GuestAbiContract contract) =>
|
||||
contract.GprPossibleWriteMask != 0 || contract.FprPossibleWriteMask != 0 ||
|
||||
contract.CrPossibleWriteMask != 0 || contract.MayWriteXer ||
|
||||
contract.MayWriteCtr || contract.MayWriteLr || contract.MayWriteFpscr ||
|
||||
contract.GqrPossibleWriteMask != 0 || contract.HidPossibleWriteMask != 0;
|
||||
|
||||
private static int StateFreeOutputCount(GuestAbiContract contract) =>
|
||||
System.Numerics.BitOperations.PopCount(contract.GprPossibleWriteMask) +
|
||||
System.Numerics.BitOperations.PopCount(contract.FprPossibleWriteMask) +
|
||||
(contract.CrPossibleWriteMask != 0 ? 1 : 0) +
|
||||
(contract.MayWriteXer ? 1 : 0) + (contract.MayWriteCtr ? 1 : 0) +
|
||||
(contract.MayWriteLr ? 1 : 0) + (contract.MayWriteFpscr ? 1 : 0) +
|
||||
System.Numerics.BitOperations.PopCount(contract.GqrPossibleWriteMask) +
|
||||
System.Numerics.BitOperations.PopCount(contract.HidPossibleWriteMask);
|
||||
|
||||
private static uint StateFreeGprInputMask(GuestAbiContract contract) =>
|
||||
contract.GprReadBeforeWriteMask;
|
||||
|
||||
private static int StateFreeInputCount(GuestAbiContract contract) =>
|
||||
System.Numerics.BitOperations.PopCount(StateFreeGprInputMask(contract)) +
|
||||
// Scalar writes preserve PS1, so every possible FPR output also needs
|
||||
// the incoming packed value at the native boundary.
|
||||
System.Numerics.BitOperations.PopCount(
|
||||
contract.FprReadBeforeWriteMask | contract.FprPossibleWriteMask) +
|
||||
(contract.CrReadBeforeWriteMask != 0 || contract.CrPossibleWriteMask != 0 ? 1 : 0) +
|
||||
(contract.ReadsXerBeforeWrite || contract.MayWriteXer ? 1 : 0) +
|
||||
(contract.ReadsCtrBeforeWrite || contract.MayWriteCtr ? 1 : 0) +
|
||||
(contract.ReadsLrBeforeWrite || contract.MayWriteLr ? 1 : 0) +
|
||||
(contract.ReadsFpscrBeforeWrite || contract.MayWriteFpscr ? 1 : 0) +
|
||||
System.Numerics.BitOperations.PopCount(
|
||||
(uint)(contract.GqrReadBeforeWriteMask | contract.GqrPossibleWriteMask)) +
|
||||
System.Numerics.BitOperations.PopCount(
|
||||
(uint)(contract.HidReadBeforeWriteMask | contract.HidPossibleWriteMask));
|
||||
|
||||
private static bool StateFreeFitsNativeRegisterBudget(GuestAbiContract contract) =>
|
||||
StateFreeInputCount(contract) <= 4 && StateFreeOutputCount(contract) <= 2;
|
||||
|
||||
// Do not use Clang's __regcall here: a state-free callee using host r12 as scratch once collided
|
||||
// with a caller that kept the guest-memory page table live in r12, crashing at address 0x4808. The
|
||||
// platform ABI avoids that until regions can be fused before host register allocation.
|
||||
private static string StateFreeCallingConvention(GuestAbiContract contract) => string.Empty;
|
||||
|
||||
private static string StateFreeResultType(uint address) => $"MkwStateFreeResult_{address:X8}";
|
||||
|
||||
private static string StateFreeAvailabilityCondition(
|
||||
uint root,
|
||||
IReadOnlyDictionary<uint, GuestAbiContract> contracts,
|
||||
IReadOnlyDictionary<uint, string> symbols)
|
||||
{
|
||||
var region = new SortedSet<uint>();
|
||||
void Visit(uint address)
|
||||
{
|
||||
if (!region.Add(address) || !contracts.TryGetValue(address, out var contract)) return;
|
||||
foreach (var target in contract.DirectCallTargets)
|
||||
if (symbols.ContainsKey(target)) Visit(target);
|
||||
}
|
||||
Visit(root);
|
||||
// Ordinary no-inline native calls: on Windows x64, over four argument slots spill to the stack
|
||||
// and over two results need an sret object, defeating register residency (first hit in the
|
||||
// THP-heavy 0x80054460 region, 36 inputs / 31 outputs). Enable only when the whole transitive
|
||||
// region fits the native register ABI; larger regions need region fusion, not a giant prototype.
|
||||
if (region.Any(address =>
|
||||
!contracts.TryGetValue(address, out var contract) ||
|
||||
!StateFreeFitsNativeRegisterBudget(contract)))
|
||||
return "false";
|
||||
// Base shards are shared by base and Retro Rewind; profile-neutral dispatch can still pick a mod
|
||||
// translation for any descendant even with compile-time-constant call traits. Every member must
|
||||
// resolve to the base body before skipping CpuContext dispatch, or the region silently drops a
|
||||
// Retro Rewind override and corrupts guest state.
|
||||
return $"MkwStateFreeAbiEnabled(0x{root:X8}u) && " + string.Join(" && ", region.Select(static address =>
|
||||
$"KnownTranslatedCpuCall<0x{address:X8}u>::kAvailable && " +
|
||||
$"!KnownTranslatedCpuCall<0x{address:X8}u>::kMustRemainDynamicallyDispatchable && " +
|
||||
$"IsBaseTranslatedCpuTargetActive<0x{address:X8}u>()"));
|
||||
}
|
||||
|
||||
private static string StateFreeReturnType(uint address, GuestAbiContract contract) =>
|
||||
StateFreeOutputCount(contract) switch
|
||||
{
|
||||
0 => "void",
|
||||
1 => "uint64_t",
|
||||
2 => "MkwStateFreeResult2",
|
||||
_ => StateFreeResultType(address)
|
||||
};
|
||||
|
||||
private static void AppendStateFreeResultDeclaration(StringBuilder sb, uint address, GuestAbiContract contract)
|
||||
{
|
||||
var outputCount = StateFreeOutputCount(contract);
|
||||
if (outputCount <= 2) return;
|
||||
sb.AppendLine($"#ifndef MKW_STATE_FREE_RESULT_{address:X8}_DEFINED");
|
||||
sb.AppendLine($"#define MKW_STATE_FREE_RESULT_{address:X8}_DEFINED 1");
|
||||
sb.AppendLine($"struct {StateFreeResultType(address)} {{");
|
||||
for (var gpr = 0; gpr < 32; ++gpr)
|
||||
if ((contract.GprPossibleWriteMask & (1u << gpr)) != 0) sb.AppendLine($" uint32_t r{gpr};");
|
||||
for (var fpr = 0; fpr < 32; ++fpr)
|
||||
if ((contract.FprPossibleWriteMask & (1u << fpr)) != 0) sb.AppendLine($" PPC_FPR f{fpr};");
|
||||
if (contract.CrPossibleWriteMask != 0) sb.AppendLine(" uint32_t cr;");
|
||||
if (contract.MayWriteXer) sb.AppendLine(" uint32_t xer;");
|
||||
if (contract.MayWriteCtr) sb.AppendLine(" uint32_t ctr;");
|
||||
if (contract.MayWriteLr) sb.AppendLine(" uint32_t lr;");
|
||||
if (contract.MayWriteFpscr) sb.AppendLine(" uint32_t fpscr;");
|
||||
for (var gqr = 0; gqr < 8; ++gqr)
|
||||
if ((contract.GqrPossibleWriteMask & (1 << gqr)) != 0) sb.AppendLine($" uint32_t gqr{gqr};");
|
||||
for (var hid = 0; hid < 3; ++hid)
|
||||
if ((contract.HidPossibleWriteMask & (1 << hid)) != 0) sb.AppendLine($" uint32_t hid{hid};");
|
||||
sb.AppendLine("};");
|
||||
sb.AppendLine("#endif");
|
||||
}
|
||||
|
||||
private static string StateFreeParameterList(GuestAbiContract contract, bool includeNames)
|
||||
{
|
||||
var parameters = new List<string>();
|
||||
var gprInputs = StateFreeGprInputMask(contract);
|
||||
for (var gpr = 0; gpr < 32; ++gpr)
|
||||
{
|
||||
if ((gprInputs & (1u << gpr)) != 0)
|
||||
parameters.Add(includeNames ? $"uint32_t native_r{gpr}" : "uint32_t");
|
||||
}
|
||||
for (var fpr = 0; fpr < 32; ++fpr)
|
||||
{
|
||||
// Scalar writes preserve PS1, so the previous architectural FPR is
|
||||
// conservatively an input until representation ownership proves a
|
||||
// complete packed overwrite.
|
||||
if (((contract.FprReadBeforeWriteMask | contract.FprPossibleWriteMask) & (1u << fpr)) != 0)
|
||||
parameters.Add(includeNames ? $"PPC_FPR native_f{fpr}" : "PPC_FPR");
|
||||
}
|
||||
if (contract.CrReadBeforeWriteMask != 0 || contract.CrPossibleWriteMask != 0)
|
||||
parameters.Add(includeNames ? "uint32_t native_cr" : "uint32_t");
|
||||
if (contract.ReadsXerBeforeWrite || contract.MayWriteXer)
|
||||
parameters.Add(includeNames ? "uint32_t native_xer" : "uint32_t");
|
||||
if (contract.ReadsCtrBeforeWrite || contract.MayWriteCtr)
|
||||
parameters.Add(includeNames ? "uint32_t native_ctr" : "uint32_t");
|
||||
if (contract.ReadsLrBeforeWrite || contract.MayWriteLr)
|
||||
parameters.Add(includeNames ? "uint32_t native_lr" : "uint32_t");
|
||||
if (contract.ReadsFpscrBeforeWrite || contract.MayWriteFpscr)
|
||||
parameters.Add(includeNames ? "uint32_t native_fpscr" : "uint32_t");
|
||||
for (var gqr = 0; gqr < 8; ++gqr)
|
||||
if (((contract.GqrReadBeforeWriteMask | contract.GqrPossibleWriteMask) & (1 << gqr)) != 0)
|
||||
parameters.Add(includeNames ? $"uint32_t native_gqr{gqr}" : "uint32_t");
|
||||
for (var hid = 0; hid < 3; ++hid)
|
||||
if (((contract.HidReadBeforeWriteMask | contract.HidPossibleWriteMask) & (1 << hid)) != 0)
|
||||
parameters.Add(includeNames ? $"uint32_t native_hid{hid}" : "uint32_t");
|
||||
return string.Join(", ", parameters);
|
||||
}
|
||||
|
||||
private static string StateFreeCallArguments(GuestAbiContract contract, uint? callSiteLr = null)
|
||||
{
|
||||
var arguments = new List<string>();
|
||||
var gprInputs = StateFreeGprInputMask(contract);
|
||||
for (var gpr = 0; gpr < 32; ++gpr)
|
||||
{
|
||||
if ((gprInputs & (1u << gpr)) != 0)
|
||||
arguments.Add($"ctx->gpr[{gpr}]");
|
||||
}
|
||||
for (var fpr = 0; fpr < 32; ++fpr)
|
||||
if (((contract.FprReadBeforeWriteMask | contract.FprPossibleWriteMask) & (1u << fpr)) != 0)
|
||||
arguments.Add($"ctx->fpr[{fpr}]");
|
||||
if (contract.CrReadBeforeWriteMask != 0 || contract.CrPossibleWriteMask != 0) arguments.Add("ctx->cr");
|
||||
if (contract.ReadsXerBeforeWrite || contract.MayWriteXer) arguments.Add("ctx->xer");
|
||||
if (contract.ReadsCtrBeforeWrite || contract.MayWriteCtr) arguments.Add("ctx->ctr");
|
||||
if (contract.ReadsLrBeforeWrite || contract.MayWriteLr)
|
||||
arguments.Add(callSiteLr.HasValue ? $"0x{callSiteLr.Value:X8}u" : "ctx->lr");
|
||||
if (contract.ReadsFpscrBeforeWrite || contract.MayWriteFpscr) arguments.Add("ctx->fpscr");
|
||||
for (var gqr = 0; gqr < 8; ++gqr)
|
||||
if (((contract.GqrReadBeforeWriteMask | contract.GqrPossibleWriteMask) & (1 << gqr)) != 0)
|
||||
arguments.Add($"ctx->gqr[{gqr}]");
|
||||
for (var hid = 0; hid < 3; ++hid)
|
||||
if (((contract.HidReadBeforeWriteMask | contract.HidPossibleWriteMask) & (1 << hid)) != 0)
|
||||
arguments.Add($"ctx->hid{hid}");
|
||||
return string.Join(", ", arguments);
|
||||
}
|
||||
|
||||
// Resident marshalling: pass/receive the resident locals directly instead of naming ctx, which would
|
||||
// force a full CpuContext spill/reload around every call even though residency already holds the
|
||||
// values in host registers. The slow arm still flushes/reloads CpuContext; see EmitResidentStateFreeCall.
|
||||
|
||||
private static string StateFreeResidentCallArguments(
|
||||
RegisterResidency residency, GuestAbiContract contract, uint? callSiteLr)
|
||||
{
|
||||
var arguments = new List<string>();
|
||||
var gprInputs = StateFreeGprInputMask(contract);
|
||||
for (var gpr = 0; gpr < 32; ++gpr)
|
||||
{
|
||||
if ((gprInputs & (1u << gpr)) != 0)
|
||||
arguments.Add(residency.Gpr(gpr, written: false));
|
||||
}
|
||||
for (var fpr = 0; fpr < 32; ++fpr)
|
||||
if (((contract.FprReadBeforeWriteMask | contract.FprPossibleWriteMask) & (1u << fpr)) != 0)
|
||||
arguments.Add(residency.FprStorage(fpr, written: false));
|
||||
if (contract.CrReadBeforeWriteMask != 0 || contract.CrPossibleWriteMask != 0)
|
||||
arguments.Add(residency.Cr(written: false));
|
||||
if (contract.ReadsXerBeforeWrite || contract.MayWriteXer) arguments.Add(residency.Xer(written: false));
|
||||
if (contract.ReadsCtrBeforeWrite || contract.MayWriteCtr) arguments.Add(residency.Ctr(written: false));
|
||||
// LR, FPSCR, the GQRs and the HID registers are never localized, so they
|
||||
// keep their architectural spelling exactly as the ctx form uses.
|
||||
if (contract.ReadsLrBeforeWrite || contract.MayWriteLr)
|
||||
arguments.Add(callSiteLr.HasValue ? $"0x{callSiteLr.Value:X8}u" : "ctx->lr");
|
||||
if (contract.ReadsFpscrBeforeWrite || contract.MayWriteFpscr) arguments.Add("ctx->fpscr");
|
||||
for (var gqr = 0; gqr < 8; ++gqr)
|
||||
if (((contract.GqrReadBeforeWriteMask | contract.GqrPossibleWriteMask) & (1 << gqr)) != 0)
|
||||
arguments.Add($"ctx->gqr[{gqr}]");
|
||||
for (var hid = 0; hid < 3; ++hid)
|
||||
if (((contract.HidReadBeforeWriteMask | contract.HidPossibleWriteMask) & (1 << hid)) != 0)
|
||||
arguments.Add($"ctx->hid{hid}");
|
||||
return string.Join(", ", arguments);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result unpacking into the resident locals. Field/value order must match
|
||||
/// <see cref="StateFreeReturnExpression"/> exactly, since this is a positional carrier for the
|
||||
/// callee's packed one- and two-output shapes.
|
||||
/// </summary>
|
||||
private static void AppendStateFreeResidentResultStores(
|
||||
StringBuilder sb, string pad, RegisterResidency residency,
|
||||
GuestAbiContract contract, string resultName)
|
||||
{
|
||||
var outputCount = StateFreeOutputCount(contract);
|
||||
var resultIndex = 0;
|
||||
string Value(string field) => outputCount switch
|
||||
{
|
||||
1 => resultName,
|
||||
2 => $"{resultName}[{resultIndex++}]",
|
||||
_ => $"{resultName}.{field}"
|
||||
};
|
||||
for (var gpr = 0; gpr < 32; ++gpr)
|
||||
if ((contract.GprPossibleWriteMask & (1u << gpr)) != 0)
|
||||
{
|
||||
var value = Value($"r{gpr}");
|
||||
sb.AppendLine($"{pad}{residency.Gpr(gpr, written: true)} = static_cast<uint32_t>({value});");
|
||||
}
|
||||
for (var fpr = 0; fpr < 32; ++fpr)
|
||||
if ((contract.FprPossibleWriteMask & (1u << fpr)) != 0)
|
||||
{
|
||||
var value = Value($"f{fpr}");
|
||||
if (outputCount > 2) value += ".raw";
|
||||
sb.AppendLine($"{pad}{residency.FprStorage(fpr, written: true)}.raw = static_cast<uint64_t>({value});");
|
||||
}
|
||||
if (contract.CrPossibleWriteMask != 0)
|
||||
sb.AppendLine($"{pad}{residency.Cr(written: true)} = static_cast<uint32_t>({Value("cr")});");
|
||||
if (contract.MayWriteXer)
|
||||
sb.AppendLine($"{pad}{residency.Xer(written: true)} = static_cast<uint32_t>({Value("xer")});");
|
||||
if (contract.MayWriteCtr)
|
||||
sb.AppendLine($"{pad}{residency.Ctr(written: true)} = static_cast<uint32_t>({Value("ctr")});");
|
||||
if (contract.MayWriteLr) sb.AppendLine($"{pad}ctx->lr = static_cast<uint32_t>({Value("lr")});");
|
||||
if (contract.MayWriteFpscr) sb.AppendLine($"{pad}ctx->fpscr = static_cast<uint32_t>({Value("fpscr")});");
|
||||
for (var gqr = 0; gqr < 8; ++gqr)
|
||||
if ((contract.GqrPossibleWriteMask & (1 << gqr)) != 0)
|
||||
sb.AppendLine($"{pad}ctx->gqr[{gqr}] = static_cast<uint32_t>({Value($"gqr{gqr}")});");
|
||||
for (var hid = 0; hid < 3; ++hid)
|
||||
if ((contract.HidPossibleWriteMask & (1 << hid)) != 0)
|
||||
sb.AppendLine($"{pad}ctx->hid{hid} = static_cast<uint32_t>({Value($"hid{hid}")});");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits one state-free call site whose fast arm marshals through the resident locals.
|
||||
/// <para>The <c>InvokeDirectCpu</c> fallback arm is live code, not dead code, since a mod overlay
|
||||
/// can replace any region member after this body was emitted, so it carries its own full
|
||||
/// flush/reload pair; only the fast arm skips the CpuContext boundary.</para>
|
||||
/// </summary>
|
||||
private static void EmitResidentStateFreeCall(
|
||||
StringBuilder sb,
|
||||
string pad,
|
||||
int bufferBaseLength,
|
||||
uint address,
|
||||
string symbol,
|
||||
GuestAbiContract contract,
|
||||
uint? callSiteLr,
|
||||
IReadOnlyDictionary<uint, GuestAbiContract> stateFreeAbiContracts,
|
||||
IReadOnlyDictionary<uint, string> stateFreeCallSymbols)
|
||||
{
|
||||
var residency = _activeResidency ?? throw new InvalidOperationException(
|
||||
$"Resident state-free marshalling requested for 0x{address:X8} without an active residency.");
|
||||
|
||||
AppendFlush(sb, pad, ResidencyBoundarySync.StateFreeResidentOuter);
|
||||
var inner = pad + " ";
|
||||
sb.AppendLine($"{pad}if ({StateFreeAvailabilityCondition(address, stateFreeAbiContracts, stateFreeCallSymbols)}) {{");
|
||||
var arguments = StateFreeResidentCallArguments(residency, contract, callSiteLr);
|
||||
if (StateFreeHasOutputs(contract))
|
||||
{
|
||||
// Uniquified by the call site's position in the translation unit,
|
||||
// exactly like the CpuContext form.
|
||||
var resultName = $"state_free_result_{address:X8}_{bufferBaseLength + sb.Length:X}";
|
||||
sb.AppendLine($"{inner}const auto {resultName} = {symbol}({arguments});");
|
||||
AppendStateFreeResidentResultStores(sb, inner, residency, contract, resultName);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"{inner}{symbol}({arguments});");
|
||||
}
|
||||
sb.AppendLine($"{pad}}} else {{");
|
||||
AppendFlush(sb, inner, ResidencyBoundarySync.Full);
|
||||
RecordEmittedDirectCallTarget(address);
|
||||
sb.AppendLine($"{inner}InvokeDirectCpu<0x{address:X8}u>(ctx);");
|
||||
AppendReload(sb, inner, ResidencyBoundarySync.Full);
|
||||
sb.AppendLine($"{pad}}}");
|
||||
}
|
||||
|
||||
private static void AppendStateFreeResultStores(
|
||||
StringBuilder sb, string pad, uint address, GuestAbiContract contract, string resultName)
|
||||
{
|
||||
var outputCount = StateFreeOutputCount(contract);
|
||||
var resultIndex = 0;
|
||||
string Value(string field) => outputCount switch
|
||||
{
|
||||
1 => resultName,
|
||||
2 => $"{resultName}[{resultIndex++}]",
|
||||
_ => $"{resultName}.{field}"
|
||||
};
|
||||
for (var gpr = 0; gpr < 32; ++gpr)
|
||||
if ((contract.GprPossibleWriteMask & (1u << gpr)) != 0)
|
||||
{
|
||||
var value = Value($"r{gpr}");
|
||||
sb.AppendLine($"{pad}ctx->gpr[{gpr}] = static_cast<uint32_t>({value});");
|
||||
}
|
||||
for (var fpr = 0; fpr < 32; ++fpr)
|
||||
if ((contract.FprPossibleWriteMask & (1u << fpr)) != 0)
|
||||
{
|
||||
var value = Value($"f{fpr}");
|
||||
if (outputCount > 2) value += ".raw";
|
||||
sb.AppendLine($"{pad}ctx->fpr[{fpr}].raw = static_cast<uint64_t>({value});");
|
||||
}
|
||||
if (contract.CrPossibleWriteMask != 0) sb.AppendLine($"{pad}ctx->cr = static_cast<uint32_t>({Value("cr")});");
|
||||
if (contract.MayWriteXer) sb.AppendLine($"{pad}ctx->xer = static_cast<uint32_t>({Value("xer")});");
|
||||
if (contract.MayWriteCtr) sb.AppendLine($"{pad}ctx->ctr = static_cast<uint32_t>({Value("ctr")});");
|
||||
if (contract.MayWriteLr) sb.AppendLine($"{pad}ctx->lr = static_cast<uint32_t>({Value("lr")});");
|
||||
if (contract.MayWriteFpscr) sb.AppendLine($"{pad}ctx->fpscr = static_cast<uint32_t>({Value("fpscr")});");
|
||||
for (var gqr = 0; gqr < 8; ++gqr)
|
||||
if ((contract.GqrPossibleWriteMask & (1 << gqr)) != 0) sb.AppendLine($"{pad}ctx->gqr[{gqr}] = static_cast<uint32_t>({Value($"gqr{gqr}")});");
|
||||
for (var hid = 0; hid < 3; ++hid)
|
||||
if ((contract.HidPossibleWriteMask & (1 << hid)) != 0) sb.AppendLine($"{pad}ctx->hid{hid} = static_cast<uint32_t>({Value($"hid{hid}")});");
|
||||
}
|
||||
|
||||
private static string StateFreeReturnExpression(GuestAbiContract contract, string body)
|
||||
{
|
||||
var values = new List<string>();
|
||||
var packed = StateFreeOutputCount(contract) <= 2;
|
||||
var declared = CollectStateFreeCachedDeclarations(body);
|
||||
for (var gpr = 0; gpr < 32; ++gpr)
|
||||
if ((contract.GprPossibleWriteMask & (1u << gpr)) != 0)
|
||||
{
|
||||
var cached = $"cached_r{gpr}";
|
||||
var value = declared.Contains(cached) ? cached : $"native_r{gpr}";
|
||||
values.Add(packed ? $"static_cast<uint64_t>({value})" : value);
|
||||
}
|
||||
for (var fpr = 0; fpr < 32; ++fpr)
|
||||
if ((contract.FprPossibleWriteMask & (1u << fpr)) != 0)
|
||||
{
|
||||
var cached = $"cached_f{fpr}";
|
||||
var value = declared.Contains(cached) ? cached : $"native_f{fpr}";
|
||||
values.Add(packed ? $"{value}.raw" : value);
|
||||
}
|
||||
if (contract.CrPossibleWriteMask != 0)
|
||||
{
|
||||
var value = declared.Contains("cached_cr") ? "cached_cr" : "native_cr";
|
||||
values.Add(packed ? $"static_cast<uint64_t>({value})" : value);
|
||||
}
|
||||
if (contract.MayWriteXer)
|
||||
{
|
||||
var value = declared.Contains("cached_xer") ? "cached_xer" : "native_xer";
|
||||
values.Add(packed ? $"static_cast<uint64_t>({value})" : value);
|
||||
}
|
||||
if (contract.MayWriteCtr)
|
||||
{
|
||||
var value = declared.Contains("cached_ctr") ? "cached_ctr" : "native_ctr";
|
||||
values.Add(packed ? $"static_cast<uint64_t>({value})" : value);
|
||||
}
|
||||
if (contract.MayWriteLr) values.Add(packed ? "static_cast<uint64_t>(native_lr)" : "native_lr");
|
||||
if (contract.MayWriteFpscr) values.Add(packed ? "static_cast<uint64_t>(native_fpscr)" : "native_fpscr");
|
||||
for (var gqr = 0; gqr < 8; ++gqr)
|
||||
if ((contract.GqrPossibleWriteMask & (1 << gqr)) != 0)
|
||||
values.Add(packed ? $"static_cast<uint64_t>(native_gqr{gqr})" : $"native_gqr{gqr}");
|
||||
for (var hid = 0; hid < 3; ++hid)
|
||||
if ((contract.HidPossibleWriteMask & (1 << hid)) != 0)
|
||||
values.Add(packed ? $"static_cast<uint64_t>(native_hid{hid})" : $"native_hid{hid}");
|
||||
return values.Count == 1
|
||||
? $"return {values[0]};"
|
||||
: $"return {{ {string.Join(", ", values)} }};";
|
||||
}
|
||||
|
||||
private static string RemoveStateFreeDirectCallFallbacks(string body)
|
||||
{
|
||||
const string prefix = "if (MkwStateFreeAbiEnabled(";
|
||||
var search = 0;
|
||||
while ((search = body.IndexOf(prefix, search, StringComparison.Ordinal)) >= 0)
|
||||
{
|
||||
var open = body.IndexOf('{', search);
|
||||
if (open < 0) break;
|
||||
var trueClose = FindMatchingBrace(body, open);
|
||||
if (trueClose < 0) break;
|
||||
var cursor = trueClose + 1;
|
||||
while (cursor < body.Length && char.IsWhiteSpace(body[cursor])) ++cursor;
|
||||
if (!body.AsSpan(cursor).StartsWith("else", StringComparison.Ordinal))
|
||||
{
|
||||
search = trueClose + 1;
|
||||
continue;
|
||||
}
|
||||
cursor += 4;
|
||||
while (cursor < body.Length && char.IsWhiteSpace(body[cursor])) ++cursor;
|
||||
if (cursor >= body.Length || body[cursor] != '{')
|
||||
{
|
||||
search = trueClose + 1;
|
||||
continue;
|
||||
}
|
||||
var falseClose = FindMatchingBrace(body, cursor);
|
||||
if (falseClose < 0) break;
|
||||
var fastBody = body[(open + 1)..trueClose];
|
||||
body = body[..search] + fastBody + body[(falseClose + 1)..];
|
||||
search += fastBody.Length;
|
||||
}
|
||||
return body;
|
||||
|
||||
static int FindMatchingBrace(string text, int open)
|
||||
{
|
||||
var depth = 0;
|
||||
for (var index = open; index < text.Length; ++index)
|
||||
{
|
||||
if (text[index] == '{') ++depth;
|
||||
else if (text[index] == '}' && --depth == 0) return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExtractFunctionBody(string code, string implementationName)
|
||||
{
|
||||
var declaration = FunctionDefinitionSignature(implementationName);
|
||||
var declarationStart = code.IndexOf(declaration, StringComparison.Ordinal);
|
||||
if (declarationStart < 0)
|
||||
throw new InvalidOperationException($"Could not find ordinary body for {implementationName}.");
|
||||
var openBrace = code.IndexOf('{', declarationStart + declaration.Length);
|
||||
if (openBrace < 0)
|
||||
throw new InvalidOperationException($"Could not find opening brace for {implementationName}.");
|
||||
var closeBrace = FindFunctionCloseBrace(code, openBrace, implementationName);
|
||||
return code[openBrace..(closeBrace + 1)];
|
||||
}
|
||||
|
||||
private static string ReplaceFunctionBody(string code, string implementationName, string replacementBody)
|
||||
{
|
||||
var declaration = FunctionDefinitionSignature(implementationName);
|
||||
var declarationStart = code.IndexOf(declaration, StringComparison.Ordinal);
|
||||
if (declarationStart < 0)
|
||||
throw new InvalidOperationException($"Could not find cached body for {implementationName}.");
|
||||
var openBrace = code.IndexOf('{', declarationStart + declaration.Length);
|
||||
if (openBrace < 0)
|
||||
throw new InvalidOperationException($"Could not find cached opening brace for {implementationName}.");
|
||||
var closeBrace = FindFunctionCloseBrace(code, openBrace, implementationName);
|
||||
return code[..openBrace] + replacementBody + code[(closeBrace + 1)..];
|
||||
}
|
||||
|
||||
private static int FindFunctionCloseBrace(string code, int openBrace, string implementationName)
|
||||
{
|
||||
var depth = 0;
|
||||
for (var index = openBrace; index < code.Length; ++index)
|
||||
{
|
||||
if (code[index] == '{') ++depth;
|
||||
else if (code[index] == '}' && --depth == 0) return index;
|
||||
}
|
||||
throw new InvalidOperationException($"Found unterminated body for {implementationName}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends one explicit-state clone. <paramref name="facts"/> reports the interface the clone
|
||||
/// actually published, since the contract is refined while the body is rewritten (body-derived
|
||||
/// GQR inputs) and the caller cannot predict it upfront.
|
||||
/// </summary>
|
||||
private static string AppendStateFreeLeafVariant(
|
||||
string code,
|
||||
string implementationName,
|
||||
string stateFreeSymbol,
|
||||
uint entryPoint,
|
||||
GuestAbiContract contract,
|
||||
out GuestStateFreeEmissionFacts facts,
|
||||
bool preferInline = false,
|
||||
IReadOnlySet<uint>? boundaryContextTargets = null,
|
||||
IReadOnlyDictionary<uint, GuestAbiContract>? guestAbiContracts = null)
|
||||
{
|
||||
if (!IsStateFreeCallSafe(contract))
|
||||
throw new InvalidOperationException($"State-free ABI at 0x{entryPoint:X8} crosses a full-context boundary.");
|
||||
|
||||
var declaration = FunctionDefinitionSignature(implementationName);
|
||||
var declarationStart = code.IndexOf(declaration, StringComparison.Ordinal);
|
||||
if (declarationStart < 0)
|
||||
throw new InvalidOperationException($"State-free ABI could not find the leaf body for 0x{entryPoint:X8}.");
|
||||
var openBrace = code.IndexOf('{', declarationStart + declaration.Length);
|
||||
if (openBrace < 0)
|
||||
throw new InvalidOperationException($"State-free ABI found no body for 0x{entryPoint:X8}.");
|
||||
|
||||
var depth = 0;
|
||||
var closeBrace = -1;
|
||||
for (var index = openBrace; index < code.Length; ++index)
|
||||
{
|
||||
if (code[index] == '{') ++depth;
|
||||
else if (code[index] == '}' && --depth == 0)
|
||||
{
|
||||
closeBrace = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (closeBrace < 0)
|
||||
throw new InvalidOperationException($"State-free ABI found an unterminated body for 0x{entryPoint:X8}.");
|
||||
|
||||
var body = code[(openBrace + 1)..closeBrace];
|
||||
body = StateFreePatterns.DifferentialBlock.Replace(body, Environment.NewLine);
|
||||
body = RemoveStateFreeDirectCallFallbacks(body);
|
||||
|
||||
// One scan per shape instead of one rebuilt pattern per GQR. The
|
||||
// captured index text is compared literally, so a hypothetical
|
||||
// zero-padded spelling stays a miss exactly as it was before.
|
||||
var referencedGqrIndices = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (Match match in StateFreePatterns.CtxGqrAccess.Matches(body))
|
||||
{
|
||||
referencedGqrIndices.Add(match.Groups[1].Value);
|
||||
}
|
||||
|
||||
foreach (Match match in StateFreePatterns.PsqInlineGqrOperand.Matches(body))
|
||||
{
|
||||
referencedGqrIndices.Add(match.Groups[1].Value);
|
||||
}
|
||||
|
||||
byte bodyGqrInputs = 0;
|
||||
for (var gqr = 0; gqr < 8; ++gqr)
|
||||
{
|
||||
if (referencedGqrIndices.Contains(gqr.ToString(System.Globalization.CultureInfo.InvariantCulture)))
|
||||
bodyGqrInputs |= (byte)(1 << gqr);
|
||||
}
|
||||
contract = contract with
|
||||
{
|
||||
GqrReadBeforeWriteMask = (byte)(contract.GqrReadBeforeWriteMask | bodyGqrInputs)
|
||||
};
|
||||
|
||||
// Public PPC ABI flushes are replaced by explicit native results.
|
||||
body = StateFreePatterns.CachedGprFlush.Replace(body, string.Empty);
|
||||
body = StateFreePatterns.CachedSpecialFlush.Replace(body, string.Empty);
|
||||
body = StateFreePatterns.CachedFprFlush.Replace(body, string.Empty);
|
||||
|
||||
// The per-register replacements below are order sensitive and stay a
|
||||
// sequential chain. These guards only skip scans that provably cannot
|
||||
// match: nothing in the chain ever introduces one of these markers.
|
||||
var containsCachedGpr = body.Contains("cached_r", StringComparison.Ordinal);
|
||||
var containsCtxGpr = body.Contains("ctx->gpr[", StringComparison.Ordinal);
|
||||
var containsCachedFpr = body.Contains("cached_f", StringComparison.Ordinal);
|
||||
var containsCtxFpr = body.Contains("ctx->fpr[", StringComparison.Ordinal);
|
||||
for (var gpr = 0; gpr < 32 && (containsCachedGpr || containsCtxGpr); ++gpr)
|
||||
{
|
||||
// A function can have several return blocks, and later formatting
|
||||
// may place a flush beside a label instead of at the start of a
|
||||
// physical line. Remove the exact architectural write wherever it
|
||||
// occurs; the cached value remains authoritative inside this clone.
|
||||
if (containsCachedGpr && containsCtxGpr)
|
||||
body = body.Replace($"ctx->gpr[{gpr}] = cached_r{gpr};", string.Empty, StringComparison.Ordinal);
|
||||
if ((StateFreeGprInputMask(contract) & (1u << gpr)) != 0)
|
||||
{
|
||||
if (containsCtxGpr)
|
||||
body = body.Replace($"ctx->gpr[{gpr}]", $"native_r{gpr}", StringComparison.Ordinal);
|
||||
}
|
||||
else if (containsCachedGpr && containsCtxGpr)
|
||||
{
|
||||
body = body.Replace(
|
||||
$"uint32_t cached_r{gpr} = ctx->gpr[{gpr}];",
|
||||
$"uint32_t cached_r{gpr}{{}};",
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
for (var fpr = 0; fpr < 32 && (containsCachedFpr || containsCtxFpr); ++fpr)
|
||||
{
|
||||
if (containsCachedFpr && containsCtxFpr)
|
||||
body = body.Replace($"ctx->fpr[{fpr}] = cached_f{fpr};", string.Empty, StringComparison.Ordinal);
|
||||
if (((contract.FprReadBeforeWriteMask | contract.FprPossibleWriteMask) & (1u << fpr)) != 0)
|
||||
{
|
||||
if (containsCtxFpr)
|
||||
body = body.Replace($"ctx->fpr[{fpr}]", $"native_f{fpr}", StringComparison.Ordinal);
|
||||
}
|
||||
else if (containsCachedFpr && containsCtxFpr)
|
||||
{
|
||||
body = body.Replace(
|
||||
$"PPC_FPR cached_f{fpr} = ctx->fpr[{fpr}];",
|
||||
$"PPC_FPR cached_f{fpr}{{}};",
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
body = body.Replace("ctx->cr = cached_cr;", string.Empty, StringComparison.Ordinal);
|
||||
body = body.Replace("ctx->xer = cached_xer;", string.Empty, StringComparison.Ordinal);
|
||||
body = body.Replace("ctx->ctr = cached_ctr;", string.Empty, StringComparison.Ordinal);
|
||||
body = body.Replace("ctx->lr = cached_lr;", string.Empty, StringComparison.Ordinal);
|
||||
if (contract.CrReadBeforeWriteMask != 0 || contract.CrPossibleWriteMask != 0)
|
||||
body = body.Replace("ctx->cr", "native_cr", StringComparison.Ordinal);
|
||||
if (contract.ReadsXerBeforeWrite || contract.MayWriteXer)
|
||||
body = body.Replace("ctx->xer", "native_xer", StringComparison.Ordinal);
|
||||
if (contract.ReadsCtrBeforeWrite || contract.MayWriteCtr)
|
||||
body = body.Replace("ctx->ctr", "native_ctr", StringComparison.Ordinal);
|
||||
if (contract.ReadsLrBeforeWrite || contract.MayWriteLr)
|
||||
body = body.Replace("ctx->lr", "native_lr", StringComparison.Ordinal);
|
||||
else
|
||||
body = StateFreePatterns.LrStore.Replace(body, string.Empty);
|
||||
if (contract.ReadsFpscrBeforeWrite || contract.MayWriteFpscr)
|
||||
body = body.Replace("ctx->fpscr", "native_fpscr", StringComparison.Ordinal);
|
||||
for (var gqr = 0; gqr < 8; ++gqr)
|
||||
if (((contract.GqrReadBeforeWriteMask | contract.GqrPossibleWriteMask) & (1 << gqr)) != 0)
|
||||
{
|
||||
body = body.Replace($"ctx->gqr[{gqr}]", $"native_gqr{gqr}", StringComparison.Ordinal);
|
||||
body = body.Replace($"ctx->gqr[{gqr}u]", $"native_gqr{gqr}", StringComparison.Ordinal);
|
||||
}
|
||||
for (var hid = 0; hid < 3; ++hid)
|
||||
if (((contract.HidReadBeforeWriteMask | contract.HidPossibleWriteMask) & (1 << hid)) != 0)
|
||||
body = body.Replace($"ctx->hid{hid}", $"native_hid{hid}", StringComparison.Ordinal);
|
||||
|
||||
if (boundaryContextTargets is { Count: > 0 })
|
||||
{
|
||||
var boundaryContracts = guestAbiContracts ?? throw new InvalidOperationException(
|
||||
$"State-free boundary contracts are missing for 0x{entryPoint:X8}.");
|
||||
body = EnsureStateFreeBoundarySpecialLocals(
|
||||
body, entryPoint, boundaryContextTargets, boundaryContracts);
|
||||
body = RewriteStateFreeBoundaryCalls(
|
||||
body, entryPoint, boundaryContextTargets,
|
||||
boundaryContracts);
|
||||
}
|
||||
|
||||
// Generic PSQ helpers need only the selected GQR value. Keep that value
|
||||
// in the native signature instead of reintroducing CpuContext solely to
|
||||
// read one array element inside an otherwise direct memory operation.
|
||||
body = StateFreePatterns.PsqLInline.Replace(body,
|
||||
static match => $"PPC_PsqLStateInline<{match.Groups["w"].Value}u, {match.Groups["i"].Value}u, false>(native_gqr{match.Groups["i"].Value}, ");
|
||||
body = StateFreePatterns.PsqLStackInline.Replace(body,
|
||||
static match => $"PPC_PsqLStateInline<{match.Groups["w"].Value}u, {match.Groups["i"].Value}u, true>(native_gqr{match.Groups["i"].Value}, ");
|
||||
body = StateFreePatterns.PsqStInline.Replace(body,
|
||||
static match => $"PPC_PsqStStateInline<{match.Groups["w"].Value}u, {match.Groups["i"].Value}u, false>(native_gqr{match.Groups["i"].Value}, ");
|
||||
body = StateFreePatterns.PsqStStackInline.Replace(body,
|
||||
static match => $"PPC_PsqStStateInline<{match.Groups["w"].Value}u, {match.Groups["i"].Value}u, true>(native_gqr{match.Groups["i"].Value}, ");
|
||||
body = StateFreePatterns.PsqLResolvedInline.Replace(body,
|
||||
static match => $"PPC_PsqLResolvedStateInline<{match.Groups["w"].Value}u, {match.Groups["i"].Value}u>(native_gqr{match.Groups["i"].Value}, ");
|
||||
body = StateFreePatterns.PsqStResolvedInline.Replace(body,
|
||||
static match => $"PPC_PsqStResolvedStateInline<{match.Groups["w"].Value}u, {match.Groups["i"].Value}u>(native_gqr{match.Groups["i"].Value}, ");
|
||||
body = StateFreePatterns.PsqKnownInline.Replace(body, "$1(nullptr, ");
|
||||
|
||||
// The process-wide A/B gate belongs only at CpuContext-to-explicit-state
|
||||
// entry boundaries. Once execution is inside a context-free region its
|
||||
// direct descendants must remain context-free as well.
|
||||
if (contract.CrReadBeforeWriteMask == 0 && contract.CrPossibleWriteMask == 0)
|
||||
body = body.Replace("uint32_t cached_cr = ctx->cr;", "uint32_t cached_cr = 0;", StringComparison.Ordinal);
|
||||
if (!contract.ReadsXerBeforeWrite && !contract.MayWriteXer)
|
||||
body = body.Replace("uint32_t cached_xer = ctx->xer;", "uint32_t cached_xer = 0;", StringComparison.Ordinal);
|
||||
if (!contract.ReadsCtrBeforeWrite && !contract.MayWriteCtr)
|
||||
body = body.Replace("uint32_t cached_ctr = ctx->ctr;", "uint32_t cached_ctr = 0;", StringComparison.Ordinal);
|
||||
|
||||
if (StateFreeHasOutputs(contract))
|
||||
{
|
||||
var result = StateFreeReturnExpression(contract, body);
|
||||
body = StateFreePatterns.BareReturn.Replace(body, result);
|
||||
if (!StateFreePatterns.AggregateReturn.IsMatch(body))
|
||||
body += Environment.NewLine + " " + result + Environment.NewLine;
|
||||
}
|
||||
|
||||
if (StateFreePatterns.ContextWord.IsMatch(body))
|
||||
{
|
||||
var retained = string.Join(" | ", body
|
||||
.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None)
|
||||
.Where(static line => StateFreePatterns.ContextWord.IsMatch(line))
|
||||
.Select(static line => line.Trim()));
|
||||
throw new InvalidOperationException(
|
||||
$"State-free ABI at 0x{entryPoint:X8} retained CpuContext access: {retained}");
|
||||
}
|
||||
|
||||
var variant = new StringBuilder(body.Length + 512);
|
||||
variant.AppendLine();
|
||||
AppendStateFreeResultDeclaration(variant, entryPoint, contract);
|
||||
var bodyAttribute = preferInline ? "MKW_PPC_ALWAYS_INLINE_BODY" : "MKW_PPC_NO_INLINE";
|
||||
variant.AppendLine($"extern \"C\" {bodyAttribute} {StateFreeReturnType(entryPoint, contract)} {StateFreeCallingConvention(contract)}{stateFreeSymbol}({StateFreeParameterList(contract, includeNames: true)})");
|
||||
variant.AppendLine("{");
|
||||
variant.Append(body);
|
||||
if (!body.EndsWith('\n')) variant.AppendLine();
|
||||
variant.AppendLine("}");
|
||||
facts = new GuestStateFreeEmissionFacts(
|
||||
entryPoint, stateFreeSymbol, contract, StateFreeGprInputMask(contract));
|
||||
variant.AppendLine($"// RECOMP_STATE_FREE_ABI address=0x{entryPoint:X8} symbol={stateFreeSymbol} " +
|
||||
$"gpr_in=0x{StateFreeGprInputMask(contract):X8} gpr_out=0x{contract.GprPossibleWriteMask:X8} " +
|
||||
$"fpr_in=0x{contract.FprReadBeforeWriteMask:X8} fpr_out=0x{contract.FprPossibleWriteMask:X8} " +
|
||||
$"cr_in=0x{contract.CrReadBeforeWriteMask:X2} cr_out=0x{contract.CrPossibleWriteMask:X2} " +
|
||||
$"gqr_in=0x{contract.GqrReadBeforeWriteMask:X2} hid_in=0x{contract.HidReadBeforeWriteMask:X2} hid_out=0x{contract.HidPossibleWriteMask:X2} " +
|
||||
$"xer_in={(contract.ReadsXerBeforeWrite ? 1 : 0)} ctr_in={(contract.ReadsCtrBeforeWrite ? 1 : 0)} lr_in={(contract.ReadsLrBeforeWrite ? 1 : 0)}");
|
||||
return code.Insert(closeBrace + 1, variant.ToString());
|
||||
}
|
||||
|
||||
private static string RewriteStateFreeBoundaryCalls(
|
||||
string body,
|
||||
uint entryPoint,
|
||||
IReadOnlySet<uint> boundaryTargets,
|
||||
IReadOnlyDictionary<uint, GuestAbiContract> contracts)
|
||||
{
|
||||
// The body is fixed for the whole rewrite, so the cached-local inventory
|
||||
// is collected once instead of once per register per matched call site.
|
||||
var declared = CollectStateFreeCachedDeclarations(body);
|
||||
return StateFreePatterns.BoundaryDirectCall.Replace(body, match =>
|
||||
{
|
||||
var address = uint.Parse(match.Groups["address"].Value,
|
||||
System.Globalization.NumberStyles.HexNumber,
|
||||
System.Globalization.CultureInfo.InvariantCulture);
|
||||
if (!boundaryTargets.Contains(address))
|
||||
return match.Value;
|
||||
if (!contracts.TryGetValue(address, out var boundary) || boundary.HasFullSynchronizationFence)
|
||||
throw new InvalidOperationException(
|
||||
$"Fused state-free boundary 0x{entryPoint:X8} -> 0x{address:X8} has no precise contract.");
|
||||
|
||||
var pad = match.Groups["pad"].Value;
|
||||
var replacement = new StringBuilder();
|
||||
string CurrentGpr(int index) =>
|
||||
declared.Contains($"cached_r{index}") ? $"cached_r{index}" : $"native_r{index}";
|
||||
string CurrentFpr(int index) =>
|
||||
declared.Contains($"cached_f{index}") ? $"cached_f{index}" : $"native_f{index}";
|
||||
var inputGprs = boundary.GprReadBeforeWriteMask | boundary.GprPossibleWriteMask;
|
||||
var inputFprs = boundary.FprReadBeforeWriteMask | boundary.FprPossibleWriteMask;
|
||||
for (var gpr = 0; gpr < 32; ++gpr)
|
||||
if ((inputGprs & (1u << gpr)) != 0)
|
||||
replacement.AppendLine($"{pad}fused_boundary_ctx->gpr[{gpr}] = {CurrentGpr(gpr)};");
|
||||
for (var fpr = 0; fpr < 32; ++fpr)
|
||||
if ((inputFprs & (1u << fpr)) != 0)
|
||||
replacement.AppendLine($"{pad}fused_boundary_ctx->fpr[{fpr}] = {CurrentFpr(fpr)};");
|
||||
if (boundary.CrReadBeforeWriteMask != 0 || boundary.CrPossibleWriteMask != 0)
|
||||
replacement.AppendLine($"{pad}fused_boundary_ctx->cr = cached_cr;");
|
||||
if (boundary.ReadsXerBeforeWrite || boundary.MayWriteXer)
|
||||
replacement.AppendLine($"{pad}fused_boundary_ctx->xer = cached_xer;");
|
||||
if (boundary.ReadsCtrBeforeWrite || boundary.MayWriteCtr)
|
||||
replacement.AppendLine($"{pad}fused_boundary_ctx->ctr = cached_ctr;");
|
||||
if (boundary.ReadsLrBeforeWrite || boundary.MayWriteLr)
|
||||
replacement.AppendLine($"{pad}fused_boundary_ctx->lr = native_lr;");
|
||||
if (boundary.ReadsFpscrBeforeWrite || boundary.MayWriteFpscr)
|
||||
replacement.AppendLine($"{pad}fused_boundary_ctx->fpscr = native_fpscr;");
|
||||
for (var gqr = 0; gqr < 8; ++gqr)
|
||||
if (((boundary.GqrReadBeforeWriteMask | boundary.GqrPossibleWriteMask) & (1 << gqr)) != 0)
|
||||
replacement.AppendLine($"{pad}fused_boundary_ctx->gqr[{gqr}] = native_gqr{gqr};");
|
||||
for (var hid = 0; hid < 3; ++hid)
|
||||
if (((boundary.HidReadBeforeWriteMask | boundary.HidPossibleWriteMask) & (1 << hid)) != 0)
|
||||
replacement.AppendLine($"{pad}fused_boundary_ctx->hid{hid} = native_hid{hid};");
|
||||
|
||||
replacement.AppendLine($"{pad}InvokeDirectCpu<0x{address:X8}u>(fused_boundary_ctx);");
|
||||
|
||||
for (var gpr = 0; gpr < 32; ++gpr)
|
||||
if ((boundary.GprPossibleWriteMask & (1u << gpr)) != 0)
|
||||
replacement.AppendLine($"{pad}{CurrentGpr(gpr)} = fused_boundary_ctx->gpr[{gpr}];");
|
||||
for (var fpr = 0; fpr < 32; ++fpr)
|
||||
if ((boundary.FprPossibleWriteMask & (1u << fpr)) != 0)
|
||||
replacement.AppendLine($"{pad}{CurrentFpr(fpr)} = fused_boundary_ctx->fpr[{fpr}];");
|
||||
if (boundary.CrPossibleWriteMask != 0)
|
||||
replacement.AppendLine($"{pad}cached_cr = fused_boundary_ctx->cr;");
|
||||
if (boundary.MayWriteXer)
|
||||
replacement.AppendLine($"{pad}cached_xer = fused_boundary_ctx->xer;");
|
||||
if (boundary.MayWriteCtr)
|
||||
replacement.AppendLine($"{pad}cached_ctr = fused_boundary_ctx->ctr;");
|
||||
if (boundary.MayWriteLr)
|
||||
replacement.AppendLine($"{pad}native_lr = fused_boundary_ctx->lr;");
|
||||
if (boundary.MayWriteFpscr)
|
||||
replacement.AppendLine($"{pad}native_fpscr = fused_boundary_ctx->fpscr;");
|
||||
for (var gqr = 0; gqr < 8; ++gqr)
|
||||
if ((boundary.GqrPossibleWriteMask & (1 << gqr)) != 0)
|
||||
replacement.AppendLine($"{pad}native_gqr{gqr} = fused_boundary_ctx->gqr[{gqr}];");
|
||||
for (var hid = 0; hid < 3; ++hid)
|
||||
if ((boundary.HidPossibleWriteMask & (1 << hid)) != 0)
|
||||
replacement.AppendLine($"{pad}native_hid{hid} = fused_boundary_ctx->hid{hid};");
|
||||
return replacement.ToString().TrimEnd();
|
||||
});
|
||||
}
|
||||
|
||||
private static string EnsureStateFreeBoundarySpecialLocals(
|
||||
string body,
|
||||
uint entryPoint,
|
||||
IReadOnlySet<uint> boundaryTargets,
|
||||
IReadOnlyDictionary<uint, GuestAbiContract> contracts)
|
||||
{
|
||||
var needsCr = false;
|
||||
var needsXer = false;
|
||||
var needsCtr = false;
|
||||
// Collected in one pass; the per-target patterns only differed in the
|
||||
// literal address, and the address text they matched is captured here.
|
||||
var presentBoundaryAddresses = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (Match match in StateFreePatterns.BoundaryDirectCallAddress.Matches(body))
|
||||
{
|
||||
presentBoundaryAddresses.Add(match.Groups[1].Value);
|
||||
}
|
||||
|
||||
foreach (var address in boundaryTargets)
|
||||
{
|
||||
// The selector owns one boundary set for the whole fused region,
|
||||
// while this method is producing one member clone. Only calls that
|
||||
// actually occur in this member contribute to its local residency
|
||||
// and signature requirements.
|
||||
if (!presentBoundaryAddresses.Contains(address.ToString("X8", System.Globalization.CultureInfo.InvariantCulture)))
|
||||
continue;
|
||||
if (!contracts.TryGetValue(address, out var boundary) || boundary.HasFullSynchronizationFence)
|
||||
throw new InvalidOperationException(
|
||||
$"Fused state-free boundary 0x{entryPoint:X8} -> 0x{address:X8} has no precise contract.");
|
||||
needsCr |= boundary.CrReadBeforeWriteMask != 0 || boundary.CrPossibleWriteMask != 0;
|
||||
needsXer |= boundary.ReadsXerBeforeWrite || boundary.MayWriteXer;
|
||||
needsCtr |= boundary.ReadsCtrBeforeWrite || boundary.MayWriteCtr;
|
||||
}
|
||||
|
||||
// A region's interprocedural contract can make special state live solely because an outlined
|
||||
// descendant observes it, even when the root body never created the usual cached local. Derive
|
||||
// these locals from the precise boundary contracts rather than incidental root-body instructions.
|
||||
var declarations = new StringBuilder();
|
||||
var declared = needsCr || needsXer || needsCtr
|
||||
? CollectStateFreeCachedDeclarations(body)
|
||||
: null;
|
||||
if (needsCr && !declared!.Contains("cached_cr"))
|
||||
declarations.AppendLine(" uint32_t cached_cr = native_cr;");
|
||||
if (needsXer && !declared!.Contains("cached_xer"))
|
||||
declarations.AppendLine(" uint32_t cached_xer = native_xer;");
|
||||
if (needsCtr && !declared!.Contains("cached_ctr"))
|
||||
declarations.AppendLine(" uint32_t cached_ctr = native_ctr;");
|
||||
return declarations.Length == 0 ? body : declarations + body;
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Translator.Core.IO;
|
||||
using Translator.Core.Loading;
|
||||
using Translator.Core.Parsing.Dol;
|
||||
using Translator.Core.Parsing.Rel;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
/// <summary>
|
||||
/// Generates C++ code to initialize DOL and REL data sections directly in the binary.
|
||||
/// This allows running without loading a RAM dump at runtime.
|
||||
/// </summary>
|
||||
public static class DataSectionGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a C++ header/source pair that initializes all data sections.
|
||||
/// </summary>
|
||||
public static void Generate(
|
||||
DolFile dol,
|
||||
RelImage? rel,
|
||||
string outputPath,
|
||||
string projectName = "PowerPC DOL",
|
||||
string relName = "rel_module",
|
||||
string? blobReferenceDirectory = null)
|
||||
{
|
||||
var sections = new List<DataSectionEntry>();
|
||||
|
||||
// Add DOL data sections (skip .bss - it's zero-initialized)
|
||||
foreach (var section in dol.Sections)
|
||||
{
|
||||
if (section.Kind == SectionKind.Bss || !section.HasData || section.Size == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Include both code and data sections - they all need to be in memory
|
||||
sections.Add(new DataSectionEntry(
|
||||
Name: SanitizeName(section.Name),
|
||||
Address: section.VirtualAddress,
|
||||
Data: section.Data,
|
||||
Source: "DOL"));
|
||||
}
|
||||
|
||||
// Add REL data if provided
|
||||
if (rel != null && rel.Data.Length > 0)
|
||||
{
|
||||
sections.Add(new DataSectionEntry(
|
||||
Name: SanitizeName(relName),
|
||||
Address: rel.BaseAddress,
|
||||
Data: rel.Data,
|
||||
Source: "REL"));
|
||||
}
|
||||
|
||||
var blobDirectory = Path.Combine(
|
||||
Path.GetDirectoryName(outputPath) ?? ".",
|
||||
Path.GetFileNameWithoutExtension(outputPath) + "_blobs");
|
||||
var blobAssemblyPath = Path.Combine(
|
||||
Path.GetDirectoryName(outputPath) ?? ".",
|
||||
Path.GetFileNameWithoutExtension(outputPath) + "_blobs.S");
|
||||
|
||||
WriteBlobFiles(
|
||||
sections,
|
||||
blobDirectory,
|
||||
blobReferenceDirectory ?? blobDirectory,
|
||||
blobAssemblyPath);
|
||||
WriteSourceFile(sections, outputPath, dol.BssAddress, dol.BssSize, projectName);
|
||||
}
|
||||
|
||||
private static string SanitizeName(string name)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var c in name)
|
||||
{
|
||||
if (char.IsLetterOrDigit(c))
|
||||
{
|
||||
sb.Append(c);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append('_');
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static void WriteBlobFiles(
|
||||
List<DataSectionEntry> sections,
|
||||
string blobDirectory,
|
||||
string blobReferenceDirectory,
|
||||
string assemblyPath)
|
||||
{
|
||||
var blobs = sections.Select(section => new AssemblyBlob(
|
||||
$"{section.Name}.bin",
|
||||
$"kData_{section.Name}",
|
||||
section.Data,
|
||||
$"{section.Source}: {section.Name} @ 0x{section.Address:X8} ({section.Data.Length} bytes)"))
|
||||
.ToList();
|
||||
AssemblyBlobWriter.Write(
|
||||
assemblyPath,
|
||||
blobDirectory,
|
||||
blobReferenceDirectory,
|
||||
blobs,
|
||||
"// AUTO-GENERATED - DO NOT EDIT",
|
||||
"// Binary DOL/REL section payloads for data_sections_init.cpp.");
|
||||
}
|
||||
|
||||
private static void WriteSourceFile(
|
||||
List<DataSectionEntry> sections,
|
||||
string path,
|
||||
uint bssAddress,
|
||||
uint bssSize,
|
||||
string projectName)
|
||||
{
|
||||
var sb = new StringBuilder(capacity: 8 * 1024 * 1024);
|
||||
using var writer = new StringWriter(sb);
|
||||
|
||||
writer.WriteLine("// AUTO-GENERATED - DO NOT EDIT");
|
||||
writer.WriteLine($"// Data section initializer for {projectName}");
|
||||
writer.WriteLine("// This file embeds the DOL and REL data sections directly in the binary.");
|
||||
writer.WriteLine("#include \"memory.h\"");
|
||||
writer.WriteLine("#include <cstring>");
|
||||
writer.WriteLine("#include <cstddef>");
|
||||
writer.WriteLine("#include <cstdint>");
|
||||
writer.WriteLine("#include <iostream>");
|
||||
writer.WriteLine();
|
||||
|
||||
writer.WriteLine("extern \"C\" {");
|
||||
foreach (var section in sections)
|
||||
{
|
||||
writer.WriteLine($"extern const uint8_t kData_{section.Name}[];");
|
||||
}
|
||||
writer.WriteLine("} // extern \"C\"");
|
||||
|
||||
// Write the initialization function with C linkage (for cross-compilation-unit linking)
|
||||
writer.WriteLine();
|
||||
writer.WriteLine("namespace {");
|
||||
writer.WriteLine("bool g_dataInitialized = false;");
|
||||
writer.WriteLine("} // namespace");
|
||||
writer.WriteLine();
|
||||
writer.WriteLine("extern \"C\" void InitializeDataSections() {");
|
||||
writer.WriteLine(" if (g_dataInitialized) return;");
|
||||
writer.WriteLine(" g_dataInitialized = true;");
|
||||
writer.WriteLine();
|
||||
writer.WriteLine(" std::cout << \"[runtime] Initializing embedded data sections...\" << std::endl;");
|
||||
writer.WriteLine();
|
||||
|
||||
foreach (var section in sections)
|
||||
{
|
||||
writer.WriteLine($" // {section.Source} section: {section.Name} @ 0x{section.Address:X8}");
|
||||
writer.WriteLine($" constexpr size_t kSize_{section.Name} = {section.Data.Length}u;");
|
||||
writer.WriteLine($" if (Memory::Contains(0x{section.Address:X8}u, kSize_{section.Name})) {{");
|
||||
writer.WriteLine($" std::memcpy(Memory::GetPointer(0x{section.Address:X8}u, kSize_{section.Name}),");
|
||||
writer.WriteLine($" kData_{section.Name}, kSize_{section.Name});");
|
||||
writer.WriteLine($" std::cout << \"[runtime] Loaded {section.Name} ({section.Data.Length} bytes) @ 0x{section.Address:X8}\" << std::endl;");
|
||||
writer.WriteLine(" }");
|
||||
writer.WriteLine();
|
||||
}
|
||||
|
||||
// BSS is zero-initialized (memory starts at zero so we don't need to do anything)
|
||||
if (bssSize > 0)
|
||||
{
|
||||
writer.WriteLine($" // BSS section @ 0x{bssAddress:X8} ({bssSize} bytes) - memory already zero-initialized");
|
||||
}
|
||||
|
||||
writer.WriteLine("}");
|
||||
writer.WriteLine();
|
||||
|
||||
// Also provide a check function with C linkage
|
||||
writer.WriteLine("extern \"C\" bool IsDataSectionsInitialized() {");
|
||||
writer.WriteLine(" return g_dataInitialized;");
|
||||
writer.WriteLine("}");
|
||||
|
||||
FileOutput.WriteTextIfChanged(path, sb.ToString());
|
||||
}
|
||||
|
||||
private record DataSectionEntry(string Name, uint Address, ReadOnlyMemory<byte> Data, string Source);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Translator.Core;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
public sealed class GuestFunctionAbi
|
||||
{
|
||||
private readonly HashSet<string> _scalarFloatArgumentRegisters;
|
||||
private readonly HashSet<string> _argumentRegisters;
|
||||
|
||||
public GuestFunctionAbi(
|
||||
IEnumerable<string>? scalarFloatArgumentRegisters = null,
|
||||
bool returnsPairedScalarFloat = false,
|
||||
bool writesFloatReturnRegister = false,
|
||||
IEnumerable<string>? argumentRegisters = null,
|
||||
bool preservesVolatileContext = false,
|
||||
bool writesGprReturnRegister = true)
|
||||
{
|
||||
_scalarFloatArgumentRegisters = (scalarFloatArgumentRegisters ?? Enumerable.Empty<string>())
|
||||
.Select(RegisterNameUtils.StripNumericSuffix)
|
||||
.Where(static r => !string.IsNullOrWhiteSpace(r))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
_argumentRegisters = (argumentRegisters ?? Enumerable.Empty<string>())
|
||||
.Select(RegisterNameUtils.StripNumericSuffix)
|
||||
.Where(static r => !string.IsNullOrWhiteSpace(r))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
ReturnsPairedScalarFloat = returnsPairedScalarFloat;
|
||||
WritesFloatReturnRegister = writesFloatReturnRegister || returnsPairedScalarFloat;
|
||||
PreservesVolatileContext = preservesVolatileContext;
|
||||
WritesGprReturnRegister = writesGprReturnRegister;
|
||||
}
|
||||
|
||||
public static GuestFunctionAbi Empty { get; } = new();
|
||||
|
||||
public IReadOnlySet<string> ScalarFloatArgumentRegisters => _scalarFloatArgumentRegisters;
|
||||
|
||||
public IReadOnlySet<string> ArgumentRegisters => _argumentRegisters;
|
||||
|
||||
public bool HasKnownArgumentRegisters => _argumentRegisters.Count != 0;
|
||||
|
||||
public bool ReturnsPairedScalarFloat { get; }
|
||||
|
||||
public bool WritesFloatReturnRegister { get; }
|
||||
|
||||
public bool WritesGprReturnRegister { get; }
|
||||
|
||||
public bool PreservesVolatileContext { get; }
|
||||
|
||||
public bool HasScalarFloatArgument(string registerName)
|
||||
{
|
||||
var baseName = RegisterNameUtils.StripNumericSuffix(registerName);
|
||||
return _scalarFloatArgumentRegisters.Contains(baseName);
|
||||
}
|
||||
|
||||
public bool HasArgumentRegister(string registerName)
|
||||
{
|
||||
var baseName = RegisterNameUtils.StripNumericSuffix(registerName);
|
||||
return _argumentRegisters.Contains(baseName);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IGuestFunctionAbiProvider
|
||||
{
|
||||
bool TryGetGuestFunctionAbi(string target, out GuestFunctionAbi abi);
|
||||
}
|
||||
|
||||
public sealed class CompositeGuestFunctionAbiProvider : IGuestFunctionAbiProvider
|
||||
{
|
||||
private readonly IReadOnlyList<IGuestFunctionAbiProvider> _providers;
|
||||
|
||||
public CompositeGuestFunctionAbiProvider(params IGuestFunctionAbiProvider[] providers)
|
||||
{
|
||||
_providers = providers.Where(static p => p is not null).ToArray();
|
||||
}
|
||||
|
||||
public bool TryGetGuestFunctionAbi(string target, out GuestFunctionAbi abi)
|
||||
{
|
||||
foreach (var provider in _providers)
|
||||
{
|
||||
if (provider.TryGetGuestFunctionAbi(target, out abi))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
abi = GuestFunctionAbi.Empty;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EmptyGuestFunctionAbiProvider : IGuestFunctionAbiProvider
|
||||
{
|
||||
public static EmptyGuestFunctionAbiProvider Instance { get; } = new();
|
||||
|
||||
private EmptyGuestFunctionAbiProvider()
|
||||
{
|
||||
}
|
||||
|
||||
public bool TryGetGuestFunctionAbi(string target, out GuestFunctionAbi abi)
|
||||
{
|
||||
abi = GuestFunctionAbi.Empty;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,798 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Globalization;
|
||||
using Translator.Core.Ir;
|
||||
using Translator.Core.Analysis;
|
||||
using Translator.Core.Analysis.Ssa;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
/// <summary>
|
||||
/// Converts repeated accesses through one SSA base into an explicit resolved
|
||||
/// guest-memory range. Lifetimes are deliberately bounded by calls and block
|
||||
/// edges; widening across CFG edges is a separate dominance proof.
|
||||
/// </summary>
|
||||
public static class GuestMemoryRangeLowering
|
||||
{
|
||||
private const int MaxRangeLength = 4096;
|
||||
|
||||
// Hardware register window (gather pipe, CP/PE/PI registers, EFB aperture below 0xCE000000).
|
||||
// Nothing here is backed by a host mapping, so hoisting it would only feed the cold unresolved
|
||||
// chain, and it would also rewrite gather-pipe stores out of the IrStore shape that
|
||||
// TryEmitKnownGpuFifoStore needs for its direct GX_HLE_FIFO_Write* fast path. Keep it unhoisted.
|
||||
private const uint HardwareRegisterWindowStart = 0xCC000000u;
|
||||
private const uint HardwareRegisterWindowEnd = 0xCE000000u;
|
||||
|
||||
public static IrFunction Lower(IrFunction function, int minimumAccesses = 8,
|
||||
bool ignoreDisabledPcTraces = false,
|
||||
bool scalarOnly = false)
|
||||
{
|
||||
var nextRange = 0;
|
||||
var hardwareConstants = CxxLinearCodeGenerator.BuildKnownUIntConstants(function);
|
||||
function = LowerFunctionLiveInRanges(
|
||||
function, ref nextRange, minimumAccesses, ignoreDisabledPcTraces,
|
||||
hardwareConstants);
|
||||
var blocks = function.Blocks.Select(block =>
|
||||
block with { Instructions = CombineAdjacentLoads(
|
||||
LowerBlock(block.Instructions, ref nextRange, minimumAccesses,
|
||||
scalarOnly, hardwareConstants),
|
||||
ignoreDisabledPcTraces) }).ToArray();
|
||||
return function with { Blocks = blocks };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when the instruction's effective address is a compile-time constant inside the
|
||||
/// hardware register window. Must use the same constant map as <see cref="CxxLinearCodeGenerator.BuildKnownUIntConstants"/>
|
||||
/// so every gather-pipe write the emitter would classify is excluded here too.
|
||||
/// </summary>
|
||||
internal static bool IsProvenHardwareRegisterAccess(
|
||||
IrInstruction instruction, IReadOnlyDictionary<string, uint>? constants)
|
||||
{
|
||||
if (constants is null || constants.Count == 0)
|
||||
return false;
|
||||
var address = instruction switch
|
||||
{
|
||||
IrLoad load => load.Address,
|
||||
IrStore store => store.Address,
|
||||
IrCall call when TryParsePsq(call, out _) &&
|
||||
call.Arguments[0].RegisterName is { } psqBase =>
|
||||
new IrAddress(psqBase, 0),
|
||||
_ => null
|
||||
};
|
||||
if (address is null || !constants.TryGetValue(address.Base, out var baseAddress))
|
||||
return false;
|
||||
var effective = unchecked(baseAddress + (uint)address.Offset);
|
||||
return effective >= HardwareRegisterWindowStart && effective < HardwareRegisterWindowEnd;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reuses scalar ranges across CFG edges only when the first access dominates every consumer
|
||||
/// and no invalidating call can occur on any path between them (the cross-CFG memory epoch proof).
|
||||
/// </summary>
|
||||
private static IrFunction LowerFunctionLiveInRanges(
|
||||
IrFunction function, ref int nextRange, int minimumAccesses,
|
||||
bool ignoreDisabledPcTraces,
|
||||
IReadOnlyDictionary<string, uint>? hardwareConstants = null)
|
||||
{
|
||||
var definitions = new Dictionary<string, List<DefinitionSite>>(StringComparer.OrdinalIgnoreCase);
|
||||
for (var blockIndex = 0; blockIndex < function.Blocks.Count; blockIndex++)
|
||||
for (var instructionIndex = 0;
|
||||
instructionIndex < function.Blocks[blockIndex].Instructions.Count;
|
||||
instructionIndex++)
|
||||
{
|
||||
foreach (var destination in DefinedDestinations(
|
||||
function.Blocks[blockIndex].Instructions[instructionIndex]))
|
||||
{
|
||||
if (!definitions.TryGetValue(destination, out var sites))
|
||||
definitions[destination] = sites = new List<DefinitionSite>();
|
||||
sites.Add(new DefinitionSite(blockIndex, instructionIndex));
|
||||
}
|
||||
}
|
||||
var candidates = new List<FunctionAccess>();
|
||||
for (var blockIndex = 0; blockIndex < function.Blocks.Count; blockIndex++)
|
||||
{
|
||||
foreach (var access in BuildAccesses(function.Blocks[blockIndex].Instructions,
|
||||
normalizeArchitecturalRegisters: false, hardwareConstants))
|
||||
if (access.Psq is null)
|
||||
candidates.Add(new FunctionAccess(blockIndex, access));
|
||||
}
|
||||
if (!candidates.GroupBy(candidate => new AccessClass(
|
||||
candidate.Access.Address.Base, candidate.Access.IsStore))
|
||||
.Any(group => group.Count() >= minimumAccesses))
|
||||
return function;
|
||||
|
||||
var cfg = IrCfg.Build(function);
|
||||
var dominators = ComputeDominators(cfg);
|
||||
var reachability = ComputeReachability(cfg);
|
||||
var cyclicBlocks = cfg.Blocks.Keys.Where(label =>
|
||||
cfg.Successors(label).Any(successor => reachability[successor].Contains(label)))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var plans = new Dictionary<(int Block, int Instruction), Plan>();
|
||||
var resolves = new Dictionary<int, List<PlacedResolve>>();
|
||||
foreach (var group in candidates.GroupBy(candidate => new AccessClass(
|
||||
candidate.Access.Address.Base, candidate.Access.IsStore)))
|
||||
{
|
||||
var remaining = group.ToList();
|
||||
if (remaining.Count < minimumAccesses) continue;
|
||||
if (remaining.Any(candidate => HasOverlappingOppositeAccess(
|
||||
candidate.Access, candidates.Select(static item => item.Access))))
|
||||
continue;
|
||||
var invalidators = CollectInvalidators(function, group.Key.Base);
|
||||
DefinitionSite? definition = null;
|
||||
if (definitions.TryGetValue(group.Key.Base, out var definitionSites))
|
||||
{
|
||||
// The SSA value must have one authoritative definition and it
|
||||
// must dominate every consumer. Reused version names on
|
||||
// mutually-exclusive edges are conservatively rejected.
|
||||
if (definitionSites.Count != 1) continue;
|
||||
definition = definitionSites[0];
|
||||
var defined = definitionSites[0];
|
||||
var definitionBlock = function.Blocks[defined.BlockIndex].Label;
|
||||
if (remaining.Any(candidate =>
|
||||
!dominators[candidate.AccessBlockLabel(function)].Contains(definitionBlock) ||
|
||||
candidate.BlockIndex == defined.BlockIndex &&
|
||||
candidate.Access.Index <= defined.InstructionIndex))
|
||||
continue;
|
||||
}
|
||||
while (remaining.Count >= minimumAccesses)
|
||||
{
|
||||
FunctionAccess? bestAnchor = null;
|
||||
FunctionAccess[] bestMembers = [];
|
||||
foreach (var anchor in remaining)
|
||||
{
|
||||
var members = remaining.Where(candidate =>
|
||||
DominatesAccess(function, dominators, anchor, candidate) &&
|
||||
!HasInvalidatingBoundaryOnPath(
|
||||
function, reachability, cyclicBlocks,
|
||||
invalidators, anchor, candidate))
|
||||
.OrderBy(candidate => candidate.Access.Index)
|
||||
.ToList();
|
||||
// Keep a bounded affine window around the real anchor. A
|
||||
// distant outlier must not suppress a useful safe fragment.
|
||||
var selected = new List<FunctionAccess>();
|
||||
long windowMin = anchor.Access.Address.Offset;
|
||||
long windowMax = windowMin + anchor.Access.SizeBytes;
|
||||
foreach (var candidate in members.OrderBy(candidate =>
|
||||
Math.Abs((long)candidate.Access.Address.Offset - anchor.Access.Address.Offset)))
|
||||
{
|
||||
var nextMin = Math.Min(windowMin, candidate.Access.Address.Offset);
|
||||
var nextMax = Math.Max(windowMax,
|
||||
(long)candidate.Access.Address.Offset + candidate.Access.SizeBytes);
|
||||
if (nextMax - nextMin > MaxRangeLength) continue;
|
||||
windowMin = nextMin;
|
||||
windowMax = nextMax;
|
||||
selected.Add(candidate);
|
||||
}
|
||||
if (selected.Count > bestMembers.Length)
|
||||
{
|
||||
bestAnchor = anchor;
|
||||
bestMembers = selected.ToArray();
|
||||
}
|
||||
}
|
||||
if (bestAnchor is null || bestMembers.Length < minimumAccesses) break;
|
||||
|
||||
var min = bestMembers.Min(static candidate => (long)candidate.Access.Address.Offset);
|
||||
var max = bestMembers.Max(static candidate =>
|
||||
(long)candidate.Access.Address.Offset + candidate.Access.SizeBytes);
|
||||
var range = $"guest_range_{nextRange++}";
|
||||
var plan = new Plan(range, (int)min, (int)(max - min),
|
||||
NeedsRead: !group.Key.IsStore, NeedsWrite: group.Key.IsStore);
|
||||
var resolve = new IrResolveGuestMemoryRange(range, IrValue.Register(group.Key.Base),
|
||||
plan.MinOffset, plan.Length, plan.NeedsRead, plan.NeedsWrite);
|
||||
if (!resolves.TryGetValue(bestAnchor.BlockIndex, out var placed))
|
||||
resolves[bestAnchor.BlockIndex] = placed = new List<PlacedResolve>();
|
||||
// Placement is immediately before an access that executes on
|
||||
// every path reaching the remaining consumers.
|
||||
placed.Add(new PlacedResolve(bestAnchor.Access.Index - 1, resolve));
|
||||
foreach (var candidate in bestMembers)
|
||||
{
|
||||
plans[(candidate.BlockIndex, candidate.Access.Index)] = plan;
|
||||
remaining.Remove(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resolves.Count == 0) return function;
|
||||
|
||||
var blocks = new IrBasicBlock[function.Blocks.Count];
|
||||
for (var blockIndex = 0; blockIndex < function.Blocks.Count; blockIndex++)
|
||||
{
|
||||
var source = function.Blocks[blockIndex].Instructions;
|
||||
var accessMap = BuildAccesses(source, normalizeArchitecturalRegisters: false, hardwareConstants)
|
||||
.ToDictionary(static access => access.Index);
|
||||
resolves.TryGetValue(blockIndex, out var blockResolves);
|
||||
var output = new List<IrInstruction>(source.Count + (blockResolves?.Count ?? 0));
|
||||
// LLVM requires the phi set to stay a contiguous block prefix, so any resolve placed
|
||||
// at or before a leading phi must be emitted after the whole phi set instead.
|
||||
var leadingPhiEnd = -1;
|
||||
var sawMaterialInstruction = false;
|
||||
for (var index = 0; index < source.Count; ++index)
|
||||
{
|
||||
if (source[index] is IrPhi && !sawMaterialInstruction)
|
||||
{
|
||||
leadingPhiEnd = index;
|
||||
continue;
|
||||
}
|
||||
if (source[index] is IrComment or IrTracePpc) continue;
|
||||
sawMaterialInstruction = true;
|
||||
}
|
||||
var placedAfter = blockResolves?.GroupBy(placed =>
|
||||
placed.AfterInstructionIndex < 0 ||
|
||||
placed.AfterInstructionIndex <= leadingPhiEnd &&
|
||||
source[placed.AfterInstructionIndex] is IrPhi
|
||||
? leadingPhiEnd
|
||||
: placed.AfterInstructionIndex)
|
||||
.ToDictionary(static group => group.Key,
|
||||
static group => group.Select(placed => placed.Resolve).ToArray());
|
||||
if (placedAfter is not null && leadingPhiEnd < 0 && placedAfter.Remove(-1, out var entryResolves))
|
||||
output.AddRange(entryResolves);
|
||||
for (var instructionIndex = 0; instructionIndex < source.Count; instructionIndex++)
|
||||
{
|
||||
if (!plans.TryGetValue((blockIndex, instructionIndex), out var plan))
|
||||
{
|
||||
output.Add(source[instructionIndex]);
|
||||
if (placedAfter is not null && placedAfter.TryGetValue(instructionIndex, out var afterUnchanged))
|
||||
output.AddRange(afterUnchanged);
|
||||
continue;
|
||||
}
|
||||
var access = accessMap[instructionIndex];
|
||||
output.Add(ToResolved(source[instructionIndex], access, plan));
|
||||
if (placedAfter is not null && placedAfter.TryGetValue(instructionIndex, out var afterResolved))
|
||||
output.AddRange(afterResolved);
|
||||
}
|
||||
blocks[blockIndex] = function.Blocks[blockIndex] with
|
||||
{ Instructions = CombineAdjacentLoads(output, ignoreDisabledPcTraces) };
|
||||
}
|
||||
return function with { Blocks = blocks };
|
||||
}
|
||||
|
||||
private static bool DominatesAccess(
|
||||
IrFunction function,
|
||||
IReadOnlyDictionary<string, HashSet<string>> dominators,
|
||||
FunctionAccess anchor,
|
||||
FunctionAccess consumer)
|
||||
{
|
||||
if (anchor.BlockIndex == consumer.BlockIndex)
|
||||
return anchor.Access.Index <= consumer.Access.Index;
|
||||
return dominators[consumer.AccessBlockLabel(function)].Contains(
|
||||
anchor.AccessBlockLabel(function));
|
||||
}
|
||||
|
||||
private static bool HasOverlappingOppositeAccess(
|
||||
Access access, IEnumerable<Access> candidates)
|
||||
{
|
||||
var accessStart = (long)access.Address.Offset;
|
||||
var accessEnd = accessStart + access.SizeBytes;
|
||||
return candidates.Any(candidate =>
|
||||
candidate.IsStore != access.IsStore &&
|
||||
candidate.Address.Base.Equals(
|
||||
access.Address.Base, StringComparison.OrdinalIgnoreCase) &&
|
||||
accessStart < (long)candidate.Address.Offset + candidate.SizeBytes &&
|
||||
candidate.Address.Offset < accessEnd);
|
||||
}
|
||||
|
||||
private static bool HasInvalidatingBoundaryOnPath(
|
||||
IrFunction function,
|
||||
IReadOnlyDictionary<string, HashSet<string>> reachability,
|
||||
IReadOnlySet<string> cyclicBlocks,
|
||||
IReadOnlyList<InstructionSite> invalidators,
|
||||
FunctionAccess anchor,
|
||||
FunctionAccess consumer)
|
||||
{
|
||||
var anchorLabel = anchor.AccessBlockLabel(function);
|
||||
var consumerLabel = consumer.AccessBlockLabel(function);
|
||||
foreach (var invalidator in invalidators)
|
||||
{
|
||||
var boundaryLabel = function.Blocks[invalidator.BlockIndex].Label;
|
||||
var afterAnchor = invalidator.BlockIndex == anchor.BlockIndex
|
||||
? invalidator.InstructionIndex > anchor.Access.Index
|
||||
: reachability[anchorLabel].Contains(boundaryLabel);
|
||||
var beforeConsumer = invalidator.BlockIndex == consumer.BlockIndex
|
||||
? invalidator.InstructionIndex < consumer.Access.Index ||
|
||||
anchor.BlockIndex != consumer.BlockIndex &&
|
||||
cyclicBlocks.Contains(consumerLabel)
|
||||
: reachability[boundaryLabel].Contains(consumerLabel);
|
||||
if (afterAnchor && beforeConsumer) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<InstructionSite> CollectInvalidators(
|
||||
IrFunction function, string baseName)
|
||||
{
|
||||
var result = new List<InstructionSite>();
|
||||
for (var blockIndex = 0; blockIndex < function.Blocks.Count; blockIndex++)
|
||||
for (var instructionIndex = 0;
|
||||
instructionIndex < function.Blocks[blockIndex].Instructions.Count;
|
||||
instructionIndex++)
|
||||
{
|
||||
var instruction = function.Blocks[blockIndex].Instructions[instructionIndex];
|
||||
if (IsMemoryEpochBoundary(instruction) ||
|
||||
Defines(instruction, baseName, normalizeArchitecturalRegisters: true))
|
||||
result.Add(new InstructionSite(blockIndex, instructionIndex));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Dictionary<string, HashSet<string>> ComputeDominators(IrCfg cfg)
|
||||
{
|
||||
var comparer = StringComparer.OrdinalIgnoreCase;
|
||||
var labels = cfg.Blocks.Keys.ToArray();
|
||||
var all = labels.ToHashSet(comparer);
|
||||
var result = labels.ToDictionary(label => label,
|
||||
label => label.Equals(cfg.Entry, StringComparison.OrdinalIgnoreCase)
|
||||
? new HashSet<string>([label], comparer)
|
||||
: new HashSet<string>(all, comparer), comparer);
|
||||
bool changed;
|
||||
do
|
||||
{
|
||||
changed = false;
|
||||
foreach (var label in labels)
|
||||
{
|
||||
if (label.Equals(cfg.Entry, StringComparison.OrdinalIgnoreCase)) continue;
|
||||
var predecessors = cfg.Predecessors(label);
|
||||
var next = predecessors.Count == 0
|
||||
? new HashSet<string>(comparer)
|
||||
: new HashSet<string>(result[predecessors[0]], comparer);
|
||||
foreach (var predecessor in predecessors.Skip(1)) next.IntersectWith(result[predecessor]);
|
||||
next.Add(label);
|
||||
if (result[label].SetEquals(next)) continue;
|
||||
result[label] = next;
|
||||
changed = true;
|
||||
}
|
||||
} while (changed);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Dictionary<string, HashSet<string>> ComputeReachability(IrCfg cfg)
|
||||
{
|
||||
var comparer = StringComparer.OrdinalIgnoreCase;
|
||||
var result = new Dictionary<string, HashSet<string>>(comparer);
|
||||
foreach (var start in cfg.Blocks.Keys)
|
||||
{
|
||||
var reachable = new HashSet<string>(comparer);
|
||||
var pending = new Stack<string>();
|
||||
pending.Push(start);
|
||||
while (pending.Count != 0)
|
||||
{
|
||||
var current = pending.Pop();
|
||||
if (!reachable.Add(current)) continue;
|
||||
foreach (var successor in cfg.Successors(current)) pending.Push(successor);
|
||||
}
|
||||
result[start] = reachable;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IrInstruction ToResolved(IrInstruction instruction, Access access, Plan plan)
|
||||
{
|
||||
var relativeOffset = access.Address.Offset - plan.MinOffset;
|
||||
return instruction switch
|
||||
{
|
||||
IrLoad load => new IrResolvedLoad(load.Destination, plan.Range, load.Address,
|
||||
relativeOffset, load.SizeBytes),
|
||||
IrStore store => new IrResolvedStore(plan.Range, store.Address, relativeOffset,
|
||||
store.Source, store.SizeBytes),
|
||||
IrCall call when access.Psq is { IsStore: false } psq =>
|
||||
new IrResolvedPsqLoad(call.Destination, plan.Range, call.Arguments[0], relativeOffset,
|
||||
psq.W, psq.I, psq.KnownGqr, psq.GuardKnownGqr),
|
||||
IrCall call when access.Psq is { IsStore: true } psq =>
|
||||
new IrResolvedPsqStore(plan.Range, call.Arguments[0], relativeOffset, call.Arguments[1],
|
||||
psq.W, psq.I, psq.KnownGqr, psq.GuardKnownGqr),
|
||||
_ => instruction
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<IrInstruction> CombineAdjacentLoads(
|
||||
IReadOnlyList<IrInstruction> instructions, bool ignoreDisabledPcTraces)
|
||||
{
|
||||
var result = new List<IrInstruction>(instructions.Count);
|
||||
for (var index = 0; index < instructions.Count; index++)
|
||||
{
|
||||
var next = index + 1;
|
||||
if (instructions[index] is IrResolvedLoad first)
|
||||
{
|
||||
while (next < instructions.Count &&
|
||||
((ignoreDisabledPcTraces && instructions[next] is IrTracePpc) ||
|
||||
IsMovableAddressCalculation(instructions[next], first.Destination))) next++;
|
||||
}
|
||||
else if (instructions[index] is IrResolvedStore)
|
||||
{
|
||||
while (next < instructions.Count &&
|
||||
((ignoreDisabledPcTraces && instructions[next] is IrTracePpc) ||
|
||||
IsMovableAddressCalculation(instructions[next], null))) next++;
|
||||
}
|
||||
|
||||
if (next < instructions.Count &&
|
||||
instructions[index] is IrResolvedLoad firstLoad &&
|
||||
instructions[next] is IrResolvedLoad secondLoad &&
|
||||
firstLoad.Range.Equals(secondLoad.Range, StringComparison.Ordinal) &&
|
||||
firstLoad.SizeBytes == secondLoad.SizeBytes && firstLoad.SizeBytes is 2 or 4 &&
|
||||
Math.Abs(secondLoad.RangeOffset - firstLoad.RangeOffset) == firstLoad.SizeBytes)
|
||||
{
|
||||
for (var movable = index + 1; movable < next; movable++) result.Add(instructions[movable]);
|
||||
var descending = secondLoad.RangeOffset < firstLoad.RangeOffset;
|
||||
result.Add(new IrResolvedLoadPair(firstLoad.Destination, secondLoad.Destination, firstLoad.Range,
|
||||
firstLoad.OriginalAddress, secondLoad.OriginalAddress,
|
||||
Math.Min(firstLoad.RangeOffset, secondLoad.RangeOffset), firstLoad.SizeBytes, descending));
|
||||
index = next;
|
||||
continue;
|
||||
}
|
||||
if (next < instructions.Count &&
|
||||
instructions[index] is IrResolvedStore firstStore &&
|
||||
instructions[next] is IrResolvedStore secondStore &&
|
||||
firstStore.Range.Equals(secondStore.Range, StringComparison.Ordinal) &&
|
||||
firstStore.SizeBytes == secondStore.SizeBytes && firstStore.SizeBytes is 2 or 4 &&
|
||||
Math.Abs(secondStore.RangeOffset - firstStore.RangeOffset) == firstStore.SizeBytes)
|
||||
{
|
||||
for (var movable = index + 1; movable < next; movable++) result.Add(instructions[movable]);
|
||||
var descending = secondStore.RangeOffset < firstStore.RangeOffset;
|
||||
result.Add(new IrResolvedStorePair(firstStore.Range, firstStore.OriginalAddress,
|
||||
secondStore.OriginalAddress, Math.Min(firstStore.RangeOffset, secondStore.RangeOffset),
|
||||
firstStore.Source, secondStore.Source, firstStore.SizeBytes, descending));
|
||||
index = next;
|
||||
continue;
|
||||
}
|
||||
result.Add(instructions[index]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool IsMovableAddressCalculation(IrInstruction instruction, string? forbiddenUse)
|
||||
{
|
||||
static bool Uses(IrValue value, string name) =>
|
||||
value.RegisterName?.Equals(name, StringComparison.OrdinalIgnoreCase) == true;
|
||||
|
||||
return instruction switch
|
||||
{
|
||||
IrAssign assign when !IsArchitecturalRegister(assign.Destination) =>
|
||||
forbiddenUse is null || !Uses(assign.Value, forbiddenUse),
|
||||
IrBinary binary when !IsArchitecturalRegister(binary.Destination) =>
|
||||
forbiddenUse is null || (!Uses(binary.Left, forbiddenUse) && !Uses(binary.Right, forbiddenUse)),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<IrInstruction> LowerBlock(
|
||||
IReadOnlyList<IrInstruction> instructions,
|
||||
ref int nextRange,
|
||||
int minimumAccesses,
|
||||
bool scalarOnly,
|
||||
IReadOnlyDictionary<string, uint>? hardwareConstants = null)
|
||||
{
|
||||
var result = new List<IrInstruction>(instructions.Count);
|
||||
var segment = new List<IrInstruction>();
|
||||
var rangeCounter = nextRange;
|
||||
|
||||
void Flush()
|
||||
{
|
||||
if (segment.Count == 0) return;
|
||||
LowerSegment(segment, result, ref rangeCounter, minimumAccesses,
|
||||
scalarOnly, hardwareConstants);
|
||||
segment.Clear();
|
||||
}
|
||||
|
||||
foreach (var instruction in instructions)
|
||||
{
|
||||
if (IsOwnershipBoundary(instruction))
|
||||
{
|
||||
Flush();
|
||||
result.Add(instruction);
|
||||
}
|
||||
else
|
||||
{
|
||||
segment.Add(instruction);
|
||||
}
|
||||
}
|
||||
Flush();
|
||||
nextRange = rangeCounter;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void LowerSegment(
|
||||
IReadOnlyList<IrInstruction> segment,
|
||||
List<IrInstruction> output,
|
||||
ref int nextRange,
|
||||
int minimumAccesses,
|
||||
bool scalarOnly,
|
||||
IReadOnlyDictionary<string, uint>? hardwareConstants = null)
|
||||
{
|
||||
var accesses = BuildAccesses(segment,
|
||||
normalizeArchitecturalRegisters: true, hardwareConstants)
|
||||
.Where(access => !scalarOnly || access.Psq is null)
|
||||
.ToArray();
|
||||
var accessesByIndex = accesses.ToDictionary(static access => access.Index);
|
||||
var plans = new Dictionary<int, Plan>();
|
||||
|
||||
foreach (var group in accesses.GroupBy(access =>
|
||||
new AccessClass(access.Address.Base, access.IsStore)))
|
||||
{
|
||||
var grouped = group.OrderBy(static access => access.Index).ToArray();
|
||||
if (grouped.Length < 2) continue;
|
||||
// Resolving a range costs about as much as one checked access, so require enough
|
||||
// consumers to amortize it; a per-function cache for just two accesses measurably
|
||||
// increased stack traffic and CPU time.
|
||||
if (grouped.Length < minimumAccesses) continue;
|
||||
if (grouped.Any(access => HasOverlappingOppositeAccess(access, accesses)))
|
||||
continue;
|
||||
var min = grouped.Min(static access => (long)access.Address.Offset);
|
||||
var max = grouped.Max(static access => (long)access.Address.Offset + access.SizeBytes);
|
||||
var length = max - min;
|
||||
if (length <= 0 || length > MaxRangeLength) continue;
|
||||
|
||||
var range = $"guest_range_{nextRange++}";
|
||||
var plan = new Plan(range, (int)min, (int)length,
|
||||
grouped.Any(static access => !access.IsStore), grouped.Any(static access => access.IsStore));
|
||||
foreach (var access in grouped) plans[access.Index] = plan;
|
||||
}
|
||||
|
||||
var emitted = new HashSet<string>(StringComparer.Ordinal);
|
||||
for (var index = 0; index < segment.Count; index++)
|
||||
{
|
||||
if (!plans.TryGetValue(index, out var plan))
|
||||
{
|
||||
output.Add(segment[index]);
|
||||
continue;
|
||||
}
|
||||
|
||||
var access = accessesByIndex[index];
|
||||
if (emitted.Add(plan.Range))
|
||||
{
|
||||
// The anchor's raw base register holds root + BaseDelta at
|
||||
// this point, so shifting the canonical minimum back by the
|
||||
// delta makes the emitted expression equal root + MinOffset.
|
||||
output.Add(new IrResolveGuestMemoryRange(
|
||||
plan.Range, IrValue.Register(access.OriginalBase ?? access.Address.Base),
|
||||
checked(plan.MinOffset - access.BaseDelta),
|
||||
plan.Length, plan.NeedsRead, plan.NeedsWrite));
|
||||
}
|
||||
|
||||
output.Add(ToResolved(segment[index], access, plan));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsOwnershipBoundary(IrInstruction instruction) => instruction switch
|
||||
{
|
||||
IrCall call => IsMemoryEpochBoundary(call),
|
||||
IrIndirectCall or IrBranch or IrJump or IrIndirectJump or IrJumpTable or IrReturn or IrUndefined => true,
|
||||
_ => false
|
||||
};
|
||||
|
||||
// Catalog effects, rather than helper-name prefixes, define transparency.
|
||||
// Unknown/complete-context helpers, guest re-entry, suspension, thread
|
||||
// switching, and hidden GPR writes all end the pointer lifetime.
|
||||
private static bool IsMemoryEpochBoundary(IrInstruction instruction) => instruction switch
|
||||
{
|
||||
IrCall call => IsMemoryEpochBoundary(GuestHelperEffectCatalog.Analyze(call)),
|
||||
IrIndirectCall => true,
|
||||
_ => false
|
||||
};
|
||||
|
||||
private static bool IsMemoryEpochBoundary(GuestHelperEffect effect)
|
||||
{
|
||||
const GuestCallBoundaryFlags unsafeFlags =
|
||||
GuestCallBoundaryFlags.RequiresCompleteContext |
|
||||
GuestCallBoundaryFlags.CanSuspend |
|
||||
GuestCallBoundaryFlags.CanSwitchThreads |
|
||||
GuestCallBoundaryFlags.InvokesGuestCode;
|
||||
return effect.GprWriteMask != 0 || (effect.BoundaryFlags & unsafeFlags) != 0;
|
||||
}
|
||||
|
||||
private static bool Defines(IrInstruction instruction, string name,
|
||||
bool normalizeArchitecturalRegisters = true)
|
||||
{
|
||||
foreach (var destination in DefinedDestinations(instruction))
|
||||
{
|
||||
var matches = normalizeArchitecturalRegisters
|
||||
? NormalizeRegisterIdentity(destination).Equals(
|
||||
NormalizeRegisterIdentity(name), StringComparison.OrdinalIgnoreCase)
|
||||
: destination.Equals(name, StringComparison.OrdinalIgnoreCase);
|
||||
if (matches) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IEnumerable<Access> BuildAccesses(IReadOnlyList<IrInstruction> segment,
|
||||
bool normalizeArchitecturalRegisters = true,
|
||||
IReadOnlyDictionary<string, uint>? hardwareConstants = null)
|
||||
{
|
||||
var affine = new Dictionary<string, (string Root, int Offset)>(StringComparer.OrdinalIgnoreCase);
|
||||
// psq_stu/stwu-style pointer walks redefine the architectural base on
|
||||
// every step. Tracking the redefinition as an affine update keeps the
|
||||
// whole chain in one canonical root; an untrackable write instead
|
||||
// starts a new generation ("r10#2") so accesses through different
|
||||
// runtime values can never share a range proof.
|
||||
var generations = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
(string Root, int Offset) Lookup(string name)
|
||||
{
|
||||
if (affine.TryGetValue(name, out var known)) return known;
|
||||
var normalized = normalizeArchitecturalRegisters
|
||||
? NormalizeRegisterIdentity(name) : name;
|
||||
if (normalizeArchitecturalRegisters && IsArchitecturalRegister(name))
|
||||
{
|
||||
if (!normalized.Equals(name, StringComparison.OrdinalIgnoreCase) &&
|
||||
affine.TryGetValue(normalized, out var knownNormalized))
|
||||
return knownNormalized;
|
||||
if (generations.TryGetValue(normalized, out var generation) && generation > 0)
|
||||
return (normalized + "#" + generation, 0);
|
||||
}
|
||||
return (normalized, 0);
|
||||
}
|
||||
|
||||
void InvalidateDestination(string destination)
|
||||
{
|
||||
if (normalizeArchitecturalRegisters && IsArchitecturalRegister(destination))
|
||||
{
|
||||
var normalized = NormalizeRegisterIdentity(destination);
|
||||
generations[normalized] =
|
||||
generations.TryGetValue(normalized, out var generation) ? generation + 1 : 1;
|
||||
affine.Remove(normalized);
|
||||
return;
|
||||
}
|
||||
affine.Remove(destination);
|
||||
}
|
||||
|
||||
for (var index = 0; index < segment.Count; index++)
|
||||
{
|
||||
var instruction = segment[index];
|
||||
// A hardware-register access is deliberately not an access at all
|
||||
// as far as range proofs are concerned; the affine tracker below
|
||||
// still observes the instruction's register writes.
|
||||
var raw = IsProvenHardwareRegisterAccess(instruction, hardwareConstants)
|
||||
? null
|
||||
: Access.TryCreate(instruction, index);
|
||||
if (raw is not null)
|
||||
{
|
||||
var (root, delta) = Lookup(raw.Address.Base);
|
||||
var canonical = new IrAddress(root, checked(delta + raw.Address.Offset));
|
||||
yield return raw with
|
||||
{
|
||||
Address = canonical,
|
||||
OriginalBase = raw.Address.Base,
|
||||
BaseDelta = delta,
|
||||
};
|
||||
}
|
||||
|
||||
switch (instruction)
|
||||
{
|
||||
case IrAssign assign when assign.Value is { Kind: "register", RegisterName: { } source }:
|
||||
if (IsArchitecturalRegister(assign.Destination))
|
||||
{
|
||||
if (normalizeArchitecturalRegisters)
|
||||
affine[NormalizeRegisterIdentity(assign.Destination)] = Lookup(source);
|
||||
else affine.Remove(assign.Destination);
|
||||
}
|
||||
else affine[assign.Destination] = Lookup(source);
|
||||
break;
|
||||
case IrBinary binary when binary.Op.Equals("add", StringComparison.OrdinalIgnoreCase):
|
||||
if (TryAffineAdd(binary, Lookup, out var value) &&
|
||||
(normalizeArchitecturalRegisters || !IsArchitecturalRegister(binary.Destination)))
|
||||
{
|
||||
affine[normalizeArchitecturalRegisters &&
|
||||
IsArchitecturalRegister(binary.Destination)
|
||||
? NormalizeRegisterIdentity(binary.Destination)
|
||||
: binary.Destination] = value;
|
||||
}
|
||||
else InvalidateDestination(binary.Destination);
|
||||
break;
|
||||
default:
|
||||
foreach (var destination in DefinedDestinations(instruction))
|
||||
InvalidateDestination(destination);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryAffineAdd(IrBinary binary,
|
||||
Func<string, (string Root, int Offset)> lookup,
|
||||
out (string Root, int Offset) result)
|
||||
{
|
||||
result = default;
|
||||
IrValue register;
|
||||
long constant;
|
||||
if (binary.Left is { Kind: "register" } && binary.Right is { Kind: "const", Constant: { } right })
|
||||
{ register = binary.Left; constant = right; }
|
||||
else if (binary.Right is { Kind: "register" } && binary.Left is { Kind: "const", Constant: { } left })
|
||||
{ register = binary.Right; constant = left; }
|
||||
else return false;
|
||||
if (register.RegisterName is null || constant is < int.MinValue or > int.MaxValue) return false;
|
||||
var baseValue = lookup(register.RegisterName);
|
||||
var sum = (long)baseValue.Offset + constant;
|
||||
if (sum is < int.MinValue or > int.MaxValue) return false;
|
||||
result = (baseValue.Root, (int)sum);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Every register this instruction writes. Must delegate to
|
||||
/// <see cref="IrRegisterDataFlow.Definitions"/>, not a local node-kind switch: a prior switch missed
|
||||
/// resolved-load kinds, which shared one range root across loads and caused the minimap shadow-pane
|
||||
/// black-icon bug in CtrlRace2DMapCharacter::calcTransform.</summary>
|
||||
private static IEnumerable<string> DefinedDestinations(IrInstruction instruction) =>
|
||||
IrRegisterDataFlow.Definitions(instruction);
|
||||
|
||||
private static string NormalizeRegisterIdentity(string name)
|
||||
{
|
||||
if (!IsArchitecturalRegister(name)) return name;
|
||||
var separator = name.IndexOf('_');
|
||||
return separator < 0 ? name : name[..separator];
|
||||
}
|
||||
|
||||
private static bool IsArchitecturalRegister(string name)
|
||||
{
|
||||
var separator = name.IndexOf('_');
|
||||
var baseName = separator < 0 ? name : name[..separator];
|
||||
if (separator >= 0 && !name[(separator + 1)..].All(char.IsDigit)) return false;
|
||||
if (baseName.Length < 2) return false;
|
||||
if (baseName[0] is not ('r' or 'R' or 'f' or 'F')) return false;
|
||||
return int.TryParse(baseName.AsSpan(1), out var index) && index is >= 0 and < 32;
|
||||
}
|
||||
|
||||
// Address is the canonical (root-relative) form used for grouping.
|
||||
// OriginalBase/BaseDelta reconstruct the anchor-time expression: the raw
|
||||
// base register plus the affine delta the tracker had accumulated for it,
|
||||
// so a resolve emitted at the anchor can reference a register that is
|
||||
// itself walked forward later in the chain.
|
||||
private sealed record Access(int Index, IrAddress Address, int SizeBytes, bool IsStore, PsqAccess? Psq = null,
|
||||
string? OriginalBase = null, int BaseDelta = 0)
|
||||
{
|
||||
public static Access? TryCreate(IrInstruction instruction, int index) => instruction switch
|
||||
{
|
||||
IrLoad load => new Access(index, load.Address, load.SizeBytes, false),
|
||||
IrStore store => new Access(index, store.Address, store.SizeBytes, true),
|
||||
IrCall call when TryParsePsq(call, out var psq) && call.Arguments[0].RegisterName is { } address =>
|
||||
new Access(index, new IrAddress(address, 0), psq.W == 0 ? 8 : 4, psq.IsStore, psq),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private sealed record PsqAccess(bool IsStore, uint W, uint I, uint? KnownGqr, bool GuardKnownGqr);
|
||||
|
||||
private static bool TryParsePsq(IrCall call, out PsqAccess psq)
|
||||
{
|
||||
psq = null!;
|
||||
var isLoad = call.Target.Equals("PPC_PsqL", StringComparison.OrdinalIgnoreCase) ||
|
||||
call.Target.StartsWith("PPC_PsqLKnown_", StringComparison.OrdinalIgnoreCase) ||
|
||||
call.Target.StartsWith("PPC_PsqLKnownGuarded_", StringComparison.OrdinalIgnoreCase);
|
||||
var isStore = call.Target.Equals("PPC_PsqSt", StringComparison.OrdinalIgnoreCase) ||
|
||||
call.Target.StartsWith("PPC_PsqStKnown_", StringComparison.OrdinalIgnoreCase) ||
|
||||
call.Target.StartsWith("PPC_PsqStKnownGuarded_", StringComparison.OrdinalIgnoreCase);
|
||||
if ((!isLoad && !isStore) || call.Arguments.Count < (isLoad ? 3 : 4) ||
|
||||
call.Arguments[0] is not { Kind: "register", RegisterName: not null }) return false;
|
||||
var wArg = call.Arguments[isLoad ? 1 : 2];
|
||||
var iArg = call.Arguments[isLoad ? 2 : 3];
|
||||
if (wArg.Constant is not (0 or 1) || iArg.Constant is < 0 or > 7) return false;
|
||||
uint? known = null;
|
||||
var guardedMarker = isLoad ? "PPC_PsqLKnownGuarded_" : "PPC_PsqStKnownGuarded_";
|
||||
var knownMarker = isLoad ? "PPC_PsqLKnown_" : "PPC_PsqStKnown_";
|
||||
var guarded = call.Target.StartsWith(guardedMarker, StringComparison.OrdinalIgnoreCase);
|
||||
var marker = guarded ? guardedMarker : knownMarker;
|
||||
if (call.Target.StartsWith(marker, StringComparison.OrdinalIgnoreCase) &&
|
||||
uint.TryParse(call.Target.AsSpan(marker.Length), NumberStyles.HexNumber,
|
||||
CultureInfo.InvariantCulture, out var parsed)) known = parsed;
|
||||
psq = new PsqAccess(isStore, (uint)wArg.Constant.GetValueOrDefault(),
|
||||
(uint)iArg.Constant.GetValueOrDefault(), known, guarded);
|
||||
return true;
|
||||
}
|
||||
|
||||
private sealed record Plan(string Range, int MinOffset, int Length, bool NeedsRead, bool NeedsWrite);
|
||||
private sealed record FunctionAccess(int BlockIndex, Access Access)
|
||||
{
|
||||
public string AccessBlockLabel(IrFunction function) => function.Blocks[BlockIndex].Label;
|
||||
}
|
||||
|
||||
private sealed record DefinitionSite(int BlockIndex, int InstructionIndex);
|
||||
private sealed record AccessClass(string Base, bool IsStore)
|
||||
{
|
||||
public bool Equals(AccessClass? other) => other is not null &&
|
||||
Base.Equals(other.Base, StringComparison.OrdinalIgnoreCase) &&
|
||||
IsStore == other.IsStore;
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(
|
||||
StringComparer.OrdinalIgnoreCase.GetHashCode(Base),
|
||||
IsStore);
|
||||
}
|
||||
private sealed record InstructionSite(int BlockIndex, int InstructionIndex);
|
||||
private sealed record PlacedResolve(int AfterInstructionIndex, IrResolveGuestMemoryRange Resolve);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
public static class PpcFloatCallSemantics
|
||||
{
|
||||
public static bool IsPairedProducerTarget(string target)
|
||||
{
|
||||
// PPC_PsToScalar returns a scalar value, not a paired-single payload.
|
||||
if (target.Equals("PPC_PsToScalar", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return target.StartsWith("PPC_Ps", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_PsqL", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_LoadPairedSingle", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fres", StringComparison.OrdinalIgnoreCase);
|
||||
// Note: PPC_Frsqrte is NOT a paired-single producer - it's a double-precision instruction (opcode 63).
|
||||
}
|
||||
|
||||
public static bool IsPairedConsumerTarget(string target)
|
||||
{
|
||||
// Paired-single consumers expect their operands to already be in paired form.
|
||||
// PPC_PsqL consumes an address and returns paired output, while
|
||||
// PPC_StorePairedSingle writes raw paired payloads directly.
|
||||
if (target.Equals("PPC_PsqL", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_StorePairedSingle", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// PPC_PsFromScalar consumes a scalar input and produces paired output.
|
||||
if (target.Equals("PPC_PsFromScalar", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target.Equals("PPC_Fres", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return target.StartsWith("PPC_Ps", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_LoadPairedSingle", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_PsqSt", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool IsSinglePrecisionConsumerTarget(string target)
|
||||
{
|
||||
return target.Equals("PPC_Fadds", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fsubs", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fmuls", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fdivs", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fmadds", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fmsubs", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fnmadds", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fnmsubs", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool IsScalarFloatConsumerTarget(string target)
|
||||
{
|
||||
return target.Equals("PPC_Fctiwz", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fsqrt", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Frsqrte", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fsel", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fmadd", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fmsub", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fnmadd", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fnmsub", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fadds", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fsubs", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fmuls", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fdivs", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fmadds", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fmsubs", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fnmadds", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fnmsubs", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_Fcmp", StringComparison.OrdinalIgnoreCase) ||
|
||||
target.Equals("PPC_PsFromScalar", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Translator.Core.Representation;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
public static class RepresentationFormatter
|
||||
{
|
||||
public static string ToCxx(ValueRepresentation representation)
|
||||
{
|
||||
if (representation == ValueRepresentation.Void) return "void";
|
||||
if (representation == ValueRepresentation.Int32) return "int32_t";
|
||||
if (representation == ValueRepresentation.UInt32) return "uint32_t";
|
||||
if (representation == ValueRepresentation.Int64) return "int64_t";
|
||||
if (representation == ValueRepresentation.UInt64) return "uint64_t";
|
||||
if (representation == ValueRepresentation.Float32) return "float";
|
||||
if (representation == ValueRepresentation.Float64) return "double";
|
||||
return "uint32_t";
|
||||
}
|
||||
|
||||
public static string DefaultValue(ValueRepresentation representation)
|
||||
{
|
||||
if (representation == ValueRepresentation.Void) return string.Empty;
|
||||
if (representation == ValueRepresentation.Float32) return "0.0f";
|
||||
if (representation == ValueRepresentation.Float64) return "0.0";
|
||||
return "0";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Translator.Core.IO;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
/// <summary>
|
||||
/// Generates runtime configuration metadata for the C++ runtime harness.
|
||||
/// This includes critical architectural values like SDA base pointers that
|
||||
/// must be initialized before any translated code can execute.
|
||||
/// </summary>
|
||||
public sealed class RuntimeConfigGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a C++ header file containing runtime configuration metadata. SDA bases come from the
|
||||
/// project manifest, not from scanning the boot code: they're data pinned to the DOL's SHA-256, and
|
||||
/// guessing them from section midpoints could silently produce a wrong runtime.
|
||||
/// </summary>
|
||||
public static void GenerateConfigHeader(
|
||||
uint sda1Base,
|
||||
uint sda2Base,
|
||||
string outputPath,
|
||||
string projectName = "PowerPC DOL")
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"// AUTO-GENERATED for {projectName}");
|
||||
sb.AppendLine("// DO NOT EDIT THIS FILE MANUALLY");
|
||||
sb.AppendLine("//");
|
||||
sb.AppendLine("// The runtime loads these into r2/r13 before executing any translated code.");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("#pragma once");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("#include <cstdint>");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("namespace RuntimeConfig {");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("// PowerPC ABI Small Data Area base pointers");
|
||||
sb.AppendLine("// These MUST be loaded into r2 and r13 before executing any translated code.");
|
||||
sb.AppendLine();
|
||||
|
||||
// SDA1 (r13) - read/write small data
|
||||
sb.AppendLine($"// _SDA_BASE_ (r13): Base pointer for .sdata/.sbss sections");
|
||||
sb.AppendLine($"constexpr uint32_t SDA1_BASE = 0x{sda1Base:X8}u; // memory.sda_base");
|
||||
sb.AppendLine();
|
||||
|
||||
// SDA2 (r2) - read-only small data
|
||||
sb.AppendLine($"// _SDA2_BASE_ (r2): Base pointer for .sdata2/.sbss2 sections");
|
||||
sb.AppendLine($"constexpr uint32_t SDA2_BASE = 0x{sda2Base:X8}u; // memory.sda2_base");
|
||||
sb.AppendLine();
|
||||
|
||||
|
||||
sb.AppendLine("} // namespace RuntimeConfig");
|
||||
sb.AppendLine();
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
|
||||
FileOutput.WriteTextIfChanged(outputPath, sb.ToString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Translator.Core.CodeGen;
|
||||
|
||||
public sealed class RuntimeNativeFunctionAbiProvider : IGuestFunctionAbiProvider
|
||||
{
|
||||
private static readonly Regex StubVoidRegex = GeneratedMarkers.NativeOverrideVoidArgumentsPattern();
|
||||
|
||||
private readonly Dictionary<string, GuestFunctionAbi> _abis;
|
||||
|
||||
public RuntimeNativeFunctionAbiProvider(IReadOnlyDictionary<uint, GuestFunctionAbi> abis)
|
||||
{
|
||||
_abis = abis.ToDictionary(
|
||||
static item => $"func_{item.Key:X8}",
|
||||
static item => item.Value,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public bool TryGetGuestFunctionAbi(string target, out GuestFunctionAbi abi)
|
||||
{
|
||||
if (GuestTargetParser.TryParseAddress(target, out var address))
|
||||
{
|
||||
return _abis.TryGetValue($"func_{address:X8}", out abi!);
|
||||
}
|
||||
|
||||
return _abis.TryGetValue(target, out abi!);
|
||||
}
|
||||
|
||||
public static RuntimeNativeFunctionAbiProvider LoadVoidStubAbisFromDirectory(string directory)
|
||||
{
|
||||
return new RuntimeNativeFunctionAbiProvider(AnalyzeVoidStubAbisFromDirectory(directory));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the declared-ABI provider, trusting only void stubs whose addresses are in
|
||||
/// <paramref name="includeAddresses"/> (the project's <c>native_abi_directories</c>); stub
|
||||
/// signatures outside that set are documentation, not ABI contracts.
|
||||
/// </summary>
|
||||
public static RuntimeNativeFunctionAbiProvider FromIndex(
|
||||
RuntimeNativeIndex index, IReadOnlySet<uint> includeAddresses)
|
||||
{
|
||||
var abis = index.VoidStubAbis
|
||||
.Where(entry => includeAddresses.Contains(entry.Address))
|
||||
.ToDictionary(
|
||||
static entry => entry.Address,
|
||||
static entry => new GuestFunctionAbi(
|
||||
argumentRegisters: entry.ArgumentRegisters,
|
||||
preservesVolatileContext: true,
|
||||
writesGprReturnRegister: false,
|
||||
writesFloatReturnRegister: false,
|
||||
scalarFloatArgumentRegisters: entry.ScalarFloatArgumentRegisters));
|
||||
return new RuntimeNativeFunctionAbiProvider(abis);
|
||||
}
|
||||
|
||||
public static IReadOnlyDictionary<uint, GuestFunctionAbi> AnalyzeVoidStubAbisFromDirectory(string directory)
|
||||
=> AnalyzeVoidStubAbis(NativeSourceParsing.ReadDirectory(directory));
|
||||
|
||||
internal static IReadOnlyDictionary<uint, GuestFunctionAbi> AnalyzeVoidStubAbis(
|
||||
IReadOnlyList<NativeSourceFile> sources)
|
||||
{
|
||||
var abis = new Dictionary<uint, GuestFunctionAbi>();
|
||||
|
||||
foreach (var sourceFile in sources.Where(static source =>
|
||||
source.FullPath.EndsWith(".cpp", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
var content = sourceFile.Content;
|
||||
foreach (Match match in StubVoidRegex.Matches(content))
|
||||
{
|
||||
var args = match.Groups["args"].Value;
|
||||
if (args.Contains("CpuContext", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!GuestTargetParser.TryParseHexAddress(match.Groups["addr"].Value, out var address))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var argumentRegisters = InferArgumentRegisters(args);
|
||||
abis[address] = new GuestFunctionAbi(
|
||||
argumentRegisters: argumentRegisters,
|
||||
preservesVolatileContext: true,
|
||||
writesGprReturnRegister: false,
|
||||
writesFloatReturnRegister: false,
|
||||
scalarFloatArgumentRegisters: argumentRegisters.Where(static r => r.StartsWith("f", StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
}
|
||||
|
||||
return abis;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> InferArgumentRegisters(string args)
|
||||
{
|
||||
var result = new List<string>();
|
||||
var gpr = 3;
|
||||
var fpr = 1;
|
||||
foreach (var argument in NativeSourceParsing.SplitArguments(args))
|
||||
{
|
||||
var trimmed = argument.Trim();
|
||||
if (trimmed.Length == 0 || string.Equals(trimmed, "void", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (NativeSourceParsing.IsFloatingPointValueArgument(trimmed))
|
||||
{
|
||||
if (fpr <= 13)
|
||||
{
|
||||
result.Add($"f{fpr}");
|
||||
}
|
||||
fpr++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (gpr <= 10)
|
||||
{
|
||||
result.Add($"r{gpr}");
|
||||
}
|
||||
gpr++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Translator.Core.Loading;
|
||||
|
||||
namespace Translator.Core.Disassembly;
|
||||
|
||||
internal static class JumpTableDetector
|
||||
{
|
||||
private const int MaxBacktrackInstructions = 192;
|
||||
private const int MaxEntryCount = 512;
|
||||
|
||||
public static bool TryRecognize(
|
||||
IReadOnlyList<PpcInstruction> ordered,
|
||||
IReadOnlyDictionary<uint, int> indexByAddress,
|
||||
uint bctrAddress,
|
||||
uint functionStart,
|
||||
uint functionEnd,
|
||||
ProgramImage image,
|
||||
out IReadOnlyList<uint> targets)
|
||||
{
|
||||
targets = Array.Empty<uint>();
|
||||
if (!indexByAddress.TryGetValue(bctrAddress, out var bctrIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var mtctrIndex = FindPrevious(ordered, bctrIndex, "mtctr", limit: 6);
|
||||
if (mtctrIndex < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ordered[mtctrIndex].Operands.Count == 0 || ordered[mtctrIndex].Operands[0] is not PpcRegisterOperand ctrOperand)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var targetRegister = ctrOperand.Name;
|
||||
|
||||
var loadIndex = FindPreviousLoad(ordered, mtctrIndex, targetRegister);
|
||||
if (loadIndex < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var load = ordered[loadIndex];
|
||||
if (!string.Equals(load.Mnemonic, "lwzx", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false; // Only handle lwzx-based tables for now.
|
||||
}
|
||||
|
||||
if (load.Operands.Count < 3 || load.Operands[0] is not PpcRegisterOperand dest ||
|
||||
load.Operands[1] is not PpcRegisterOperand baseReg ||
|
||||
load.Operands[2] is not PpcRegisterOperand indexReg)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(dest.Name, targetRegister, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var entriesAreRelative = false;
|
||||
if (!TryResolveLisAddiConstant(ordered, loadIndex, baseReg.Name, out var tableBase) &&
|
||||
!TryResolvePcRelativeLoadedConstant(ordered, loadIndex, baseReg.Name, image, out tableBase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
entriesAreRelative = HasRelativeEntryAdd(ordered, loadIndex, mtctrIndex, targetRegister, baseReg.Name);
|
||||
|
||||
if (!TryFindUpperBound(ordered, loadIndex, indexReg.Name, out var upperBound) || upperBound < 0 || upperBound >= MaxEntryCount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var entryCount = upperBound + 1;
|
||||
var dedupList = new List<uint>();
|
||||
var seen = new HashSet<uint>();
|
||||
for (var i = 0; i < entryCount; i++)
|
||||
{
|
||||
var entryAddress = unchecked(tableBase + (uint)(i * 4));
|
||||
if (!TryReadWord(image, entryAddress, out var entry))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var target = entriesAreRelative ? unchecked(tableBase + entry) : entry;
|
||||
|
||||
if (!indexByAddress.ContainsKey(target))
|
||||
{
|
||||
// Allow targets that haven't been decoded yet as long as they
|
||||
// fall within the current function's decoding window.
|
||||
if (target < functionStart || target >= functionEnd || (target & 3) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (seen.Add(target))
|
||||
{
|
||||
dedupList.Add(target);
|
||||
}
|
||||
}
|
||||
|
||||
if (dedupList.Count == 0)
|
||||
{
|
||||
return false; // Not a real switch.
|
||||
}
|
||||
|
||||
targets = dedupList;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int FindPrevious(IReadOnlyList<PpcInstruction> ordered, int startIndex, string mnemonic, int limit)
|
||||
{
|
||||
for (var i = startIndex - 1; i >= 0 && startIndex - i <= limit; i--)
|
||||
{
|
||||
// Comparison already ignores case, so lowering the needle only
|
||||
// allocated a copy of it.
|
||||
if (string.Equals(ordered[i].Mnemonic, mnemonic, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static bool HasRelativeEntryAdd(
|
||||
IReadOnlyList<PpcInstruction> ordered,
|
||||
int loadIndex,
|
||||
int mtctrIndex,
|
||||
string targetRegister,
|
||||
string tableBaseRegister)
|
||||
{
|
||||
for (var i = loadIndex + 1; i < mtctrIndex; i++)
|
||||
{
|
||||
var ins = ordered[i];
|
||||
if (!string.Equals(ins.Mnemonic, "add", StringComparison.OrdinalIgnoreCase) ||
|
||||
ins.Operands.Count < 3 ||
|
||||
ins.Operands[0] is not PpcRegisterOperand dest ||
|
||||
ins.Operands[1] is not PpcRegisterOperand left ||
|
||||
ins.Operands[2] is not PpcRegisterOperand right ||
|
||||
!RegistersEqual(dest.Name, targetRegister))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((RegistersEqual(left.Name, targetRegister) && RegistersEqual(right.Name, tableBaseRegister)) ||
|
||||
(RegistersEqual(right.Name, targetRegister) && RegistersEqual(left.Name, tableBaseRegister)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int FindPreviousLoad(IReadOnlyList<PpcInstruction> ordered, int startIndex, string register)
|
||||
{
|
||||
for (var i = startIndex - 1; i >= 0 && startIndex - i <= MaxBacktrackInstructions; i--)
|
||||
{
|
||||
var ins = ordered[i];
|
||||
if (ins.Operands.Count == 0 || ins.Operands[0] is not PpcRegisterOperand dest)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.Equals(dest.Name, register, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Decoder mnemonics are lowercase by construction.
|
||||
if (ins.Mnemonic is "lwzx" or "lwz")
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static bool TryResolveLisAddiConstant(IReadOnlyList<PpcInstruction> ordered, int startIndex, string register, out uint value)
|
||||
{
|
||||
var haveOffset = false;
|
||||
int offset = 0;
|
||||
var baseRegister = register;
|
||||
for (var i = startIndex - 1; i >= 0 && startIndex - i <= MaxBacktrackInstructions; i--)
|
||||
{
|
||||
var ins = ordered[i];
|
||||
if (ins.Operands.Count == 0 || ins.Operands[0] is not PpcRegisterOperand dest)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.Equals(dest.Name, baseRegister, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var mnemonic = ins.Mnemonic;
|
||||
if (!haveOffset && mnemonic == "addi" && ins.Operands.Count >= 3 && ins.Operands[1] is PpcRegisterOperand addBase &&
|
||||
ins.Operands[2] is PpcImmediateOperand addImm)
|
||||
{
|
||||
offset = (short)addImm.Value;
|
||||
haveOffset = true;
|
||||
// Track the source register so we can resolve lis/addi from a different base.
|
||||
baseRegister = addBase.Name;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mnemonic == "lis" && ins.Operands.Count >= 2 && ins.Operands[1] is PpcImmediateOperand hiImm)
|
||||
{
|
||||
var hi = hiImm.Value << 16;
|
||||
var low = haveOffset ? offset : 0;
|
||||
value = unchecked((uint)(hi + low));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryResolvePcRelativeLoadedConstant(
|
||||
IReadOnlyList<PpcInstruction> ordered,
|
||||
int startIndex,
|
||||
string register,
|
||||
ProgramImage image,
|
||||
out uint value)
|
||||
{
|
||||
for (var i = startIndex - 1; i >= 0 && startIndex - i <= MaxBacktrackInstructions; i--)
|
||||
{
|
||||
var ins = ordered[i];
|
||||
if (!string.Equals(ins.Mnemonic, "lwz", StringComparison.OrdinalIgnoreCase) ||
|
||||
ins.Operands.Count < 2 ||
|
||||
ins.Operands[0] is not PpcRegisterOperand dest ||
|
||||
ins.Operands[1] is not PpcDisplacementOperand displacement ||
|
||||
!RegistersEqual(dest.Name, register))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryResolvePicBaseRegister(ordered, i, displacement.BaseRegister, image, out var picBase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var literalAddress = AddSigned(picBase, displacement.Offset);
|
||||
if (TryReadWord(image, literalAddress, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryResolvePicBaseRegister(
|
||||
IReadOnlyList<PpcInstruction> ordered,
|
||||
int startIndex,
|
||||
string register,
|
||||
ProgramImage image,
|
||||
out uint value)
|
||||
{
|
||||
for (var addIndex = startIndex - 1; addIndex >= 0 && startIndex - addIndex <= MaxBacktrackInstructions; addIndex--)
|
||||
{
|
||||
var add = ordered[addIndex];
|
||||
if (!string.Equals(add.Mnemonic, "add", StringComparison.OrdinalIgnoreCase) ||
|
||||
add.Operands.Count < 3 ||
|
||||
add.Operands[0] is not PpcRegisterOperand dest ||
|
||||
add.Operands[1] is not PpcRegisterOperand left ||
|
||||
add.Operands[2] is not PpcRegisterOperand right ||
|
||||
!RegistersEqual(dest.Name, register))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string offsetRegister;
|
||||
if (RegistersEqual(left.Name, register))
|
||||
{
|
||||
offsetRegister = right.Name;
|
||||
}
|
||||
else if (RegistersEqual(right.Name, register))
|
||||
{
|
||||
offsetRegister = left.Name;
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryFindRegisterDisplacementLoadBefore(ordered, addIndex, offsetRegister, register, out var offsetLoad) ||
|
||||
!TryFindMflrBefore(ordered, offsetLoad.Index, register, out var mflrIndex) ||
|
||||
!TryFindLinkCallBefore(ordered, mflrIndex, out var linkAddress))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var offsetAddress = AddSigned(linkAddress, offsetLoad.Displacement);
|
||||
if (!TryReadWord(image, offsetAddress, out var picOffset))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
value = unchecked(linkAddress + picOffset);
|
||||
return true;
|
||||
}
|
||||
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryFindRegisterDisplacementLoadBefore(
|
||||
IReadOnlyList<PpcInstruction> ordered,
|
||||
int startIndex,
|
||||
string destinationRegister,
|
||||
string baseRegister,
|
||||
out (int Index, int Displacement) result)
|
||||
{
|
||||
for (var i = startIndex - 1; i >= 0 && startIndex - i <= MaxBacktrackInstructions; i--)
|
||||
{
|
||||
var ins = ordered[i];
|
||||
if (string.Equals(ins.Mnemonic, "lwz", StringComparison.OrdinalIgnoreCase) &&
|
||||
ins.Operands.Count >= 2 &&
|
||||
ins.Operands[0] is PpcRegisterOperand dest &&
|
||||
ins.Operands[1] is PpcDisplacementOperand displacement &&
|
||||
RegistersEqual(dest.Name, destinationRegister) &&
|
||||
RegistersEqual(displacement.BaseRegister, baseRegister))
|
||||
{
|
||||
result = (i, displacement.Offset);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryFindMflrBefore(
|
||||
IReadOnlyList<PpcInstruction> ordered,
|
||||
int startIndex,
|
||||
string destinationRegister,
|
||||
out int mflrIndex)
|
||||
{
|
||||
for (var i = startIndex - 1; i >= 0 && startIndex - i <= MaxBacktrackInstructions; i--)
|
||||
{
|
||||
var ins = ordered[i];
|
||||
if (string.Equals(ins.Mnemonic, "mflr", StringComparison.OrdinalIgnoreCase) &&
|
||||
ins.Operands.Count >= 1 &&
|
||||
ins.Operands[0] is PpcRegisterOperand dest &&
|
||||
RegistersEqual(dest.Name, destinationRegister))
|
||||
{
|
||||
mflrIndex = i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
mflrIndex = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryFindLinkCallBefore(
|
||||
IReadOnlyList<PpcInstruction> ordered,
|
||||
int startIndex,
|
||||
out uint linkAddress)
|
||||
{
|
||||
for (var i = startIndex - 1; i >= 0 && startIndex - i <= MaxBacktrackInstructions; i--)
|
||||
{
|
||||
var ins = ordered[i];
|
||||
if (ins.IsCall)
|
||||
{
|
||||
linkAddress = ins.EndAddress;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
linkAddress = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryFindUpperBound(IReadOnlyList<PpcInstruction> ordered, int startIndex, string register, out int upperBound)
|
||||
{
|
||||
// Preferred: compare directly on the same register used for lwzx indexing.
|
||||
for (var i = startIndex - 1; i >= 0 && startIndex - i <= MaxBacktrackInstructions; i--)
|
||||
{
|
||||
var ins = ordered[i];
|
||||
if (ins.Operands.Count < 2 || ins.Operands[0] is not PpcRegisterOperand reg ||
|
||||
!string.Equals(reg.Name, register, StringComparison.OrdinalIgnoreCase) ||
|
||||
ins.Operands[1] is not PpcImmediateOperand imm)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ins.Mnemonic is "cmplwi" or "cmpwi")
|
||||
{
|
||||
upperBound = imm.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: some loops compare a logical counter (e.g. r18) distinct from the byte-offset
|
||||
// register (e.g. r30) that feeds lwzx, so the bound check may not target the index register.
|
||||
for (var i = startIndex - 1; i >= 0 && startIndex - i <= MaxBacktrackInstructions; i--)
|
||||
{
|
||||
var ins = ordered[i];
|
||||
if (ins.Operands.Count < 2 || ins.Operands[1] is not PpcImmediateOperand imm)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ins.Mnemonic is not ("cmplwi" or "cmpwi"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (imm.Value < 0 || imm.Value >= MaxEntryCount)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
upperBound = imm.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
upperBound = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryReadWord(ProgramImage image, uint address, out uint value)
|
||||
{
|
||||
if (!image.Contains(address, sizeof(uint)))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
var span = image.Memory.AsSpan(image.GetOffset(address, sizeof(uint)), sizeof(uint));
|
||||
value = BinaryPrimitives.ReadUInt32BigEndian(span);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool RegistersEqual(string left, string right) =>
|
||||
string.Equals(left, right, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static uint AddSigned(uint value, int offset) =>
|
||||
unchecked(value + (uint)offset);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace Translator.Core.Disassembly;
|
||||
|
||||
public static partial class PpcDecoder
|
||||
{
|
||||
private static int GetField(uint word, int startBit, int width = 5)
|
||||
{
|
||||
var mask = (1 << width) - 1;
|
||||
return (int)((word >> startBit) & (uint)mask);
|
||||
}
|
||||
|
||||
private static int SignExtend(uint value, int bits)
|
||||
{
|
||||
var shift = 32 - bits;
|
||||
return (int)(value << shift) >> shift;
|
||||
}
|
||||
|
||||
private static string? TryDecodeCrBranchMnemonic(int bo, int bi)
|
||||
{
|
||||
// Mask off the hint bits: BO=001zy (4/5) branches when the CR bit is false, BO=011zy (12/13) when true.
|
||||
var baseBo = bo & 0x1E;
|
||||
if (baseBo is not (4 or 12))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var crBit = bi % 4;
|
||||
var table = baseBo == 12 ? TrueCrBranchMnemonics : FalseCrBranchMnemonics;
|
||||
|
||||
return table[crBit];
|
||||
}
|
||||
|
||||
private static readonly string[] TrueCrBranchMnemonics = { "blt", "bgt", "beq", "bso" };
|
||||
|
||||
private static readonly string[] FalseCrBranchMnemonics = { "bge", "ble", "bne", "bns" };
|
||||
|
||||
// Operands are immutable records compared by value, so caching one shared instance per
|
||||
// register number avoids an allocation on every decoded operand without changing behavior.
|
||||
private const int RegisterFileSize = 32;
|
||||
|
||||
private static readonly string[] GprNames = BuildRegisterNames("r");
|
||||
|
||||
private static readonly string[] FprNames = BuildRegisterNames("f");
|
||||
|
||||
private static readonly PpcRegisterOperand[] GprOperands = BuildRegisterOperands(GprNames);
|
||||
|
||||
private static readonly PpcRegisterOperand[] FprOperands = BuildRegisterOperands(FprNames);
|
||||
|
||||
private static readonly PpcConditionRegisterOperand[] CrFieldOperands = BuildCrFieldOperands();
|
||||
|
||||
private static readonly PpcConditionRegisterOperand[] CrBitOperands = BuildCrBitOperands();
|
||||
|
||||
private static string[] BuildRegisterNames(string prefix)
|
||||
{
|
||||
var names = new string[RegisterFileSize];
|
||||
for (var i = 0; i < names.Length; i++)
|
||||
{
|
||||
names[i] = prefix + i.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
private static PpcRegisterOperand[] BuildRegisterOperands(string[] names)
|
||||
{
|
||||
var operands = new PpcRegisterOperand[names.Length];
|
||||
for (var i = 0; i < operands.Length; i++)
|
||||
{
|
||||
operands[i] = new PpcRegisterOperand(names[i], i);
|
||||
}
|
||||
|
||||
return operands;
|
||||
}
|
||||
|
||||
private static PpcConditionRegisterOperand[] BuildCrFieldOperands()
|
||||
{
|
||||
var operands = new PpcConditionRegisterOperand[8];
|
||||
for (var i = 0; i < operands.Length; i++)
|
||||
{
|
||||
operands[i] = new PpcConditionRegisterOperand($"cr{i}", i * 4);
|
||||
}
|
||||
|
||||
return operands;
|
||||
}
|
||||
|
||||
private static PpcConditionRegisterOperand[] BuildCrBitOperands()
|
||||
{
|
||||
var operands = new PpcConditionRegisterOperand[RegisterFileSize];
|
||||
for (var i = 0; i < operands.Length; i++)
|
||||
{
|
||||
operands[i] = new PpcConditionRegisterOperand($"crb{i}", i);
|
||||
}
|
||||
|
||||
return operands;
|
||||
}
|
||||
|
||||
private static PpcRegisterOperand Reg(int number) =>
|
||||
(uint)number < (uint)GprOperands.Length
|
||||
? GprOperands[number]
|
||||
: new PpcRegisterOperand($"r{number}", number);
|
||||
|
||||
private static string RegName(int number) =>
|
||||
(uint)number < (uint)GprNames.Length ? GprNames[number] : $"r{number}";
|
||||
|
||||
private static PpcRegisterOperand FReg(int number) =>
|
||||
(uint)number < (uint)FprOperands.Length
|
||||
? FprOperands[number]
|
||||
: new PpcRegisterOperand($"f{number}", number);
|
||||
|
||||
private static PpcConditionRegisterOperand CrField(int field) =>
|
||||
(uint)field < (uint)CrFieldOperands.Length
|
||||
? CrFieldOperands[field]
|
||||
: new PpcConditionRegisterOperand($"cr{field}", field * 4);
|
||||
|
||||
private static PpcConditionRegisterOperand CrBit(int bit) =>
|
||||
(uint)bit < (uint)CrBitOperands.Length
|
||||
? CrBitOperands[bit]
|
||||
: new PpcConditionRegisterOperand($"crb{bit}", bit);
|
||||
|
||||
private static int DecodeSpr(uint word)
|
||||
{
|
||||
// SPR is split across bits 11-15 (high) and 16-20 (low)
|
||||
return ((int)(word >> 16) & 0x1F) | (((int)word >> 6) & 0x3E0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace Translator.Core.Disassembly;
|
||||
|
||||
public static partial class PpcDecoder
|
||||
{
|
||||
private static void DecodePrimary19(uint address, uint word, List<PpcOperand> operands, List<uint> branches,
|
||||
ref string mnemonic, ref bool isReturn, ref bool isCall, ref bool isCond)
|
||||
{
|
||||
var field0 = GetField(word, 21, 5);
|
||||
var field1 = GetField(word, 16, 5);
|
||||
var field2 = GetField(word, 11, 5);
|
||||
var xo = GetField(word, 1, 10);
|
||||
var lk = (word & 1) != 0;
|
||||
var bo = field0;
|
||||
var bi = field1;
|
||||
|
||||
if (xo == 16) // bclr
|
||||
{
|
||||
var isAlways = bo == 20; // BI is ignored when BO says "always"
|
||||
var crField = bi / 4;
|
||||
if (isAlways)
|
||||
{
|
||||
mnemonic = lk ? "blrl" : "blr";
|
||||
}
|
||||
else if (lk)
|
||||
{
|
||||
// Conditional with link - use extended mnemonics with 'l' suffix if possible
|
||||
var crBit = bi % 4;
|
||||
mnemonic = (bo, crBit) switch
|
||||
{
|
||||
(12, 0) => "bltlrl",
|
||||
(12, 1) => "bgtlrl",
|
||||
(12, 2) => "beqlrl",
|
||||
(12, 3) => "bsolrl",
|
||||
(4, 0) => "bgelrl",
|
||||
(4, 1) => "blelrl",
|
||||
(4, 2) => "bnelrl",
|
||||
(4, 3) => "bnslrl",
|
||||
_ => "bclrl"
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Conditional without link - extended mnemonics for common CR-based returns.
|
||||
// BO=12 branches when the CR bit is 1, BO=4 branches when it is 0.
|
||||
var crBit = bi % 4;
|
||||
mnemonic = (bo, crBit) switch
|
||||
{
|
||||
(12, 0) => "bltlr",
|
||||
(12, 1) => "bgtlr",
|
||||
(12, 2) => "beqlr",
|
||||
(12, 3) => "bsolr",
|
||||
(4, 0) => "bgelr",
|
||||
(4, 1) => "blelr",
|
||||
(4, 2) => "bnelr",
|
||||
(4, 3) => "bnslr",
|
||||
_ => "bclr"
|
||||
};
|
||||
}
|
||||
|
||||
isCall = lk;
|
||||
isReturn = !lk && mnemonic.EndsWith("lr", StringComparison.OrdinalIgnoreCase);
|
||||
isCond = !isAlways;
|
||||
if (isCond && crField != 0)
|
||||
{
|
||||
operands.Add(CrField(crField));
|
||||
}
|
||||
}
|
||||
else if (xo == 528) // bcctr
|
||||
{
|
||||
var isAlways = bo == 20; // BI is ignored when BO says "always"
|
||||
var crField = bi / 4;
|
||||
if (isAlways)
|
||||
{
|
||||
mnemonic = lk ? "bctrl" : "bctr";
|
||||
}
|
||||
else
|
||||
{
|
||||
mnemonic = lk ? "bcctrl" : "bcctr";
|
||||
}
|
||||
isCall = lk;
|
||||
// Unconditional bctr never falls through (tail/virtual dispatch, or later a switch
|
||||
// terminator via JumpTableDetector); the disassembler must not treat it as linear code.
|
||||
isReturn = !lk && isAlways;
|
||||
isCond = !isAlways;
|
||||
if (isCond && crField != 0)
|
||||
{
|
||||
operands.Add(CrField(crField));
|
||||
}
|
||||
}
|
||||
else if (xo == 50)
|
||||
{
|
||||
mnemonic = "rfi";
|
||||
isReturn = true;
|
||||
}
|
||||
else if (xo == 150)
|
||||
{
|
||||
mnemonic = "isync";
|
||||
}
|
||||
else if (xo == 0)
|
||||
{
|
||||
mnemonic = "mcrf";
|
||||
operands.Add(CrField(field0 / 4));
|
||||
operands.Add(CrField(field1 / 4));
|
||||
}
|
||||
else if (xo is 33 or 129 or 193 or 225 or 257 or 289 or 417 or 449)
|
||||
{
|
||||
mnemonic = xo switch
|
||||
{
|
||||
33 => "crnor",
|
||||
129 => "crandc",
|
||||
193 => "crxor",
|
||||
225 => "crnand",
|
||||
257 => "crand",
|
||||
289 => "creqv",
|
||||
417 => "crorc",
|
||||
449 => "cror",
|
||||
_ => "unk19"
|
||||
};
|
||||
|
||||
operands.Add(CrBit(field0));
|
||||
operands.Add(CrBit(field1));
|
||||
operands.Add(CrBit(field2));
|
||||
}
|
||||
else
|
||||
{
|
||||
mnemonic = "unk19";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,698 @@
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace Translator.Core.Disassembly;
|
||||
|
||||
public static partial class PpcDecoder
|
||||
{
|
||||
private static void DecodePrimary31(uint address, uint word, List<PpcOperand> operands, List<uint> branches,
|
||||
ref string mnemonic, ref bool isReturn, ref bool isCall, ref bool isCond)
|
||||
{
|
||||
// For XO-form arithmetic instructions, bit 21 (our bit 10) is the OE bit.
|
||||
// We extract both the full 10-bit XO and the 9-bit XO with separate OE.
|
||||
var xo10 = GetField(word, 1, 10); // Full 10-bit XO for X-form instructions
|
||||
var xo = GetField(word, 1, 9); // 9-bit XO for XO-form instructions
|
||||
var oe = ((word >> 10) & 1) != 0; // OE bit (bit 21)
|
||||
var rc = (word & 1) != 0;
|
||||
var rs = GetField(word, 21);
|
||||
var ra = GetField(word, 16);
|
||||
var rb = GetField(word, 11);
|
||||
|
||||
// Helper for arithmetic mnemonics with OE and Rc support
|
||||
string ArithMnemonic(string baseName) => baseName + (oe ? "o" : "") + (rc ? "." : "");
|
||||
|
||||
switch (xo)
|
||||
{
|
||||
case 0: // cmp (cmpw)
|
||||
{
|
||||
if (xo10 == 512)
|
||||
{
|
||||
mnemonic = "mcrxr";
|
||||
operands.Add(CrField(rs / 4));
|
||||
break;
|
||||
}
|
||||
|
||||
var crfD = GetField(word, 23, 3);
|
||||
var lBit = GetField(word, 21, 1);
|
||||
if (lBit != 0)
|
||||
mnemonic = "invalid_cmp";
|
||||
else
|
||||
mnemonic = "cmpw";
|
||||
|
||||
operands.Add(CrField(crfD));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
isCond = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case 4: // tw
|
||||
mnemonic = "tw";
|
||||
operands.Add(new PpcImmediateOperand(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 8: // subfc rD,rA,rB
|
||||
mnemonic = ArithMnemonic("subfc");
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 10: // addc rD,rA,rB
|
||||
mnemonic = ArithMnemonic("addc");
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 19: // mfcr rD
|
||||
mnemonic = "mfcr";
|
||||
operands.Add(Reg(rs));
|
||||
break;
|
||||
|
||||
case 20: // lwarx rD,rA,rB
|
||||
mnemonic = "lwarx";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
case 28: // and rA,rS,rB
|
||||
mnemonic = rc ? "and." : "and";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 26: // cntlzw rA,rS
|
||||
mnemonic = rc ? "cntlzw." : "cntlzw";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rs));
|
||||
break;
|
||||
|
||||
case 32: // cmplw
|
||||
{
|
||||
var crfD = GetField(word, 23, 3);
|
||||
mnemonic = "cmplw";
|
||||
if (crfD != 0)
|
||||
operands.Add(CrField(crfD));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
isCond = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case 60: // andc rA,rS,rB
|
||||
mnemonic = rc ? "andc." : "andc";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
|
||||
|
||||
case 83: // mfmsr rD
|
||||
if (xo10 == 595)
|
||||
{
|
||||
mnemonic = "mfsr";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(new PpcImmediateOperand(ra & 0xF));
|
||||
}
|
||||
else
|
||||
{
|
||||
mnemonic = "mfmsr";
|
||||
operands.Add(Reg(rs));
|
||||
}
|
||||
break;
|
||||
|
||||
case 146: // mtmsr rS
|
||||
mnemonic = "mtmsr";
|
||||
operands.Add(Reg(rs));
|
||||
break;
|
||||
|
||||
case 144: // mtcrf CRM,rS
|
||||
mnemonic = "mtcrf";
|
||||
operands.Add(new PpcImmediateOperand((int)((word >> 12) & 0xFF)));
|
||||
operands.Add(Reg(rs));
|
||||
break;
|
||||
|
||||
|
||||
|
||||
case 124: // nor rA,rS,rB
|
||||
mnemonic = rc ? "nor." : "nor";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 476: // nand rA,rS,rB
|
||||
mnemonic = rc ? "nand." : "nand";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 104: // neg rD,rA
|
||||
mnemonic = ArithMnemonic("neg");
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
break;
|
||||
|
||||
case 136: // subfe rD,rA,rB
|
||||
mnemonic = ArithMnemonic("subfe");
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
|
||||
|
||||
case 40: // subf rD,rA,rB
|
||||
mnemonic = ArithMnemonic("subf");
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 232: // subfme rD,rA
|
||||
mnemonic = ArithMnemonic("subfme");
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
break;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
case 202: // addze rD,rA
|
||||
mnemonic = ArithMnemonic("addze");
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
break;
|
||||
|
||||
case 234: // addme rD,rA
|
||||
mnemonic = ArithMnemonic("addme");
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
break;
|
||||
|
||||
case 235: // mullw rD,rA,rB
|
||||
mnemonic = ArithMnemonic("mullw");
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 266: // add rD,rA,rB
|
||||
mnemonic = ArithMnemonic("add");
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
|
||||
|
||||
case 316: // xor rA,rS,rB
|
||||
mnemonic = rc ? "xor." : "xor";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 284: // eqv rA,rS,rB
|
||||
mnemonic = rc ? "eqv." : "eqv";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 339: // mfspr rD,SPR
|
||||
var spr = DecodeSpr(word);
|
||||
mnemonic = spr switch
|
||||
{
|
||||
8 => "mflr",
|
||||
9 => "mfctr",
|
||||
1 => "mfxer",
|
||||
_ => "mfspr"
|
||||
};
|
||||
operands.Add(Reg(rs));
|
||||
if (mnemonic == "mfspr")
|
||||
{
|
||||
operands.Add(new PpcImmediateOperand(spr));
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
case 412: // orc rA,rS,rB
|
||||
mnemonic = rc ? "orc." : "orc";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
|
||||
|
||||
case 444: // or / mr alias
|
||||
mnemonic = (!rc && rs == rb) ? "mr" : (rc ? "or." : "or");
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 11: // mulhwu rD,rA,rB
|
||||
mnemonic = rc ? "mulhwu." : "mulhwu";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 75: // mulhw rD,rA,rB (signed multiply high word)
|
||||
mnemonic = rc ? "mulhw." : "mulhw";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 459: // divwu rD,rA,rB
|
||||
mnemonic = ArithMnemonic("divwu");
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 467: // mtspr SPR,rS
|
||||
spr = DecodeSpr(word);
|
||||
mnemonic = spr switch
|
||||
{
|
||||
8 => "mtlr",
|
||||
9 => "mtctr",
|
||||
1 => "mtxer",
|
||||
_ => "mtspr"
|
||||
};
|
||||
if (mnemonic == "mtspr")
|
||||
{
|
||||
operands.Add(new PpcImmediateOperand(spr));
|
||||
operands.Add(Reg(rs));
|
||||
}
|
||||
else
|
||||
{
|
||||
operands.Add(Reg(rs));
|
||||
}
|
||||
break;
|
||||
|
||||
case 210: // mtsr SR,rS
|
||||
mnemonic = "mtsr";
|
||||
operands.Add(new PpcImmediateOperand(ra & 0xF));
|
||||
operands.Add(Reg(rs));
|
||||
break;
|
||||
|
||||
case 242: // mtsrin rS,rB
|
||||
mnemonic = "mtsrin";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 491: // divw rD,rA,rB
|
||||
mnemonic = ArithMnemonic("divw");
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 498: // stwcx. rS,rA,rB
|
||||
mnemonic = "stwcx.";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
isCond = true;
|
||||
break;
|
||||
|
||||
|
||||
|
||||
default:
|
||||
// X-form instructions use the full 10-bit XO (no OE bit)
|
||||
// Handle them here with a secondary switch on xo10
|
||||
switch (xo10)
|
||||
{
|
||||
case 23: // lwzx rD,rA,rB
|
||||
mnemonic = "lwzx";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 24: // slw rA,rS,rB
|
||||
mnemonic = rc ? "slw." : "slw";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 54: // dcbst rA,rB
|
||||
mnemonic = "dcbst";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 86: // dcbf rA,rB
|
||||
mnemonic = "dcbf";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 55: // lwzux rD,rA,rB
|
||||
mnemonic = "lwzux";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 87: // lbzx rD,rA,rB
|
||||
mnemonic = "lbzx";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 151: // stwx rS,rA,rB
|
||||
mnemonic = "stwx";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 183: // stwux rS,rA,rB
|
||||
mnemonic = "stwux";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 246: // dcbtst rA,rB
|
||||
mnemonic = "dcbtst";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 215: // stbx rS,rA,rB
|
||||
mnemonic = "stbx";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 247: // stbux rS,rA,rB
|
||||
mnemonic = "stbux";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 278: // dcbt rA,rB
|
||||
mnemonic = "dcbt";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 310: // eciwx rD,rA,rB
|
||||
mnemonic = "eciwx";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 306: // tlbie rB
|
||||
mnemonic = "tlbie";
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 279: // lhzx rD,rA,rB
|
||||
mnemonic = "lhzx";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 311: // lhzux rD,rA,rB
|
||||
mnemonic = "lhzux";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 343: // lhax rD,rA,rB
|
||||
mnemonic = "lhax";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 375: // lhaux rD,rA,rB
|
||||
mnemonic = "lhaux";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 407: // sthx rS,rA,rB
|
||||
mnemonic = "sthx";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 438: // ecowx rS,rA,rB
|
||||
mnemonic = "ecowx";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 439: // sthux rS,rA,rB
|
||||
mnemonic = "sthux";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 470: // dcbi rA,rB
|
||||
mnemonic = "dcbi";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 536: // srw rA,rS,rB
|
||||
mnemonic = rc ? "srw." : "srw";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 598: // sync
|
||||
mnemonic = "sync";
|
||||
break;
|
||||
|
||||
case 566: // tlbsync
|
||||
mnemonic = "tlbsync";
|
||||
break;
|
||||
|
||||
case 533: // lswx rD,rA,rB
|
||||
mnemonic = "lswx";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 597: // lswi rD,rA,NB
|
||||
mnemonic = "lswi";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(new PpcImmediateOperand((int)rb));
|
||||
break;
|
||||
|
||||
case 792: // sraw rA,rS,rB
|
||||
mnemonic = rc ? "sraw." : "sraw";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 824: // srawi rA,rS,SH
|
||||
var sh5 = GetField(word, 11, 5);
|
||||
mnemonic = rc ? "srawi." : "srawi";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(new PpcImmediateOperand(sh5));
|
||||
break;
|
||||
|
||||
case 535: // lfsx frD,rA,rB
|
||||
mnemonic = "lfsx";
|
||||
operands.Add(FReg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 567: // lfsux frD,rA,rB
|
||||
mnemonic = "lfsux";
|
||||
operands.Add(FReg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 599: // lfdx frD,rA,rB
|
||||
mnemonic = "lfdx";
|
||||
operands.Add(FReg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 631: // lfdux frD,rA,rB
|
||||
mnemonic = "lfdux";
|
||||
operands.Add(FReg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 663: // stfsx frS,rA,rB
|
||||
mnemonic = "stfsx";
|
||||
operands.Add(FReg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 661: // stswx rS,rA,rB
|
||||
mnemonic = "stswx";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 695: // stfsux frS,rA,rB
|
||||
mnemonic = "stfsux";
|
||||
operands.Add(FReg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 725: // stswi rS,rA,NB
|
||||
mnemonic = "stswi";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(new PpcImmediateOperand((int)rb));
|
||||
break;
|
||||
|
||||
case 727: // stfdx frS,rA,rB
|
||||
mnemonic = "stfdx";
|
||||
operands.Add(FReg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 758: // dcba rA,rB
|
||||
mnemonic = "dcba";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 759: // stfdux frS,rA,rB
|
||||
mnemonic = "stfdux";
|
||||
operands.Add(FReg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 854: // eieio
|
||||
mnemonic = "eieio";
|
||||
break;
|
||||
|
||||
case 371: // mftb rD,TBR
|
||||
var tbr = DecodeSpr(word);
|
||||
mnemonic = tbr == 269 ? "mftbu" : "mftb";
|
||||
operands.Add(Reg(rs));
|
||||
if (tbr is not (268 or 269))
|
||||
{
|
||||
operands.Add(new PpcImmediateOperand(tbr));
|
||||
}
|
||||
break;
|
||||
|
||||
case 534: // lwbrx rD,rA,rB
|
||||
mnemonic = "lwbrx";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 662: // stwbrx rS,rA,rB
|
||||
mnemonic = "stwbrx";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 659: // mfsrin rD,rB
|
||||
mnemonic = "mfsrin";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 790: // lhbrx rD,rA,rB
|
||||
mnemonic = "lhbrx";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 918: // sthbrx rS,rA,rB
|
||||
mnemonic = "sthbrx";
|
||||
operands.Add(Reg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 982: // icbi rA,rB
|
||||
mnemonic = "icbi";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 983: // stfiwx frS,rA,rB
|
||||
mnemonic = "stfiwx";
|
||||
operands.Add(FReg(rs));
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 1014: // dcbz rA,rB
|
||||
mnemonic = "dcbz";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rb));
|
||||
break;
|
||||
|
||||
case 922: // extsh rA,rS
|
||||
mnemonic = rc ? "extsh." : "extsh";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rs));
|
||||
break;
|
||||
|
||||
case 954: // extsb rA,rS
|
||||
mnemonic = rc ? "extsb." : "extsb";
|
||||
operands.Add(Reg(ra));
|
||||
operands.Add(Reg(rs));
|
||||
break;
|
||||
|
||||
default:
|
||||
mnemonic = $"xo_{xo10}";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
namespace Translator.Core.Disassembly;
|
||||
|
||||
public static partial class PpcDecoder
|
||||
{
|
||||
private static void DecodePrimary59(uint address, uint word, List<PpcOperand> operands, List<uint> branches,
|
||||
ref string mnemonic, ref bool isReturn, ref bool isCall, ref bool isCond)
|
||||
{
|
||||
var xo = GetField(word, 1, 5);
|
||||
var rc = (word & 1) != 0;
|
||||
var frt = GetField(word, 21);
|
||||
var fra = GetField(word, 16);
|
||||
var frb = GetField(word, 11);
|
||||
var frc = GetField(word, 6);
|
||||
|
||||
switch (xo)
|
||||
{
|
||||
case 18:
|
||||
mnemonic = rc ? "fdivs." : "fdivs";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
case 20:
|
||||
mnemonic = rc ? "fsubs." : "fsubs";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
case 21:
|
||||
mnemonic = rc ? "fadds." : "fadds";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
case 24:
|
||||
mnemonic = rc ? "fres." : "fres";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
case 25:
|
||||
mnemonic = rc ? "fmuls." : "fmuls";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
break;
|
||||
case 28:
|
||||
mnemonic = rc ? "fmsubs." : "fmsubs";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
case 29:
|
||||
mnemonic = rc ? "fmadds." : "fmadds";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
case 30:
|
||||
mnemonic = rc ? "fnmsubs." : "fnmsubs";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
case 31:
|
||||
mnemonic = rc ? "fnmadds." : "fnmadds";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
default:
|
||||
mnemonic = "opc_59";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace Translator.Core.Disassembly;
|
||||
|
||||
public static partial class PpcDecoder
|
||||
{
|
||||
private static void DecodePrimary63(uint address, uint word, List<PpcOperand> operands, List<uint> branches,
|
||||
ref string mnemonic, ref bool isReturn, ref bool isCall, ref bool isCond)
|
||||
{
|
||||
var xo = GetField(word, 1, 10);
|
||||
var rc = (word & 1) != 0;
|
||||
var frt = GetField(word, 21);
|
||||
var fra = GetField(word, 16);
|
||||
var frb = GetField(word, 11);
|
||||
var frc = GetField(word, 6);
|
||||
var xo5 = xo & 0x1F;
|
||||
|
||||
switch (xo5)
|
||||
{
|
||||
case 23: // fsel
|
||||
mnemonic = rc ? "fsel." : "fsel";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
operands.Add(FReg(frb));
|
||||
return;
|
||||
case 25: // fmul
|
||||
mnemonic = rc ? "fmul." : "fmul";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
return;
|
||||
case 28: // fmsub
|
||||
mnemonic = rc ? "fmsub." : "fmsub";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
operands.Add(FReg(frb));
|
||||
return;
|
||||
case 29: // fmadd
|
||||
mnemonic = rc ? "fmadd." : "fmadd";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
operands.Add(FReg(frb));
|
||||
return;
|
||||
case 30: // fnmsub
|
||||
mnemonic = rc ? "fnmsub." : "fnmsub";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
operands.Add(FReg(frb));
|
||||
return;
|
||||
case 31: // fnmadd
|
||||
mnemonic = rc ? "fnmadd." : "fnmadd";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
operands.Add(FReg(frb));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (xo)
|
||||
{
|
||||
case 64: // mcrfs
|
||||
{
|
||||
var crfD = GetField(word, 23, 3);
|
||||
var crfS = GetField(word, 18, 3);
|
||||
mnemonic = "mcrfs";
|
||||
operands.Add(CrField(crfD));
|
||||
operands.Add(CrField(crfS));
|
||||
isCond = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case 0: // fcmpu
|
||||
{
|
||||
var crfD = GetField(word, 23, 3);
|
||||
mnemonic = "fcmpu";
|
||||
operands.Add(CrField(crfD));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frb));
|
||||
isCond = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case 32: // fcmpo
|
||||
{
|
||||
var crfD = GetField(word, 23, 3);
|
||||
mnemonic = "fcmpo";
|
||||
operands.Add(CrField(crfD));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frb));
|
||||
isCond = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case 38: // mtfsb1
|
||||
mnemonic = rc ? "mtfsb1." : "mtfsb1";
|
||||
operands.Add(new PpcImmediateOperand(frt));
|
||||
break;
|
||||
|
||||
case 70: // mtfsb0
|
||||
mnemonic = rc ? "mtfsb0." : "mtfsb0";
|
||||
operands.Add(new PpcImmediateOperand(frt));
|
||||
break;
|
||||
|
||||
case 134: // mtfsfi
|
||||
mnemonic = rc ? "mtfsfi." : "mtfsfi";
|
||||
operands.Add(new PpcImmediateOperand(GetField(word, 23, 3)));
|
||||
operands.Add(new PpcImmediateOperand(GetField(word, 12, 4)));
|
||||
break;
|
||||
|
||||
case 583: // mffs
|
||||
mnemonic = rc ? "mffs." : "mffs";
|
||||
operands.Add(FReg(frt));
|
||||
break;
|
||||
|
||||
case 711: // mtfsf
|
||||
mnemonic = rc ? "mtfsf." : "mtfsf";
|
||||
operands.Add(new PpcImmediateOperand((int)((word >> 17) & 0xFF)));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 14: // fctiw
|
||||
mnemonic = rc ? "fctiw." : "fctiw";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 15: // fctiwz
|
||||
mnemonic = rc ? "fctiwz." : "fctiwz";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 12: // frsp
|
||||
mnemonic = rc ? "frsp." : "frsp";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 18: // fdiv
|
||||
mnemonic = rc ? "fdiv." : "fdiv";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 20: // fsub
|
||||
mnemonic = rc ? "fsub." : "fsub";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 21: // fadd
|
||||
mnemonic = rc ? "fadd." : "fadd";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 24: // fres
|
||||
mnemonic = rc ? "fres." : "fres";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 26: // frsqrte
|
||||
mnemonic = rc ? "frsqrte." : "frsqrte";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 40: // fneg
|
||||
mnemonic = rc ? "fneg." : "fneg";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 136: // fnabs
|
||||
mnemonic = rc ? "fnabs." : "fnabs";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 72: // fmr
|
||||
mnemonic = rc ? "fmr." : "fmr";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 264: // fabs
|
||||
mnemonic = rc ? "fabs." : "fabs";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
default:
|
||||
mnemonic = $"fp_{xo}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,820 @@
|
||||
|
||||
namespace Translator.Core.Disassembly;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal in-house PowerPC decoder for the Wii/Gekko instruction set. It returns typed
|
||||
/// operands so later stages never have to parse disassembly strings.
|
||||
/// </summary>
|
||||
public static partial class PpcDecoder
|
||||
{
|
||||
public static PpcInstruction Decode(uint address, uint word)
|
||||
{
|
||||
var primary = (int)((word >> 26) & 0x3F);
|
||||
var operands = new List<PpcOperand>();
|
||||
var branches = new List<uint>();
|
||||
var mnemonic = "unknown";
|
||||
var isReturn = false;
|
||||
var isCall = false;
|
||||
var isCond = false;
|
||||
|
||||
int rD, rA, rS;
|
||||
|
||||
switch (primary)
|
||||
{
|
||||
case 3: // twi
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "twi";
|
||||
operands.Add(new PpcImmediateOperand(GetField(word, 21, 5)));
|
||||
operands.Add(Reg(rA));
|
||||
operands.Add(new PpcImmediateOperand((short)(word & 0xFFFF)));
|
||||
break;
|
||||
|
||||
case 7: // mulli
|
||||
rD = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "mulli";
|
||||
operands.Add(Reg(rD));
|
||||
operands.Add(Reg(rA));
|
||||
operands.Add(new PpcImmediateOperand((short)(word & 0xFFFF)));
|
||||
break;
|
||||
|
||||
case 8: // subfic
|
||||
rD = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "subfic";
|
||||
operands.Add(Reg(rD));
|
||||
operands.Add(Reg(rA));
|
||||
operands.Add(new PpcImmediateOperand((short)(word & 0xFFFF)));
|
||||
break;
|
||||
|
||||
case 10: // cmplwi
|
||||
{
|
||||
var crfD = GetField(word, 23, 3); // Bits 6-8 (3 bits)
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "cmplwi";
|
||||
if (crfD != 0)
|
||||
operands.Add(CrField(crfD));
|
||||
operands.Add(Reg(rA));
|
||||
operands.Add(new PpcImmediateOperand((int)(word & 0xFFFF)));
|
||||
isCond = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case 11: // cmpwi
|
||||
{
|
||||
var crfD = GetField(word, 23, 3); // Bits 6-8 (3 bits)
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "cmpwi";
|
||||
if (crfD != 0)
|
||||
operands.Add(CrField(crfD));
|
||||
operands.Add(Reg(rA));
|
||||
operands.Add(new PpcImmediateOperand((short)(word & 0xFFFF)));
|
||||
isCond = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case 12: // addic
|
||||
rD = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "addic";
|
||||
operands.Add(Reg(rD));
|
||||
operands.Add(Reg(rA));
|
||||
operands.Add(new PpcImmediateOperand((short)(word & 0xFFFF)));
|
||||
break;
|
||||
|
||||
case 13: // addic.
|
||||
rD = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "addic.";
|
||||
operands.Add(Reg(rD));
|
||||
operands.Add(Reg(rA));
|
||||
operands.Add(new PpcImmediateOperand((short)(word & 0xFFFF)));
|
||||
isCond = true;
|
||||
break;
|
||||
|
||||
case 14: // addi / li
|
||||
rD = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
var simm = (short)(word & 0xFFFF);
|
||||
if (rA == 0)
|
||||
{
|
||||
mnemonic = "li";
|
||||
operands.Add(Reg(rD));
|
||||
operands.Add(new PpcImmediateOperand(simm));
|
||||
}
|
||||
else
|
||||
{
|
||||
mnemonic = "addi";
|
||||
operands.Add(Reg(rD));
|
||||
operands.Add(Reg(rA));
|
||||
operands.Add(new PpcImmediateOperand(simm));
|
||||
}
|
||||
break;
|
||||
|
||||
case 15: // addis / lis
|
||||
rD = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
simm = (short)(word & 0xFFFF);
|
||||
if (rA == 0)
|
||||
{
|
||||
mnemonic = "lis";
|
||||
operands.Add(Reg(rD));
|
||||
operands.Add(new PpcImmediateOperand(simm));
|
||||
}
|
||||
else
|
||||
{
|
||||
mnemonic = "addis";
|
||||
operands.Add(Reg(rD));
|
||||
operands.Add(Reg(rA));
|
||||
operands.Add(new PpcImmediateOperand(simm));
|
||||
}
|
||||
break;
|
||||
|
||||
case 16: // bc and its extended mnemonics
|
||||
var bo = GetField(word, 21, 5);
|
||||
var bi = GetField(word, 16, 5);
|
||||
var bd = SignExtend((word >> 2) & 0x3FFFu, 14) << 2;
|
||||
var aa = ((word >> 1) & 1) != 0;
|
||||
var lk = (word & 1) != 0; // LK bit - set link register
|
||||
var target = aa ? (uint)bd : address + (uint)bd;
|
||||
var crField = bi / 4; // CR0-CR7 field
|
||||
|
||||
var branchMnemonic = TryDecodeCrBranchMnemonic(bo, bi);
|
||||
if (branchMnemonic is not null)
|
||||
{
|
||||
mnemonic = lk ? branchMnemonic + "l" : branchMnemonic;
|
||||
}
|
||||
else if ((bo & 0x1E) == 16)
|
||||
{
|
||||
mnemonic = lk ? "bdnzl" : "bdnz";
|
||||
}
|
||||
else if ((bo & 0x1E) == 18)
|
||||
{
|
||||
mnemonic = lk ? "bdzl" : "bdz";
|
||||
}
|
||||
else
|
||||
{
|
||||
mnemonic = lk ? "bcl" : "bc";
|
||||
operands.Add(new PpcImmediateOperand(bo));
|
||||
operands.Add(new PpcImmediateOperand(bi));
|
||||
}
|
||||
|
||||
// Add CR field operand for non-cr0 branches so the lifter knows which field to check
|
||||
if (crField != 0)
|
||||
{
|
||||
operands.Add(CrField(crField));
|
||||
}
|
||||
operands.Add(new PpcBranchTargetOperand(target));
|
||||
branches.Add(target);
|
||||
isCond = true;
|
||||
isCall = lk; // LK bit means this is a call (sets LR)
|
||||
break;
|
||||
|
||||
case 17: // sc
|
||||
mnemonic = "sc";
|
||||
break;
|
||||
|
||||
|
||||
case 18: // b / bl
|
||||
var li = SignExtend((word >> 2) & 0x00FFFFFFu, 24) << 2;
|
||||
aa = ((word >> 1) & 1) != 0;
|
||||
lk = (word & 1) != 0;
|
||||
target = aa ? (uint)li : address + (uint)li;
|
||||
mnemonic = lk ? "bl" : "b";
|
||||
operands.Add(new PpcBranchTargetOperand(target));
|
||||
branches.Add(target);
|
||||
isCall = lk;
|
||||
break;
|
||||
|
||||
case 19:
|
||||
DecodePrimary19(address, word, operands, branches, ref mnemonic, ref isReturn, ref isCall, ref isCond);
|
||||
break;
|
||||
case 20: // rlwimi
|
||||
rS = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
var sh = GetField(word, 11, 5);
|
||||
var mb = GetField(word, 6, 5);
|
||||
var me = GetField(word, 1, 5);
|
||||
mnemonic = (word & 1) != 0 ? "rlwimi." : "rlwimi";
|
||||
operands.Add(Reg(rA));
|
||||
operands.Add(Reg(rS));
|
||||
operands.Add(new PpcImmediateOperand(sh));
|
||||
operands.Add(new PpcImmediateOperand(mb));
|
||||
operands.Add(new PpcImmediateOperand(me));
|
||||
isCond = (word & 1) != 0;
|
||||
break;
|
||||
|
||||
case 21: // rlwinm
|
||||
rS = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
sh = GetField(word, 11, 5);
|
||||
mb = GetField(word, 6, 5);
|
||||
me = GetField(word, 1, 5);
|
||||
mnemonic = (word & 1) != 0 ? "rlwinm." : "rlwinm";
|
||||
operands.Add(Reg(rA));
|
||||
operands.Add(Reg(rS));
|
||||
operands.Add(new PpcImmediateOperand(sh));
|
||||
operands.Add(new PpcImmediateOperand(mb));
|
||||
operands.Add(new PpcImmediateOperand(me));
|
||||
isCond = (word & 1) != 0;
|
||||
break;
|
||||
|
||||
case 23: // rlwnm - Rotate Left Word then AND with Mask (register shift)
|
||||
rS = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
var rB = GetField(word, 11); // Register containing shift amount
|
||||
mb = GetField(word, 6, 5);
|
||||
me = GetField(word, 1, 5);
|
||||
mnemonic = (word & 1) != 0 ? "rlwnm." : "rlwnm";
|
||||
operands.Add(Reg(rA));
|
||||
operands.Add(Reg(rS));
|
||||
operands.Add(Reg(rB)); // Register instead of immediate
|
||||
operands.Add(new PpcImmediateOperand(mb));
|
||||
operands.Add(new PpcImmediateOperand(me));
|
||||
isCond = (word & 1) != 0;
|
||||
break;
|
||||
|
||||
case 4: // paired-single arithmetic/select
|
||||
{
|
||||
// Extract full 10-bit XO (bits 21-30)
|
||||
var xo = (int)((word >> 1) & 0x3FF);
|
||||
// Extract lower 5 bits (bits 26-30)
|
||||
var xo5 = xo & 0x1F;
|
||||
var rc = (word & 1) != 0;
|
||||
|
||||
var frt = GetField(word, 21);
|
||||
var fra = GetField(word, 16);
|
||||
var frb = GetField(word, 11);
|
||||
var frc = GetField(word, 6);
|
||||
|
||||
// First handle A-form instructions (which use bits 21-25 as frC operand, not part of opcode)
|
||||
// These are identified by the lower 5 bits of XO (bits 26-30 in word)
|
||||
switch (xo5)
|
||||
{
|
||||
case 29: // ps_madd (XO=29)
|
||||
mnemonic = rc ? "ps_madd." : "ps_madd";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 28: // ps_msub (XO=28)
|
||||
mnemonic = rc ? "ps_msub." : "ps_msub";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 30: // ps_nmsub (XO=30)
|
||||
mnemonic = rc ? "ps_nmsub." : "ps_nmsub";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 31: // ps_nmadd (XO=31)
|
||||
mnemonic = rc ? "ps_nmadd." : "ps_nmadd";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 25: // ps_mul (XO=25)
|
||||
mnemonic = rc ? "ps_mul." : "ps_mul";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
break;
|
||||
|
||||
case 23: // ps_sel (XO=23)
|
||||
mnemonic = rc ? "ps_sel." : "ps_sel";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 14: // ps_madds0 (XO5=14)
|
||||
mnemonic = rc ? "ps_madds0." : "ps_madds0";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 15: // ps_madds1 (XO5=15)
|
||||
mnemonic = rc ? "ps_madds1." : "ps_madds1";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 10: // ps_sum0 (XO5=10)
|
||||
mnemonic = rc ? "ps_sum0." : "ps_sum0";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 11: // ps_sum1 (XO5=11)
|
||||
mnemonic = rc ? "ps_sum1." : "ps_sum1";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 12: // ps_muls0 (XO5=12)
|
||||
mnemonic = rc ? "ps_muls0." : "ps_muls0";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
break;
|
||||
|
||||
case 13: // ps_muls1 (XO5=13)
|
||||
mnemonic = rc ? "ps_muls1." : "ps_muls1";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frc));
|
||||
break;
|
||||
|
||||
default:
|
||||
// If not A-form, check full 10-bit XO for X-form instructions
|
||||
var psqIndexedXo = xo & 0x3F;
|
||||
if (psqIndexedXo is 6 or 38)
|
||||
{
|
||||
var w = (word >> 10) & 0x1;
|
||||
var quant = (word >> 7) & 0x7;
|
||||
mnemonic = psqIndexedXo == 38 ? "psq_lux" : "psq_lx";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(Reg(fra));
|
||||
operands.Add(Reg(frb));
|
||||
operands.Add(new PpcImmediateOperand((int)w));
|
||||
operands.Add(new PpcImmediateOperand((int)quant));
|
||||
break;
|
||||
}
|
||||
|
||||
if (psqIndexedXo is 7 or 39)
|
||||
{
|
||||
var w = (word >> 10) & 0x1;
|
||||
var quant = (word >> 7) & 0x7;
|
||||
mnemonic = psqIndexedXo == 39 ? "psq_stux" : "psq_stx";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(Reg(fra));
|
||||
operands.Add(Reg(frb));
|
||||
operands.Add(new PpcImmediateOperand((int)w));
|
||||
operands.Add(new PpcImmediateOperand((int)quant));
|
||||
break;
|
||||
}
|
||||
|
||||
switch (xo)
|
||||
{
|
||||
case 21: // ps_add (XO=21)
|
||||
mnemonic = rc ? "ps_add." : "ps_add";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 20: // ps_sub (XO=20)
|
||||
mnemonic = rc ? "ps_sub." : "ps_sub";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 18: // ps_div (XO=18)
|
||||
mnemonic = rc ? "ps_div." : "ps_div";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 24: // ps_res (XO=24)
|
||||
mnemonic = rc ? "ps_res." : "ps_res";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 26: // ps_rsqrte (XO=26)
|
||||
mnemonic = rc ? "ps_rsqrte." : "ps_rsqrte";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 528: // ps_merge00 (XO=528)
|
||||
mnemonic = rc ? "ps_merge00." : "ps_merge00";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 560: // ps_merge01 (XO=560)
|
||||
mnemonic = rc ? "ps_merge01." : "ps_merge01";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 592: // ps_merge10 (XO=592)
|
||||
mnemonic = rc ? "ps_merge10." : "ps_merge10";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 624: // ps_merge11 (XO=624)
|
||||
mnemonic = rc ? "ps_merge11." : "ps_merge11";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 72: // ps_mr (XO=72)
|
||||
mnemonic = rc ? "ps_mr." : "ps_mr";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 40: // ps_neg (XO=40)
|
||||
mnemonic = rc ? "ps_neg." : "ps_neg";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 264: // ps_abs (XO=264)
|
||||
mnemonic = rc ? "ps_abs." : "ps_abs";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 136: // ps_nabs (XO=136)
|
||||
mnemonic = rc ? "ps_nabs." : "ps_nabs";
|
||||
operands.Add(FReg(frt));
|
||||
operands.Add(FReg(frb));
|
||||
break;
|
||||
|
||||
case 0: // ps_cmpu0 (XO=0)
|
||||
{
|
||||
var crfD = GetField(word, 23, 3);
|
||||
mnemonic = "ps_cmpu0";
|
||||
operands.Add(CrField(crfD));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frb));
|
||||
isCond = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case 32: // ps_cmpo0 (XO=32)
|
||||
{
|
||||
var crfD = GetField(word, 23, 3);
|
||||
mnemonic = "ps_cmpo0";
|
||||
operands.Add(CrField(crfD));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frb));
|
||||
isCond = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case 64: // ps_cmpu1 (XO=64)
|
||||
{
|
||||
var crfD = GetField(word, 23, 3);
|
||||
mnemonic = "ps_cmpu1";
|
||||
operands.Add(CrField(crfD));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frb));
|
||||
isCond = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case 96: // ps_cmpo1 (XO=96)
|
||||
{
|
||||
var crfD = GetField(word, 23, 3);
|
||||
mnemonic = "ps_cmpo1";
|
||||
operands.Add(CrField(crfD));
|
||||
operands.Add(FReg(fra));
|
||||
operands.Add(FReg(frb));
|
||||
isCond = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case 1014: // dcbz_l (XO=1014)
|
||||
mnemonic = "dcbz_l";
|
||||
operands.Add(Reg(fra));
|
||||
operands.Add(Reg(frb));
|
||||
break;
|
||||
|
||||
default:
|
||||
mnemonic = $"opc_4_{xo}";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 24: // ori / nop alias
|
||||
rA = GetField(word, 16);
|
||||
rS = GetField(word, 21);
|
||||
var uimm16 = (int)(word & 0xFFFF);
|
||||
mnemonic = (rA == 0 && rS == 0 && uimm16 == 0) ? "nop" : "ori";
|
||||
operands.Add(Reg(rA));
|
||||
operands.Add(Reg(rS));
|
||||
operands.Add(new PpcImmediateOperand(uimm16));
|
||||
break;
|
||||
|
||||
case 25: // oris
|
||||
rA = GetField(word, 16);
|
||||
rS = GetField(word, 21);
|
||||
uimm16 = (int)(word & 0xFFFF);
|
||||
mnemonic = "oris";
|
||||
operands.Add(Reg(rA));
|
||||
operands.Add(Reg(rS));
|
||||
operands.Add(new PpcImmediateOperand(uimm16));
|
||||
break;
|
||||
|
||||
case 26: // xori
|
||||
rA = GetField(word, 16);
|
||||
rS = GetField(word, 21);
|
||||
uimm16 = (int)(word & 0xFFFF);
|
||||
mnemonic = "xori";
|
||||
operands.Add(Reg(rA));
|
||||
operands.Add(Reg(rS));
|
||||
operands.Add(new PpcImmediateOperand(uimm16));
|
||||
break;
|
||||
|
||||
case 27: // xoris
|
||||
rA = GetField(word, 16);
|
||||
rS = GetField(word, 21);
|
||||
uimm16 = (int)(word & 0xFFFF);
|
||||
mnemonic = "xoris";
|
||||
operands.Add(Reg(rA));
|
||||
operands.Add(Reg(rS));
|
||||
operands.Add(new PpcImmediateOperand(uimm16));
|
||||
break;
|
||||
|
||||
case 28: // andi.
|
||||
rA = GetField(word, 16);
|
||||
rS = GetField(word, 21);
|
||||
uimm16 = (int)(word & 0xFFFF);
|
||||
mnemonic = "andi.";
|
||||
operands.Add(Reg(rA));
|
||||
operands.Add(Reg(rS));
|
||||
operands.Add(new PpcImmediateOperand(uimm16));
|
||||
isCond = true;
|
||||
break;
|
||||
|
||||
case 29: // andis.
|
||||
rA = GetField(word, 16);
|
||||
rS = GetField(word, 21);
|
||||
uimm16 = (int)(word & 0xFFFF);
|
||||
mnemonic = "andis.";
|
||||
operands.Add(Reg(rA));
|
||||
operands.Add(Reg(rS));
|
||||
operands.Add(new PpcImmediateOperand(uimm16));
|
||||
isCond = true;
|
||||
break;
|
||||
|
||||
case 32: // lwz
|
||||
rD = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "lwz";
|
||||
operands.Add(Reg(rD));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 33: // lwzu
|
||||
rD = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "lwzu";
|
||||
operands.Add(Reg(rD));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 34: // lbz
|
||||
rD = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "lbz";
|
||||
operands.Add(Reg(rD));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 35: // lbzu
|
||||
rD = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "lbzu";
|
||||
operands.Add(Reg(rD));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 36: // stw
|
||||
rS = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "stw";
|
||||
operands.Add(Reg(rS));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 37: // stwu
|
||||
rS = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "stwu";
|
||||
operands.Add(Reg(rS));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 38: // stb
|
||||
rS = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "stb";
|
||||
operands.Add(Reg(rS));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 39: // stbu
|
||||
rS = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "stbu";
|
||||
operands.Add(Reg(rS));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 40: // lhz
|
||||
rD = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "lhz";
|
||||
operands.Add(Reg(rD));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 41: // lhzu
|
||||
rD = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "lhzu";
|
||||
operands.Add(Reg(rD));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 42: // lha
|
||||
rD = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "lha";
|
||||
operands.Add(Reg(rD));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 43: // lhau
|
||||
rD = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "lhau";
|
||||
operands.Add(Reg(rD));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 44: // sth
|
||||
rS = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "sth";
|
||||
operands.Add(Reg(rS));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 45: // sthu
|
||||
rS = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "sthu";
|
||||
operands.Add(Reg(rS));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 46: // lmw
|
||||
rD = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "lmw";
|
||||
operands.Add(Reg(rD));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 47: // stmw
|
||||
rS = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "stmw";
|
||||
operands.Add(Reg(rS));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 48: // lfs
|
||||
rD = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "lfs";
|
||||
operands.Add(FReg(rD));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 49: // lfsu
|
||||
rD = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "lfsu";
|
||||
operands.Add(FReg(rD));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 50: // lfd
|
||||
rD = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "lfd";
|
||||
operands.Add(FReg(rD));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 51: // lfdu
|
||||
rD = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "lfdu";
|
||||
operands.Add(FReg(rD));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 52: // stfs
|
||||
rS = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "stfs";
|
||||
operands.Add(FReg(rS));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 53: // stfsu
|
||||
rS = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "stfsu";
|
||||
operands.Add(FReg(rS));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 54: // stfd
|
||||
rS = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "stfd";
|
||||
operands.Add(FReg(rS));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 55: // stfdu
|
||||
rS = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
mnemonic = "stfdu";
|
||||
operands.Add(FReg(rS));
|
||||
operands.Add(new PpcDisplacementOperand((short)(word & 0xFFFF), RegName(rA), rA));
|
||||
break;
|
||||
|
||||
case 56: // psq_l
|
||||
case 57: // psq_lu
|
||||
case 60: // psq_st
|
||||
case 61: // psq_stu
|
||||
{
|
||||
var fr = GetField(word, 21);
|
||||
rA = GetField(word, 16);
|
||||
var w = (word >> 15) & 0x1;
|
||||
var quant = (word >> 12) & 0x7;
|
||||
var disp = SignExtend(word & 0xFFFu, 12);
|
||||
|
||||
mnemonic = primary switch
|
||||
{
|
||||
56 => "psq_l",
|
||||
57 => "psq_lu",
|
||||
60 => "psq_st",
|
||||
61 => "psq_stu",
|
||||
_ => $"opc_{primary}"
|
||||
};
|
||||
|
||||
operands.Add(FReg(fr));
|
||||
operands.Add(new PpcDisplacementOperand(disp, RegName(rA), rA));
|
||||
operands.Add(new PpcImmediateOperand((int)w));
|
||||
operands.Add(new PpcImmediateOperand((int)quant));
|
||||
break;
|
||||
}
|
||||
|
||||
case 63:
|
||||
DecodePrimary63(address, word, operands, branches, ref mnemonic, ref isReturn, ref isCall, ref isCond);
|
||||
break;
|
||||
case 59:
|
||||
DecodePrimary59(address, word, operands, branches, ref mnemonic, ref isReturn, ref isCall, ref isCond);
|
||||
break;
|
||||
case 31:
|
||||
DecodePrimary31(address, word, operands, branches, ref mnemonic, ref isReturn, ref isCall, ref isCond);
|
||||
break;
|
||||
default:
|
||||
mnemonic = $"opc_{primary}";
|
||||
break;
|
||||
}
|
||||
|
||||
return new PpcInstruction(address, word, mnemonic, operands, branches, isReturn, isCall, isCond);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Translator.Core.Loading;
|
||||
|
||||
namespace Translator.Core.Disassembly;
|
||||
|
||||
/// <summary>
|
||||
/// Simple disassembler that walks reachable instructions using the in-process PPC decoder.
|
||||
/// Optimized for demand-driven decoding to avoid decoding unreachable instructions.
|
||||
/// </summary>
|
||||
public sealed class PpcDisassemblyLimitExceededException : InvalidOperationException
|
||||
{
|
||||
public PpcDisassemblyLimitExceededException(
|
||||
uint entryPoint,
|
||||
int maxInstructions,
|
||||
int maxBytes,
|
||||
int decodedInstructions,
|
||||
uint blockedAddress)
|
||||
: base(
|
||||
$"Disassembly budget exhausted for 0x{entryPoint:X8} after {decodedInstructions} reachable instructions; " +
|
||||
$"next reachable address was 0x{blockedAddress:X8} (maxInstructions={maxInstructions}, maxBytes=0x{maxBytes:X}). " +
|
||||
"Increase the disassembly budget before trusting the generated control flow.")
|
||||
{
|
||||
EntryPoint = entryPoint;
|
||||
MaxInstructions = maxInstructions;
|
||||
MaxBytes = maxBytes;
|
||||
DecodedInstructions = decodedInstructions;
|
||||
BlockedAddress = blockedAddress;
|
||||
}
|
||||
|
||||
public uint EntryPoint { get; }
|
||||
public int MaxInstructions { get; }
|
||||
public int MaxBytes { get; }
|
||||
public int DecodedInstructions { get; }
|
||||
public uint BlockedAddress { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records every membership question asked of the known-function entry point set.
|
||||
/// A cached decode is replayable only while all of these still answer the same way.
|
||||
/// </summary>
|
||||
public sealed class BoundaryProbeLog
|
||||
{
|
||||
private List<uint>? _absent;
|
||||
|
||||
/// <summary>Addresses that were asked about and were not boundaries.</summary>
|
||||
public IReadOnlyList<uint> AbsentBoundaries => (IReadOnlyList<uint>?)_absent ?? Array.Empty<uint>();
|
||||
|
||||
public void RecordAbsent(uint address)
|
||||
{
|
||||
_absent ??= new List<uint>();
|
||||
if (!_absent.Contains(address))
|
||||
{
|
||||
_absent.Add(address);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True while every recorded question still has the same answer. Boundary
|
||||
/// sets only ever grow during a run, so only the negative answers can flip.
|
||||
/// </summary>
|
||||
public bool IsStillValid(IReadOnlySet<uint>? knownFunctionEntryPoints)
|
||||
{
|
||||
if (_absent is null || knownFunctionEntryPoints is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
for (var i = 0; i < _absent.Count; i++)
|
||||
{
|
||||
if (knownFunctionEntryPoints.Contains(_absent[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PpcDisassembler : IDisposable
|
||||
{
|
||||
public IReadOnlyList<PpcInstruction> DisassembleFunction(
|
||||
ProgramImage image,
|
||||
uint entryPoint,
|
||||
int maxInstructions = 256,
|
||||
int maxBytes = 0x800,
|
||||
IReadOnlySet<uint>? knownFunctionEntryPoints = null,
|
||||
BoundaryProbeLog? boundaryProbes = null)
|
||||
{
|
||||
var instructions = DisassembleFunctionCore(
|
||||
image, entryPoint, maxInstructions, maxBytes, knownFunctionEntryPoints, boundaryProbes, throwOnBudget: true);
|
||||
return instructions!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Budget-tolerant variant for speculative decoding (leaf inlining), where an
|
||||
/// oversized body is a normal "not a candidate" answer, not an error to throw and swallow.
|
||||
/// </summary>
|
||||
public IReadOnlyList<PpcInstruction>? TryDisassembleFunction(
|
||||
ProgramImage image,
|
||||
uint entryPoint,
|
||||
int maxInstructions,
|
||||
int maxBytes,
|
||||
IReadOnlySet<uint>? knownFunctionEntryPoints = null,
|
||||
BoundaryProbeLog? boundaryProbes = null)
|
||||
=> DisassembleFunctionCore(
|
||||
image, entryPoint, maxInstructions, maxBytes, knownFunctionEntryPoints, boundaryProbes, throwOnBudget: false);
|
||||
|
||||
private static IReadOnlyList<PpcInstruction>? DisassembleFunctionCore(
|
||||
ProgramImage image,
|
||||
uint entryPoint,
|
||||
int maxInstructions,
|
||||
int maxBytes,
|
||||
IReadOnlySet<uint>? knownFunctionEntryPoints,
|
||||
BoundaryProbeLog? boundaryProbes,
|
||||
bool throwOnBudget)
|
||||
{
|
||||
if (!image.Contains(entryPoint, sizeof(uint)))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(entryPoint), $"Address 0x{entryPoint:X8} is outside the loaded RAM image.");
|
||||
}
|
||||
var offset = image.GetOffset(entryPoint, sizeof(uint));
|
||||
|
||||
var available = Math.Min(maxBytes, image.Memory.Length - offset);
|
||||
|
||||
// Demand-driven decoding: decode only the instructions we actually visit
|
||||
var instructionMap = new Dictionary<uint, PpcInstruction>();
|
||||
var reachable = new HashSet<uint>();
|
||||
var localControlFlowTargets = new HashSet<uint>();
|
||||
var worklist = new Queue<uint>();
|
||||
worklist.Enqueue(entryPoint);
|
||||
|
||||
var endAddress = entryPoint + (uint)available;
|
||||
|
||||
// Ordered view used by jump-table recognition, rebuilt lazily when the decoded set grows.
|
||||
List<PpcInstruction>? orderedCache = null;
|
||||
Dictionary<uint, int>? indexByAddressCache = null;
|
||||
var orderedCacheCount = -1;
|
||||
|
||||
while (worklist.Count > 0)
|
||||
{
|
||||
if (reachable.Count >= maxInstructions)
|
||||
{
|
||||
if (!throwOnBudget)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new PpcDisassemblyLimitExceededException(
|
||||
entryPoint,
|
||||
maxInstructions,
|
||||
maxBytes,
|
||||
reachable.Count,
|
||||
worklist.Peek());
|
||||
}
|
||||
|
||||
var cursor = worklist.Dequeue();
|
||||
while (cursor >= entryPoint && cursor < endAddress)
|
||||
{
|
||||
if (reachable.Count >= maxInstructions)
|
||||
{
|
||||
if (!throwOnBudget)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new PpcDisassemblyLimitExceededException(
|
||||
entryPoint,
|
||||
maxInstructions,
|
||||
maxBytes,
|
||||
reachable.Count,
|
||||
cursor);
|
||||
}
|
||||
|
||||
// Decode on demand
|
||||
if (!instructionMap.TryGetValue(cursor, out var ins))
|
||||
{
|
||||
if (!image.Contains(cursor, sizeof(uint)))
|
||||
{
|
||||
break;
|
||||
}
|
||||
var cursorOffset = image.GetOffset(cursor, sizeof(uint));
|
||||
var word = BinaryPrimitives.ReadUInt32BigEndian(image.Memory.AsSpan(cursorOffset, 4));
|
||||
ins = PpcDecoder.Decode(cursor, word);
|
||||
instructionMap[cursor] = ins;
|
||||
}
|
||||
|
||||
if (!reachable.Add(ins.Address))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var recognizedJumpTable = false;
|
||||
if (ins.BranchTargets.Count == 0 && string.Equals(ins.Mnemonic, "bctr", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// For jump tables, we need the ordered list - build it lazily
|
||||
if (orderedCache is null || indexByAddressCache is null || orderedCacheCount != instructionMap.Count)
|
||||
{
|
||||
orderedCache = instructionMap.Values.OrderBy(i => i.Address).ToList();
|
||||
indexByAddressCache = new Dictionary<uint, int>(orderedCache.Count);
|
||||
for (var idx = 0; idx < orderedCache.Count; idx++)
|
||||
{
|
||||
indexByAddressCache[orderedCache[idx].Address] = idx;
|
||||
}
|
||||
|
||||
orderedCacheCount = instructionMap.Count;
|
||||
}
|
||||
|
||||
if (JumpTableDetector.TryRecognize(orderedCache, indexByAddressCache, ins.Address, entryPoint, endAddress, image, out var jtTargets))
|
||||
{
|
||||
ins = new PpcInstruction(ins.Address, ins.Word, ins.Mnemonic, ins.Operands, jtTargets, isReturn: false, ins.IsCall, ins.IsConditionalBranch);
|
||||
instructionMap[ins.Address] = ins;
|
||||
// Same address, so the ordered view's count is unchanged but its content is stale; drop it.
|
||||
orderedCache = null;
|
||||
indexByAddressCache = null;
|
||||
orderedCacheCount = -1;
|
||||
foreach (var target in jtTargets)
|
||||
{
|
||||
localControlFlowTargets.Add(target);
|
||||
}
|
||||
recognizedJumpTable = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ins.IsConditionalBranch)
|
||||
{
|
||||
foreach (var target in ins.BranchTargets)
|
||||
{
|
||||
localControlFlowTargets.Add(target);
|
||||
if (!reachable.Contains(target))
|
||||
{
|
||||
worklist.Enqueue(target);
|
||||
}
|
||||
}
|
||||
cursor = ins.EndAddress;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ins.IsUnconditionalBranch)
|
||||
{
|
||||
foreach (var target in ins.BranchTargets)
|
||||
{
|
||||
if ((recognizedJumpTable || !IsKnownExternalFunctionEntry(target)) && !reachable.Contains(target))
|
||||
{
|
||||
worklist.Enqueue(target);
|
||||
}
|
||||
}
|
||||
break; // no fallthrough
|
||||
}
|
||||
|
||||
if (ins.IsReturn)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = ins.EndAddress;
|
||||
}
|
||||
}
|
||||
|
||||
var addresses = new uint[reachable.Count];
|
||||
reachable.CopyTo(addresses);
|
||||
Array.Sort(addresses);
|
||||
var ordered = new List<PpcInstruction>(addresses.Length);
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
ordered.Add(instructionMap[address]);
|
||||
}
|
||||
|
||||
return ordered;
|
||||
|
||||
bool IsKnownExternalFunctionEntry(uint target)
|
||||
{
|
||||
if (target == entryPoint || localControlFlowTargets.Contains(target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (knownFunctionEntryPoints is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var known = knownFunctionEntryPoints.Contains(target);
|
||||
if (!known)
|
||||
{
|
||||
boundaryProbes?.RecordAbsent(target);
|
||||
}
|
||||
|
||||
return known;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// nothing to dispose currently
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Translator.Core.Disassembly;
|
||||
|
||||
/// <summary>
|
||||
/// Light wrapper over a decoded PowerPC instruction with semantic flags and typed operands.
|
||||
/// This type is intentionally immutable so tests can compare instances by value.
|
||||
/// </summary>
|
||||
public sealed class PpcInstruction
|
||||
{
|
||||
public PpcInstruction(
|
||||
uint address,
|
||||
uint word,
|
||||
string mnemonic,
|
||||
IReadOnlyList<PpcOperand> operands,
|
||||
IReadOnlyList<uint> branchTargets,
|
||||
bool isReturn,
|
||||
bool isCall,
|
||||
bool isConditionalBranch)
|
||||
{
|
||||
Address = address;
|
||||
Word = word;
|
||||
Mnemonic = mnemonic;
|
||||
Operands = operands;
|
||||
BranchTargets = branchTargets;
|
||||
IsReturn = isReturn;
|
||||
IsCall = isCall;
|
||||
IsConditionalBranch = isConditionalBranch;
|
||||
}
|
||||
|
||||
public uint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The raw big-endian instruction word, replacing the former byte array to save two
|
||||
/// allocations per decoded instruction.
|
||||
/// </summary>
|
||||
public uint Word { get; }
|
||||
|
||||
public string Mnemonic { get; }
|
||||
public IReadOnlyList<PpcOperand> Operands { get; }
|
||||
public IReadOnlyList<uint> BranchTargets { get; }
|
||||
public bool IsReturn { get; }
|
||||
public bool IsCall { get; }
|
||||
public bool IsConditionalBranch { get; }
|
||||
|
||||
public string OperandText => string.Join(", ", Operands.Select(o => o.ToOperandString()));
|
||||
|
||||
public uint EndAddress => checked(Address + 4u);
|
||||
|
||||
public bool IsUnconditionalBranch => BranchTargets.Count > 0 && !IsConditionalBranch && !IsCall && !IsReturn;
|
||||
|
||||
public override string ToString() => $"0x{Address:X8}: {Mnemonic} {OperandText}".Trim();
|
||||
|
||||
public static PpcInstruction Synthetic(
|
||||
uint address,
|
||||
uint opcode,
|
||||
string mnemonic,
|
||||
IReadOnlyList<PpcOperand> operands,
|
||||
IReadOnlyList<uint>? branchTargets = null,
|
||||
bool? isReturn = null,
|
||||
bool? isCall = null,
|
||||
bool? isConditional = null)
|
||||
{
|
||||
var (ret, call, cond) = InferFlags(mnemonic, isReturn, isCall, isConditional);
|
||||
return new PpcInstruction(address, opcode, mnemonic, operands, branchTargets ?? Array.Empty<uint>(), ret, call, cond);
|
||||
}
|
||||
|
||||
private static (bool ret, bool call, bool cond) InferFlags(string mnemonic, bool? retOpt, bool? callOpt, bool? condOpt)
|
||||
{
|
||||
if (retOpt.HasValue || callOpt.HasValue || condOpt.HasValue)
|
||||
{
|
||||
return (retOpt ?? false, callOpt ?? false, condOpt ?? false);
|
||||
}
|
||||
|
||||
var lower = mnemonic.ToLowerInvariant();
|
||||
var ret = lower is "bctr" or "rfi" ||
|
||||
(lower.StartsWith("b", StringComparison.Ordinal) && lower.EndsWith("lr", StringComparison.Ordinal) &&
|
||||
!lower.Contains("lrl", StringComparison.Ordinal));
|
||||
var call = lower is "bl" or "bcl" or "blrl" || lower.Contains("bctrl") || lower.Contains("bclrl");
|
||||
var cond = lower.StartsWith("bc") || lower.StartsWith("bge") || lower.StartsWith("ble") || lower.StartsWith("beq") ||
|
||||
lower.StartsWith("bne") || lower.StartsWith("bgt") || lower.StartsWith("blt") || lower.StartsWith("bd");
|
||||
return (ret, call, cond);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
|
||||
namespace Translator.Core.Disassembly;
|
||||
|
||||
/// <summary>
|
||||
/// Typed representation of a PowerPC instruction operand to avoid any string parsing in later stages.
|
||||
/// </summary>
|
||||
public abstract record PpcOperand
|
||||
{
|
||||
public abstract string ToOperandString();
|
||||
|
||||
public override string ToString() => ToOperandString();
|
||||
}
|
||||
|
||||
public sealed record PpcRegisterOperand(string Name, int Number) : PpcOperand
|
||||
{
|
||||
public override string ToOperandString() => Name;
|
||||
}
|
||||
|
||||
public sealed record PpcImmediateOperand(int Value, bool PreferHex = false) : PpcOperand
|
||||
{
|
||||
public override string ToOperandString()
|
||||
{
|
||||
if (!PreferHex)
|
||||
{
|
||||
return Value.ToString();
|
||||
}
|
||||
|
||||
return Value < 0 ? $"-0x{Math.Abs(Value):X}" : $"0x{Value:X}";
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record PpcDisplacementOperand(int Offset, string BaseRegister, int BaseRegisterNumber) : PpcOperand
|
||||
{
|
||||
public override string ToOperandString()
|
||||
{
|
||||
var text = Offset == 0 ? "0" : (Offset < 0 ? $"-0x{Math.Abs(Offset):X}" : $"0x{Offset:X}");
|
||||
return $"{text}({BaseRegister})";
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record PpcBranchTargetOperand(uint TargetAddress) : PpcOperand
|
||||
{
|
||||
public override string ToOperandString() => $"0x{TargetAddress:X8}";
|
||||
}
|
||||
|
||||
public sealed record PpcConditionRegisterOperand(string Name, int BitIndex) : PpcOperand
|
||||
{
|
||||
public override string ToOperandString() => Name;
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
namespace Translator.Core.Disassembly;
|
||||
|
||||
/// <summary>
|
||||
/// Allocation-free view of the fixed fields shared by the raw PowerPC instruction scanners.
|
||||
/// Not a second semantic decoder: callers still decide which fields matter for a given opcode.
|
||||
/// </summary>
|
||||
internal readonly struct PpcWordFields
|
||||
{
|
||||
private readonly uint Word;
|
||||
|
||||
public PpcWordFields(uint word) => Word = word;
|
||||
|
||||
/// <summary>
|
||||
/// The big-endian instruction word at <paramref name="offset"/>. Three raw
|
||||
/// scanners carried a byte-identical private copy of this one-liner.
|
||||
/// </summary>
|
||||
public static uint ReadBigEndianWord(byte[] data, int offset) =>
|
||||
System.Buffers.Binary.BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(offset, 4));
|
||||
|
||||
public int PrimaryOpcode => (int)((Word >> 26) & 0x3Fu);
|
||||
|
||||
/// <summary>Bits 21-25, named RS/RT depending on the instruction form.</summary>
|
||||
public int GprField0 => (int)((Word >> 21) & 0x1Fu);
|
||||
|
||||
/// <summary>Bits 16-20, normally RA.</summary>
|
||||
public int GprField1 => (int)((Word >> 16) & 0x1Fu);
|
||||
|
||||
/// <summary>Bits 11-15, normally RB.</summary>
|
||||
public int GprField2 => (int)((Word >> 11) & 0x1Fu);
|
||||
|
||||
public int ExtendedOpcode => (int)((Word >> 1) & 0x3FFu);
|
||||
|
||||
/// <summary>
|
||||
/// The split SPR field used by mfspr/mtspr. The low five SPR bits are in
|
||||
/// bits 16-20 and the high five bits are in bits 11-15.
|
||||
/// </summary>
|
||||
public int Spr => (int)(((Word >> 16) & 0x1Fu) | ((Word >> 6) & 0x3E0u));
|
||||
|
||||
public short SignedImmediate16 => unchecked((short)(Word & 0xFFFFu));
|
||||
|
||||
public uint UnsignedImmediate16 => Word & 0xFFFFu;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Small raw-word recognizers shared by module and patch scanners, ordered by the names in
|
||||
/// each instruction form rather than the decoder's alias-oriented operand list.
|
||||
/// </summary>
|
||||
internal static class PpcInstructionPatterns
|
||||
{
|
||||
public static bool TryGetLis(uint word, out int destination, out uint highImmediate)
|
||||
{
|
||||
var fields = new PpcWordFields(word);
|
||||
if (fields.PrimaryOpcode == 15 && fields.GprField1 == 0)
|
||||
{
|
||||
destination = fields.GprField0;
|
||||
highImmediate = fields.UnsignedImmediate16;
|
||||
return true;
|
||||
}
|
||||
|
||||
destination = 0;
|
||||
highImmediate = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryGetAddi(uint word, out int destination, out int source, out int immediate)
|
||||
{
|
||||
var fields = new PpcWordFields(word);
|
||||
if (fields.PrimaryOpcode == 14)
|
||||
{
|
||||
destination = fields.GprField0;
|
||||
source = fields.GprField1;
|
||||
immediate = fields.SignedImmediate16;
|
||||
return true;
|
||||
}
|
||||
|
||||
destination = 0;
|
||||
source = 0;
|
||||
immediate = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryGetOri(uint word, out int source, out int destination, out uint immediate) =>
|
||||
TryGetLogicalImmediate(word, 24, out source, out destination, out immediate);
|
||||
|
||||
public static bool TryGetOris(uint word, out int source, out int destination, out uint immediate) =>
|
||||
TryGetLogicalImmediate(word, 25, out source, out destination, out immediate);
|
||||
|
||||
private static bool TryGetLogicalImmediate(
|
||||
uint word,
|
||||
int opcode,
|
||||
out int source,
|
||||
out int destination,
|
||||
out uint immediate)
|
||||
{
|
||||
var fields = new PpcWordFields(word);
|
||||
if (fields.PrimaryOpcode == opcode)
|
||||
{
|
||||
source = fields.GprField0;
|
||||
destination = fields.GprField1;
|
||||
immediate = fields.UnsignedImmediate16;
|
||||
return true;
|
||||
}
|
||||
|
||||
source = 0;
|
||||
destination = 0;
|
||||
immediate = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryGetOr(uint word, out int source, out int destination, out int otherSource)
|
||||
{
|
||||
var fields = new PpcWordFields(word);
|
||||
if (fields.PrimaryOpcode == 31 && fields.ExtendedOpcode == 444)
|
||||
{
|
||||
source = fields.GprField0;
|
||||
destination = fields.GprField1;
|
||||
otherSource = fields.GprField2;
|
||||
return true;
|
||||
}
|
||||
|
||||
source = 0;
|
||||
destination = 0;
|
||||
otherSource = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryGetRlwinm(
|
||||
uint word,
|
||||
out int source,
|
||||
out int destination,
|
||||
out int shift,
|
||||
out int maskBegin,
|
||||
out int maskEnd)
|
||||
{
|
||||
var fields = new PpcWordFields(word);
|
||||
if (fields.PrimaryOpcode == 21)
|
||||
{
|
||||
source = fields.GprField0;
|
||||
destination = fields.GprField1;
|
||||
shift = fields.GprField2;
|
||||
maskBegin = (int)((word >> 6) & 0x1Fu);
|
||||
maskEnd = (int)((word >> 1) & 0x1Fu);
|
||||
return true;
|
||||
}
|
||||
|
||||
source = 0;
|
||||
destination = 0;
|
||||
shift = 0;
|
||||
maskBegin = 0;
|
||||
maskEnd = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryGetMtspr(uint word, int expectedSpr, out int source)
|
||||
{
|
||||
var fields = new PpcWordFields(word);
|
||||
if (fields.PrimaryOpcode == 31 &&
|
||||
fields.ExtendedOpcode == 467 &&
|
||||
fields.Spr == expectedSpr)
|
||||
{
|
||||
source = fields.GprField0;
|
||||
return true;
|
||||
}
|
||||
|
||||
source = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Branch recognizers for raw image scanners. Separate linked/unlinked forms preserve the
|
||||
/// b/bl distinction and avoid accepting absolute or indirect branches.
|
||||
/// </summary>
|
||||
internal static class PpcControlFlow
|
||||
{
|
||||
private const uint RelativeBranchMask = 0xFC000003u;
|
||||
private const uint RelativeBranch = 0x48000000u;
|
||||
private const uint RelativeBranchLink = 0x48000001u;
|
||||
|
||||
public static bool IsRelativeUnlinkedBranch(uint word) =>
|
||||
(word & RelativeBranchMask) == RelativeBranch;
|
||||
|
||||
public static bool IsRelativeLinkedBranch(uint word) =>
|
||||
(word & RelativeBranchMask) == RelativeBranchLink;
|
||||
|
||||
public static bool TryDecodeRelativeBranchTarget(uint address, uint word, out uint target)
|
||||
{
|
||||
if (!IsRelativeUnlinkedBranch(word))
|
||||
{
|
||||
target = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
target = DecodeRelative24Target(address, word);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryDecodeRelativeBranchLinkTarget(uint address, uint word, out uint target)
|
||||
{
|
||||
if (!IsRelativeLinkedBranch(word))
|
||||
{
|
||||
target = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
target = DecodeRelative24Target(address, word);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryDecodeConditionalRelativeBranchTarget(uint address, uint word, out uint target)
|
||||
{
|
||||
var fields = new PpcWordFields(word);
|
||||
if (fields.PrimaryOpcode != 16 || (word & 0x2u) != 0)
|
||||
{
|
||||
target = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
var offset = word & 0xFFFCu;
|
||||
if ((offset & 0x8000u) != 0)
|
||||
{
|
||||
offset |= 0xFFFF0000u;
|
||||
}
|
||||
|
||||
target = unchecked((uint)((int)address + (int)offset));
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool MayChangeControlFlow(uint word) =>
|
||||
new PpcWordFields(word).PrimaryOpcode is 16 or 18 or 19;
|
||||
|
||||
public static bool IsReturn(uint word) => word == 0x4E800020u;
|
||||
|
||||
public static bool IsBctr(uint word) => word == 0x4E800420u;
|
||||
|
||||
private static uint DecodeRelative24Target(uint address, uint word)
|
||||
{
|
||||
var offset = word & 0x03FFFFFCu;
|
||||
if ((offset & 0x02000000u) != 0)
|
||||
{
|
||||
offset |= 0xFC000000u;
|
||||
}
|
||||
|
||||
return unchecked((uint)((int)address + (int)offset));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One conservative "which GPR can this raw word clobber" policy, shared by every raw scanner tracking
|
||||
/// a register across straight-line code. <c>ori</c>/<c>rlwinm</c>/<c>andi.</c> write RA (bits 16-20),
|
||||
/// not RS; every arm over-approximates on purpose, since "no" wrongly emits a patch at the wrong address.
|
||||
/// </summary>
|
||||
internal static class PpcRegisterEffects
|
||||
{
|
||||
/// <summary>
|
||||
/// True when this word can write <paramref name="register"/>. FPR/CR/memory destinations
|
||||
/// answer false for every GPR, except update forms which also write back to RA.
|
||||
/// </summary>
|
||||
public static bool MayWriteGpr(uint word, int register)
|
||||
{
|
||||
var fields = new PpcWordFields(word);
|
||||
var rt = fields.GprField0;
|
||||
var ra = fields.GprField1;
|
||||
return fields.PrimaryOpcode switch
|
||||
{
|
||||
// D-form arithmetic and plain loads: destination is RT.
|
||||
7 or 8 or 12 or 13 or 14 or 15 or 32 or 34 or 40 or 42 => rt == register,
|
||||
// Load with update: RT and the RA base are both written.
|
||||
33 or 35 or 41 or 43 => rt == register || ra == register,
|
||||
// D-form logical and rotate: destination is RA, not RS.
|
||||
20 or 21 or 23 or 24 or 25 or 26 or 27 or 28 or 29 => ra == register,
|
||||
// X-form: not worth splitting by sub-opcode, so treat RT or RA as clobbered.
|
||||
31 => rt == register || ra == register,
|
||||
// Store with update writes the RA base back.
|
||||
37 or 39 or 45 => ra == register,
|
||||
// lmw loads RT through r31.
|
||||
46 => register >= rt,
|
||||
// Float load/store with update writes the RA base back; the value
|
||||
// register is an FPR.
|
||||
49 or 51 or 53 or 55 or 57 or 61 => ra == register,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Translator.Core.Analysis;
|
||||
|
||||
namespace Translator.Core;
|
||||
|
||||
/// <summary>
|
||||
/// The one definition of every marker the translator writes into generated C++ and every macro
|
||||
/// spelling it reads back out of hand-written runtime C++. Emitter and parsers share these members
|
||||
/// so copies can't drift the way hand-duplicated literals once did, silently dropping a registration.
|
||||
/// </summary>
|
||||
public static partial class GeneratedMarkers
|
||||
{
|
||||
// ---------------------------------------------------------------------
|
||||
// Markers emitted into generated C++ (see CxxLinearCodeGenerator).
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>The guest ABI contract line, without its comment prefix.</summary>
|
||||
public static string GuestAbi(GuestAbiContract contract) =>
|
||||
$"RECOMP_GUEST_ABI gpr_read=0x{contract.GprReadBeforeWriteMask:X8} " +
|
||||
$"gpr_write=0x{contract.GprPossibleWriteMask:X8} " +
|
||||
$"gpr_return=0x{contract.GprReturnMask:X8} " +
|
||||
$"fpr_read=0x{contract.FprReadBeforeWriteMask:X8} " +
|
||||
$"fpr_write=0x{contract.FprPossibleWriteMask:X8} " +
|
||||
$"fpr_return=0x{contract.FprReturnMask:X8} " +
|
||||
$"cr_read=0x{contract.CrReadBeforeWriteMask:X2} " +
|
||||
$"cr_write=0x{contract.CrPossibleWriteMask:X2} " +
|
||||
$"xer_read={(contract.ReadsXerBeforeWrite ? 1 : 0)} " +
|
||||
$"xer_write={(contract.MayWriteXer ? 1 : 0)} " +
|
||||
$"fence={(contract.HasFullSynchronizationFence ? 1 : 0)}";
|
||||
|
||||
/// <summary>The registration facts of a base-translation function.</summary>
|
||||
public static string BaseRegistration(
|
||||
uint entryPoint,
|
||||
string symbol,
|
||||
bool preservesNonvolatileFprs,
|
||||
uint nonvolatileFprWriteMask) =>
|
||||
$"// RECOMP_REGISTRATION base 0x{entryPoint:X8} {symbol} " +
|
||||
$"preserves={Bool(preservesNonvolatileFprs)} fpr_mask=0x{nonvolatileFprWriteMask:X8}";
|
||||
|
||||
/// <summary>
|
||||
/// The registration facts of a mod overlay function. <paramref name="prettyName"/>
|
||||
/// is already escaped for a C++ string literal by the caller.
|
||||
/// </summary>
|
||||
public static string ModRegistration(
|
||||
uint entryPoint,
|
||||
string symbol,
|
||||
string prettyName,
|
||||
bool preservesNonvolatileFprs,
|
||||
uint nonvolatileFprWriteMask,
|
||||
uint priority,
|
||||
ulong moduleId) =>
|
||||
$"// RECOMP_REGISTRATION mod 0x{entryPoint:X8} {symbol} \"{prettyName}\" " +
|
||||
$"preserves={Bool(preservesNonvolatileFprs)} fpr_mask=0x{nonvolatileFprWriteMask:X8} " +
|
||||
$"priority={priority} module_id={moduleId}";
|
||||
|
||||
private static string Bool(bool value) => value ? "true" : "false";
|
||||
|
||||
[GeneratedRegex(@"RECOMP_GUEST_ABI gpr_read=0x(?<gr>[0-9A-Fa-f]{8}) gpr_write=0x(?<gw>[0-9A-Fa-f]{8}) gpr_return=0x(?<gret>[0-9A-Fa-f]{8}) fpr_read=0x(?<fr>[0-9A-Fa-f]{8}) fpr_write=0x(?<fw>[0-9A-Fa-f]{8}) fpr_return=0x(?<fret>[0-9A-Fa-f]{8}) cr_read=0x(?<crr>[0-9A-Fa-f]{2}) cr_write=0x(?<crw>[0-9A-Fa-f]{2}) xer_read=(?<xr>[01]) xer_write=(?<xw>[01]) fence=(?<fence>[01])", RegexOptions.CultureInvariant)]
|
||||
public static partial Regex GuestAbiPattern();
|
||||
|
||||
[GeneratedRegex(@"// RECOMP_REGISTRATION base 0x(?<address>[0-9A-Fa-f]{8}) (?<symbol>[A-Za-z_][A-Za-z0-9_]*) preserves=(?<preserves>true|false) fpr_mask=0x(?<mask>[0-9A-Fa-f]+)", RegexOptions.CultureInvariant)]
|
||||
public static partial Regex BaseRegistrationPattern();
|
||||
|
||||
/// <summary>
|
||||
/// Every group the emitter writes, module id included. A copy of this pattern
|
||||
/// used to stop at the priority field, so the resolved dispatch profile and
|
||||
/// the shard emitter disagreed about what a well-formed marker even was.
|
||||
/// </summary>
|
||||
[GeneratedRegex("""// RECOMP_REGISTRATION mod 0x(?<address>[0-9A-Fa-f]{8}) (?<symbol>[A-Za-z_][A-Za-z0-9_]*) "(?<name>[^"]*)" preserves=(?<preserves>true|false) fpr_mask=0x(?<mask>[0-9A-Fa-f]+) priority=(?<priority>[0-9]+) module_id=(?<moduleId>[0-9]+)""", RegexOptions.CultureInvariant)]
|
||||
public static partial Regex ModRegistrationPattern();
|
||||
|
||||
/// <summary>
|
||||
/// Matches a whole registration marker line. The marker is translator-internal
|
||||
/// metadata: a compiled shard has no use for it, so it is dropped rather than
|
||||
/// carried into tens of thousands of lines of shard text.
|
||||
/// </summary>
|
||||
[GeneratedRegex(@"^// RECOMP_REGISTRATION [^\r\n]*\r?\n?", RegexOptions.Multiline | RegexOptions.CultureInvariant)]
|
||||
public static partial Regex RegistrationLinePattern();
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Registration macros in hand-written runtime C++.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// <c>REGISTER_NATIVE_FUNCTION(0x…, symbol)</c> and its <c>_AS</c> alias form;
|
||||
/// the <c>as</c> group tells the two apart.
|
||||
/// </summary>
|
||||
[GeneratedRegex(@"REGISTER_NATIVE_FUNCTION(?<as>_AS)?\s*\(\s*0x(?<address>[0-9A-Fa-f]{8})\s*,\s*(?<symbol>[A-Za-z_][A-Za-z0-9_]*)", RegexOptions.CultureInvariant)]
|
||||
public static partial Regex NativeFunctionRegistrationPattern();
|
||||
|
||||
[GeneratedRegex(@"REGISTER_TRANSLATED_FUNCTION\s*\(\s*0x(?<address>[0-9A-Fa-f]{8})\s*,\s*(?<symbol>[A-Za-z_][A-Za-z0-9_]*)", RegexOptions.CultureInvariant)]
|
||||
public static partial Regex TranslatedFunctionRegistrationPattern();
|
||||
|
||||
/// <summary>
|
||||
/// The registration half of <c>PPC_NATIVE_OVERRIDE</c>: address and symbol
|
||||
/// only. Deliberately a prefix match, so an override whose argument list
|
||||
/// contains a semicolon still registers.
|
||||
/// </summary>
|
||||
[GeneratedRegex(@"PPC_NATIVE_OVERRIDE(?:_VOID)?\s*\(\s*(?<address>[0-9A-Fa-f]{8})\s*,\s*(?<symbol>[A-Za-z_][A-Za-z0-9_]*)", RegexOptions.CultureInvariant)]
|
||||
public static partial Regex NativeOverridePattern();
|
||||
|
||||
/// <summary>
|
||||
/// The same macro read for its signature rather than its registration: the
|
||||
/// effect analyzer needs the argument list, which only the full
|
||||
/// statement-terminated form can capture.
|
||||
/// </summary>
|
||||
[GeneratedRegex(@"PPC_NATIVE_OVERRIDE(?<void>_VOID)?\s*\(\s*(?<address>[0-9A-Fa-f]{8})\s*,\s*(?<symbol>[A-Za-z_][A-Za-z0-9_]*)\s*,?\s*(?<tail>[^;]*?)\);", RegexOptions.CultureInvariant | RegexOptions.Singleline)]
|
||||
public static partial Regex NativeOverrideSignaturePattern();
|
||||
|
||||
/// <summary>
|
||||
/// The void-stub form read for its declared host parameter list, which is
|
||||
/// what the void-stub ABI provider infers argument registers from.
|
||||
/// </summary>
|
||||
[GeneratedRegex(@"PPC_NATIVE_OVERRIDE_VOID\s*\(\s*(?<addr>[0-9A-Fa-f]+)\s*,\s*[^,]+,\s*\((?<args>[^)]*)\)\s*,", RegexOptions.CultureInvariant | RegexOptions.Singleline)]
|
||||
public static partial Regex NativeOverrideVoidArgumentsPattern();
|
||||
|
||||
[GeneratedRegex(@"GX_FATAL_STUB\s*\(\s*(?<address>[0-9A-Fa-f]{8})", RegexOptions.CultureInvariant)]
|
||||
public static partial Regex FatalStubPattern();
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Translator.Core;
|
||||
|
||||
/// <summary>
|
||||
/// The one place guest addresses are parsed out of text. Every spelling must be culture-invariant;
|
||||
/// a culture-sensitive parse is what broke translation under tr-TR once already.
|
||||
/// </summary>
|
||||
public static class GuestTargetParser
|
||||
{
|
||||
/// <summary>
|
||||
/// An IR call/jump target: <c>0x8000ABCD</c> or <c>func_8000ABCD</c>.
|
||||
/// Anything else - a runtime helper name, a native symbol - is deliberately
|
||||
/// not an address.
|
||||
/// </summary>
|
||||
public static bool TryParseAddress(string target, out uint address)
|
||||
{
|
||||
address = 0;
|
||||
if (string.IsNullOrWhiteSpace(target)) return false;
|
||||
|
||||
var trimmed = target.Trim();
|
||||
var hex = trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
||||
? trimmed.AsSpan(2)
|
||||
: trimmed.StartsWith("func_", StringComparison.OrdinalIgnoreCase)
|
||||
? trimmed.AsSpan(5)
|
||||
: default;
|
||||
return !hex.IsEmpty && TryParseHexDigits(hex, out address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// As <see cref="TryParseAddress"/>, but a target with neither prefix is read as a bare decimal,
|
||||
/// for the GQR propagation pass's synthetic edges spelled as plain numbers.
|
||||
/// </summary>
|
||||
public static bool TryParseAddressOrDecimal(string target, out uint address) =>
|
||||
TryParseAddress(target, out address) ||
|
||||
uint.TryParse(target, NumberStyles.Integer, CultureInfo.InvariantCulture, out address);
|
||||
|
||||
/// <summary>
|
||||
/// A hexadecimal guest address whose <c>0x</c> prefix is optional: the
|
||||
/// spelling used by CLI options, generated file names, marker groups and
|
||||
/// JSON records.
|
||||
/// </summary>
|
||||
public static bool TryParseHexAddress(string text, out uint address)
|
||||
{
|
||||
address = 0;
|
||||
if (string.IsNullOrWhiteSpace(text)) return false;
|
||||
|
||||
var hex = text.AsSpan().Trim();
|
||||
if (hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) hex = hex[2..];
|
||||
return TryParseHexDigits(hex, out address);
|
||||
}
|
||||
|
||||
/// <summary>Throwing form of <see cref="TryParseHexAddress"/>, for command-line arguments.</summary>
|
||||
public static uint ParseHexAddress(string text) =>
|
||||
TryParseHexAddress(text, out var address)
|
||||
? address
|
||||
: throw new FormatException($"'{text}' is not a hexadecimal guest address.");
|
||||
|
||||
/// <summary>
|
||||
/// A generated local-label name: <c>loc_8000ABCD</c>, or the raw
|
||||
/// <c>0x8000ABCD</c> spelling the emitter uses before a label is named.
|
||||
/// Returns null for any other identifier.
|
||||
/// </summary>
|
||||
public static uint? TryParseLocalLabelAddress(string label)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(label)) return null;
|
||||
|
||||
var trimmed = label.Trim();
|
||||
var hex = trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
||||
? trimmed.AsSpan(2)
|
||||
: trimmed.StartsWith("loc_", StringComparison.OrdinalIgnoreCase)
|
||||
? trimmed.AsSpan(4)
|
||||
: default;
|
||||
return !hex.IsEmpty && TryParseHexDigits(hex, out var address) ? address : null;
|
||||
}
|
||||
|
||||
private static bool TryParseHexDigits(ReadOnlySpan<char> hex, out uint address) =>
|
||||
uint.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out address);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Translator.Core.Loading;
|
||||
|
||||
namespace Translator.Core.IO;
|
||||
|
||||
internal sealed record AssemblyBlob(string FileName, string Symbol, ReadOnlyMemory<byte> Data, string Comment);
|
||||
|
||||
internal static class AssemblyBlobWriter
|
||||
{
|
||||
public static void Write(
|
||||
string assemblyPath,
|
||||
string blobDirectory,
|
||||
string blobReferenceDirectory,
|
||||
IReadOnlyList<AssemblyBlob> blobs,
|
||||
params string[] headerLines)
|
||||
{
|
||||
Directory.CreateDirectory(blobDirectory);
|
||||
var expectedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var assembly = new StringBuilder();
|
||||
foreach (var header in headerLines) assembly.AppendLine(header);
|
||||
assembly.AppendLine(".section .rdata,\"dr\"");
|
||||
assembly.AppendLine();
|
||||
|
||||
foreach (var blob in blobs)
|
||||
{
|
||||
var blobPath = Path.Combine(blobDirectory, blob.FileName);
|
||||
expectedFiles.Add(Path.GetFullPath(blobPath));
|
||||
FileOutput.WriteBytesIfChanged(blobPath, blob.Data.Span);
|
||||
var hash = ChecksumUtilities.Sha256Hex(blob.Data.Span);
|
||||
assembly.AppendLine($"// {blob.Comment}; sha256={hash}");
|
||||
assembly.AppendLine(".p2align 4");
|
||||
assembly.AppendLine($".globl {blob.Symbol}");
|
||||
assembly.AppendLine($"{blob.Symbol}:");
|
||||
var referencePath = Path.Combine(blobReferenceDirectory, blob.FileName);
|
||||
assembly.AppendLine($".incbin \"{SanitizeAssemblyPath(referencePath)}\"");
|
||||
assembly.AppendLine();
|
||||
}
|
||||
|
||||
foreach (var stalePath in Directory.EnumerateFiles(blobDirectory, "*.bin"))
|
||||
{
|
||||
if (!expectedFiles.Contains(Path.GetFullPath(stalePath))) File.Delete(stalePath);
|
||||
}
|
||||
FileOutput.WriteTextIfChanged(assemblyPath, assembly.ToString());
|
||||
}
|
||||
|
||||
private static string SanitizeAssemblyPath(string path) => Path.GetFullPath(path).Replace('\\', '/');
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace Translator.Core.IO;
|
||||
|
||||
internal sealed class BigEndianBinaryReader : IDisposable
|
||||
{
|
||||
private readonly BinaryReader _reader;
|
||||
|
||||
public BigEndianBinaryReader(Stream stream, bool leaveOpen = false)
|
||||
{
|
||||
_reader = new BinaryReader(stream, Encoding.ASCII, leaveOpen);
|
||||
}
|
||||
|
||||
public long Position => _reader.BaseStream.Position;
|
||||
|
||||
public void Seek(long offset, SeekOrigin origin) => _reader.BaseStream.Seek(offset, origin);
|
||||
|
||||
public byte ReadByte() => _reader.ReadByte();
|
||||
|
||||
public ushort ReadUInt16()
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[2];
|
||||
EnsureRead(buffer);
|
||||
return BinaryPrimitives.ReadUInt16BigEndian(buffer);
|
||||
}
|
||||
|
||||
public uint ReadUInt32()
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[4];
|
||||
EnsureRead(buffer);
|
||||
return BinaryPrimitives.ReadUInt32BigEndian(buffer);
|
||||
}
|
||||
|
||||
public byte[] ReadBytes(int count)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(count);
|
||||
var data = new byte[count];
|
||||
EnsureRead(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
private void EnsureRead(Span<byte> destination)
|
||||
{
|
||||
var start = Position;
|
||||
try
|
||||
{
|
||||
_reader.BaseStream.ReadExactly(destination);
|
||||
}
|
||||
catch (EndOfStreamException exception)
|
||||
{
|
||||
throw new EndOfStreamException(
|
||||
$"Expected {destination.Length} bytes at 0x{start:X}, but the stream ended early.",
|
||||
exception);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => _reader.Dispose();
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace Translator.Core.IO;
|
||||
|
||||
/// <summary>
|
||||
/// The one generated-file writer. Every write is content-gated (so an unchanged
|
||||
/// file keeps its modification time and Ninja does not rebuild the world) and
|
||||
/// atomic (so an interrupted run never leaves a truncated artifact behind).
|
||||
/// </summary>
|
||||
public static class FileOutput
|
||||
{
|
||||
private const int CompareBufferSize = 128 * 1024;
|
||||
|
||||
public static bool WriteTextIfChanged(string path, string content) =>
|
||||
WriteBytesIfChanged(path, Encoding.UTF8.GetBytes(content));
|
||||
|
||||
public static bool WriteBytesIfChanged(string path, ReadOnlySpan<byte> content)
|
||||
{
|
||||
var fullPath = Path.GetFullPath(path);
|
||||
if (ContentEquals(fullPath, content)) return false;
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
|
||||
var temporary = $"{fullPath}.tmp.{Guid.NewGuid():N}";
|
||||
using (var stream = new FileStream(
|
||||
temporary, FileMode.CreateNew, FileAccess.Write, FileShare.None, CompareBufferSize))
|
||||
{
|
||||
stream.Write(content);
|
||||
}
|
||||
File.Move(temporary, fullPath, overwrite: true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Byte-compares an existing file against in-memory content without materializing it, since
|
||||
/// generated payloads are hundreds of megabytes in aggregate.
|
||||
/// </summary>
|
||||
public static bool ContentEquals(string path, ReadOnlySpan<byte> content)
|
||||
{
|
||||
var info = new FileInfo(path);
|
||||
if (!info.Exists || info.Length != content.Length) return false;
|
||||
|
||||
using var stream = new FileStream(
|
||||
path, FileMode.Open, FileAccess.Read, FileShare.Read, CompareBufferSize, FileOptions.SequentialScan);
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(CompareBufferSize);
|
||||
try
|
||||
{
|
||||
var offset = 0;
|
||||
while (offset < content.Length)
|
||||
{
|
||||
var read = stream.Read(buffer, 0, Math.Min(buffer.Length, content.Length - offset));
|
||||
if (read <= 0) return false;
|
||||
if (!buffer.AsSpan(0, read).SequenceEqual(content.Slice(offset, read))) return false;
|
||||
offset += read;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Byte-compares two files with the same streamed, pooled-buffer strategy as
|
||||
/// <see cref="ContentEquals"/>, so publication and gating agree on what
|
||||
/// "identical" means and neither materialises a large generated file.
|
||||
/// </summary>
|
||||
public static bool FilesEqual(string left, string right)
|
||||
{
|
||||
var leftInfo = new FileInfo(left);
|
||||
var rightInfo = new FileInfo(right);
|
||||
if (leftInfo.Length != rightInfo.Length) return false;
|
||||
|
||||
using var leftStream = new FileStream(
|
||||
left, FileMode.Open, FileAccess.Read, FileShare.Read, CompareBufferSize, FileOptions.SequentialScan);
|
||||
using var rightStream = new FileStream(
|
||||
right, FileMode.Open, FileAccess.Read, FileShare.Read, CompareBufferSize, FileOptions.SequentialScan);
|
||||
var leftBuffer = ArrayPool<byte>.Shared.Rent(CompareBufferSize);
|
||||
var rightBuffer = ArrayPool<byte>.Shared.Rent(CompareBufferSize);
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var leftRead = leftStream.Read(leftBuffer, 0, CompareBufferSize);
|
||||
var rightRead = rightStream.Read(rightBuffer, 0, CompareBufferSize);
|
||||
if (leftRead != rightRead) return false;
|
||||
if (leftRead == 0) return true;
|
||||
if (!leftBuffer.AsSpan(0, leftRead).SequenceEqual(rightBuffer.AsSpan(0, rightRead))) return false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(leftBuffer);
|
||||
ArrayPool<byte>.Shared.Return(rightBuffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Translator.Core.IO;
|
||||
|
||||
/// <summary>
|
||||
/// The one JSON artifact writer and the only serializer options the translator owns.
|
||||
/// Publishes through <see cref="FileOutput.WriteBytesIfChanged"/>, so writes are content-gated and atomic.
|
||||
/// </summary>
|
||||
public static class JsonOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// Machine-read documents. Indentation only inflates files that already
|
||||
/// measure tens of megabytes and that no human opens.
|
||||
/// </summary>
|
||||
public static readonly JsonSerializerOptions Compact = new() { WriteIndented = false };
|
||||
|
||||
/// <summary>Machine-read documents whose schema is spelled in camelCase.</summary>
|
||||
public static readonly JsonSerializerOptions CompactCamelCase = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
/// <summary>Documents small enough - and read by people often enough - to indent.</summary>
|
||||
public static readonly JsonSerializerOptions Indented = new() { WriteIndented = true };
|
||||
|
||||
private static readonly byte[] TrailingNewLine = Encoding.UTF8.GetBytes(Environment.NewLine);
|
||||
|
||||
public static bool WriteIfChanged<T>(string path, T value, JsonSerializerOptions options)
|
||||
{
|
||||
var json = JsonSerializer.SerializeToUtf8Bytes(value, options);
|
||||
var content = new byte[json.Length + TrailingNewLine.Length];
|
||||
json.CopyTo(content, 0);
|
||||
TrailingNewLine.CopyTo(content, json.Length);
|
||||
return FileOutput.WriteBytesIfChanged(path, content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
namespace Translator.Core.IO;
|
||||
|
||||
/// <summary>
|
||||
/// Generates a complete directory tree away from the live output and publishes it only
|
||||
/// after the producer succeeds. Byte-identical files retain their live last-write time so
|
||||
/// incremental build systems do not rebuild unchanged generated sources.
|
||||
/// </summary>
|
||||
public static class TransactionalDirectoryOutput
|
||||
{
|
||||
private const int FileSystemRetryCount = 8;
|
||||
|
||||
public sealed record PublishResult(
|
||||
int AddedFiles,
|
||||
int UpdatedFiles,
|
||||
int RemovedFiles,
|
||||
int UnchangedFiles);
|
||||
|
||||
/// <summary>
|
||||
/// The producer's exit code, the publication counts when one happened, and any
|
||||
/// non-fatal cleanup warnings. Warnings are returned rather than printed: this
|
||||
/// is Core, and only the CLI writes to the console.
|
||||
/// </summary>
|
||||
public sealed record GenerateAndPublishResult(
|
||||
int ExitCode,
|
||||
PublishResult? Publication,
|
||||
IReadOnlyList<string> Warnings);
|
||||
|
||||
public static GenerateAndPublishResult GenerateAndPublish(
|
||||
string destinationDirectory,
|
||||
Func<string, int> producer,
|
||||
Action<string, int>? onProducerFailure = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(destinationDirectory);
|
||||
ArgumentNullException.ThrowIfNull(producer);
|
||||
|
||||
var destination = Path.GetFullPath(destinationDirectory);
|
||||
var parent = Directory.GetParent(destination)?.FullName
|
||||
?? throw new InvalidOperationException($"Output directory has no parent: {destination}");
|
||||
Directory.CreateDirectory(parent);
|
||||
|
||||
var leaf = Path.GetFileName(destination.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
||||
var staging = Path.Combine(parent, $".{leaf}.translate-mod-staging-{Guid.NewGuid():N}");
|
||||
// The same list instance is handed to the result record, so warnings the
|
||||
// staging cleanup below adds still reach the caller.
|
||||
var warnings = new List<string>();
|
||||
Directory.CreateDirectory(staging);
|
||||
|
||||
try
|
||||
{
|
||||
var producerResult = producer(staging);
|
||||
if (producerResult != 0)
|
||||
{
|
||||
onProducerFailure?.Invoke(staging, producerResult);
|
||||
return new GenerateAndPublishResult(producerResult, null, warnings);
|
||||
}
|
||||
|
||||
return new GenerateAndPublishResult(0, Publish(staging, destination, warnings), warnings);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(staging))
|
||||
{
|
||||
TryDeleteDirectory(staging, "staging output", warnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static PublishResult Publish(string staging, string destination, List<string> warnings)
|
||||
{
|
||||
var stagedFiles = EnumerateRelativeFiles(staging);
|
||||
var liveFiles = Directory.Exists(destination)
|
||||
? EnumerateRelativeFiles(destination)
|
||||
: new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var added = 0;
|
||||
var updated = 0;
|
||||
var unchanged = 0;
|
||||
foreach (var (relativePath, stagedPath) in stagedFiles)
|
||||
{
|
||||
if (!liveFiles.TryGetValue(relativePath, out var livePath))
|
||||
{
|
||||
added++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!FilesEqual(stagedPath, livePath))
|
||||
{
|
||||
updated++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ninja/MSBuild use last-write time to determine whether generated C++ must
|
||||
// be rebuilt. The clean stage is authoritative for membership, while the live
|
||||
// tree is authoritative for the timestamp of identical content.
|
||||
File.SetLastWriteTimeUtc(stagedPath, File.GetLastWriteTimeUtc(livePath));
|
||||
unchanged++;
|
||||
}
|
||||
|
||||
var removed = liveFiles.Keys.Count(path => !stagedFiles.ContainsKey(path));
|
||||
if (!Directory.Exists(destination))
|
||||
{
|
||||
MoveDirectoryWithRetry(staging, destination);
|
||||
return new PublishResult(added, updated, removed, unchanged);
|
||||
}
|
||||
|
||||
var parent = Directory.GetParent(destination)!.FullName;
|
||||
var leaf = Path.GetFileName(destination.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
||||
var backup = Path.Combine(parent, $".{leaf}.translate-mod-backup-{Guid.NewGuid():N}");
|
||||
|
||||
MoveDirectoryWithRetry(destination, backup);
|
||||
try
|
||||
{
|
||||
MoveDirectoryWithRetry(staging, destination);
|
||||
}
|
||||
catch (Exception publishError)
|
||||
{
|
||||
if (!Directory.Exists(destination) && Directory.Exists(backup))
|
||||
{
|
||||
try
|
||||
{
|
||||
MoveDirectoryWithRetry(backup, destination);
|
||||
}
|
||||
catch (Exception rollbackError)
|
||||
{
|
||||
throw new AggregateException(
|
||||
$"Failed to publish generated output and restore last-known-good directory {destination}.",
|
||||
publishError,
|
||||
rollbackError);
|
||||
}
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
||||
// Publication already succeeded. A stale backup is safe and should not make a
|
||||
// correct generated tree appear to have failed.
|
||||
TryDeleteDirectory(backup, "prior output backup", warnings);
|
||||
|
||||
return new PublishResult(added, updated, removed, unchanged);
|
||||
}
|
||||
|
||||
private static void MoveDirectoryWithRetry(string source, string destination)
|
||||
{
|
||||
RetryTransientFileSystemAction(
|
||||
() => Directory.Move(source, destination),
|
||||
$"move directory '{source}' to '{destination}'");
|
||||
}
|
||||
|
||||
private static void TryDeleteDirectory(string path, string description, List<string> warnings)
|
||||
{
|
||||
try
|
||||
{
|
||||
RetryTransientFileSystemAction(
|
||||
() => Directory.Delete(path, recursive: true),
|
||||
$"delete {description} '{path}'");
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
warnings.Add($"failed to delete {description} {path}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RetryTransientFileSystemAction(Action action, string description)
|
||||
{
|
||||
for (var attempt = 1; ; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
return;
|
||||
}
|
||||
catch (Exception ex) when ((ex is IOException || ex is UnauthorizedAccessException) &&
|
||||
attempt < FileSystemRetryCount)
|
||||
{
|
||||
// Antivirus/indexer/build-system handles can briefly prevent directory
|
||||
// renames on Windows. Use bounded backoff; persistent failures still
|
||||
// trigger the last-known-good rollback above.
|
||||
Thread.Sleep(50 * attempt);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
|
||||
{
|
||||
throw new IOException(
|
||||
$"Unable to {description} after {FileSystemRetryCount} attempts.",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> EnumerateRelativeFiles(string root) =>
|
||||
Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories)
|
||||
.ToDictionary(
|
||||
path => Path.GetRelativePath(root, path),
|
||||
path => path,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static bool FilesEqual(string left, string right) => FileOutput.FilesEqual(left, right);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Translator.Core.Ir;
|
||||
|
||||
public abstract record IrNode(string Kind);
|
||||
|
||||
public abstract record IrInstruction(string Kind) : IrNode(Kind);
|
||||
|
||||
public sealed record IrAssign(string Destination, IrValue Value) : IrInstruction("assign");
|
||||
|
||||
public sealed record IrBinary(string Destination, IrValue Left, IrValue Right, string Op) : IrInstruction("binary");
|
||||
|
||||
public sealed record IrLoad(string Destination, IrAddress Address, int SizeBytes) : IrInstruction("load");
|
||||
|
||||
public sealed record IrStore(IrAddress Address, IrValue Source, int SizeBytes) : IrInstruction("store");
|
||||
|
||||
/// <summary>
|
||||
/// Proves that [Base + MinOffset, Base + MinOffset + Length) is a contiguous
|
||||
/// mapped guest-RAM range and materializes its host pointer. A null host value
|
||||
/// records a failed proof and makes resolved accesses use their exact slow path.
|
||||
/// </summary>
|
||||
public sealed record IrResolveGuestMemoryRange(
|
||||
string Destination,
|
||||
IrValue Base,
|
||||
int MinOffset,
|
||||
int Length,
|
||||
bool NeedsReadAccess,
|
||||
bool NeedsWriteAccess) : IrInstruction("resolve_guest_memory_range");
|
||||
|
||||
public sealed record IrResolvedLoad(
|
||||
string Destination,
|
||||
string Range,
|
||||
IrAddress OriginalAddress,
|
||||
int RangeOffset,
|
||||
int SizeBytes) : IrInstruction("resolved_load");
|
||||
|
||||
public sealed record IrResolvedStore(
|
||||
string Range,
|
||||
IrAddress OriginalAddress,
|
||||
int RangeOffset,
|
||||
IrValue Source,
|
||||
int SizeBytes) : IrInstruction("resolved_store");
|
||||
|
||||
public sealed record IrResolvedPsqLoad(
|
||||
string Destination, string Range, IrValue OriginalAddress, int RangeOffset,
|
||||
uint W, uint I, uint? KnownGqr, bool GuardKnownGqr = false) : IrInstruction("resolved_psq_load");
|
||||
|
||||
public sealed record IrResolvedPsqStore(
|
||||
string Range, IrValue OriginalAddress, int RangeOffset, IrValue Source,
|
||||
uint W, uint I, uint? KnownGqr, bool GuardKnownGqr = false) : IrInstruction("resolved_psq_store");
|
||||
|
||||
public sealed record IrResolvedLoadPair(
|
||||
string FirstDestination, string SecondDestination, string Range,
|
||||
IrAddress FirstOriginalAddress, IrAddress SecondOriginalAddress,
|
||||
int RangeOffset, int ElementSizeBytes, bool Descending = false) : IrInstruction("resolved_load_pair");
|
||||
|
||||
public sealed record IrResolvedStorePair(
|
||||
string Range, IrAddress FirstOriginalAddress, IrAddress SecondOriginalAddress,
|
||||
int RangeOffset, IrValue FirstSource, IrValue SecondSource, int ElementSizeBytes,
|
||||
bool Descending = false)
|
||||
: IrInstruction("resolved_store_pair");
|
||||
|
||||
public sealed record IrCall(string Destination, string Target, IReadOnlyList<IrValue> Arguments) : IrInstruction("call");
|
||||
|
||||
/// <summary>
|
||||
/// Indirect call through a register or computed address (e.g., blrl/bctrl).
|
||||
/// Destination captures the link register write or return value slot, mirroring IrCall semantics.
|
||||
/// </summary>
|
||||
public sealed record IrIndirectCall(string Destination, IrValue Target, IReadOnlyList<IrValue> Arguments) : IrInstruction("indirect_call");
|
||||
|
||||
public sealed record IrSetCrField(int FieldIndex, IrValue Left, IrValue Right, bool IsUnsigned) : IrInstruction("set_cr_field");
|
||||
|
||||
/// <summary>
|
||||
/// SSA merge of values coming from predecessor blocks.
|
||||
/// Sources map predecessor label -> SSA value name.
|
||||
/// </summary>
|
||||
public sealed record IrPhi(string Destination, IReadOnlyDictionary<string, string> Sources) : IrInstruction("phi");
|
||||
|
||||
public sealed record IrBranch(string Condition, string TrueLabel, string FalseLabel, string ConditionRegister = "cr0") : IrInstruction("branch");
|
||||
|
||||
public sealed record IrJump(string TargetLabel) : IrInstruction("jump");
|
||||
|
||||
/// <summary>
|
||||
/// Indirect jump through a register (e.g., bctr without link).
|
||||
/// Unlike IrIndirectCall, this represents an intra-function jump (e.g., switch statement)
|
||||
/// and should not perform a function call.
|
||||
/// </summary>
|
||||
public sealed record IrIndirectJump(IrValue Target) : IrInstruction("indirect_jump");
|
||||
|
||||
/// <summary>
|
||||
/// Jump table produced from a recognized switch statement. Selector is the register
|
||||
/// holding the computed target address, and each case maps a literal address to a
|
||||
/// basic block label within the same function.
|
||||
/// </summary>
|
||||
public sealed record IrJumpTable(string Selector, IReadOnlyList<IrJumpTableCase> Cases) : IrInstruction("jump_table");
|
||||
|
||||
public sealed record IrJumpTableCase(uint TargetAddress, string TargetLabel);
|
||||
|
||||
public sealed record IrReturn(IrValue? Value) : IrInstruction("return");
|
||||
|
||||
public sealed record IrComment(string Text) : IrInstruction("comment");
|
||||
|
||||
public sealed record IrTracePpc(uint Address, string Disassembly, string RawHex)
|
||||
: IrInstruction("trace_ppc");
|
||||
|
||||
/// <summary>
|
||||
/// Marks an instruction as intentionally untranslatable in the current runtime model.
|
||||
/// Codegen should emit a runtime UNDEFINED trap that reports PC + raw opcode.
|
||||
/// </summary>
|
||||
public sealed record IrUndefined(uint Address, uint RawInstruction, string Disassembly, string? Reason = null)
|
||||
: IrInstruction("undefined");
|
||||
|
||||
public sealed record IrAddress(string Base, int Offset);
|
||||
|
||||
public sealed record IrValue(string Kind, string? RegisterName = null, long? Constant = null)
|
||||
{
|
||||
public static IrValue Register(string name) => new("register", RegisterName: name);
|
||||
public static IrValue Imm(long value) => new("const", Constant: value);
|
||||
}
|
||||
|
||||
public sealed record IrBasicBlock(string Label, IReadOnlyList<IrInstruction> Instructions);
|
||||
|
||||
public sealed record IrFunction(string Name, string EntryLabel, IReadOnlyList<IrBasicBlock> Blocks);
|
||||
@@ -0,0 +1,184 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Translator.Core.Ir;
|
||||
|
||||
internal static class IrRegisterDataFlow
|
||||
{
|
||||
public static IEnumerable<string> Uses(IrInstruction instruction)
|
||||
{
|
||||
switch (instruction)
|
||||
{
|
||||
case IrAssign assign when TryRegister(assign.Value, out var source):
|
||||
yield return source;
|
||||
break;
|
||||
case IrBinary binary:
|
||||
if (TryRegister(binary.Left, out var left)) yield return left;
|
||||
if (TryRegister(binary.Right, out var right)) yield return right;
|
||||
break;
|
||||
case IrLoad load:
|
||||
yield return load.Address.Base;
|
||||
break;
|
||||
case IrStore store:
|
||||
yield return store.Address.Base;
|
||||
if (TryRegister(store.Source, out var stored)) yield return stored;
|
||||
break;
|
||||
case IrResolveGuestMemoryRange resolve when TryRegister(resolve.Base, out var rangeBase):
|
||||
yield return rangeBase;
|
||||
break;
|
||||
case IrResolvedLoad load:
|
||||
yield return load.OriginalAddress.Base;
|
||||
break;
|
||||
case IrResolvedStore store:
|
||||
yield return store.OriginalAddress.Base;
|
||||
if (TryRegister(store.Source, out var resolvedStored)) yield return resolvedStored;
|
||||
break;
|
||||
case IrResolvedPsqLoad load:
|
||||
if (TryRegister(load.OriginalAddress, out var psqLoadAddress)) yield return psqLoadAddress;
|
||||
if (load.KnownGqr is null || load.GuardKnownGqr) yield return $"gqr{load.I}";
|
||||
break;
|
||||
case IrResolvedPsqStore store:
|
||||
if (TryRegister(store.OriginalAddress, out var psqStoreAddress)) yield return psqStoreAddress;
|
||||
if (TryRegister(store.Source, out var psqStored)) yield return psqStored;
|
||||
if (store.KnownGqr is null || store.GuardKnownGqr) yield return $"gqr{store.I}";
|
||||
break;
|
||||
case IrResolvedLoadPair pair:
|
||||
yield return pair.FirstOriginalAddress.Base;
|
||||
yield return pair.SecondOriginalAddress.Base;
|
||||
break;
|
||||
case IrResolvedStorePair pair:
|
||||
yield return pair.FirstOriginalAddress.Base;
|
||||
yield return pair.SecondOriginalAddress.Base;
|
||||
if (TryRegister(pair.FirstSource, out var firstStored)) yield return firstStored;
|
||||
if (TryRegister(pair.SecondSource, out var secondStored)) yield return secondStored;
|
||||
break;
|
||||
case IrCall call:
|
||||
foreach (var argument in call.Arguments)
|
||||
if (TryRegister(argument, out var name)) yield return name;
|
||||
break;
|
||||
case IrIndirectCall call:
|
||||
if (TryRegister(call.Target, out var target)) yield return target;
|
||||
foreach (var argument in call.Arguments)
|
||||
if (TryRegister(argument, out var name)) yield return name;
|
||||
break;
|
||||
case IrIndirectJump jump when TryRegister(jump.Target, out var jumpTarget):
|
||||
yield return jumpTarget;
|
||||
break;
|
||||
case IrReturn ret when ret.Value is not null && TryRegister(ret.Value, out var returned):
|
||||
yield return returned;
|
||||
break;
|
||||
case IrBranch branch when IsRegisterName(branch.ConditionRegister):
|
||||
yield return branch.ConditionRegister;
|
||||
break;
|
||||
case IrPhi phi:
|
||||
foreach (var source in phi.Sources.Values) yield return source;
|
||||
break;
|
||||
case IrSetCrField setCr:
|
||||
if (TryRegister(setCr.Left, out var compareLeft)) yield return compareLeft;
|
||||
if (TryRegister(setCr.Right, out var compareRight)) yield return compareRight;
|
||||
yield return "xer";
|
||||
break;
|
||||
case IrJumpTable table:
|
||||
yield return table.Selector;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<string> Definitions(IrInstruction instruction)
|
||||
{
|
||||
switch (instruction)
|
||||
{
|
||||
case IrAssign assign:
|
||||
yield return assign.Destination;
|
||||
break;
|
||||
case IrBinary binary:
|
||||
yield return binary.Destination;
|
||||
break;
|
||||
case IrLoad load:
|
||||
yield return load.Destination;
|
||||
break;
|
||||
case IrResolveGuestMemoryRange resolve:
|
||||
yield return resolve.Destination;
|
||||
break;
|
||||
case IrResolvedLoad load:
|
||||
yield return load.Destination;
|
||||
break;
|
||||
case IrResolvedPsqLoad load:
|
||||
yield return load.Destination;
|
||||
break;
|
||||
case IrResolvedLoadPair pair:
|
||||
yield return pair.FirstDestination;
|
||||
yield return pair.SecondDestination;
|
||||
break;
|
||||
case IrCall call when !string.IsNullOrWhiteSpace(call.Destination):
|
||||
yield return call.Destination;
|
||||
break;
|
||||
case IrIndirectCall call when !string.IsNullOrWhiteSpace(call.Destination):
|
||||
yield return call.Destination;
|
||||
break;
|
||||
case IrPhi phi:
|
||||
yield return phi.Destination;
|
||||
break;
|
||||
case IrSetCrField setCr:
|
||||
yield return $"cr{setCr.FieldIndex}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsRegisterName(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name)) return false;
|
||||
|
||||
var baseName = BaseName(name);
|
||||
if (baseName.Length is >= 2 and <= 3 &&
|
||||
(baseName[0] == 'r' || baseName[0] == 'f') &&
|
||||
int.TryParse(baseName.AsSpan(1), out var register) && register is >= 0 and < 32)
|
||||
return true;
|
||||
|
||||
if (baseName.Length == 3 &&
|
||||
baseName.StartsWith("cr", StringComparison.OrdinalIgnoreCase) &&
|
||||
char.IsDigit(baseName[2]) && (baseName[2] - '0') is >= 0 and <= 7)
|
||||
return true;
|
||||
|
||||
if (baseName.Length is >= 4 and <= 5 &&
|
||||
baseName.StartsWith("crb", StringComparison.OrdinalIgnoreCase) &&
|
||||
int.TryParse(baseName.AsSpan(3), out var crBit) && crBit is >= 0 and < 32)
|
||||
return true;
|
||||
|
||||
if (baseName.Length == 4 && baseName.StartsWith("gqr", StringComparison.OrdinalIgnoreCase) &&
|
||||
baseName[3] is >= '0' and <= '7')
|
||||
return true;
|
||||
|
||||
return baseName.Equals("cr", StringComparison.OrdinalIgnoreCase) ||
|
||||
baseName.Equals("lr", StringComparison.OrdinalIgnoreCase) ||
|
||||
baseName.Equals("ctr", StringComparison.OrdinalIgnoreCase) ||
|
||||
baseName.Equals("xer", StringComparison.OrdinalIgnoreCase) ||
|
||||
baseName.Equals("fpscr", StringComparison.OrdinalIgnoreCase) ||
|
||||
baseName.Equals("msr", StringComparison.OrdinalIgnoreCase) ||
|
||||
baseName.Equals("dar", StringComparison.OrdinalIgnoreCase) ||
|
||||
baseName.Equals("dsisr", StringComparison.OrdinalIgnoreCase) ||
|
||||
baseName.Equals("iccr", StringComparison.OrdinalIgnoreCase) ||
|
||||
baseName.Equals("tbr", StringComparison.OrdinalIgnoreCase) ||
|
||||
baseName.Equals("tbl", StringComparison.OrdinalIgnoreCase) ||
|
||||
baseName.Equals("tbu", StringComparison.OrdinalIgnoreCase) ||
|
||||
baseName.Equals("hid0", StringComparison.OrdinalIgnoreCase) ||
|
||||
baseName.Equals("hid1", StringComparison.OrdinalIgnoreCase) ||
|
||||
baseName.Equals("hid2", StringComparison.OrdinalIgnoreCase) ||
|
||||
baseName.Equals("srr0", StringComparison.OrdinalIgnoreCase) ||
|
||||
baseName.Equals("srr1", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the architectural/SSA base spelling for a value name. Only a numeric suffix is an SSA
|
||||
/// version, so lifter temporaries like <c>r3_addc_left</c> stay temporaries instead of being
|
||||
/// mistaken for architectural <c>r3</c>.
|
||||
/// </summary>
|
||||
public static string BaseName(string name) =>
|
||||
RegisterNameUtils.StripNumericSuffix(name);
|
||||
|
||||
private static bool TryRegister(IrValue value, out string register)
|
||||
{
|
||||
register = value.RegisterName ?? string.Empty;
|
||||
return value.Kind == "register" && register.Length > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using Translator.Core.Disassembly;
|
||||
using Translator.Core.Ir;
|
||||
|
||||
namespace Translator.Core.Lifting;
|
||||
|
||||
public sealed record LiftedInstruction(PpcInstruction Origin, IReadOnlyList<IrInstruction> Ir);
|
||||
@@ -0,0 +1,274 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Translator.Core.Disassembly;
|
||||
using Translator.Core.Ir;
|
||||
|
||||
namespace Translator.Core.Lifting;
|
||||
|
||||
public sealed partial class PpcLifter
|
||||
{
|
||||
private static IReadOnlyList<IrInstruction>? TryLiftFloating(
|
||||
PpcInstruction ins,
|
||||
string mnemonic,
|
||||
IReadOnlyList<PpcOperand> ops,
|
||||
IReadOnlyList<string> operands)
|
||||
{
|
||||
switch (mnemonic)
|
||||
{
|
||||
case "mffs":
|
||||
case "mffs.":
|
||||
case "fp_583":
|
||||
return LiftMffs(ins);
|
||||
|
||||
case "mtfsf":
|
||||
case "mtfsf.":
|
||||
case "fp_711":
|
||||
return LiftMtfsf(ins);
|
||||
|
||||
case "mtfsfi":
|
||||
case "mtfsfi.":
|
||||
case "fp_134":
|
||||
return LiftMtfsfi(ins);
|
||||
|
||||
case "mtfsb0":
|
||||
case "mtfsb0.":
|
||||
case "fp_70":
|
||||
return LiftMtfsb0(ins);
|
||||
|
||||
case "mtfsb1":
|
||||
case "mtfsb1.":
|
||||
case "fp_38":
|
||||
return LiftMtfsb1(ins);
|
||||
|
||||
case "fadd" when operands.Count == 3:
|
||||
case "fadd." when operands.Count == 3:
|
||||
return new[] { new IrBinary(operands[0], IrValue.Register(operands[1]), IrValue.Register(operands[2]), "add") };
|
||||
|
||||
case "fsub" when operands.Count == 3:
|
||||
case "fsub." when operands.Count == 3:
|
||||
return new[] { new IrBinary(operands[0], IrValue.Register(operands[1]), IrValue.Register(operands[2]), "sub") };
|
||||
|
||||
case "fmul" when operands.Count == 3:
|
||||
case "fmul." when operands.Count == 3:
|
||||
return new[] { new IrBinary(operands[0], IrValue.Register(operands[1]), IrValue.Register(operands[2]), "mul") };
|
||||
|
||||
case "fdiv" when operands.Count == 3:
|
||||
case "fdiv." when operands.Count == 3:
|
||||
return new[] { new IrBinary(operands[0], IrValue.Register(operands[1]), IrValue.Register(operands[2]), "fdiv") };
|
||||
|
||||
case "fadds" when operands.Count == 3:
|
||||
case "fadds." when operands.Count == 3:
|
||||
return new[] { new IrCall(operands[0], "PPC_Fadds", new[] { IrValue.Register(operands[1]), IrValue.Register(operands[2]) }) };
|
||||
|
||||
case "fsubs" when operands.Count == 3:
|
||||
case "fsubs." when operands.Count == 3:
|
||||
return new[] { new IrCall(operands[0], "PPC_Fsubs", new[] { IrValue.Register(operands[1]), IrValue.Register(operands[2]) }) };
|
||||
|
||||
case "fmuls" when operands.Count == 3:
|
||||
case "fmuls." when operands.Count == 3:
|
||||
return new[] { new IrCall(operands[0], "PPC_Fmuls", new[] { IrValue.Register(operands[1]), IrValue.Register(operands[2]) }) };
|
||||
|
||||
case "fdivs" when operands.Count == 3:
|
||||
case "fdivs." when operands.Count == 3:
|
||||
return new[] { new IrCall(operands[0], "PPC_Fdivs", new[] { IrValue.Register(operands[1]), IrValue.Register(operands[2]) }) };
|
||||
|
||||
case "fres" when operands.Count == 2:
|
||||
case "fres." when operands.Count == 2:
|
||||
return new[] { new IrCall(operands[0], "PPC_Fres", new[] { IrValue.Register(operands[1]) }) };
|
||||
|
||||
case "fmsubs" when operands.Count == 4:
|
||||
case "fmsubs." when operands.Count == 4:
|
||||
return new[] { new IrCall(operands[0], "PPC_Fmsubs", new[] { IrValue.Register(operands[1]), IrValue.Register(operands[2]), IrValue.Register(operands[3]) }) };
|
||||
|
||||
case "fmadds" when operands.Count == 4:
|
||||
case "fmadds." when operands.Count == 4:
|
||||
return new[] { new IrCall(operands[0], "PPC_Fmadds", new[] { IrValue.Register(operands[1]), IrValue.Register(operands[2]), IrValue.Register(operands[3]) }) };
|
||||
|
||||
case "fnmsubs" when operands.Count == 4:
|
||||
case "fnmsubs." when operands.Count == 4:
|
||||
return new[] { new IrCall(operands[0], "PPC_Fnmsubs", new[] { IrValue.Register(operands[1]), IrValue.Register(operands[2]), IrValue.Register(operands[3]) }) };
|
||||
|
||||
case "fnmadds" when operands.Count == 4:
|
||||
case "fnmadds." when operands.Count == 4:
|
||||
return new[] { new IrCall(operands[0], "PPC_Fnmadds", new[] { IrValue.Register(operands[1]), IrValue.Register(operands[2]), IrValue.Register(operands[3]) }) };
|
||||
|
||||
case "fp_57":
|
||||
{
|
||||
// fp_57 corresponds to Opcode 63 with bits 21-30 = 57.
|
||||
// This splits into C=1 and XO=25 (fmul).
|
||||
// We decode as A-Form to be safe.
|
||||
uint raw1 = ReadRawInstruction(ins);
|
||||
int rD = (int)((raw1 >> 21) & 0x1F);
|
||||
int rA = (int)((raw1 >> 16) & 0x1F);
|
||||
int rC = (int)((raw1 >> 6) & 0x1F);
|
||||
// XO is at bits 26-30 (raw >> 1 & 0x1F).
|
||||
// We assume fmul based on the ID, but decoding standard A-form logic works too.
|
||||
return new[] { new IrBinary($"f{rD}", IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}"), "mul") };
|
||||
}
|
||||
|
||||
case "frsp":
|
||||
case "frsp.":
|
||||
case "fp_12":
|
||||
{
|
||||
string dest, src;
|
||||
// Handle cases where disassembler fallback (fp_12) might not provide parsed operands
|
||||
if (operands.Count >= 2)
|
||||
{
|
||||
dest = operands[0];
|
||||
src = operands[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
uint raw1 = ReadRawInstruction(ins);
|
||||
int rD = (int)((raw1 >> 21) & 0x1F);
|
||||
int rB = (int)((raw1 >> 11) & 0x1F);
|
||||
dest = $"f{rD}";
|
||||
src = $"f{rB}";
|
||||
}
|
||||
|
||||
// frsp is a unary operation, mapped here as a binary op with a dummy 0 immediate
|
||||
// similar to how fneg/fabs are often handled in this lifter.
|
||||
return new[] { new IrBinary(dest, IrValue.Register(src), IrValue.Imm(0), "frsp") };
|
||||
}
|
||||
case "fp_281":
|
||||
{
|
||||
// fp_281 is fmul with rC=8.
|
||||
// 281 = (8 << 5) | 25. XO 25 is fmul.
|
||||
uint raw1 = ReadRawInstruction(ins);
|
||||
int rD = (int)((raw1 >> 21) & 0x1F);
|
||||
int rA = (int)((raw1 >> 16) & 0x1F);
|
||||
int rC = (int)((raw1 >> 6) & 0x1F);
|
||||
return new[] { new IrBinary($"f{rD}", IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}"), "mul") };
|
||||
}
|
||||
case "fneg" when operands.Count == 2:
|
||||
case "fneg." when operands.Count == 2:
|
||||
return new[] { new IrBinary(operands[0], IrValue.Register(operands[1]), IrValue.Imm(0), "fneg") };
|
||||
|
||||
case "fabs" when operands.Count == 2:
|
||||
case "fabs." when operands.Count == 2:
|
||||
return new[] { new IrBinary(operands[0], IrValue.Register(operands[1]), IrValue.Imm(0), "fabs") };
|
||||
|
||||
case "fnabs" when operands.Count == 2:
|
||||
case "fnabs." when operands.Count == 2:
|
||||
return new[]
|
||||
{
|
||||
new IrBinary(operands[0], IrValue.Register(operands[1]), IrValue.Imm(0), "fabs"),
|
||||
new IrBinary(operands[0], IrValue.Register(operands[0]), IrValue.Imm(0), "fneg")
|
||||
};
|
||||
|
||||
case "fmr" when operands.Count == 2:
|
||||
case "fmr." when operands.Count == 2:
|
||||
return new[] { new IrAssign(operands[0], IrValue.Register(operands[1])) };
|
||||
|
||||
case "fctiw" when operands.Count == 2:
|
||||
case "fctiw." when operands.Count == 2:
|
||||
return new[] { new IrBinary(operands[0], IrValue.Register(operands[1]), IrValue.Imm(0), "fctiw") };
|
||||
|
||||
case "fctiwz" when operands.Count == 2:
|
||||
case "fctiwz." when operands.Count == 2:
|
||||
return new[] { new IrBinary(operands[0], IrValue.Register(operands[1]), IrValue.Imm(0), "fctiwz") };
|
||||
|
||||
case "fsel" when operands.Count == 4:
|
||||
case "fsel." when operands.Count == 4:
|
||||
return new[] { new IrCall(operands[0], "PPC_Fsel", new[] { IrValue.Register(operands[1]), IrValue.Register(operands[3]), IrValue.Register(operands[2]) }) };
|
||||
|
||||
case "frsqrte" when operands.Count == 2:
|
||||
case "frsqrte." when operands.Count == 2:
|
||||
return new[] { new IrCall(operands[0], "PPC_Frsqrte", new[] { IrValue.Register(operands[1]) }) };
|
||||
|
||||
case "fmsub" when operands.Count == 4:
|
||||
case "fmsub." when operands.Count == 4:
|
||||
return new[] { new IrCall(operands[0], "PPC_Fmsub", new[] { IrValue.Register(operands[1]), IrValue.Register(operands[2]), IrValue.Register(operands[3]) }) };
|
||||
|
||||
case "fmadd" when operands.Count == 4:
|
||||
case "fmadd." when operands.Count == 4:
|
||||
return new[] { new IrCall(operands[0], "PPC_Fmadd", new[] { IrValue.Register(operands[1]), IrValue.Register(operands[2]), IrValue.Register(operands[3]) }) };
|
||||
|
||||
case "fnmsub" when operands.Count == 4:
|
||||
case "fnmsub." when operands.Count == 4:
|
||||
return new[] { new IrCall(operands[0], "PPC_Fnmsub", new[] { IrValue.Register(operands[1]), IrValue.Register(operands[2]), IrValue.Register(operands[3]) }) };
|
||||
|
||||
case "fnmadd" when operands.Count == 4:
|
||||
case "fnmadd." when operands.Count == 4:
|
||||
return new[] { new IrCall(operands[0], "PPC_Fnmadd", new[] { IrValue.Register(operands[1]), IrValue.Register(operands[2]), IrValue.Register(operands[3]) }) };
|
||||
|
||||
case "fcmpu" when operands.Count == 3:
|
||||
case "fcmpo" when operands.Count == 3:
|
||||
{
|
||||
// Model the CR write explicitly so leaf/resident register caches update
|
||||
// the authoritative CR storage instead of a stale CpuContext shadow.
|
||||
var destCr = ParseCrFieldIndex(operands[0]);
|
||||
return new[]
|
||||
{
|
||||
new IrSetCrField(destCr, IrValue.Register(operands[1]),
|
||||
IrValue.Register(operands[2]), false)
|
||||
};
|
||||
}
|
||||
|
||||
case "opc_59":
|
||||
{
|
||||
uint raw1 = ReadRawInstruction(ins);
|
||||
// Decode fields for A-Form instruction
|
||||
// XO is at bits 26-30 (bits 1-5 in our raw uint from LSB)
|
||||
int xo = (int)((raw1 >> 1) & 0x1F);
|
||||
int rD = (int)((raw1 >> 21) & 0x1F);
|
||||
int rA = (int)((raw1 >> 16) & 0x1F);
|
||||
int rB = (int)((raw1 >> 11) & 0x1F);
|
||||
int rC = (int)((raw1 >> 6) & 0x1F);
|
||||
|
||||
return xo switch
|
||||
{
|
||||
18 => new[] { new IrCall($"f{rD}", "PPC_Fdivs", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rB}") }) }, // fdivs
|
||||
20 => new[] { new IrCall($"f{rD}", "PPC_Fsubs", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rB}") }) }, // fsubs
|
||||
21 => new[] { new IrCall($"f{rD}", "PPC_Fadds", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rB}") }) }, // fadds
|
||||
22 => new[] { new IrCall($"f{rD}", "PPC_Fsqrt", new[] { IrValue.Register($"f{rB}") }) }, // fsqrts
|
||||
23 => new[] { new IrCall($"f{rD}", "PPC_Fsel", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rB}"), IrValue.Register($"f{rC}") }) }, // fsel
|
||||
24 => new[] { new IrCall($"f{rD}", "PPC_Fres", new[] { IrValue.Register($"f{rB}") }) }, // fres
|
||||
25 => new[] { new IrCall($"f{rD}", "PPC_Fmuls", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}") }) }, // fmuls (uses A and C)
|
||||
26 => new[] { new IrCall($"f{rD}", "PPC_Frsqrte", new[] { IrValue.Register($"f{rB}") }) }, // frsqrtes
|
||||
28 => new[] { new IrCall($"f{rD}", "PPC_Fmsubs", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}"), IrValue.Register($"f{rB}") }) }, // fmsubs
|
||||
29 => new[] { new IrCall($"f{rD}", "PPC_Fmadds", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}"), IrValue.Register($"f{rB}") }) }, // fmadds
|
||||
30 => new[] { new IrCall($"f{rD}", "PPC_Fnmsubs", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}"), IrValue.Register($"f{rB}") }) }, // fnmsubs
|
||||
31 => new[] { new IrCall($"f{rD}", "PPC_Fnmadds", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}"), IrValue.Register($"f{rB}") }) }, // fnmadds
|
||||
_ => throw new NotImplementedException($"UNIMPLEMENTED opc_59 XO {xo} @ 0x{ins.Address:X8}")
|
||||
};
|
||||
}
|
||||
|
||||
case var s when s.StartsWith("fp_"):
|
||||
{
|
||||
uint raw1 = ReadRawInstruction(ins);
|
||||
// Extract real XO from bits 26-30
|
||||
int xo = (int)((raw1 >> 1) & 0x1F);
|
||||
|
||||
int rD = (int)((raw1 >> 21) & 0x1F);
|
||||
int rA = (int)((raw1 >> 16) & 0x1F);
|
||||
int rB = (int)((raw1 >> 11) & 0x1F);
|
||||
int rC = (int)((raw1 >> 6) & 0x1F);
|
||||
|
||||
return xo switch
|
||||
{
|
||||
18 => new[] { new IrBinary($"f{rD}", IrValue.Register($"f{rA}"), IrValue.Register($"f{rB}"), "fdiv") },
|
||||
20 => new[] { new IrBinary($"f{rD}", IrValue.Register($"f{rA}"), IrValue.Register($"f{rB}"), "sub") },
|
||||
21 => new[] { new IrBinary($"f{rD}", IrValue.Register($"f{rA}"), IrValue.Register($"f{rB}"), "add") },
|
||||
22 => new[] { new IrCall($"f{rD}", "PPC_Fsqrt", new[] { IrValue.Register($"f{rB}") }) }, // fsqrt
|
||||
23 => new[] { new IrCall($"f{rD}", "PPC_Fsel", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rB}"), IrValue.Register($"f{rC}") }) }, // fsel
|
||||
24 => new[] { new IrCall($"f{rD}", "PPC_Fres", new[] { IrValue.Register($"f{rB}") }) }, // fres
|
||||
25 => new[] { new IrBinary($"f{rD}", IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}"), "mul") }, // fmul uses A and C
|
||||
26 => new[] { new IrCall($"f{rD}", "PPC_Frsqrte", new[] { IrValue.Register($"f{rB}") }) }, // frsqrte
|
||||
28 => new[] { new IrCall($"f{rD}", "PPC_Fmsub", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}"), IrValue.Register($"f{rB}") }) }, // fmsub
|
||||
29 => new[] { new IrCall($"f{rD}", "PPC_Fmadd", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}"), IrValue.Register($"f{rB}") }) }, // fmadd
|
||||
30 => new[] { new IrCall($"f{rD}", "PPC_Fnmsub", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}"), IrValue.Register($"f{rB}") }) }, // fnmsub
|
||||
31 => new[] { new IrCall($"f{rD}", "PPC_Fnmadd", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}"), IrValue.Register($"f{rB}") }) }, // fnmadd
|
||||
32 => new[] { new IrBinary($"f{rD}", IrValue.Register($"f{rB}"), IrValue.Imm(0), "fneg") }, // fneg (unary)
|
||||
12 => new[] { new IrBinary($"f{rD}", IrValue.Register($"f{rB}"), IrValue.Imm(0), "frsp") }, // frsp (unary)
|
||||
8 => new[] { new IrBinary($"f{rD}", IrValue.Register($"f{rB}"), IrValue.Imm(0), "fcmp") }, // fcmp
|
||||
_ => throw new NotImplementedException($"UNIMPLEMENTED fp_ fallback with XO {xo} @ 0x{ins.Address:X8}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Translator.Core.Disassembly;
|
||||
using Translator.Core.Ir;
|
||||
|
||||
namespace Translator.Core.Lifting;
|
||||
|
||||
public sealed partial class PpcLifter
|
||||
{
|
||||
private static IReadOnlyList<IrInstruction>? TryLiftPairedSingle(
|
||||
PpcInstruction ins,
|
||||
string mnemonic,
|
||||
IReadOnlyList<PpcOperand> ops,
|
||||
IReadOnlyList<string> operands)
|
||||
{
|
||||
string Reg(int idx) => RegisterOperandName(ops, idx);
|
||||
int Imm(int idx) => ImmediateOperandValue(ops, idx);
|
||||
switch (mnemonic)
|
||||
{
|
||||
case "psq_l" when ops.Count == 4:
|
||||
{
|
||||
var (offset, reg) = ParseDisplacement(operands[1]);
|
||||
return EmitPairedSingleLoad(Reg(0), reg, offset, Imm(2), Imm(3), updateBase: false, ins.Address);
|
||||
}
|
||||
|
||||
case "psq_lu" when ops.Count == 4:
|
||||
{
|
||||
var (offset, reg) = ParseDisplacement(operands[1]);
|
||||
return EmitPairedSingleLoad(Reg(0), reg, offset, Imm(2), Imm(3), updateBase: true, ins.Address);
|
||||
}
|
||||
|
||||
case "psq_st" when ops.Count == 4:
|
||||
{
|
||||
var (offset, reg) = ParseDisplacement(operands[1]);
|
||||
return EmitPairedSingleStore(Reg(0), reg, offset, Imm(2), Imm(3), updateBase: false, ins.Address);
|
||||
}
|
||||
|
||||
case "psq_stu" when ops.Count == 4:
|
||||
{
|
||||
var (offset, reg) = ParseDisplacement(operands[1]);
|
||||
return EmitPairedSingleStore(Reg(0), reg, offset, Imm(2), Imm(3), updateBase: true, ins.Address);
|
||||
}
|
||||
|
||||
case "psq_lx":
|
||||
return LiftPairedSingleIndexedLoad(ins, ReadRawInstruction(ins), updateBase: false);
|
||||
|
||||
case "psq_lux":
|
||||
return LiftPairedSingleIndexedLoad(ins, ReadRawInstruction(ins), updateBase: true);
|
||||
|
||||
case "psq_stx":
|
||||
return LiftPairedSingleIndexedStore(ins, ReadRawInstruction(ins), updateBase: false);
|
||||
|
||||
case "psq_stux":
|
||||
return LiftPairedSingleIndexedStore(ins, ReadRawInstruction(ins), updateBase: true);
|
||||
|
||||
case "ps_add" when ops.Count == 3:
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsAdd", new[] { IrValue.Register(Reg(1)), IrValue.Register(Reg(2)) }) };
|
||||
|
||||
case "ps_sub" when ops.Count == 3:
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsSub", new[] { IrValue.Register(Reg(1)), IrValue.Register(Reg(2)) }) };
|
||||
|
||||
case "ps_div" when ops.Count == 3:
|
||||
case "ps_div." when ops.Count == 3:
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsDiv", new[] { IrValue.Register(Reg(1)), IrValue.Register(Reg(2)) }) };
|
||||
|
||||
case "ps_mul" when ops.Count == 3:
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsMul", new[] { IrValue.Register(Reg(1)), IrValue.Register(Reg(2)) }) };
|
||||
|
||||
case "ps_msub" when ops.Count == 4:
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsMsub", new[] { IrValue.Register(Reg(1)), IrValue.Register(Reg(2)), IrValue.Register(Reg(3)) }) };
|
||||
|
||||
case "ps_madd" when ops.Count == 4:
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsMadd", new[] { IrValue.Register(Reg(1)), IrValue.Register(Reg(2)), IrValue.Register(Reg(3)) }) };
|
||||
|
||||
case "ps_nmsub" when ops.Count == 4:
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsNmsub", new[] { IrValue.Register(Reg(1)), IrValue.Register(Reg(2)), IrValue.Register(Reg(3)) }) };
|
||||
|
||||
case "ps_nmadd" when ops.Count == 4:
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsNmadd", new[] { IrValue.Register(Reg(1)), IrValue.Register(Reg(2)), IrValue.Register(Reg(3)) }) };
|
||||
|
||||
case "ps_madds0" when ops.Count == 4:
|
||||
case "ps_madds0." when ops.Count == 4:
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsMadds0", new[] { IrValue.Register(Reg(1)), IrValue.Register(Reg(2)), IrValue.Register(Reg(3)) }) };
|
||||
|
||||
case "ps_madds1" when ops.Count == 4:
|
||||
case "ps_madds1." when ops.Count == 4:
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsMadds1", new[] { IrValue.Register(Reg(1)), IrValue.Register(Reg(2)), IrValue.Register(Reg(3)) }) };
|
||||
|
||||
case "ps_sum0" when ops.Count == 4:
|
||||
case "ps_sum0." when ops.Count == 4:
|
||||
// Decoder outputs (D, A, C, B) per IBM syntax, but PPC_PsSum0 expects (frA, frB, frC)
|
||||
// So we pass Reg(1)=frA, Reg(3)=frB, Reg(2)=frC
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsSum0", new[] { IrValue.Register(Reg(1)), IrValue.Register(Reg(3)), IrValue.Register(Reg(2)) }) };
|
||||
|
||||
case "ps_sum1" when ops.Count == 4:
|
||||
case "ps_sum1." when ops.Count == 4:
|
||||
// Decoder outputs (D, A, C, B) per IBM syntax, but PPC_PsSum1 expects (frA, frB, frC)
|
||||
// So we pass Reg(1)=frA, Reg(3)=frB, Reg(2)=frC
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsSum1", new[] { IrValue.Register(Reg(1)), IrValue.Register(Reg(3)), IrValue.Register(Reg(2)) }) };
|
||||
|
||||
case "ps_muls0" when ops.Count == 3:
|
||||
case "ps_muls0." when ops.Count == 3:
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsMuls0", new[] { IrValue.Register(Reg(1)), IrValue.Register(Reg(2)) }) };
|
||||
|
||||
case "ps_muls1" when ops.Count == 3:
|
||||
case "ps_muls1." when ops.Count == 3:
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsMuls1", new[] { IrValue.Register(Reg(1)), IrValue.Register(Reg(2)) }) };
|
||||
|
||||
case "ps_sel" when ops.Count == 4:
|
||||
{
|
||||
// Check for misidentified ps_neg (XO 40 encoded in bits 21-30)
|
||||
uint rawSel = ReadRawInstruction(ins);
|
||||
int selXo = (int)((rawSel >> 1) & 0x3FF);
|
||||
if (selXo == 40)
|
||||
{
|
||||
// ps_neg frD, frB (D is bits 6-10, B is bits 16-20)
|
||||
int selRD = (int)((rawSel >> 21) & 0x1F);
|
||||
int selRB = (int)((rawSel >> 11) & 0x1F);
|
||||
return new[] { new IrCall($"f{selRD}", "PPC_PsNeg", new[] { IrValue.Register($"f{selRB}") }) };
|
||||
}
|
||||
|
||||
// ps_sel frD, frA, frC, frB
|
||||
// frD = frA >= 0 ? frC : frB
|
||||
// PPC_PsSel(lhs=frC, control=frA, rhs=frB)
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsSel", new[] { IrValue.Register(Reg(2)), IrValue.Register(Reg(1)), IrValue.Register(Reg(3)) }) };
|
||||
}
|
||||
|
||||
case "ps_res" when ops.Count == 2:
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsRes", new[] { IrValue.Register(Reg(1)) }) };
|
||||
|
||||
case "ps_rsqrte" when ops.Count == 2:
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsRsqrte", new[] { IrValue.Register(Reg(1)) }) };
|
||||
|
||||
case "ps_neg":
|
||||
case "ps_neg.":
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsNeg", new[] { IrValue.Register(Reg(1)) }) };
|
||||
|
||||
case "ps_abs":
|
||||
case "ps_abs.":
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsAbs", new[] { IrValue.Register(Reg(1)) }) };
|
||||
|
||||
case "ps_nabs":
|
||||
case "ps_nabs.":
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsNabs", new[] { IrValue.Register(Reg(1)) }) };
|
||||
|
||||
case "ps_merge00" when ops.Count == 3:
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsMerge00", new[] { IrValue.Register(Reg(1)), IrValue.Register(Reg(2)) }) };
|
||||
|
||||
case "ps_merge01" when ops.Count == 3:
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsMerge01", new[] { IrValue.Register(Reg(1)), IrValue.Register(Reg(2)) }) };
|
||||
|
||||
case "ps_merge10" when ops.Count == 3:
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsMerge10", new[] { IrValue.Register(Reg(1)), IrValue.Register(Reg(2)) }) };
|
||||
|
||||
case "ps_merge11" when ops.Count == 3:
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsMerge11", new[] { IrValue.Register(Reg(1)), IrValue.Register(Reg(2)) }) };
|
||||
|
||||
case "ps_mr" when ops.Count == 2:
|
||||
case "ps_mr." when ops.Count == 2:
|
||||
// ps_mr copies both lanes, unlike scalar fmr which writes only PS0
|
||||
// and preserves the destination's existing PS1 lane.
|
||||
return new[] { new IrCall(Reg(0), "PPC_PsMr", new[] { IrValue.Register(Reg(1)) }) };
|
||||
|
||||
case "ps_cmpo0" when ops.Count == 3:
|
||||
case "ps_cmpo0." when ops.Count == 3: // Dot form doesn't really exist for compare but just in case
|
||||
{
|
||||
var dest = operands[0]; // cr field
|
||||
var crField = ParseCrFieldName(dest);
|
||||
// PPC_PsCmpo0(crField, frA, frB)
|
||||
return new[] { new IrCall(string.Empty, "PPC_PsCmpo0", new[] { IrValue.Imm(crField), IrValue.Register(Reg(1)), IrValue.Register(Reg(2)) }) };
|
||||
}
|
||||
|
||||
case "ps_cmpu0" when ops.Count == 3:
|
||||
case "ps_cmpu0." when ops.Count == 3:
|
||||
{
|
||||
var dest = operands[0];
|
||||
var crField = ParseCrFieldName(dest);
|
||||
return new[] { new IrCall(string.Empty, "PPC_PsCmpu0", new[] { IrValue.Imm(crField), IrValue.Register(Reg(1)), IrValue.Register(Reg(2)) }) };
|
||||
}
|
||||
|
||||
case "ps_cmpo1" when ops.Count == 3:
|
||||
case "ps_cmpo1." when ops.Count == 3:
|
||||
{
|
||||
var dest = operands[0];
|
||||
var crField = ParseCrFieldName(dest);
|
||||
return new[] { new IrCall(string.Empty, "PPC_PsCmpo1", new[] { IrValue.Imm(crField), IrValue.Register(Reg(1)), IrValue.Register(Reg(2)) }) };
|
||||
}
|
||||
|
||||
case "ps_cmpu1" when ops.Count == 3:
|
||||
case "ps_cmpu1." when ops.Count == 3:
|
||||
{
|
||||
var dest = operands[0];
|
||||
var crField = ParseCrFieldName(dest);
|
||||
return new[] { new IrCall(string.Empty, "PPC_PsCmpu1", new[] { IrValue.Imm(crField), IrValue.Register(Reg(1)), IrValue.Register(Reg(2)) }) };
|
||||
}
|
||||
|
||||
// Some toolchains emit raw opcode labels (opc_4_XX) for paired-single ops.
|
||||
// Map the ones we know we use back to the corresponding ps_* helpers.
|
||||
case "opc_4_50": // ps_mul (XO 25, Rc=0)
|
||||
case "opc_4_51": // ps_mul. (XO 25, Rc=1)
|
||||
{
|
||||
uint raw1 = ReadRawInstruction(ins);
|
||||
int rD = (int)((raw1 >> 21) & 0x1F);
|
||||
int rA = (int)((raw1 >> 16) & 0x1F);
|
||||
int rC = (int)((raw1 >> 6) & 0x1F);
|
||||
return new[] { new IrCall($"f{rD}", "PPC_PsMul", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}") }) };
|
||||
}
|
||||
|
||||
case var s when s.StartsWith("opc_4_"):
|
||||
{
|
||||
if (!int.TryParse(s.Split('_').Last(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var subop))
|
||||
{
|
||||
throw new NotImplementedException($"UNIMPLEMENTED paired-single opcode '{s}' @ 0x{ins.Address:X8}");
|
||||
}
|
||||
|
||||
uint raw1 = ReadRawInstruction(ins);
|
||||
int rD = (int)((raw1 >> 21) & 0x1F);
|
||||
int rA = (int)((raw1 >> 16) & 0x1F);
|
||||
int rB = (int)((raw1 >> 11) & 0x1F);
|
||||
int rC = (int)((raw1 >> 6) & 0x1F);
|
||||
|
||||
// Some paired-single instructions use X-form encoding with 10-bit XO in bits 1-10.
|
||||
// The disassembler may give us the wrong A-form subop, so check X-form XO first.
|
||||
int xformXO = (int)((raw1 >> 1) & 0x3FF); // bits 1-10
|
||||
if (xformXO == 40) // ps_neg: frD = -frB (paired)
|
||||
{
|
||||
return new[] { new IrCall($"f{rD}", "PPC_PsNeg", new[] { IrValue.Register($"f{rB}") }) };
|
||||
}
|
||||
|
||||
return subop switch
|
||||
{
|
||||
0 or 1 or 2 or 3 => LiftCompare("cr0", IrValue.Register($"f{rA}"), IrValue.Register($"f{rB}"), "sub", isUnsigned: false),
|
||||
4 => new[] { new IrCall($"f{rD}", "PPC_PsMr", new[] { IrValue.Register($"f{rB}") }) },
|
||||
5 => new[] { new IrCall($"f{rD}", "PPC_PsNabs", new[] { IrValue.Register($"f{rB}") }) },
|
||||
6 or 12 => LiftPairedSingleIndexedLoad(ins, raw1, updateBase: false), // psq_lx (XO=6, 6<<1=12)
|
||||
7 or 14 => LiftPairedSingleIndexedStore(ins, raw1, updateBase: false), // psq_stx (XO=7, 7<<1=14)
|
||||
|
||||
// Validated Arithmetic Ops (XO << 1 | Rc)
|
||||
20 or 21 => new[] { new IrCall($"f{rD}", "PPC_PsSum0", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rB}"), IrValue.Register($"f{rC}") }) }, // ps_sum0 (XO=10)
|
||||
22 or 23 => new[] { new IrCall($"f{rD}", "PPC_PsSum1", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rB}"), IrValue.Register($"f{rC}") }) }, // ps_sum1 (XO=11)
|
||||
24 or 25 => new[] { new IrCall($"f{rD}", "PPC_PsMuls0", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}") }) }, // ps_muls0 (XO=12)
|
||||
26 or 27 => new[] { new IrCall($"f{rD}", "PPC_PsMuls1", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}") }) }, // ps_muls1 (XO=13)
|
||||
28 or 29 => new[] { new IrCall($"f{rD}", "PPC_PsMadds0", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}"), IrValue.Register($"f{rB}") }) }, // ps_madds0 (XO=14)
|
||||
30 or 31 => new[] { new IrCall($"f{rD}", "PPC_PsMadds1", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}"), IrValue.Register($"f{rB}") }) }, // ps_madds1 (XO=15)
|
||||
|
||||
36 or 37 => new[] { new IrCall($"f{rD}", "PPC_PsDiv", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rB}") }) }, // ps_div (XO=18)
|
||||
40 or 41 => new[] { new IrCall($"f{rD}", "PPC_PsSub", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rB}") }) }, // ps_sub (XO=20)
|
||||
42 or 43 => new[] { new IrCall($"f{rD}", "PPC_PsAdd", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rB}") }) }, // ps_add (XO=21)
|
||||
44 or 45 => new[] { new IrCall($"f{rD}", "PPC_PsAbs", new[] { IrValue.Register($"f{rB}") }) }, // ps_abs (XO=22)
|
||||
46 or 47 => new[] { new IrCall($"f{rD}", "PPC_PsSel", new[] { IrValue.Register($"f{rC}"), IrValue.Register($"f{rA}"), IrValue.Register($"f{rB}") }) }, // ps_sel (XO=23)
|
||||
48 or 49 => new[] { new IrCall($"f{rD}", "PPC_PsRes", new[] { IrValue.Register($"f{rB}") }) }, // ps_res (XO=24)
|
||||
50 or 51 => new[] { new IrCall($"f{rD}", "PPC_PsMul", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}") }) }, // ps_mul (XO=25)
|
||||
52 or 53 => new[] { new IrCall($"f{rD}", "PPC_PsRsqrte", new[] { IrValue.Register($"f{rB}") }) }, // ps_rsqrte (XO=26)
|
||||
|
||||
56 or 57 => new[] { new IrCall($"f{rD}", "PPC_PsMsub", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}"), IrValue.Register($"f{rB}") }) }, // ps_msub (XO=28)
|
||||
58 or 59 => new[] { new IrCall($"f{rD}", "PPC_PsMadd", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}"), IrValue.Register($"f{rB}") }) }, // ps_madd (XO=29)
|
||||
60 or 61 => new[] { new IrCall($"f{rD}", "PPC_PsNmsub", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}"), IrValue.Register($"f{rB}") }) }, // ps_nmsub (XO=30)
|
||||
62 or 63 => new[] { new IrCall($"f{rD}", "PPC_PsNmadd", new[] { IrValue.Register($"f{rA}"), IrValue.Register($"f{rC}"), IrValue.Register($"f{rB}") }) }, // ps_nmadd (XO=31)
|
||||
|
||||
// Memory Ops (using raw XO?)
|
||||
38 or 76 => LiftPairedSingleIndexedLoad(ins, raw1, updateBase: true), // psq_lux (XO=38, 38<<1=76)
|
||||
39 or 78 => LiftPairedSingleIndexedStore(ins, raw1, updateBase: true), // psq_stux (XO=39, 39<<1=78)
|
||||
_ => throw new NotImplementedException($"UNIMPLEMENTED paired-single opcode '{s}' @ 0x{ins.Address:X8}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace Translator.Core.Loading;
|
||||
|
||||
public readonly record struct AddressRange(uint Start, uint End)
|
||||
{
|
||||
public uint Length => checked(End - Start);
|
||||
|
||||
public bool Contains(uint address) => address >= Start && address < End;
|
||||
|
||||
public static AddressRange FromStartAndSize(uint start, uint size) => new(start, checked(start + size));
|
||||
|
||||
public static AddressRange Union(AddressRange a, AddressRange b) => new(
|
||||
Math.Min(a.Start, b.Start),
|
||||
Math.Max(a.End, b.End));
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Translator.Core.Loading;
|
||||
|
||||
/// <summary>
|
||||
/// The single SHA-256 hex spelling used everywhere identities are compared. Lower-case is load-bearing:
|
||||
/// a divergent casing anywhere would silently invalidate content-addressed reuse.
|
||||
/// </summary>
|
||||
public static class ChecksumUtilities
|
||||
{
|
||||
public static string Sha256Hex(ReadOnlySpan<byte> data) =>
|
||||
Convert.ToHexString(SHA256.HashData(data)).ToLowerInvariant();
|
||||
|
||||
public static string Sha256Hex(byte[] data) =>
|
||||
Sha256Hex((ReadOnlySpan<byte>)data);
|
||||
|
||||
/// <summary>Digest of raw hash bytes that were already computed elsewhere.</summary>
|
||||
public static string ToHex(ReadOnlySpan<byte> hash) =>
|
||||
Convert.ToHexString(hash).ToLowerInvariant();
|
||||
|
||||
/// <summary>Streams the file rather than materializing it, so hashing a REL costs no copy.</summary>
|
||||
public static string Sha256HexOfFile(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
return ToHex(SHA256.HashData(stream));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Translator.Core.Loading;
|
||||
|
||||
/// <summary>
|
||||
/// The authoritative list of function entry points and, where known, symbol names: one
|
||||
/// <c>hexaddr name</c> line each (unnamed entries repeat the address). Replaces the old heuristic
|
||||
/// discovery seeders and the hand-maintained force-translate list that patched their misses.
|
||||
/// </summary>
|
||||
public sealed class FunctionMap
|
||||
{
|
||||
private readonly uint[] _addresses;
|
||||
private readonly Dictionary<uint, string> _namesByAddress;
|
||||
private readonly Dictionary<string, uint> _addressesByName;
|
||||
|
||||
private FunctionMap(
|
||||
string sourcePath,
|
||||
uint[] addresses,
|
||||
Dictionary<uint, string> namesByAddress,
|
||||
Dictionary<string, uint> addressesByName)
|
||||
{
|
||||
SourcePath = sourcePath;
|
||||
_addresses = addresses;
|
||||
_namesByAddress = namesByAddress;
|
||||
_addressesByName = addressesByName;
|
||||
}
|
||||
|
||||
/// <summary>Path the map was read from, for diagnostics.</summary>
|
||||
public string SourcePath { get; }
|
||||
|
||||
/// <summary>Every function start in the map, ascending and distinct.</summary>
|
||||
public IReadOnlyList<uint> Addresses => _addresses;
|
||||
|
||||
/// <summary>Entries that carry a real symbol name.</summary>
|
||||
public int NamedCount => _namesByAddress.Count;
|
||||
|
||||
public bool Contains(uint address) => _namesByAddress.ContainsKey(address) || IndexOf(address) >= 0;
|
||||
|
||||
/// <summary>The map's symbol for <paramref name="address"/>, or null when unnamed or absent.</summary>
|
||||
public string? NameOf(uint address) =>
|
||||
_namesByAddress.TryGetValue(address, out var name) ? name : null;
|
||||
|
||||
/// <summary>Resolves a symbol name to its address.</summary>
|
||||
public bool TryGetAddress(string symbol, out uint address) =>
|
||||
_addressesByName.TryGetValue(symbol, out address);
|
||||
|
||||
/// <summary>
|
||||
/// Named entries matching "<paramref name="prefix"/>{int}", keyed by that integer. Recovers CodeWarrior
|
||||
/// save/restore thunk ranges from the map instead of hardcoding addresses.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<int, uint> NumberedFamily(string prefix)
|
||||
{
|
||||
var family = new Dictionary<int, uint>();
|
||||
foreach (var (address, name) in _namesByAddress)
|
||||
{
|
||||
if (!name.StartsWith(prefix, StringComparison.Ordinal)) continue;
|
||||
var suffix = name.AsSpan(prefix.Length);
|
||||
if (suffix.Length == 0) continue;
|
||||
if (!int.TryParse(suffix, NumberStyles.None, CultureInfo.InvariantCulture, out var index)) continue;
|
||||
family[index] = address;
|
||||
}
|
||||
|
||||
return family;
|
||||
}
|
||||
|
||||
public static FunctionMap Load(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
throw new FileNotFoundException("Configured function map was not found.", path);
|
||||
}
|
||||
|
||||
return Parse(File.ReadLines(path), path);
|
||||
}
|
||||
|
||||
public static FunctionMap Parse(IEnumerable<string> lines, string sourcePath)
|
||||
{
|
||||
var namesByAddress = new Dictionary<uint, string>();
|
||||
var addressesByName = new Dictionary<string, uint>(StringComparer.Ordinal);
|
||||
var addresses = new HashSet<uint>();
|
||||
var lineNumber = 0;
|
||||
|
||||
foreach (var rawLine in lines)
|
||||
{
|
||||
lineNumber++;
|
||||
var line = rawLine.Trim();
|
||||
if (line.Length == 0 || line[0] == '#') continue;
|
||||
|
||||
var separator = line.IndexOfAny([' ', '\t']);
|
||||
var addressText = separator < 0 ? line : line[..separator];
|
||||
if (!uint.TryParse(addressText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var address))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Invalid function map entry at {sourcePath}:{lineNumber}: '{rawLine}' does not start with a hexadecimal address.");
|
||||
}
|
||||
|
||||
addresses.Add(address);
|
||||
|
||||
var name = separator < 0 ? string.Empty : line[(separator + 1)..].Trim();
|
||||
// Unnamed entries repeat the address as "0x{addr}"; that is a
|
||||
// placeholder, not a symbol, so it must never become a lookup key.
|
||||
if (name.Length == 0 || name.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
namesByAddress[address] = name;
|
||||
// A symbol can legitimately repeat (static functions in different
|
||||
// translation units); the lowest address wins so lookups are stable.
|
||||
if (!addressesByName.TryGetValue(name, out var existing) || address < existing)
|
||||
{
|
||||
addressesByName[name] = address;
|
||||
}
|
||||
}
|
||||
|
||||
if (addresses.Count == 0)
|
||||
{
|
||||
throw new InvalidDataException($"Function map {sourcePath} contains no entries.");
|
||||
}
|
||||
|
||||
var sorted = addresses.ToArray();
|
||||
Array.Sort(sorted);
|
||||
return new FunctionMap(sourcePath, sorted, namesByAddress, addressesByName);
|
||||
}
|
||||
|
||||
private int IndexOf(uint address) => Array.BinarySearch(_addresses, address);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Translator.Core.Loading;
|
||||
|
||||
public static class MemoryLayout
|
||||
{
|
||||
public const uint RamBase = 0x80000000;
|
||||
public const int RamSize = 24 * 1024 * 1024; // 24 MiB MEM1
|
||||
public const uint DolBaseAddress = 0x80004000; // First executable section start
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Translator.Core.Parsing.Dol;
|
||||
using Translator.Core.Parsing.Rel;
|
||||
|
||||
namespace Translator.Core.Loading;
|
||||
|
||||
public sealed class ProgramImage
|
||||
{
|
||||
public ProgramImage(
|
||||
byte[] memory,
|
||||
AddressRange usedRange,
|
||||
AddressRange dolRange,
|
||||
AddressRange relRange,
|
||||
string sha256,
|
||||
uint memoryBase = MemoryLayout.RamBase)
|
||||
{
|
||||
Memory = memory;
|
||||
UsedRange = usedRange;
|
||||
DolRange = dolRange;
|
||||
RelRange = relRange;
|
||||
Sha256 = sha256;
|
||||
MemoryBase = memoryBase;
|
||||
}
|
||||
|
||||
public byte[] Memory { get; }
|
||||
public uint MemoryBase { get; }
|
||||
public uint MemoryEnd => checked(MemoryBase + (uint)Memory.Length);
|
||||
public AddressRange UsedRange { get; }
|
||||
public AddressRange DolRange { get; }
|
||||
public AddressRange RelRange { get; }
|
||||
public bool HasRel => RelRange.End > RelRange.Start;
|
||||
public string Sha256 { get; }
|
||||
|
||||
public bool Contains(uint address, int size = 1)
|
||||
{
|
||||
if (size < 0 || address < MemoryBase)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var offset = (ulong)address - MemoryBase;
|
||||
return offset + (uint)size <= (ulong)Memory.Length;
|
||||
}
|
||||
|
||||
public int GetOffset(uint address, int size = 1)
|
||||
{
|
||||
if (!Contains(address, size))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(address),
|
||||
$"Guest range 0x{address:X8}+0x{size:X} is outside 0x{MemoryBase:X8}-0x{MemoryEnd:X8}.");
|
||||
}
|
||||
|
||||
return checked((int)(address - MemoryBase));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Translator.Core.Parsing.Dol;
|
||||
using Translator.Core.Parsing.Rel;
|
||||
|
||||
namespace Translator.Core.Loading;
|
||||
|
||||
public sealed class ProgramImageBuilder
|
||||
{
|
||||
public ProgramImage Build(string dolPath, string relPath, uint relBaseAddress)
|
||||
{
|
||||
var dol = DolFile.Load(dolPath);
|
||||
var rel = RelFile.Load(relPath).BuildImage(relBaseAddress);
|
||||
return Build(dol, rel);
|
||||
}
|
||||
|
||||
public ProgramImage Build(
|
||||
DolFile dol,
|
||||
RelImage? rel = null,
|
||||
uint ramBase = MemoryLayout.RamBase,
|
||||
int ramSize = MemoryLayout.RamSize)
|
||||
{
|
||||
if (ramSize <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(ramSize));
|
||||
}
|
||||
|
||||
var ram = new byte[ramSize];
|
||||
|
||||
// Some DOL headers report a coarse BSS span that overlaps initialized small-data sections.
|
||||
// Clear first so the bytes that exist in the file win when ranges overlap.
|
||||
foreach (var bss in dol.Sections.Where(s => s.Kind == SectionKind.Bss))
|
||||
{
|
||||
var offset = checked((int)(bss.VirtualAddress - ramBase));
|
||||
var end = offset + (int)bss.Size;
|
||||
if (offset < 0 || end > ram.Length)
|
||||
{
|
||||
throw new InvalidOperationException($"BSS section spills outside RAM window (offset {offset}, end {end})");
|
||||
}
|
||||
|
||||
ram.AsSpan(offset, (int)bss.Size).Clear();
|
||||
}
|
||||
|
||||
foreach (var section in dol.Sections)
|
||||
{
|
||||
if (section.Size == 0 || section.Kind == SectionKind.Bss)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var offset = checked((int)(section.VirtualAddress - ramBase));
|
||||
var end = offset + (int)section.Size;
|
||||
if (offset < 0 || end > ram.Length)
|
||||
{
|
||||
throw new InvalidOperationException($"DOL section {section.Name} spills outside RAM window (offset {offset}, end {end})");
|
||||
}
|
||||
|
||||
section.Data.Span.CopyTo(ram.AsSpan(offset, (int)section.Size));
|
||||
}
|
||||
|
||||
var relRange = new AddressRange(0, 0);
|
||||
if (rel is not null)
|
||||
{
|
||||
var relOffset = checked((int)(rel.BaseAddress - ramBase));
|
||||
var relEnd = relOffset + rel.Data.Length;
|
||||
if (relOffset < 0 || relEnd > ram.Length)
|
||||
{
|
||||
throw new InvalidOperationException($"REL image spills outside RAM window (offset {relOffset}, end {relEnd})");
|
||||
}
|
||||
|
||||
rel.Data.CopyTo(ram, relOffset);
|
||||
relRange = rel.Range;
|
||||
}
|
||||
|
||||
var usedRange = rel is null ? dol.MemoryRange : AddressRange.Union(dol.MemoryRange, rel.Range);
|
||||
|
||||
var sha = ChecksumUtilities.Sha256Hex(ram);
|
||||
return new ProgramImage(ram, usedRange, dol.MemoryRange, relRange, sha, ramBase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Translator.Core.Mods;
|
||||
|
||||
public sealed record BaseManifest(
|
||||
string Format,
|
||||
int FormatVersion,
|
||||
string GameId,
|
||||
string Region,
|
||||
string ImageSha256,
|
||||
uint RelBaseAddress,
|
||||
IReadOnlyList<BaseSectionMetadata> Sections,
|
||||
IReadOnlyList<BaseFunctionRangeMetadata> Functions,
|
||||
string FunctionRangesFile);
|
||||
|
||||
public sealed record BaseSectionMetadata(
|
||||
string Name,
|
||||
string Source,
|
||||
uint GuestStart,
|
||||
uint GuestEnd,
|
||||
bool Executable,
|
||||
bool Writable,
|
||||
string? ImageFile,
|
||||
uint ImageOffset);
|
||||
|
||||
public sealed record BaseFunctionRangeMetadata(
|
||||
uint Start,
|
||||
uint End,
|
||||
string Name,
|
||||
string SourceSection,
|
||||
uint SourceImageOffset,
|
||||
string RangeSource,
|
||||
IReadOnlyList<string> Flags);
|
||||
|
||||
public sealed class BaseFunctionIndex
|
||||
{
|
||||
private readonly List<BaseFunctionRangeMetadata> _functions;
|
||||
|
||||
public BaseFunctionIndex(IEnumerable<BaseFunctionRangeMetadata> functions)
|
||||
{
|
||||
_functions = functions.OrderBy(f => f.Start).ToList();
|
||||
}
|
||||
|
||||
public static BaseFunctionIndex Load(string path)
|
||||
{
|
||||
var json = File.ReadAllText(path);
|
||||
var manifest = JsonSerializer.Deserialize<BaseManifest>(json)
|
||||
?? throw new InvalidDataException($"Failed to parse base manifest: {path}");
|
||||
return new BaseFunctionIndex(manifest.Functions);
|
||||
}
|
||||
|
||||
public BaseFunctionRangeMetadata? FindContaining(uint address)
|
||||
{
|
||||
var lo = 0;
|
||||
var hi = _functions.Count - 1;
|
||||
while (lo <= hi)
|
||||
{
|
||||
var mid = lo + ((hi - lo) / 2);
|
||||
var function = _functions[mid];
|
||||
if (address < function.Start)
|
||||
{
|
||||
hi = mid - 1;
|
||||
}
|
||||
else if (address >= function.End)
|
||||
{
|
||||
lo = mid + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return function;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Translator.Core.IO;
|
||||
using Translator.Core.Loading;
|
||||
using Translator.Core.Parsing.Dol;
|
||||
using Translator.Core.Parsing.Rel;
|
||||
|
||||
namespace Translator.Core.Mods;
|
||||
|
||||
public sealed class BaseManifestBuildResult
|
||||
{
|
||||
public required BaseManifest Manifest { get; init; }
|
||||
public required string ManifestPath { get; init; }
|
||||
public required string FunctionRangesPath { get; init; }
|
||||
}
|
||||
|
||||
public static partial class BaseManifestBuilder
|
||||
{
|
||||
public static BaseManifestBuildResult BuildAndWrite(
|
||||
DolFile dol,
|
||||
RelFile rel,
|
||||
RelImage relImage,
|
||||
ProgramImage image,
|
||||
string generatedFunctionsDir,
|
||||
string outputDir,
|
||||
string manifestFormat,
|
||||
string gameId,
|
||||
string region,
|
||||
string fileStem = "base",
|
||||
uint lowMemoryStart = MemoryLayout.RamBase,
|
||||
uint lowMemoryEnd = 0x80004000u,
|
||||
string dolSourceName = "main.dol",
|
||||
string relSourceName = "module.rel",
|
||||
BaseTranslationOutputMetadata? translationOutput = null)
|
||||
{
|
||||
Directory.CreateDirectory(outputDir);
|
||||
|
||||
var sections = new List<BaseSectionMetadata>();
|
||||
var dolTextPath = Path.Combine(outputDir, $"{fileStem}_main_text.bin");
|
||||
var relTextPath = Path.Combine(outputDir, $"{fileStem}_rel_text.bin");
|
||||
|
||||
var dolTextOffsets = WriteDolExecutableImage(dol, dolTextPath);
|
||||
sections.Add(new BaseSectionMetadata(
|
||||
"lowmem",
|
||||
"runtime-lowmem",
|
||||
lowMemoryStart,
|
||||
lowMemoryEnd,
|
||||
Executable: false,
|
||||
Writable: true,
|
||||
ImageFile: null,
|
||||
ImageOffset: 0));
|
||||
|
||||
foreach (var section in dol.Sections.Where(s => s.Size > 0))
|
||||
{
|
||||
sections.Add(new BaseSectionMetadata(
|
||||
section.Name,
|
||||
dolSourceName,
|
||||
section.VirtualAddress,
|
||||
checked(section.VirtualAddress + section.Size),
|
||||
section.IsExecutable,
|
||||
section.Kind is SectionKind.Data or SectionKind.Bss,
|
||||
section.IsExecutable ? Path.GetFileName(dolTextPath) : null,
|
||||
section.IsExecutable && dolTextOffsets.TryGetValue(section.Name, out var imageOffset) ? imageOffset : 0));
|
||||
}
|
||||
|
||||
var relTextOffsets = WriteRelExecutableImage(rel, relImage, relTextPath);
|
||||
foreach (var section in rel.Sections.Where(s => s.Size > 0))
|
||||
{
|
||||
var guestStart = relImage.BaseAddress + section.FileOffset;
|
||||
sections.Add(new BaseSectionMetadata(
|
||||
$"{relSourceName}:{section.Index}",
|
||||
relSourceName,
|
||||
guestStart,
|
||||
checked(guestStart + section.Size),
|
||||
section.Executable,
|
||||
!section.Executable,
|
||||
section.Executable ? Path.GetFileName(relTextPath) : null,
|
||||
section.Executable && relTextOffsets.TryGetValue(section.Index, out var imageOffset) ? imageOffset : 0));
|
||||
}
|
||||
|
||||
var functions = BuildFunctionRanges(sections, generatedFunctionsDir, translationOutput);
|
||||
var manifest = new BaseManifest(
|
||||
manifestFormat,
|
||||
1,
|
||||
gameId,
|
||||
region,
|
||||
image.Sha256,
|
||||
relImage.BaseAddress,
|
||||
sections,
|
||||
functions,
|
||||
$"{fileStem}_function_ranges.json");
|
||||
|
||||
var manifestPath = Path.Combine(outputDir, $"{fileStem}_manifest.json");
|
||||
var functionRangesPath = Path.Combine(outputDir, manifest.FunctionRangesFile);
|
||||
|
||||
JsonWrite(manifestPath, manifest);
|
||||
JsonWrite(functionRangesPath, functions);
|
||||
|
||||
return new BaseManifestBuildResult
|
||||
{
|
||||
Manifest = manifest,
|
||||
ManifestPath = manifestPath,
|
||||
FunctionRangesPath = functionRangesPath
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, uint> WriteDolExecutableImage(DolFile dol, string path)
|
||||
{
|
||||
var offsets = new Dictionary<string, uint>(StringComparer.OrdinalIgnoreCase);
|
||||
using var output = File.Create(path);
|
||||
foreach (var section in dol.Sections.Where(s => s.IsExecutable && s.Size > 0).OrderBy(s => s.VirtualAddress))
|
||||
{
|
||||
offsets[section.Name] = checked((uint)output.Position);
|
||||
output.Write(section.Data.Span);
|
||||
}
|
||||
return offsets;
|
||||
}
|
||||
|
||||
private static Dictionary<int, uint> WriteRelExecutableImage(RelFile rel, RelImage relImage, string path)
|
||||
{
|
||||
var offsets = new Dictionary<int, uint>();
|
||||
using var output = File.Create(path);
|
||||
foreach (var section in rel.Sections.Where(s => s.Executable && s.Size > 0).OrderBy(s => s.FileOffset))
|
||||
{
|
||||
if (section.FileOffset + section.Size > relImage.Data.Length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
offsets[section.Index] = checked((uint)output.Position);
|
||||
output.Write(relImage.Data.AsSpan(checked((int)section.FileOffset), checked((int)section.Size)));
|
||||
}
|
||||
return offsets;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<BaseFunctionRangeMetadata> BuildFunctionRanges(
|
||||
IReadOnlyList<BaseSectionMetadata> sections,
|
||||
string generatedFunctionsDir,
|
||||
BaseTranslationOutputMetadata? translationOutput)
|
||||
{
|
||||
var starts = (translationOutput is null
|
||||
? DiscoverCanonicalGeneratedFunctionStarts(generatedFunctionsDir)
|
||||
: DiscoverCanonicalGeneratedFunctionStarts(translationOutput))
|
||||
.Where(start => sections.Any(s => s.Executable && start >= s.GuestStart && start < s.GuestEnd))
|
||||
.Distinct()
|
||||
.OrderBy(x => x)
|
||||
.ToList();
|
||||
|
||||
var bySection = sections.Where(s => s.Executable).OrderBy(s => s.GuestStart).ToList();
|
||||
var result = new List<BaseFunctionRangeMetadata>(starts.Count);
|
||||
|
||||
foreach (var section in bySection)
|
||||
{
|
||||
var sectionStarts = starts.Where(start => start >= section.GuestStart && start < section.GuestEnd).ToList();
|
||||
for (var i = 0; i < sectionStarts.Count; i++)
|
||||
{
|
||||
var start = sectionStarts[i];
|
||||
var end = i + 1 < sectionStarts.Count ? sectionStarts[i + 1] : section.GuestEnd;
|
||||
if (end <= start)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(new BaseFunctionRangeMetadata(
|
||||
start,
|
||||
end,
|
||||
$"func_{start:X8}",
|
||||
section.Name,
|
||||
checked(section.ImageOffset + (start - section.GuestStart)),
|
||||
"generated-function-starts-next-start-or-section-end",
|
||||
["Executable", "BaseFunction", "CanBeOverridden", "HasKnownTranslation"]));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static IReadOnlyList<uint> DiscoverCanonicalGeneratedFunctionStarts(string generatedFunctionsDir)
|
||||
{
|
||||
var files = new List<(uint Start, string Path)>();
|
||||
if (!Directory.Exists(generatedFunctionsDir))
|
||||
{
|
||||
return Array.Empty<uint>();
|
||||
}
|
||||
|
||||
foreach (var path in Directory.EnumerateFiles(generatedFunctionsDir, "*.cpp", SearchOption.AllDirectories))
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(path);
|
||||
var match = AddressRegex().Match(fileName);
|
||||
if (match.Success && GuestTargetParser.TryParseHexAddress(match.Groups[1].Value, out var start))
|
||||
{
|
||||
files.Add((start, path));
|
||||
}
|
||||
}
|
||||
|
||||
// Recursive discovery can emit a standalone translation for a switch
|
||||
// case label as well as the real function containing that label. Such
|
||||
// aliases must not split the base function range: an executable patch
|
||||
// in the case block has to rebuild and override the enclosing function,
|
||||
// otherwise its internal goto bypasses the alias overlay entirely.
|
||||
var embeddedAliases = new HashSet<uint>();
|
||||
foreach (var (ownerStart, path) in files)
|
||||
{
|
||||
var source = File.ReadAllText(path);
|
||||
foreach (Match match in LocalLabelRegex().Matches(source))
|
||||
{
|
||||
if (GuestTargetParser.TryParseHexAddress(match.Groups[1].Value, out var label) &&
|
||||
label > ownerStart)
|
||||
{
|
||||
embeddedAliases.Add(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
.Select(f => f.Start)
|
||||
.Where(start => !embeddedAliases.Contains(start))
|
||||
.Distinct()
|
||||
.OrderBy(start => start)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
internal static IReadOnlyList<uint> DiscoverCanonicalGeneratedFunctionStarts(
|
||||
BaseTranslationOutputMetadata translationOutput)
|
||||
{
|
||||
// Fall-through interior entries (no branch target, so the label check below can't see them)
|
||||
// are excluded, or splitting the ranges there would re-home a mod patch onto an entry no
|
||||
// caller dispatches to while real callers keep running the enclosing translation unpatched.
|
||||
var provenFunctions = translationOutput.Functions
|
||||
.Where(static function => !function.InteriorToOtherTranslation)
|
||||
.ToList();
|
||||
|
||||
// A separately emitted switch case can share an address with an internal
|
||||
// block of its owning translation. The analysis-owned labels retain that
|
||||
// relationship without parsing generated C++.
|
||||
var embeddedAliases = provenFunctions
|
||||
.SelectMany(function => function.LocalLabelAddresses
|
||||
.Where(label => label > function.EntryPoint))
|
||||
.ToHashSet();
|
||||
|
||||
return provenFunctions
|
||||
.Select(function => function.EntryPoint)
|
||||
.Where(start => !embeddedAliases.Contains(start))
|
||||
.Distinct()
|
||||
.OrderBy(start => start)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// The manifest and the function-range table are machine-read build inputs, so
|
||||
// they are written compact. The shared writer also makes them gated and
|
||||
// atomic; File.Create truncated the live file first, so an interrupted run
|
||||
// could publish a half-written manifest that later commands would parse.
|
||||
private static void JsonWrite<T>(string path, T value) =>
|
||||
JsonOutput.WriteIfChanged(path, value, JsonOutput.Compact);
|
||||
|
||||
[GeneratedRegex("([0-9A-Fa-f]{8})")]
|
||||
private static partial Regex AddressRegex();
|
||||
|
||||
[GeneratedRegex(@"\bloc_([0-9A-Fa-f]{8})\s*:")]
|
||||
private static partial Regex LocalLabelRegex();
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
using System.Text.Json;
|
||||
using Translator.Core.IO;
|
||||
using Translator.Core.Parsing.Kamek;
|
||||
|
||||
namespace Translator.Core.Mods;
|
||||
|
||||
/// <summary>
|
||||
/// A base translation only really depends on the Kamek patch addresses that land inside a translated
|
||||
/// function. This record stores those ranges plus which patched addresses fell inside them, so reuse
|
||||
/// can key on that instead of retranslating on every Code.pul digest change.
|
||||
/// </summary>
|
||||
public sealed record BaseTranslationModAwareness(
|
||||
string Format,
|
||||
int FormatVersion,
|
||||
/// <summary>
|
||||
/// Echo of the translation identity, when the caller supplied one. The launcher's local build
|
||||
/// does not, so this is not what ties the record to a translation: living in the same
|
||||
/// <c>generated</c> directory under the same success-only provenance stamp is.
|
||||
/// </summary>
|
||||
string? TranslationIdentityHash,
|
||||
/// <summary>
|
||||
/// How many functions the translation that wrote this record emitted. A consumer that also reads
|
||||
/// the output metadata compares the two, so a record left behind by a different translation of
|
||||
/// the same workspace cannot be mistaken for this one's.
|
||||
/// </summary>
|
||||
int TranslatedFunctionCount,
|
||||
IReadOnlyList<BaseTranslationModAwarenessProfile> Profiles,
|
||||
/// <summary>
|
||||
/// Sorted, coalesced <c>[start, endExclusive)</c> guest ranges of every translated function,
|
||||
/// flattened to <c>start0, end0, start1, end1, ...</c>. Containment in one of these is the whole
|
||||
/// test for whether a patched address was consequential.
|
||||
/// </summary>
|
||||
IReadOnlyList<uint> TranslatedFunctionRanges)
|
||||
{
|
||||
public const string CurrentFormat = "mkw-base-translation-mod-awareness";
|
||||
public const int CurrentFormatVersion = 1;
|
||||
|
||||
/// <summary>The file name this record is always written under, beside the output metadata.</summary>
|
||||
public const string FileName = "base_translation_mod_awareness.json";
|
||||
|
||||
public static BaseTranslationModAwareness Create(
|
||||
string? translationIdentityHash,
|
||||
int translatedFunctionCount,
|
||||
IEnumerable<(string Profile, string Region, string CodePulSha256, IReadOnlySet<uint> PatchedAddresses)> profiles,
|
||||
IReadOnlyDictionary<uint, uint> translatedFunctionEnds)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(profiles);
|
||||
ArgumentNullException.ThrowIfNull(translatedFunctionEnds);
|
||||
|
||||
var ranges = CoalesceRanges(translatedFunctionEnds);
|
||||
var recorded = profiles
|
||||
.Select(profile => new BaseTranslationModAwarenessProfile(
|
||||
profile.Profile,
|
||||
profile.Region,
|
||||
profile.CodePulSha256,
|
||||
Consequential(profile.PatchedAddresses, ranges)))
|
||||
.OrderBy(static profile => profile.Profile, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
return new BaseTranslationModAwareness(CurrentFormat, CurrentFormatVersion, translationIdentityHash,
|
||||
translatedFunctionCount, recorded, ranges);
|
||||
}
|
||||
|
||||
/// <summary>Whether a base translation carrying this record may be reused for
|
||||
/// <paramref name="codePulPath"/> under <paramref name="profileName"/>. Everything unexpected
|
||||
/// answers false with a reason, since a wrong "yes" silently mis-translates the product.</summary>
|
||||
public bool CoversCodePul(string profileName, string codePulPath, out string reason)
|
||||
{
|
||||
reason = string.Empty;
|
||||
if (Profiles.Count != 1)
|
||||
{
|
||||
// The recorded sets are per profile, but the decisions they describe were made from the
|
||||
// union of every profile the translation saw. Testing one profile's replacement against
|
||||
// that union is only equivalent when it is the only profile there is.
|
||||
reason = $"the base translation was produced from {Profiles.Count} mod profile(s), " +
|
||||
"so a single profile's Code.pul cannot be substituted in isolation";
|
||||
return false;
|
||||
}
|
||||
|
||||
var recorded = Profiles[0];
|
||||
if (!string.Equals(recorded.Profile, profileName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
reason = $"the base translation knows profile '{recorded.Profile}', not '{profileName}'";
|
||||
return false;
|
||||
}
|
||||
|
||||
IReadOnlyList<uint> candidate;
|
||||
try
|
||||
{
|
||||
candidate = Consequential(PatchedAddresses(codePulPath, recorded.Region), TranslatedFunctionRanges);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or InvalidDataException or ArgumentException)
|
||||
{
|
||||
reason = $"the candidate Code.pul could not be read as a Kamek patch set ({ex.Message})";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (candidate.Count != recorded.ConsequentialPatchedAddresses.Count)
|
||||
{
|
||||
reason = $"it patches {candidate.Count} translated function address(es), " +
|
||||
$"against the {recorded.ConsequentialPatchedAddresses.Count} this translation was built around";
|
||||
return false;
|
||||
}
|
||||
for (var i = 0; i < candidate.Count; i++)
|
||||
{
|
||||
if (candidate[i] == recorded.ConsequentialPatchedAddresses[i]) continue;
|
||||
reason = $"it moves a patch onto translated function address 0x{candidate[i]:X8}";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>The absolute Kamek command addresses one Code.pul applies in one region.</summary>
|
||||
public static IReadOnlySet<uint> PatchedAddresses(string codePulPath, string region)
|
||||
{
|
||||
var chunk = KamekPulFile.Load(codePulPath).SelectRegion(region);
|
||||
var patched = new HashSet<uint>();
|
||||
foreach (var command in chunk.Commands)
|
||||
{
|
||||
if (command.AddressIsAbsolute) patched.Add(command.Address);
|
||||
}
|
||||
return patched;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<uint> Consequential(IReadOnlySet<uint> patched, IReadOnlyList<uint> ranges)
|
||||
{
|
||||
var consequential = new List<uint>();
|
||||
foreach (var address in patched)
|
||||
{
|
||||
if (Contains(ranges, address)) consequential.Add(address);
|
||||
}
|
||||
consequential.Sort();
|
||||
return consequential;
|
||||
}
|
||||
|
||||
/// <summary>Binary search over the flattened, non-overlapping, ascending range pairs.</summary>
|
||||
private static bool Contains(IReadOnlyList<uint> ranges, uint address)
|
||||
{
|
||||
var low = 0;
|
||||
var high = (ranges.Count / 2) - 1;
|
||||
while (low <= high)
|
||||
{
|
||||
var middle = low + ((high - low) / 2);
|
||||
var start = ranges[middle * 2];
|
||||
var end = ranges[(middle * 2) + 1];
|
||||
if (address < start) high = middle - 1;
|
||||
else if (address >= end) low = middle + 1;
|
||||
else return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static uint[] CoalesceRanges(IReadOnlyDictionary<uint, uint> translatedFunctionEnds)
|
||||
{
|
||||
var ordered = translatedFunctionEnds
|
||||
.Where(static entry => entry.Value > entry.Key)
|
||||
.OrderBy(static entry => entry.Key)
|
||||
.ToArray();
|
||||
var flattened = new List<uint>(ordered.Length * 2);
|
||||
foreach (var (start, end) in ordered)
|
||||
{
|
||||
if (flattened.Count != 0 && start <= flattened[^1])
|
||||
{
|
||||
if (end > flattened[^1]) flattened[^1] = end;
|
||||
continue;
|
||||
}
|
||||
flattened.Add(start);
|
||||
flattened.Add(end);
|
||||
}
|
||||
return flattened.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>One mod profile's contribution to a base translation's mod-patch awareness.</summary>
|
||||
public sealed record BaseTranslationModAwarenessProfile(
|
||||
string Profile,
|
||||
string Region,
|
||||
string CodePulSha256,
|
||||
/// <summary>Ascending; the patched addresses that landed inside a translated function.</summary>
|
||||
IReadOnlyList<uint> ConsequentialPatchedAddresses);
|
||||
|
||||
public static class BaseTranslationModAwarenessFile
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = JsonOutput.CompactCamelCase;
|
||||
|
||||
public static BaseTranslationModAwareness Read(string path)
|
||||
{
|
||||
var awareness = JsonSerializer.Deserialize<BaseTranslationModAwareness>(File.ReadAllText(path), JsonOptions)
|
||||
?? throw new InvalidDataException($"Base translation mod-awareness record is empty: {path}");
|
||||
Validate(awareness, path);
|
||||
return awareness;
|
||||
}
|
||||
|
||||
public static BaseTranslationModAwareness? TryRead(string path)
|
||||
{
|
||||
if (!File.Exists(path)) return null;
|
||||
try { return Read(path); }
|
||||
catch (Exception ex) when (ex is IOException or JsonException or InvalidDataException) { return null; }
|
||||
}
|
||||
|
||||
public static bool WriteIfChangedAtomic(string path, BaseTranslationModAwareness awareness)
|
||||
{
|
||||
Validate(awareness, path);
|
||||
return JsonOutput.WriteIfChanged(path, awareness, JsonOptions);
|
||||
}
|
||||
|
||||
private static void Validate(BaseTranslationModAwareness awareness, string source)
|
||||
{
|
||||
if (!string.Equals(awareness.Format, BaseTranslationModAwareness.CurrentFormat, StringComparison.Ordinal) ||
|
||||
awareness.FormatVersion != BaseTranslationModAwareness.CurrentFormatVersion)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Unsupported base translation mod-awareness record in '{source}': " +
|
||||
$"{awareness.Format} v{awareness.FormatVersion}.");
|
||||
}
|
||||
if (awareness.TranslatedFunctionCount <= 0)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Base translation mod-awareness record has no translated function count in '{source}'.");
|
||||
}
|
||||
if (awareness.TranslatedFunctionRanges.Count == 0 || (awareness.TranslatedFunctionRanges.Count % 2) != 0)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Base translation mod-awareness record has a malformed range table in '{source}'.");
|
||||
}
|
||||
for (var i = 0; i < awareness.TranslatedFunctionRanges.Count; i += 2)
|
||||
{
|
||||
if (awareness.TranslatedFunctionRanges[i] >= awareness.TranslatedFunctionRanges[i + 1] ||
|
||||
(i > 0 && awareness.TranslatedFunctionRanges[i] < awareness.TranslatedFunctionRanges[i - 1]))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Base translation mod-awareness ranges are not ascending and disjoint in '{source}'.");
|
||||
}
|
||||
}
|
||||
foreach (var profile in awareness.Profiles)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(profile.Profile) || profile.CodePulSha256.Length != 64)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Base translation mod-awareness record has an invalid profile in '{source}'.");
|
||||
}
|
||||
for (var i = 1; i < profile.ConsequentialPatchedAddresses.Count; i++)
|
||||
{
|
||||
if (profile.ConsequentialPatchedAddresses[i] <= profile.ConsequentialPatchedAddresses[i - 1])
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Base translation mod-awareness addresses are not ascending in '{source}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Translator.Core.IO;
|
||||
using Translator.Core.Translation;
|
||||
using Translator.Core.Loading;
|
||||
|
||||
namespace Translator.Core.Mods;
|
||||
|
||||
/// <summary>Compact, deterministic description of a base translation output tree, the source of truth
|
||||
/// for downstream manifests and pruning so they don't need to parse tens of thousands of C++ files.</summary>
|
||||
public sealed record BaseTranslationOutputMetadata(
|
||||
string Format,
|
||||
int FormatVersion,
|
||||
string? TranslationIdentityHash,
|
||||
TranslationQualityMetadata Quality,
|
||||
IReadOnlyList<BaseTranslationFunctionMetadata> Functions,
|
||||
string? SourceBundlePath = null,
|
||||
IReadOnlyList<BaseTranslationModPatchAwareness>? ModPatchAwareness = null)
|
||||
{
|
||||
public const string CurrentFormat = "mkw-base-translation-output";
|
||||
public const int CurrentFormatVersion = 2;
|
||||
|
||||
public static BaseTranslationOutputMetadata Create(
|
||||
IEnumerable<BaseTranslationFunctionMetadata> functions,
|
||||
TranslationQualityMetadata quality,
|
||||
string? translationIdentityHash = null,
|
||||
string? sourceBundlePath = null,
|
||||
IReadOnlyList<BaseTranslationModPatchAwareness>? modPatchAwareness = null)
|
||||
{
|
||||
var ordered = functions
|
||||
.OrderBy(function => function.RelativePath, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (ordered.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Base translation output metadata cannot be empty.");
|
||||
}
|
||||
quality.Validate("base translation output metadata");
|
||||
|
||||
var duplicatePath = ordered
|
||||
.GroupBy(function => function.RelativePath, StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(group => group.Count() > 1);
|
||||
if (duplicatePath is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Base translation output metadata contains duplicate path '{duplicatePath.Key}'.");
|
||||
}
|
||||
|
||||
return new BaseTranslationOutputMetadata(
|
||||
CurrentFormat,
|
||||
CurrentFormatVersion,
|
||||
translationIdentityHash,
|
||||
quality,
|
||||
ordered,
|
||||
sourceBundlePath,
|
||||
modPatchAwareness);
|
||||
}
|
||||
|
||||
public void RequireReleaseEligible(string source)
|
||||
{
|
||||
Quality.Validate(source);
|
||||
if (Quality.UnsupportedInstructionCount != 0 || Quality.InvalidSsaFunctionCount != 0)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Release translation quality failure in '{source}': " +
|
||||
$"{Quality.UnsupportedInstructionCount} unsupported instruction(s), " +
|
||||
$"{Quality.InvalidSsaFunctionCount} invalid SSA function(s).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record TranslationQualityMetadata(
|
||||
int UnsupportedInstructionCount,
|
||||
int InvalidSsaFunctionCount)
|
||||
{
|
||||
public static TranslationQualityMetadata Clean { get; } = new(0, 0);
|
||||
|
||||
internal void Validate(string source)
|
||||
{
|
||||
if (UnsupportedInstructionCount < 0 || InvalidSsaFunctionCount < 0)
|
||||
{
|
||||
throw new InvalidDataException($"Translation quality counts cannot be negative in '{source}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records that the base translation knows one mod profile's patch set (leaf-inlining blocks and
|
||||
/// residency fences at every address the mod can win). A mod translation must refuse a base tree
|
||||
/// whose awareness doesn't cover its own Code.pul, or it silently bakes in vanilla code paths.
|
||||
/// </summary>
|
||||
public sealed record BaseTranslationModPatchAwareness(
|
||||
string Profile,
|
||||
string CodePulSha256);
|
||||
|
||||
public sealed record BaseTranslationFunctionMetadata(
|
||||
string RelativePath,
|
||||
long Size,
|
||||
string Sha256,
|
||||
uint EntryPoint,
|
||||
IReadOnlyList<uint> LocalLabelAddresses,
|
||||
BaseTranslationFunctionBuildMetadata? Build = null,
|
||||
// True for a fall-through interior address (e.g. a split-switch artifact) that another
|
||||
// translated function's control flow already executes. Registered and dispatchable, but the
|
||||
// base manifest must not let it split function ranges: patches have to rebuild the enclosing
|
||||
// function since every real caller runs its inline copy of the bytes, not this alias.
|
||||
bool InteriorToOtherTranslation = false)
|
||||
{
|
||||
public static BaseTranslationFunctionMetadata FromTranslation(
|
||||
string outputRoot,
|
||||
string outputPath,
|
||||
FunctionTranslationResult translation)
|
||||
{
|
||||
var root = Path.GetFullPath(outputRoot).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
var path = Path.GetFullPath(outputPath);
|
||||
var prefix = root + Path.DirectorySeparatorChar;
|
||||
if (!path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Generated function path '{path}' is outside output root '{root}'.");
|
||||
}
|
||||
|
||||
var relativePath = path[prefix.Length..].Replace(Path.DirectorySeparatorChar, '/');
|
||||
var bytes = Encoding.UTF8.GetBytes(translation.CxxCode);
|
||||
var localLabels = translation.Ssa.Function.Blocks
|
||||
.Select(block => TryParseLocalLabelAddress(block.Label))
|
||||
.Where(address => address.HasValue)
|
||||
.Select(address => address!.Value)
|
||||
.Where(address => address > translation.EntryPoint)
|
||||
.Distinct()
|
||||
.OrderBy(address => address)
|
||||
.ToArray();
|
||||
|
||||
// The emitter reports what it emitted. Recovering these facts by running
|
||||
// regular expressions over C++ text produced in this same process was a
|
||||
// silent-failure hazard; the comment markers inside the text remain the
|
||||
// cross-run persistence format that build sharding parses from files.
|
||||
var emission = translation.Emission;
|
||||
var registration = emission?.Registration
|
||||
?? throw new InvalidDataException(
|
||||
$"Translated function 0x{translation.EntryPoint:X8} has no build registration metadata.");
|
||||
var directCalls = emission!.EmittedDirectCallTargets;
|
||||
var abiComment = emission.GuestAbiMarker;
|
||||
var build = new BaseTranslationFunctionBuildMetadata(
|
||||
registration.Symbol,
|
||||
registration.PreservesNonvolatileFprs,
|
||||
registration.NonvolatileFprWriteMask,
|
||||
directCalls,
|
||||
string.IsNullOrWhiteSpace(abiComment) ? null : abiComment,
|
||||
translation.Ssa.Function.Blocks.Sum(block => block.Instructions.Count));
|
||||
|
||||
return new BaseTranslationFunctionMetadata(
|
||||
relativePath,
|
||||
bytes.LongLength,
|
||||
ChecksumUtilities.Sha256Hex(bytes),
|
||||
translation.EntryPoint,
|
||||
localLabels,
|
||||
build);
|
||||
}
|
||||
|
||||
private static uint? TryParseLocalLabelAddress(string label) =>
|
||||
GuestTargetParser.TryParseLocalLabelAddress(label);
|
||||
}
|
||||
|
||||
public sealed record BaseTranslationFunctionBuildMetadata(
|
||||
string Symbol,
|
||||
bool PreservesNonvolatileFprs,
|
||||
uint NonvolatileFprWriteMask,
|
||||
IReadOnlyList<uint> DirectCallDependencies,
|
||||
string? GuestAbiComment,
|
||||
int EstimatedIrInstructions);
|
||||
|
||||
public static class BaseTranslationOutputMetadataFile
|
||||
{
|
||||
// Machine-read only: indentation tripled the payload of a file that already
|
||||
// measures tens of megabytes and is never opened by a human.
|
||||
private static readonly JsonSerializerOptions JsonOptions = JsonOutput.CompactCamelCase;
|
||||
|
||||
public static BaseTranslationOutputMetadata Read(string path)
|
||||
{
|
||||
var metadata = JsonSerializer.Deserialize<BaseTranslationOutputMetadata>(File.ReadAllText(path), JsonOptions)
|
||||
?? throw new InvalidDataException($"Base translation output metadata is empty: {path}");
|
||||
Validate(metadata, path);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public static bool WriteIfChangedAtomic(string path, BaseTranslationOutputMetadata metadata)
|
||||
{
|
||||
Validate(metadata, path);
|
||||
// Serialise straight into a byte buffer. The previous shape built a
|
||||
// multi-megabyte UTF-16 string and then read the whole live file back as a
|
||||
// second one purely to answer "did anything change?".
|
||||
return JsonOutput.WriteIfChanged(path, metadata, JsonOptions);
|
||||
}
|
||||
|
||||
private static void Validate(BaseTranslationOutputMetadata metadata, string source)
|
||||
{
|
||||
if (!string.Equals(metadata.Format, BaseTranslationOutputMetadata.CurrentFormat, StringComparison.Ordinal) ||
|
||||
metadata.FormatVersion != BaseTranslationOutputMetadata.CurrentFormatVersion)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Unsupported base translation output metadata in '{source}': {metadata.Format} v{metadata.FormatVersion}.");
|
||||
}
|
||||
if (metadata.Quality is null)
|
||||
{
|
||||
throw new InvalidDataException($"Base translation output metadata has no quality counters in '{source}'.");
|
||||
}
|
||||
metadata.Quality.Validate(source);
|
||||
|
||||
var seenPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var seenEntries = new HashSet<uint>();
|
||||
foreach (var function in metadata.Functions)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(function.RelativePath) ||
|
||||
Path.IsPathRooted(function.RelativePath) ||
|
||||
function.RelativePath.Split('/', '\\').Any(component => component == "..") ||
|
||||
!function.RelativePath.EndsWith(".cpp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Invalid generated function path '{function.RelativePath}' in '{source}'.");
|
||||
}
|
||||
if (!seenPaths.Add(function.RelativePath) || !seenEntries.Add(function.EntryPoint))
|
||||
{
|
||||
throw new InvalidDataException($"Duplicate generated function entry in '{source}'.");
|
||||
}
|
||||
if (function.Size < 0 || function.Sha256.Length != 64 ||
|
||||
!IsHexadecimal(function.Sha256))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Invalid size or SHA-256 for '{function.RelativePath}' in '{source}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enumerable.All over a string allocates a char enumerator and a delegate
|
||||
// invocation per character, for tens of thousands of digests per run.
|
||||
private static bool IsHexadecimal(string value)
|
||||
{
|
||||
foreach (var character in value.AsSpan())
|
||||
{
|
||||
if (!Uri.IsHexDigit(character)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text.Json;
|
||||
using Translator.Core.Disassembly;
|
||||
using Translator.Core.Parsing.Kamek;
|
||||
using Translator.Core.Mods.Mkwii;
|
||||
|
||||
namespace Translator.Core.Mods;
|
||||
|
||||
public sealed record ContinuationEntry(
|
||||
uint Address,
|
||||
uint ContainingFunctionStart,
|
||||
uint ContainingFunctionEnd,
|
||||
string SectionName,
|
||||
uint SourceCommandAddress,
|
||||
KamekCommandId SourceCommandId,
|
||||
string Reason);
|
||||
|
||||
public sealed class ContinuationPlan
|
||||
{
|
||||
public required IReadOnlyList<ContinuationEntry> Entries { get; init; }
|
||||
}
|
||||
|
||||
public static class ContinuationPlanner
|
||||
{
|
||||
private const uint BctrInstruction = 0x4E800420u;
|
||||
private const uint BlrInstruction = 0x4E800020u;
|
||||
private const int MaxTailJumpConstantLookbackBytes = 64;
|
||||
|
||||
public static ContinuationPlan Build(KamekChunk chunk, BaseManifest baseManifest, uint moduleGuestBase)
|
||||
{
|
||||
var functionIndex = new BaseFunctionIndex(baseManifest.Functions);
|
||||
var entries = new Dictionary<uint, ContinuationEntry>();
|
||||
|
||||
foreach (var command in chunk.Commands)
|
||||
{
|
||||
if (!IsBranchLikeTarget(command.Id) || command.Arguments.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var target = KamekAddress.Resolve(command.Arguments[0], moduleGuestBase);
|
||||
var section = FindSection(baseManifest, target);
|
||||
if (section is null || !section.Executable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var function = functionIndex.FindContaining(target);
|
||||
if (function is null || function.Start == target)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.TryAdd(target, new ContinuationEntry(
|
||||
target,
|
||||
function.Start,
|
||||
function.End,
|
||||
section.Name,
|
||||
command.AddressIsRelative ? checked(moduleGuestBase + command.Address) : command.Address,
|
||||
command.Id,
|
||||
"branch-like Code.pul target lands inside a base function"));
|
||||
}
|
||||
|
||||
return new ContinuationPlan
|
||||
{
|
||||
Entries = entries.Values.OrderBy(e => e.Address).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public static ContinuationPlan AddModuleTailJumpContinuations(
|
||||
ContinuationPlan plan,
|
||||
BaseManifest baseManifest,
|
||||
uint moduleGuestBase,
|
||||
byte[] relocatedModuleImage)
|
||||
{
|
||||
if (relocatedModuleImage.Length < 4)
|
||||
{
|
||||
return plan;
|
||||
}
|
||||
|
||||
var functionIndex = new BaseFunctionIndex(baseManifest.Functions);
|
||||
var entries = plan.Entries.ToDictionary(e => e.Address);
|
||||
|
||||
foreach (var tailJump in DiscoverModuleTailJumps(moduleGuestBase, relocatedModuleImage))
|
||||
{
|
||||
var section = FindSection(baseManifest, tailJump.TargetAddress);
|
||||
if (section is null || !section.Executable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var function = functionIndex.FindContaining(tailJump.TargetAddress);
|
||||
if (function is null || function.Start == tailJump.TargetAddress)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.TryAdd(tailJump.TargetAddress, new ContinuationEntry(
|
||||
tailJump.TargetAddress,
|
||||
function.Start,
|
||||
function.End,
|
||||
section.Name,
|
||||
tailJump.SourceAddress,
|
||||
KamekCommandId.Branch,
|
||||
"Kamek module tail jump lands inside a base function"));
|
||||
}
|
||||
|
||||
return new ContinuationPlan
|
||||
{
|
||||
Entries = entries.Values.OrderBy(e => e.Address).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public static ContinuationPlan AddRetroWfcExecutableHookContinuations(
|
||||
ContinuationPlan plan,
|
||||
BaseManifest baseManifest,
|
||||
IEnumerable<RetroWfcExecutableHookPlan> hooks)
|
||||
{
|
||||
var functionIndex = new BaseFunctionIndex(baseManifest.Functions);
|
||||
var entries = plan.Entries.ToDictionary(e => e.Address);
|
||||
|
||||
foreach (var hook in hooks)
|
||||
{
|
||||
var target = hook.ContinuationAddress;
|
||||
var section = FindSection(baseManifest, target);
|
||||
if (section is null || !section.Executable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var function = functionIndex.FindContaining(target);
|
||||
if (function is null || function.Start == target)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var action = hook.TargetActionId ?? string.Join(",", hook.SemanticActionIds);
|
||||
entries.TryAdd(target, new ContinuationEntry(
|
||||
target,
|
||||
function.Start,
|
||||
function.End,
|
||||
section.Name,
|
||||
hook.Address,
|
||||
KamekCommandId.Branch,
|
||||
$"Retro WFC executable hook continuation {action}"));
|
||||
}
|
||||
|
||||
return new ContinuationPlan
|
||||
{
|
||||
Entries = entries.Values.OrderBy(e => e.Address).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static bool IsBranchLikeTarget(KamekCommandId id) =>
|
||||
id is KamekCommandId.Rel24 or KamekCommandId.Branch or KamekCommandId.BranchLink;
|
||||
|
||||
private static BaseSectionMetadata? FindSection(BaseManifest manifest, uint address) =>
|
||||
manifest.Sections.FirstOrDefault(section => address >= section.GuestStart && address < section.GuestEnd);
|
||||
|
||||
private static IEnumerable<ModuleTailJump> DiscoverModuleTailJumps(
|
||||
uint moduleGuestBase,
|
||||
byte[] relocatedModuleImage)
|
||||
{
|
||||
for (var offset = 0; offset + 4 <= relocatedModuleImage.Length; offset += 4)
|
||||
{
|
||||
var word = PpcWordFields.ReadBigEndianWord(relocatedModuleImage, offset);
|
||||
if (word != BctrInstruction && word != BlrInstruction)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var sprWriteOffset = offset - 4;
|
||||
if (sprWriteOffset < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var sprWrite = PpcWordFields.ReadBigEndianWord(relocatedModuleImage, sprWriteOffset);
|
||||
int sourceRegister;
|
||||
var hasRegisterSource = word == BctrInstruction
|
||||
? PpcInstructionPatterns.TryGetMtspr(sprWrite, 9, out sourceRegister)
|
||||
: PpcInstructionPatterns.TryGetMtspr(sprWrite, 8, out sourceRegister);
|
||||
if (!hasRegisterSource)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryResolveConstantRegisterValue(
|
||||
relocatedModuleImage,
|
||||
sprWriteOffset,
|
||||
sourceRegister,
|
||||
out var targetAddress))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return new ModuleTailJump(
|
||||
checked(moduleGuestBase + (uint)offset),
|
||||
targetAddress);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryResolveConstantRegisterValue(
|
||||
byte[] relocatedModuleImage,
|
||||
int beforeOffset,
|
||||
int register,
|
||||
out uint value)
|
||||
{
|
||||
var lowOperation = LowImmediateOperation.None;
|
||||
var lowImmediate = 0u;
|
||||
var scanStart = Math.Max(0, beforeOffset - MaxTailJumpConstantLookbackBytes);
|
||||
|
||||
for (var offset = beforeOffset - 4; offset >= scanStart; offset -= 4)
|
||||
{
|
||||
var word = PpcWordFields.ReadBigEndianWord(relocatedModuleImage, offset);
|
||||
|
||||
if (PpcInstructionPatterns.TryGetOri(word, out var oriSource, out var oriDestination, out var oriImmediate) &&
|
||||
oriDestination == register)
|
||||
{
|
||||
if (oriSource != register || lowOperation != LowImmediateOperation.None)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
lowOperation = LowImmediateOperation.Or;
|
||||
lowImmediate = oriImmediate;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (PpcInstructionPatterns.TryGetAddi(word, out var addiDestination, out var addiSource, out var addiImmediate) &&
|
||||
addiDestination == register)
|
||||
{
|
||||
if (addiSource != register || lowOperation != LowImmediateOperation.None)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
lowOperation = LowImmediateOperation.AddSigned;
|
||||
lowImmediate = unchecked((uint)addiImmediate);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (PpcInstructionPatterns.TryGetLis(word, out var lisDestination, out var highImmediate) &&
|
||||
lisDestination == register)
|
||||
{
|
||||
var baseValue = highImmediate << 16;
|
||||
value = lowOperation switch
|
||||
{
|
||||
LowImmediateOperation.None => baseValue,
|
||||
LowImmediateOperation.Or => baseValue | lowImmediate,
|
||||
LowImmediateOperation.AddSigned => unchecked(baseValue + (uint)(short)lowImmediate),
|
||||
_ => baseValue
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (PpcRegisterEffects.MayWriteGpr(word, register))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private sealed record ModuleTailJump(uint SourceAddress, uint TargetAddress);
|
||||
|
||||
private enum LowImmediateOperation
|
||||
{
|
||||
None,
|
||||
Or,
|
||||
AddSigned
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
using System.Text.Json;
|
||||
using Translator.Core.Parsing.Kamek;
|
||||
|
||||
namespace Translator.Core.Mods;
|
||||
|
||||
public enum KamekPatchClassificationKind
|
||||
{
|
||||
ModuleCommand,
|
||||
BaseExecutablePatch,
|
||||
BaseDataPatch,
|
||||
DroppedReservedRegion,
|
||||
Unsupported
|
||||
}
|
||||
|
||||
public sealed record KamekPatchClassification(
|
||||
KamekPatchClassificationKind Kind,
|
||||
KamekCommandId CommandId,
|
||||
uint CommandAddress,
|
||||
bool CommandAddressIsAbsolute,
|
||||
uint? ContainingFunctionStart,
|
||||
uint? ContainingFunctionEnd,
|
||||
string? SectionName,
|
||||
string Reason,
|
||||
IReadOnlyList<uint> Arguments);
|
||||
|
||||
public sealed class KamekPatchPlan
|
||||
{
|
||||
public required uint ModuleGuestBase { get; init; }
|
||||
public required IReadOnlyList<KamekPatchClassification> Classifications { get; init; }
|
||||
|
||||
public IEnumerable<KamekPatchClassification> ModuleCommands =>
|
||||
Classifications.Where(c => c.Kind == KamekPatchClassificationKind.ModuleCommand);
|
||||
|
||||
public IEnumerable<KamekPatchClassification> ExecutablePatches =>
|
||||
Classifications.Where(c => c.Kind == KamekPatchClassificationKind.BaseExecutablePatch);
|
||||
|
||||
public IEnumerable<KamekPatchClassification> DataPatches =>
|
||||
Classifications.Where(c => c.Kind == KamekPatchClassificationKind.BaseDataPatch);
|
||||
|
||||
public IEnumerable<KamekPatchClassification> DroppedReservedRegionWrites =>
|
||||
Classifications.Where(c => c.Kind == KamekPatchClassificationKind.DroppedReservedRegion);
|
||||
|
||||
public IEnumerable<KamekPatchClassification> Unsupported =>
|
||||
Classifications.Where(c => c.Kind == KamekPatchClassificationKind.Unsupported);
|
||||
|
||||
public string BuildTextReport(string title)
|
||||
{
|
||||
using var writer = new StringWriter();
|
||||
writer.WriteLine(title);
|
||||
writer.WriteLine($"Module base: 0x{ModuleGuestBase:X8}");
|
||||
writer.WriteLine();
|
||||
writer.WriteLine("Classification summary:");
|
||||
foreach (var group in Classifications.GroupBy(c => c.Kind).OrderBy(g => g.Key.ToString()))
|
||||
{
|
||||
writer.WriteLine($" {group.Key,-22} {group.Count(),8:N0}");
|
||||
}
|
||||
writer.WriteLine();
|
||||
|
||||
writer.WriteLine("Executable overlays:");
|
||||
foreach (var group in ExecutablePatches
|
||||
.Where(c => c.ContainingFunctionStart.HasValue && c.ContainingFunctionEnd.HasValue)
|
||||
.GroupBy(c => (Start: c.ContainingFunctionStart!.Value, End: c.ContainingFunctionEnd!.Value, c.SectionName))
|
||||
.OrderBy(g => g.Key.Start))
|
||||
{
|
||||
writer.WriteLine($" 0x{group.Key.Start:X8}-0x{group.Key.End:X8} {group.Key.SectionName}");
|
||||
foreach (var item in group.OrderBy(c => c.CommandAddress).Take(32))
|
||||
{
|
||||
writer.WriteLine($" 0x{item.CommandAddress:X8} {item.CommandId} {FormatArgs(item.Arguments)}");
|
||||
}
|
||||
if (group.Count() > 32)
|
||||
{
|
||||
writer.WriteLine($" ... {group.Count() - 32:N0} more");
|
||||
}
|
||||
}
|
||||
if (!ExecutablePatches.Any())
|
||||
{
|
||||
writer.WriteLine(" none");
|
||||
}
|
||||
writer.WriteLine();
|
||||
|
||||
writer.WriteLine("Data patches:");
|
||||
foreach (var group in DataPatches.GroupBy(c => c.SectionName ?? "<unknown>").OrderBy(g => g.Key))
|
||||
{
|
||||
writer.WriteLine($" {group.Key}: {group.Count():N0}");
|
||||
}
|
||||
if (!DataPatches.Any())
|
||||
{
|
||||
writer.WriteLine(" none");
|
||||
}
|
||||
writer.WriteLine();
|
||||
|
||||
writer.WriteLine("Unsupported:");
|
||||
foreach (var item in Unsupported.Take(128))
|
||||
{
|
||||
writer.WriteLine($" 0x{item.CommandAddress:X8} {item.CommandId}: {item.Reason}");
|
||||
}
|
||||
if (!Unsupported.Any())
|
||||
{
|
||||
writer.WriteLine(" none");
|
||||
}
|
||||
else if (Unsupported.Count() > 128)
|
||||
{
|
||||
writer.WriteLine($" ... {Unsupported.Count() - 128:N0} more");
|
||||
}
|
||||
|
||||
return writer.ToString();
|
||||
}
|
||||
|
||||
private static string FormatArgs(IReadOnlyList<uint> args) =>
|
||||
args.Count == 0 ? string.Empty : string.Join(", ", args.Select(a => $"0x{a:X8}"));
|
||||
}
|
||||
|
||||
public static class KamekPatchPlanner
|
||||
{
|
||||
private const uint CodehandlerRegionStart = 0x80001920;
|
||||
private const uint CodehandlerRegionEnd = 0x80001924;
|
||||
|
||||
public static KamekPatchPlan Build(KamekChunk chunk, BaseManifest baseManifest, uint moduleGuestBase)
|
||||
{
|
||||
var functionIndex = new BaseFunctionIndex(baseManifest.Functions);
|
||||
var classifications = new List<KamekPatchClassification>(chunk.Commands.Count);
|
||||
|
||||
foreach (var command in chunk.Commands)
|
||||
{
|
||||
if (command.AddressIsRelative)
|
||||
{
|
||||
classifications.Add(new KamekPatchClassification(
|
||||
KamekPatchClassificationKind.ModuleCommand,
|
||||
command.Id,
|
||||
moduleGuestBase + command.Address,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
"Kamek module",
|
||||
"relative command applies to Kamek module image",
|
||||
command.Arguments));
|
||||
continue;
|
||||
}
|
||||
|
||||
var section = FindSection(baseManifest, command.Address);
|
||||
if (section is null)
|
||||
{
|
||||
classifications.Add(Unsupported(command, "absolute command address is outside known base sections"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!section.Executable &&
|
||||
command.Address >= CodehandlerRegionStart && command.Address < CodehandlerRegionEnd)
|
||||
{
|
||||
classifications.Add(new KamekPatchClassification(
|
||||
KamekPatchClassificationKind.DroppedReservedRegion,
|
||||
command.Id,
|
||||
command.Address,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
section.Name,
|
||||
"write to the WFC codehandler probe word is dropped; the recomp keeps 0x80001920 clean for the WFC anticheat probe",
|
||||
command.Arguments));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (section.Executable)
|
||||
{
|
||||
var function = functionIndex.FindContaining(command.Address);
|
||||
if (function is null)
|
||||
{
|
||||
classifications.Add(Unsupported(command, $"executable address is in {section.Name} but no containing function range was found"));
|
||||
continue;
|
||||
}
|
||||
|
||||
classifications.Add(new KamekPatchClassification(
|
||||
KamekPatchClassificationKind.BaseExecutablePatch,
|
||||
command.Id,
|
||||
command.Address,
|
||||
true,
|
||||
function.Start,
|
||||
function.End,
|
||||
section.Name,
|
||||
"absolute executable command requires patched base overlay",
|
||||
command.Arguments));
|
||||
continue;
|
||||
}
|
||||
|
||||
classifications.Add(new KamekPatchClassification(
|
||||
KamekPatchClassificationKind.BaseDataPatch,
|
||||
command.Id,
|
||||
command.Address,
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
section.Name,
|
||||
"absolute non-executable command applies as data patch",
|
||||
command.Arguments));
|
||||
}
|
||||
|
||||
return new KamekPatchPlan
|
||||
{
|
||||
ModuleGuestBase = moduleGuestBase,
|
||||
Classifications = classifications
|
||||
};
|
||||
}
|
||||
|
||||
private static KamekPatchClassification Unsupported(KamekCommand command, string reason) =>
|
||||
new(
|
||||
KamekPatchClassificationKind.Unsupported,
|
||||
command.Id,
|
||||
command.Address,
|
||||
command.AddressIsAbsolute,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
reason,
|
||||
command.Arguments);
|
||||
|
||||
private static BaseSectionMetadata? FindSection(BaseManifest manifest, uint address) =>
|
||||
manifest.Sections.FirstOrDefault(section => address >= section.GuestStart && address < section.GuestEnd);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Translator.Core.Parsing.Kamek;
|
||||
|
||||
namespace Translator.Core.Mods.Mkwii;
|
||||
|
||||
public static class RetroWfcBootstrapSuppressor
|
||||
{
|
||||
public static KamekChunk Suppress(KamekChunk chunk, uint hookAddress)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(chunk);
|
||||
|
||||
var matching = chunk.Commands
|
||||
.Where(command => command.AddressIsAbsolute && command.Address == hookAddress)
|
||||
.ToArray();
|
||||
if (matching.Length != 1 ||
|
||||
matching[0].Id != KamekCommandId.Branch ||
|
||||
matching[0].Arguments.Count != 1)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Expected exactly one absolute Branch command at the configured Retro WFC legacy bootstrap hook " +
|
||||
$"0x{hookAddress:X8}, but found {matching.Length} matching command(s).");
|
||||
}
|
||||
|
||||
var commands = chunk.Commands
|
||||
.Where(command => !ReferenceEquals(command, matching[0]))
|
||||
.ToArray();
|
||||
return new KamekChunk(
|
||||
chunk.Index,
|
||||
chunk.FileOffset,
|
||||
chunk.BssSize,
|
||||
chunk.CodeSize,
|
||||
chunk.CtorStart,
|
||||
chunk.CtorEnd,
|
||||
chunk.ChunkSize,
|
||||
chunk.CodeBlob,
|
||||
commands);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
using System.Text.Json;
|
||||
using static Translator.Core.Mods.Mkwii.RetroWfcJson;
|
||||
|
||||
namespace Translator.Core.Mods.Mkwii;
|
||||
|
||||
public sealed record RetroWfcStaticBytePatchPlan(
|
||||
int RecordIndex,
|
||||
string Intent,
|
||||
uint Address,
|
||||
int Size,
|
||||
string SectionName,
|
||||
bool SectionExecutable,
|
||||
uint? ContainingFunctionStart,
|
||||
uint? ContainingFunctionEnd,
|
||||
string SourceSha256,
|
||||
string BytesHex,
|
||||
string LoweringKind);
|
||||
|
||||
public sealed record RetroWfcExecutableHookPlan(
|
||||
int RecordIndex,
|
||||
string Intent,
|
||||
string TypeName,
|
||||
uint Address,
|
||||
int InstructionCount,
|
||||
uint ContainingFunctionStart,
|
||||
uint ContainingFunctionEnd,
|
||||
string SectionName,
|
||||
IReadOnlyList<string> SemanticActionIds,
|
||||
string LoweringKind,
|
||||
uint? TargetAddress,
|
||||
string? TargetActionId,
|
||||
string? TargetKind,
|
||||
string? TargetSymbol,
|
||||
uint ContinuationAddress);
|
||||
|
||||
public sealed record RetroWfcStaticPointerPlan(
|
||||
int RecordIndex,
|
||||
uint Address,
|
||||
string SectionName,
|
||||
bool SectionExecutable,
|
||||
uint? ContainingFunctionStart,
|
||||
uint? ContainingFunctionEnd,
|
||||
IReadOnlyList<string> SemanticActionIds,
|
||||
string LoweringKind,
|
||||
uint? TargetAddress,
|
||||
string? TargetActionId,
|
||||
string? TargetKind,
|
||||
string? TargetSymbol);
|
||||
|
||||
public sealed record RetroWfcLoweringIssue(
|
||||
string Severity,
|
||||
string Code,
|
||||
string Message,
|
||||
string? Path);
|
||||
|
||||
public sealed record RetroWfcContractLoweringPlan(
|
||||
string? Source,
|
||||
int PatchRecordCount,
|
||||
IReadOnlyList<RetroWfcStaticBytePatchPlan> StaticBytePatches,
|
||||
IReadOnlyList<RetroWfcExecutableHookPlan> ExecutableHooks,
|
||||
IReadOnlyList<RetroWfcStaticPointerPlan> StaticPointers,
|
||||
IReadOnlyList<RetroWfcLoweringIssue> Issues)
|
||||
{
|
||||
public bool IsPlannable => Issues.All(i => !string.Equals(i.Severity, "error", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
public string BuildTextReport()
|
||||
{
|
||||
using var writer = new StringWriter();
|
||||
writer.WriteLine("Retro WFC recomp contract lowering plan");
|
||||
if (!string.IsNullOrWhiteSpace(Source))
|
||||
{
|
||||
writer.WriteLine($" source: {Source}");
|
||||
}
|
||||
writer.WriteLine($" patch records: {PatchRecordCount:N0}");
|
||||
writer.WriteLine($" static byte patches: {StaticBytePatches.Count:N0}");
|
||||
writer.WriteLine($" executable hooks: {ExecutableHooks.Count:N0}");
|
||||
writer.WriteLine($" static pointer writes: {StaticPointers.Count:N0}");
|
||||
writer.WriteLine($" plannable: {IsPlannable}");
|
||||
writer.WriteLine();
|
||||
|
||||
writer.WriteLine("Static byte patches:");
|
||||
foreach (var patch in StaticBytePatches.OrderBy(p => p.Address))
|
||||
{
|
||||
var function = patch.ContainingFunctionStart.HasValue
|
||||
? $" function=0x{patch.ContainingFunctionStart.Value:X8}-0x{patch.ContainingFunctionEnd!.Value:X8}"
|
||||
: "";
|
||||
writer.WriteLine($" #{patch.RecordIndex} 0x{patch.Address:X8}+0x{patch.Size:X} {patch.LoweringKind} section={patch.SectionName}{function}");
|
||||
}
|
||||
if (StaticBytePatches.Count == 0)
|
||||
{
|
||||
writer.WriteLine(" none");
|
||||
}
|
||||
writer.WriteLine();
|
||||
|
||||
writer.WriteLine("Executable hooks:");
|
||||
foreach (var hook in ExecutableHooks.OrderBy(h => h.Address))
|
||||
{
|
||||
writer.WriteLine($" #{hook.RecordIndex} 0x{hook.Address:X8} {hook.TypeName}/{hook.Intent} instructions={hook.InstructionCount} function=0x{hook.ContainingFunctionStart:X8}-0x{hook.ContainingFunctionEnd:X8}");
|
||||
writer.WriteLine($" actions: {string.Join(", ", hook.SemanticActionIds)}");
|
||||
writer.WriteLine(hook.TargetAddress.HasValue
|
||||
? $" target: 0x{hook.TargetAddress.Value:X8} ({hook.TargetKind}, {hook.TargetActionId}) continuation=0x{hook.ContinuationAddress:X8}"
|
||||
: $" target: unresolved continuation=0x{hook.ContinuationAddress:X8}");
|
||||
}
|
||||
if (ExecutableHooks.Count == 0)
|
||||
{
|
||||
writer.WriteLine(" none");
|
||||
}
|
||||
writer.WriteLine();
|
||||
|
||||
writer.WriteLine("Static pointer writes:");
|
||||
foreach (var pointer in StaticPointers.OrderBy(p => p.Address))
|
||||
{
|
||||
var function = pointer.ContainingFunctionStart.HasValue
|
||||
? $" function=0x{pointer.ContainingFunctionStart.Value:X8}-0x{pointer.ContainingFunctionEnd!.Value:X8}"
|
||||
: "";
|
||||
writer.WriteLine($" #{pointer.RecordIndex} 0x{pointer.Address:X8} {pointer.LoweringKind} section={pointer.SectionName}{function}");
|
||||
writer.WriteLine($" actions: {string.Join(", ", pointer.SemanticActionIds)}");
|
||||
writer.WriteLine(pointer.TargetAddress.HasValue
|
||||
? $" target: 0x{pointer.TargetAddress.Value:X8} ({pointer.TargetKind}, {pointer.TargetActionId})"
|
||||
: " target: unresolved");
|
||||
}
|
||||
if (StaticPointers.Count == 0)
|
||||
{
|
||||
writer.WriteLine(" none");
|
||||
}
|
||||
writer.WriteLine();
|
||||
|
||||
var errors = Issues.Where(i => string.Equals(i.Severity, "error", StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
var warnings = Issues.Where(i => string.Equals(i.Severity, "warning", StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
writer.WriteLine($"Errors: {errors.Count:N0}");
|
||||
foreach (var issue in errors)
|
||||
{
|
||||
writer.WriteLine($" [{issue.Code}] {issue.Message}{FormatIssuePath(issue.Path)}");
|
||||
}
|
||||
writer.WriteLine($"Warnings: {warnings.Count:N0}");
|
||||
foreach (var issue in warnings)
|
||||
{
|
||||
writer.WriteLine($" [{issue.Code}] {issue.Message}{FormatIssuePath(issue.Path)}");
|
||||
}
|
||||
|
||||
return writer.ToString();
|
||||
}
|
||||
|
||||
private static string FormatIssuePath(string? path) =>
|
||||
string.IsNullOrWhiteSpace(path) ? "" : $" ({path})";
|
||||
}
|
||||
|
||||
public static class RetroWfcContractLoweringPlanner
|
||||
{
|
||||
public static RetroWfcContractLoweringPlan Build(
|
||||
string json,
|
||||
BaseManifest baseManifest,
|
||||
string? source = null,
|
||||
IReadOnlyDictionary<string, RetroWfcSemanticActionTarget>? semanticActionTargets = null)
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var root = document.RootElement;
|
||||
var issues = new List<RetroWfcLoweringIssue>();
|
||||
var staticBytePatches = new List<RetroWfcStaticBytePatchPlan>();
|
||||
var executableHooks = new List<RetroWfcExecutableHookPlan>();
|
||||
var staticPointers = new List<RetroWfcStaticPointerPlan>();
|
||||
var functionIndex = new BaseFunctionIndex(baseManifest.Functions);
|
||||
|
||||
if (!root.TryGetProperty("patchRecords", out var records) || records.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
issues.Add(Error("MissingPatchRecords", "Contract is missing patchRecords.", "$.patchRecords"));
|
||||
return new RetroWfcContractLoweringPlan(source, 0, staticBytePatches, executableHooks, staticPointers, issues);
|
||||
}
|
||||
|
||||
var ordinal = 0;
|
||||
foreach (var record in records.EnumerateArray())
|
||||
{
|
||||
var path = $"$.patchRecords[{ordinal}]";
|
||||
var recordIndex = GetInt(record, "index") ?? ordinal;
|
||||
var intent = GetString(record, "intent") ?? "";
|
||||
switch (intent)
|
||||
{
|
||||
case "staticBytes":
|
||||
TryPlanStaticBytes(record, recordIndex, path, baseManifest, functionIndex, staticBytePatches, issues);
|
||||
break;
|
||||
case "executableHook":
|
||||
case "executableHookWithContinuation":
|
||||
case "executableCall":
|
||||
case "executableCtrHook":
|
||||
case "executableCtrCall":
|
||||
TryPlanExecutableHook(record, recordIndex, path, baseManifest, functionIndex, semanticActionTargets, executableHooks, issues);
|
||||
break;
|
||||
case "staticPointer":
|
||||
TryPlanStaticPointer(record, recordIndex, path, baseManifest, functionIndex, semanticActionTargets, staticPointers, issues);
|
||||
break;
|
||||
default:
|
||||
issues.Add(Error("UnsupportedIntent", $"Unsupported Retro WFC patch intent '{intent}'.", path + ".intent"));
|
||||
break;
|
||||
}
|
||||
ordinal++;
|
||||
}
|
||||
|
||||
return new RetroWfcContractLoweringPlan(source, records.GetArrayLength(), staticBytePatches, executableHooks, staticPointers, issues);
|
||||
}
|
||||
|
||||
private static void TryPlanStaticBytes(
|
||||
JsonElement record,
|
||||
int recordIndex,
|
||||
string path,
|
||||
BaseManifest baseManifest,
|
||||
BaseFunctionIndex functionIndex,
|
||||
List<RetroWfcStaticBytePatchPlan> result,
|
||||
List<RetroWfcLoweringIssue> issues)
|
||||
{
|
||||
if (!TryReadAddress(record, "address", out var address))
|
||||
{
|
||||
issues.Add(Error("MissingPatchAddress", "Static byte patch is missing a valid address.", path + ".address"));
|
||||
return;
|
||||
}
|
||||
var section = FindSection(baseManifest, address);
|
||||
if (section is null)
|
||||
{
|
||||
issues.Add(Error("PatchAddressOutsideBase", $"Static byte patch address 0x{address:X8} is outside known base sections.", path + ".address"));
|
||||
return;
|
||||
}
|
||||
|
||||
var sourceBytes = GetObject(record, "sourceBytes");
|
||||
if (sourceBytes is null || GetString(sourceBytes.Value, "kind") != "payloadSlice")
|
||||
{
|
||||
issues.Add(Error("StaticBytesNotResolved", "Static byte patch must contain sourceBytes kind payloadSlice.", path + ".sourceBytes"));
|
||||
return;
|
||||
}
|
||||
var hex = GetString(sourceBytes.Value, "hex");
|
||||
var sha = GetString(sourceBytes.Value, "sha256");
|
||||
if (string.IsNullOrWhiteSpace(hex) || !IsEvenHex(hex))
|
||||
{
|
||||
issues.Add(Error("InvalidStaticBytesHex", "Static byte patch sourceBytes.hex must be even-length hex.", path + ".sourceBytes.hex"));
|
||||
return;
|
||||
}
|
||||
var size = hex.Length / 2;
|
||||
if ((ulong)address + (ulong)size > section.GuestEnd)
|
||||
{
|
||||
issues.Add(Error("StaticBytesCrossSectionBoundary", $"Static byte patch 0x{address:X8}+0x{size:X} crosses section '{section.Name}'.", path + ".address"));
|
||||
return;
|
||||
}
|
||||
if (GetObject(record, "target") is not { } target ||
|
||||
GetString(target, "resolvedBy") != "sourceBytes" ||
|
||||
GetBool(target, "resolutionRequired") != false)
|
||||
{
|
||||
issues.Add(Error("StaticBytesTargetNotResolved", "Static byte patch target must be resolved by sourceBytes.", path + ".target"));
|
||||
return;
|
||||
}
|
||||
|
||||
BaseFunctionRangeMetadata? function = null;
|
||||
if (section.Executable)
|
||||
{
|
||||
function = functionIndex.FindContaining(address);
|
||||
if (function is null)
|
||||
{
|
||||
issues.Add(Error("StaticExecutablePatchMissingFunction", $"Executable static byte patch 0x{address:X8} has no containing function.", path + ".address"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(new RetroWfcStaticBytePatchPlan(
|
||||
recordIndex,
|
||||
"staticBytes",
|
||||
address,
|
||||
size,
|
||||
section.Name,
|
||||
section.Executable,
|
||||
function?.Start,
|
||||
function?.End,
|
||||
sha ?? "",
|
||||
hex,
|
||||
section.Executable ? "overlayBytePatch" : "dataBytePatch"));
|
||||
}
|
||||
|
||||
private static void TryPlanExecutableHook(
|
||||
JsonElement record,
|
||||
int recordIndex,
|
||||
string path,
|
||||
BaseManifest baseManifest,
|
||||
BaseFunctionIndex functionIndex,
|
||||
IReadOnlyDictionary<string, RetroWfcSemanticActionTarget>? semanticActionTargets,
|
||||
List<RetroWfcExecutableHookPlan> result,
|
||||
List<RetroWfcLoweringIssue> issues)
|
||||
{
|
||||
if (!TryReadAddress(record, "address", out var address))
|
||||
{
|
||||
issues.Add(Error("MissingHookAddress", "Executable hook is missing a valid address.", path + ".address"));
|
||||
return;
|
||||
}
|
||||
var section = FindSection(baseManifest, address);
|
||||
if (section is null || !section.Executable)
|
||||
{
|
||||
issues.Add(Error("HookAddressNotExecutable", $"Executable hook address 0x{address:X8} is not in an executable base section.", path + ".address"));
|
||||
return;
|
||||
}
|
||||
var function = functionIndex.FindContaining(address);
|
||||
if (function is null)
|
||||
{
|
||||
issues.Add(Error("HookMissingFunction", $"Executable hook address 0x{address:X8} has no containing function.", path + ".address"));
|
||||
return;
|
||||
}
|
||||
|
||||
var actionIds = ReadStringArray(record, "semanticActionIds");
|
||||
if (actionIds.Count == 0)
|
||||
{
|
||||
issues.Add(Error("HookMissingSemanticAction", "Executable hook has no semantic action id.", path + ".semanticActionIds"));
|
||||
return;
|
||||
}
|
||||
|
||||
var typeName = GetString(record, "typeName") ?? "";
|
||||
var instructionCount = GetInt(record, "instructionCount") ?? (typeName is "branchCtr" or "branchCtrLink" ? 4 : 1);
|
||||
if (instructionCount <= 0)
|
||||
{
|
||||
issues.Add(Error("InvalidHookInstructionCount", "Executable hook instruction count must be positive.", path + ".instructionCount"));
|
||||
return;
|
||||
}
|
||||
var target = ResolveActionTarget(actionIds, semanticActionTargets, issues, path + ".semanticActionIds");
|
||||
var continuationAddress = checked(address + (uint)(instructionCount * 4));
|
||||
|
||||
result.Add(new RetroWfcExecutableHookPlan(
|
||||
recordIndex,
|
||||
GetString(record, "intent") ?? "",
|
||||
typeName,
|
||||
address,
|
||||
instructionCount,
|
||||
function.Start,
|
||||
function.End,
|
||||
section.Name,
|
||||
actionIds,
|
||||
target is null ? "overlayHookPatchPendingAction" : "overlayHookPatch",
|
||||
target?.GuestAddress,
|
||||
target?.ActionId,
|
||||
target?.Kind,
|
||||
target?.Symbol,
|
||||
continuationAddress));
|
||||
}
|
||||
|
||||
private static void TryPlanStaticPointer(
|
||||
JsonElement record,
|
||||
int recordIndex,
|
||||
string path,
|
||||
BaseManifest baseManifest,
|
||||
BaseFunctionIndex functionIndex,
|
||||
IReadOnlyDictionary<string, RetroWfcSemanticActionTarget>? semanticActionTargets,
|
||||
List<RetroWfcStaticPointerPlan> result,
|
||||
List<RetroWfcLoweringIssue> issues)
|
||||
{
|
||||
if (!TryReadAddress(record, "address", out var address))
|
||||
{
|
||||
issues.Add(Error("MissingStaticPointerAddress", "Static pointer write is missing a valid address.", path + ".address"));
|
||||
return;
|
||||
}
|
||||
var section = FindSection(baseManifest, address);
|
||||
if (section is null)
|
||||
{
|
||||
issues.Add(Error("StaticPointerOutsideBase", $"Static pointer address 0x{address:X8} is outside known base sections.", path + ".address"));
|
||||
return;
|
||||
}
|
||||
|
||||
var actionIds = ReadStringArray(record, "semanticActionIds");
|
||||
if (actionIds.Count == 0)
|
||||
{
|
||||
issues.Add(Error("StaticPointerMissingSemanticAction", "Static pointer write has no semantic action id.", path + ".semanticActionIds"));
|
||||
return;
|
||||
}
|
||||
|
||||
BaseFunctionRangeMetadata? function = null;
|
||||
if (section.Executable)
|
||||
{
|
||||
function = functionIndex.FindContaining(address);
|
||||
if (function is null)
|
||||
{
|
||||
issues.Add(Error("StaticPointerExecutableMissingFunction", $"Executable static pointer address 0x{address:X8} has no containing function.", path + ".address"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
var target = ResolveActionTarget(actionIds, semanticActionTargets, issues, path + ".semanticActionIds");
|
||||
|
||||
result.Add(new RetroWfcStaticPointerPlan(
|
||||
recordIndex,
|
||||
address,
|
||||
section.Name,
|
||||
section.Executable,
|
||||
function?.Start,
|
||||
function?.End,
|
||||
actionIds,
|
||||
target is null
|
||||
? (section.Executable ? "overlayPointerPatchPendingAction" : "dataPointerPatchPendingAction")
|
||||
: (section.Executable ? "overlayPointerPatch" : "dataPointerPatch"),
|
||||
target?.GuestAddress,
|
||||
target?.ActionId,
|
||||
target?.Kind,
|
||||
target?.Symbol));
|
||||
}
|
||||
|
||||
private static RetroWfcSemanticActionTarget? ResolveActionTarget(
|
||||
IReadOnlyList<string> actionIds,
|
||||
IReadOnlyDictionary<string, RetroWfcSemanticActionTarget>? semanticActionTargets,
|
||||
List<RetroWfcLoweringIssue> issues,
|
||||
string path)
|
||||
{
|
||||
if (semanticActionTargets is null || semanticActionTargets.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var matches = actionIds
|
||||
.Select(id => semanticActionTargets.TryGetValue(id, out var target) ? target : null)
|
||||
.Where(target => target is not null)
|
||||
.Cast<RetroWfcSemanticActionTarget>()
|
||||
.ToList();
|
||||
if (matches.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (matches.Count != actionIds.Count)
|
||||
{
|
||||
issues.Add(Error("PartialSemanticResolution", "Only some semantic action IDs for this record have resolutions.", path));
|
||||
return null;
|
||||
}
|
||||
|
||||
var first = matches[0];
|
||||
if (matches.Any(m => m.GuestAddress != first.GuestAddress || !string.Equals(m.Kind, first.Kind, StringComparison.Ordinal)))
|
||||
{
|
||||
issues.Add(Error("ConflictingSemanticResolution", "Semantic action IDs for this record resolve to conflicting targets.", path));
|
||||
return null;
|
||||
}
|
||||
|
||||
return first;
|
||||
}
|
||||
|
||||
private static BaseSectionMetadata? FindSection(BaseManifest manifest, uint address) =>
|
||||
manifest.Sections.FirstOrDefault(section => address >= section.GuestStart && address < section.GuestEnd);
|
||||
|
||||
private static bool IsEvenHex(string text) =>
|
||||
text.Length % 2 == 0 && text.All(c => char.IsAsciiHexDigit(c));
|
||||
|
||||
private static RetroWfcLoweringIssue Error(string code, string message, string? path) =>
|
||||
new("error", code, message, path);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Translator.Core.Mods.Mkwii;
|
||||
|
||||
internal static class RetroWfcJson
|
||||
{
|
||||
public static JsonElement? GetObject(JsonElement obj, string name) =>
|
||||
obj.ValueKind == JsonValueKind.Object &&
|
||||
obj.TryGetProperty(name, out var value) &&
|
||||
value.ValueKind == JsonValueKind.Object ? value : null;
|
||||
|
||||
public static JsonElement? GetArray(JsonElement obj, string name) =>
|
||||
obj.ValueKind == JsonValueKind.Object &&
|
||||
obj.TryGetProperty(name, out var value) &&
|
||||
value.ValueKind == JsonValueKind.Array ? value : null;
|
||||
|
||||
public static string? GetString(JsonElement obj, string name) =>
|
||||
obj.ValueKind == JsonValueKind.Object &&
|
||||
obj.TryGetProperty(name, out var value) &&
|
||||
value.ValueKind == JsonValueKind.String ? value.GetString() : null;
|
||||
|
||||
public static bool? GetBool(JsonElement obj, string name) =>
|
||||
obj.ValueKind == JsonValueKind.Object &&
|
||||
obj.TryGetProperty(name, out var value) &&
|
||||
value.ValueKind is JsonValueKind.True or JsonValueKind.False ? value.GetBoolean() : null;
|
||||
|
||||
public static int? GetInt(JsonElement obj, string name) =>
|
||||
obj.ValueKind == JsonValueKind.Object &&
|
||||
obj.TryGetProperty(name, out var value) &&
|
||||
value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out var result) ? result : null;
|
||||
|
||||
public static IReadOnlyList<string> ReadStringArray(JsonElement obj, string name)
|
||||
{
|
||||
if (GetArray(obj, name) is not { } array) return Array.Empty<string>();
|
||||
return array.EnumerateArray()
|
||||
.Where(static item => item.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(item.GetString()))
|
||||
.Select(static item => item.GetString()!)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static bool TryReadAddress(JsonElement obj, string name, out uint value)
|
||||
{
|
||||
value = 0;
|
||||
return obj.ValueKind == JsonValueKind.Object &&
|
||||
obj.TryGetProperty(name, out var element) &&
|
||||
TryReadAddress(element, out value);
|
||||
}
|
||||
|
||||
public static bool TryReadAddress(JsonElement element, out uint value)
|
||||
{
|
||||
value = 0;
|
||||
var text = element.ValueKind == JsonValueKind.String ? element.GetString() : null;
|
||||
return text is not null && GuestTargetParser.TryParseHexAddress(text, out value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Translator.Core.Mods.Mkwii;
|
||||
|
||||
/// <summary>
|
||||
/// A target synthesized while parsing the active WWFC payload descriptor.
|
||||
/// </summary>
|
||||
public sealed record RetroWfcSemanticActionTarget(
|
||||
string ActionId,
|
||||
string Kind,
|
||||
uint GuestAddress,
|
||||
uint ModuleOffset,
|
||||
string? Symbol,
|
||||
string? Source);
|
||||
|
||||
/// <summary>
|
||||
/// A constructor or initialization callback discovered in the active payload image.
|
||||
/// </summary>
|
||||
public sealed record RetroWfcHelperInitCallback(
|
||||
string Kind,
|
||||
uint TargetAddress,
|
||||
uint ModuleOffset,
|
||||
string? Symbol);
|
||||
@@ -0,0 +1,524 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Translator.Core.Loading;
|
||||
|
||||
namespace Translator.Core.Mods.Mkwii;
|
||||
|
||||
public sealed record RetroWfcPayloadSummary(
|
||||
string? Source,
|
||||
string Game,
|
||||
uint FormatVersion,
|
||||
uint PayloadVersion,
|
||||
string PayloadSha256,
|
||||
uint HelperModuleOffset,
|
||||
uint HelperModuleBase,
|
||||
int PayloadImageSize,
|
||||
uint InitializationTargetAddress,
|
||||
uint InitializationSuccessReturnValue,
|
||||
uint ConstructorsStartOffset,
|
||||
uint ConstructorsEndOffset,
|
||||
uint ExecutableStartOffset,
|
||||
uint ExecutableEndOffset,
|
||||
IReadOnlyList<RetroWfcHelperInitCallback> InitializationCallbacks)
|
||||
{
|
||||
}
|
||||
|
||||
public sealed record RetroWfcPayloadLoadResult(
|
||||
RetroWfcPayloadSummary Summary,
|
||||
byte[] RelocatedImage,
|
||||
RetroWfcContractLoweringPlan LoweringPlan);
|
||||
|
||||
/// <summary>
|
||||
/// Reads the normal WWFC payload format directly. The only recomp-specific
|
||||
/// extension is the static-consumer descriptor referenced by payload info v3.
|
||||
/// </summary>
|
||||
public static class RetroWfcPayload
|
||||
{
|
||||
private const int HeaderInfoOffset = 0x130;
|
||||
private const int InfoSizeV3 = 0x74;
|
||||
private const int PatchRecordSize = 0x10;
|
||||
private const uint SupportedPayloadFormat = 3;
|
||||
private const uint SupportedStaticConsumerFormat = 1;
|
||||
|
||||
private sealed record PendingBranchBack(int RecordIndex, uint PayloadOffset, uint DestinationAddress);
|
||||
|
||||
public static RetroWfcPayloadLoadResult Parse(
|
||||
byte[] payloadImage,
|
||||
BaseManifest baseManifest,
|
||||
uint moduleGuestBase,
|
||||
uint helperModuleOffset,
|
||||
string? source = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(payloadImage);
|
||||
ArgumentNullException.ThrowIfNull(baseManifest);
|
||||
|
||||
RequireRange(payloadImage, 0, HeaderInfoOffset + InfoSizeV3, "payload header");
|
||||
if (!payloadImage.AsSpan(0, 12).SequenceEqual("WWFC/Payload"u8))
|
||||
{
|
||||
throw new InvalidDataException("Retro WFC payload has an invalid magic value.");
|
||||
}
|
||||
|
||||
var totalSize = ReadU32(payloadImage, 0x0C);
|
||||
if (totalSize != payloadImage.Length)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Retro WFC payload header size 0x{totalSize:X} does not match the supplied image size 0x{payloadImage.Length:X}.");
|
||||
}
|
||||
|
||||
var formatVersion = ReadInfo(payloadImage, 0x00);
|
||||
var compatibleVersion = ReadInfo(payloadImage, 0x04);
|
||||
if (formatVersion < SupportedPayloadFormat || compatibleVersion > SupportedPayloadFormat)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Retro WFC payload format {formatVersion} (compatible with {compatibleVersion}) cannot be read by format {SupportedPayloadFormat}.");
|
||||
}
|
||||
|
||||
var game = Encoding.ASCII.GetString(payloadImage, HeaderInfoOffset + 0x08, 0x0C).TrimEnd('\0');
|
||||
ValidateGame(game, baseManifest.GameId);
|
||||
for (var offset = HeaderInfoOffset + 0x3C; offset < HeaderInfoOffset + 0x50; offset += 4)
|
||||
{
|
||||
if (ReadU32(payloadImage, offset) != 0)
|
||||
{
|
||||
throw new InvalidDataException($"Retro WFC payload reserved info word at 0x{offset:X} must be zero.");
|
||||
}
|
||||
}
|
||||
|
||||
var gotStart = ReadInfo(payloadImage, 0x18);
|
||||
var gotEnd = ReadInfo(payloadImage, 0x1C);
|
||||
var fixupStart = ReadInfo(payloadImage, 0x20);
|
||||
var fixupEnd = ReadInfo(payloadImage, 0x24);
|
||||
var patchStart = ReadInfo(payloadImage, 0x28);
|
||||
var patchEnd = ReadInfo(payloadImage, 0x2C);
|
||||
var staticInfoOffset = ReadInfo(payloadImage, 0x70);
|
||||
ValidateAlignedRange(payloadImage, gotStart, gotEnd, 4, "GOT");
|
||||
ValidateAlignedRange(payloadImage, fixupStart, fixupEnd, 4, "fixup table");
|
||||
ValidateAlignedRange(payloadImage, patchStart, patchEnd, PatchRecordSize, "patch list");
|
||||
|
||||
RequireRange(payloadImage, staticInfoOffset, 0x10, "static-consumer descriptor");
|
||||
var staticInfoVersion = ReadU32(payloadImage, staticInfoOffset);
|
||||
if (staticInfoVersion != SupportedStaticConsumerFormat)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Retro WFC static-consumer descriptor format {staticInfoVersion} is not supported.");
|
||||
}
|
||||
|
||||
var patchFreeEntry = ReadU32(payloadImage, checked((int)staticInfoOffset + 0x04));
|
||||
var constructorsStart = patchEnd;
|
||||
var constructorsEnd = ReadU32(payloadImage, checked((int)staticInfoOffset + 0x08));
|
||||
var executableStart = constructorsEnd;
|
||||
var executableEnd = ReadU32(payloadImage, checked((int)staticInfoOffset + 0x0C));
|
||||
ValidateAlignedRange(payloadImage, constructorsStart, constructorsEnd, 4, "constructor table");
|
||||
ValidateAlignedRange(payloadImage, executableStart, executableEnd, 4, "executable payload");
|
||||
RequireExecutableOffset(patchFreeEntry, executableStart, executableEnd, "patch-free entry point");
|
||||
|
||||
var helperModuleBase = checked(moduleGuestBase + helperModuleOffset);
|
||||
var semanticTargets = new Dictionary<string, RetroWfcSemanticActionTarget>(StringComparer.Ordinal);
|
||||
var contractRecords = new List<Dictionary<string, object?>>();
|
||||
var pendingBranchBacks = new List<PendingBranchBack>();
|
||||
var recordCount = checked((int)((patchEnd - patchStart) / PatchRecordSize));
|
||||
|
||||
for (var index = 0; index < recordCount; index++)
|
||||
{
|
||||
var offset = checked((int)patchStart + index * PatchRecordSize);
|
||||
var level = payloadImage[offset];
|
||||
var type = payloadImage[offset + 1];
|
||||
var address = ReadU32(payloadImage, offset + 4);
|
||||
var arg0 = ReadU32(payloadImage, offset + 8);
|
||||
var arg1 = ReadU32(payloadImage, offset + 12);
|
||||
if ((level & 0x10) != 0 || address == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case 0:
|
||||
AddStaticBytePatch(payloadImage, contractRecords, index, address, arg0, arg1);
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
AddExecutablePatch(
|
||||
payloadImage,
|
||||
baseManifest,
|
||||
semanticTargets,
|
||||
contractRecords,
|
||||
pendingBranchBacks,
|
||||
index,
|
||||
type,
|
||||
address,
|
||||
arg0,
|
||||
arg1,
|
||||
helperModuleOffset,
|
||||
helperModuleBase,
|
||||
executableStart,
|
||||
executableEnd);
|
||||
break;
|
||||
case 6:
|
||||
AddStaticPointerPatch(
|
||||
payloadImage,
|
||||
baseManifest,
|
||||
semanticTargets,
|
||||
contractRecords,
|
||||
index,
|
||||
address,
|
||||
arg0,
|
||||
helperModuleOffset,
|
||||
helperModuleBase,
|
||||
executableStart,
|
||||
executableEnd);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidDataException($"Retro WFC patch #{index} has unsupported type {type}.");
|
||||
}
|
||||
}
|
||||
|
||||
var relocatedImage = payloadImage.ToArray();
|
||||
RelocatePayload(relocatedImage, helperModuleBase, gotStart, gotEnd, fixupStart, fixupEnd);
|
||||
foreach (var branchBack in pendingBranchBacks)
|
||||
{
|
||||
var branchAddress = checked(helperModuleBase + branchBack.PayloadOffset);
|
||||
var instruction = 0x48000000u | ((branchBack.DestinationAddress - branchAddress) & 0x03FFFFFCu);
|
||||
WriteU32(relocatedImage, branchBack.PayloadOffset, instruction);
|
||||
}
|
||||
|
||||
var callbacks = ReadConstructors(
|
||||
payloadImage,
|
||||
helperModuleOffset,
|
||||
helperModuleBase,
|
||||
constructorsStart,
|
||||
constructorsEnd,
|
||||
executableStart,
|
||||
executableEnd);
|
||||
var contractJson = JsonSerializer.Serialize(new { patchRecords = contractRecords });
|
||||
var loweringPlan = RetroWfcContractLoweringPlanner.Build(
|
||||
contractJson,
|
||||
baseManifest,
|
||||
source,
|
||||
semanticTargets);
|
||||
if (!loweringPlan.IsPlannable)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"Retro WFC payload patch list could not be lowered statically." +
|
||||
Environment.NewLine +
|
||||
loweringPlan.BuildTextReport());
|
||||
}
|
||||
|
||||
var summary = new RetroWfcPayloadSummary(
|
||||
source,
|
||||
game,
|
||||
formatVersion,
|
||||
ReadInfo(payloadImage, 0x14),
|
||||
ChecksumUtilities.Sha256Hex(payloadImage),
|
||||
helperModuleOffset,
|
||||
helperModuleBase,
|
||||
payloadImage.Length,
|
||||
checked(helperModuleBase + patchFreeEntry),
|
||||
0,
|
||||
constructorsStart,
|
||||
constructorsEnd,
|
||||
executableStart,
|
||||
executableEnd,
|
||||
callbacks);
|
||||
return new RetroWfcPayloadLoadResult(summary, relocatedImage, loweringPlan);
|
||||
}
|
||||
|
||||
private static void AddStaticBytePatch(
|
||||
byte[] payloadImage,
|
||||
List<Dictionary<string, object?>> records,
|
||||
int index,
|
||||
uint address,
|
||||
uint sourceOffset,
|
||||
uint size)
|
||||
{
|
||||
RequireRange(payloadImage, sourceOffset, size, $"patch #{index} source bytes");
|
||||
var bytes = payloadImage.AsSpan(checked((int)sourceOffset), checked((int)size));
|
||||
records.Add(new Dictionary<string, object?>
|
||||
{
|
||||
["index"] = index,
|
||||
["intent"] = "staticBytes",
|
||||
["typeName"] = "write",
|
||||
["address"] = $"0x{address:X8}",
|
||||
["sourceBytes"] = new
|
||||
{
|
||||
kind = "payloadSlice",
|
||||
hex = ChecksumUtilities.ToHex(bytes),
|
||||
sha256 = ChecksumUtilities.Sha256Hex(bytes),
|
||||
},
|
||||
["target"] = new
|
||||
{
|
||||
resolvedBy = "sourceBytes",
|
||||
resolutionRequired = false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private static void AddExecutablePatch(
|
||||
byte[] payloadImage,
|
||||
BaseManifest baseManifest,
|
||||
Dictionary<string, RetroWfcSemanticActionTarget> semanticTargets,
|
||||
List<Dictionary<string, object?>> records,
|
||||
List<PendingBranchBack> pendingBranchBacks,
|
||||
int index,
|
||||
byte type,
|
||||
uint address,
|
||||
uint targetValue,
|
||||
uint arg1,
|
||||
uint helperModuleOffset,
|
||||
uint helperModuleBase,
|
||||
uint executableStart,
|
||||
uint executableEnd)
|
||||
{
|
||||
var (targetAddress, targetKind, moduleOffset) = ResolveTarget(
|
||||
payloadImage,
|
||||
baseManifest,
|
||||
targetValue,
|
||||
helperModuleOffset,
|
||||
helperModuleBase,
|
||||
executableStart,
|
||||
executableEnd,
|
||||
requireExecutable: true,
|
||||
$"patch #{index} branch target");
|
||||
if ((type is 4 or 5) && arg1 > 31)
|
||||
{
|
||||
throw new InvalidDataException($"Retro WFC patch #{index} uses invalid CTR scratch register {arg1}.");
|
||||
}
|
||||
|
||||
if (type == 2)
|
||||
{
|
||||
if ((arg1 & 0x80000000u) != 0)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Retro WFC patch #{index} writes its branch-back outside the payload; static lowering does not support that layout.");
|
||||
}
|
||||
RequireExecutableOffset(arg1, executableStart, executableEnd, $"patch #{index} branch-back");
|
||||
pendingBranchBacks.Add(new PendingBranchBack(index, arg1, checked(address + 4)));
|
||||
}
|
||||
|
||||
var actionId = $"payload.patch.{index}.target";
|
||||
semanticTargets.Add(
|
||||
actionId,
|
||||
new RetroWfcSemanticActionTarget(actionId, targetKind, targetAddress, moduleOffset, null, "wwfc-payload"));
|
||||
var (intent, typeName, instructionCount) = type switch
|
||||
{
|
||||
1 => ("executableHook", "branch", 1),
|
||||
2 => ("executableHookWithContinuation", "branchHook", 1),
|
||||
3 => ("executableCall", "call", 1),
|
||||
4 => ("executableCtrHook", "branchCtr", 4),
|
||||
5 => ("executableCtrCall", "branchCtrLink", 4),
|
||||
_ => throw new InvalidOperationException(),
|
||||
};
|
||||
records.Add(new Dictionary<string, object?>
|
||||
{
|
||||
["index"] = index,
|
||||
["intent"] = intent,
|
||||
["typeName"] = typeName,
|
||||
["address"] = $"0x{address:X8}",
|
||||
["instructionCount"] = instructionCount,
|
||||
["semanticActionIds"] = new[] { actionId },
|
||||
});
|
||||
}
|
||||
|
||||
private static void AddStaticPointerPatch(
|
||||
byte[] payloadImage,
|
||||
BaseManifest baseManifest,
|
||||
Dictionary<string, RetroWfcSemanticActionTarget> semanticTargets,
|
||||
List<Dictionary<string, object?>> records,
|
||||
int index,
|
||||
uint address,
|
||||
uint targetValue,
|
||||
uint helperModuleOffset,
|
||||
uint helperModuleBase,
|
||||
uint executableStart,
|
||||
uint executableEnd)
|
||||
{
|
||||
var (targetAddress, targetKind, moduleOffset) = ResolveTarget(
|
||||
payloadImage,
|
||||
baseManifest,
|
||||
targetValue,
|
||||
helperModuleOffset,
|
||||
helperModuleBase,
|
||||
executableStart,
|
||||
executableEnd,
|
||||
requireExecutable: false,
|
||||
$"patch #{index} pointer target");
|
||||
var actionId = $"payload.patch.{index}.target";
|
||||
semanticTargets.Add(
|
||||
actionId,
|
||||
new RetroWfcSemanticActionTarget(actionId, targetKind, targetAddress, moduleOffset, null, "wwfc-payload"));
|
||||
records.Add(new Dictionary<string, object?>
|
||||
{
|
||||
["index"] = index,
|
||||
["intent"] = "staticPointer",
|
||||
["typeName"] = "writePointer",
|
||||
["address"] = $"0x{address:X8}",
|
||||
["semanticActionIds"] = new[] { actionId },
|
||||
});
|
||||
}
|
||||
|
||||
private static (uint Address, string Kind, uint ModuleOffset) ResolveTarget(
|
||||
byte[] payloadImage,
|
||||
BaseManifest baseManifest,
|
||||
uint value,
|
||||
uint helperModuleOffset,
|
||||
uint helperModuleBase,
|
||||
uint executableStart,
|
||||
uint executableEnd,
|
||||
bool requireExecutable,
|
||||
string description)
|
||||
{
|
||||
if ((value & 0x80000000u) == 0)
|
||||
{
|
||||
RequireRange(payloadImage, value, requireExecutable ? 4u : 1u, description);
|
||||
var executable = value >= executableStart && value < executableEnd;
|
||||
if (requireExecutable && !executable)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Retro WFC {description} offset 0x{value:X} is outside the declared executable range.");
|
||||
}
|
||||
return (
|
||||
checked(helperModuleBase + value),
|
||||
executable ? "moduleFunction" : "moduleData",
|
||||
checked(helperModuleOffset + value));
|
||||
}
|
||||
|
||||
var section = baseManifest.Sections.FirstOrDefault(s => value >= s.GuestStart && value < s.GuestEnd);
|
||||
if (section is null || (requireExecutable && !section.Executable))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Retro WFC {description} address 0x{value:X8} is outside a compatible base section.");
|
||||
}
|
||||
return (value, section.Executable ? "baseFunction" : "baseData", 0);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<RetroWfcHelperInitCallback> ReadConstructors(
|
||||
byte[] payloadImage,
|
||||
uint helperModuleOffset,
|
||||
uint helperModuleBase,
|
||||
uint start,
|
||||
uint end,
|
||||
uint executableStart,
|
||||
uint executableEnd)
|
||||
{
|
||||
var callbacks = new List<RetroWfcHelperInitCallback>();
|
||||
for (var offset = start; offset < end; offset += 4)
|
||||
{
|
||||
var targetOffset = ReadU32(payloadImage, offset);
|
||||
if (targetOffset is 0 or 0xFFFFFFFFu)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
RequireExecutableOffset(targetOffset, executableStart, executableEnd, "constructor");
|
||||
callbacks.Add(new RetroWfcHelperInitCallback(
|
||||
"constructor",
|
||||
checked(helperModuleBase + targetOffset),
|
||||
checked(helperModuleOffset + targetOffset),
|
||||
null));
|
||||
}
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
private static void RelocatePayload(
|
||||
byte[] image,
|
||||
uint payloadBase,
|
||||
uint gotStart,
|
||||
uint gotEnd,
|
||||
uint fixupStart,
|
||||
uint fixupEnd)
|
||||
{
|
||||
for (var offset = gotStart; offset < gotEnd; offset += 4)
|
||||
{
|
||||
RelocateWord(image, offset, payloadBase);
|
||||
}
|
||||
|
||||
for (var offset = fixupStart; offset < fixupEnd; offset += 4)
|
||||
{
|
||||
var targetOffset = ReadU32(image, offset);
|
||||
RequireRange(image, targetOffset, 4, "fixup target");
|
||||
RelocateWord(image, targetOffset, payloadBase);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RelocateWord(byte[] image, uint offset, uint payloadBase)
|
||||
{
|
||||
var value = ReadU32(image, offset);
|
||||
if ((value & 0x80000000u) == 0)
|
||||
{
|
||||
WriteU32(image, offset, checked(payloadBase + value));
|
||||
}
|
||||
}
|
||||
|
||||
private static uint ReadInfo(byte[] image, int relativeOffset) =>
|
||||
ReadU32(image, HeaderInfoOffset + relativeOffset);
|
||||
|
||||
private static uint ReadU32(byte[] image, uint offset) =>
|
||||
ReadU32(image, checked((int)offset));
|
||||
|
||||
private static uint ReadU32(byte[] image, int offset)
|
||||
{
|
||||
RequireRange(image, offset, 4, "32-bit value");
|
||||
return BinaryPrimitives.ReadUInt32BigEndian(image.AsSpan(offset, 4));
|
||||
}
|
||||
|
||||
private static void WriteU32(byte[] image, uint offset, uint value)
|
||||
{
|
||||
RequireRange(image, offset, 4, "32-bit value");
|
||||
BinaryPrimitives.WriteUInt32BigEndian(image.AsSpan(checked((int)offset), 4), value);
|
||||
}
|
||||
|
||||
private static void ValidateGame(string payloadGame, string baseGame)
|
||||
{
|
||||
if (payloadGame.Length < 5 ||
|
||||
baseGame.Length < 4 ||
|
||||
!payloadGame.StartsWith(baseGame[..4], StringComparison.OrdinalIgnoreCase) ||
|
||||
payloadGame[4] != 'D')
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Retro WFC payload game '{payloadGame}' is not compatible with base game '{baseGame}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateAlignedRange(
|
||||
byte[] image,
|
||||
uint start,
|
||||
uint end,
|
||||
int elementSize,
|
||||
string description)
|
||||
{
|
||||
if (end < start || (start & 3u) != 0 || ((end - start) % elementSize) != 0)
|
||||
{
|
||||
throw new InvalidDataException($"Retro WFC payload {description} range 0x{start:X}-0x{end:X} is malformed.");
|
||||
}
|
||||
RequireRange(image, start, end - start, description);
|
||||
}
|
||||
|
||||
private static void RequireExecutableOffset(uint offset, uint start, uint end, string description)
|
||||
{
|
||||
if ((offset & 3) != 0 || offset < start || offset >= end)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Retro WFC payload {description} offset 0x{offset:X} is outside executable range 0x{start:X}-0x{end:X}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RequireRange(byte[] image, uint offset, uint size, string description)
|
||||
{
|
||||
if (offset > image.Length || size > image.Length - offset)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Retro WFC payload {description} range 0x{offset:X}+0x{size:X} is outside the image.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RequireRange(byte[] image, int offset, int size, string description)
|
||||
{
|
||||
if (offset < 0 || size < 0 || offset > image.Length || size > image.Length - offset)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Retro WFC payload {description} range 0x{offset:X}+0x{size:X} is outside the image.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
using System.Text;
|
||||
using Translator.Core.IO;
|
||||
using Translator.Core.Parsing.Kamek;
|
||||
using Translator.Core.Mods.Mkwii;
|
||||
|
||||
namespace Translator.Core.Mods;
|
||||
|
||||
public static class ModDataPatchWriter
|
||||
{
|
||||
public static void Write(
|
||||
string path,
|
||||
KamekPatchPlan patchPlan,
|
||||
uint moduleLinkBase,
|
||||
BaseManifest baseManifest,
|
||||
IReadOnlyList<string> dvdOverlayRoots,
|
||||
byte[] relocatedModuleImage,
|
||||
byte[]? sourceKamekCodeSha1Digest,
|
||||
IReadOnlyCollection<RetroWfcStaticPointerPlan>? retroWfcStaticPointers,
|
||||
uint moduleBssOffset,
|
||||
uint moduleBssSize,
|
||||
uint ctorStart,
|
||||
uint ctorEnd,
|
||||
bool emitCtorRunner,
|
||||
uint? retroWfcInitializerAddress = null,
|
||||
uint? retroWfcInitializerPayloadBase = null,
|
||||
uint? retroWfcInitializerSuccessReturnValue = null,
|
||||
string? publishedBlobDirectory = null,
|
||||
string? riivolutionXml = null,
|
||||
IReadOnlyList<ModRiivolutionOption>? riivolutionOptions = null)
|
||||
{
|
||||
var blobAssemblyPath = Path.Combine(
|
||||
Path.GetDirectoryName(path) ?? ".",
|
||||
Path.GetFileNameWithoutExtension(path) + "_blobs.S");
|
||||
var blobDirectory = Path.Combine(
|
||||
Path.GetDirectoryName(path) ?? ".",
|
||||
Path.GetFileNameWithoutExtension(path) + "_blobs");
|
||||
var blobs = new List<ModDataBlob>
|
||||
{
|
||||
new("kModuleImage", "module_image.bin", relocatedModuleImage),
|
||||
};
|
||||
if (sourceKamekCodeSha1Digest is { Length: 20 })
|
||||
{
|
||||
blobs.Add(new ModDataBlob("kKamekCodeSha1Digest", "kamek_code_sha1.bin", sourceKamekCodeSha1Digest));
|
||||
}
|
||||
WriteBlobFiles(
|
||||
blobAssemblyPath,
|
||||
blobDirectory,
|
||||
publishedBlobDirectory ?? blobDirectory,
|
||||
blobs);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
var emitRetroWfcInitializer =
|
||||
retroWfcInitializerAddress.HasValue &&
|
||||
retroWfcInitializerPayloadBase.HasValue &&
|
||||
retroWfcInitializerSuccessReturnValue.HasValue;
|
||||
sb.AppendLine("// Generated by translator translate-mod.");
|
||||
sb.AppendLine("#include \"abi_bridge.h\"");
|
||||
sb.AppendLine("#include \"memory.h\"");
|
||||
sb.AppendLine("#include \"recomp_mod_loader.h\"");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("#include <cstdint>");
|
||||
sb.AppendLine("#include <cstring>");
|
||||
sb.AppendLine("#include <iostream>");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("extern \"C\" {");
|
||||
foreach (var blob in blobs)
|
||||
{
|
||||
sb.AppendLine($"extern const uint8_t {blob.Symbol}[];");
|
||||
}
|
||||
sb.AppendLine("} // extern \"C\"");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("namespace {");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"constexpr uint32_t kModuleGuestBase = 0x{patchPlan.ModuleGuestBase:X8}u;");
|
||||
sb.AppendLine($"constexpr uint32_t kModuleImageSize = 0x{relocatedModuleImage.Length:X8}u;");
|
||||
sb.AppendLine($"constexpr uint32_t kModuleBssOffset = 0x{moduleBssOffset:X8}u;");
|
||||
sb.AppendLine($"constexpr uint32_t kModuleBssSize = 0x{moduleBssSize:X8}u;");
|
||||
sb.AppendLine("constexpr uint32_t kModuleBssEnd = kModuleBssOffset + kModuleBssSize;");
|
||||
sb.AppendLine("constexpr uint32_t kModuleReservedSize = kModuleImageSize > kModuleBssEnd ? kModuleImageSize : kModuleBssEnd;");
|
||||
sb.AppendLine($"constexpr uint32_t kCtorStart = 0x{ctorStart:X8}u;");
|
||||
sb.AppendLine($"constexpr uint32_t kCtorEnd = 0x{ctorEnd:X8}u;");
|
||||
if (emitRetroWfcInitializer)
|
||||
{
|
||||
sb.AppendLine($"constexpr uint32_t kRetroWfcInitializerAddress = 0x{retroWfcInitializerAddress!.Value:X8}u;");
|
||||
sb.AppendLine($"constexpr uint32_t kRetroWfcInitializerPayloadBase = 0x{retroWfcInitializerPayloadBase!.Value:X8}u;");
|
||||
sb.AppendLine($"constexpr uint32_t kRetroWfcInitializerSuccessReturnValue = 0x{retroWfcInitializerSuccessReturnValue!.Value:X8}u;");
|
||||
}
|
||||
sb.AppendLine("constexpr uint32_t kModuleReservedEnd = kModuleGuestBase + kModuleReservedSize;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("void InitializeModuleImage() {");
|
||||
sb.AppendLine(" std::memcpy(Memory::GetPointer(kModuleGuestBase, kModuleImageSize), kModuleImage, kModuleImageSize);");
|
||||
sb.AppendLine(" if (kModuleBssSize != 0) {");
|
||||
sb.AppendLine(" std::memset(Memory::GetPointer(kModuleGuestBase + kModuleBssOffset, kModuleBssSize), 0, kModuleBssSize);");
|
||||
sb.AppendLine(" }");
|
||||
if (sourceKamekCodeSha1Digest is { Length: 20 })
|
||||
{
|
||||
sb.AppendLine(" std::memcpy(Memory::GetPointer(0x800017B0u, 20u), kKamekCodeSha1Digest, 20u);");
|
||||
}
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
EmitStaticPatchHelpers(sb);
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("void RegisterExecutableRanges() {");
|
||||
var knownFunctionsBySection = baseManifest.Functions
|
||||
.Where(f => f.End > f.Start && f.Flags.Contains("HasKnownTranslation"))
|
||||
.GroupBy(f => f.SourceSection)
|
||||
.ToDictionary(g => g.Key, g => g.OrderBy(f => f.Start).First());
|
||||
foreach (var section in baseManifest.Sections
|
||||
.Where(s => s.Executable && s.GuestEnd > s.GuestStart)
|
||||
.OrderBy(s => s.GuestStart))
|
||||
{
|
||||
var guardedStart = knownFunctionsBySection.TryGetValue(section.Name, out var firstFunction)
|
||||
? firstFunction.Start
|
||||
: section.GuestStart;
|
||||
if (guardedStart >= section.GuestEnd)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = $"{section.Source}:{section.Name}";
|
||||
sb.AppendLine(
|
||||
$" RecompMod::RegisterExecutableRange(0x{guardedStart:X8}u, 0x{section.GuestEnd:X8}u, \"{EscapeCxxStringLiteral(name)}\");");
|
||||
}
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("void ApplyExecutableBasePatches() {");
|
||||
foreach (var patch in patchPlan.ExecutablePatches.OrderBy(p => p.CommandAddress))
|
||||
{
|
||||
EmitBaseExecutablePatch(sb, patch, moduleLinkBase);
|
||||
}
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
if (emitCtorRunner)
|
||||
{
|
||||
sb.AppendLine("void RunCtors() {");
|
||||
sb.AppendLine(" InitializePersistentCpuContext();");
|
||||
sb.AppendLine(" CpuContext& cpu = GetPersistentCpuContext();");
|
||||
sb.AppendLine(" for (uint32_t ptr = kCtorStart; ptr + 4 <= kCtorEnd; ptr += 4) {");
|
||||
sb.AppendLine(" const uint32_t ctor = Memory::Read32(ptr);");
|
||||
sb.AppendLine(" if (ctor != 0) {");
|
||||
sb.AppendLine(" InvokeIndirectCpu(ctor, &cpu);");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
if (emitRetroWfcInitializer)
|
||||
{
|
||||
sb.AppendLine("void RunRetroWfcInitializer() {");
|
||||
sb.AppendLine(" InitializePersistentCpuContext();");
|
||||
sb.AppendLine(" CpuContext& cpu = GetPersistentCpuContext();");
|
||||
sb.AppendLine(" cpu.gpr[3] = kRetroWfcInitializerPayloadBase;");
|
||||
sb.AppendLine(" InvokeIndirectCpu(kRetroWfcInitializerAddress, &cpu);");
|
||||
sb.AppendLine(" if (cpu.gpr[3] != kRetroWfcInitializerSuccessReturnValue) {");
|
||||
sb.AppendLine(" std::cerr << \"[mod] FATAL Retro WFC helper initialization failed: result=0x\" << std::hex << cpu.gpr[3]");
|
||||
sb.AppendLine(" << \" expected=0x\" << kRetroWfcInitializerSuccessReturnValue << std::dec << std::endl;");
|
||||
sb.AppendLine(" std::abort();");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
sb.AppendLine("void ApplyDataPatches() {");
|
||||
sb.AppendLine(" InitializeModuleImage();");
|
||||
sb.AppendLine(" ApplyExecutableBasePatches();");
|
||||
foreach (var patch in patchPlan.DataPatches.OrderBy(p => p.CommandAddress))
|
||||
{
|
||||
EmitPatch(sb, patch, patchPlan.ModuleGuestBase);
|
||||
}
|
||||
foreach (var pointer in (retroWfcStaticPointers ?? Array.Empty<RetroWfcStaticPointerPlan>())
|
||||
.Where(p => !p.SectionExecutable && p.TargetAddress.HasValue)
|
||||
.OrderBy(p => p.Address)
|
||||
.ThenBy(p => p.RecordIndex))
|
||||
{
|
||||
sb.AppendLine($" Memory::Write32(0x{pointer.Address:X8}u, 0x{pointer.TargetAddress!.Value:X8}u); // Retro WFC {EscapeCxxStringLiteral(pointer.TargetActionId ?? string.Join(",", pointer.SemanticActionIds))}");
|
||||
}
|
||||
sb.AppendLine(" RegisterExecutableRanges();");
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("struct RegisterModDataPatches {");
|
||||
sb.AppendLine(" RegisterModDataPatches() {");
|
||||
sb.AppendLine(" RecompMod::RegisterMemoryReservation(kModuleGuestBase, kModuleReservedEnd, \"Kamek module\");");
|
||||
sb.AppendLine(" RecompMod::RegisterMemoryInitializer(&ApplyDataPatches);");
|
||||
if (emitCtorRunner)
|
||||
{
|
||||
sb.AppendLine(" RecompMod::RegisterPostRelInitializer(&RunCtors);");
|
||||
}
|
||||
if (emitRetroWfcInitializer)
|
||||
{
|
||||
sb.AppendLine(" RecompMod::RegisterPostRelInitializer(&RunRetroWfcInitializer);");
|
||||
}
|
||||
foreach (var root in dvdOverlayRoots)
|
||||
{
|
||||
sb.AppendLine($" RecompMod::RegisterDvdOverlayRoot(\"{EscapeCxxStringLiteral(root)}\");");
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(riivolutionXml))
|
||||
{
|
||||
sb.AppendLine($" RecompMod::RegisterRiivolutionXml(\"{EscapeCxxStringLiteral(riivolutionXml)}\");");
|
||||
}
|
||||
foreach (var option in riivolutionOptions ?? [])
|
||||
{
|
||||
sb.AppendLine(
|
||||
$" RecompMod::RegisterRiivolutionOption(\"{EscapeCxxStringLiteral(option.Section)}\", \"{EscapeCxxStringLiteral(option.Option)}\", {option.Choice}u);");
|
||||
}
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("};");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("RegisterModDataPatches g_registerModDataPatches;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("} // namespace");
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
FileOutput.WriteTextIfChanged(path, sb.ToString());
|
||||
}
|
||||
|
||||
private static void WriteBlobFiles(
|
||||
string assemblyPath,
|
||||
string blobDirectory,
|
||||
string blobReferenceDirectory,
|
||||
IReadOnlyList<ModDataBlob> blobs)
|
||||
{
|
||||
var assemblyBlobs = blobs.Select(blob => new AssemblyBlob(
|
||||
blob.FileName,
|
||||
blob.Symbol,
|
||||
blob.Data,
|
||||
$"{blob.FileName}: {blob.Data.Length} bytes"))
|
||||
.ToList();
|
||||
AssemblyBlobWriter.Write(
|
||||
assemblyPath,
|
||||
blobDirectory,
|
||||
blobReferenceDirectory,
|
||||
assemblyBlobs,
|
||||
"// Generated by translator translate-mod.",
|
||||
"// Binary payloads referenced by mod_data_patches.cpp.");
|
||||
}
|
||||
|
||||
private sealed record ModDataBlob(string Symbol, string FileName, byte[] Data);
|
||||
|
||||
private static void EmitStaticPatchHelpers(StringBuilder sb)
|
||||
{
|
||||
sb.AppendLine("void PatchGuestU8(uint32_t address, uint32_t value) {");
|
||||
sb.AppendLine(" auto* ptr = Memory::GetPointer(address, 1);");
|
||||
sb.AppendLine(" ptr[0] = static_cast<uint8_t>(value);");
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("void PatchGuestU16(uint32_t address, uint32_t value) {");
|
||||
sb.AppendLine(" auto* ptr = Memory::GetPointer(address, 2);");
|
||||
sb.AppendLine(" ptr[0] = static_cast<uint8_t>((value >> 8) & 0xFFu);");
|
||||
sb.AppendLine(" ptr[1] = static_cast<uint8_t>(value & 0xFFu);");
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("void PatchGuestU32(uint32_t address, uint32_t value) {");
|
||||
sb.AppendLine(" auto* ptr = Memory::GetPointer(address, 4);");
|
||||
sb.AppendLine(" ptr[0] = static_cast<uint8_t>((value >> 24) & 0xFFu);");
|
||||
sb.AppendLine(" ptr[1] = static_cast<uint8_t>((value >> 16) & 0xFFu);");
|
||||
sb.AppendLine(" ptr[2] = static_cast<uint8_t>((value >> 8) & 0xFFu);");
|
||||
sb.AppendLine(" ptr[3] = static_cast<uint8_t>(value & 0xFFu);");
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("bool EncodeStaticPpcBranch(uint32_t from, uint32_t to, bool link, uint32_t& instruction) {");
|
||||
sb.AppendLine(" const int32_t delta = static_cast<int32_t>(to) - static_cast<int32_t>(from);");
|
||||
sb.AppendLine(" if ((delta & 0x3) != 0 || delta < -0x02000000 || delta > 0x01FFFFFC) {");
|
||||
sb.AppendLine(" return false;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" instruction = 0x48000000u | (static_cast<uint32_t>(delta) & 0x03FFFFFCu) | (link ? 1u : 0u);");
|
||||
sb.AppendLine(" return true;");
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("void FatalStaticPatchRange(uint32_t address, uint32_t target) {");
|
||||
sb.AppendLine(" std::cerr << \"[mod] FATAL static executable branch target out of range\" << std::endl;");
|
||||
sb.AppendLine(" std::cerr << \" address: 0x\" << std::hex << address << std::endl;");
|
||||
sb.AppendLine(" std::cerr << \" target: 0x\" << target << std::dec << std::endl;");
|
||||
sb.AppendLine(" std::abort();");
|
||||
sb.AppendLine("}");
|
||||
}
|
||||
|
||||
private static void EmitBaseExecutablePatch(StringBuilder sb, KamekPatchClassification patch, uint moduleLinkBase)
|
||||
{
|
||||
var address = patch.CommandAddress;
|
||||
switch (patch.CommandId)
|
||||
{
|
||||
case KamekCommandId.Addr32:
|
||||
{
|
||||
var value = KamekAddress.Resolve(patch.Arguments[0], moduleLinkBase);
|
||||
sb.AppendLine($" PatchGuestU32(0x{address:X8}u, 0x{value:X8}u);");
|
||||
return;
|
||||
}
|
||||
case KamekCommandId.Addr16Lo:
|
||||
{
|
||||
var value = KamekAddress.Resolve(patch.Arguments[0], moduleLinkBase) & 0xFFFFu;
|
||||
sb.AppendLine($" PatchGuestU16(0x{address:X8}u, 0x{value:X4}u);");
|
||||
return;
|
||||
}
|
||||
case KamekCommandId.Addr16Hi:
|
||||
{
|
||||
var value = KamekAddress.Resolve(patch.Arguments[0], moduleLinkBase) >> 16;
|
||||
sb.AppendLine($" PatchGuestU16(0x{address:X8}u, 0x{value:X4}u);");
|
||||
return;
|
||||
}
|
||||
case KamekCommandId.Addr16Ha:
|
||||
{
|
||||
var value = (KamekAddress.Resolve(patch.Arguments[0], moduleLinkBase) + 0x8000u) >> 16;
|
||||
sb.AppendLine($" PatchGuestU16(0x{address:X8}u, 0x{value:X4}u);");
|
||||
return;
|
||||
}
|
||||
case KamekCommandId.Rel24:
|
||||
{
|
||||
var target = KamekAddress.Resolve(patch.Arguments[0], moduleLinkBase);
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" const uint32_t address = 0x{address:X8}u;");
|
||||
sb.AppendLine($" const uint32_t target = 0x{target:X8}u;");
|
||||
sb.AppendLine(" const uint32_t original = Memory::Read32(address);");
|
||||
sb.AppendLine(" const int32_t delta = static_cast<int32_t>(target) - static_cast<int32_t>(address);");
|
||||
sb.AppendLine(" if ((delta & 0x3) != 0 || delta < -0x02000000 || delta > 0x01FFFFFC) {");
|
||||
sb.AppendLine(" FatalStaticPatchRange(address, target);");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" PatchGuestU32(address, (original & 0xFC000003u) | (static_cast<uint32_t>(delta) & 0x03FFFFFCu));");
|
||||
sb.AppendLine(" }");
|
||||
return;
|
||||
}
|
||||
case KamekCommandId.Write32:
|
||||
sb.AppendLine($" PatchGuestU32(0x{address:X8}u, 0x{patch.Arguments[0]:X8}u);");
|
||||
return;
|
||||
case KamekCommandId.Write16:
|
||||
sb.AppendLine($" PatchGuestU16(0x{address:X8}u, 0x{patch.Arguments[0] & 0xFFFFu:X4}u);");
|
||||
return;
|
||||
case KamekCommandId.Write8:
|
||||
sb.AppendLine($" PatchGuestU8(0x{address:X8}u, 0x{patch.Arguments[0] & 0xFFu:X2}u);");
|
||||
return;
|
||||
case KamekCommandId.CondWritePointer:
|
||||
{
|
||||
var value = KamekAddress.Resolve(patch.Arguments[0], moduleLinkBase);
|
||||
EmitConditionalShadowWrite32(sb, address, patch.Arguments[1], value);
|
||||
return;
|
||||
}
|
||||
case KamekCommandId.CondWrite32:
|
||||
EmitConditionalShadowWrite32(sb, address, patch.Arguments[1], patch.Arguments[0]);
|
||||
return;
|
||||
case KamekCommandId.CondWrite16:
|
||||
EmitConditionalShadowWrite16(sb, address, patch.Arguments[1], patch.Arguments[0]);
|
||||
return;
|
||||
case KamekCommandId.CondWrite8:
|
||||
EmitConditionalShadowWrite8(sb, address, patch.Arguments[1], patch.Arguments[0]);
|
||||
return;
|
||||
case KamekCommandId.Branch:
|
||||
case KamekCommandId.BranchLink:
|
||||
{
|
||||
var target = KamekAddress.Resolve(patch.Arguments[0], moduleLinkBase);
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" uint32_t instruction = 0;");
|
||||
sb.AppendLine($" if (!EncodeStaticPpcBranch(0x{address:X8}u, 0x{target:X8}u, {(patch.CommandId == KamekCommandId.BranchLink ? "true" : "false")}, instruction)) {{");
|
||||
sb.AppendLine($" FatalStaticPatchRange(0x{address:X8}u, 0x{target:X8}u);");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine($" PatchGuestU32(0x{address:X8}u, instruction);");
|
||||
sb.AppendLine(" }");
|
||||
return;
|
||||
}
|
||||
default:
|
||||
throw new InvalidDataException($"Cannot emit executable shadow patch for {patch.CommandId} at 0x{address:X8}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void EmitPatch(StringBuilder sb, KamekPatchClassification patch, uint moduleGuestBase)
|
||||
{
|
||||
var address = patch.CommandAddress;
|
||||
switch (patch.CommandId)
|
||||
{
|
||||
case KamekCommandId.Addr32:
|
||||
{
|
||||
var value = KamekAddress.Resolve(patch.Arguments[0], moduleGuestBase);
|
||||
sb.AppendLine($" Memory::Write32(0x{address:X8}u, 0x{value:X8}u);");
|
||||
return;
|
||||
}
|
||||
case KamekCommandId.Addr16Lo:
|
||||
{
|
||||
var value = KamekAddress.Resolve(patch.Arguments[0], moduleGuestBase) & 0xFFFFu;
|
||||
sb.AppendLine($" Memory::Write16(0x{address:X8}u, 0x{value:X4}u);");
|
||||
return;
|
||||
}
|
||||
case KamekCommandId.Addr16Hi:
|
||||
{
|
||||
var value = KamekAddress.Resolve(patch.Arguments[0], moduleGuestBase) >> 16;
|
||||
sb.AppendLine($" Memory::Write16(0x{address:X8}u, 0x{value:X4}u);");
|
||||
return;
|
||||
}
|
||||
case KamekCommandId.Addr16Ha:
|
||||
{
|
||||
var value = (KamekAddress.Resolve(patch.Arguments[0], moduleGuestBase) + 0x8000u) >> 16;
|
||||
sb.AppendLine($" Memory::Write16(0x{address:X8}u, 0x{value:X4}u);");
|
||||
return;
|
||||
}
|
||||
case KamekCommandId.Write32:
|
||||
sb.AppendLine($" Memory::Write32(0x{address:X8}u, 0x{patch.Arguments[0]:X8}u);");
|
||||
return;
|
||||
case KamekCommandId.Write16:
|
||||
sb.AppendLine($" Memory::Write16(0x{address:X8}u, 0x{patch.Arguments[0] & 0xFFFFu:X4}u);");
|
||||
return;
|
||||
case KamekCommandId.Write8:
|
||||
sb.AppendLine($" Memory::Write8(0x{address:X8}u, 0x{patch.Arguments[0] & 0xFFu:X2}u);");
|
||||
return;
|
||||
case KamekCommandId.CondWritePointer:
|
||||
{
|
||||
var value = KamekAddress.Resolve(patch.Arguments[0], moduleGuestBase);
|
||||
EmitConditionalWrite32(sb, address, patch.Arguments[1], value);
|
||||
return;
|
||||
}
|
||||
case KamekCommandId.CondWrite32:
|
||||
EmitConditionalWrite32(sb, address, patch.Arguments[1], patch.Arguments[0]);
|
||||
return;
|
||||
case KamekCommandId.CondWrite16:
|
||||
EmitConditionalWrite16(sb, address, patch.Arguments[1] & 0xFFFFu, patch.Arguments[0] & 0xFFFFu);
|
||||
return;
|
||||
case KamekCommandId.CondWrite8:
|
||||
EmitConditionalWrite8(sb, address, patch.Arguments[1] & 0xFFu, patch.Arguments[0] & 0xFFu);
|
||||
return;
|
||||
default:
|
||||
throw new InvalidDataException($"Cannot emit data patch for {patch.CommandId} at 0x{address:X8}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void EmitConditionalWrite32(StringBuilder sb, uint address, uint expected, uint value)
|
||||
{
|
||||
sb.AppendLine($" if (Memory::Read32(0x{address:X8}u) == 0x{expected:X8}u) {{");
|
||||
sb.AppendLine($" Memory::Write32(0x{address:X8}u, 0x{value:X8}u);");
|
||||
sb.AppendLine(" } else {");
|
||||
sb.AppendLine($" std::cerr << \"[mod] Skipped CondWrite32 at 0x{address:X8}\" << std::endl;");
|
||||
sb.AppendLine(" }");
|
||||
}
|
||||
|
||||
private static void EmitConditionalWrite16(StringBuilder sb, uint address, uint expected, uint value)
|
||||
{
|
||||
sb.AppendLine($" if (Memory::Read16(0x{address:X8}u) == 0x{expected:X4}u) {{");
|
||||
sb.AppendLine($" Memory::Write16(0x{address:X8}u, 0x{value:X4}u);");
|
||||
sb.AppendLine(" } else {");
|
||||
sb.AppendLine($" std::cerr << \"[mod] Skipped CondWrite16 at 0x{address:X8}\" << std::endl;");
|
||||
sb.AppendLine(" }");
|
||||
}
|
||||
|
||||
private static void EmitConditionalWrite8(StringBuilder sb, uint address, uint expected, uint value)
|
||||
{
|
||||
sb.AppendLine($" if (Memory::Read8(0x{address:X8}u) == 0x{expected:X2}u) {{");
|
||||
sb.AppendLine($" Memory::Write8(0x{address:X8}u, 0x{value:X2}u);");
|
||||
sb.AppendLine(" } else {");
|
||||
sb.AppendLine($" std::cerr << \"[mod] Skipped CondWrite8 at 0x{address:X8}\" << std::endl;");
|
||||
sb.AppendLine(" }");
|
||||
}
|
||||
|
||||
private static void EmitConditionalShadowWrite32(StringBuilder sb, uint address, uint expected, uint value)
|
||||
{
|
||||
sb.AppendLine($" if (Memory::Read32(0x{address:X8}u) == 0x{expected:X8}u) {{");
|
||||
sb.AppendLine($" PatchGuestU32(0x{address:X8}u, 0x{value:X8}u);");
|
||||
sb.AppendLine(" } else {");
|
||||
sb.AppendLine($" std::cerr << \"[mod] Skipped executable CondWrite32 shadow patch at 0x{address:X8}\" << std::endl;");
|
||||
sb.AppendLine(" }");
|
||||
}
|
||||
|
||||
private static void EmitConditionalShadowWrite16(StringBuilder sb, uint address, uint expected, uint value)
|
||||
{
|
||||
sb.AppendLine($" if (Memory::Read16(0x{address:X8}u) == 0x{expected & 0xFFFFu:X4}u) {{");
|
||||
sb.AppendLine($" PatchGuestU16(0x{address:X8}u, 0x{value & 0xFFFFu:X4}u);");
|
||||
sb.AppendLine(" } else {");
|
||||
sb.AppendLine($" std::cerr << \"[mod] Skipped executable CondWrite16 shadow patch at 0x{address:X8}\" << std::endl;");
|
||||
sb.AppendLine(" }");
|
||||
}
|
||||
|
||||
private static void EmitConditionalShadowWrite8(StringBuilder sb, uint address, uint expected, uint value)
|
||||
{
|
||||
sb.AppendLine($" if (Memory::Read8(0x{address:X8}u) == 0x{expected & 0xFFu:X2}u) {{");
|
||||
sb.AppendLine($" PatchGuestU8(0x{address:X8}u, 0x{value & 0xFFu:X2}u);");
|
||||
sb.AppendLine(" } else {");
|
||||
sb.AppendLine($" std::cerr << \"[mod] Skipped executable CondWrite8 shadow patch at 0x{address:X8}\" << std::endl;");
|
||||
sb.AppendLine(" }");
|
||||
}
|
||||
|
||||
private static string EscapeCxxStringLiteral(string value) =>
|
||||
value.Replace("\\", "\\\\", StringComparison.Ordinal)
|
||||
.Replace("\"", "\\\"", StringComparison.Ordinal);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace Translator.Core.Mods;
|
||||
|
||||
/// <summary>
|
||||
/// A Riivolution option choice pinned by the distribution spec. Section may be empty,
|
||||
/// meaning "match any section"; Choice is the 1-based choice index, 0 meaning disabled.
|
||||
/// </summary>
|
||||
public sealed record ModRiivolutionOption(string Section, string Option, uint Choice);
|
||||
|
||||
/// <summary>
|
||||
/// The pack-relative directories the runtime mounts over the disc file system.
|
||||
/// These reach the runtime as <c>RecompMod::RegisterDvdOverlayRoot</c> calls in the
|
||||
/// generated mod data-patch translation unit, not as a side-car manifest.
|
||||
/// </summary>
|
||||
public static class ModDvdOverlayRoots
|
||||
{
|
||||
public static IReadOnlyList<string> Discover(string? modRoot)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modRoot))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var sourceRoot = Path.GetFullPath(modRoot);
|
||||
var packageRoot = Path.GetFileName(sourceRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
||||
if (string.IsNullOrWhiteSpace(packageRoot))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var candidates = new[]
|
||||
{
|
||||
(Source: sourceRoot, Package: packageRoot),
|
||||
(Source: Path.Combine(sourceRoot, "files"), Package: Path.Combine(packageRoot, "files"))
|
||||
};
|
||||
|
||||
return candidates
|
||||
.Where(candidate => Directory.Exists(candidate.Source))
|
||||
.Select(candidate => candidate.Package.Replace('\\', '/'))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,733 @@
|
||||
using System.Buffers.Binary;
|
||||
using Translator.Core.Disassembly;
|
||||
using Translator.Core.Parsing.Kamek;
|
||||
|
||||
namespace Translator.Core.Mods;
|
||||
|
||||
public sealed record ModFunctionStart(uint Address, string Reason);
|
||||
|
||||
/// <summary>
|
||||
/// Recovers module entry points from the relocated Kamek image alone. Retro Rewind
|
||||
/// packs ship no Code.map, so these heuristics are the only source of module
|
||||
/// function boundaries.
|
||||
/// </summary>
|
||||
public static class ModFunctionDiscovery
|
||||
{
|
||||
public static IReadOnlyList<ModFunctionStart> DiscoverKamekFunctions(
|
||||
KamekChunk chunk,
|
||||
uint moduleGuestBase,
|
||||
byte[] relocatedModuleImage)
|
||||
{
|
||||
var starts = new Dictionary<uint, string>();
|
||||
var moduleCodeEnd = checked(moduleGuestBase + chunk.CodeSize);
|
||||
var moduleImageEnd = checked(moduleGuestBase + (uint)relocatedModuleImage.Length);
|
||||
|
||||
void Add(uint address, string reason, uint moduleEnd)
|
||||
{
|
||||
if (address < moduleGuestBase || address >= moduleEnd || (address & 0x3u) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
starts.TryAdd(address, reason);
|
||||
}
|
||||
|
||||
void AddCodeAddress(uint address, string reason) => Add(address, reason, moduleCodeEnd);
|
||||
|
||||
void AddImageAddress(uint address, string reason) => Add(address, reason, moduleImageEnd);
|
||||
|
||||
if (LooksLikeFunctionPointerTarget(moduleGuestBase, moduleGuestBase, moduleCodeEnd, relocatedModuleImage))
|
||||
{
|
||||
AddCodeAddress(moduleGuestBase, "module base");
|
||||
}
|
||||
|
||||
foreach (var command in chunk.Commands)
|
||||
{
|
||||
if (command.Arguments.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (command.Id)
|
||||
{
|
||||
case KamekCommandId.Rel24:
|
||||
case KamekCommandId.Branch:
|
||||
case KamekCommandId.BranchLink:
|
||||
AddImageAddress(KamekAddress.Resolve(command.Arguments[0], moduleGuestBase), command.Id.ToString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.CtorStart <= chunk.CtorEnd && chunk.CtorEnd <= relocatedModuleImage.Length)
|
||||
{
|
||||
for (var offset = checked((int)chunk.CtorStart); offset + 4 <= chunk.CtorEnd; offset += 4)
|
||||
{
|
||||
var target = BinaryPrimitives.ReadUInt32BigEndian(relocatedModuleImage.AsSpan(offset, 4));
|
||||
AddImageAddress(target, "ctor");
|
||||
}
|
||||
}
|
||||
|
||||
// Kamek modules frequently register callbacks through static tables or lists that are
|
||||
// walked indirectly by module startup code. Seed those native translations ahead of time.
|
||||
for (var offset = 0; offset + 4 <= relocatedModuleImage.Length; offset += 4)
|
||||
{
|
||||
var value = BinaryPrimitives.ReadUInt32BigEndian(relocatedModuleImage.AsSpan(offset, 4));
|
||||
if (LooksLikeFunctionPointerTarget(value, moduleGuestBase, moduleImageEnd, relocatedModuleImage))
|
||||
{
|
||||
AddImageAddress(value, "module data pointer");
|
||||
}
|
||||
}
|
||||
|
||||
for (var offset = 0; offset + 4 <= relocatedModuleImage.Length; offset += 4)
|
||||
{
|
||||
var address = checked(moduleGuestBase + (uint)offset);
|
||||
if (LooksLikeFunctionPrologue(address, moduleGuestBase, moduleCodeEnd, relocatedModuleImage))
|
||||
{
|
||||
AddCodeAddress(address, "module prologue scan");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var address in DiscoverSynthesizedFunctionPointers(moduleGuestBase, moduleImageEnd, relocatedModuleImage))
|
||||
{
|
||||
AddImageAddress(address, "module synthesized pointer");
|
||||
}
|
||||
|
||||
foreach (var address in DiscoverLeafBoundaryFunctions(moduleGuestBase, moduleCodeEnd, relocatedModuleImage, starts.Keys))
|
||||
{
|
||||
AddCodeAddress(address, "module leaf boundary scan");
|
||||
}
|
||||
|
||||
foreach (var address in DiscoverTailEntryFunctions(moduleGuestBase, moduleImageEnd, relocatedModuleImage, starts.Keys))
|
||||
{
|
||||
AddImageAddress(address, "module tail-entry scan");
|
||||
}
|
||||
|
||||
return starts
|
||||
.OrderBy(kvp => kvp.Key)
|
||||
.Select(kvp => new ModFunctionStart(kvp.Key, kvp.Value))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static bool LooksLikeFunctionPrologue(
|
||||
uint address,
|
||||
uint moduleGuestBase,
|
||||
uint moduleEnd,
|
||||
byte[] relocatedModuleImage)
|
||||
{
|
||||
if (address < moduleGuestBase ||
|
||||
address >= moduleEnd ||
|
||||
(address & 0x3u) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var offset = checked((int)(address - moduleGuestBase));
|
||||
if (offset < 0 || offset + 4 > relocatedModuleImage.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var firstWord = PpcWordFields.ReadBigEndianWord(relocatedModuleImage, offset);
|
||||
if (IsStackFramePrologue(firstWord))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (offset + 8 <= relocatedModuleImage.Length)
|
||||
{
|
||||
var secondWord = PpcWordFields.ReadBigEndianWord(relocatedModuleImage, offset + 4);
|
||||
if (firstWord == 0x7C0802A6u && IsStackFramePrologue(secondWord))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool LooksLikeFunctionPointerTarget(
|
||||
uint address,
|
||||
uint moduleGuestBase,
|
||||
uint moduleEnd,
|
||||
byte[] relocatedModuleImage)
|
||||
{
|
||||
if (address < moduleGuestBase || address >= moduleEnd || (address & 0x3u) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var offset = checked((int)(address - moduleGuestBase));
|
||||
if (offset < 0 || offset + 4 > relocatedModuleImage.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var firstWord = PpcWordFields.ReadBigEndianWord(relocatedModuleImage, offset);
|
||||
if (firstWord == 0 || firstWord == 0xFFFFFFFFu)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return LooksLikeFunctionPrologue(address, moduleGuestBase, moduleEnd, relocatedModuleImage) ||
|
||||
LooksLikeBranchTrampoline(address, firstWord, moduleGuestBase, moduleEnd) ||
|
||||
PpcControlFlow.IsReturn(firstWord) ||
|
||||
LooksLikeLeafFunction(address, moduleGuestBase, moduleEnd, relocatedModuleImage);
|
||||
}
|
||||
|
||||
private static bool LooksLikeBranchTrampoline(uint address, uint word, uint moduleGuestBase, uint moduleEnd) =>
|
||||
// ASCII data starting with 'H' or 'I' (0x48/0x49) decodes as a forward
|
||||
// unconditional branch, so a lone branch word only counts as a
|
||||
// trampoline when it lands somewhere code can actually live: base-game
|
||||
// MEM1 or the module image itself, which sits above kGuestExecutableLimit
|
||||
// now that the overlay is based at 0x81800000.
|
||||
PpcControlFlow.IsRelativeUnlinkedBranch(word) &&
|
||||
PpcControlFlow.TryDecodeRelativeBranchTarget(address, word, out var target) &&
|
||||
(target & 0x3u) == 0 &&
|
||||
((target >= 0x80000000u && target < kGuestExecutableLimit) ||
|
||||
(target >= moduleGuestBase && target < moduleEnd));
|
||||
|
||||
private static IEnumerable<uint> DiscoverSynthesizedFunctionPointers(
|
||||
uint moduleGuestBase,
|
||||
uint moduleEnd,
|
||||
byte[] relocatedModuleImage)
|
||||
{
|
||||
for (var offset = 0; offset + 8 <= relocatedModuleImage.Length; offset += 4)
|
||||
{
|
||||
var first = PpcWordFields.ReadBigEndianWord(relocatedModuleImage, offset);
|
||||
if (!PpcInstructionPatterns.TryGetLis(first, out var register, out var high))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var baseValue = high << 16;
|
||||
var scanEnd = Math.Min(offset + 36, relocatedModuleImage.Length);
|
||||
for (var cursor = offset + 4; cursor + 4 <= scanEnd; cursor += 4)
|
||||
{
|
||||
var word = PpcWordFields.ReadBigEndianWord(relocatedModuleImage, cursor);
|
||||
if (TryBuildAddress(word, register, baseValue, out var candidate, out var valueRegister))
|
||||
{
|
||||
if (LooksLikeSynthesizedFunctionPointerTarget(candidate, moduleGuestBase, moduleEnd, relocatedModuleImage) &&
|
||||
IsPointerValueUsed(relocatedModuleImage, cursor + 4, valueRegister, moduleGuestBase, moduleEnd))
|
||||
{
|
||||
yield return candidate;
|
||||
}
|
||||
|
||||
if (valueRegister == register)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (WritesRegister(word, register))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<uint> DiscoverLeafBoundaryFunctions(
|
||||
uint moduleGuestBase,
|
||||
uint moduleEnd,
|
||||
byte[] relocatedModuleImage,
|
||||
IEnumerable<uint> knownStarts)
|
||||
{
|
||||
var knownStartSet = knownStarts.ToHashSet();
|
||||
var sortedKnownStarts = knownStartSet.Order().ToArray();
|
||||
|
||||
for (var offset = 4; offset + 4 <= relocatedModuleImage.Length; offset += 4)
|
||||
{
|
||||
var address = checked(moduleGuestBase + (uint)offset);
|
||||
if (knownStartSet.Contains(address))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var previousAddress = checked(moduleGuestBase + (uint)(offset - 4));
|
||||
var previousWord = PpcWordFields.ReadBigEndianWord(relocatedModuleImage, offset - 4);
|
||||
if (!IsFunctionBoundaryInstruction(previousAddress, previousWord, knownStartSet, moduleGuestBase, moduleEnd))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var nextKnownStart = NextKnownStartAfter(sortedKnownStarts, address);
|
||||
if (nextKnownStart is null || nextKnownStart.Value - address > 0x200u)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (LooksLikeFunctionPrologue(address, moduleGuestBase, moduleEnd, relocatedModuleImage))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (LooksLikeBoundedLeafFunction(address, nextKnownStart.Value, moduleGuestBase, moduleEnd, relocatedModuleImage, knownStartSet))
|
||||
{
|
||||
yield return address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static uint? NextKnownStartAfter(uint[] sortedKnownStarts, uint address)
|
||||
{
|
||||
foreach (var start in sortedKnownStarts)
|
||||
{
|
||||
if (start > address)
|
||||
{
|
||||
return start;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<uint> DiscoverTailEntryFunctions(
|
||||
uint moduleGuestBase,
|
||||
uint moduleEnd,
|
||||
byte[] relocatedModuleImage,
|
||||
IEnumerable<uint> knownStarts)
|
||||
{
|
||||
var knownStartSet = knownStarts.ToHashSet();
|
||||
var sortedKnownStarts = knownStartSet.Order().ToArray();
|
||||
|
||||
for (var offset = 4; offset + 4 <= relocatedModuleImage.Length; offset += 4)
|
||||
{
|
||||
var address = checked(moduleGuestBase + (uint)offset);
|
||||
if (knownStartSet.Contains(address) ||
|
||||
LooksLikeFunctionPrologue(address, moduleGuestBase, moduleEnd, relocatedModuleImage))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var previousWord = PpcWordFields.ReadBigEndianWord(relocatedModuleImage, offset - 4);
|
||||
if (!IsFunctionTerminator(previousWord) && !PpcControlFlow.IsRelativeUnlinkedBranch(previousWord))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var nextBoundary = NextKnownStartOrPrologueAfter(sortedKnownStarts, address, moduleGuestBase, moduleEnd, relocatedModuleImage);
|
||||
if (nextBoundary is null || nextBoundary.Value - address > 0x40u)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (LooksLikeShortTailEntry(address, nextBoundary.Value, moduleGuestBase, moduleEnd, relocatedModuleImage))
|
||||
{
|
||||
yield return address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static uint? NextKnownStartOrPrologueAfter(
|
||||
uint[] sortedKnownStarts,
|
||||
uint address,
|
||||
uint moduleGuestBase,
|
||||
uint moduleEnd,
|
||||
byte[] relocatedModuleImage)
|
||||
{
|
||||
var nextKnownStart = NextKnownStartAfter(sortedKnownStarts, address);
|
||||
var scanEnd = nextKnownStart ?? moduleEnd;
|
||||
|
||||
for (var cursor = checked(address + 4); cursor < scanEnd; cursor += 4)
|
||||
{
|
||||
if (LooksLikeFunctionPrologue(cursor, moduleGuestBase, moduleEnd, relocatedModuleImage))
|
||||
{
|
||||
return cursor;
|
||||
}
|
||||
}
|
||||
|
||||
return nextKnownStart;
|
||||
}
|
||||
|
||||
private static bool LooksLikeShortTailEntry(
|
||||
uint address,
|
||||
uint nextBoundary,
|
||||
uint moduleGuestBase,
|
||||
uint moduleEnd,
|
||||
byte[] relocatedModuleImage)
|
||||
{
|
||||
if (address >= nextBoundary ||
|
||||
address < moduleGuestBase ||
|
||||
nextBoundary > moduleEnd ||
|
||||
(address & 0x3u) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var offset = checked((int)(address - moduleGuestBase));
|
||||
var endOffset = checked((int)Math.Min(nextBoundary - moduleGuestBase, (uint)relocatedModuleImage.Length));
|
||||
if (offset < 0 || offset + 8 > endOffset || LooksLikeInlineAsciiData(relocatedModuleImage, offset, endOffset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var sawNonBranchInstruction = false;
|
||||
for (var cursor = offset; cursor + 4 <= endOffset; cursor += 4)
|
||||
{
|
||||
var word = PpcWordFields.ReadBigEndianWord(relocatedModuleImage, cursor);
|
||||
if (!LooksLikeInstruction(word))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentAddress = checked(moduleGuestBase + (uint)cursor);
|
||||
if (PpcControlFlow.IsRelativeUnlinkedBranch(word) &&
|
||||
PpcControlFlow.TryDecodeRelativeBranchTarget(currentAddress, word, out var branchTarget))
|
||||
{
|
||||
return sawNonBranchInstruction &&
|
||||
(branchTarget < address || branchTarget >= nextBoundary) &&
|
||||
LooksLikeExecutableTailTarget(branchTarget, moduleGuestBase, moduleEnd, relocatedModuleImage);
|
||||
}
|
||||
|
||||
if (IsFunctionTerminator(word))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
sawNonBranchInstruction = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool LooksLikeExecutableTailTarget(
|
||||
uint address,
|
||||
uint moduleGuestBase,
|
||||
uint moduleEnd,
|
||||
byte[] relocatedModuleImage) =>
|
||||
LooksLikeExternalExecutableBranchTarget(address, moduleGuestBase, moduleEnd) ||
|
||||
LooksLikeFunctionPointerTarget(address, moduleGuestBase, moduleEnd, relocatedModuleImage);
|
||||
|
||||
private static bool TryBuildAddress(uint word, int baseRegister, uint baseValue, out uint address, out int valueRegister)
|
||||
{
|
||||
if (PpcInstructionPatterns.TryGetAddi(word, out var destination, out var source, out var immediate))
|
||||
{
|
||||
if (source != baseRegister)
|
||||
{
|
||||
address = 0;
|
||||
valueRegister = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
address = unchecked(baseValue + (uint)immediate);
|
||||
valueRegister = destination;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (PpcInstructionPatterns.TryGetOri(word, out var oriSource, out var oriDestination, out var oriImmediate))
|
||||
{
|
||||
if (oriSource != baseRegister)
|
||||
{
|
||||
address = 0;
|
||||
valueRegister = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
address = baseValue | oriImmediate;
|
||||
valueRegister = oriDestination;
|
||||
return true;
|
||||
}
|
||||
|
||||
address = 0;
|
||||
valueRegister = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsPointerValueUsed(
|
||||
byte[] relocatedModuleImage,
|
||||
int startOffset,
|
||||
int register,
|
||||
uint moduleGuestBase,
|
||||
uint moduleEnd)
|
||||
{
|
||||
// Bounded to cover typical constructor prologue/setup before the value is stored or used;
|
||||
// a write to the value register still ends the search so a later unrelated store can't fake liveness.
|
||||
var scanEnd = Math.Min(startOffset + 128, relocatedModuleImage.Length);
|
||||
for (var offset = startOffset; offset + 4 <= scanEnd; offset += 4)
|
||||
{
|
||||
var word = PpcWordFields.ReadBigEndianWord(relocatedModuleImage, offset);
|
||||
if (PpcRegisterEffects.MayWriteGpr(word, register))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (StoresRegister(word, register) || MovesRegisterToCtr(word, register))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsBranchWithLink(word))
|
||||
{
|
||||
// A call passes r3-r10 into the callee and clobbers the other
|
||||
// volatile registers, so the pointer either escapes here as an
|
||||
// argument or dies with the call. Callee-saved values survive.
|
||||
if (IsArgumentRegister(register))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!IsCalleeSavedRegister(register))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (PpcControlFlow.IsRelativeUnlinkedBranch(word))
|
||||
{
|
||||
// An unconditional branch out of the module is a tail call into
|
||||
// base code: argument registers flow into the callee unchanged.
|
||||
if (PpcControlFlow.TryDecodeRelativeBranchTarget(checked(moduleGuestBase + (uint)offset), word, out var branchTarget) &&
|
||||
LooksLikeExternalExecutableBranchTarget(branchTarget, moduleGuestBase, moduleEnd))
|
||||
{
|
||||
return IsArgumentRegister(register);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsFunctionTerminator(word))
|
||||
{
|
||||
// bctr is an indirect tail call, so argument registers escape;
|
||||
// blr only publishes the pointer through the r3 return value.
|
||||
// Either way the linear scan must not run into the next function.
|
||||
return word == 0x4E800420u ? IsArgumentRegister(register) : register == 3;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsArgumentRegister(int register) => register is >= 3 and <= 10;
|
||||
|
||||
private static bool IsCalleeSavedRegister(int register) => register is 1 or 2 or >= 13;
|
||||
|
||||
private static bool IsBranchWithLink(uint word)
|
||||
{
|
||||
var fields = new PpcWordFields(word);
|
||||
return (fields.PrimaryOpcode == 18 && (word & 0x1u) != 0) || // bl / bla
|
||||
word == 0x4E800021u || // blrl
|
||||
word == 0x4E800421u; // bctrl
|
||||
}
|
||||
|
||||
private static bool LooksLikeSynthesizedFunctionPointerTarget(
|
||||
uint address,
|
||||
uint moduleGuestBase,
|
||||
uint moduleEnd,
|
||||
byte[] relocatedModuleImage)
|
||||
{
|
||||
if (!LooksLikeFunctionPointerTarget(address, moduleGuestBase, moduleEnd, relocatedModuleImage))
|
||||
{
|
||||
return LooksLikeLeafFunction(address, moduleGuestBase, moduleEnd, relocatedModuleImage);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool LooksLikeLeafFunction(
|
||||
uint address,
|
||||
uint moduleGuestBase,
|
||||
uint moduleEnd,
|
||||
byte[] relocatedModuleImage)
|
||||
{
|
||||
if (address < moduleGuestBase || address >= moduleEnd || (address & 0x3u) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var offset = checked((int)(address - moduleGuestBase));
|
||||
if (offset < 0 || offset + 4 > relocatedModuleImage.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var firstWord = PpcWordFields.ReadBigEndianWord(relocatedModuleImage, offset);
|
||||
if (firstWord == 0 || firstWord == 0xFFFFFFFFu)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var scanEnd = Math.Min(offset + 128, relocatedModuleImage.Length);
|
||||
for (var cursor = offset; cursor + 4 <= scanEnd; cursor += 4)
|
||||
{
|
||||
var word = PpcWordFields.ReadBigEndianWord(relocatedModuleImage, cursor);
|
||||
if (IsFunctionTerminator(word))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (cursor != offset && LooksLikeFunctionPrologue(checked(moduleGuestBase + (uint)cursor), moduleGuestBase, moduleEnd, relocatedModuleImage))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool LooksLikeBoundedLeafFunction(
|
||||
uint address,
|
||||
uint nextKnownStart,
|
||||
uint moduleGuestBase,
|
||||
uint moduleEnd,
|
||||
byte[] relocatedModuleImage,
|
||||
IReadOnlySet<uint> knownStarts)
|
||||
{
|
||||
if (address >= nextKnownStart ||
|
||||
address < moduleGuestBase ||
|
||||
address >= moduleEnd ||
|
||||
(address & 0x3u) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var offset = checked((int)(address - moduleGuestBase));
|
||||
var endOffset = checked((int)Math.Min(nextKnownStart - moduleGuestBase, (uint)relocatedModuleImage.Length));
|
||||
if (offset < 0 || offset + 4 > endOffset)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var firstWord = PpcWordFields.ReadBigEndianWord(relocatedModuleImage, offset);
|
||||
if (!LooksLikeInstruction(firstWord) || IsFunctionTerminator(firstWord) || LooksLikeInlineAsciiData(relocatedModuleImage, offset, endOffset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var sawNonBranchInstruction = false;
|
||||
for (var cursor = offset; cursor + 4 <= endOffset; cursor += 4)
|
||||
{
|
||||
var word = PpcWordFields.ReadBigEndianWord(relocatedModuleImage, cursor);
|
||||
if (!LooksLikeInstruction(word))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsFunctionTerminator(word))
|
||||
{
|
||||
return sawNonBranchInstruction;
|
||||
}
|
||||
|
||||
var currentAddress = checked(moduleGuestBase + (uint)cursor);
|
||||
if (PpcControlFlow.TryDecodeConditionalRelativeBranchTarget(currentAddress, word, out var conditionalTarget) &&
|
||||
(conditionalTarget < address || conditionalTarget >= nextKnownStart))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PpcControlFlow.IsRelativeUnlinkedBranch(word) &&
|
||||
PpcControlFlow.TryDecodeRelativeBranchTarget(currentAddress, word, out var branchTarget) &&
|
||||
(branchTarget < address || branchTarget >= nextKnownStart))
|
||||
{
|
||||
if (knownStarts.Contains(branchTarget) ||
|
||||
LooksLikeExternalExecutableBranchTarget(branchTarget, moduleGuestBase, moduleEnd))
|
||||
{
|
||||
return sawNonBranchInstruction;
|
||||
}
|
||||
}
|
||||
|
||||
if (!PpcControlFlow.IsRelativeUnlinkedBranch(word))
|
||||
{
|
||||
sawNonBranchInstruction = true;
|
||||
}
|
||||
|
||||
if (cursor != offset && LooksLikeFunctionPrologue(checked(moduleGuestBase + (uint)cursor), moduleGuestBase, moduleEnd, relocatedModuleImage))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool LooksLikeInlineAsciiData(byte[] relocatedModuleImage, int offset, int endOffset)
|
||||
{
|
||||
var scanEnd = Math.Min(offset + 16, endOffset);
|
||||
var examined = 0;
|
||||
var printableOrZero = 0;
|
||||
|
||||
for (var cursor = offset; cursor < scanEnd; cursor++)
|
||||
{
|
||||
var value = relocatedModuleImage[cursor];
|
||||
examined++;
|
||||
if (value == 0 || (value >= 0x20 && value <= 0x7E))
|
||||
{
|
||||
printableOrZero++;
|
||||
}
|
||||
}
|
||||
|
||||
return examined >= 8 && printableOrZero == examined;
|
||||
}
|
||||
|
||||
private static bool WritesRegister(uint word, int register)
|
||||
=> PpcRegisterEffects.MayWriteGpr(word, register);
|
||||
|
||||
private static bool StoresRegister(uint word, int register)
|
||||
{
|
||||
var fields = new PpcWordFields(word);
|
||||
var opcode = fields.PrimaryOpcode;
|
||||
var rs = fields.GprField0;
|
||||
return (opcode is 36 or 37 or 38 or 44 or 45 or 46 or 52 or 53 or 54) &&
|
||||
rs == register;
|
||||
}
|
||||
|
||||
private static bool MovesRegisterToCtr(uint word, int register)
|
||||
{
|
||||
// mtctr rS is mtspr 9,rS. The split SPR field encodes CTR as 0x120.
|
||||
return PpcInstructionPatterns.TryGetMtspr(word, 9, out var source) && source == register;
|
||||
}
|
||||
|
||||
private static bool IsStackFramePrologue(uint word) =>
|
||||
(word & 0xFFFF0000u) == 0x94210000u &&
|
||||
(short)(word & 0xFFFFu) < 0;
|
||||
|
||||
private static bool IsFunctionBoundaryInstruction(
|
||||
uint address,
|
||||
uint word,
|
||||
IReadOnlySet<uint> knownStarts,
|
||||
uint moduleGuestBase,
|
||||
uint moduleEnd)
|
||||
{
|
||||
if (IsFunctionTerminator(word))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return PpcControlFlow.IsRelativeUnlinkedBranch(word) &&
|
||||
PpcControlFlow.TryDecodeRelativeBranchTarget(address, word, out var branchTarget) &&
|
||||
(knownStarts.Contains(branchTarget) ||
|
||||
LooksLikeExternalExecutableBranchTarget(branchTarget, moduleGuestBase, moduleEnd));
|
||||
}
|
||||
|
||||
// End of cached MEM1 (0x80000000-0x817FFFFF); guest code never executes
|
||||
// above it, so branch targets beyond this are data misread as instructions.
|
||||
private const uint kGuestExecutableLimit = 0x81800000u;
|
||||
|
||||
private static bool LooksLikeExternalExecutableBranchTarget(uint address, uint moduleGuestBase, uint moduleEnd) =>
|
||||
address >= 0x80000000u &&
|
||||
address < kGuestExecutableLimit &&
|
||||
(address & 0x3u) == 0 &&
|
||||
(address < moduleGuestBase || address >= moduleEnd);
|
||||
|
||||
private static bool LooksLikeInstruction(uint word)
|
||||
{
|
||||
if (word == 0 || word == 0xFFFFFFFFu)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var opcode = new PpcWordFields(word).PrimaryOpcode;
|
||||
return opcode is
|
||||
3 or 4 or
|
||||
7 or 8 or 10 or 11 or 12 or 13 or 14 or 15 or 16 or 17 or 18 or 19 or
|
||||
20 or 21 or 23 or 24 or 26 or 28 or 29 or 31 or
|
||||
32 or 33 or 34 or 35 or 36 or 37 or 38 or 39 or
|
||||
40 or 41 or 42 or 43 or 44 or 45 or 46 or 47 or
|
||||
48 or 49 or 50 or 51 or 52 or 53 or 54 or 55 or
|
||||
56 or 57 or 58 or 59 or 60 or 61 or 62 or 63;
|
||||
}
|
||||
|
||||
private static bool IsFunctionTerminator(uint word) =>
|
||||
PpcControlFlow.IsBctr(word) ||
|
||||
PpcControlFlow.IsReturn(word);
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using Translator.Core.Parsing.Kamek;
|
||||
using Translator.Core.Mods.Mkwii;
|
||||
using Translator.Core.Loading;
|
||||
|
||||
namespace Translator.Core.Mods;
|
||||
|
||||
public sealed record PatchApplicationRecord(
|
||||
uint Address,
|
||||
KamekCommandId CommandId,
|
||||
string Target,
|
||||
string Status,
|
||||
string Detail);
|
||||
|
||||
public sealed record OverlayFunctionImage(
|
||||
uint Start,
|
||||
uint End,
|
||||
string Name,
|
||||
string SectionName,
|
||||
string ImageFile,
|
||||
string Sha256,
|
||||
int PatchCount,
|
||||
IReadOnlyList<PatchApplicationRecord> Patches);
|
||||
|
||||
public sealed record OverlayBuildResult(
|
||||
uint ModuleGuestBase,
|
||||
string ModuleImageFile,
|
||||
uint ModuleImageSize,
|
||||
string ModuleImageSha256,
|
||||
int ModulePatchCount,
|
||||
IReadOnlyList<OverlayFunctionImage> OverlayFunctions,
|
||||
IReadOnlyList<PatchApplicationRecord> Diagnostics);
|
||||
|
||||
public static class OverlayFunctionBuilder
|
||||
{
|
||||
public static OverlayBuildResult BuildAndWrite(
|
||||
KamekChunk chunk,
|
||||
BaseManifest baseManifest,
|
||||
KamekPatchPlan patchPlan,
|
||||
string baseManifestDirectory,
|
||||
string outputDirectory,
|
||||
IReadOnlyCollection<uint>? additionalOverlayFunctionStarts = null,
|
||||
byte[]? additionalModuleImage = null,
|
||||
uint? additionalModuleImageOffset = null,
|
||||
IReadOnlyCollection<RetroWfcStaticBytePatchPlan>? retroWfcStaticBytePatches = null,
|
||||
IReadOnlyCollection<RetroWfcExecutableHookPlan>? retroWfcExecutableHooks = null)
|
||||
{
|
||||
var overlayDir = Path.Combine(outputDirectory, "overlay_images");
|
||||
Directory.CreateDirectory(overlayDir);
|
||||
|
||||
var diagnostics = new List<PatchApplicationRecord>();
|
||||
var moduleImage = BuildModuleImage(chunk.CodeBlob, additionalModuleImage, additionalModuleImageOffset);
|
||||
var modulePatchCount = 0;
|
||||
foreach (var command in chunk.Commands.Where(c => c.AddressIsRelative))
|
||||
{
|
||||
var target = patchPlan.ModuleGuestBase + command.Address;
|
||||
if (TryApplyPatch(
|
||||
moduleImage,
|
||||
patchPlan.ModuleGuestBase,
|
||||
target,
|
||||
command,
|
||||
patchPlan.ModuleGuestBase,
|
||||
out var record))
|
||||
{
|
||||
modulePatchCount++;
|
||||
}
|
||||
diagnostics.Add(record);
|
||||
}
|
||||
|
||||
var moduleImageFile = $"kamek_module_{patchPlan.ModuleGuestBase:X8}.bin";
|
||||
File.WriteAllBytes(Path.Combine(overlayDir, moduleImageFile), moduleImage);
|
||||
|
||||
var sectionsByName = baseManifest.Sections.ToDictionary(s => s.Name, StringComparer.OrdinalIgnoreCase);
|
||||
var imageCache = new Dictionary<string, byte[]>(StringComparer.OrdinalIgnoreCase);
|
||||
var functionIndex = new BaseFunctionIndex(baseManifest.Functions);
|
||||
var overlays = new List<OverlayFunctionImage>();
|
||||
|
||||
var executablePatchGroups = patchPlan.ExecutablePatches
|
||||
.Where(c => c.ContainingFunctionStart.HasValue)
|
||||
.GroupBy(c => c.ContainingFunctionStart!.Value)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
var retroWfcStaticBytePatchSource = retroWfcStaticBytePatches is null
|
||||
? Enumerable.Empty<RetroWfcStaticBytePatchPlan>()
|
||||
: retroWfcStaticBytePatches;
|
||||
var retroWfcStaticBytePatchGroups = retroWfcStaticBytePatchSource
|
||||
.Where(p => p.SectionExecutable && p.ContainingFunctionStart.HasValue)
|
||||
.GroupBy(p => p.ContainingFunctionStart!.Value)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
var retroWfcExecutableHookSource = retroWfcExecutableHooks is null
|
||||
? Enumerable.Empty<RetroWfcExecutableHookPlan>()
|
||||
: retroWfcExecutableHooks;
|
||||
var retroWfcExecutableHookGroups = retroWfcExecutableHookSource
|
||||
.Where(h => h.TargetAddress.HasValue)
|
||||
.GroupBy(h => h.ContainingFunctionStart)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
var overlayStarts = new SortedSet<uint>(executablePatchGroups.Keys);
|
||||
foreach (var start in retroWfcStaticBytePatchGroups.Keys)
|
||||
{
|
||||
overlayStarts.Add(start);
|
||||
}
|
||||
foreach (var start in retroWfcExecutableHookGroups.Keys)
|
||||
{
|
||||
overlayStarts.Add(start);
|
||||
}
|
||||
if (additionalOverlayFunctionStarts is not null)
|
||||
{
|
||||
foreach (var start in additionalOverlayFunctionStarts)
|
||||
{
|
||||
overlayStarts.Add(start);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var functionStart in overlayStarts)
|
||||
{
|
||||
var function = functionIndex.FindContaining(functionStart)
|
||||
?? throw new InvalidDataException($"No base function found for overlay 0x{functionStart:X8}.");
|
||||
|
||||
if (!sectionsByName.TryGetValue(function.SourceSection, out var section))
|
||||
{
|
||||
throw new InvalidDataException($"Base function 0x{function.Start:X8} references unknown section '{function.SourceSection}'.");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(section.ImageFile))
|
||||
{
|
||||
throw new InvalidDataException($"Base section '{section.Name}' has no executable image file.");
|
||||
}
|
||||
|
||||
var sourceImage = LoadImage(baseManifestDirectory, section.ImageFile, imageCache);
|
||||
var sourceOffset = checked((int)function.SourceImageOffset);
|
||||
var functionSize = checked((int)(function.End - function.Start));
|
||||
if (sourceOffset < 0 || sourceOffset + functionSize > sourceImage.Length)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Base function 0x{function.Start:X8}-0x{function.End:X8} exceeds source image '{section.ImageFile}'.");
|
||||
}
|
||||
|
||||
var bytes = new byte[functionSize];
|
||||
Array.Copy(sourceImage, sourceOffset, bytes, 0, functionSize);
|
||||
|
||||
var patchRecords = new List<PatchApplicationRecord>();
|
||||
executablePatchGroups.TryGetValue(function.Start, out var group);
|
||||
foreach (var classification in (group ?? new List<KamekPatchClassification>()).OrderBy(c => c.CommandAddress))
|
||||
{
|
||||
var command = FindMatchingCommand(chunk, classification);
|
||||
if (TryApplyPatch(
|
||||
bytes,
|
||||
function.Start,
|
||||
classification.CommandAddress,
|
||||
command,
|
||||
patchPlan.ModuleGuestBase,
|
||||
out var record))
|
||||
{
|
||||
patchRecords.Add(record);
|
||||
}
|
||||
else
|
||||
{
|
||||
patchRecords.Add(record);
|
||||
diagnostics.Add(record);
|
||||
}
|
||||
}
|
||||
retroWfcExecutableHookGroups.TryGetValue(function.Start, out var executableHookGroup);
|
||||
var executableHooks = executableHookGroup is null
|
||||
? Enumerable.Empty<RetroWfcExecutableHookPlan>()
|
||||
: executableHookGroup;
|
||||
foreach (var hook in executableHooks.OrderBy(h => h.Address).ThenBy(h => h.RecordIndex))
|
||||
{
|
||||
if (TryApplyRetroWfcExecutableHookPatch(
|
||||
bytes,
|
||||
function.Start,
|
||||
hook,
|
||||
out var record))
|
||||
{
|
||||
patchRecords.Add(record);
|
||||
}
|
||||
else
|
||||
{
|
||||
patchRecords.Add(record);
|
||||
diagnostics.Add(record);
|
||||
}
|
||||
}
|
||||
retroWfcStaticBytePatchGroups.TryGetValue(function.Start, out var staticByteGroup);
|
||||
var staticBytePatches = staticByteGroup is null
|
||||
? Enumerable.Empty<RetroWfcStaticBytePatchPlan>()
|
||||
: staticByteGroup;
|
||||
foreach (var patch in staticBytePatches.OrderBy(p => p.Address).ThenBy(p => p.RecordIndex))
|
||||
{
|
||||
if (TryApplyRetroWfcStaticBytePatch(
|
||||
bytes,
|
||||
function.Start,
|
||||
patch,
|
||||
out var record))
|
||||
{
|
||||
patchRecords.Add(record);
|
||||
}
|
||||
else
|
||||
{
|
||||
patchRecords.Add(record);
|
||||
diagnostics.Add(record);
|
||||
}
|
||||
}
|
||||
|
||||
var imageFile = $"overlay_{function.Start:X8}.bin";
|
||||
File.WriteAllBytes(Path.Combine(overlayDir, imageFile), bytes);
|
||||
|
||||
overlays.Add(new OverlayFunctionImage(
|
||||
function.Start,
|
||||
function.End,
|
||||
function.Name,
|
||||
function.SourceSection,
|
||||
imageFile,
|
||||
ChecksumUtilities.Sha256Hex(bytes),
|
||||
patchRecords.Count(p => p.Status == "applied"),
|
||||
patchRecords));
|
||||
}
|
||||
|
||||
var result = new OverlayBuildResult(
|
||||
patchPlan.ModuleGuestBase,
|
||||
moduleImageFile,
|
||||
checked((uint)moduleImage.Length),
|
||||
ChecksumUtilities.Sha256Hex(moduleImage),
|
||||
modulePatchCount,
|
||||
overlays,
|
||||
diagnostics.Where(d => d.Status != "applied").ToList());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private const uint kPpcNop = 0x60000000;
|
||||
|
||||
private static byte[] BuildModuleImage(byte[] baseImage, byte[]? additionalImage, uint? additionalOffset)
|
||||
{
|
||||
if (additionalImage is not { Length: > 0 })
|
||||
{
|
||||
return baseImage.ToArray();
|
||||
}
|
||||
if (!additionalOffset.HasValue)
|
||||
{
|
||||
throw new InvalidDataException("Additional module image bytes require an explicit module offset.");
|
||||
}
|
||||
if (additionalOffset.Value < baseImage.Length)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Additional module image offset 0x{additionalOffset.Value:X} overlaps base module image length 0x{baseImage.Length:X}.");
|
||||
}
|
||||
|
||||
var totalSize = checked((int)additionalOffset.Value + additionalImage.Length);
|
||||
var result = new byte[totalSize];
|
||||
Array.Copy(baseImage, result, baseImage.Length);
|
||||
Array.Copy(additionalImage, 0, result, checked((int)additionalOffset.Value), additionalImage.Length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool RetroWfcHookUsesLink(RetroWfcExecutableHookPlan hook) =>
|
||||
hook.TypeName is "call" or "branchCtrLink" ||
|
||||
hook.Intent.Contains("Call", StringComparison.Ordinal);
|
||||
|
||||
private static KamekCommand FindMatchingCommand(KamekChunk chunk, KamekPatchClassification classification)
|
||||
{
|
||||
return chunk.Commands.FirstOrDefault(command =>
|
||||
command.Id == classification.CommandId &&
|
||||
command.Address == classification.CommandAddress &&
|
||||
command.AddressIsAbsolute == classification.CommandAddressIsAbsolute &&
|
||||
command.Arguments.SequenceEqual(classification.Arguments))
|
||||
?? throw new InvalidDataException(
|
||||
$"Could not match classified command {classification.CommandId} at 0x{classification.CommandAddress:X8} back to Code.pul command stream.");
|
||||
}
|
||||
|
||||
private static byte[] LoadImage(string baseManifestDirectory, string imageFile, Dictionary<string, byte[]> cache)
|
||||
{
|
||||
if (cache.TryGetValue(imageFile, out var bytes))
|
||||
{
|
||||
return bytes;
|
||||
}
|
||||
|
||||
var path = Path.Combine(baseManifestDirectory, imageFile);
|
||||
bytes = File.ReadAllBytes(path);
|
||||
cache[imageFile] = bytes;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static bool TryApplyPatch(
|
||||
byte[] image,
|
||||
uint imageGuestStart,
|
||||
uint patchAddress,
|
||||
KamekCommand command,
|
||||
uint moduleGuestBase,
|
||||
out PatchApplicationRecord record)
|
||||
{
|
||||
try
|
||||
{
|
||||
var applied = ApplyPatch(image, imageGuestStart, patchAddress, command, moduleGuestBase, out var detail);
|
||||
record = new PatchApplicationRecord(patchAddress, command.Id, FormatTarget(command, moduleGuestBase), applied ? "applied" : "skipped", detail);
|
||||
return applied;
|
||||
}
|
||||
catch (Exception ex) when (ex is InvalidDataException or ArgumentOutOfRangeException or OverflowException)
|
||||
{
|
||||
record = new PatchApplicationRecord(patchAddress, command.Id, FormatTarget(command, moduleGuestBase), "failed", ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryApplyRetroWfcExecutableHookPatch(
|
||||
byte[] image,
|
||||
uint imageGuestStart,
|
||||
RetroWfcExecutableHookPlan hook,
|
||||
out PatchApplicationRecord record)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!hook.TargetAddress.HasValue)
|
||||
{
|
||||
throw new InvalidDataException($"Retro WFC executable hook #{hook.RecordIndex} has no resolved semantic target.");
|
||||
}
|
||||
if (hook.InstructionCount <= 0)
|
||||
{
|
||||
throw new InvalidDataException($"Retro WFC executable hook #{hook.RecordIndex} has invalid instruction count {hook.InstructionCount}.");
|
||||
}
|
||||
|
||||
var offset = checked((int)(hook.Address - imageGuestStart));
|
||||
EnsureRange(image, offset, checked(hook.InstructionCount * sizeof(uint)));
|
||||
var link = RetroWfcHookUsesLink(hook);
|
||||
var branch = KamekPpcEncoding.EncodeBranch(hook.Address, hook.TargetAddress.Value, link);
|
||||
WriteUInt32(image, offset, branch);
|
||||
for (var i = 1; i < hook.InstructionCount; i++)
|
||||
{
|
||||
WriteUInt32(image, offset + i * sizeof(uint), kPpcNop);
|
||||
}
|
||||
|
||||
var action = hook.TargetActionId ?? string.Join(",", hook.SemanticActionIds);
|
||||
record = new PatchApplicationRecord(
|
||||
hook.Address,
|
||||
link ? KamekCommandId.BranchLink : KamekCommandId.Branch,
|
||||
$"RetroWFC#{hook.RecordIndex}",
|
||||
"applied",
|
||||
$"retro wfc semantic hook {action} -> 0x{hook.TargetAddress.Value:X8}, continuation=0x{hook.ContinuationAddress:X8}, instructions={hook.InstructionCount:N0}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is InvalidDataException or ArgumentOutOfRangeException or OverflowException)
|
||||
{
|
||||
record = new PatchApplicationRecord(
|
||||
hook.Address,
|
||||
RetroWfcHookUsesLink(hook) ? KamekCommandId.BranchLink : KamekCommandId.Branch,
|
||||
$"RetroWFC#{hook.RecordIndex}",
|
||||
"failed",
|
||||
ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryApplyRetroWfcStaticBytePatch(
|
||||
byte[] image,
|
||||
uint imageGuestStart,
|
||||
RetroWfcStaticBytePatchPlan patch,
|
||||
out PatchApplicationRecord record)
|
||||
{
|
||||
try
|
||||
{
|
||||
var patchBytes = Convert.FromHexString(patch.BytesHex);
|
||||
if (patchBytes.Length != patch.Size)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Retro WFC static byte patch #{patch.RecordIndex} declares {patch.Size:N0} byte(s) but contains {patchBytes.Length:N0} byte(s).");
|
||||
}
|
||||
|
||||
var offset = checked((int)(patch.Address - imageGuestStart));
|
||||
EnsureRange(image, offset, patchBytes.Length);
|
||||
patchBytes.CopyTo(image.AsSpan(offset, patchBytes.Length));
|
||||
|
||||
record = new PatchApplicationRecord(
|
||||
patch.Address,
|
||||
KamekCommandId.Write8,
|
||||
$"RetroWFC#{patch.RecordIndex}",
|
||||
"applied",
|
||||
$"retro wfc static byte patch {patchBytes.Length:N0} byte(s) sha256={patch.SourceSha256}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is FormatException or InvalidDataException or ArgumentOutOfRangeException or OverflowException)
|
||||
{
|
||||
record = new PatchApplicationRecord(
|
||||
patch.Address,
|
||||
KamekCommandId.Write8,
|
||||
$"RetroWFC#{patch.RecordIndex}",
|
||||
"failed",
|
||||
ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ApplyPatch(
|
||||
byte[] image,
|
||||
uint imageGuestStart,
|
||||
uint patchAddress,
|
||||
KamekCommand command,
|
||||
uint moduleGuestBase,
|
||||
out string detail)
|
||||
{
|
||||
var offset = checked((int)(patchAddress - imageGuestStart));
|
||||
switch (command.Id)
|
||||
{
|
||||
case KamekCommandId.Addr32:
|
||||
{
|
||||
var target = KamekAddress.Resolve(command.Arguments[0], moduleGuestBase);
|
||||
WriteUInt32(image, offset, target);
|
||||
detail = $"write pointer 0x{target:X8}";
|
||||
return true;
|
||||
}
|
||||
case KamekCommandId.Addr16Lo:
|
||||
{
|
||||
var target = KamekAddress.Resolve(command.Arguments[0], moduleGuestBase);
|
||||
WriteUInt16(image, offset, (ushort)(target & 0xFFFFu));
|
||||
detail = $"write lo16(0x{target:X8})";
|
||||
return true;
|
||||
}
|
||||
case KamekCommandId.Addr16Hi:
|
||||
{
|
||||
var target = KamekAddress.Resolve(command.Arguments[0], moduleGuestBase);
|
||||
WriteUInt16(image, offset, (ushort)(target >> 16));
|
||||
detail = $"write hi16(0x{target:X8})";
|
||||
return true;
|
||||
}
|
||||
case KamekCommandId.Addr16Ha:
|
||||
{
|
||||
var target = KamekAddress.Resolve(command.Arguments[0], moduleGuestBase);
|
||||
WriteUInt16(image, offset, (ushort)((target + 0x8000u) >> 16));
|
||||
detail = $"write ha16(0x{target:X8})";
|
||||
return true;
|
||||
}
|
||||
case KamekCommandId.Rel24:
|
||||
{
|
||||
var target = KamekAddress.Resolve(command.Arguments[0], moduleGuestBase);
|
||||
var original = ReadUInt32(image, offset);
|
||||
var delta = unchecked((int)target - (int)patchAddress);
|
||||
if ((delta & 0x3) != 0 || delta < -0x02000000 || delta > 0x01FFFFFC)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(command), $"rel24 target 0x{target:X8} is out of range from 0x{patchAddress:X8}.");
|
||||
}
|
||||
|
||||
var patched = (original & 0xFC000003u) | (unchecked((uint)delta) & 0x03FFFFFCu);
|
||||
WriteUInt32(image, offset, patched);
|
||||
detail = $"rel24 0x{target:X8} encoded 0x{patched:X8}";
|
||||
return true;
|
||||
}
|
||||
case KamekCommandId.Write32:
|
||||
WriteUInt32(image, offset, command.Arguments[0]);
|
||||
detail = $"write32 0x{command.Arguments[0]:X8}";
|
||||
return true;
|
||||
case KamekCommandId.Write16:
|
||||
WriteUInt16(image, offset, checked((ushort)command.Arguments[0]));
|
||||
detail = $"write16 0x{command.Arguments[0] & 0xFFFFu:X4}";
|
||||
return true;
|
||||
case KamekCommandId.Write8:
|
||||
WriteByte(image, offset, checked((byte)command.Arguments[0]));
|
||||
detail = $"write8 0x{command.Arguments[0] & 0xFFu:X2}";
|
||||
return true;
|
||||
case KamekCommandId.CondWritePointer:
|
||||
return ApplyConditionalWrite32(image, offset, command.Arguments[1], KamekAddress.Resolve(command.Arguments[0], moduleGuestBase), out detail);
|
||||
case KamekCommandId.CondWrite32:
|
||||
return ApplyConditionalWrite32(image, offset, command.Arguments[1], command.Arguments[0], out detail);
|
||||
case KamekCommandId.CondWrite16:
|
||||
return ApplyConditionalWrite16(image, offset, checked((ushort)command.Arguments[1]), checked((ushort)command.Arguments[0]), out detail);
|
||||
case KamekCommandId.CondWrite8:
|
||||
return ApplyConditionalWrite8(image, offset, checked((byte)command.Arguments[1]), checked((byte)command.Arguments[0]), out detail);
|
||||
case KamekCommandId.Branch:
|
||||
case KamekCommandId.BranchLink:
|
||||
{
|
||||
var target = KamekAddress.Resolve(command.Arguments[0], moduleGuestBase);
|
||||
var patched = KamekPpcEncoding.EncodeBranch(patchAddress, target, command.Id == KamekCommandId.BranchLink);
|
||||
WriteUInt32(image, offset, patched);
|
||||
detail = $"{command.Id} 0x{target:X8} encoded 0x{patched:X8}";
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
throw new InvalidDataException($"Unsupported Code.pul command id {command.Id}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ApplyConditionalWrite32(byte[] image, int offset, uint expected, uint value, out string detail)
|
||||
{
|
||||
var actual = ReadUInt32(image, offset);
|
||||
if (actual != expected)
|
||||
{
|
||||
detail = $"conditional write32 skipped; expected 0x{expected:X8}, actual 0x{actual:X8}";
|
||||
return false;
|
||||
}
|
||||
|
||||
WriteUInt32(image, offset, value);
|
||||
detail = $"conditional write32 applied 0x{value:X8}";
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ApplyConditionalWrite16(byte[] image, int offset, ushort expected, ushort value, out string detail)
|
||||
{
|
||||
var actual = ReadUInt16(image, offset);
|
||||
if (actual != expected)
|
||||
{
|
||||
detail = $"conditional write16 skipped; expected 0x{expected:X4}, actual 0x{actual:X4}";
|
||||
return false;
|
||||
}
|
||||
|
||||
WriteUInt16(image, offset, value);
|
||||
detail = $"conditional write16 applied 0x{value:X4}";
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ApplyConditionalWrite8(byte[] image, int offset, byte expected, byte value, out string detail)
|
||||
{
|
||||
var actual = ReadByte(image, offset);
|
||||
if (actual != expected)
|
||||
{
|
||||
detail = $"conditional write8 skipped; expected 0x{expected:X2}, actual 0x{actual:X2}";
|
||||
return false;
|
||||
}
|
||||
|
||||
WriteByte(image, offset, value);
|
||||
detail = $"conditional write8 applied 0x{value:X2}";
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string FormatTarget(KamekCommand command, uint moduleGuestBase)
|
||||
{
|
||||
if (command.Arguments.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return command.Id switch
|
||||
{
|
||||
KamekCommandId.Addr32 or
|
||||
KamekCommandId.Addr16Lo or
|
||||
KamekCommandId.Addr16Hi or
|
||||
KamekCommandId.Addr16Ha or
|
||||
KamekCommandId.Rel24 or
|
||||
KamekCommandId.Branch or
|
||||
KamekCommandId.BranchLink or
|
||||
KamekCommandId.CondWritePointer => $"0x{KamekAddress.Resolve(command.Arguments[0], moduleGuestBase):X8}",
|
||||
_ => string.Join(", ", command.Arguments.Select(arg => $"0x{arg:X8}"))
|
||||
};
|
||||
}
|
||||
|
||||
private static uint ReadUInt32(byte[] image, int offset)
|
||||
{
|
||||
EnsureRange(image, offset, sizeof(uint));
|
||||
return BinaryPrimitives.ReadUInt32BigEndian(image.AsSpan(offset, sizeof(uint)));
|
||||
}
|
||||
|
||||
private static ushort ReadUInt16(byte[] image, int offset)
|
||||
{
|
||||
EnsureRange(image, offset, sizeof(ushort));
|
||||
return BinaryPrimitives.ReadUInt16BigEndian(image.AsSpan(offset, sizeof(ushort)));
|
||||
}
|
||||
|
||||
private static byte ReadByte(byte[] image, int offset)
|
||||
{
|
||||
EnsureRange(image, offset, sizeof(byte));
|
||||
return image[offset];
|
||||
}
|
||||
|
||||
private static void WriteUInt32(byte[] image, int offset, uint value)
|
||||
{
|
||||
EnsureRange(image, offset, sizeof(uint));
|
||||
BinaryPrimitives.WriteUInt32BigEndian(image.AsSpan(offset, sizeof(uint)), value);
|
||||
}
|
||||
|
||||
private static void WriteUInt16(byte[] image, int offset, ushort value)
|
||||
{
|
||||
EnsureRange(image, offset, sizeof(ushort));
|
||||
BinaryPrimitives.WriteUInt16BigEndian(image.AsSpan(offset, sizeof(ushort)), value);
|
||||
}
|
||||
|
||||
private static void WriteByte(byte[] image, int offset, byte value)
|
||||
{
|
||||
EnsureRange(image, offset, sizeof(byte));
|
||||
image[offset] = value;
|
||||
}
|
||||
|
||||
private static void EnsureRange(byte[] image, int offset, int size)
|
||||
{
|
||||
if (offset < 0 || offset + size > image.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(offset), $"Patch offset 0x{offset:X} size {size} exceeds image length 0x{image.Length:X}.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Translator.Core;
|
||||
|
||||
internal sealed record NativeSourceFile(string FullPath, string RelativePath, string Content);
|
||||
|
||||
internal static class NativeSourceParsing
|
||||
{
|
||||
public static IReadOnlyList<NativeSourceFile> ReadDirectory(string directory)
|
||||
{
|
||||
var root = Path.GetFullPath(directory);
|
||||
if (!Directory.Exists(root))
|
||||
return [];
|
||||
|
||||
return Directory.EnumerateFiles(root, "*.*", SearchOption.AllDirectories)
|
||||
.Where(static path => path.EndsWith(".cpp", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.EndsWith(".h", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.EndsWith(".hpp", StringComparison.OrdinalIgnoreCase))
|
||||
.Order(StringComparer.OrdinalIgnoreCase)
|
||||
.Select(path => new NativeSourceFile(
|
||||
path,
|
||||
Path.GetRelativePath(root, path).Replace('\\', '/'),
|
||||
StripCommentsAndLiterals(File.ReadAllText(path))))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static IEnumerable<string> SplitArguments(string arguments)
|
||||
{
|
||||
var start = 0;
|
||||
var depth = 0;
|
||||
for (var index = 0; index < arguments.Length; index++)
|
||||
{
|
||||
var character = arguments[index];
|
||||
if (character is '(' or '[' or '<') depth++;
|
||||
else if (character is ')' or ']' or '>') depth = Math.Max(0, depth - 1);
|
||||
else if (character == ',' && depth == 0)
|
||||
{
|
||||
yield return arguments[start..index];
|
||||
start = index + 1;
|
||||
}
|
||||
}
|
||||
yield return arguments[start..];
|
||||
}
|
||||
|
||||
public static bool IsFloatingPointValueArgument(string argument) =>
|
||||
!argument.Contains('*', StringComparison.Ordinal) &&
|
||||
!argument.Contains('&', StringComparison.Ordinal) &&
|
||||
Regex.IsMatch(argument, @"\b(float|double)\b", RegexOptions.CultureInvariant);
|
||||
|
||||
public static string StripCommentsAndLiterals(string content)
|
||||
{
|
||||
var output = new StringBuilder(content.Length);
|
||||
var inLineComment = false;
|
||||
var inBlockComment = false;
|
||||
var inLiteral = false;
|
||||
var quote = '\0';
|
||||
for (var index = 0; index < content.Length; index++)
|
||||
{
|
||||
var character = content[index];
|
||||
var next = index + 1 < content.Length ? content[index + 1] : '\0';
|
||||
if (inLineComment)
|
||||
{
|
||||
if (character == '\n') { inLineComment = false; output.Append(character); }
|
||||
continue;
|
||||
}
|
||||
if (inBlockComment)
|
||||
{
|
||||
if (character == '*' && next == '/') { inBlockComment = false; index++; }
|
||||
continue;
|
||||
}
|
||||
if (inLiteral)
|
||||
{
|
||||
output.Append(' ');
|
||||
if (character == '\\' && index + 1 < content.Length) { output.Append(' '); index++; }
|
||||
else if (character == quote) inLiteral = false;
|
||||
continue;
|
||||
}
|
||||
if (character == '/' && next == '/') { inLineComment = true; index++; continue; }
|
||||
if (character == '/' && next == '*') { inBlockComment = true; index++; continue; }
|
||||
if (character is '\'' or '"') { inLiteral = true; quote = character; output.Append(' '); continue; }
|
||||
output.Append(character);
|
||||
}
|
||||
return output.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Translator.Core.IO;
|
||||
using Translator.Core.Loading;
|
||||
|
||||
namespace Translator.Core.Parsing.Dol;
|
||||
|
||||
public sealed class DolFile
|
||||
{
|
||||
private const int TextCount = 7;
|
||||
private const int DataCount = 11;
|
||||
private static readonly string[] TextNames = {".init", ".text", "text2", "text3", "text4", "text5", "text6"};
|
||||
private static readonly string[] DataNames = {"extab", "extabindex", ".ctors", ".dtors", ".rodata", ".data", ".sdata", ".sdata2", "data8", "data9", "data10"};
|
||||
|
||||
private DolFile(IReadOnlyList<DolSection> sections, uint entryPoint, uint bssAddress, uint bssSize)
|
||||
{
|
||||
Sections = sections;
|
||||
EntryPoint = entryPoint;
|
||||
BssAddress = bssAddress;
|
||||
BssSize = bssSize;
|
||||
MemoryRange = CalculateRange(sections, bssAddress, bssSize);
|
||||
}
|
||||
|
||||
public IReadOnlyList<DolSection> Sections { get; }
|
||||
public uint EntryPoint { get; }
|
||||
public uint BssAddress { get; }
|
||||
public uint BssSize { get; }
|
||||
public AddressRange MemoryRange { get; }
|
||||
|
||||
public IEnumerable<DolSection> ExecutableSections => Sections.Where(s => s.IsExecutable);
|
||||
|
||||
public static DolFile Load(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
return Load(stream);
|
||||
}
|
||||
|
||||
public static DolFile Load(Stream stream)
|
||||
{
|
||||
if (!stream.CanSeek)
|
||||
{
|
||||
throw new ArgumentException("DOL parsing requires a seekable stream", nameof(stream));
|
||||
}
|
||||
|
||||
using var reader = new BigEndianBinaryReader(stream, leaveOpen: true);
|
||||
|
||||
var textOffsets = ReadArray(reader, offset: 0x00, count: TextCount);
|
||||
var dataOffsets = ReadArray(reader, offset: 0x1C, count: DataCount);
|
||||
var textAddresses = ReadArray(reader, offset: 0x48, count: TextCount);
|
||||
var dataAddresses = ReadArray(reader, offset: 0x64, count: DataCount);
|
||||
var textSizes = ReadArray(reader, offset: 0x90, count: TextCount);
|
||||
var dataSizes = ReadArray(reader, offset: 0xAC, count: DataCount);
|
||||
|
||||
reader.Seek(0xD8, SeekOrigin.Begin);
|
||||
var bssAddress = reader.ReadUInt32();
|
||||
var bssSize = reader.ReadUInt32();
|
||||
var entryPoint = reader.ReadUInt32();
|
||||
|
||||
var sections = new List<DolSection>();
|
||||
|
||||
for (var i = 0; i < TextCount; i++)
|
||||
{
|
||||
if (textSizes[i] == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var data = ReadSectionData(stream, textOffsets[i], textSizes[i]);
|
||||
sections.Add(new DolSection(
|
||||
index: i,
|
||||
name: TextNames.ElementAtOrDefault(i) ?? $"text{i}",
|
||||
kind: SectionKind.Text,
|
||||
fileOffset: textOffsets[i],
|
||||
virtualAddress: textAddresses[i],
|
||||
size: textSizes[i],
|
||||
data: data));
|
||||
}
|
||||
|
||||
for (var i = 0; i < DataCount; i++)
|
||||
{
|
||||
if (dataSizes[i] == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var data = ReadSectionData(stream, dataOffsets[i], dataSizes[i]);
|
||||
sections.Add(new DolSection(
|
||||
index: i,
|
||||
name: DataNames.ElementAtOrDefault(i) ?? $"data{i}",
|
||||
kind: SectionKind.Data,
|
||||
fileOffset: dataOffsets[i],
|
||||
virtualAddress: dataAddresses[i],
|
||||
size: dataSizes[i],
|
||||
data: data));
|
||||
}
|
||||
|
||||
if (bssSize > 0)
|
||||
{
|
||||
sections.Add(new DolSection(
|
||||
index: sections.Count,
|
||||
name: ".bss",
|
||||
kind: SectionKind.Bss,
|
||||
fileOffset: 0,
|
||||
virtualAddress: bssAddress,
|
||||
size: bssSize,
|
||||
data: ReadOnlyMemory<byte>.Empty));
|
||||
}
|
||||
|
||||
return new DolFile(new ReadOnlyCollection<DolSection>(sections), entryPoint, bssAddress, bssSize);
|
||||
}
|
||||
|
||||
private static AddressRange CalculateRange(IEnumerable<DolSection> sections, uint bssAddress, uint bssSize)
|
||||
{
|
||||
var hasAny = false;
|
||||
uint start = uint.MaxValue;
|
||||
uint end = 0;
|
||||
|
||||
foreach (var section in sections)
|
||||
{
|
||||
if (section.Size == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
hasAny = true;
|
||||
start = Math.Min(start, section.VirtualAddress);
|
||||
end = Math.Max(end, section.Range.End);
|
||||
}
|
||||
|
||||
if (bssSize > 0)
|
||||
{
|
||||
hasAny = true;
|
||||
start = Math.Min(start, bssAddress);
|
||||
end = Math.Max(end, checked(bssAddress + bssSize));
|
||||
}
|
||||
|
||||
if (!hasAny)
|
||||
{
|
||||
return new AddressRange(0, 0);
|
||||
}
|
||||
|
||||
return new AddressRange(start, end);
|
||||
}
|
||||
|
||||
private static uint[] ReadArray(BigEndianBinaryReader reader, int offset, int count)
|
||||
{
|
||||
reader.Seek(offset, SeekOrigin.Begin);
|
||||
var values = new uint[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
values[i] = reader.ReadUInt32();
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private static ReadOnlyMemory<byte> ReadSectionData(Stream stream, uint offset, uint size)
|
||||
{
|
||||
if (size == 0)
|
||||
{
|
||||
return ReadOnlyMemory<byte>.Empty;
|
||||
}
|
||||
|
||||
var length = checked((int)size);
|
||||
var data = new byte[length];
|
||||
stream.Seek(offset, SeekOrigin.Begin);
|
||||
try
|
||||
{
|
||||
stream.ReadExactly(data);
|
||||
}
|
||||
catch (EndOfStreamException exception)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Section at file offset 0x{offset:X} is truncated; expected {data.Length} bytes.",
|
||||
exception);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using Translator.Core.Loading;
|
||||
|
||||
namespace Translator.Core.Parsing.Dol;
|
||||
|
||||
public enum SectionKind
|
||||
{
|
||||
Text,
|
||||
Data,
|
||||
Bss
|
||||
}
|
||||
|
||||
public sealed class DolSection
|
||||
{
|
||||
public DolSection(int index, string name, SectionKind kind, uint fileOffset, uint virtualAddress, uint size, ReadOnlyMemory<byte> data)
|
||||
{
|
||||
Index = index;
|
||||
Name = name;
|
||||
Kind = kind;
|
||||
FileOffset = fileOffset;
|
||||
VirtualAddress = virtualAddress;
|
||||
Size = size;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public int Index { get; }
|
||||
public string Name { get; }
|
||||
public SectionKind Kind { get; }
|
||||
public uint FileOffset { get; }
|
||||
public uint VirtualAddress { get; }
|
||||
public uint Size { get; }
|
||||
public AddressRange Range => AddressRange.FromStartAndSize(VirtualAddress, Size);
|
||||
public ReadOnlyMemory<byte> Data { get; }
|
||||
public bool HasData => Data.Length > 0;
|
||||
public bool IsExecutable => Kind == SectionKind.Text;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Translator.Core.Parsing.Kamek;
|
||||
|
||||
/// <summary>
|
||||
/// Kamek command operands are either an absolute guest address or an offset into
|
||||
/// the loaded module. The 0x80000000 split is the whole rule, and every planner
|
||||
/// that walks a command stream needs it.
|
||||
/// </summary>
|
||||
public static class KamekAddress
|
||||
{
|
||||
public static uint Resolve(uint raw, uint moduleGuestBase) =>
|
||||
raw >= 0x80000000u ? raw : checked(moduleGuestBase + raw);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
using System.Buffers.Binary;
|
||||
using Translator.Core.IO;
|
||||
|
||||
namespace Translator.Core.Parsing.Kamek;
|
||||
|
||||
public sealed class KamekChunk
|
||||
{
|
||||
public const uint Magic0 = 0x4B616D65;
|
||||
public const uint Magic1 = 0x6B000003;
|
||||
public const int HeaderSize = 0x20;
|
||||
|
||||
public KamekChunk(
|
||||
int index,
|
||||
long fileOffset,
|
||||
uint bssSize,
|
||||
uint codeSize,
|
||||
uint ctorStart,
|
||||
uint ctorEnd,
|
||||
uint chunkSize,
|
||||
byte[] codeBlob,
|
||||
IReadOnlyList<KamekCommand> commands)
|
||||
{
|
||||
Index = index;
|
||||
FileOffset = fileOffset;
|
||||
BssSize = bssSize;
|
||||
CodeSize = codeSize;
|
||||
CtorStart = ctorStart;
|
||||
CtorEnd = ctorEnd;
|
||||
ChunkSize = chunkSize;
|
||||
CodeBlob = codeBlob;
|
||||
Commands = commands;
|
||||
}
|
||||
|
||||
public int Index { get; }
|
||||
public long FileOffset { get; }
|
||||
public uint BssSize { get; }
|
||||
public uint CodeSize { get; }
|
||||
public uint CtorStart { get; }
|
||||
public uint CtorEnd { get; }
|
||||
public uint ChunkSize { get; }
|
||||
public byte[] CodeBlob { get; }
|
||||
public IReadOnlyList<KamekCommand> Commands { get; }
|
||||
|
||||
public int RelativeCommandCount => Commands.Count(c => c.AddressIsRelative);
|
||||
public int AbsoluteCommandCount => Commands.Count(c => c.AddressIsAbsolute);
|
||||
|
||||
public IReadOnlyDictionary<KamekCommandId, int> CommandCounts =>
|
||||
Commands
|
||||
.GroupBy(c => c.Id)
|
||||
.OrderBy(g => (byte)g.Key)
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
public static bool HasMagic(ReadOnlySpan<byte> data, int offset)
|
||||
{
|
||||
if (offset < 0 || offset + 8 > data.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset, 4)) == Magic0 &&
|
||||
BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset + 4, 4)) == Magic1;
|
||||
}
|
||||
|
||||
public static KamekChunk Parse(byte[] data, int offset, int expectedSize, int index)
|
||||
{
|
||||
if (offset < 0 || offset + HeaderSize > data.Length)
|
||||
{
|
||||
throw new InvalidDataException($"Kamek chunk {index} header at 0x{offset:X} is outside file.");
|
||||
}
|
||||
|
||||
using var stream = new MemoryStream(data, writable: false);
|
||||
using var reader = new BigEndianBinaryReader(stream);
|
||||
reader.Seek(offset, SeekOrigin.Begin);
|
||||
|
||||
var magic0 = reader.ReadUInt32();
|
||||
var magic1 = reader.ReadUInt32();
|
||||
if (magic0 != Magic0 || magic1 != Magic1)
|
||||
{
|
||||
throw new InvalidDataException($"Kamek chunk {index} at 0x{offset:X} has invalid magic 0x{magic0:X8}/0x{magic1:X8}.");
|
||||
}
|
||||
|
||||
var bssSize = reader.ReadUInt32();
|
||||
var codeSize = reader.ReadUInt32();
|
||||
var ctorStart = reader.ReadUInt32();
|
||||
var ctorEnd = reader.ReadUInt32();
|
||||
var chunkSize = reader.ReadUInt32();
|
||||
_ = reader.ReadUInt32(); // reserved
|
||||
|
||||
if (chunkSize < HeaderSize + codeSize)
|
||||
{
|
||||
throw new InvalidDataException($"Kamek chunk {index} chunkSize 0x{chunkSize:X} is smaller than header+code.");
|
||||
}
|
||||
|
||||
if (expectedSize != 0 && chunkSize != expectedSize)
|
||||
{
|
||||
throw new InvalidDataException($"Kamek chunk {index} chunkSize 0x{chunkSize:X} does not match combined size 0x{expectedSize:X}.");
|
||||
}
|
||||
|
||||
if (offset + chunkSize > data.Length)
|
||||
{
|
||||
throw new InvalidDataException($"Kamek chunk {index} extends past file end.");
|
||||
}
|
||||
|
||||
if (ctorStart > codeSize || ctorEnd > codeSize || ctorStart > ctorEnd || ((ctorEnd - ctorStart) % 4) != 0)
|
||||
{
|
||||
throw new InvalidDataException($"Kamek chunk {index} has invalid ctor range 0x{ctorStart:X}-0x{ctorEnd:X} for codeSize 0x{codeSize:X}.");
|
||||
}
|
||||
|
||||
var codeBlob = reader.ReadBytes(checked((int)codeSize));
|
||||
var commandEnd = offset + checked((int)chunkSize);
|
||||
var commands = new List<KamekCommand>();
|
||||
|
||||
while (reader.Position < commandEnd)
|
||||
{
|
||||
var commandOffset = reader.Position;
|
||||
var commandWord = reader.ReadUInt32();
|
||||
var rawId = (byte)(commandWord >> 24);
|
||||
if (!Enum.IsDefined(typeof(KamekCommandId), rawId))
|
||||
{
|
||||
throw new InvalidDataException($"Unknown Kamek command id {rawId} at file offset 0x{commandOffset:X}.");
|
||||
}
|
||||
|
||||
var id = (KamekCommandId)rawId;
|
||||
var addressEnc = commandWord & 0x00FFFFFFu;
|
||||
bool addressIsAbsolute;
|
||||
uint address;
|
||||
if (addressEnc == 0x00FFFFFEu)
|
||||
{
|
||||
addressIsAbsolute = true;
|
||||
address = reader.ReadUInt32();
|
||||
}
|
||||
else
|
||||
{
|
||||
addressIsAbsolute = false;
|
||||
address = addressEnc;
|
||||
}
|
||||
|
||||
var argCount = ArgumentCount(id);
|
||||
var args = new uint[argCount];
|
||||
for (var i = 0; i < argCount; i++)
|
||||
{
|
||||
args[i] = reader.ReadUInt32();
|
||||
}
|
||||
|
||||
commands.Add(new KamekCommand(commandOffset, commandWord, id, addressIsAbsolute, address, args));
|
||||
}
|
||||
|
||||
if (reader.Position != commandEnd)
|
||||
{
|
||||
throw new InvalidDataException($"Kamek chunk {index} command stream ended at 0x{reader.Position:X}, expected 0x{commandEnd:X}.");
|
||||
}
|
||||
|
||||
return new KamekChunk(index, offset, bssSize, codeSize, ctorStart, ctorEnd, chunkSize, codeBlob, commands);
|
||||
}
|
||||
|
||||
public static int ArgumentCount(KamekCommandId id) => id switch
|
||||
{
|
||||
KamekCommandId.Addr32 => 1,
|
||||
KamekCommandId.Addr16Lo => 1,
|
||||
KamekCommandId.Addr16Hi => 1,
|
||||
KamekCommandId.Addr16Ha => 1,
|
||||
KamekCommandId.Rel24 => 1,
|
||||
KamekCommandId.Write32 => 1,
|
||||
KamekCommandId.Write16 => 1,
|
||||
KamekCommandId.Write8 => 1,
|
||||
KamekCommandId.CondWritePointer => 2,
|
||||
KamekCommandId.CondWrite32 => 2,
|
||||
KamekCommandId.CondWrite16 => 2,
|
||||
KamekCommandId.CondWrite8 => 2,
|
||||
KamekCommandId.Branch => 1,
|
||||
KamekCommandId.BranchLink => 1,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(id), id, "Unsupported Kamek command id")
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Translator.Core.Parsing.Kamek;
|
||||
|
||||
public sealed record KamekCommand(
|
||||
long StreamOffset,
|
||||
uint CommandWord,
|
||||
KamekCommandId Id,
|
||||
bool AddressIsAbsolute,
|
||||
uint Address,
|
||||
IReadOnlyList<uint> Arguments)
|
||||
{
|
||||
public bool AddressIsRelative => !AddressIsAbsolute;
|
||||
}
|
||||
Loaded 100 of 1099 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user