This commit is contained in:
patchzyy committed 2026-08-23 17:10:50 +02:00
commit ec226e8348
1099 files changed
+400981

No files matched your search

+145
View File
@@ -0,0 +1,145 @@
# Wiicompiled Static Recompiler
The translator parses GameCube/Wii DOL files, decodes PowerPC instructions, lifts them through
IR/SSA and type inference, and emits C++ that is compiled into a native executable together with
a runtime in `runtime/`. Project-specific paths and addresses are supplied through a versioned
YAML manifest, so the translator itself contains no game-specific data.
> Translating a DOL does not exempt you from owning the game it came from.
## How translating a DOL works
A project manifest (YAML) names the input DOL and pins its layout; everything else is derived.
Translation is four commands:
1. **`translate-recursive <entry-point> --project <manifest>`** - walks the call graph from the
entry point, decodes every reachable function, and emits C++ (plus JSON metadata describing
what was emitted).
2. **`generate-data-init --project <manifest>`** - writes the embedded `.data`/`.rodata`/`.sdata`
section initializer and `RuntimeConfig.h`.
3. **`emit-build-shards --project <manifest>`** - emits the CMake build graph (`shards.cmake`)
covering both generated sources and `runtime/src`.
4. **CMake + Ninja with Clang** compiles `runtime/` plus the generated output into one executable.
Discovery is purely recursive from the entry point unless the manifest provides an optional
`function_map` (one `hexaddr name` per line) that seeds additional function boundaries. Unsupported
instructions fail translation by default.
See `projects/examples/generic-dol.yml` for a minimal manifest driven by `RECOMP_GENERIC_DOL`.
## Prerequisites
| Tool | Notes |
| --- | --- |
| .NET 8 SDK | Builds and runs the translator. |
| CMake ≥ 3.16 and Ninja | Configures and drives the native build. |
| Clang / LLVM | The shipped build uses LLVM-MinGW targeting `x86-64-v3`. MSVC is not the tested path. |
Build the CLI once and invoke the assembly directly:
```powershell
dotnet build translator/src/Translator.Cli/Translator.Cli.csproj -c Release
$translator = 'translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll'
```
## Manifest essentials
- `inputs.dol.path` - the DOL to translate; optional SHA-256 pinning rejects wrong revisions.
- `memory.base` / `size` - guest address space.
- `memory.sda_base` / `sda2_base` - the r13/r2 Small Data Area bases your DOL's boot code installs
(`lis`/`ori` pairs in `__init_registers`). Required by any command that writes `RuntimeConfig.h`;
the translator does not guess them.
- `translation.function_map.path` - optional symbol map used as the discovery oracle.
- `translation.allow_unsupported_instructions` - off by default; enabling it emits runtime traps
instead of failing, and such a build can never ship.
Relative paths resolve from `workspace_root`, which itself resolves from the manifest directory.
## Commands
- `info [--project path]`
- `translate-recursive <address> --project path`
- `generate-data-init --project path`
- `emit-base-manifest --project path`
- `emit-build-shards --project path`
- `translate-mod --project path [--profile name] ...` - static Kamek/Pulsar module translation
Any command prints its own option list with `--help`.
## Test
```powershell
dotnet test translator/Translator.sln -c Release
```
+43
View File
@@ -0,0 +1,43 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4FC58505-F37D-4144-BB2A-7799E35E74F6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Translator.Core", "src\Translator.Core\Translator.Core.csproj", "{3AA6B3E5-AA08-46CB-AED1-FE538953BF15}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Translator.Cli", "src\Translator.Cli\Translator.Cli.csproj", "{139F781E-A98F-4DB4-8E51-4BF044D130E9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{670CFC5B-3E95-4565-83E8-2FEF6A562D62}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Translator.Tests", "tests\Translator.Tests\Translator.Tests.csproj", "{A5FE1FF8-59B1-48B8-BAF3-FC05E21A675E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3AA6B3E5-AA08-46CB-AED1-FE538953BF15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3AA6B3E5-AA08-46CB-AED1-FE538953BF15}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3AA6B3E5-AA08-46CB-AED1-FE538953BF15}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3AA6B3E5-AA08-46CB-AED1-FE538953BF15}.Release|Any CPU.Build.0 = Release|Any CPU
{139F781E-A98F-4DB4-8E51-4BF044D130E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{139F781E-A98F-4DB4-8E51-4BF044D130E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{139F781E-A98F-4DB4-8E51-4BF044D130E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{139F781E-A98F-4DB4-8E51-4BF044D130E9}.Release|Any CPU.Build.0 = Release|Any CPU
{A5FE1FF8-59B1-48B8-BAF3-FC05E21A675E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A5FE1FF8-59B1-48B8-BAF3-FC05E21A675E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A5FE1FF8-59B1-48B8-BAF3-FC05E21A675E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A5FE1FF8-59B1-48B8-BAF3-FC05E21A675E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{3AA6B3E5-AA08-46CB-AED1-FE538953BF15} = {4FC58505-F37D-4144-BB2A-7799E35E74F6}
{139F781E-A98F-4DB4-8E51-4BF044D130E9} = {4FC58505-F37D-4144-BB2A-7799E35E74F6}
{A5FE1FF8-59B1-48B8-BAF3-FC05E21A675E} = {670CFC5B-3E95-4565-83E8-2FEF6A562D62}
EndGlobalSection
EndGlobal
@@ -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);
@@ -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-&gt;cr/ctx-&gt;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-&gt;lr, so the LR store is skipped unless the callee reads LR, the call site
/// dispatches on ctx-&gt;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-&gt;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-&gt;</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-&gt;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-&gt;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>_&lt;version&gt;</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_&lt;name&gt;</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);
}
Loaded 100 of 1099 files, more files were not shown because too many files have changed in this diff. Show more