mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-10-20 05:33:23 -04:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88703702df | |||
| c255df5136 | |||
| 2ee48fc880 | |||
| d7b64361e7 | |||
| 5ee158918f | |||
| bd12087cc3 | |||
| 93353ee335 | |||
| d5296779ec | |||
| a93eaf582f | |||
| ed6fea0efb | |||
| 95a49f3c89 | |||
| 60197f2e75 | |||
| b87186c96b | |||
| 0b12fc864a | |||
| 5688b8b5b6 |
@@ -429,7 +429,8 @@ try {
|
||||
}
|
||||
$redist = Join-Path $Toolkit 'Redist'
|
||||
if (Test-Path -LiteralPath $redist -PathType Container) {
|
||||
Copy-Item -Path (Join-Path $redist '*.dll') -Destination $Destination -Force
|
||||
Get-ChildItem -LiteralPath $redist -Filter '*.dll' -File |
|
||||
Copy-Item -Destination $Destination -Force
|
||||
}
|
||||
foreach ($name in @('config','wii_bootstrap')) {
|
||||
$directory = Join-Path $build $name
|
||||
|
||||
@@ -145,7 +145,8 @@ function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Descri
|
||||
else { Write-Host "${LogPrefix}: $Description" }
|
||||
$quotedArguments = @($Arguments | ForEach-Object {
|
||||
if ($_.Contains('"')) { throw "A native build argument contains an unsupported quote: $_" }
|
||||
'"' + $_ + '"'
|
||||
# Backslashes directly before the closing quote would otherwise escape it.
|
||||
'"' + ($_ -replace '(\\+)$', '$1$1') + '"'
|
||||
})
|
||||
if ($WaitForProcessTree) {
|
||||
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -501,17 +501,31 @@ internal sealed class InstallerEngine
|
||||
var dol = Path.Combine(dataRoot, "sys", "main.dol");
|
||||
var rel = Path.Combine(dataRoot, "files", "rel", "StaticR.rel");
|
||||
var fst = Path.Combine(dataRoot, "sys", "fst.bin");
|
||||
if (!File.Exists(dol) || !File.Exists(rel) || !File.Exists(fst))
|
||||
throw new InvalidDataException("The disc extraction is missing required Mario Kart Wii files.");
|
||||
var dolHash = InputValidation.Sha256File(dol);
|
||||
var relHash = InputValidation.Sha256File(rel);
|
||||
if (!dolHash.Equals(manifest.ExpectedDolSha256, StringComparison.OrdinalIgnoreCase) ||
|
||||
!relHash.Equals(manifest.ExpectedRelSha256, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var missing = new[] { dol, rel, fst }.Where(path => !File.Exists(path))
|
||||
.Select(path => Path.GetRelativePath(dataRoot, path)).ToList();
|
||||
if (missing.Count > 0)
|
||||
throw new InvalidDataException(
|
||||
"The disc is RMCP01 but does not match the supported clean PAL revision. " +
|
||||
"Patched or otherwise modified game code cannot be installed safely.");
|
||||
"The disc extraction is missing required Mario Kart Wii files: " +
|
||||
string.Join(", ", missing) + ".");
|
||||
|
||||
var mismatches = new List<string>();
|
||||
foreach (var (name, path, expected) in new[]
|
||||
{
|
||||
("main.dol", dol, manifest.ExpectedDolSha256),
|
||||
("StaticR.rel", rel, manifest.ExpectedRelSha256)
|
||||
})
|
||||
{
|
||||
var actual = InputValidation.Sha256File(path);
|
||||
if (!actual.Equals(expected, StringComparison.OrdinalIgnoreCase))
|
||||
mismatches.Add($"{name} is {new FileInfo(path).Length:N0} bytes with SHA-256 {actual} " +
|
||||
$"(expected {expected})");
|
||||
}
|
||||
|
||||
if (mismatches.Count > 0)
|
||||
throw new InvalidDataException(
|
||||
"The disc is RMCP01 but its game code does not match the supported clean PAL revision: " +
|
||||
string.Join("; ", mismatches) + ". " +
|
||||
"Patched or otherwise modified game code cannot be installed safely.");
|
||||
}
|
||||
|
||||
private static void ValidateInstallDirectory(string path) =>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
+14
-2
@@ -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
|
||||
@@ -437,7 +447,9 @@ if [[ "$cc_bin" == */* ]]; then
|
||||
configure_args+=(-DCMAKE_ASM_COMPILER="$cc_bin")
|
||||
fi
|
||||
if [[ -n "$fuse_ld_override" ]]; then
|
||||
configure_args+=(-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=$fuse_ld_override")
|
||||
# CMAKE_EXE_LINKER_FLAGS is one string that CMake pastes into the shell link command.
|
||||
printf -v fuse_ld_quoted '%q' "$fuse_ld_override"
|
||||
configure_args+=(-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=$fuse_ld_quoted")
|
||||
fi
|
||||
if [[ -n "$native_prebuilt_dir" ]]; then
|
||||
configure_args+=(-DMKW_NATIVE_PREBUILT_DIR="$native_prebuilt_dir")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -51,8 +51,10 @@ dol=$(find "$scratch/extracted" -type f -path '*/sys/main.dol' -print -quit)
|
||||
rel=$(find "$scratch/extracted" -type f -path '*/files/rel/StaticR.rel' -print -quit)
|
||||
[[ -n "$dol" ]] || fail 'nodtool extraction did not contain sys/main.dol'
|
||||
[[ -n "$rel" ]] || fail 'nodtool extraction did not contain files/rel/StaticR.rel'
|
||||
[[ $(sha256 "$dol") == "$EXPECTED_DOL_SHA256" ]] || fail 'disc is not the supported clean PAL RMCP01 Mario Kart Wii image'
|
||||
[[ $(sha256 "$rel") == "$EXPECTED_REL_SHA256" ]] || fail 'disc has an unexpected StaticR.rel; use a clean PAL RMCP01 image'
|
||||
dol_sha=$(sha256 "$dol")
|
||||
rel_sha=$(sha256 "$rel")
|
||||
[[ $dol_sha == "$EXPECTED_DOL_SHA256" ]] || fail "main.dol does not match the clean PAL RMCP01 image (expected $EXPECTED_DOL_SHA256, got $dol_sha)"
|
||||
[[ $rel_sha == "$EXPECTED_REL_SHA256" ]] || fail "StaticR.rel does not match the clean PAL RMCP01 image (expected $EXPECTED_REL_SHA256, got $rel_sha)"
|
||||
|
||||
data_root=$(dirname "$(dirname "$dol")")
|
||||
[[ -d "$data_root/files" ]] || fail 'nodtool extraction did not contain the Wii files directory'
|
||||
|
||||
@@ -11,7 +11,13 @@ workspace="$support_root/BuildWorkspace"
|
||||
products="$support_root/Products"
|
||||
|
||||
fail() { printf 'WiiCompiled Setup: %s\n' "$*" >&2; exit 1; }
|
||||
notice() { /usr/bin/osascript -e "display dialog \"${1//\"/\\\"}\" buttons {\"OK\"} default button \"OK\" with icon caution" >/dev/null; }
|
||||
notice() {
|
||||
/usr/bin/osascript - "$1" >/dev/null <<'APPLESCRIPT'
|
||||
on run argv
|
||||
display dialog (item 1 of argv) buttons {"OK"} default button "OK" with icon caution
|
||||
end run
|
||||
APPLESCRIPT
|
||||
}
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: setup.command --game IMAGE [--retro-dir DIR] [--install-location {user|applications}]
|
||||
@@ -146,7 +152,7 @@ fi
|
||||
"$workspace/Launcher/local-build-macos.command" "${build_args[@]}" --profile "$profile" --cmake "$cmake_bin" --ninja "$ninja_bin" --translator-bin "$translator"
|
||||
|
||||
config="$support_root/Config.toml"
|
||||
toml_string() { printf '%s' "$1" | sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g'; }
|
||||
toml_string() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'; }
|
||||
set_path() {
|
||||
local key=$1 value=$2 encoded line temporary
|
||||
encoded=$(toml_string "$value"); line="$key = \"$encoded\""; temporary="$config.tmp"
|
||||
@@ -172,8 +178,13 @@ install_app() {
|
||||
local app=$1
|
||||
[[ -d "$products/$app" ]] || return 0
|
||||
if [[ "$destination" == /Applications ]]; then
|
||||
command="mkdir -p /Applications && rm -rf '/Applications/$app' && ditto '$products/$app' '/Applications/$app'"
|
||||
/usr/bin/osascript -e "do shell script \"$command\" with administrator privileges"
|
||||
/usr/bin/osascript - "$products/$app" "/Applications/$app" <<'APPLESCRIPT'
|
||||
on run argv
|
||||
set sourcePath to quoted form of (item 1 of argv)
|
||||
set installedPath to quoted form of (item 2 of argv)
|
||||
do shell script "mkdir -p /Applications && rm -rf " & installedPath & " && /usr/bin/ditto " & sourcePath & " " & installedPath with administrator privileges
|
||||
end run
|
||||
APPLESCRIPT
|
||||
else
|
||||
mkdir -p "$destination"; rm -rf "$destination/$app"; /usr/bin/ditto "$products/$app" "$destination/$app"
|
||||
fi
|
||||
|
||||
@@ -15,6 +15,7 @@ function(aurora_copy_runtime_dlls target)
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_RUNTIME_DLLS:${_t}> $<TARGET_FILE_DIR:${target}>
|
||||
COMMAND_EXPAND_LISTS
|
||||
VERBATIM
|
||||
)
|
||||
endforeach ()
|
||||
|
||||
@@ -30,6 +31,7 @@ function(aurora_copy_runtime_dlls target)
|
||||
if (EXISTS "${_dawn_bin_dir}/${_dll}")
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_dawn_bin_dir}/${_dll}" $<TARGET_FILE_DIR:${target}>
|
||||
VERBATIM
|
||||
)
|
||||
endif ()
|
||||
endforeach ()
|
||||
|
||||
@@ -164,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).
|
||||
|
||||
@@ -195,7 +195,7 @@ std::array<PADButtonMapping, PAD_BUTTON_COUNT> g_defaultButtonsJoyPair{{
|
||||
}};
|
||||
|
||||
// Wii U Pro Controllers through SDL's HIDAPI Wii driver. No SDL_GamepadType
|
||||
// singles them out, so they are picked by the name the driver gives them (see
|
||||
// singles them out, so they are picked by name or USB id (see
|
||||
// __PADSetDefaultMapping). Wii Remotes, with or without a Nunchuk or Classic
|
||||
// Controller, are read by the game through KPAD instead and never get a
|
||||
// GameCube mapping (the runtime hides those ports from PADRead).
|
||||
@@ -451,11 +451,14 @@ static void reset_alt_button_mapping(aurora::input::GameController* controller)
|
||||
|
||||
// SDL's hidapi Wii driver names the pad "Nintendo Wii U Pro Controller"; the
|
||||
// other names it produces are Wii Remotes, which the game reads through KPAD
|
||||
// and which therefore never take a GameCube mapping.
|
||||
// and which therefore never take a GameCube mapping. The Linux kernel driver's
|
||||
// "Nintendo Wii Remote Pro Controller" is only recognised by its USB id.
|
||||
static bool wii_default_mapping(const aurora::input::GameController* controller,
|
||||
std::array<PADButtonMapping, PAD_BUTTON_COUNT>& out) {
|
||||
const char* name = SDL_GetGamepadName(controller->m_controller);
|
||||
if (name == nullptr || SDL_strstr(name, "Wii U Pro Controller") == nullptr) {
|
||||
const bool wiiUProId = SDL_GetGamepadVendor(controller->m_controller) == 0x057e &&
|
||||
SDL_GetGamepadProduct(controller->m_controller) == 0x0330;
|
||||
if (!wiiUProId && (name == nullptr || SDL_strstr(name, "Wii U Pro Controller") == nullptr)) {
|
||||
return false;
|
||||
}
|
||||
out = g_defaultButtonsWiiUPro;
|
||||
|
||||
@@ -174,6 +174,12 @@ static std::string color_arg_reg(GXTevColorArg arg, size_t stageIdx, const Shade
|
||||
case GX_CC_KONST: {
|
||||
switch (stage.kcSel) {
|
||||
DEFAULT_FATAL("invalid kcSel {}", underlying(stage.kcSel));
|
||||
// Unsourced encodings evaluate to zero in hardware.
|
||||
case static_cast<GXTevKColorSel>(0x08):
|
||||
case static_cast<GXTevKColorSel>(0x09):
|
||||
case static_cast<GXTevKColorSel>(0x0A):
|
||||
case static_cast<GXTevKColorSel>(0x0B):
|
||||
return "vec3f(0.0)";
|
||||
case GX_TEV_KCSEL_8_8:
|
||||
return "vec3f(1.0)";
|
||||
case GX_TEV_KCSEL_7_8:
|
||||
@@ -275,6 +281,16 @@ static std::string alpha_arg_reg(GXTevAlphaArg arg, size_t stageIdx, const Shade
|
||||
case GX_CA_KONST: {
|
||||
switch (stage.kaSel) {
|
||||
DEFAULT_FATAL("invalid kaSel {}", underlying(stage.kaSel));
|
||||
// Unsourced encodings evaluate to zero in hardware.
|
||||
case static_cast<GXTevKAlphaSel>(0x08):
|
||||
case static_cast<GXTevKAlphaSel>(0x09):
|
||||
case static_cast<GXTevKAlphaSel>(0x0A):
|
||||
case static_cast<GXTevKAlphaSel>(0x0B):
|
||||
case static_cast<GXTevKAlphaSel>(0x0C):
|
||||
case static_cast<GXTevKAlphaSel>(0x0D):
|
||||
case static_cast<GXTevKAlphaSel>(0x0E):
|
||||
case static_cast<GXTevKAlphaSel>(0x0F):
|
||||
return "0.0";
|
||||
case GX_TEV_KASEL_8_8:
|
||||
return "1.0";
|
||||
case GX_TEV_KASEL_7_8:
|
||||
|
||||
@@ -893,9 +893,14 @@ void release_surface() noexcept {
|
||||
}
|
||||
g_surface = {};
|
||||
if (hadSurface && g_instance && g_device && g_queue) {
|
||||
// Retired swapchain is freed after a later submission completes and the device ticks.
|
||||
submit_staging_commands(g_device.CreateCommandEncoder().Finish());
|
||||
const auto future = g_queue.OnSubmittedWorkDone(
|
||||
wgpu::CallbackMode::WaitAnyOnly, [](wgpu::QueueWorkDoneStatus, wgpu::StringView) {});
|
||||
g_instance.WaitAny(future, 1000000000);
|
||||
if (const auto status = g_instance.WaitAny(future, 1000000000); status != wgpu::WaitStatus::Success) {
|
||||
Log.warn("Timed out waiting for queue idle on surface release: {}", magic_enum::enum_name(status));
|
||||
}
|
||||
g_device.Tick();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,12 @@ else()
|
||||
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS x86_64/arm64")
|
||||
endif()
|
||||
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
# CMake and Ninja write response files with Windows quoting, but clang tokenizes them with
|
||||
# POSIX rules by default, where a bare apostrophe in a path opens a quoted span.
|
||||
add_link_options(--rsp-quoting=windows)
|
||||
endif()
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
message(FATAL_ERROR "WiiCompiled only supports Release builds")
|
||||
endif()
|
||||
@@ -56,6 +62,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)
|
||||
|
||||
@@ -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)
|
||||
@@ -226,7 +229,7 @@ function(mkw_configure_product target)
|
||||
(MKW_SQLITE_TARGET_TYPE STREQUAL "SHARED_LIBRARY" OR
|
||||
MKW_SQLITE_TARGET_TYPE STREQUAL "MODULE_LIBRARY"))
|
||||
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
|
||||
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}> VERBATIM)
|
||||
endif()
|
||||
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
@@ -260,7 +263,7 @@ function(mkw_configure_product target)
|
||||
endif()
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${runtime_dll_path}" $<TARGET_FILE_DIR:${target}>)
|
||||
"${runtime_dll_path}" $<TARGET_FILE_DIR:${target}> VERBATIM)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
@@ -269,7 +272,7 @@ function(mkw_configure_product target)
|
||||
message(FATAL_ERROR "Missing Wii first-run bootstrap payload: ${MKW_WII_BOOTSTRAP_SOURCE_DIR}")
|
||||
endif()
|
||||
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${MKW_WII_BOOTSTRAP_SOURCE_DIR}" "$<TARGET_FILE_DIR:${target}>/wii_bootstrap")
|
||||
"${MKW_WII_BOOTSTRAP_SOURCE_DIR}" "$<TARGET_FILE_DIR:${target}>/wii_bootstrap" VERBATIM)
|
||||
|
||||
set(MKW_DSP_COEFFICIENT_ROM "${MKW_RUNTIME_SOURCE_DIR}/assets/dsp/dsp_coef.bin")
|
||||
if(NOT EXISTS "${MKW_DSP_COEFFICIENT_ROM}")
|
||||
@@ -281,7 +284,7 @@ function(mkw_configure_product target)
|
||||
message(FATAL_ERROR "Wii DSP coefficient ROM hash mismatch: ${MKW_DSP_COEFFICIENT_ROM_SHA256}")
|
||||
endif()
|
||||
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MKW_DSP_COEFFICIENT_ROM}" "$<TARGET_FILE_DIR:${target}>/dsp_coef.bin")
|
||||
"${MKW_DSP_COEFFICIENT_ROM}" "$<TARGET_FILE_DIR:${target}>/dsp_coef.bin" VERBATIM)
|
||||
|
||||
# Aurora imports this portable recipe database into each user's writable
|
||||
# pipeline cache. Keep the upstream filename so its default resourcesPath
|
||||
@@ -293,7 +296,7 @@ function(mkw_configure_product target)
|
||||
endif()
|
||||
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MKW_INITIAL_PIPELINE_CACHE}"
|
||||
"$<TARGET_FILE_DIR:${target}>/initial_pipeline_cache.db")
|
||||
"$<TARGET_FILE_DIR:${target}>/initial_pipeline_cache.db" VERBATIM)
|
||||
|
||||
# Non-Windows TLS (runtime/src/hle/net/network_ssl.cpp's mbed TLS path) needs a trusted root
|
||||
# CA bundle to verify server certificates against - Windows gets this for free from the OS via
|
||||
@@ -307,7 +310,7 @@ function(mkw_configure_product target)
|
||||
message(FATAL_ERROR "Missing TLS root CA bundle: ${MKW_CA_CERTIFICATE_BUNDLE}")
|
||||
endif()
|
||||
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MKW_CA_CERTIFICATE_BUNDLE}" "$<TARGET_FILE_DIR:${target}>/cacert.pem")
|
||||
"${MKW_CA_CERTIFICATE_BUNDLE}" "$<TARGET_FILE_DIR:${target}>/cacert.pem" VERBATIM)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
|
||||
@@ -69,10 +69,11 @@ struct RuntimeUserConfig {
|
||||
// SDL's sensor frame (x right, y out of the button face, z towards the user).
|
||||
// SDL's Wii driver falls back to a nominal zero point when its read of the
|
||||
// remote's calibration block times out (common over Bluetooth), so this is
|
||||
// measured in the overlay with the remote at rest.
|
||||
std::optional<double> wiiAccelOffsetX;
|
||||
std::optional<double> wiiAccelOffsetY;
|
||||
std::optional<double> wiiAccelOffsetZ;
|
||||
// measured in the overlay with the remote at rest. Stored per remote under
|
||||
// [controller.wii_accel_offsets]; the old shared
|
||||
// wii_accel_offset_* keys are only detected, to report that they are ignored.
|
||||
std::map<std::string, std::array<double, 3>> wiiAccelOffsets;
|
||||
bool hasLegacyWiiAccelOffset = false;
|
||||
// Debugging aid: append every KPAD sample of the Bluetooth remote (raw and
|
||||
// corrected accelerometer, buttons) to wii_accel_trace.csv next to Config.toml.
|
||||
std::optional<bool> wiiAccelTrace;
|
||||
@@ -138,28 +139,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();
|
||||
@@ -403,6 +382,28 @@ inline void AppendOverlayRoots(RuntimeUserConfig& config, const std::string& roo
|
||||
}
|
||||
|
||||
// Reads every supported setting out of a parsed Config.toml document.
|
||||
// [x, y, z] of finite numbers (integers allowed), else nullopt.
|
||||
inline std::optional<std::array<double, 3>> ParseAccelOffset(const toml::value& value) {
|
||||
if (!value.is_array() || value.as_array().size() != 3) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::array<double, 3> offset{};
|
||||
for (size_t i = 0; i < 3; ++i) {
|
||||
const toml::value& element = value.as_array()[i];
|
||||
if (element.is_floating()) {
|
||||
offset[i] = element.as_floating();
|
||||
} else if (element.is_integer()) {
|
||||
offset[i] = static_cast<double>(element.as_integer());
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!std::isfinite(offset[i])) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
RuntimeUserConfig config;
|
||||
|
||||
@@ -426,6 +427,13 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
config.controllerExpressions[key] = value.as_string();
|
||||
}
|
||||
}
|
||||
if (section->contains("wii_accel_offsets") && section->at("wii_accel_offsets").is_table()) {
|
||||
for (const auto& [key, value] : section->at("wii_accel_offsets").as_table()) {
|
||||
if (const auto offset = ParseAccelOffset(value)) {
|
||||
config.wiiAccelOffsets[key] = *offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
|
||||
@@ -487,9 +495,9 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
FindConfigValue<bool>(document, "audio", "attenuate_music_when_media_plays");
|
||||
config.wiiRemotes = FindConfigValue<bool>(document, "controller", "wii_remotes");
|
||||
config.wiiContinuousScan = FindConfigValue<bool>(document, "controller", "wii_continuous_scan");
|
||||
config.wiiAccelOffsetX = FindConfigValue<double>(document, "controller", "wii_accel_offset_x");
|
||||
config.wiiAccelOffsetY = FindConfigValue<double>(document, "controller", "wii_accel_offset_y");
|
||||
config.wiiAccelOffsetZ = FindConfigValue<double>(document, "controller", "wii_accel_offset_z");
|
||||
for (const char* key : {"wii_accel_offset_x", "wii_accel_offset_y", "wii_accel_offset_z"}) {
|
||||
config.hasLegacyWiiAccelOffset |= FindConfigValue<double>(document, "controller", key).value_or(0.0) != 0.0;
|
||||
}
|
||||
config.wiiAccelTrace = FindConfigValue<bool>(document, "controller", "wii_accel_trace");
|
||||
config.networkEnabled = FindConfigValue<bool>(document, "network", "enabled");
|
||||
config.discordPresenceEnabled = FindConfigValue<bool>(document, "discord", "enabled");
|
||||
@@ -546,68 +554,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 +609,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);
|
||||
}
|
||||
|
||||
@@ -884,12 +872,13 @@ inline bool SetWiiContinuousScanEnabled(bool value) {
|
||||
return WriteSetting("controller", "wii_continuous_scan", value ? "true" : "false");
|
||||
}
|
||||
|
||||
// Wii Remote accelerometer zero-point correction (g, SDL sensor frame); all zero
|
||||
// when the remote has not been calibrated.
|
||||
inline std::array<double, 3> WiiAccelOffset() {
|
||||
const RuntimeUserConfig& config = Get();
|
||||
return {config.wiiAccelOffsetX.value_or(0.0), config.wiiAccelOffsetY.value_or(0.0),
|
||||
config.wiiAccelOffsetZ.value_or(0.0)};
|
||||
// Stored correction of the remote identified by `remoteKey`, if it has one.
|
||||
inline std::optional<std::array<double, 3>> WiiAccelOffsetFor(const std::string& remoteKey) {
|
||||
const auto it = Get().wiiAccelOffsets.find(remoteKey);
|
||||
if (it == Get().wiiAccelOffsets.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Whether to write the per-frame accelerometer trace (off unless asked for).
|
||||
@@ -897,26 +886,14 @@ inline bool WiiAccelTraceEnabled(bool fallback = false) {
|
||||
return Get().wiiAccelTrace.value_or(fallback);
|
||||
}
|
||||
|
||||
// True while a non-zero correction is stored ("Clear calibration" writes zeros).
|
||||
inline bool HasWiiAccelOffset() {
|
||||
const std::array<double, 3> offset = WiiAccelOffset();
|
||||
return offset[0] != 0.0 || offset[1] != 0.0 || offset[2] != 0.0;
|
||||
}
|
||||
|
||||
// Persists the accelerometer correction measured by the overlay's calibration.
|
||||
inline bool SetWiiAccelOffset(const std::array<double, 3>& offset) {
|
||||
Mutable().wiiAccelOffsetX = offset[0];
|
||||
Mutable().wiiAccelOffsetY = offset[1];
|
||||
Mutable().wiiAccelOffsetZ = offset[2];
|
||||
bool ok = true;
|
||||
const char* keys[3] = {"wii_accel_offset_x", "wii_accel_offset_y", "wii_accel_offset_z"};
|
||||
for (size_t i = 0; i < 3; ++i) {
|
||||
// Always a float literal, so a whole-number offset does not come back as a TOML integer.
|
||||
std::ostringstream formatted;
|
||||
formatted << std::fixed << std::setprecision(4) << offset[i];
|
||||
ok = WriteSetting("controller", keys[i], formatted.str()) && ok;
|
||||
}
|
||||
return ok;
|
||||
// Persists the accelerometer correction of one remote. `remoteKey` must be a bare TOML key.
|
||||
inline bool SetWiiAccelOffsetFor(const std::string& remoteKey, const std::array<double, 3>& offset) {
|
||||
Mutable().wiiAccelOffsets[remoteKey] = offset;
|
||||
// Always float literals, so a whole-number offset does not come back as a TOML integer.
|
||||
std::ostringstream formatted;
|
||||
formatted << std::fixed << std::setprecision(4) << '[' << offset[0] << ", " << offset[1] << ", " << offset[2]
|
||||
<< ']';
|
||||
return WriteSetting("controller.wii_accel_offsets", remoteKey, formatted.str());
|
||||
}
|
||||
|
||||
// Target frame rate for frame interpolation, or 0 to disable it.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
struct PADStatus;
|
||||
|
||||
@@ -73,23 +75,20 @@ 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, stored per
|
||||
// physical remote under [controller.wii_accel_offsets]. Expects the remote lying
|
||||
// still, buttons up; Poll() averages about a second and stores it for that remote.
|
||||
void StartAccelCalibration(uint32_t chan);
|
||||
// Forgets the stored correction.
|
||||
void ClearAccelCalibration();
|
||||
bool IsAccelCalibrating();
|
||||
// Forgets the stored correction of the remote on `chan`.
|
||||
void ClearAccelCalibration(uint32_t chan);
|
||||
bool IsAccelCalibrating(uint32_t chan);
|
||||
// 0..1 while a calibration is collecting samples.
|
||||
float AccelCalibrationProgress();
|
||||
// Outcome of the last calibration attempt for the overlay, or nullptr.
|
||||
const char* AccelCalibrationMessage();
|
||||
// Correction in effect for the remote on `chan` (zeros when uncalibrated), or
|
||||
// nullopt when the port has no remote.
|
||||
std::optional<std::array<double, 3>> ReadAccelOffset(uint32_t chan);
|
||||
// Outcome of the last calibration attempt on `chan` for the overlay, or nullptr.
|
||||
const char* AccelCalibrationMessage(uint32_t chan);
|
||||
|
||||
// Reports "no controller" on the GameCube side for every port served through
|
||||
// KPAD, so the game never sees the same remote twice. Runs after aurora's PADRead.
|
||||
|
||||
@@ -89,6 +89,11 @@ void WritePadStatus(uint32_t base, const PADStatus& status) {
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" bool PAD_HLE_RumbleEnabled()
|
||||
{
|
||||
return g_rumbleEnabled.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
extern "C" void PAD_HLE_SetRumbleEnabled(bool enabled)
|
||||
{
|
||||
g_rumbleEnabled.store(enabled, std::memory_order_relaxed);
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <dolphin/pad.h>
|
||||
|
||||
extern "C" bool PAD_HLE_RumbleEnabled();
|
||||
void NandQueueIosCallback(uint32_t callbackPtr, int32_t result, uint32_t callbackArg);
|
||||
|
||||
namespace {
|
||||
@@ -143,8 +146,8 @@ PPC_NATIVE_OVERRIDE(801C0990, WPADProbe_HLE, int32_t, (uint32_t chan, uint32_t t
|
||||
|
||||
extern "C" void WPADControlMotor_HLE(uint32_t chan, uint32_t command)
|
||||
{
|
||||
(void)chan;
|
||||
(void)command;
|
||||
const bool on = command != 0 && PAD_HLE_RumbleEnabled();
|
||||
PADControlMotor(chan, on ? PAD_MOTOR_RUMBLE : PAD_MOTOR_STOP_HARD);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C0EC4, WPADControlMotor_HLE, void, (uint32_t chan, uint32_t command), (chan, command));
|
||||
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -309,10 +309,9 @@ 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.
|
||||
if (WiiRemoteInput::IsAccelCalibrating()) {
|
||||
// Residual per-axis bias left after SDL's factory-calibrated decode, measured
|
||||
// here with the remote at rest.
|
||||
if (WiiRemoteInput::IsAccelCalibrating(port)) {
|
||||
ImGui::ProgressBar(WiiRemoteInput::AccelCalibrationProgress(), ImVec2(220.0f, 0.0f), "Hold still...");
|
||||
} else if (ImGui::Button("Calibrate (remote lying flat, buttons up)")) {
|
||||
WiiRemoteInput::StartAccelCalibration(port);
|
||||
@@ -322,18 +321,20 @@ void DrawWiiRemoteAccelerometer(uint32_t port) {
|
||||
"for about two seconds. Corrects the steering offset of a remote held sideways.");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(!RuntimeConfigFile::HasWiiAccelOffset() || WiiRemoteInput::IsAccelCalibrating());
|
||||
const auto stored = WiiRemoteInput::ReadAccelOffset(port);
|
||||
const std::array<double, 3> offset = stored.value_or(std::array<double, 3>{});
|
||||
const bool calibrated = offset[0] != 0.0 || offset[1] != 0.0 || offset[2] != 0.0;
|
||||
ImGui::BeginDisabled(!calibrated || WiiRemoteInput::IsAccelCalibrating(port));
|
||||
if (ImGui::Button("Clear calibration")) {
|
||||
WiiRemoteInput::ClearAccelCalibration();
|
||||
WiiRemoteInput::ClearAccelCalibration(port);
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (const char* message = WiiRemoteInput::AccelCalibrationMessage()) {
|
||||
if (const char* message = WiiRemoteInput::AccelCalibrationMessage(port)) {
|
||||
ImGui::TextWrapped("%s", message);
|
||||
} else if (RuntimeConfigFile::HasWiiAccelOffset()) {
|
||||
const std::array<double, 3> offset = RuntimeConfigFile::WiiAccelOffset();
|
||||
} else if (calibrated) {
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,6 +791,12 @@ void DrawControllerSettings() {
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (WiiRemoteInput::IsRemoteChannel(selectedGamePort)) {
|
||||
ImGui::TextDisabled("The game reads this controller as a Wii Remote, so the button mapping does not apply.");
|
||||
DrawRumbleSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t mappingCount = 0;
|
||||
PADButtonMapping* mappings = PADGetButtonMappings(static_cast<uint32_t>(g_controllerPort), &mappingCount);
|
||||
if (mappings == nullptr || mappingCount != PAD_BUTTON_COUNT) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <dolphin/pad.h>
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <SDL3/SDL_hidapi.h>
|
||||
#include <SDL3/SDL_hints.h>
|
||||
#include <SDL3/SDL_log.h>
|
||||
#include <SDL3/SDL_sensor.h>
|
||||
@@ -12,11 +13,16 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace WiiRemoteInput {
|
||||
@@ -63,6 +69,8 @@ constexpr uint32_t kClUp = 0x0001, kClLeft = 0x0002, kClZR = 0x0004, kClX = 0x00
|
||||
kClB = 0x0040, kClZL = 0x0080, kClR = 0x0200, kClPlus = 0x0400, kClHome = 0x0800,
|
||||
kClMinus = 0x1000, kClL = 0x2000, kClDown = 0x4000, kClRight = 0x8000;
|
||||
|
||||
constexpr Uint16 kNintendoVendor = 0x057e;
|
||||
|
||||
// How long a vanished remote keeps its channel alive with neutral input. SDL's
|
||||
// in-place reconnect after an extension change takes well under a second; a
|
||||
// remote that is really gone shows up as disconnected after this.
|
||||
@@ -86,36 +94,46 @@ bool g_scanning = false;
|
||||
uint64_t g_driverOffSinceMs = 0;
|
||||
// When the current scan started (last Wii controller seen).
|
||||
uint64_t g_lostAtMs = 0;
|
||||
bool g_hidrawWarned = false;
|
||||
|
||||
// Instance ids whose accelerometers have been switched on. SDL keeps sensors
|
||||
// off until asked and forgets that when the gamepad is closed, so a re-paired
|
||||
// remote gets a fresh id and is enabled again.
|
||||
std::array<SDL_JoystickID, PAD_MAX_CONTROLLERS> g_sensorsEnabledFor{};
|
||||
|
||||
// Zero-point correction subtracted from the remote's accelerometer, in g and in
|
||||
// SDL's sensor frame; loaded from Config.toml on first use, replaced by a
|
||||
// calibration run. The Nunchuk accelerometer is left uncorrected.
|
||||
std::array<float, 3> g_accelOffset{};
|
||||
bool g_accelOffsetLoaded = false;
|
||||
// Zero-point correction subtracted from a remote's accelerometer, in g and in
|
||||
// SDL's sensor frame, with the config key it is stored under. Resolved once per
|
||||
// gamepad instance (a re-created joystick of the same remote resolves to the same
|
||||
// key) and replaced by a calibration run. The Nunchuk accelerometer is left
|
||||
// uncorrected.
|
||||
struct RemoteCalibration {
|
||||
SDL_JoystickID instance = 0;
|
||||
std::string key;
|
||||
std::array<float, 3> offset{};
|
||||
};
|
||||
std::array<RemoteCalibration, PAD_MAX_CONTROLLERS> g_remoteCalibration{};
|
||||
|
||||
// Frames sampled by a calibration run (about 1.5 s at 60 Hz) and how far a
|
||||
// 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;
|
||||
uint32_t chan = 0;
|
||||
SDL_JoystickID instance = 0;
|
||||
int count = 0;
|
||||
double sum[3] = {};
|
||||
float first[3] = {};
|
||||
};
|
||||
AccelCalibration g_calibration;
|
||||
char g_calibrationMessage[160] = {};
|
||||
uint32_t g_calibrationMessageChan = 0;
|
||||
SDL_JoystickID g_calibrationMessageRemote = 0;
|
||||
|
||||
// Last accepted KPAD acc per port, for the remote and for the Nunchuk. Reused on
|
||||
// frames that bring no sample or a glitched one, so the game never sees a jump.
|
||||
@@ -155,14 +173,73 @@ void EnsureSensors(SDL_Gamepad* gamepad, uint32_t port) {
|
||||
}
|
||||
}
|
||||
|
||||
// Loads the stored zero-point correction once.
|
||||
const std::array<float, 3>& AccelOffset() {
|
||||
if (!g_accelOffsetLoaded) {
|
||||
const std::array<double, 3> stored = RuntimeConfigFile::WiiAccelOffset();
|
||||
for (size_t i = 0; i < 3; ++i) g_accelOffset[i] = static_cast<float>(stored[i]);
|
||||
g_accelOffsetLoaded = true;
|
||||
// USB interface number of the HID device at `path`, or -1 when it is not a USB
|
||||
// interface or cannot be found. A DolphinBar shows each remote slot as one.
|
||||
int UsbInterfaceForPath(const char* path, Uint16 vendor, Uint16 product) {
|
||||
if (path == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
return g_accelOffset;
|
||||
int interfaceNumber = -1;
|
||||
SDL_hid_device_info* list = SDL_hid_enumerate(vendor, product);
|
||||
for (const SDL_hid_device_info* info = list; info != nullptr; info = info->next) {
|
||||
if (info->path != nullptr && std::strcmp(info->path, path) == 0) {
|
||||
interfaceNumber = info->interface_number;
|
||||
break;
|
||||
}
|
||||
}
|
||||
SDL_hid_free_enumeration(list);
|
||||
return interfaceNumber;
|
||||
}
|
||||
|
||||
// Config key identifying the physical remote behind a gamepad: its HID serial
|
||||
// (the Bluetooth address when paired directly), else its USB interface number
|
||||
// (a DolphinBar has no serial and one interface per remote slot) and the game
|
||||
// port, else the game port. Only characters valid in a bare TOML key.
|
||||
std::string RemoteKey(SDL_Gamepad* gamepad, uint32_t port) {
|
||||
std::string serial;
|
||||
if (const char* raw = SDL_GetGamepadSerial(gamepad)) {
|
||||
for (const char* c = raw; *c != '\0'; ++c) {
|
||||
if (std::isalnum(static_cast<unsigned char>(*c))) {
|
||||
serial += static_cast<char>(std::tolower(static_cast<unsigned char>(*c)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (serial.find_first_not_of('0') != std::string::npos) {
|
||||
return "serial_" + serial;
|
||||
}
|
||||
const Uint16 vendor = SDL_GetGamepadVendor(gamepad);
|
||||
const Uint16 product = SDL_GetGamepadProduct(gamepad);
|
||||
const int interfaceNumber = UsbInterfaceForPath(SDL_GetGamepadPath(gamepad), vendor, product);
|
||||
char key[48];
|
||||
if (interfaceNumber >= 0) {
|
||||
std::snprintf(key, sizeof(key), "usb_%04x_%04x_if%d_port%u", vendor, product, interfaceNumber,
|
||||
port + 1);
|
||||
} else {
|
||||
std::snprintf(key, sizeof(key), "port_%u", port + 1);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
// Calibration of the remote currently on `port`, resolved on first use per gamepad
|
||||
// instance. A remote without a stored entry has a zero offset.
|
||||
RemoteCalibration& CalibrationFor(SDL_Gamepad* gamepad, uint32_t port) {
|
||||
RemoteCalibration& calibration = g_remoteCalibration[port];
|
||||
const SDL_JoystickID id = SDL_GetGamepadID(gamepad);
|
||||
if (calibration.instance != id) {
|
||||
static bool s_legacyReported = false;
|
||||
if (!s_legacyReported && RuntimeConfigFile::Get().hasLegacyWiiAccelOffset) {
|
||||
s_legacyReported = true;
|
||||
RT_LOG(RT_TAG_CONFIG) << "Old shared Wii Remote calibration ignored; recalibrate each remote in F10"
|
||||
<< std::endl;
|
||||
}
|
||||
calibration.instance = id;
|
||||
calibration.key = RemoteKey(gamepad, port);
|
||||
const std::optional<std::array<double, 3>> stored = RuntimeConfigFile::WiiAccelOffsetFor(calibration.key);
|
||||
for (size_t i = 0; i < 3; ++i) calibration.offset[i] = stored ? static_cast<float>((*stored)[i]) : 0.0f;
|
||||
RT_LOG(RT_TAG_CONFIG) << "Wii Remote on port " << (port + 1) << ": calibration key " << calibration.key
|
||||
<< (stored ? "" : " (no stored offset)") << std::endl;
|
||||
}
|
||||
return calibration;
|
||||
}
|
||||
|
||||
// Raw SDL sample in m/s^2, rejecting anything SDL has not delivered yet.
|
||||
@@ -200,9 +277,10 @@ bool IsGlitchedSample(SDL_SensorType sensor, const float* g) {
|
||||
// +x left, +y towards the user, +z out of the button face (wiibrew, Dolphin);
|
||||
// SDL_hidapi_wii.c posts (-wiiX, wiiZ, wiiY): x right across the face, y out of
|
||||
// the face (+1 at rest, buttons up), z towards the user, i.e. away from the tip.
|
||||
// The remote's sample gets the zero-point correction; the Nunchuk's does not.
|
||||
// False when there is no sample yet or the sample is a glitch (see above).
|
||||
bool ReadAccelG(SDL_Gamepad* gamepad, SDL_SensorType sensor, float* g) {
|
||||
// The remote's sample gets the zero-point correction of the remote on `port`; the
|
||||
// Nunchuk's does not. False when there is no sample yet or the sample is a glitch
|
||||
// (see above).
|
||||
bool ReadAccelG(SDL_Gamepad* gamepad, uint32_t port, SDL_SensorType sensor, float* g) {
|
||||
float sdl[3] = {};
|
||||
if (!ReadSdlAccel(gamepad, sensor, sdl)) {
|
||||
return false;
|
||||
@@ -212,7 +290,7 @@ bool ReadAccelG(SDL_Gamepad* gamepad, SDL_SensorType sensor, float* g) {
|
||||
return false;
|
||||
}
|
||||
if (sensor == SDL_SENSOR_ACCEL) {
|
||||
const std::array<float, 3>& offset = AccelOffset();
|
||||
const std::array<float, 3>& offset = CalibrationFor(gamepad, port).offset;
|
||||
for (int i = 0; i < 3; ++i) g[i] -= offset[i];
|
||||
}
|
||||
return true;
|
||||
@@ -231,9 +309,9 @@ void AccelGToKpad(const float* g, float* kpad) {
|
||||
}
|
||||
|
||||
// Sensor -> KPAD acc for ReadKpadSample.
|
||||
bool ReadAccelAsKpad(SDL_Gamepad* gamepad, SDL_SensorType sensor, float* kpad) {
|
||||
bool ReadAccelAsKpad(SDL_Gamepad* gamepad, uint32_t port, SDL_SensorType sensor, float* kpad) {
|
||||
float g[3] = {};
|
||||
if (!ReadAccelG(gamepad, sensor, g)) {
|
||||
if (!ReadAccelG(gamepad, port, sensor, g)) {
|
||||
return false;
|
||||
}
|
||||
AccelGToKpad(g, kpad);
|
||||
@@ -275,10 +353,25 @@ void TraceSample(uint32_t chan, const KpadSample& sample, const float* rawG, boo
|
||||
trace.flush();
|
||||
}
|
||||
|
||||
// Ends a calibration run with a message for the overlay.
|
||||
void FinishAccelCalibration(const char* message) {
|
||||
g_calibration.active = false;
|
||||
// Gamepad of the Wii Remote on `chan`, or nullptr.
|
||||
SDL_Gamepad* RemoteGamepad(uint32_t chan) {
|
||||
if (chan >= PAD_MAX_CONTROLLERS || !IsRemoteChannel(chan)) {
|
||||
return nullptr;
|
||||
}
|
||||
return SDL_GetGamepadFromPlayerIndex(static_cast<int>(chan));
|
||||
}
|
||||
|
||||
void SetCalibrationMessage(uint32_t chan, const char* message) {
|
||||
SDL_Gamepad* gamepad = RemoteGamepad(chan);
|
||||
g_calibrationMessageChan = chan;
|
||||
g_calibrationMessageRemote = gamepad != nullptr ? SDL_GetGamepadID(gamepad) : 0;
|
||||
std::snprintf(g_calibrationMessage, sizeof(g_calibrationMessage), "%s", message);
|
||||
}
|
||||
|
||||
// Ends a calibration run with a message for the overlay.
|
||||
void FinishAccelCalibration(uint32_t chan, const char* message) {
|
||||
g_calibration.active = false;
|
||||
SetCalibrationMessage(chan, message);
|
||||
RT_LOG(RT_TAG_CONFIG) << "Wii Remote accelerometer calibration: " << message << std::endl;
|
||||
}
|
||||
|
||||
@@ -288,9 +381,10 @@ void StepAccelCalibration() {
|
||||
if (!g_calibration.active) {
|
||||
return;
|
||||
}
|
||||
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(g_calibration.chan));
|
||||
if (gamepad == nullptr || !IsRemoteChannel(g_calibration.chan)) {
|
||||
FinishAccelCalibration("Cancelled: the Wii Remote went away.");
|
||||
SDL_Gamepad* gamepad = RemoteGamepad(g_calibration.chan);
|
||||
if (gamepad == nullptr ||
|
||||
SDL_GetGamepadID(gamepad) != g_calibration.instance) {
|
||||
FinishAccelCalibration(g_calibration.chan, "Cancelled: the Wii Remote went away.");
|
||||
return;
|
||||
}
|
||||
EnsureSensors(gamepad, g_calibration.chan);
|
||||
@@ -308,7 +402,7 @@ void StepAccelCalibration() {
|
||||
} else {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (std::fabs(g[i] - g_calibration.first[i]) > kCalibrationMaxDeviationG) {
|
||||
FinishAccelCalibration("Failed: the remote moved. Put it down, buttons up, and try again.");
|
||||
FinishAccelCalibration(g_calibration.chan, "Failed: the remote moved. Put it down, buttons up, and try again.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -320,19 +414,20 @@ 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) {
|
||||
FinishAccelCalibration("Failed: the remote was not resting flat with the buttons up.");
|
||||
if (length < kCalibrationMinGravityG || length > kCalibrationMaxGravityG ||
|
||||
mean[1] / length < kCalibrationMinTiltCosine) {
|
||||
FinishAccelCalibration(g_calibration.chan, "Failed: the remote was not resting flat with the buttons up.");
|
||||
return;
|
||||
}
|
||||
const std::array<double, 3> offset = {mean[0], mean[1] - 1.0, mean[2]};
|
||||
for (int i = 0; i < 3; ++i) g_accelOffset[i] = static_cast<float>(offset[i]);
|
||||
g_accelOffsetLoaded = true;
|
||||
const bool saved = RuntimeConfigFile::SetWiiAccelOffset(offset);
|
||||
RemoteCalibration& calibration = CalibrationFor(gamepad, g_calibration.chan);
|
||||
for (int i = 0; i < 3; ++i) calibration.offset[i] = static_cast<float>(offset[i]);
|
||||
const bool saved = RuntimeConfigFile::SetWiiAccelOffsetFor(calibration.key, offset);
|
||||
char message[160];
|
||||
std::snprintf(message, sizeof(message), "%s offset x %+.3f y %+.3f z %+.3f g",
|
||||
saved ? "Calibrated:" : "Calibrated (could not write Config.toml):", offset[0], offset[1],
|
||||
offset[2]);
|
||||
FinishAccelCalibration(message);
|
||||
FinishAccelCalibration(g_calibration.chan, message);
|
||||
}
|
||||
|
||||
// True when any controller aurora knows about is a Wii device.
|
||||
@@ -346,6 +441,110 @@ bool AnyWiiControllerConnected() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// True for an evdev node served by the kernel hid-wiimote driver.
|
||||
bool IsKernelWiiNode(SDL_JoystickID id) {
|
||||
const char* path = SDL_GetGamepadPathForID(id);
|
||||
return SDL_GetGamepadVendorForID(id) == kNintendoVendor && path != nullptr &&
|
||||
std::strncmp(path, "/dev/input/", 11) == 0;
|
||||
}
|
||||
|
||||
bool IsClassicNodeName(const char* name) {
|
||||
return NameContains(name, "Classic Controller");
|
||||
}
|
||||
|
||||
// HID device behind a kernel evdev node; the remote and its extension nodes share it.
|
||||
std::string KernelParent(SDL_JoystickID id) {
|
||||
static std::unordered_map<SDL_JoystickID, std::string> s_parents;
|
||||
if (const auto it = s_parents.find(id); it != s_parents.end()) {
|
||||
return it->second;
|
||||
}
|
||||
const char* path = SDL_GetGamepadPathForID(id);
|
||||
if (path == nullptr) {
|
||||
return {};
|
||||
}
|
||||
std::error_code ec;
|
||||
const auto parent = std::filesystem::canonical(
|
||||
std::filesystem::path("/sys/class/input") / std::filesystem::path(path).filename() / "device" / "device", ec);
|
||||
if (ec) {
|
||||
return {};
|
||||
}
|
||||
return s_parents.emplace(id, parent.string()).first->second;
|
||||
}
|
||||
|
||||
// The kernel node on the same remote as `gamepad`: its Classic Controller node
|
||||
// when `wantClassic`, else its remote node.
|
||||
SDL_Gamepad* KernelSibling(SDL_Gamepad* gamepad, bool wantClassic) {
|
||||
const SDL_JoystickID self = SDL_GetGamepadID(gamepad);
|
||||
const std::string parent = KernelParent(self);
|
||||
if (parent.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
int count = 0;
|
||||
SDL_JoystickID* ids = SDL_GetGamepads(&count);
|
||||
if (ids == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
SDL_Gamepad* sibling = nullptr;
|
||||
for (int i = 0; i < count && sibling == nullptr; ++i) {
|
||||
if (ids[i] != self && IsKernelWiiNode(ids[i]) &&
|
||||
IsClassicNodeName(SDL_GetGamepadNameForID(ids[i])) == wantClassic && KernelParent(ids[i]) == parent) {
|
||||
sibling = SDL_GetGamepadFromID(ids[i]);
|
||||
}
|
||||
}
|
||||
SDL_free(ids);
|
||||
return sibling;
|
||||
}
|
||||
|
||||
// The kernel exposes a remote and its Classic Controller as separate gamepads.
|
||||
// The remote's port reads both, as on a Wii; the Classic node's own port stays empty.
|
||||
Kind KindForGamepad(SDL_Gamepad* gamepad) {
|
||||
if (gamepad == nullptr) {
|
||||
return Kind::NotWii;
|
||||
}
|
||||
const char* name = SDL_GetGamepadName(gamepad);
|
||||
const Kind kind = KindForName(name);
|
||||
if ((kind != Kind::Remote && kind != Kind::RemoteWithClassic) || !IsKernelWiiNode(SDL_GetGamepadID(gamepad))) {
|
||||
return kind;
|
||||
}
|
||||
if (IsClassicNodeName(name)) {
|
||||
return KernelSibling(gamepad, false) != nullptr ? Kind::NotWii : kind;
|
||||
}
|
||||
return KernelSibling(gamepad, true) != nullptr ? Kind::RemoteWithClassic : kind;
|
||||
}
|
||||
|
||||
// A kernel Classic Controller node whose remote node carries it.
|
||||
bool IsCarriedKernelNode(SDL_Gamepad* gamepad) {
|
||||
return gamepad != nullptr && IsKernelWiiNode(SDL_GetGamepadID(gamepad)) &&
|
||||
IsClassicNodeName(SDL_GetGamepadName(gamepad)) && KernelSibling(gamepad, false) != nullptr;
|
||||
}
|
||||
|
||||
// Logs once when the kernel driver serves a Wii controller SDL's driver could have had.
|
||||
void WarnIfKernelDriverServing() {
|
||||
if (g_hidrawWarned || !g_wiiDriverEnabled) {
|
||||
return;
|
||||
}
|
||||
const uint64_t now = SDL_GetTicks();
|
||||
static uint64_t s_nextCheckMs = 0;
|
||||
if (now < s_nextCheckMs) {
|
||||
return;
|
||||
}
|
||||
s_nextCheckMs = now + 2000;
|
||||
int count = 0;
|
||||
SDL_JoystickID* ids = SDL_GetGamepads(&count);
|
||||
if (ids == nullptr) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < count && !g_hidrawWarned; ++i) {
|
||||
const Uint16 product = SDL_GetGamepadProductForID(ids[i]);
|
||||
if ((product == 0x0306 || product == 0x0330) && IsKernelWiiNode(ids[i])) {
|
||||
g_hidrawWarned = true;
|
||||
RT_LOG(RT_TAG_CONFIG) << "A Wii controller is served by the kernel hid-wiimote driver. For full Wii "
|
||||
<< "Remote support see the Linux udev rule in the README." << std::endl;
|
||||
}
|
||||
}
|
||||
SDL_free(ids);
|
||||
}
|
||||
|
||||
// Route SDL's input diagnostics (HIDAPI open failures, the Wii driver's
|
||||
// extension/status messages) into console.log, minus the periodic chatter.
|
||||
// A sub-warning message is written once: SDL repeats the same line on every
|
||||
@@ -464,6 +663,7 @@ void Poll() {
|
||||
for (uint32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
||||
(void)EffectiveKind(port);
|
||||
}
|
||||
WarnIfKernelDriverServing();
|
||||
if (!g_wiiDriverEnabled) {
|
||||
return;
|
||||
}
|
||||
@@ -518,11 +718,13 @@ uint32_t ScanCount() {
|
||||
return g_scanCount;
|
||||
}
|
||||
|
||||
// Maps the gamepad name SDL's Wii driver reports to a Kind.
|
||||
// Maps the gamepad name SDL reports to a Kind: "Nintendo Wii Remote",
|
||||
// "... with Nunchuk", "... with Classic Controller" and "Nintendo Wii U Pro
|
||||
// Controller" from SDL's Wii driver; the Linux kernel's "Nintendo Wii Remote
|
||||
// Classic Controller" is read like the Classic Controller.
|
||||
Kind KindForName(const char* name) {
|
||||
// Names come from SDL's hidapi Wii driver: "Nintendo Wii Remote",
|
||||
// "Nintendo Wii Remote with Nunchuk", "Nintendo Wii Remote with Classic
|
||||
// Controller" and "Nintendo Wii U Pro Controller".
|
||||
// The kernel's Pro Controller is a plain gamepad with a built-in SDL mapping.
|
||||
if (NameContains(name, "Wii Remote Pro Controller")) return Kind::NotWii;
|
||||
if (NameContains(name, "Wii U Pro Controller")) return Kind::WiiUPro;
|
||||
if (!NameContains(name, "Wii Remote")) return Kind::NotWii;
|
||||
if (NameContains(name, "Nunchuk")) return Kind::RemoteWithNunchuk;
|
||||
@@ -534,8 +736,7 @@ Kind KindForName(const char* name) {
|
||||
Kind KindForPort(uint32_t port) {
|
||||
if (port >= PAD_MAX_CONTROLLERS) return Kind::NotWii;
|
||||
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(port));
|
||||
if (gamepad == nullptr) return Kind::NotWii;
|
||||
return KindForName(SDL_GetGamepadName(gamepad));
|
||||
return KindForGamepad(gamepad);
|
||||
}
|
||||
|
||||
// Human-readable name of a Kind for the settings overlay.
|
||||
@@ -561,7 +762,7 @@ Kind EffectiveKind(uint32_t chan) {
|
||||
if (chan >= PAD_MAX_CONTROLLERS) return Kind::NotWii;
|
||||
PortMemory& memory = g_ports[chan];
|
||||
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(chan));
|
||||
const Kind live = gamepad != nullptr ? KindForName(SDL_GetGamepadName(gamepad)) : Kind::NotWii;
|
||||
const Kind live = KindForGamepad(gamepad);
|
||||
const uint64_t now = SDL_GetTicks();
|
||||
if (live != Kind::NotWii) {
|
||||
memory.lastKind = live;
|
||||
@@ -587,7 +788,8 @@ bool IsRemoteChannel(uint32_t chan) {
|
||||
// Marks KPAD-served ports as "no controller" in the GameCube pad statuses.
|
||||
void HideRemotesFromPad(PADStatus* statuses, uint32_t count) {
|
||||
for (uint32_t port = 0; port < count && port < PAD_MAX_CONTROLLERS; ++port) {
|
||||
if (IsRemoteChannel(port)) {
|
||||
if (IsRemoteChannel(port) ||
|
||||
IsCarriedKernelNode(SDL_GetGamepadFromPlayerIndex(static_cast<int>(port)))) {
|
||||
statuses[port] = {};
|
||||
statuses[port].err = PAD_ERR_NO_CONTROLLER;
|
||||
}
|
||||
@@ -602,24 +804,47 @@ 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) {
|
||||
const long value = invert ? -static_cast<long>(axis) : axis;
|
||||
return static_cast<int16_t>(std::clamp(std::lround(value / 64.0), -512L, 511L));
|
||||
}
|
||||
|
||||
// The kernel driver's evdev axes span SDL's full range; scaled to WPAD's.
|
||||
int16_t KernelClassicStickRaw(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));
|
||||
}
|
||||
|
||||
// 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.
|
||||
bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
|
||||
if (chan >= PAD_MAX_CONTROLLERS) {
|
||||
return false;
|
||||
}
|
||||
SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(chan));
|
||||
const Kind kind = gamepad != nullptr ? KindForName(SDL_GetGamepadName(gamepad)) : Kind::NotWii;
|
||||
const Kind kind = KindForGamepad(gamepad);
|
||||
if (!IsKpadKind(kind)) {
|
||||
const Kind remembered = EffectiveKind(chan);
|
||||
if (!IsKpadKind(remembered)) {
|
||||
@@ -635,26 +860,48 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
|
||||
EnsureSensors(gamepad, chan);
|
||||
|
||||
sample = {};
|
||||
const auto raw = [&](int index, uint32_t bit) {
|
||||
if (SDL_GetJoystickButton(joystick, index)) sample.hold |= bit;
|
||||
};
|
||||
raw(kRawA, kWpadA);
|
||||
raw(kRawB, kWpadB);
|
||||
raw(kRawOne, kWpadOne);
|
||||
raw(kRawTwo, kWpadTwo);
|
||||
raw(kRawPlus, kWpadPlus);
|
||||
raw(kRawMinus, kWpadMinus);
|
||||
raw(kRawHome, kWpadHome);
|
||||
raw(kRawDpadUp, kWpadUp);
|
||||
raw(kRawDpadDown, kWpadDown);
|
||||
raw(kRawDpadLeft, kWpadLeft);
|
||||
raw(kRawDpadRight, kWpadRight);
|
||||
// The kernel's evdev nodes have regular SDL mappings, so they are read as gamepads.
|
||||
const bool kernel = IsKernelWiiNode(SDL_GetGamepadID(gamepad));
|
||||
const bool classicNode = kernel && IsClassicNodeName(SDL_GetGamepadName(gamepad));
|
||||
if (kernel) {
|
||||
const auto mapped = [&](SDL_GamepadButton button, uint32_t bit) {
|
||||
if (SDL_GetGamepadButton(gamepad, button)) sample.hold |= bit;
|
||||
};
|
||||
if (!classicNode) {
|
||||
mapped(SDL_GAMEPAD_BUTTON_EAST, kWpadA);
|
||||
mapped(SDL_GAMEPAD_BUTTON_SOUTH, kWpadB);
|
||||
mapped(SDL_GAMEPAD_BUTTON_WEST, kWpadOne);
|
||||
mapped(SDL_GAMEPAD_BUTTON_NORTH, kWpadTwo);
|
||||
mapped(SDL_GAMEPAD_BUTTON_START, kWpadPlus);
|
||||
mapped(SDL_GAMEPAD_BUTTON_BACK, kWpadMinus);
|
||||
mapped(SDL_GAMEPAD_BUTTON_GUIDE, kWpadHome);
|
||||
mapped(SDL_GAMEPAD_BUTTON_DPAD_UP, kWpadUp);
|
||||
mapped(SDL_GAMEPAD_BUTTON_DPAD_DOWN, kWpadDown);
|
||||
mapped(SDL_GAMEPAD_BUTTON_DPAD_LEFT, kWpadLeft);
|
||||
mapped(SDL_GAMEPAD_BUTTON_DPAD_RIGHT, kWpadRight);
|
||||
}
|
||||
} else {
|
||||
const auto raw = [&](int index, uint32_t bit) {
|
||||
if (SDL_GetJoystickButton(joystick, index)) sample.hold |= bit;
|
||||
};
|
||||
raw(kRawA, kWpadA);
|
||||
raw(kRawB, kWpadB);
|
||||
raw(kRawOne, kWpadOne);
|
||||
raw(kRawTwo, kWpadTwo);
|
||||
raw(kRawPlus, kWpadPlus);
|
||||
raw(kRawMinus, kWpadMinus);
|
||||
raw(kRawHome, kWpadHome);
|
||||
raw(kRawDpadUp, kWpadUp);
|
||||
raw(kRawDpadDown, kWpadDown);
|
||||
raw(kRawDpadLeft, kWpadLeft);
|
||||
raw(kRawDpadRight, kWpadRight);
|
||||
}
|
||||
|
||||
float rawG[3] = {};
|
||||
const bool haveRaw = ReadSdlAccel(gamepad, SDL_SENSOR_ACCEL, rawG);
|
||||
for (float& v : rawG) v /= kStandardGravity;
|
||||
LastAcc& last = g_lastAcc[chan];
|
||||
const bool accepted = ReadAccelAsKpad(gamepad, SDL_SENSOR_ACCEL, sample.acc);
|
||||
const bool accepted = ReadAccelAsKpad(gamepad, chan, SDL_SENSOR_ACCEL, sample.acc);
|
||||
if (accepted) {
|
||||
last.valid = true;
|
||||
for (int i = 0; i < 3; ++i) last.acc[i] = sample.acc[i];
|
||||
@@ -674,8 +921,17 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
|
||||
// Home, the L/R clicks as shoulders, the D-pad as buttons 11-14 (never
|
||||
// through SDL's gamepad mapping, which expects a hat), ZL/ZR as the
|
||||
// trigger axes.
|
||||
const auto cl = [&](int index, uint32_t bit) {
|
||||
if (SDL_GetJoystickButton(joystick, index)) sample.clHold |= bit;
|
||||
SDL_Gamepad* classic = gamepad;
|
||||
if (kernel && !classicNode) {
|
||||
classic = KernelSibling(gamepad, true);
|
||||
if (classic == nullptr) {
|
||||
classic = gamepad;
|
||||
}
|
||||
}
|
||||
const auto cl = [&](SDL_GamepadButton button, uint32_t bit) {
|
||||
if (kernel ? SDL_GetGamepadButton(classic, button) : SDL_GetJoystickButton(joystick, button)) {
|
||||
sample.clHold |= bit;
|
||||
}
|
||||
};
|
||||
cl(SDL_GAMEPAD_BUTTON_EAST, kClA);
|
||||
cl(SDL_GAMEPAD_BUTTON_SOUTH, kClB);
|
||||
@@ -690,20 +946,29 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
|
||||
cl(SDL_GAMEPAD_BUTTON_DPAD_DOWN, kClDown);
|
||||
cl(SDL_GAMEPAD_BUTTON_DPAD_LEFT, kClLeft);
|
||||
cl(SDL_GAMEPAD_BUTTON_DPAD_RIGHT, kClRight);
|
||||
if (SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0) sample.clHold |= kClZL;
|
||||
if (SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > 0) sample.clHold |= kClZR;
|
||||
const Sint16 lx = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
|
||||
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);
|
||||
if (SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 0) sample.clHold |= kClZL;
|
||||
if (SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > 0) sample.clHold |= kClZR;
|
||||
const Sint16 lx = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_LEFTX);
|
||||
const Sint16 ly = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_LEFTY);
|
||||
const Sint16 rx = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_RIGHTX);
|
||||
const Sint16 ry = SDL_GetGamepadAxis(classic, SDL_GAMEPAD_AXIS_RIGHTY);
|
||||
if (kernel) {
|
||||
sample.clLStickRaw[0] = KernelClassicStickRaw(lx, false);
|
||||
sample.clLStickRaw[1] = KernelClassicStickRaw(ly, true);
|
||||
sample.clRStickRaw[0] = KernelClassicStickRaw(rx, false);
|
||||
sample.clRStickRaw[1] = KernelClassicStickRaw(ry, true);
|
||||
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);
|
||||
} else {
|
||||
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;
|
||||
@@ -718,7 +983,7 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
|
||||
sample.stick[1] = -static_cast<float>(SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY)) / 32767.0f;
|
||||
for (float& v : sample.stick) v = std::clamp(v, -1.0f, 1.0f);
|
||||
LastAcc& lastNunchuk = g_lastNunchukAcc[chan];
|
||||
if (ReadAccelAsKpad(gamepad, SDL_SENSOR_ACCEL_L, sample.nunchukAcc)) {
|
||||
if (ReadAccelAsKpad(gamepad, chan, SDL_SENSOR_ACCEL_L, sample.nunchukAcc)) {
|
||||
lastNunchuk.valid = true;
|
||||
for (int i = 0; i < 3; ++i) lastNunchuk.acc[i] = sample.nunchukAcc[i];
|
||||
} else {
|
||||
@@ -738,7 +1003,7 @@ bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]) {
|
||||
return false;
|
||||
}
|
||||
EnsureSensors(gamepad, chan);
|
||||
if (!ReadAccelG(gamepad, SDL_SENSOR_ACCEL, sdlG)) {
|
||||
if (!ReadAccelG(gamepad, chan, SDL_SENSOR_ACCEL, sdlG)) {
|
||||
return false;
|
||||
}
|
||||
AccelGToKpad(sdlG, kpadAcc);
|
||||
@@ -747,28 +1012,47 @@ bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]) {
|
||||
|
||||
// Begins collecting rest samples from the remote on `chan`.
|
||||
void StartAccelCalibration(uint32_t chan) {
|
||||
if (!IsRemoteChannel(chan)) {
|
||||
FinishAccelCalibration("No Wii Remote on this port.");
|
||||
SDL_Gamepad* gamepad = RemoteGamepad(chan);
|
||||
if (gamepad == nullptr) {
|
||||
FinishAccelCalibration(chan, "No Wii Remote on this port.");
|
||||
return;
|
||||
}
|
||||
g_calibration = {};
|
||||
g_calibration.active = true;
|
||||
g_calibration.chan = chan;
|
||||
g_calibration.instance = SDL_GetGamepadID(gamepad);
|
||||
g_calibrationMessage[0] = '\0';
|
||||
}
|
||||
|
||||
// Drops the stored correction and goes back to SDL's raw reading.
|
||||
void ClearAccelCalibration() {
|
||||
g_calibration.active = false;
|
||||
g_accelOffset = {};
|
||||
g_accelOffsetLoaded = true;
|
||||
RuntimeConfigFile::SetWiiAccelOffset({0.0, 0.0, 0.0});
|
||||
std::snprintf(g_calibrationMessage, sizeof(g_calibrationMessage), "Calibration cleared.");
|
||||
// Zeroes the stored correction of the remote on `chan`.
|
||||
void ClearAccelCalibration(uint32_t chan) {
|
||||
if (g_calibration.chan == chan) {
|
||||
g_calibration.active = false;
|
||||
}
|
||||
SDL_Gamepad* gamepad = RemoteGamepad(chan);
|
||||
if (gamepad == nullptr) {
|
||||
SetCalibrationMessage(chan, "No Wii Remote on this port.");
|
||||
return;
|
||||
}
|
||||
RemoteCalibration& calibration = CalibrationFor(gamepad, chan);
|
||||
calibration.offset = {};
|
||||
RuntimeConfigFile::SetWiiAccelOffsetFor(calibration.key, {0.0, 0.0, 0.0});
|
||||
SetCalibrationMessage(chan, "Calibration cleared.");
|
||||
}
|
||||
|
||||
// True while a calibration run is collecting samples.
|
||||
bool IsAccelCalibrating() {
|
||||
return g_calibration.active;
|
||||
// Correction in effect for the remote on `chan`, for the overlay.
|
||||
std::optional<std::array<double, 3>> ReadAccelOffset(uint32_t chan) {
|
||||
SDL_Gamepad* gamepad = RemoteGamepad(chan);
|
||||
if (gamepad == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const RemoteCalibration& calibration = CalibrationFor(gamepad, chan);
|
||||
return std::array<double, 3>{calibration.offset[0], calibration.offset[1], calibration.offset[2]};
|
||||
}
|
||||
|
||||
// True while a calibration run is collecting samples from `chan`.
|
||||
bool IsAccelCalibrating(uint32_t chan) {
|
||||
return g_calibration.active && g_calibration.chan == chan;
|
||||
}
|
||||
|
||||
// Share of the calibration samples collected so far, 0..1; 0 when idle.
|
||||
@@ -776,9 +1060,14 @@ float AccelCalibrationProgress() {
|
||||
return g_calibration.active ? static_cast<float>(g_calibration.count) / kCalibrationSamples : 0.0f;
|
||||
}
|
||||
|
||||
// Outcome of the last calibration run for the overlay, or nullptr before any.
|
||||
const char* AccelCalibrationMessage() {
|
||||
return g_calibrationMessage[0] != '\0' ? g_calibrationMessage : nullptr;
|
||||
// Outcome of the last calibration run on `chan` for the overlay, or nullptr.
|
||||
const char* AccelCalibrationMessage(uint32_t chan) {
|
||||
if (g_calibrationMessage[0] == '\0' || g_calibrationMessageChan != chan) {
|
||||
return nullptr;
|
||||
}
|
||||
SDL_Gamepad* gamepad = RemoteGamepad(chan);
|
||||
const SDL_JoystickID remote = gamepad != nullptr ? SDL_GetGamepadID(gamepad) : 0;
|
||||
return remote == g_calibrationMessageRemote ? g_calibrationMessage : nullptr;
|
||||
}
|
||||
|
||||
} // namespace WiiRemoteInput
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user