Compare commits

...

8 Commits

Author SHA1 Message Date
patchzyy c3cc0840eb merge main 2026-10-18 14:09:28 +02:00
patchzyy ed6fea0efb fix 279 (#297)
* fix 279

* treat conditional calls as calls

* keep local linked branches as jumps
2026-10-18 14:03:38 +02:00
patchzyy 95a49f3c89 fix 280 (#298)
* fix 280

* keep scalars and implicit tables in config
2026-10-18 13:47:47 +02:00
patchzyy 60197f2e75 Update AuroraSDL3Patches.cmake (#307)
* Update AuroraSDL3Patches.cmake

* wait for rumble before closing device
2026-10-18 13:47:25 +02:00
patchzyy b87186c96b Calibrate Wii accelerometers from EEPROM (#306) 2026-10-18 13:47:01 +02:00
patchzyy 0b12fc864a mac version fix (#296) 2026-10-18 13:08:15 +02:00
patchzyy 3d4b29d9b9 Fix soft-drift 2026-10-18 11:34:19 +02:00
patchzyy 90589131b5 Fix the date 2026-10-18 00:09:54 +02:00
23 changed files with 838 additions and 308 deletions
@@ -1,4 +1,7 @@
using System.Text;
using Tomlyn;
using Tomlyn.Parsing;
using Tomlyn.Syntax;
namespace WiiCompiled.Setup.Common;
@@ -17,12 +20,6 @@ public static class RuntimeConfiguration
{
public const string ConfigFileName = "Config.toml";
/// <summary>
/// Ordinal, matching the runtime parser/writer (<c>runtime_config.h</c>) exactly. Case-insensitive
/// matching here would let setup rewrite a <c>[Paths]</c>/<c>Dvd_Root</c> spelling the runtime ignores.
/// </summary>
private const StringComparison KeyComparison = StringComparison.Ordinal;
/// <summary>The per-user configuration an ordinary (non-portable) installation shares.</summary>
public static string ApplicationDataConfigPath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
@@ -75,9 +72,11 @@ public static class RuntimeConfiguration
public static void RemoveDvdRootIfOwned(string configPath, string dvdRoot) =>
RemovePathIfOwned(configPath, "dvd_root", dvdRoot);
/// <summary>The raw stored text of a <c>[paths]</c> key, exactly as the file holds it.</summary>
/// <summary>The stored string of a <c>[paths]</c> key, exactly as the file holds it.</summary>
public static string? GetPath(string configPath, string key) =>
TryUnquoteToml(GetRawValue(configPath, "paths", key) ?? "", out var value) ? value : null;
TryLoad(configPath) is { } document && Find(document, ["paths"], key)?.Item.Value is StringValueSyntax value
? value.Value!
: null;
/// <summary>
/// A <c>[paths]</c> value as the runtime will actually use it. The runtime resolves every relative
@@ -99,9 +98,19 @@ public static class RuntimeConfiguration
private static void SetPath(string configPath, string key, string value)
{
var lines = ReadLinesOrDefault(configPath);
SetSectionValue(lines, "paths", key, QuoteToml(FormatPathValue(configPath, value)));
WriteLines(configPath, lines);
var document = Parse(File.Exists(configPath) ? File.ReadAllText(configPath) : DefaultConfig);
var section = new[] { "paths" };
var newValue = new StringValueSyntax(FormatPathValue(configPath, value));
if (Find(document, section, key) is { } existing)
{
LastToken(newValue).TrailingTrivia = LastToken(existing.Item.Value!).TrailingTrivia;
existing.Item.Value = newValue;
}
else
{
Append(document, section, new KeyValueSyntax(key, newValue));
}
Save(configPath, document);
}
/// <summary>
@@ -123,176 +132,104 @@ public static class RuntimeConfiguration
private static void RemovePathIfOwned(string configPath, string key, string expectedValue)
{
if (!File.Exists(configPath)) return;
var lines = File.ReadAllLines(configPath).ToList();
var inPaths = false;
var changed = false;
for (var index = 0; index < lines.Count; index++)
{
var trimmed = RemoveComment(lines[index]).Trim();
if (IsSection(trimmed))
{
inPaths = trimmed.Equals("[paths]", KeyComparison);
continue;
}
if (!inPaths || trimmed.Length == 0) continue;
var equals = trimmed.IndexOf('=');
if (equals < 0 || !trimmed[..equals].Trim().Equals(key, KeyComparison))
continue;
if (!TryUnquoteToml(trimmed[(equals + 1)..].Trim(), out var configured)) return;
// A portable installation stores this relative, so ownership is decided on the path the
// runtime would actually resolve, not on the stored text.
if (!ResolveAgainstConfig(configPath, configured)
.Equals(Path.GetFullPath(expectedValue), StringComparison.OrdinalIgnoreCase))
return;
lines.RemoveAt(index);
changed = true;
break;
}
if (changed) WriteLines(configPath, lines);
if (TryLoad(configPath) is not { } document ||
Find(document, ["paths"], key) is not { } entry ||
entry.Item.Value is not StringValueSyntax configured)
return;
// A portable installation stores this relative, so ownership is decided on the path the
// runtime would actually resolve, not on the stored text.
if (!ResolveAgainstConfig(configPath, configured.Value!)
.Equals(Path.GetFullPath(expectedValue), StringComparison.OrdinalIgnoreCase))
return;
var previous = entry.Previous?.EndOfLineToken ?? entry.Table?.EndOfLineToken;
if (previous is not null && entry.Item.EndOfLineToken?.TrailingTrivia is { } trivia)
(previous.TrailingTrivia ??= []).AddRange(trivia);
entry.Items.RemoveChild(entry.Item);
Save(configPath, document);
}
/// <summary>The raw TOML literal stored for a key, or null when the section or key is absent.</summary>
public static string? GetRawValue(string configPath, string section, string key)
private const string DefaultConfig = "# WiiCompiled user configuration\n\n";
private sealed record Entry(
KeyValueSyntax Item, SyntaxList<KeyValueSyntax> Items, TableSyntax? Table, KeyValueSyntax? Previous);
private static DocumentSyntax Parse(string text) => SyntaxParser.ParseStrict(text);
private static DocumentSyntax? TryLoad(string configPath)
{
if (!File.Exists(configPath)) return null;
var header = $"[{section}]";
var inSection = false;
foreach (var line in File.ReadLines(configPath))
try { return Parse(File.ReadAllText(configPath)); }
catch (TomlException) { return null; }
}
/// <summary>Re-parses the edited tree before replacing the file, so an invalid edit never reaches disk.</summary>
private static void Save(string configPath, DocumentSyntax document)
{
var text = document.ToString();
Parse(text);
FileSystemUtilities.WriteAtomic(configPath, new UTF8Encoding(false).GetBytes(text));
}
private static IEnumerable<TableSyntax> Tables(DocumentSyntax document) =>
document.Tables.OfType<TableSyntax>();
private static string[] Parts(KeySyntax key) =>
[Part(key.Key!), .. key.DotKeys.Select(dotted => Part(dotted.Key!))];
private static string Part(BareKeyOrStringValueSyntax part) =>
part is StringValueSyntax quoted ? quoted.Value! : ((BareKeySyntax)part).Key!.Text!;
/// <summary>Matches by TOML meaning, so <c>"dvd_root"</c> and <c>dvd_root</c> are the same key.</summary>
private static Entry? Find(DocumentSyntax document, string[] section, string key)
{
string[] wanted = [.. section, key];
var scopes = Tables(document)
.Select(table => (Table: (TableSyntax?)table, Prefix: Parts(table.Name!), Items: table.Items))
.Prepend((Table: null, Prefix: [], Items: document.KeyValues));
foreach (var (table, prefix, items) in scopes)
{
var trimmed = RemoveComment(line).Trim();
if (IsSection(trimmed))
KeyValueSyntax? previous = null;
foreach (var item in items)
{
inSection = trimmed.Equals(header, KeyComparison);
continue;
if (prefix.Concat(Parts(item.Key!)).SequenceEqual(wanted))
return new Entry(item, items, table, previous);
previous = item;
}
if (!inSection || trimmed.Length == 0) continue;
var equals = trimmed.IndexOf('=');
if (equals < 0 || !trimmed[..equals].Trim().Equals(key, KeyComparison)) continue;
return trimmed[(equals + 1)..].Trim();
}
return null;
}
/// <summary>
/// Replaces one key in one section, preserving comments, ordering, and every unrelated setting -
/// this file belongs to the user and to the in-game settings bar as much as to setup. A
/// commented-out key is deliberately not matched, matching the runtime's own writer.
/// </summary>
private static void SetSectionValue(List<string> lines, string section, string key, string value)
private static void Append(DocumentSyntax document, string[] section, KeyValueSyntax item)
{
var header = $"[{section}]";
var sectionStart = -1;
var sectionEnd = lines.Count;
for (var index = 0; index < lines.Count; index++)
var text = document.ToString();
var newline = text.Contains("\r\n") ? "\r\n" : "\n";
var table = Tables(document).FirstOrDefault(t => Parts(t.Name!).SequenceEqual(section));
if (table is null)
{
var trimmed = RemoveComment(lines[index]).Trim();
if (!IsSection(trimmed)) continue;
if (sectionStart >= 0)
{
sectionEnd = index;
break;
}
if (trimmed.Equals(header, KeyComparison)) sectionStart = index;
}
if (sectionStart < 0)
{
if (lines.Count > 0 && lines[^1].Length != 0) lines.Add("");
lines.Add(header);
lines.Add($"{key} = {value}");
return;
var name = new KeySyntax(section[0]);
foreach (var part in section.Skip(1)) name.DotKeys.Add(new DottedKeyItemSyntax(part));
table = new TableSyntax(name) { EndOfLineToken = NewLine(newline) };
var blankLine = text.EndsWith("\n\n") || text.EndsWith("\n\r\n");
var gap = text.Length == 0 ? 0 : !text.EndsWith('\n') ? 2 : blankLine ? 0 : 1;
for (var i = 0; i < gap; i++) (table.LeadingTrivia ??= []).Add(new SyntaxTrivia(TokenKind.NewLine, newline));
document.Tables.Add(table);
}
for (var index = sectionStart + 1; index < sectionEnd; index++)
{
var trimmed = RemoveComment(lines[index]).Trim();
if (trimmed.Length == 0) continue;
var equals = trimmed.IndexOf('=');
if (equals >= 0 && trimmed[..equals].Trim().Equals(key, KeyComparison))
{
lines[index] = $"{key} = {value}";
return;
}
}
// Append after the section's last real line, not after the blank line that separates it from
// the next header, so a generated file stays readable for the user who also edits it by hand.
var insertAt = sectionEnd;
while (insertAt > sectionStart + 1 && lines[insertAt - 1].Trim().Length == 0) insertAt--;
lines.Insert(insertAt, $"{key} = {value}");
// Blank lines and comments after the last entry stay after the new one.
var last = table.Items.LastOrDefault();
var before = last is null ? table.EndOfLineToken ??= NewLine(newline) : last.EndOfLineToken ??= NewLine(newline);
item.EndOfLineToken = NewLine(newline);
item.EndOfLineToken.TrailingTrivia = before.TrailingTrivia;
before.TrailingTrivia = null;
table.Items.Add(item);
}
private static List<string> ReadLinesOrDefault(string configPath) =>
File.Exists(configPath) ? File.ReadAllLines(configPath).ToList() : DefaultConfigLines();
private static SyntaxToken NewLine(string text) => new(TokenKind.NewLine, text);
private static void WriteLines(string configPath, List<string> lines) =>
FileSystemUtilities.WriteAtomic(configPath,
Encoding.UTF8.GetBytes(string.Join(Environment.NewLine, lines) + Environment.NewLine));
private static List<string> DefaultConfigLines() =>
[
"# WiiCompiled user configuration",
"",
"[paths]"
];
private static bool IsSection(string value) =>
value.Length >= 2 && value[0] == '[' && value[^1] == ']';
/// <summary>
/// Line-for-line port of <c>RemoveComment</c> in <c>runtime_config.h</c>, including its backslash
/// escaping inside basic strings: without it a Windows path ending in <c>\\"</c> reads as
/// re-opening the quote, and the two implementations disagree about which <c>#</c> starts a comment.
/// </summary>
private static string RemoveComment(string line)
private static SyntaxToken LastToken(SyntaxNode node)
{
var inSingle = false;
var inDouble = false;
var escaped = false;
for (var index = 0; index < line.Length; index++)
{
var character = line[index];
if (inDouble && character == '\\' && !escaped)
{
escaped = true;
continue;
}
if (character == '\'' && !inDouble) inSingle = !inSingle;
else if (character == '"' && !inSingle && !escaped) inDouble = !inDouble;
else if (character == '#' && !inSingle && !inDouble) return line[..index];
escaped = false;
}
return line;
}
private static string QuoteToml(string value) =>
"\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
public static bool TryUnquoteToml(string value, out string result)
{
result = "";
if (value.Length < 2) return false;
// Literal strings carry no escapes; the runtime's parser treats them the same way.
if (value[0] == '\'' && value[^1] == '\'')
{
result = value[1..^1];
return true;
}
if (value[0] != '"' || value[^1] != '"') return false;
var builder = new StringBuilder(value.Length - 2);
for (var index = 1; index < value.Length - 1; index++)
{
if (value[index] == '\\' && index + 1 < value.Length - 1)
{
var escaped = value[++index];
builder.Append(escaped switch { '\\' => '\\', '"' => '"', _ => escaped });
}
else
{
builder.Append(value[index]);
}
}
result = builder.ToString();
return true;
while (node is not SyntaxToken)
node = node.GetChild(node.ChildrenCount - 1)!;
return (SyntaxToken)node;
}
}
@@ -11,4 +11,7 @@
<DebugType>embedded</DebugType>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Tomlyn" Version="2.10.1" />
</ItemGroup>
</Project>
@@ -82,6 +82,7 @@ internal static class BuildRunner
{
startInfo.ArgumentList.Add("--sysroot"); startInfo.ArgumentList.Add(sysroot);
}
startInfo.ArgumentList.Add("--setup-version"); startInfo.ArgumentList.Add(ProductInfo.Version);
using var process = new Process { StartInfo = startInfo };
var window = new BuildProgressWindow(reporter, InstallStages.Build, start: 6, end: 96);
@@ -1,5 +1,6 @@
using System.Buffers.Binary;
using System.Security.Cryptography;
using Tomlyn;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
@@ -51,6 +52,7 @@ internal static class SelfTests
RuntimeConfiguration.SetRetroRewindRoot, RuntimeConfiguration.RemoveRetroRewindRootIfOwned),
failures);
Test("Runtime configuration key matching", TestRuntimeConfigurationKeyMatching, failures);
Test("Runtime configuration quoted keys", TestRuntimeConfigurationQuotedKeys, failures);
Test("Install location validation", TestInstallLocationValidation, failures);
Test("Command line contract", TestCommandLineContract, failures);
Test("Portable root discovery", TestPortableRootDiscovery, failures);
@@ -229,6 +231,70 @@ internal static class SelfTests
}
}
/// <summary>
/// Quoted and bare spellings of a key or table are the same TOML key, so rewriting replaces the
/// entry in place, keeps everything else byte-identical, and never touches an unparseable file.
/// </summary>
private static void TestRuntimeConfigurationQuotedKeys()
{
var root = Path.Combine(Path.GetTempPath(), "mkwc-tomlquoted-" + Guid.NewGuid().ToString("N"));
var config = Path.Combine(root, RuntimeConfiguration.ConfigFileName);
var owned = Path.Combine(root, "Owned Location");
var fullOwned = Path.GetFullPath(owned);
try
{
Directory.CreateDirectory(owned);
foreach (var header in new[] { "[\"paths\"]", "[ 'paths' ]", "[paths]" })
foreach (var key in new[] { "\"dvd_root\"", "'dvd_root'", "\"dvd\\u005Froot\"", "dvd_root" })
{
File.WriteAllText(config,
$"# top\r\n[video]\r\nwidescreen = true # keep\r\n\r\n{header}\r\n {key}=\"D:\\\\Old\" # note\r\nother = 1\r\n");
RuntimeConfiguration.SetDvdRoot(config, owned);
var text = File.ReadAllText(config);
var expected = $"# top\r\n[video]\r\nwidescreen = true # keep\r\n\r\n{header}\r\n {key}=" +
$"\"{fullOwned.Replace("\\", "\\\\")}\" # note\r\nother = 1\r\n";
if (text != expected)
throw new Exception($"{header} {key} was not replaced in place:\r\n{text}");
if (RuntimeConfiguration.GetResolvedPath(config, "dvd_root") != fullOwned)
throw new Exception($"The replaced {key} could not be read back.");
}
File.WriteAllText(config, "[paths]\r\nother = 1\r\n\r\n# next\r\n[video]\r\nx = 2");
RuntimeConfiguration.SetDvdRoot(config, owned);
var appended = File.ReadAllText(config);
if (!appended.StartsWith("[paths]\r\nother = 1\r\ndvd_root = ", StringComparison.Ordinal) ||
!appended.EndsWith("\r\n\r\n# next\r\n[video]\r\nx = 2", StringComparison.Ordinal))
throw new Exception("A missing key was not appended to its section:\r\n" + appended);
File.WriteAllText(config, "dvd_root = \"D:\\\\Root\"\r\n[video]\r\nx = 2\r\n");
RuntimeConfiguration.SetDvdRoot(config, owned);
var created = File.ReadAllText(config);
if (!created.StartsWith("dvd_root = \"D:\\\\Root\"\r\n[video]\r\nx = 2\r\n\r\n[paths]\r\n", StringComparison.Ordinal) ||
RuntimeConfiguration.GetResolvedPath(config, "dvd_root") != fullOwned)
throw new Exception("A missing section was not created without touching a same-named root key:\r\n" + created);
File.Delete(config);
RuntimeConfiguration.SetDvdRoot(config, owned);
if (RuntimeConfiguration.GetResolvedPath(config, "dvd_root") != fullOwned)
throw new Exception("A missing config file was not created.");
const string broken = "[paths]\r\ndvd_root = 1\r\ndvd_root = 2\r\n";
File.WriteAllText(config, broken);
try
{
RuntimeConfiguration.SetDvdRoot(config, owned);
throw new Exception("An unparseable config was rewritten.");
}
catch (TomlException) { }
if (File.ReadAllText(config) != broken)
throw new Exception("An unparseable config was modified.");
}
finally
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}
/// <summary>
/// One location rule set covers both an installation directory and a portable root, so the
/// rejections a portable install performs are the ones an ordinary install performs too.
+9 -1
View File
@@ -27,12 +27,15 @@ Usage: local-build-macos.command --output-dir DIR [options]
--cmake PATH --ninja PATH Override build tools
--dotnet PATH Override dotnet
--translator-bin PATH Use a self-contained Translator.Cli executable
--setup-version VERSION Version recorded in the runtime log (default: <Version> in
Launcher/Directory.Build.props when present)
EOF
}
workspace="$default_workspace"; profile=base; output_dir=""; base_output_dir=""
game=""; nodtool=""; retro_root=""; retro_wfc=""; skip_retro_wfc=0; force_clean=0
parallel=0; cmake_bin=cmake; ninja_bin=ninja; dotnet_bin=dotnet; translator_bin=""
setup_version=""
while (($#)); do
case "$1" in
--workspace) workspace=${2:-}; shift 2 ;;
@@ -50,6 +53,7 @@ while (($#)); do
--ninja) ninja_bin=${2:-}; shift 2 ;;
--dotnet) dotnet_bin=${2:-}; shift 2 ;;
--translator-bin) translator_bin=${2:-}; shift 2 ;;
--setup-version) setup_version=${2:-}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown option: $1" ;;
esac
@@ -60,6 +64,10 @@ macos_arch=$(uname -m)
case "$macos_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $macos_arch" ;; esac
macos_deployment_target=12.0
workspace=$(cd "$workspace" && pwd)
if [[ -z "$setup_version" && -f "$workspace/Launcher/Directory.Build.props" ]]; then
setup_version=$(sed -nE 's/.*<Version>([^<]+)<\/Version>.*/\1/p' "$workspace/Launcher/Directory.Build.props" | head -n 1)
fi
[[ "$setup_version" =~ ^[0-9A-Za-z.+-]*$ ]] || fail '--setup-version contains unsupported characters'
[[ -n "$output_dir" ]] || fail '--output-dir is required'
case "$profile" in base|retro-rewind|both) ;; *) fail '--profile must be base, retro-rewind, or both' ;; esac
builds_retro=0; [[ "$profile" != base ]] && builds_retro=1
@@ -136,7 +144,7 @@ step emit-build-shards 'Preparing native build shards'; translator "${args[@]}"
step configure-native 'Configuring the native toolchain'
# Use Aurora's pinned SDL3 source on macOS. A system SDL3 can be older than
# Aurora's required API even when find_package() succeeds.
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DCMAKE_OSX_ARCHITECTURES="$macos_arch" -DCMAKE_OSX_DEPLOYMENT_TARGET="$macos_deployment_target" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs" -DAURORA_SDL3_PROVIDER=vendor
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DCMAKE_OSX_ARCHITECTURES="$macos_arch" -DCMAKE_OSX_DEPLOYMENT_TARGET="$macos_deployment_target" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs" -DAURORA_SDL3_PROVIDER=vendor -DMKW_SETUP_VERSION="$setup_version"
targets=(); [[ "$profile" != retro-rewind ]] && targets+=(WiiCompiled); [[ "$profile" != base ]] && targets+=(RetroRewind)
step compile "Compiling ${targets[*]} locally"; "$cmake_bin" --build "$native_build" --target "${targets[@]}" --parallel "$global_jobs"
if [[ "$profile" != retro-rewind ]]; then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product WiiCompiled --output-dir "${base_output_dir:-$output_dir}" --architecture "$macos_arch" --minimum-system-version "$macos_deployment_target"; fi
+11 -1
View File
@@ -66,6 +66,7 @@ translator_bin_override=""
fuse_ld_override=""
native_prebuilt_dir=""
sysroot=""
setup_version=""
usage() {
cat <<'EOF'
@@ -90,6 +91,8 @@ Usage: local-build.sh --output-dir DIR [options]
skips compiling aurora-main from source entirely
--sysroot PATH Passed to CMake as -DCMAKE_SYSROOT: where the compiler resolves
standard headers/startup files
--setup-version VERSION Version recorded in the runtime log (default: <Version> in
Launcher/Directory.Build.props when present)
EOF
}
@@ -114,11 +117,17 @@ while [[ $# -gt 0 ]]; do
--translator-bin) translator_bin_override=$2; shift 2 ;;
--native-prebuilt-dir) native_prebuilt_dir=$2; shift 2 ;;
--sysroot) sysroot=$2; shift 2 ;;
--setup-version) setup_version=$2; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown argument: $1" ;;
esac
done
if [[ -z "$setup_version" && -f "$workspace/Launcher/Directory.Build.props" ]]; then
setup_version=$(sed -nE 's/.*<Version>([^<]+)<\/Version>.*/\1/p' "$workspace/Launcher/Directory.Build.props" | head -n 1)
fi
[[ "$setup_version" =~ ^[0-9A-Za-z.+-]*$ ]] || fail "--setup-version contains unsupported characters"
[[ -n "$output_dir" ]] || { usage; fail "--output-dir is required"; }
case "$profile" in
base|retro-rewind|both) ;;
@@ -418,7 +427,8 @@ configure_args=(-S "$workspace/runtime" -B "$build" -G Ninja
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_C_COMPILER="$cc_bin" -DCMAKE_CXX_COMPILER="$cxx_bin"
-DCMAKE_MAKE_PROGRAM="$ninja_bin"
-DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs")
-DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs"
-DMKW_SETUP_VERSION="$setup_version")
# CMAKE_C_COMPILER/CXX_COMPILER stay stable across AppImage runs on their own (they're exactly
# what's passed via -D above, and AppRun points --cc/--cxx at a symlink it re-targets at the
# current mount every launch - see build-appimage.sh). CMAKE_AR/RANLIB/LINKER/ASM_COMPILER do NOT
+1
View File
@@ -151,6 +151,7 @@ for source in aurora-main projects runtime translator; do
copy_clean "$workspace/$source" "$resources/workspace/$source"
done
mkdir -p "$resources/workspace/Launcher/macos"
copy_clean "$workspace/Launcher/Directory.Build.props" "$resources/workspace/Launcher/Directory.Build.props"
copy_clean "$workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/local-build-macos.command"
copy_clean "$workspace/Launcher/macos/extract-disc.command" "$resources/workspace/Launcher/macos/extract-disc.command"
copy_clean "$workspace/Launcher/macos/publish-app.command" "$resources/workspace/Launcher/macos/publish-app.command"
+259
View File
@@ -32,6 +32,52 @@ function(aurora_sdl3_apply_patches sdl_source_dir)
if (NOT EXISTS "${_wii}")
message(FATAL_ERROR "aurora: SDL3 source tree at ${sdl_source_dir} has no SDL_hidapi_wii.c")
endif ()
set(_rumble "${sdl_source_dir}/src/joystick/hidapi/SDL_hidapi_rumble.c")
if (NOT EXISTS "${_rumble}")
message(FATAL_ERROR "aurora: SDL3 source tree at ${sdl_source_dir} has no SDL_hidapi_rumble.c")
endif ()
set(_hidapi "${sdl_source_dir}/src/joystick/hidapi/SDL_hidapijoystick.c")
if (NOT EXISTS "${_hidapi}")
message(FATAL_ERROR "aurora: SDL3 source tree at ${sdl_source_dir} has no SDL_hidapijoystick.c")
endif ()
# Backport of SDL 71f4af732 (drop once SDL includes it): rumble writes must not hold dev_lock.
_aurora_sdl3_replace("${_rumble}" "HIDAPI rumble writes no longer hold the device lock"
[==[ if (request) {
SDL_LockMutex(request->device->dev_lock);
if (request->device->dev) {
#ifdef DEBUG_RUMBLE
HIDAPI_DumpPacket("Rumble packet: size = %d", request->data, request->size);
#endif
SDL_hid_write(request->device->dev, request->data, request->size);
}
SDL_UnlockMutex(request->device->dev_lock);
]==]
[==[ if (request) {
if (request->device->dev) {
#ifdef DEBUG_RUMBLE
HIDAPI_DumpPacket("Rumble packet: size = %d", request->data, request->size);
#endif
SDL_hid_write(request->device->dev, request->data, request->size);
}
]==])
# Without the lock, closing the handle must wait for in-flight rumble writes.
_aurora_sdl3_replace("${_hidapi}" "HIDAPI device close waits for pending rumble"
[==[ device->driver->FreeDevice(device);
device->driver = NULL;
SDL_LockMutex(device->dev_lock);
]==]
[==[ device->driver->FreeDevice(device);
device->driver = NULL;
while (SDL_GetAtomicInt(&device->rumble_pending) > 0) {
SDL_Delay(10);
}
SDL_LockMutex(device->dev_lock);
]==])
# Wii Remote: rebuild the joystick in place after an extension change.
#
@@ -118,6 +164,219 @@ static void UpdateDeviceIdentity(SDL_HIDAPI_Device *device)
now = SDL_GetTicks();
]==])
# Classic Controller sticks: the stock path stretches each half-range by learned min/max;
# this posts WPAD's linear value (raw - first-report centre) * unit, times 64.
_aurora_sdl3_replace("${_wii}" "Wii Classic Controller linear sticks (helper)"
[==[static void HandleGamepadControllerButtonData(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick, const WiiButtonData *data)
{
]==]
[==[/* WiiCompiled: axis = (raw - centre) * unit * 64, y inverted; see AuroraSDL3Patches.cmake */
static void PostClassicStickLinear(Uint64 timestamp, SDL_Joystick *joystick, StickCalibrationData *calibration, Uint8 axis, Uint16 data, int unit)
{
int value;
if (!calibration->center) {
// Center on first read
calibration->center = data;
return;
}
value = ((int)data - (int)calibration->center) * unit * 64;
if (axis == SDL_GAMEPAD_AXIS_LEFTY || axis == SDL_GAMEPAD_AXIS_RIGHTY) {
value = -value;
}
SDL_SendJoystickAxis(timestamp, joystick, axis, (Sint16)SDL_clamp(value, SDL_JOYSTICK_AXIS_MIN, SDL_JOYSTICK_AXIS_MAX));
}
static void HandleGamepadControllerButtonData(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick, const WiiButtonData *data)
{
]==])
_aurora_sdl3_replace("${_wii}" "Wii Classic Controller linear sticks (axes)"
[==[ PostStickCalibrated(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[0], SDL_GAMEPAD_AXIS_LEFTX, lx);
PostStickCalibrated(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[1], SDL_GAMEPAD_AXIS_LEFTY, ly);
PostStickCalibrated(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[2], SDL_GAMEPAD_AXIS_RIGHTX, rx);
PostStickCalibrated(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[3], SDL_GAMEPAD_AXIS_RIGHTY, ry);
]==]
[==[ PostClassicStickLinear(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[0], SDL_GAMEPAD_AXIS_LEFTX, lx, 16);
PostClassicStickLinear(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[1], SDL_GAMEPAD_AXIS_LEFTY, ly, 16);
PostClassicStickLinear(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[2], SDL_GAMEPAD_AXIS_RIGHTX, rx, 32);
PostClassicStickLinear(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[3], SDL_GAMEPAD_AXIS_RIGHTY, ry, 32);
]==])
# Wii Remote: decode the accelerometer with the factory calibration from the EEPROM.
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (context fields)"
[==[ StickCalibrationData m_StickCalibrationData[6];
} SDL_DriverWii_Context;
]==]
[==[ StickCalibrationData m_StickCalibrationData[6];
float m_fAccelZeroG[3];
float m_fAccelPerG[3];
} SDL_DriverWii_Context;
]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (memory space parameter)"
[==[static bool ReadRegister(SDL_DriverWii_Context *ctx, Uint32 address, int size, bool sync)
{
Uint8 readRequest[7];
readRequest[0] = k_eWiiOutputReportIDs_ReadMemory;
readRequest[1] = (Uint8)(0x04 | (Uint8)ctx->m_bRumbleActive);
]==]
[==[static bool ReadMemory(SDL_DriverWii_Context *ctx, Uint8 addressSpace, Uint32 address, int size, bool sync);
static bool ReadRegister(SDL_DriverWii_Context *ctx, Uint32 address, int size, bool sync)
{
return ReadMemory(ctx, 0x04, address, size, sync);
}
static bool ReadMemory(SDL_DriverWii_Context *ctx, Uint8 addressSpace, Uint32 address, int size, bool sync)
{
Uint8 readRequest[7];
readRequest[0] = k_eWiiOutputReportIDs_ReadMemory;
readRequest[1] = (Uint8)(addressSpace | (Uint8)ctx->m_bRumbleActive);
]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (EEPROM read)"
[==[static bool HIDAPI_DriverWii_InitDevice(SDL_HIDAPI_Device *device)
{
]==]
[==[// Nominal decode: zero-g at 0x200, 100 units per g
#define WII_ACCEL_NOMINAL_ZERO_G 512.0f
#define WII_ACCEL_NOMINAL_PER_G 100.0f
// EEPROM calibration at 0x16, copy at 0x20: zero-g XYZ, 1 g XYZ, LSBs of both, checksum
#define WII_ACCEL_CALIBRATION_SIZE 10
#define WII_ADDRESS_SPACE_EEPROM 0x00
static bool ReadAccelCalibrationBlock(SDL_DriverWii_Context *ctx, Uint8 address, Uint8 *block)
{
if (!ReadMemory(ctx, WII_ADDRESS_SPACE_EEPROM, address, WII_ACCEL_CALIBRATION_SIZE, true)) {
return false;
}
if (ctx->m_rgucReadBuffer[5] != address ||
ctx->m_rgucReadBuffer[3] != ((WII_ACCEL_CALIBRATION_SIZE - 1) << 4)) {
SDL_SetError("Unexpected EEPROM read response");
return false;
}
SDL_memcpy(block, ctx->m_rgucReadBuffer + 6, WII_ACCEL_CALIBRATION_SIZE);
return true;
}
static bool ParseAccelCalibration(SDL_DriverWii_Context *ctx, const Uint8 *block)
{
int zero[3];
int perG[3];
Uint8 checksum = 0x55;
int i;
for (i = 0; i < WII_ACCEL_CALIBRATION_SIZE - 1; ++i) {
checksum = (Uint8)(checksum + block[i]);
}
if (checksum != block[WII_ACCEL_CALIBRATION_SIZE - 1]) {
SDL_SetError("Bad checksum");
return false;
}
for (i = 0; i < 3; ++i) {
int shift = 4 - 2 * i;
int oneG = (block[4 + i] << 2) | ((block[7] >> shift) & 0x03);
zero[i] = (block[i] << 2) | ((block[3] >> shift) & 0x03);
perG[i] = oneG - zero[i];
// Sanity limits around the nominal values
if (zero[i] < 384 || zero[i] > 640 || perG[i] < 60 || perG[i] > 160) {
SDL_SetError("Implausible values");
return false;
}
}
for (i = 0; i < 3; ++i) {
ctx->m_fAccelZeroG[i] = (float)zero[i];
ctx->m_fAccelPerG[i] = (float)perG[i];
}
return true;
}
static void ReadAccelCalibration(SDL_DriverWii_Context *ctx)
{
static const Uint8 addresses[] = { 0x16, 0x20 };
Uint8 block[WII_ACCEL_CALIBRATION_SIZE];
int i;
for (i = 0; i < SDL_arraysize(addresses); ++i) {
if (ReadAccelCalibrationBlock(ctx, addresses[i], block) &&
ParseAccelCalibration(ctx, block)) {
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Accelerometer calibration from EEPROM 0x%02X: zero %d/%d/%d, %d/%d/%d units per g",
addresses[i],
(int)ctx->m_fAccelZeroG[0], (int)ctx->m_fAccelZeroG[1], (int)ctx->m_fAccelZeroG[2],
(int)ctx->m_fAccelPerG[0], (int)ctx->m_fAccelPerG[1], (int)ctx->m_fAccelPerG[2]);
return;
}
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Accelerometer calibration at EEPROM 0x%02X unusable: %s", addresses[i], SDL_GetError());
}
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Using nominal accelerometer calibration");
}
static bool HIDAPI_DriverWii_InitDevice(SDL_HIDAPI_Device *device)
{
]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (nominal defaults)"
[==[ ctx->device = device;
device->context = ctx;
]==]
[==[ ctx->device = device;
device->context = ctx;
for (int i = 0; i < 3; ++i) {
ctx->m_fAccelZeroG[i] = WII_ACCEL_NOMINAL_ZERO_G;
ctx->m_fAccelPerG[i] = WII_ACCEL_NOMINAL_PER_G;
}
]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (read at init)"
[==[ ctx->m_eExtensionControllerType = ReadExtensionControllerType(device);
]==]
[==[ ctx->m_eExtensionControllerType = ReadExtensionControllerType(device);
ReadAccelCalibration(ctx);
]==])
# Reports without accelerometer bytes (status, memory) decoded as garbage.
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer guard"
[==[ }
x = ((data->rgucAccelerometer[0] << 2) | ((data->rgucBaseButtons[0] >> 5) & 0x03))]==]
[==[ }
if (!data->hasAccelerometer) {
return;
}
x = ((data->rgucAccelerometer[0] << 2) | ((data->rgucBaseButtons[0] >> 5) & 0x03))]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (nominal constant)"
[==[const WiiButtonData *data)
{
const float ACCEL_RES_PER_G = 100.0f;
Sint16 x, y, z;
]==]
[==[const WiiButtonData *data)
{
Sint16 x, y, z;
]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (decode)"
[==[ x = ((data->rgucAccelerometer[0] << 2) | ((data->rgucBaseButtons[0] >> 5) & 0x03)) - 0x200;
y = ((data->rgucAccelerometer[1] << 2) | ((data->rgucBaseButtons[1] >> 4) & 0x02)) - 0x200;
z = ((data->rgucAccelerometer[2] << 2) | ((data->rgucBaseButtons[1] >> 5) & 0x02)) - 0x200;
values[0] = -((float)x / ACCEL_RES_PER_G) * SDL_STANDARD_GRAVITY;
values[1] = ((float)z / ACCEL_RES_PER_G) * SDL_STANDARD_GRAVITY;
values[2] = ((float)y / ACCEL_RES_PER_G) * SDL_STANDARD_GRAVITY;
]==]
[==[ x = ((data->rgucAccelerometer[0] << 2) | ((data->rgucBaseButtons[0] >> 5) & 0x03));
y = ((data->rgucAccelerometer[1] << 2) | ((data->rgucBaseButtons[1] >> 4) & 0x02));
z = ((data->rgucAccelerometer[2] << 2) | ((data->rgucBaseButtons[1] >> 5) & 0x02));
values[0] = -(((float)x - ctx->m_fAccelZeroG[0]) / ctx->m_fAccelPerG[0]) * SDL_STANDARD_GRAVITY;
values[1] = (((float)z - ctx->m_fAccelZeroG[2]) / ctx->m_fAccelPerG[2]) * SDL_STANDARD_GRAVITY;
values[2] = (((float)y - ctx->m_fAccelZeroG[1]) / ctx->m_fAccelPerG[1]) * SDL_STANDARD_GRAVITY;
]==])
endfunction()
# Script mode (FetchContent PATCH_COMMAND).
+2
View File
@@ -56,6 +56,8 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$" AND
message(FATAL_ERROR "MKW_X86_CPU_PROFILE must be v2 or v3 (got '${MKW_X86_CPU_PROFILE}')")
endif()
set(MKW_SETUP_VERSION "" CACHE STRING "WiiCompiled version recorded in the runtime log")
option(MKW_PLATFORM_TESTS_ONLY "Build host-platform tests without graphics dependencies or game assets" OFF)
include("${CMAKE_CURRENT_LIST_DIR}/cmake/HostLibraries.cmake")
if(MKW_PLATFORM_TESTS_ONLY)
+3
View File
@@ -80,6 +80,9 @@ target_compile_features(mkw_runtime_common PRIVATE cxx_std_20)
target_compile_definitions(mkw_runtime_common PRIVATE
SDL_MAIN_HANDLED
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
if(MKW_SETUP_VERSION)
target_compile_definitions(mkw_runtime_common PRIVATE MKW_SETUP_VERSION="${MKW_SETUP_VERSION}")
endif()
target_link_libraries(mkw_runtime_common PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
+42 -84
View File
@@ -138,28 +138,6 @@ inline std::string Trim(std::string_view text) {
return std::string(text.substr(begin, end - begin));
}
inline std::string RemoveComment(std::string_view line) {
bool inSingle = false;
bool inDouble = false;
bool escaped = false;
for (size_t i = 0; i < line.size(); ++i) {
const char ch = line[i];
if (inDouble && ch == '\\' && !escaped) {
escaped = true;
continue;
}
if (ch == '\'' && !inDouble) {
inSingle = !inSingle;
} else if (ch == '"' && !inSingle && !escaped) {
inDouble = !inDouble;
} else if (ch == '#' && !inSingle && !inDouble) {
return std::string(line.substr(0, i));
}
escaped = false;
}
return std::string(line);
}
inline bool IsSupportedResolutionMultiplier(float value) {
static constexpr std::array values{0.0f, 1.0f, 1.5f, 2.0f, 3.0f, 4.0f, 6.0f, 8.0f};
return std::find(values.begin(), values.end(), value) != values.end();
@@ -546,68 +524,50 @@ inline const std::optional<std::string>& ControllerButton(size_t index) {
return index < Get().controllerButtons.size() ? Get().controllerButtons[index] : empty;
}
// Update one TOML value without discarding comments, unrelated settings, or
// user-specific paths. This is used by the in-game F10 settings bar.
// Sets section.key (section may be a dotted table path) to a TOML literal, keeping the rest of the
// document's keys, order and comments. Returns nullopt when the document or value does not parse.
inline std::optional<std::string> UpdateSettingText(const std::string& text, std::string_view section,
std::string_view key, std::string_view value) {
try {
auto root = toml::parse_str<toml::ordered_type_config>(text);
auto literal = toml::parse_str<toml::ordered_type_config>("v = " + std::string(value));
auto* table = &root;
std::istringstream path{std::string(section)};
for (std::string part; std::getline(path, part, '.');) {
auto& parent = table->as_table();
if (!parent.contains(part)) {
parent[part] = toml::ordered_value(toml::ordered_table{});
}
table = &parent.at(part);
if (!table->is_table()) {
return std::nullopt;
}
}
if (table->as_table_fmt().fmt == toml::table_format::implicit) {
table->as_table_fmt().fmt = toml::table_format::multiline;
}
auto& slot = table->as_table()[std::string(key)];
auto& updated = literal.as_table().at("v");
updated.comments() = slot.comments();
slot = std::move(updated);
return toml::format(root);
} catch (const std::exception&) {
return std::nullopt;
}
}
// Update one TOML value without discarding unrelated settings or user-specific paths. This is
// used by the in-game F10 settings bar. An unparseable file is left untouched.
inline bool WriteSetting(std::string_view section, std::string_view key, std::string_view value) {
const auto path = ResolveConfigPath();
std::ifstream input(path);
std::vector<std::string> lines;
std::string line;
while (std::getline(input, line)) {
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
lines.push_back(std::move(line));
std::ostringstream existing;
if (std::ifstream input(path, std::ios::binary); input) {
existing << input.rdbuf();
}
const std::string normalizedSection = Trim(section);
const std::string normalizedKey = Trim(key);
size_t sectionStart = lines.size();
size_t sectionEnd = lines.size();
for (size_t i = 0; i < lines.size(); ++i) {
const std::string trimmed = Trim(RemoveComment(lines[i]));
if (trimmed.size() >= 2 && trimmed.front() == '[' && trimmed.back() == ']') {
const std::string found = Trim(std::string_view(trimmed).substr(1, trimmed.size() - 2));
if (sectionStart != lines.size()) {
sectionEnd = i;
break;
}
if (found == normalizedSection) {
sectionStart = i;
}
}
}
const std::string replacement = normalizedKey + " = " + std::string(value);
if (sectionStart == lines.size()) {
if (!lines.empty() && !lines.back().empty()) {
lines.emplace_back();
}
lines.emplace_back("[" + normalizedSection + "]");
lines.push_back(replacement);
} else {
bool replaced = false;
for (size_t i = sectionStart + 1; i < sectionEnd; ++i) {
const std::string uncommented = Trim(RemoveComment(lines[i]));
const size_t equals = uncommented.find('=');
if (equals != std::string::npos && Trim(std::string_view(uncommented).substr(0, equals)) == normalizedKey) {
lines[i] = replacement;
replaced = true;
break;
}
}
if (!replaced) {
// Append after the section's last real line rather than after the blank line that
// separates it from the next header: this file is edited by hand and by the host
// installer as well, and a key parked below the separator reads as if it belonged to
// the next section. The host writer (Launcher/WiiCompiled.Setup/RuntimeConfiguration.cs)
// applies exactly this rule.
size_t insertAt = sectionEnd;
while (insertAt > sectionStart + 1 && Trim(lines[insertAt - 1]).empty()) {
--insertAt;
}
lines.insert(lines.begin() + static_cast<std::ptrdiff_t>(insertAt), replacement);
}
const auto updated = UpdateSettingText(existing.str(), section, key, value);
if (!updated) {
std::cerr << "[runtime-config] Not updating invalid " << PathToUtf8(path) << std::endl;
return false;
}
std::error_code ec;
@@ -619,9 +579,7 @@ inline bool WriteSetting(std::string_view section, std::string_view key, std::st
std::cerr << "[runtime-config] Unable to write " << PathToUtf8(path) << std::endl;
return false;
}
for (const auto& outputLine : lines) {
output << outputLine << '\n';
}
output << *updated;
return static_cast<bool>(output);
}
+2 -9
View File
@@ -73,15 +73,8 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample);
// no remote or SDL has not delivered a sample yet.
bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]);
// Accelerometer zero-point calibration. SDL's Wii driver tries to read the
// remote's factory calibration block, but over Bluetooth that read often times
// out ("Using fallback accelerometer calibration" in console.log) and it falls
// back to a nominal 0x200 zero point; a real remote then carries a per-axis
// bias of up to ~0.2 g, which held sideways as a wheel is a permanent steering
// offset.
// StartAccelCalibration expects the remote lying still with the buttons up;
// Poll() then collects samples for about a second and stores the difference to
// the ideal (0, 1, 0) g in Config.toml.
// Residual zero-point trim on top of SDL's factory-calibrated decode. Expects the
// remote lying still, buttons up; Poll() averages about a second and stores it.
void StartAccelCalibration(uint32_t chan);
// Forgets the stored correction.
void ClearAccelCalibration();
+8 -3
View File
@@ -378,10 +378,15 @@ void AttachParentConsoleForDiagnostics() {
void AttachParentConsoleForDiagnostics() {}
#endif
// The setup writes build-fingerprint.json beside every product executable; its SetupVersion is
// the only version identity the runtime has (products are compiled locally, so nothing is baked
// into the binary). Surface it at the top of the transcript so every attached log self-identifies.
// The macOS and Linux local builds bake the setup version in; the Windows setup instead writes it
// to build-fingerprint.json beside the executable. Surfaced at the top of the transcript so every
// attached log self-identifies.
std::string ReadInstalledSetupVersion() {
#if defined(MKW_SETUP_VERSION)
if (constexpr std::string_view builtIn = MKW_SETUP_VERSION; !builtIn.empty()) {
return std::string(builtIn);
}
#endif
const auto directory = RuntimeConfigFile::ExecutableDirectory();
if (!directory) {
return {};
+39 -1
View File
@@ -12,6 +12,7 @@
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <ctime>
#include <stdexcept>
#include <limits>
@@ -44,11 +45,48 @@ uint32_t g_reservationAddr = 0;
bool g_hasReservation = false;
const auto g_timeBaseStart = std::chrono::steady_clock::now();
// Days from 1970-01-01 to the given civil date (Howard Hinnant's algorithm).
constexpr int64_t DaysFromCivil(int64_t y, unsigned m, unsigned d)
{
y -= m <= 2;
const int64_t era = (y >= 0 ? y : y - 399) / 400;
const unsigned yoe = static_cast<unsigned>(y - era * 400);
const unsigned doy = (153 * (m > 2 ? m - 3 : m + 9) + 2) / 5 + d - 1;
const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
return era * 146097 + static_cast<int64_t>(doe) - 719468;
}
// The Wii RTC holds local time; the OS counts ticks from 2000-01-01.
uint64_t InitialTimeBase()
{
const std::time_t now = std::time(nullptr);
std::tm local{};
#if defined(_WIN32)
if (localtime_s(&local, &now) != 0) {
return 0;
}
#else
if (localtime_r(&now, &local) == nullptr) {
return 0;
}
#endif
const int64_t days = DaysFromCivil(local.tm_year + 1900, static_cast<unsigned>(local.tm_mon + 1),
static_cast<unsigned>(local.tm_mday)) -
DaysFromCivil(2000, 1, 1);
const int64_t seconds = days * 86400 + local.tm_hour * 3600 + local.tm_min * 60 + local.tm_sec;
if (seconds <= 0) {
return 0;
}
return static_cast<uint64_t>(seconds) * TimeBaseContract::kTicksPerSecond;
}
const uint64_t g_timeBaseOrigin = InitialTimeBase();
uint64_t GetTimeBase() {
const auto elapsed = std::chrono::steady_clock::now() - g_timeBaseStart;
const auto nanoseconds =
std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed).count();
return TimeBaseContract::NanosecondsToTicks(static_cast<uint64_t>(nanoseconds));
return g_timeBaseOrigin + TimeBaseContract::NanosecondsToTicks(static_cast<uint64_t>(nanoseconds));
}
constexpr uint32_t kFpscrFx = 1u << 31;
+3 -4
View File
@@ -309,9 +309,8 @@ void DrawWiiRemoteAccelerometer(uint32_t port) {
} else {
ImGui::TextDisabled("No accelerometer data yet.");
}
// SDL's read of the remote's calibration block often times out over Bluetooth
// and it falls back to a nominal zero point, leaving a small per-axis bias;
// measured here with the remote at rest.
// Residual per-axis bias left after SDL's factory-calibrated decode, measured
// here with the remote at rest.
if (WiiRemoteInput::IsAccelCalibrating()) {
ImGui::ProgressBar(WiiRemoteInput::AccelCalibrationProgress(), ImVec2(220.0f, 0.0f), "Hold still...");
} else if (ImGui::Button("Calibrate (remote lying flat, buttons up)")) {
@@ -333,7 +332,7 @@ void DrawWiiRemoteAccelerometer(uint32_t port) {
const std::array<double, 3> offset = RuntimeConfigFile::WiiAccelOffset();
ImGui::TextDisabled("Stored offset: x %+.3f y %+.3f z %+.3f g", offset[0], offset[1], offset[2]);
} else {
ImGui::TextDisabled("Not calibrated (using SDL's zero point; see console.log for \"fallback accelerometer calibration\").");
ImGui::TextDisabled("No trim stored.");
}
}
+31 -17
View File
@@ -102,10 +102,10 @@ bool g_accelOffsetLoaded = false;
// sample may stray from the first one before the run is declared "moved".
constexpr int kCalibrationSamples = 90;
constexpr float kCalibrationMaxDeviationG = 0.15f;
// |mean| outside this range means the remote was not at rest or the
// accelerometer is far off its nominal scale; either way the offset is useless.
constexpr float kCalibrationMinGravityG = 0.8f;
constexpr float kCalibrationMaxGravityG = 1.2f;
// A resting remote reads about 1 g and lies within 20 degrees of face up.
constexpr double kCalibrationMinGravityG = 0.8;
constexpr double kCalibrationMaxGravityG = 1.2;
constexpr double kCalibrationMinTiltCosine = 0.9396926;
struct AccelCalibration {
bool active = false;
@@ -320,7 +320,8 @@ void StepAccelCalibration() {
std::array<double, 3> mean{};
for (int i = 0; i < 3; ++i) mean[i] = g_calibration.sum[i] / g_calibration.count;
const double length = std::sqrt(mean[0] * mean[0] + mean[1] * mean[1] + mean[2] * mean[2]);
if (length < kCalibrationMinGravityG || length > kCalibrationMaxGravityG || mean[1] < 0.5) {
if (length < kCalibrationMinGravityG || length > kCalibrationMaxGravityG ||
mean[1] / length < kCalibrationMinTiltCosine) {
FinishAccelCalibration("Failed: the remote was not resting flat with the buttons up.");
return;
}
@@ -602,15 +603,30 @@ void FillGraceSample(uint32_t chan, Kind kind, KpadSample& sample) {
sample.hasClassic = kind == Kind::RemoteWithClassic;
}
// SDL's stick axis (-32767..32767, y down) as WPADCLStatus carries it: WPAD
// normalises every Classic Controller stick, whatever the report's resolution,
// to a signed 10-bit value, -512..511 with 0 at the centre and +y up (RVL SDK
// WPAD.h; wut's WPADStatusClassic documents the same range).
// The patched SDL Wii driver posts Classic Controller sticks as WPAD's own
// value times 64 (aurora-main/cmake/AuroraSDL3Patches.cmake), so WPADCLStatus
// is the axis divided back: signed, 0 at the centre, -512..511, +y up.
int16_t ClassicStickRaw(Sint16 axis, bool invert) {
float value = static_cast<float>(axis) / 32767.0f;
if (invert) value = -value;
value = std::clamp(value, -1.0f, 1.0f);
return static_cast<int16_t>(std::clamp(std::lround(value * 512.0f), -512L, 511L));
const long value = invert ? -static_cast<long>(axis) : axis;
return static_cast<int16_t>(std::clamp(std::lround(value / 64.0), -512L, 511L));
}
// KPAD's default Classic Controller stick limits; the game uses the cross clamp.
constexpr float kKpadStickMin = 60.0f;
constexpr float kKpadStickMax = 308.0f;
// KPAD cross clamp: per-axis dead zone and scale, then the unit circle.
void ClassicStickFloat(const int16_t raw[2], float out[2]) {
for (int i = 0; i < 2; ++i) {
const float magnitude = std::fabs(static_cast<float>(raw[i]));
const float scaled = std::clamp((magnitude - kKpadStickMin) / (kKpadStickMax - kKpadStickMin), 0.0f, 1.0f);
out[i] = raw[i] < 0 ? -scaled : scaled;
}
const float length = std::sqrt(out[0] * out[0] + out[1] * out[1]);
if (length > 1.0f) {
out[0] /= length;
out[1] /= length;
}
}
// Samples buttons, accelerometers and the extension of the remote on a port.
@@ -696,14 +712,12 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
const Sint16 ly = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
const Sint16 rx = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX);
const Sint16 ry = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY);
sample.clLStick[0] = std::clamp(static_cast<float>(lx) / 32767.0f, -1.0f, 1.0f);
sample.clLStick[1] = std::clamp(-static_cast<float>(ly) / 32767.0f, -1.0f, 1.0f);
sample.clRStick[0] = std::clamp(static_cast<float>(rx) / 32767.0f, -1.0f, 1.0f);
sample.clRStick[1] = std::clamp(-static_cast<float>(ry) / 32767.0f, -1.0f, 1.0f);
sample.clLStickRaw[0] = ClassicStickRaw(lx, false);
sample.clLStickRaw[1] = ClassicStickRaw(ly, true);
sample.clRStickRaw[0] = ClassicStickRaw(rx, false);
sample.clRStickRaw[1] = ClassicStickRaw(ry, true);
ClassicStickFloat(sample.clLStickRaw, sample.clLStick);
ClassicStickFloat(sample.clRStickRaw, sample.clRStick);
// Only the full-press click of L/R reaches SDL; report it as a full pull.
sample.clTriggerL = (sample.clHold & kClL) ? 255 : 0;
sample.clTriggerR = (sample.clHold & kClR) ? 255 : 0;
+64
View File
@@ -1,7 +1,9 @@
#include "runtime_config.h"
#include <iostream>
#include <optional>
#include <sstream>
#include <string>
namespace {
bool Require(bool value, const char* message) {
@@ -11,6 +13,24 @@ bool Require(bool value, const char* message) {
}
return true;
}
std::optional<toml::ordered_value> Update(const std::string& original, const char* section, const char* key,
const char* value, std::string* text = nullptr) {
const auto updated = RuntimeConfigFile::UpdateSettingText(original, section, key, value);
if (!updated) return std::nullopt;
if (text) *text = *updated;
return toml::parse_str<toml::ordered_type_config>(*updated);
}
bool SetsFps(const std::string& original, const char* message) {
const auto document = Update(original, "video", "show_fps", "true");
return Require(document && document->at("video").at("show_fps").as_boolean() &&
document->at("video").as_table().size() == 1,
message);
}
bool SetsOffset(const std::string& original, const char* message) {
const auto document = Update(original, "controller.wii_accel_offsets", "x", "0.5");
return Require(document && document->at("controller").at("wii_accel_offsets").at("x").as_floating() == 0.5,
message);
}
} // namespace
int main() {
@@ -31,5 +51,49 @@ int main() {
const RuntimeUserConfig config = RuntimeConfigFile::ParseConfig(input, "invalid.toml");
if (!Require(!config.metalFxSpatialUpscaling, "invalid MetalFX setting was accepted")) return 1;
}
if (!SetsFps("[video]\nshow_fps = false\n", "bare key was not replaced")) return 1;
if (!SetsFps("[video]\n\"show_fps\" = false\n", "basic-quoted key was not replaced")) return 1;
if (!SetsFps("[video]\n'show_fps'=false\n", "literal-quoted key was not replaced")) return 1;
if (!SetsFps("[video]\n\"show\\u005Ffps\" = false\n", "escaped key was not replaced")) return 1;
if (!SetsFps("[\"video\"]\n show_fps = false\n", "quoted table header was not matched")) return 1;
if (!SetsFps("[video]\nshow_fps = [\n 1,\n]\n", "multi-line value was not replaced")) return 1;
if (!SetsFps("[video]\n", "missing key was not created")) return 1;
if (!SetsFps("", "missing table was not created")) return 1;
{
std::string text;
const auto document = Update("# top\nfoo = 1\n\n[video]\nzzz = 2 # keep\nshow_fps = false # note\naaa = 3\n\n"
"[audio]\nshow_fps = 1\n",
"video", "show_fps", "true", &text);
if (!Require(document && document->at("foo").as_integer() == 1 &&
document->at("audio").at("show_fps").as_integer() == 1 &&
document->at("video").at("zzz").as_integer() == 2 &&
document->at("video").at("aaa").as_integer() == 3 &&
document->at("video").at("show_fps").as_boolean(),
"unrelated settings were not preserved")) return 1;
const size_t zzz = text.find("zzz"), fps = text.find("show_fps"), aaa = text.find("aaa");
if (!Require(text.find("# top") != std::string::npos && text.find("# keep") != std::string::npos &&
text.find("# note") != std::string::npos && zzz < fps && fps < aaa,
"comments or key order were not preserved")) return 1;
}
if (!Require(!RuntimeConfigFile::UpdateSettingText("[video]\nshow_fps = 1\nshow_fps = 2\n", "video", "show_fps", "true"),
"a document with a duplicate key was rewritten")) return 1;
if (!Require(!RuntimeConfigFile::UpdateSettingText("[video\nshow_fps = 1\n", "video", "show_fps", "true"),
"an unparseable document was rewritten")) return 1;
if (!SetsOffset("[controller]\nrumble = true\n\n[controller.wii_accel_offsets]\nx = 0.1\ny = 2\n",
"key in an existing nested table was not replaced")) return 1;
if (!SetsOffset("[controller.wii_accel_offsets]\ny = 2\n", "key was not added to a nested table")) return 1;
if (!SetsOffset("[controller]\nrumble = true\n", "missing nested table was not created")) return 1;
if (!SetsOffset("[controller.\"wii_accel_offsets\"]\nx = 0.1\n", "quoted nested header was not matched")) return 1;
if (!SetsOffset("[controller]\nwii_accel_offsets.y = 2\n", "dotted-key table was not extended")) return 1;
if (!SetsOffset("controller.wii_accel_offsets.y = 2\n", "root-level dotted table was not extended")) return 1;
if (!Require(!RuntimeConfigFile::UpdateSettingText("controller = \"custom\"\n", "controller.wii_accel_offsets",
"x", "0.5"),
"a scalar was replaced by a table")) return 1;
{
const auto document = Update("[video.advanced]\nfoo = 1\n", "video", "show_fps", "true");
if (!Require(document && document->at("video").at("show_fps").as_boolean() &&
document->at("video").at("advanced").at("foo").as_integer() == 1,
"key was not added to an implicit table")) return 1;
}
return 0;
}
@@ -230,6 +230,11 @@ public sealed class PpcDisassembler : IDisposable
{
foreach (var target in ins.BranchTargets)
{
// A conditional call to another function returns to its fallthrough.
if (ins.IsCall && IsKnownExternalFunctionEntry(target))
{
continue;
}
localControlFlowTargets.Add(target);
if (!reachable.Contains(target))
{
@@ -690,10 +690,11 @@ public sealed partial class PpcLifter
var rawInstr = ReadRawInstruction(ins);
var bo = (int)((rawInstr >> 21) & 0x1F);
var bi = (int)((rawInstr >> 16) & 0x1F);
var fallthrough = $"0x{ins.EndAddress:X8}";
var targetLabel = mnemonic == "bcctr"
? $"indirect_ctr_{ins.Address:X8}"
: $"call_ctr_{ins.Address:X8}_{ins.EndAddress:X8}";
var linked = mnemonic == "bcctrl";
var targetLabel = linked
? $"call_ctr_{ins.Address:X8}_{ins.EndAddress:X8}"
: $"indirect_ctr_{ins.Address:X8}";
var fallthrough = linked ? LinkedFallthroughLabel(ins) : $"0x{ins.EndAddress:X8}";
return new[] { new IrBranch("raw", targetLabel, fallthrough, BuildBoConditionExpression(bo, bi, allowCtr: false)) };
}
@@ -1943,6 +1944,15 @@ public sealed partial class PpcLifter
};
case "bltl":
case "bgtl":
case "beql":
case "bsol":
case "bgel":
case "blel":
case "bnel":
case "bnsl":
case "bdnzl":
case "bdzl":
case "bcl":
{
var rawInstr = ReadRawInstruction(ins);
@@ -1978,14 +1988,13 @@ public sealed partial class PpcLifter
var rawInstr = ReadRawInstruction(ins);
var bo = (int)((rawInstr >> 21) & 0x1F);
var bi = (int)((rawInstr >> 16) & 0x1F);
var fallthrough = $"0x{ins.EndAddress:X8}";
var targetLabel = $"call_lr_{ins.Address:X8}_{ins.EndAddress:X8}";
var instructions = new List<IrInstruction>();
if ((bo & 0x04) == 0)
{
instructions.Add(new IrBinary("ctr", IrValue.Register("ctr"), IrValue.Imm(-1), "add"));
}
instructions.Add(new IrBranch("raw", targetLabel, fallthrough, BuildBoConditionExpression(bo, bi, allowCtr: true)));
instructions.Add(new IrBranch("raw", targetLabel, LinkedFallthroughLabel(ins), BuildBoConditionExpression(bo, bi, allowCtr: true)));
return instructions;
}
@@ -2004,10 +2013,9 @@ public sealed partial class PpcLifter
crField = NormalizeRegister(crOp.Name);
}
var fallthrough = $"0x{ins.EndAddress:X8}";
var targetLabel = $"call_lr_{ins.Address:X8}_{ins.EndAddress:X8}";
var cond = mnemonic.Replace("lrl", string.Empty, StringComparison.OrdinalIgnoreCase);
return new[] { new IrBranch(cond, targetLabel, fallthrough, crField) };
return new[] { new IrBranch(cond, targetLabel, LinkedFallthroughLabel(ins), crField) };
}
case "bne":
@@ -3033,6 +3041,10 @@ private static string TargetLabel(PpcInstruction ins, HashSet<uint> validAddress
return $"0x{target:X8}";
}
// Not-taken path of a linked conditional branch: LR still receives the return address.
private static string LinkedFallthroughLabel(PpcInstruction ins) =>
$"link_branch_{ins.EndAddress:X8}_{ins.EndAddress:X8}";
private static string BuildBoConditionExpression(int bo, int bi, bool allowCtr)
{
var crFieldIdx = bi / 4;
@@ -420,7 +420,7 @@ public sealed class FunctionTranslator
continue;
}
if (TryBuildSyntheticBlock(label, out var synth))
if (TryBuildSyntheticBlock(label, existingLabels, out var synth))
{
irBlocks.Add(synth);
existingLabels.Add(label);
@@ -839,7 +839,7 @@ public sealed class FunctionTranslator
return rewritten;
}
private static bool TryBuildSyntheticBlock(string label, out IrBasicBlock block)
private static bool TryBuildSyntheticBlock(string label, IReadOnlySet<string> localLabels, out IrBasicBlock block)
{
const string IndirectCtrPrefix = "indirect_ctr_";
const string CallCtrPrefix = "call_ctr_";
@@ -883,11 +883,18 @@ public sealed class FunctionTranslator
if (TryParseLinkBranchLabel(label, LinkBranchPrefix, out fallthrough, out var target))
{
block = new IrBasicBlock(label, new IrInstruction[]
{
new IrAssign("lr", IrValue.Imm((int)fallthrough)),
new IrJump($"0x{target:X8}")
});
block = new IrBasicBlock(label, target == fallthrough || localLabels.Contains($"0x{target:X8}")
? new IrInstruction[]
{
new IrAssign("lr", IrValue.Imm((int)fallthrough)),
new IrJump($"0x{target:X8}")
}
: new IrInstruction[]
{
new IrAssign("lr", IrValue.Imm((int)fallthrough)),
new IrCall(string.Empty, $"0x{target:X8}", BuildAbiCallArgs()),
new IrJump($"0x{fallthrough:X8}")
});
return true;
}
@@ -45,6 +45,37 @@ public class FunctionTranslatorCoverageTests
Assert.Equal(TimeSpan.Zero, discovery.Metrics.CodeGen);
}
[Theory]
[InlineData(0x4D820021u)] // beqlrl
[InlineData(0x4D820421u)] // bcctrl, BO=12 BI=2
public void Discover_LinkedConditionalRegisterBranchUpdatesLrWhenNotTaken(uint branchWord)
{
var memory = new byte[MemoryLayout.RamSize];
WriteWord(memory, 0x80001000, branchWord);
WriteWord(memory, 0x80001004, 0x7C6802A6); // mflr r3
WriteWord(memory, 0x80001008, 0x4E800020); // blr
var image = new ProgramImage(
memory,
AddressRange.FromStartAndSize(0x80001000, 0x1004),
AddressRange.FromStartAndSize(0x80001000, 0x1004),
default,
"test");
var discovery = new FunctionTranslator(image).Discover(
0x80001000,
TranslationOptions.Default with { MaxBytes = 0x20, AllowUnsupportedInstructions = true });
var blocks = discovery.Ssa.Function.Blocks;
var branch = Assert.Single(blocks.SelectMany(b => b.Instructions).OfType<IrBranch>());
Assert.Equal("link_branch_80001004_80001004", branch.FalseLabel);
var notTaken = Assert.Single(blocks, b => b.Label == branch.FalseLabel);
var lrAssign = Assert.Single(notTaken.Instructions.OfType<IrAssign>());
Assert.StartsWith("lr", lrAssign.Destination, StringComparison.Ordinal);
Assert.Equal(unchecked((int)0x80001004u), lrAssign.Value.Constant);
Assert.Equal("0x80001004", Assert.IsType<IrJump>(notTaken.Instructions[^1]).TargetLabel);
}
[Fact]
public void Translate_PreservesFallthroughAfterDirectLinkBranch()
{
@@ -354,18 +385,20 @@ public class FunctionTranslatorCoverageTests
Assert.Equal("f13", abiArgs[^1].RegisterName);
}
private static readonly HashSet<string> NoLocalLabels = new();
[Fact]
public void TryBuildSyntheticBlock_CoversIndirectAndCallForms()
{
var indirectArgs = new object?[] { "indirect_ctr_80001234", null! };
var indirectArgs = new object?[] { "indirect_ctr_80001234", NoLocalLabels, null! };
Assert.True(InvokePrivate<bool>("TryBuildSyntheticBlock", indirectArgs));
var indirect = Assert.IsType<IrBasicBlock>(indirectArgs[1]);
var indirect = Assert.IsType<IrBasicBlock>(indirectArgs[2]);
Assert.Equal("indirect_ctr_80001234", indirect.Label);
Assert.Equal("ctr", Assert.IsType<IrIndirectJump>(Assert.Single(indirect.Instructions)).Target.RegisterName);
var callCtrArgs = new object?[] { "call_ctr_80001000_80001004", null! };
var callCtrArgs = new object?[] { "call_ctr_80001000_80001004", NoLocalLabels, null! };
Assert.True(InvokePrivate<bool>("TryBuildSyntheticBlock", callCtrArgs));
var callCtr = Assert.IsType<IrBasicBlock>(callCtrArgs[1]);
var callCtr = Assert.IsType<IrBasicBlock>(callCtrArgs[2]);
Assert.Collection(
callCtr.Instructions,
ins =>
@@ -382,9 +415,9 @@ public class FunctionTranslatorCoverageTests
},
ins => Assert.Equal("0x80001004", Assert.IsType<IrJump>(ins).TargetLabel));
var callLrArgs = new object?[] { "call_lr_80002000_80002004", null! };
var callLrArgs = new object?[] { "call_lr_80002000_80002004", NoLocalLabels, null! };
Assert.True(InvokePrivate<bool>("TryBuildSyntheticBlock", callLrArgs));
var callLr = Assert.IsType<IrBasicBlock>(callLrArgs[1]);
var callLr = Assert.IsType<IrBasicBlock>(callLrArgs[2]);
Assert.Collection(
callLr.Instructions,
ins =>
@@ -407,9 +440,39 @@ public class FunctionTranslatorCoverageTests
},
ins => Assert.Equal("0x80002004", Assert.IsType<IrJump>(ins).TargetLabel));
var absoluteArgs = new object?[] { "0x800D7FCC", null! };
var notTakenArgs = new object?[] { "link_branch_80002004_80002004", NoLocalLabels, null! };
Assert.True(InvokePrivate<bool>("TryBuildSyntheticBlock", notTakenArgs));
var notTaken = Assert.IsType<IrBasicBlock>(notTakenArgs[2]);
Assert.Collection(
notTaken.Instructions,
ins =>
{
var assign = Assert.IsType<IrAssign>(ins);
Assert.Equal("lr", assign.Destination);
Assert.Equal(unchecked((int)0x80002004u), assign.Value.Constant);
},
ins => Assert.Equal("0x80002004", Assert.IsType<IrJump>(ins).TargetLabel));
var linkedCallArgs = new object?[] { "link_branch_80002004_800D7FCC", NoLocalLabels, null! };
Assert.True(InvokePrivate<bool>("TryBuildSyntheticBlock", linkedCallArgs));
var linkedCall = Assert.IsType<IrBasicBlock>(linkedCallArgs[2]);
Assert.Collection(
linkedCall.Instructions,
ins => Assert.Equal(unchecked((int)0x80002004u), Assert.IsType<IrAssign>(ins).Value.Constant),
ins => Assert.Equal("0x800D7FCC", Assert.IsType<IrCall>(ins).Target),
ins => Assert.Equal("0x80002004", Assert.IsType<IrJump>(ins).TargetLabel));
var localLinkedArgs = new object?[] { "link_branch_80002004_80002000", new HashSet<string> { "0x80002000" }, null! };
Assert.True(InvokePrivate<bool>("TryBuildSyntheticBlock", localLinkedArgs));
var localLinked = Assert.IsType<IrBasicBlock>(localLinkedArgs[2]);
Assert.Collection(
localLinked.Instructions,
ins => Assert.Equal(unchecked((int)0x80002004u), Assert.IsType<IrAssign>(ins).Value.Constant),
ins => Assert.Equal("0x80002000", Assert.IsType<IrJump>(ins).TargetLabel));
var absoluteArgs = new object?[] { "0x800D7FCC", NoLocalLabels, null! };
Assert.True(InvokePrivate<bool>("TryBuildSyntheticBlock", absoluteArgs));
var absolute = Assert.IsType<IrBasicBlock>(absoluteArgs[1]);
var absolute = Assert.IsType<IrBasicBlock>(absoluteArgs[2]);
Assert.Equal("0x800D7FCC", absolute.Label);
Assert.Collection(
absolute.Instructions,
@@ -421,9 +484,9 @@ public class FunctionTranslatorCoverageTests
},
ins => Assert.Null(Assert.IsType<IrReturn>(ins).Value));
var invalidArgs = new object?[] { "plain_label", null! };
var invalidArgs = new object?[] { "plain_label", NoLocalLabels, null! };
Assert.False(InvokePrivate<bool>("TryBuildSyntheticBlock", invalidArgs));
Assert.Null(invalidArgs[1]);
Assert.Null(invalidArgs[2]);
}
[Fact]
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Translator.Core.Disassembly;
using Translator.Core.Ir;
@@ -37,6 +38,85 @@ public class PpcLifterAdditionalCoverageTests
Assert.Equal($"0x{branch.EndAddress:X8}", decision.FalseLabel);
}
[Fact]
public void ConditionalCallTargetIsNotDecodedAsLocalCode()
{
const uint entry = 0x80000000;
const uint callee = entry + 0x10;
var image = TranslatorCppTestHarness.CreateImage(
(entry + 0x00, 0x41820011), // beql callee
(entry + 0x04, 0x38600001), // li r3, 1
(entry + 0x08, 0x4E800020), // blr
(callee + 0x00, 0x38600002), // li r3, 2
(callee + 0x04, 0x4E800020)); // blr
using var disassembler = new PpcDisassembler();
var instructions = disassembler.DisassembleFunction(
image, entry, maxInstructions: 32, maxBytes: 0x80, knownFunctionEntryPoints: new HashSet<uint> { entry, callee });
Assert.Equal(new[] { entry, entry + 4, entry + 8 }, instructions.Select(ins => ins.Address));
}
[Fact]
public void ConditionalCallToLocalTargetKeepsLocalEdge()
{
const uint entry = 0x80000000;
var image = TranslatorCppTestHarness.CreateImage(
(entry + 0x00, 0x4182000D), // beql local
(entry + 0x04, 0x38600001), // li r3, 1
(entry + 0x08, 0x4E800020), // blr
(entry + 0x0C, 0x38600002), // local: li r3, 2
(entry + 0x10, 0x4E800020)); // blr
using var disassembler = new PpcDisassembler();
var instructions = disassembler.DisassembleFunction(
image, entry, maxInstructions: 32, maxBytes: 0x80, knownFunctionEntryPoints: new HashSet<uint> { entry });
Assert.Contains(instructions, ins => ins.Address == entry + 0x0C);
}
[Theory]
[InlineData(0x41820009u, "beql", "0x80001008")]
[InlineData(0x41810009u, "bgtl", "0x80001008")]
[InlineData(0x40820009u, "bnel", "0x80001008")]
[InlineData(0x42000009u, "bdnzl", "0x80001008")]
[InlineData(0x42400009u, "bdzl", "0x80001008")]
public void LinkedConditionalRelativeBranchFormsSetLrOnBothPaths(uint word, string mnemonic, string target)
{
var branch = PpcDecoder.Decode(0x80001000, word);
var ir = Assert.Single(new PpcLifter().Lift(new[] { branch })).Ir;
Assert.Equal(mnemonic, branch.Mnemonic);
var lrAssign = Assert.Single(ir.OfType<IrAssign>());
Assert.Equal("lr", lrAssign.Destination);
Assert.Equal(unchecked((int)0x80001004u), lrAssign.Value.Constant);
Assert.IsType<IrAssign>(ir[^2]);
var decision = Assert.IsType<IrBranch>(ir[^1]);
Assert.Equal("raw", decision.Condition);
Assert.Equal($"link_branch_80001004_{target[2..]}", decision.TrueLabel);
Assert.Equal("0x80001004", decision.FalseLabel);
Assert.Equal(mnemonic.StartsWith("bd", StringComparison.Ordinal), ir.OfType<IrBinary>().Any(b => b.Destination == "ctr"));
}
[Theory]
[InlineData(0x4D820021u)]
[InlineData(0x4D820421u)]
[InlineData(0x4D030021u)]
public void LinkedConditionalRegisterBranchesRouteNotTakenPathThroughLrUpdate(uint word)
{
var branch = PpcDecoder.Decode(0x80001000, word);
var ir = Assert.Single(new PpcLifter().Lift(new[] { branch })).Ir;
var decision = Assert.IsType<IrBranch>(ir[^1]);
Assert.Equal("link_branch_80001004_80001004", decision.FalseLabel);
Assert.StartsWith(branch.Mnemonic.Contains("ctr", StringComparison.Ordinal) ? "call_ctr_" : "call_lr_", decision.TrueLabel, StringComparison.Ordinal);
Assert.DoesNotContain(ir, ins => ins is IrAssign { Destination: "lr" });
}
[Fact]
public void LiftsAdditionalNonDotArithmeticAndLogicalForms()
{
@@ -365,6 +445,7 @@ public class PpcLifterAdditionalCoverageTests
var bclrl = Instruction((12u << 21) | (8u << 16), "bclrl", new PpcConditionRegisterOperand("cr2", 8));
var bclrlBranch = Assert.IsType<IrBranch>(Assert.Single(Assert.Single(lifter.Lift(new[] { bclrl })).Ir));
Assert.Contains("call_lr_80000000_80000004", bclrlBranch.TrueLabel, StringComparison.Ordinal);
Assert.Equal("link_branch_80000004_80000004", bclrlBranch.FalseLabel);
Assert.Equal("raw", bclrlBranch.Condition);
Assert.Contains("GetCRBit(ctx, 2, 0)", bclrlBranch.ConditionRegister, StringComparison.Ordinal);
}
@@ -564,6 +564,7 @@ public class PpcLifterCoverageTests
Assert.Equal("beq", beqlrlBranch.Condition);
Assert.Equal("cr3", beqlrlBranch.ConditionRegister);
Assert.Contains("call_lr_80000000_80000004", beqlrlBranch.TrueLabel, StringComparison.Ordinal);
Assert.Equal("link_branch_80000004_80000004", beqlrlBranch.FalseLabel);
var bgtlrl = Instruction(0, "bgtlrl");
var bgtlrlBranch = Assert.IsType<IrBranch>(Assert.Single(Assert.Single(lifter.Lift(new[] { bgtlrl })).Ir));