mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-10-21 05:52:09 -04:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3cc0840eb | |||
| ed6fea0efb | |||
| 95a49f3c89 | |||
| 60197f2e75 | |||
| b87186c96b | |||
| 0b12fc864a | |||
| 3d4b29d9b9 | |||
| 90589131b5 | |||
| bd219e38a3 | |||
| 196b48528e | |||
| 54c608ef9c | |||
| f3df63b529 |
@@ -47,6 +47,29 @@ jobs:
|
||||
- name: Test
|
||||
run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal
|
||||
|
||||
platform_tests:
|
||||
name: Host platform (${{ matrix.runner }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runner: [ubuntu-24.04, macos-14, macos-15-intel]
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure without graphics dependencies or game assets
|
||||
run: >-
|
||||
cmake -S runtime -B build-platform -G Ninja
|
||||
-DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
|
||||
-DMKW_BUILD_PRODUCTS=OFF -DMKW_PLATFORM_TESTS_ONLY=ON
|
||||
|
||||
- name: Build and run host-platform tests
|
||||
run: |
|
||||
cmake --build build-platform --parallel 3
|
||||
ctest --test-dir build-platform --output-on-failure
|
||||
|
||||
macos_substrate:
|
||||
name: macOS arm64 (configure + substrate tests)
|
||||
runs-on: macos-14
|
||||
|
||||
@@ -11,6 +11,11 @@ on:
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Package version
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -163,14 +168,17 @@ jobs:
|
||||
|
||||
- name: Build Setup.pkg
|
||||
env:
|
||||
PACKAGE_VERSION: ${{ inputs.version }}
|
||||
TAG_VERSION: ${{ github.ref_name }}
|
||||
shell: bash
|
||||
run: |
|
||||
package_version=""
|
||||
if [[ "${TAG_VERSION:-}" =~ ^v?[0-9] ]]; then
|
||||
package_version="${TAG_VERSION#v}"
|
||||
elif [[ -f "Launcher/Directory.Build.props" ]]; then
|
||||
package_version=$(grep -m1 '<Version>' Launcher/Directory.Build.props | sed -E 's/.*<Version>([^<]+)<\/Version>.*/\1/')
|
||||
package_version="$PACKAGE_VERSION"
|
||||
if [[ -z "$package_version" ]]; then
|
||||
if [[ "${TAG_VERSION:-}" =~ ^v?[0-9] ]]; then
|
||||
package_version="${TAG_VERSION#v}"
|
||||
elif [[ -f "Launcher/Directory.Build.props" ]]; then
|
||||
package_version=$(grep -m1 '<Version>' Launcher/Directory.Build.props | sed -E 's/.*<Version>([^<]+)<\/Version>.*/\1/')
|
||||
fi
|
||||
fi
|
||||
mkdir -p Launcher/dist
|
||||
Launcher/macos/build-setup-pkg.command \
|
||||
|
||||
@@ -45,6 +45,20 @@ jobs:
|
||||
shell: pwsh
|
||||
run: ./Launcher/Prepare-PortableTools.ps1
|
||||
|
||||
- name: Build and run Windows host-platform tests
|
||||
shell: pwsh
|
||||
run: |
|
||||
$tools = Join-Path $PWD 'Launcher/artifacts/portable-tools'
|
||||
$env:PATH = "$tools/llvm-mingw/bin;$tools/CMake/bin;$tools/Ninja;$env:PATH"
|
||||
cmake -S runtime -B build-platform -G Ninja `
|
||||
-DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ `
|
||||
-DMKW_BUILD_PRODUCTS=OFF -DMKW_PLATFORM_TESTS_ONLY=ON
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
cmake --build build-platform --parallel 3
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
ctest --test-dir build-platform --output-on-failure
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
- name: Prepare pinned native dependencies
|
||||
shell: pwsh
|
||||
run: ./Launcher/Prepare-Dependencies.ps1
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -184,18 +184,31 @@ internal sealed class BuildProgressWindow
|
||||
}
|
||||
|
||||
// Anything else - a plain MKWCBUILD note, or raw tool output - stays a diagnostic and only
|
||||
// feeds the heartbeat below.
|
||||
// feeds the compile progress below.
|
||||
_reporter.Diagnostic(line);
|
||||
// Compilation announces itself once and then emits thousands of compiler lines. Treat that
|
||||
// output as a heartbeat so the slice keeps creeping forward, but only publish a progress
|
||||
// line when the rounded percentage actually changes.
|
||||
if (_fraction >= CompileFraction)
|
||||
// Compilation announces itself once and then ninja prefixes every edge with [done/total].
|
||||
// Follow that counter rather than counting lines: how many lines a build prints depends on
|
||||
// whether aurora and its dependencies are prebuilt (AppImage) or compiled here (macOS), so a
|
||||
// fixed per-line step reached the top long before the game itself compiled.
|
||||
if (_fraction >= CompileFraction && TryParseNinjaProgress(line, out var done, out var total))
|
||||
{
|
||||
_fraction = Math.Min(0.97, _fraction + 0.0015);
|
||||
_fraction = Math.Max(_fraction, CompileFraction + (0.97 - CompileFraction) * done / total);
|
||||
Emit();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseNinjaProgress(string line, out int done, out int total)
|
||||
{
|
||||
done = total = 0;
|
||||
if (!line.StartsWith('[')) return false;
|
||||
var close = line.IndexOf(']');
|
||||
var slash = line.IndexOf('/');
|
||||
return close > 0 && slash > 0 && slash < close
|
||||
&& int.TryParse(line.AsSpan(1, slash - 1), out done)
|
||||
&& int.TryParse(line.AsSpan(slash + 1, close - slash - 1), out total)
|
||||
&& total > 0 && done <= total;
|
||||
}
|
||||
|
||||
private void Emit()
|
||||
{
|
||||
var percent = Interpolate(_fraction);
|
||||
|
||||
@@ -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
|
||||
|
||||
+11
-1
@@ -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
|
||||
|
||||
@@ -20,13 +20,14 @@ resulting pkg is unsigned unless
|
||||
Developer ID Installer certificate.
|
||||
|
||||
--workspace DIR Repository root (default: script's grandparent)
|
||||
--version VERSION Bundle/package version (default: 0.1.0)
|
||||
--version VERSION Bundle/package version (default: project version, or 0.1.0)
|
||||
--installer-identity NAME Developer ID Installer identity for productbuild
|
||||
EOF
|
||||
}
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
workspace=$(cd "$script_dir/../.." && pwd); nodtool_arm64=""; nodtool_x86_64=""; translator_arm64=""; translator_x86_64=""; cmake_root=""; ninja_arm64=""; ninja_x86_64=""; output=""; version=0.1.0; identity=""
|
||||
version_supplied=false
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--workspace) workspace=${2:-}; shift 2 ;;
|
||||
@@ -38,14 +39,14 @@ while (($#)); do
|
||||
--ninja-arm64) ninja_arm64=${2:-}; shift 2 ;;
|
||||
--ninja-x86_64) ninja_x86_64=${2:-}; shift 2 ;;
|
||||
--output) output=${2:-}; shift 2 ;;
|
||||
--version) version=${2:-}; shift 2 ;;
|
||||
--version) version=${2:-}; version_supplied=true; shift 2 ;;
|
||||
--installer-identity) identity=${2:-}; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
version=${version#v}
|
||||
if [[ -z "$version" || "$version" == "0.1.0" ]]; then
|
||||
if [[ "$version_supplied" == false ]]; then
|
||||
local_csproj="$workspace/Launcher/Directory.Build.props"
|
||||
if [[ -f "$local_csproj" ]]; then
|
||||
detected=$(grep -m1 '<Version>' "$local_csproj" | sed -E 's/.*<Version>([^<]+)<\/Version>.*/\1/' || true)
|
||||
@@ -150,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"
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<a href="#building-from-source"><img alt="PowerPC static recompilation" src="https://img.shields.io/badge/PowerPC-static%20recompilation-FF9F0A"></a>
|
||||
<a href="#retro-rewind"><img alt="Retro Rewind supported" src="https://img.shields.io/badge/Retro%20Rewind-supported-FF375F"></a>
|
||||
<a href="https://github.com/TeamWheelWizard/WheelWizard/releases"><img alt="Install with Wheel Wizard" src="https://img.shields.io/badge/install%20with-Wheel%20Wizard-8B5CF6"></a>
|
||||
<a href="https://github.com/patchzyy/Wiicompiled/releases"><img alt="Total downloads" src="https://img.shields.io/github/downloads/patchzyy/Wiicompiled/total?color=00B8D9&logo=github"></a>
|
||||
<a href="LICENSE"><img alt="License: GPLv3" src="https://img.shields.io/badge/license-GPLv3-2EA44F?logo=gnu&logoColor=white"></a>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -32,6 +32,52 @@ function(aurora_sdl3_apply_patches sdl_source_dir)
|
||||
if (NOT EXISTS "${_wii}")
|
||||
message(FATAL_ERROR "aurora: SDL3 source tree at ${sdl_source_dir} has no SDL_hidapi_wii.c")
|
||||
endif ()
|
||||
set(_rumble "${sdl_source_dir}/src/joystick/hidapi/SDL_hidapi_rumble.c")
|
||||
if (NOT EXISTS "${_rumble}")
|
||||
message(FATAL_ERROR "aurora: SDL3 source tree at ${sdl_source_dir} has no SDL_hidapi_rumble.c")
|
||||
endif ()
|
||||
set(_hidapi "${sdl_source_dir}/src/joystick/hidapi/SDL_hidapijoystick.c")
|
||||
if (NOT EXISTS "${_hidapi}")
|
||||
message(FATAL_ERROR "aurora: SDL3 source tree at ${sdl_source_dir} has no SDL_hidapijoystick.c")
|
||||
endif ()
|
||||
|
||||
# Backport of SDL 71f4af732 (drop once SDL includes it): rumble writes must not hold dev_lock.
|
||||
_aurora_sdl3_replace("${_rumble}" "HIDAPI rumble writes no longer hold the device lock"
|
||||
[==[ if (request) {
|
||||
SDL_LockMutex(request->device->dev_lock);
|
||||
if (request->device->dev) {
|
||||
#ifdef DEBUG_RUMBLE
|
||||
HIDAPI_DumpPacket("Rumble packet: size = %d", request->data, request->size);
|
||||
#endif
|
||||
SDL_hid_write(request->device->dev, request->data, request->size);
|
||||
}
|
||||
SDL_UnlockMutex(request->device->dev_lock);
|
||||
]==]
|
||||
[==[ if (request) {
|
||||
if (request->device->dev) {
|
||||
#ifdef DEBUG_RUMBLE
|
||||
HIDAPI_DumpPacket("Rumble packet: size = %d", request->data, request->size);
|
||||
#endif
|
||||
SDL_hid_write(request->device->dev, request->data, request->size);
|
||||
}
|
||||
]==])
|
||||
|
||||
# Without the lock, closing the handle must wait for in-flight rumble writes.
|
||||
_aurora_sdl3_replace("${_hidapi}" "HIDAPI device close waits for pending rumble"
|
||||
[==[ device->driver->FreeDevice(device);
|
||||
device->driver = NULL;
|
||||
|
||||
SDL_LockMutex(device->dev_lock);
|
||||
]==]
|
||||
[==[ device->driver->FreeDevice(device);
|
||||
device->driver = NULL;
|
||||
|
||||
while (SDL_GetAtomicInt(&device->rumble_pending) > 0) {
|
||||
SDL_Delay(10);
|
||||
}
|
||||
|
||||
SDL_LockMutex(device->dev_lock);
|
||||
]==])
|
||||
|
||||
# Wii Remote: rebuild the joystick in place after an extension change.
|
||||
#
|
||||
@@ -118,6 +164,219 @@ static void UpdateDeviceIdentity(SDL_HIDAPI_Device *device)
|
||||
|
||||
now = SDL_GetTicks();
|
||||
]==])
|
||||
|
||||
# Classic Controller sticks: the stock path stretches each half-range by learned min/max;
|
||||
# this posts WPAD's linear value (raw - first-report centre) * unit, times 64.
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Classic Controller linear sticks (helper)"
|
||||
[==[static void HandleGamepadControllerButtonData(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick, const WiiButtonData *data)
|
||||
{
|
||||
]==]
|
||||
[==[/* WiiCompiled: axis = (raw - centre) * unit * 64, y inverted; see AuroraSDL3Patches.cmake */
|
||||
static void PostClassicStickLinear(Uint64 timestamp, SDL_Joystick *joystick, StickCalibrationData *calibration, Uint8 axis, Uint16 data, int unit)
|
||||
{
|
||||
int value;
|
||||
|
||||
if (!calibration->center) {
|
||||
// Center on first read
|
||||
calibration->center = data;
|
||||
return;
|
||||
}
|
||||
value = ((int)data - (int)calibration->center) * unit * 64;
|
||||
if (axis == SDL_GAMEPAD_AXIS_LEFTY || axis == SDL_GAMEPAD_AXIS_RIGHTY) {
|
||||
value = -value;
|
||||
}
|
||||
SDL_SendJoystickAxis(timestamp, joystick, axis, (Sint16)SDL_clamp(value, SDL_JOYSTICK_AXIS_MIN, SDL_JOYSTICK_AXIS_MAX));
|
||||
}
|
||||
|
||||
static void HandleGamepadControllerButtonData(SDL_DriverWii_Context *ctx, SDL_Joystick *joystick, const WiiButtonData *data)
|
||||
{
|
||||
]==])
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Classic Controller linear sticks (axes)"
|
||||
[==[ PostStickCalibrated(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[0], SDL_GAMEPAD_AXIS_LEFTX, lx);
|
||||
PostStickCalibrated(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[1], SDL_GAMEPAD_AXIS_LEFTY, ly);
|
||||
PostStickCalibrated(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[2], SDL_GAMEPAD_AXIS_RIGHTX, rx);
|
||||
PostStickCalibrated(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[3], SDL_GAMEPAD_AXIS_RIGHTY, ry);
|
||||
]==]
|
||||
[==[ PostClassicStickLinear(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[0], SDL_GAMEPAD_AXIS_LEFTX, lx, 16);
|
||||
PostClassicStickLinear(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[1], SDL_GAMEPAD_AXIS_LEFTY, ly, 16);
|
||||
PostClassicStickLinear(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[2], SDL_GAMEPAD_AXIS_RIGHTX, rx, 32);
|
||||
PostClassicStickLinear(ctx->timestamp, joystick, &ctx->m_StickCalibrationData[3], SDL_GAMEPAD_AXIS_RIGHTY, ry, 32);
|
||||
]==])
|
||||
|
||||
# Wii Remote: decode the accelerometer with the factory calibration from the EEPROM.
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (context fields)"
|
||||
[==[ StickCalibrationData m_StickCalibrationData[6];
|
||||
} SDL_DriverWii_Context;
|
||||
]==]
|
||||
[==[ StickCalibrationData m_StickCalibrationData[6];
|
||||
|
||||
float m_fAccelZeroG[3];
|
||||
float m_fAccelPerG[3];
|
||||
} SDL_DriverWii_Context;
|
||||
]==])
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (memory space parameter)"
|
||||
[==[static bool ReadRegister(SDL_DriverWii_Context *ctx, Uint32 address, int size, bool sync)
|
||||
{
|
||||
Uint8 readRequest[7];
|
||||
|
||||
readRequest[0] = k_eWiiOutputReportIDs_ReadMemory;
|
||||
readRequest[1] = (Uint8)(0x04 | (Uint8)ctx->m_bRumbleActive);
|
||||
]==]
|
||||
[==[static bool ReadMemory(SDL_DriverWii_Context *ctx, Uint8 addressSpace, Uint32 address, int size, bool sync);
|
||||
|
||||
static bool ReadRegister(SDL_DriverWii_Context *ctx, Uint32 address, int size, bool sync)
|
||||
{
|
||||
return ReadMemory(ctx, 0x04, address, size, sync);
|
||||
}
|
||||
|
||||
static bool ReadMemory(SDL_DriverWii_Context *ctx, Uint8 addressSpace, Uint32 address, int size, bool sync)
|
||||
{
|
||||
Uint8 readRequest[7];
|
||||
|
||||
readRequest[0] = k_eWiiOutputReportIDs_ReadMemory;
|
||||
readRequest[1] = (Uint8)(addressSpace | (Uint8)ctx->m_bRumbleActive);
|
||||
]==])
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (EEPROM read)"
|
||||
[==[static bool HIDAPI_DriverWii_InitDevice(SDL_HIDAPI_Device *device)
|
||||
{
|
||||
]==]
|
||||
[==[// Nominal decode: zero-g at 0x200, 100 units per g
|
||||
#define WII_ACCEL_NOMINAL_ZERO_G 512.0f
|
||||
#define WII_ACCEL_NOMINAL_PER_G 100.0f
|
||||
|
||||
// EEPROM calibration at 0x16, copy at 0x20: zero-g XYZ, 1 g XYZ, LSBs of both, checksum
|
||||
#define WII_ACCEL_CALIBRATION_SIZE 10
|
||||
#define WII_ADDRESS_SPACE_EEPROM 0x00
|
||||
|
||||
static bool ReadAccelCalibrationBlock(SDL_DriverWii_Context *ctx, Uint8 address, Uint8 *block)
|
||||
{
|
||||
if (!ReadMemory(ctx, WII_ADDRESS_SPACE_EEPROM, address, WII_ACCEL_CALIBRATION_SIZE, true)) {
|
||||
return false;
|
||||
}
|
||||
if (ctx->m_rgucReadBuffer[5] != address ||
|
||||
ctx->m_rgucReadBuffer[3] != ((WII_ACCEL_CALIBRATION_SIZE - 1) << 4)) {
|
||||
SDL_SetError("Unexpected EEPROM read response");
|
||||
return false;
|
||||
}
|
||||
SDL_memcpy(block, ctx->m_rgucReadBuffer + 6, WII_ACCEL_CALIBRATION_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ParseAccelCalibration(SDL_DriverWii_Context *ctx, const Uint8 *block)
|
||||
{
|
||||
int zero[3];
|
||||
int perG[3];
|
||||
Uint8 checksum = 0x55;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < WII_ACCEL_CALIBRATION_SIZE - 1; ++i) {
|
||||
checksum = (Uint8)(checksum + block[i]);
|
||||
}
|
||||
if (checksum != block[WII_ACCEL_CALIBRATION_SIZE - 1]) {
|
||||
SDL_SetError("Bad checksum");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (i = 0; i < 3; ++i) {
|
||||
int shift = 4 - 2 * i;
|
||||
int oneG = (block[4 + i] << 2) | ((block[7] >> shift) & 0x03);
|
||||
|
||||
zero[i] = (block[i] << 2) | ((block[3] >> shift) & 0x03);
|
||||
perG[i] = oneG - zero[i];
|
||||
|
||||
// Sanity limits around the nominal values
|
||||
if (zero[i] < 384 || zero[i] > 640 || perG[i] < 60 || perG[i] > 160) {
|
||||
SDL_SetError("Implausible values");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < 3; ++i) {
|
||||
ctx->m_fAccelZeroG[i] = (float)zero[i];
|
||||
ctx->m_fAccelPerG[i] = (float)perG[i];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void ReadAccelCalibration(SDL_DriverWii_Context *ctx)
|
||||
{
|
||||
static const Uint8 addresses[] = { 0x16, 0x20 };
|
||||
Uint8 block[WII_ACCEL_CALIBRATION_SIZE];
|
||||
int i;
|
||||
|
||||
for (i = 0; i < SDL_arraysize(addresses); ++i) {
|
||||
if (ReadAccelCalibrationBlock(ctx, addresses[i], block) &&
|
||||
ParseAccelCalibration(ctx, block)) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Accelerometer calibration from EEPROM 0x%02X: zero %d/%d/%d, %d/%d/%d units per g",
|
||||
addresses[i],
|
||||
(int)ctx->m_fAccelZeroG[0], (int)ctx->m_fAccelZeroG[1], (int)ctx->m_fAccelZeroG[2],
|
||||
(int)ctx->m_fAccelPerG[0], (int)ctx->m_fAccelPerG[1], (int)ctx->m_fAccelPerG[2]);
|
||||
return;
|
||||
}
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Accelerometer calibration at EEPROM 0x%02X unusable: %s", addresses[i], SDL_GetError());
|
||||
}
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Using nominal accelerometer calibration");
|
||||
}
|
||||
|
||||
static bool HIDAPI_DriverWii_InitDevice(SDL_HIDAPI_Device *device)
|
||||
{
|
||||
]==])
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (nominal defaults)"
|
||||
[==[ ctx->device = device;
|
||||
device->context = ctx;
|
||||
]==]
|
||||
[==[ ctx->device = device;
|
||||
device->context = ctx;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
ctx->m_fAccelZeroG[i] = WII_ACCEL_NOMINAL_ZERO_G;
|
||||
ctx->m_fAccelPerG[i] = WII_ACCEL_NOMINAL_PER_G;
|
||||
}
|
||||
]==])
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (read at init)"
|
||||
[==[ ctx->m_eExtensionControllerType = ReadExtensionControllerType(device);
|
||||
]==]
|
||||
[==[ ctx->m_eExtensionControllerType = ReadExtensionControllerType(device);
|
||||
ReadAccelCalibration(ctx);
|
||||
]==])
|
||||
# Reports without accelerometer bytes (status, memory) decoded as garbage.
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer guard"
|
||||
[==[ }
|
||||
|
||||
x = ((data->rgucAccelerometer[0] << 2) | ((data->rgucBaseButtons[0] >> 5) & 0x03))]==]
|
||||
[==[ }
|
||||
|
||||
if (!data->hasAccelerometer) {
|
||||
return;
|
||||
}
|
||||
|
||||
x = ((data->rgucAccelerometer[0] << 2) | ((data->rgucBaseButtons[0] >> 5) & 0x03))]==])
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (nominal constant)"
|
||||
[==[const WiiButtonData *data)
|
||||
{
|
||||
const float ACCEL_RES_PER_G = 100.0f;
|
||||
Sint16 x, y, z;
|
||||
]==]
|
||||
[==[const WiiButtonData *data)
|
||||
{
|
||||
Sint16 x, y, z;
|
||||
]==])
|
||||
_aurora_sdl3_replace("${_wii}" "Wii Remote accelerometer calibration (decode)"
|
||||
[==[ x = ((data->rgucAccelerometer[0] << 2) | ((data->rgucBaseButtons[0] >> 5) & 0x03)) - 0x200;
|
||||
y = ((data->rgucAccelerometer[1] << 2) | ((data->rgucBaseButtons[1] >> 4) & 0x02)) - 0x200;
|
||||
z = ((data->rgucAccelerometer[2] << 2) | ((data->rgucBaseButtons[1] >> 5) & 0x02)) - 0x200;
|
||||
|
||||
values[0] = -((float)x / ACCEL_RES_PER_G) * SDL_STANDARD_GRAVITY;
|
||||
values[1] = ((float)z / ACCEL_RES_PER_G) * SDL_STANDARD_GRAVITY;
|
||||
values[2] = ((float)y / ACCEL_RES_PER_G) * SDL_STANDARD_GRAVITY;
|
||||
]==]
|
||||
[==[ x = ((data->rgucAccelerometer[0] << 2) | ((data->rgucBaseButtons[0] >> 5) & 0x03));
|
||||
y = ((data->rgucAccelerometer[1] << 2) | ((data->rgucBaseButtons[1] >> 4) & 0x02));
|
||||
z = ((data->rgucAccelerometer[2] << 2) | ((data->rgucBaseButtons[1] >> 5) & 0x02));
|
||||
|
||||
values[0] = -(((float)x - ctx->m_fAccelZeroG[0]) / ctx->m_fAccelPerG[0]) * SDL_STANDARD_GRAVITY;
|
||||
values[1] = (((float)z - ctx->m_fAccelZeroG[2]) / ctx->m_fAccelPerG[2]) * SDL_STANDARD_GRAVITY;
|
||||
values[2] = (((float)y - ctx->m_fAccelZeroG[1]) / ctx->m_fAccelPerG[1]) * SDL_STANDARD_GRAVITY;
|
||||
]==])
|
||||
endfunction()
|
||||
|
||||
# Script mode (FetchContent PATCH_COMMAND).
|
||||
|
||||
@@ -66,7 +66,7 @@ void GXCallDisplayList(const void* data, u32 nbytes) {
|
||||
|
||||
// Decode the display list immediately while its borrowed resources are valid.
|
||||
aurora::gx::fifo::drain();
|
||||
aurora::gx::fifo::process(static_cast<const u8*>(data), nbytes, true);
|
||||
aurora::gx::fifo::process_all(static_cast<const u8*>(data), nbytes, true);
|
||||
}
|
||||
|
||||
void GXCallDisplayListLE(const void* data, u32 nbytes) {
|
||||
@@ -84,6 +84,6 @@ void GXCallDisplayListLE(const void* data, u32 nbytes) {
|
||||
aurora::gx::fifo::drain();
|
||||
|
||||
// Process the display list through the command processor (little-endian)
|
||||
aurora::gx::fifo::process(static_cast<const u8*>(data), nbytes, false);
|
||||
aurora::gx::fifo::process_all(static_cast<const u8*>(data), nbytes, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,7 +553,7 @@ void build_index() noexcept {
|
||||
continue;
|
||||
}
|
||||
|
||||
s_replacementIndex.try_emplace(*parsed, path);
|
||||
s_replacementIndex.try_emplace(*parsed, ReplacementIndexEntry{path});
|
||||
}
|
||||
|
||||
Log.info("Indexed {} texture replacements", s_replacementIndex.size());
|
||||
|
||||
@@ -82,19 +82,23 @@ void drain() {
|
||||
if (detail::sBufferSize == 0) {
|
||||
return;
|
||||
}
|
||||
process_all(detail::sBufferData, detail::sBufferSize, true);
|
||||
detail::sBufferSize = 0;
|
||||
}
|
||||
|
||||
void process_all(const uint8_t* data, uint32_t size, bool bigEndian) {
|
||||
uint32_t consumed = 0;
|
||||
bool retried = false;
|
||||
while (consumed < detail::sBufferSize) {
|
||||
const auto count = process(detail::sBufferData + consumed, detail::sBufferSize - consumed, true);
|
||||
while (consumed < size) {
|
||||
const auto count = process(data + consumed, size - consumed, bigEndian);
|
||||
if (count == 0 && retried)
|
||||
throw gfx::StagingCapacityError("FIFO draw does not fit after capacity submission");
|
||||
consumed += count;
|
||||
if (consumed == detail::sBufferSize) break;
|
||||
if (consumed == size) break;
|
||||
// process returned with its renderer lock released. No recursive drain.
|
||||
gfx::split_staging_batch();
|
||||
retried = true;
|
||||
}
|
||||
detail::sBufferSize = 0;
|
||||
}
|
||||
|
||||
const uint8_t* get_buffer_data() { return detail::sBufferData; }
|
||||
|
||||
@@ -81,6 +81,10 @@ bool in_display_list();
|
||||
// Drain the internal FIFO buffer through the command processor
|
||||
void drain();
|
||||
|
||||
// Decode a whole command stream, submitting the staging batch whenever a draw does not fit.
|
||||
// process() alone stops at that draw, so a caller that ignores its count drops the rest.
|
||||
void process_all(const uint8_t* data, uint32_t size, bool bigEndian);
|
||||
|
||||
// Internal buffer inspection
|
||||
const uint8_t* get_buffer_data();
|
||||
uint32_t get_buffer_size();
|
||||
|
||||
@@ -50,6 +50,9 @@ void reset_uniform_allocations() noexcept;
|
||||
const std::vector<uint8_t>& uniform_allocation(size_t index) noexcept;
|
||||
void use_draw_command_tracking(bool enabled) noexcept;
|
||||
void use_real_vertex_format_helpers(bool enabled) noexcept;
|
||||
// Admit this many draws, then refuse one as if the staging batch were full.
|
||||
void refuse_staging_admission_after(uint32_t admissions) noexcept;
|
||||
void reset_staging_capacity() noexcept;
|
||||
} // namespace aurora::gfx::testing
|
||||
|
||||
class GXFifoTest : public ::testing::Test {
|
||||
@@ -65,6 +68,7 @@ protected:
|
||||
aurora::gfx::testing::reset_resolve_pass_records();
|
||||
aurora::gfx::testing::reset_vertex_push_record();
|
||||
aurora::gfx::testing::use_real_vertex_format_helpers(false);
|
||||
aurora::gfx::testing::reset_staging_capacity();
|
||||
}
|
||||
|
||||
// Copy the internal FIFO buffer contents and clear it
|
||||
|
||||
@@ -34,6 +34,9 @@ std::optional<aurora::gx::DrawData> s_lastGxDraw;
|
||||
bool s_trackDrawCommands = false;
|
||||
bool s_useRealVertexFormatHelpers = false;
|
||||
std::deque<std::vector<uint8_t>> s_uniformAllocations;
|
||||
std::optional<uint32_t> s_stagingAdmissionsBeforeRefusal;
|
||||
bool s_stagingRefused = false;
|
||||
uint64_t s_stagingSplitCount = 0;
|
||||
} // namespace
|
||||
|
||||
// --- aurora::g_config ---
|
||||
@@ -301,8 +304,23 @@ std::pair<ByteBuffer, Range> copy_uniform(Range source) {
|
||||
uint32_t align_uniform(uint32_t value) { return (value + 255u) & ~255u; }
|
||||
uint64_t staging_uniform_bytes(uint64_t value) { return staging_padded(value, 256); }
|
||||
uint64_t staging_storage_bytes(uint64_t value) { return staging_padded(value, 256); }
|
||||
bool staging_has_space(const StagingSizes&) { return true; }
|
||||
void split_staging_batch() { throw StagingCapacityError("Unexpected split in FIFO unit test"); }
|
||||
bool staging_has_space(const StagingSizes&) {
|
||||
if (!s_stagingAdmissionsBeforeRefusal) return true;
|
||||
if (*s_stagingAdmissionsBeforeRefusal > 0) {
|
||||
--*s_stagingAdmissionsBeforeRefusal;
|
||||
return true;
|
||||
}
|
||||
// Refuse once, as a full batch would; the split that follows makes room again.
|
||||
s_stagingAdmissionsBeforeRefusal.reset();
|
||||
s_stagingRefused = true;
|
||||
return false;
|
||||
}
|
||||
void split_staging_batch() {
|
||||
if (!s_stagingRefused) throw StagingCapacityError("Unexpected split in FIFO unit test");
|
||||
s_stagingRefused = false;
|
||||
++s_stagingSplitCount;
|
||||
}
|
||||
uint64_t staging_split_count() noexcept { return s_stagingSplitCount; }
|
||||
|
||||
Vec2<uint32_t> get_render_target_size() noexcept { return s_renderTargetSize; }
|
||||
Vec2<uint32_t> get_frame_buffer_size() noexcept { return s_renderTargetSize; }
|
||||
@@ -345,6 +363,15 @@ void use_draw_command_tracking(bool enabled) noexcept {
|
||||
void use_real_vertex_format_helpers(bool enabled) noexcept {
|
||||
s_useRealVertexFormatHelpers = enabled;
|
||||
}
|
||||
void refuse_staging_admission_after(uint32_t admissions) noexcept {
|
||||
s_stagingAdmissionsBeforeRefusal = admissions;
|
||||
s_stagingRefused = false;
|
||||
}
|
||||
void reset_staging_capacity() noexcept {
|
||||
s_stagingAdmissionsBeforeRefusal.reset();
|
||||
s_stagingRefused = false;
|
||||
s_stagingSplitCount = 0;
|
||||
}
|
||||
} // namespace aurora::gfx::testing
|
||||
|
||||
// --- Pipeline/draw command stubs ---
|
||||
@@ -525,6 +552,7 @@ std::optional<TextureHandle> find_replacement(const GXTexObj_&) noexcept { retur
|
||||
namespace aurora::window {
|
||||
AuroraWindowSize get_window_size() { return {640, 480, 640, 480, 640, 480, 1.0f}; }
|
||||
void set_frame_buffer_aspect_fit(bool) {}
|
||||
void set_force_aspect_16_9(bool) {}
|
||||
} // namespace aurora::window
|
||||
|
||||
// --- WebGPU C API stubs (prevent linker errors from wgpu:: destructors) ---
|
||||
|
||||
@@ -88,6 +88,31 @@ TEST_F(GXFifoTest, SingleExpandedPrimitiveCannotMergeWithTriangles) {
|
||||
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{0, 1, 2}));
|
||||
}
|
||||
|
||||
TEST_F(GXFifoTest, DisplayListSubmitsStagingWhenADrawDoesNotFit) {
|
||||
__GXSetDirtyState();
|
||||
aurora::gx::fifo::clear_buffer();
|
||||
g_gxState.lastVtxFmt = GX_VTXFMT0;
|
||||
g_gxState.lastVtxSize = 1;
|
||||
// admitted=1 refuses the middle draw; admitted=2 refuses the last, so only a replay of the refused draw pushes 0x33.
|
||||
for (const u32 admitted : {1u, 2u}) for (const bool bigEndian : {true, false}) {
|
||||
std::vector<u8> list;
|
||||
for (const u8 fill : {0x11, 0x22, 0x33}) {
|
||||
auto bytes = draw(GX_TRIANGLES, 3);
|
||||
if (!bigEndian) std::swap(bytes[1], bytes[2]);
|
||||
std::fill(bytes.begin() + 3, bytes.end(), fill);
|
||||
list.insert(list.end(), bytes.begin(), bytes.end());
|
||||
}
|
||||
const auto splitsBefore = aurora::gfx::staging_split_count();
|
||||
aurora::gfx::testing::refuse_staging_admission_after(admitted);
|
||||
if (bigEndian) GXCallDisplayList(list.data(), static_cast<u32>(list.size()));
|
||||
else GXCallDisplayListLE(list.data(), static_cast<u32>(list.size()));
|
||||
EXPECT_EQ(aurora::gfx::staging_split_count() - splitsBefore, 1u)
|
||||
<< "admitted=" << admitted << " bigEndian=" << bigEndian;
|
||||
EXPECT_EQ(aurora::gfx::testing::last_pushed_vertices(), (std::vector<u8>{0x33, 0x33, 0x33}))
|
||||
<< "admitted=" << admitted << " bigEndian=" << bigEndian;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(StagingMapping, RetiredCallbacksCannotPublishAnotherBuffersReadiness) {
|
||||
using namespace aurora::gfx;
|
||||
StagingMapState state;
|
||||
|
||||
+26
-146
@@ -48,6 +48,26 @@ endif()
|
||||
|
||||
option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON)
|
||||
option(MKW_BUILD_PSQ_TESTS "Build focused PSQ ISA tests" OFF)
|
||||
set(MKW_X86_CPU_PROFILE "v3" CACHE STRING
|
||||
"Minimum x86-64 CPU profile for translated products (v2 or v3)")
|
||||
set_property(CACHE MKW_X86_CPU_PROFILE PROPERTY STRINGS v2 v3)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$" AND
|
||||
NOT MKW_X86_CPU_PROFILE MATCHES "^(v2|v3)$")
|
||||
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)
|
||||
if(MKW_BUILD_PRODUCTS)
|
||||
message(FATAL_ERROR "MKW_PLATFORM_TESTS_ONLY requires MKW_BUILD_PRODUCTS=OFF")
|
||||
endif()
|
||||
add_compile_definitions(NOMINMAX)
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PlatformTests.cmake")
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Preprocessor definitions that belong to this project's own code (the runtime,
|
||||
# the translated shards and the product glue) and to nothing else. They are
|
||||
@@ -85,21 +105,6 @@ target_include_directories(mkw_pugixml PUBLIC third_party/pugixml)
|
||||
target_compile_features(mkw_pugixml PUBLIC cxx_std_17)
|
||||
set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF)
|
||||
|
||||
# POSIX x86-64 guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric
|
||||
# stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
|
||||
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
|
||||
# asymmetric resume/yield coroutine libraries, which would need every call site restructured).
|
||||
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
|
||||
# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers;
|
||||
# Apple Silicon uses the project's x18-safe AArch64 assembly backend, while Intel macOS uses
|
||||
# libco's existing System V AMD64 backend.
|
||||
if(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
|
||||
add_library(mkw_libco STATIC third_party/libco/libco.c)
|
||||
add_library(mkw::libco ALIAS mkw_libco)
|
||||
target_include_directories(mkw_libco PUBLIC third_party/libco)
|
||||
set_target_properties(mkw_libco PROPERTIES UNITY_BUILD OFF)
|
||||
endif()
|
||||
|
||||
# Runtime configuration is real TOML, parsed by toml11 rather than a project-
|
||||
# specific line parser. Keep it header-only and vendored so disconnected release
|
||||
# builds have exactly the same parser as developer builds.
|
||||
@@ -341,133 +346,7 @@ set(MKW_CPU_BASELINE_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/host_cpu_baseline.cpp
|
||||
list(REMOVE_ITEM SOURCES ${MKW_BASE_PRODUCT_SOURCE} ${MKW_RETRO_REWIND_PRODUCT_SOURCE}
|
||||
${MKW_CPU_BASELINE_SOURCE} ${MKW_PLATFORM_SOURCE})
|
||||
|
||||
# This deliberately small library contains host services that are safe to
|
||||
# validate before guest memory and fiber work makes a full runtime build viable.
|
||||
add_library(mkw_platform STATIC "${MKW_PLATFORM_SOURCE}")
|
||||
target_include_directories(mkw_platform PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_platform PUBLIC cxx_std_17)
|
||||
set_target_properties(mkw_platform PROPERTIES UNITY_BUILD OFF)
|
||||
|
||||
# Keep these independent from Aurora's BUILD_TESTING option: they validate the
|
||||
# project's host-platform contracts, not Aurora's third-party test suite.
|
||||
enable_testing()
|
||||
if(MKW_BUILD_PSQ_TESTS)
|
||||
add_executable(mkw_psq_helpers_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/psq_helpers_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/ppc_quantized.cpp")
|
||||
target_include_directories(mkw_psq_helpers_tests PRIVATE
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/psq_memory"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include/isa"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_psq_helpers_tests PRIVATE cxx_std_17)
|
||||
target_compile_options(mkw_psq_helpers_tests PRIVATE
|
||||
-O2 ${MKW_TRANSLATED_PPC_FP_OPTIONS} -fno-slp-vectorize)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
target_compile_options(mkw_psq_helpers_tests PRIVATE -march=x86-64-v3)
|
||||
endif()
|
||||
set_target_properties(mkw_psq_helpers_tests PROPERTIES UNITY_BUILD OFF)
|
||||
add_test(NAME mkw_psq_helpers_tests COMMAND mkw_psq_helpers_tests)
|
||||
add_test(NAME mkw_psq_reserved_tests COMMAND "${CMAKE_COMMAND}"
|
||||
"-DPSQ_TEST_EXECUTABLE=$<TARGET_FILE:mkw_psq_helpers_tests>"
|
||||
-P "${CMAKE_CURRENT_LIST_DIR}/tests/psq_reserved_tests.cmake")
|
||||
endif()
|
||||
|
||||
add_executable(mkw_platform_paths_tests "${CMAKE_CURRENT_LIST_DIR}/tests/platform_paths_tests.cpp")
|
||||
target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
|
||||
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
|
||||
|
||||
add_executable(mkw_runtime_config_tests "${CMAKE_CURRENT_LIST_DIR}/tests/runtime_config_tests.cpp")
|
||||
target_include_directories(mkw_runtime_config_tests PRIVATE
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/third_party/toml11")
|
||||
target_compile_features(mkw_runtime_config_tests PRIVATE cxx_std_20)
|
||||
add_test(NAME mkw_runtime_config_tests COMMAND mkw_runtime_config_tests)
|
||||
|
||||
add_executable(mkw_nand_save_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_save_tests.cpp")
|
||||
target_include_directories(mkw_nand_save_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_nand_save_tests COMMAND mkw_nand_save_tests)
|
||||
|
||||
add_executable(mkw_nand_settings_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_settings_tests.cpp")
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(mkw_nand_settings_tests PRIVATE Threads::Threads)
|
||||
target_include_directories(mkw_nand_settings_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
|
||||
|
||||
add_executable(mkw_sc_serial_tests "${CMAKE_CURRENT_LIST_DIR}/tests/sc_serial_tests.cpp")
|
||||
target_include_directories(mkw_sc_serial_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_sc_serial_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_sc_serial_tests COMMAND mkw_sc_serial_tests)
|
||||
|
||||
# The input expression engine is self-contained, so it can be exercised without
|
||||
# linking the runtime or SDL.
|
||||
add_executable(mkw_input_expr_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/test_expr.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/input_expr.cpp")
|
||||
target_include_directories(mkw_input_expr_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
|
||||
|
||||
# HostContext deliberately keeps the platform-specific context primitive out
|
||||
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
|
||||
# refactors cannot silently remove its headers, implementation, or link edge.
|
||||
if(MKW_PLATFORM_LINUX)
|
||||
add_executable(mkw_linux_host_context_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp")
|
||||
target_include_directories(mkw_linux_host_context_tests PRIVATE
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/third_party/libco")
|
||||
target_compile_features(mkw_linux_host_context_tests PRIVATE cxx_std_17)
|
||||
target_link_libraries(mkw_linux_host_context_tests PRIVATE mkw::libco)
|
||||
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
|
||||
endif()
|
||||
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
# Exercise the public host-memory contracts separately from translated products.
|
||||
if(MKW_PLATFORM_MACOS_ARM64)
|
||||
# Apple Silicon's context ABI is implemented by the local assembly backend.
|
||||
enable_language(ASM)
|
||||
add_executable(mkw_macos_context_abi_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
|
||||
target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
|
||||
|
||||
add_executable(mkw_macos_host_context_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
|
||||
target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
else()
|
||||
# Intel macOS follows the same System V AMD64 libco path as Linux.
|
||||
add_executable(mkw_macos_host_context_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp")
|
||||
target_include_directories(mkw_macos_host_context_tests PRIVATE
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/third_party/libco")
|
||||
target_link_libraries(mkw_macos_host_context_tests PRIVATE mkw::libco)
|
||||
endif()
|
||||
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
|
||||
|
||||
add_executable(mkw_macos_guest_flat_memory_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_guest_flat_memory_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
|
||||
target_include_directories(mkw_macos_guest_flat_memory_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_macos_guest_flat_memory_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests)
|
||||
|
||||
add_executable(mkw_macos_external_audio_tests
|
||||
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_external_audio_tests.cpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/src/external_audio_macos.cpp")
|
||||
target_include_directories(mkw_macos_external_audio_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
|
||||
target_compile_features(mkw_macos_external_audio_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_external_audio_tests COMMAND mkw_macos_external_audio_tests)
|
||||
endif()
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PlatformTests.cmake")
|
||||
|
||||
# The translator emits the complete, content-addressed source graph. Consuming
|
||||
# this one manifest keeps configure independent of the 28k generated function
|
||||
@@ -518,10 +397,11 @@ else()
|
||||
mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
|
||||
if(MKW_PLATFORM_MACOS_X86_64)
|
||||
target_link_libraries(mkw_macos_native_compile PRIVATE mkw::libco)
|
||||
# Keep this compile-only audit on the same Haswell-era x86-64-v3
|
||||
# baseline as the translated product. PPC paired FMA helpers use
|
||||
# FMA intrinsics and intentionally cannot compile for plain x86-64.
|
||||
target_compile_options(mkw_macos_native_compile PRIVATE -march=x86-64-v3)
|
||||
target_compile_options(mkw_macos_native_compile PRIVATE
|
||||
-march=x86-64-${MKW_X86_CPU_PROFILE})
|
||||
if(MKW_X86_CPU_PROFILE STREQUAL "v2")
|
||||
target_compile_definitions(mkw_macos_native_compile PRIVATE MKW_X86_CPU_PROFILE_V2=1)
|
||||
endif()
|
||||
endif()
|
||||
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Shared by the product build and the dependency-free platform test configuration.
|
||||
get_filename_component(MKW_HOST_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
|
||||
|
||||
# POSIX guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric
|
||||
# stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
|
||||
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
|
||||
# asymmetric resume/yield coroutine libraries, which would need every call site restructured).
|
||||
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
|
||||
# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers;
|
||||
# Apple Silicon uses the project's x18-safe AArch64 assembly backend, while Intel macOS uses
|
||||
# libco's existing System V AMD64 backend.
|
||||
if(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
|
||||
add_library(mkw_libco STATIC "${MKW_HOST_RUNTIME_DIR}/third_party/libco/libco.c")
|
||||
add_library(mkw::libco ALIAS mkw_libco)
|
||||
target_include_directories(mkw_libco PUBLIC "${MKW_HOST_RUNTIME_DIR}/third_party/libco")
|
||||
set_target_properties(mkw_libco PROPERTIES UNITY_BUILD OFF)
|
||||
endif()
|
||||
|
||||
# This deliberately small library contains host services that are safe to
|
||||
# validate before guest memory and fiber work makes a full runtime build viable.
|
||||
add_library(mkw_platform STATIC "${MKW_HOST_RUNTIME_DIR}/src/platform/host_platform.cpp")
|
||||
target_include_directories(mkw_platform PUBLIC "${MKW_HOST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_platform PUBLIC cxx_std_17)
|
||||
set_target_properties(mkw_platform PROPERTIES UNITY_BUILD OFF)
|
||||
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_compile_definitions(mkw_platform PRIVATE NOMINMAX)
|
||||
target_link_libraries(mkw_platform PUBLIC shell32 ole32 uuid)
|
||||
endif()
|
||||
@@ -0,0 +1,188 @@
|
||||
get_filename_component(MKW_TEST_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
|
||||
|
||||
# Keep these independent from Aurora's BUILD_TESTING option: they validate the
|
||||
# project's host-platform contracts, not Aurora's third-party test suite.
|
||||
enable_testing()
|
||||
if(MKW_BUILD_PSQ_TESTS)
|
||||
add_executable(mkw_psq_helpers_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/psq_helpers_tests.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/ppc_quantized.cpp")
|
||||
target_include_directories(mkw_psq_helpers_tests PRIVATE
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/psq_memory"
|
||||
"${MKW_TEST_RUNTIME_DIR}/include/isa"
|
||||
"${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_psq_helpers_tests PRIVATE cxx_std_17)
|
||||
target_compile_options(mkw_psq_helpers_tests PRIVATE
|
||||
-O2 -fno-fast-math -ffp-contract=off -fno-slp-vectorize)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
target_compile_options(mkw_psq_helpers_tests PRIVATE
|
||||
-march=x86-64-${MKW_X86_CPU_PROFILE})
|
||||
endif()
|
||||
set_target_properties(mkw_psq_helpers_tests PROPERTIES UNITY_BUILD OFF)
|
||||
add_test(NAME mkw_psq_helpers_tests COMMAND mkw_psq_helpers_tests)
|
||||
add_test(NAME mkw_psq_reserved_tests COMMAND "${CMAKE_COMMAND}"
|
||||
"-DPSQ_TEST_EXECUTABLE=$<TARGET_FILE:mkw_psq_helpers_tests>"
|
||||
-P "${MKW_TEST_RUNTIME_DIR}/tests/psq_reserved_tests.cmake")
|
||||
endif()
|
||||
|
||||
add_executable(mkw_platform_paths_tests "${MKW_TEST_RUNTIME_DIR}/tests/platform_paths_tests.cpp")
|
||||
target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
|
||||
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
|
||||
|
||||
add_executable(mkw_runtime_config_tests "${MKW_TEST_RUNTIME_DIR}/tests/runtime_config_tests.cpp")
|
||||
target_include_directories(mkw_runtime_config_tests PRIVATE
|
||||
"${MKW_TEST_RUNTIME_DIR}/include"
|
||||
"${MKW_TEST_RUNTIME_DIR}/third_party/toml11")
|
||||
target_compile_features(mkw_runtime_config_tests PRIVATE cxx_std_20)
|
||||
add_test(NAME mkw_runtime_config_tests COMMAND mkw_runtime_config_tests)
|
||||
|
||||
add_executable(mkw_nand_save_tests "${MKW_TEST_RUNTIME_DIR}/tests/nand_save_tests.cpp")
|
||||
target_include_directories(mkw_nand_save_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_nand_save_tests COMMAND mkw_nand_save_tests)
|
||||
|
||||
add_executable(mkw_nand_settings_tests "${MKW_TEST_RUNTIME_DIR}/tests/nand_settings_tests.cpp")
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(mkw_nand_settings_tests PRIVATE Threads::Threads)
|
||||
target_include_directories(mkw_nand_settings_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
|
||||
|
||||
add_executable(mkw_sc_serial_tests "${MKW_TEST_RUNTIME_DIR}/tests/sc_serial_tests.cpp")
|
||||
target_include_directories(mkw_sc_serial_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_sc_serial_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_sc_serial_tests COMMAND mkw_sc_serial_tests)
|
||||
|
||||
# The input expression engine is self-contained, so it can be exercised without
|
||||
# linking the runtime or SDL.
|
||||
add_executable(mkw_input_expr_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/test_expr.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/input_expr.cpp")
|
||||
target_include_directories(mkw_input_expr_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
|
||||
|
||||
# HostContext deliberately keeps the platform-specific context primitive out
|
||||
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
|
||||
# refactors cannot silently remove its headers, implementation, or link edge.
|
||||
if(MKW_PLATFORM_LINUX)
|
||||
add_executable(mkw_linux_host_context_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp")
|
||||
target_include_directories(mkw_linux_host_context_tests PRIVATE
|
||||
"${MKW_TEST_RUNTIME_DIR}/include"
|
||||
"${MKW_TEST_RUNTIME_DIR}/third_party/libco")
|
||||
target_compile_features(mkw_linux_host_context_tests PRIVATE cxx_std_17)
|
||||
target_link_libraries(mkw_linux_host_context_tests PRIVATE mkw::libco)
|
||||
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
|
||||
endif()
|
||||
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
add_executable(mkw_windows_host_context_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp")
|
||||
target_include_directories(mkw_windows_host_context_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_windows_host_context_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_windows_host_context_tests COMMAND mkw_windows_host_context_tests)
|
||||
endif()
|
||||
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
# Probe at the base ISA, including OS support for AVX register state. Cross
|
||||
# builds execute target code only through a configured emulator.
|
||||
if(NOT CMAKE_CROSSCOMPILING OR CMAKE_CROSSCOMPILING_EMULATOR)
|
||||
include(CheckCXXSourceRuns)
|
||||
include(CMakePushCheckState)
|
||||
cmake_push_check_state(RESET)
|
||||
set(CMAKE_REQUIRED_FLAGS "-march=x86-64")
|
||||
check_cxx_source_runs([=[
|
||||
#include <cpuid.h>
|
||||
int main() {
|
||||
unsigned a, b, c, d;
|
||||
if (__get_cpuid_max(0, nullptr) < 7 ||
|
||||
__get_cpuid_max(0x80000000u, nullptr) < 0x80000001u)
|
||||
return 1;
|
||||
__cpuid_count(1, 0, a, b, c, d);
|
||||
// v2 plus FMA, MOVBE, XSAVE, OSXSAVE, AVX and F16C.
|
||||
const unsigned leaf1 = (1u << 0) | (1u << 9) | (1u << 12) |
|
||||
(1u << 13) | (1u << 19) | (1u << 20) | (1u << 22) |
|
||||
(1u << 23) | (1u << 26) | (1u << 27) | (1u << 28) | (1u << 29);
|
||||
if ((c & leaf1) != leaf1) return 1;
|
||||
__cpuid_count(7, 0, a, b, c, d);
|
||||
const unsigned leaf7 = (1u << 3) | (1u << 5) | (1u << 8);
|
||||
if ((b & leaf7) != leaf7) return 1;
|
||||
__cpuid_count(0x80000001u, 0, a, b, c, d);
|
||||
if ((c & 0x21u) != 0x21u) return 1; // LAHF-SAHF and LZCNT.
|
||||
__asm__ __volatile__("xgetbv" : "=a"(a), "=d"(d) : "c"(0));
|
||||
return (a & 0x6u) == 0x6u ? 0 : 1;
|
||||
}
|
||||
]=] MKW_HOST_SUPPORTS_X86_V3)
|
||||
cmake_pop_check_state()
|
||||
endif()
|
||||
# Compile the same oracle cases for both paths. The v2 binary proves the
|
||||
# scalar fallback stays free of FMA; the v3 binary checks the existing
|
||||
# vector intrinsic path against the same strict result.
|
||||
foreach(profile IN ITEMS v2 v3)
|
||||
add_executable(mkw_ppc_pair_fma_${profile}_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/ppc_pair_fma_tests.cpp")
|
||||
target_include_directories(mkw_ppc_pair_fma_${profile}_tests PRIVATE
|
||||
"${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_ppc_pair_fma_${profile}_tests PRIVATE cxx_std_17)
|
||||
target_compile_options(mkw_ppc_pair_fma_${profile}_tests PRIVATE
|
||||
-march=x86-64-${profile} -fno-fast-math -ffp-contract=off)
|
||||
if((NOT CMAKE_CROSSCOMPILING OR CMAKE_CROSSCOMPILING_EMULATOR) AND
|
||||
(profile STREQUAL "v2" OR MKW_HOST_SUPPORTS_X86_V3))
|
||||
add_test(NAME mkw_ppc_pair_fma_${profile}_tests COMMAND mkw_ppc_pair_fma_${profile}_tests)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
add_library(mkw_cpu_baseline_v2_compile OBJECT
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/host_cpu_baseline.cpp")
|
||||
target_compile_features(mkw_cpu_baseline_v2_compile PRIVATE cxx_std_17)
|
||||
target_compile_definitions(mkw_cpu_baseline_v2_compile PRIVATE MKW_X86_CPU_PROFILE_V2=1)
|
||||
target_compile_options(mkw_cpu_baseline_v2_compile PRIVATE -march=x86-64-v2 -w)
|
||||
endif()
|
||||
|
||||
if(MKW_PLATFORM_MACOS)
|
||||
# Exercise the public host-memory contracts separately from translated products.
|
||||
if(MKW_PLATFORM_MACOS_ARM64)
|
||||
# Apple Silicon's context ABI is implemented by the local assembly backend.
|
||||
enable_language(ASM)
|
||||
add_executable(mkw_macos_context_abi_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/macos_context_abi_tests.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/platform/macos/co_switch.S")
|
||||
target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
|
||||
|
||||
add_executable(mkw_macos_host_context_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/platform/macos/co_switch.S")
|
||||
target_include_directories(mkw_macos_host_context_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
|
||||
else()
|
||||
# Intel macOS follows the same System V AMD64 libco path as Linux.
|
||||
add_executable(mkw_macos_host_context_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp")
|
||||
target_include_directories(mkw_macos_host_context_tests PRIVATE
|
||||
"${MKW_TEST_RUNTIME_DIR}/include"
|
||||
"${MKW_TEST_RUNTIME_DIR}/third_party/libco")
|
||||
target_link_libraries(mkw_macos_host_context_tests PRIVATE mkw::libco)
|
||||
endif()
|
||||
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
|
||||
|
||||
add_executable(mkw_macos_guest_flat_memory_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/macos_guest_flat_memory_tests.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/guest_flat_memory_macos.cpp")
|
||||
target_include_directories(mkw_macos_guest_flat_memory_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_macos_guest_flat_memory_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests)
|
||||
|
||||
add_executable(mkw_macos_external_audio_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/macos_external_audio_tests.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/external_audio_macos.cpp")
|
||||
target_include_directories(mkw_macos_external_audio_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_macos_external_audio_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_macos_external_audio_tests COMMAND mkw_macos_external_audio_tests)
|
||||
endif()
|
||||
@@ -80,14 +80,17 @@ 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)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
|
||||
elseif(MKW_PLATFORM_LINUX)
|
||||
elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
|
||||
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
|
||||
# media monitoring).
|
||||
# media monitoring). Empty on platforms where dl* is already in libc/libSystem.
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
|
||||
@@ -135,15 +138,18 @@ set_target_properties(mkw_runtime_common PROPERTIES UNITY_BUILD ON UNITY_BUILD_M
|
||||
target_precompile_headers(mkw_runtime_common PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
|
||||
mkw_apply_common_compile_options(mkw_runtime_common)
|
||||
|
||||
# Host ISA guard. Windows and Linux x86_64 product targets use x86-64-v3, so
|
||||
# this object deliberately keeps the plain baseline ISA and checks the CPU
|
||||
# before any AVX2/FMA code can execute. AArch64 has no equivalent optional ISA
|
||||
# floor to probe: NEON/FMA are architectural requirements.
|
||||
# Host ISA guard deliberately keeps the plain baseline ISA and checks the
|
||||
# selected x86 profile before optional instructions can execute. AArch64 has
|
||||
# no equivalent optional ISA floor to probe: NEON/FMA are architectural
|
||||
# requirements.
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}")
|
||||
target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17)
|
||||
set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF)
|
||||
target_compile_options(mkw_cpu_baseline PRIVATE -w)
|
||||
if(MKW_X86_CPU_PROFILE STREQUAL "v2")
|
||||
target_compile_definitions(mkw_cpu_baseline PRIVATE MKW_X86_CPU_PROFILE_V2=1)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT MKW_BASE_COMMON_SHARDS)
|
||||
@@ -333,12 +339,12 @@ else()
|
||||
message(STATUS "RetroRewind target disabled (run translate-mod and emit-build-shards)")
|
||||
endif()
|
||||
|
||||
# Windows and Linux x86_64 share the x86-64-v3 floor that the CPU baseline
|
||||
# object above checks. AArch64 builds are compiled locally for the host that
|
||||
# will run them, so both Linux and Apple Silicon use the compiler's native CPU
|
||||
# tuning rather than leaving target-specific performance on the table.
|
||||
# Windows, Linux and Intel macOS x86_64 use the selected profile. AArch64
|
||||
# builds are compiled locally for the host that will run them, so both Linux
|
||||
# and Apple Silicon use the compiler's native CPU tuning rather than leaving
|
||||
# target-specific performance on the table.
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3)
|
||||
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-${MKW_X86_CPU_PROFILE})
|
||||
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
|
||||
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
|
||||
else()
|
||||
@@ -351,5 +357,8 @@ set(MKW_ALL_BUILD_TARGETS
|
||||
foreach(target IN LISTS MKW_ALL_BUILD_TARGETS)
|
||||
if(TARGET ${target} AND MKW_BASELINE_ARCH_FLAG)
|
||||
target_compile_options(${target} PRIVATE ${MKW_BASELINE_ARCH_FLAG})
|
||||
if(MKW_X86_CPU_PROFILE STREQUAL "v2")
|
||||
target_compile_definitions(${target} PRIVATE MKW_X86_CPU_PROFILE_V2=1)
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
// HostContext is the deliberately small boundary between the guest scheduler
|
||||
// and the host's cooperative-context facility. Windows uses native Fibers;
|
||||
// Linux and Intel macOS use libco's System V x86-64 backend. macOS AArch64 uses
|
||||
// Linux uses libco's host backend; Intel macOS uses its System V x86-64 backend. macOS AArch64 uses
|
||||
// the local assembly backend because it must preserve Darwin's platform-reserved
|
||||
// x18 register, which libco's AArch64 backend does not save. Its handles are
|
||||
// only valid on the thread that initialized the scheduler.
|
||||
|
||||
@@ -607,7 +607,18 @@ inline double PPC_PsMulNoNiInline(double lhs, double rhs)
|
||||
inline PpcPairVec PpcFmaddPairInline(PpcPairVec multiplicand, PpcPairVec multiplier, PpcPairVec addend)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
#if defined(__FMA__)
|
||||
return _mm_fmadd_ps(multiplicand, multiplier, addend);
|
||||
#else
|
||||
// x86-64-v2 has SSE4.2 but no FMA. Preserve the one rounding point per
|
||||
// lane through the scalar C++ FMA rather than decomposing into mul+add.
|
||||
const double a = PpcM128ToPsInline(multiplicand);
|
||||
const double c = PpcM128ToPsInline(multiplier);
|
||||
const double b = PpcM128ToPsInline(addend);
|
||||
return PpcPsToM128Inline(PpcPackPairedInline(
|
||||
PpcAccuratePsMaddLaneNoNiInline<false>(PpcGetPs0Inline(a), PpcGetPs0Inline(c), PpcGetPs0Inline(b)),
|
||||
PpcAccuratePsMaddLaneNoNiInline<false>(PpcGetPs1Inline(a), PpcGetPs1Inline(c), PpcGetPs1Inline(b))));
|
||||
#endif
|
||||
#elif defined(__aarch64__)
|
||||
const PpcPairVec result = vfma_f32(addend, multiplicand, multiplier);
|
||||
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
|
||||
@@ -619,7 +630,16 @@ inline PpcPairVec PpcFmaddPairInline(PpcPairVec multiplicand, PpcPairVec multipl
|
||||
inline PpcPairVec PpcFmsubPairInline(PpcPairVec multiplicand, PpcPairVec multiplier, PpcPairVec subtractor)
|
||||
{
|
||||
#if defined(__x86_64__)
|
||||
#if defined(__FMA__)
|
||||
return _mm_fmsub_ps(multiplicand, multiplier, subtractor);
|
||||
#else
|
||||
const double a = PpcM128ToPsInline(multiplicand);
|
||||
const double c = PpcM128ToPsInline(multiplier);
|
||||
const double b = PpcM128ToPsInline(subtractor);
|
||||
return PpcPsToM128Inline(PpcPackPairedInline(
|
||||
PpcAccuratePsMaddLaneNoNiInline<true>(PpcGetPs0Inline(a), PpcGetPs0Inline(c), PpcGetPs0Inline(b)),
|
||||
PpcAccuratePsMaddLaneNoNiInline<true>(PpcGetPs1Inline(a), PpcGetPs1Inline(c), PpcGetPs1Inline(b))));
|
||||
#endif
|
||||
#elif defined(__aarch64__)
|
||||
const PpcPairVec result = vfma_f32(vneg_f32(subtractor), multiplicand, multiplier);
|
||||
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
|
||||
|
||||
@@ -138,28 +138,6 @@ inline std::string Trim(std::string_view text) {
|
||||
return std::string(text.substr(begin, end - begin));
|
||||
}
|
||||
|
||||
inline std::string RemoveComment(std::string_view line) {
|
||||
bool inSingle = false;
|
||||
bool inDouble = false;
|
||||
bool escaped = false;
|
||||
for (size_t i = 0; i < line.size(); ++i) {
|
||||
const char ch = line[i];
|
||||
if (inDouble && ch == '\\' && !escaped) {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (ch == '\'' && !inDouble) {
|
||||
inSingle = !inSingle;
|
||||
} else if (ch == '"' && !inSingle && !escaped) {
|
||||
inDouble = !inDouble;
|
||||
} else if (ch == '#' && !inSingle && !inDouble) {
|
||||
return std::string(line.substr(0, i));
|
||||
}
|
||||
escaped = false;
|
||||
}
|
||||
return std::string(line);
|
||||
}
|
||||
|
||||
inline bool IsSupportedResolutionMultiplier(float value) {
|
||||
static constexpr std::array values{0.0f, 1.0f, 1.5f, 2.0f, 3.0f, 4.0f, 6.0f, 8.0f};
|
||||
return std::find(values.begin(), values.end(), value) != values.end();
|
||||
@@ -546,68 +524,50 @@ inline const std::optional<std::string>& ControllerButton(size_t index) {
|
||||
return index < Get().controllerButtons.size() ? Get().controllerButtons[index] : empty;
|
||||
}
|
||||
|
||||
// Update one TOML value without discarding comments, unrelated settings, or
|
||||
// user-specific paths. This is used by the in-game F10 settings bar.
|
||||
// Sets section.key (section may be a dotted table path) to a TOML literal, keeping the rest of the
|
||||
// document's keys, order and comments. Returns nullopt when the document or value does not parse.
|
||||
inline std::optional<std::string> UpdateSettingText(const std::string& text, std::string_view section,
|
||||
std::string_view key, std::string_view value) {
|
||||
try {
|
||||
auto root = toml::parse_str<toml::ordered_type_config>(text);
|
||||
auto literal = toml::parse_str<toml::ordered_type_config>("v = " + std::string(value));
|
||||
auto* table = &root;
|
||||
std::istringstream path{std::string(section)};
|
||||
for (std::string part; std::getline(path, part, '.');) {
|
||||
auto& parent = table->as_table();
|
||||
if (!parent.contains(part)) {
|
||||
parent[part] = toml::ordered_value(toml::ordered_table{});
|
||||
}
|
||||
table = &parent.at(part);
|
||||
if (!table->is_table()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
if (table->as_table_fmt().fmt == toml::table_format::implicit) {
|
||||
table->as_table_fmt().fmt = toml::table_format::multiline;
|
||||
}
|
||||
auto& slot = table->as_table()[std::string(key)];
|
||||
auto& updated = literal.as_table().at("v");
|
||||
updated.comments() = slot.comments();
|
||||
slot = std::move(updated);
|
||||
return toml::format(root);
|
||||
} catch (const std::exception&) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
// Update one TOML value without discarding unrelated settings or user-specific paths. This is
|
||||
// used by the in-game F10 settings bar. An unparseable file is left untouched.
|
||||
inline bool WriteSetting(std::string_view section, std::string_view key, std::string_view value) {
|
||||
const auto path = ResolveConfigPath();
|
||||
std::ifstream input(path);
|
||||
std::vector<std::string> lines;
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
if (!line.empty() && line.back() == '\r') {
|
||||
line.pop_back();
|
||||
}
|
||||
lines.push_back(std::move(line));
|
||||
std::ostringstream existing;
|
||||
if (std::ifstream input(path, std::ios::binary); input) {
|
||||
existing << input.rdbuf();
|
||||
}
|
||||
|
||||
const std::string normalizedSection = Trim(section);
|
||||
const std::string normalizedKey = Trim(key);
|
||||
size_t sectionStart = lines.size();
|
||||
size_t sectionEnd = lines.size();
|
||||
for (size_t i = 0; i < lines.size(); ++i) {
|
||||
const std::string trimmed = Trim(RemoveComment(lines[i]));
|
||||
if (trimmed.size() >= 2 && trimmed.front() == '[' && trimmed.back() == ']') {
|
||||
const std::string found = Trim(std::string_view(trimmed).substr(1, trimmed.size() - 2));
|
||||
if (sectionStart != lines.size()) {
|
||||
sectionEnd = i;
|
||||
break;
|
||||
}
|
||||
if (found == normalizedSection) {
|
||||
sectionStart = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::string replacement = normalizedKey + " = " + std::string(value);
|
||||
if (sectionStart == lines.size()) {
|
||||
if (!lines.empty() && !lines.back().empty()) {
|
||||
lines.emplace_back();
|
||||
}
|
||||
lines.emplace_back("[" + normalizedSection + "]");
|
||||
lines.push_back(replacement);
|
||||
} else {
|
||||
bool replaced = false;
|
||||
for (size_t i = sectionStart + 1; i < sectionEnd; ++i) {
|
||||
const std::string uncommented = Trim(RemoveComment(lines[i]));
|
||||
const size_t equals = uncommented.find('=');
|
||||
if (equals != std::string::npos && Trim(std::string_view(uncommented).substr(0, equals)) == normalizedKey) {
|
||||
lines[i] = replacement;
|
||||
replaced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!replaced) {
|
||||
// Append after the section's last real line rather than after the blank line that
|
||||
// separates it from the next header: this file is edited by hand and by the host
|
||||
// installer as well, and a key parked below the separator reads as if it belonged to
|
||||
// the next section. The host writer (Launcher/WiiCompiled.Setup/RuntimeConfiguration.cs)
|
||||
// applies exactly this rule.
|
||||
size_t insertAt = sectionEnd;
|
||||
while (insertAt > sectionStart + 1 && Trim(lines[insertAt - 1]).empty()) {
|
||||
--insertAt;
|
||||
}
|
||||
lines.insert(lines.begin() + static_cast<std::ptrdiff_t>(insertAt), replacement);
|
||||
}
|
||||
const auto updated = UpdateSettingText(existing.str(), section, key, value);
|
||||
if (!updated) {
|
||||
std::cerr << "[runtime-config] Not updating invalid " << PathToUtf8(path) << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
@@ -619,9 +579,7 @@ inline bool WriteSetting(std::string_view section, std::string_view key, std::st
|
||||
std::cerr << "[runtime-config] Unable to write " << PathToUtf8(path) << std::endl;
|
||||
return false;
|
||||
}
|
||||
for (const auto& outputLine : lines) {
|
||||
output << outputLine << '\n';
|
||||
}
|
||||
output << *updated;
|
||||
return static_cast<bool>(output);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,15 +73,8 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample);
|
||||
// no remote or SDL has not delivered a sample yet.
|
||||
bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]);
|
||||
|
||||
// Accelerometer zero-point calibration. SDL's Wii driver tries to read the
|
||||
// remote's factory calibration block, but over Bluetooth that read often times
|
||||
// out ("Using fallback accelerometer calibration" in console.log) and it falls
|
||||
// back to a nominal 0x200 zero point; a real remote then carries a per-axis
|
||||
// bias of up to ~0.2 g, which held sideways as a wheel is a permanent steering
|
||||
// offset.
|
||||
// StartAccelCalibration expects the remote lying still with the buttons up;
|
||||
// Poll() then collects samples for about a second and stores the difference to
|
||||
// the ideal (0, 1, 0) g in Config.toml.
|
||||
// Residual zero-point trim on top of SDL's factory-calibrated decode. Expects the
|
||||
// remote lying still, buttons up; Poll() averages about a second and stores it.
|
||||
void StartAccelCalibration(uint32_t chan);
|
||||
// Forgets the stored correction.
|
||||
void ClearAccelCalibration();
|
||||
|
||||
@@ -242,7 +242,6 @@ void WritePollResults(uint32_t outAddress,
|
||||
const std::vector<NetworkPollContract::CopiedDescriptor>& descriptors);
|
||||
|
||||
// network_socket.cpp
|
||||
int32_t DeleteWiiSocket(uint32_t fd);
|
||||
void CleanupAllWiiSockets();
|
||||
sockaddr_in ReadWiiSockAddr(uint32_t addr);
|
||||
int32_t HandleIpTopIoctl(uint32_t cmd, uint32_t inBuf, uint32_t inLen, uint32_t outBuf,
|
||||
|
||||
@@ -54,6 +54,9 @@ constexpr int kMaxSslSessions = 4;
|
||||
struct SslSession {
|
||||
bool active = false;
|
||||
bool handshaked = false;
|
||||
// A failed POSIX TLS write cannot be resumed with a new guest buffer.
|
||||
// Keep the slot and socket ownership intact until explicit teardown.
|
||||
bool failed = false;
|
||||
bool plaintextWfc = false;
|
||||
uint32_t socketFd = UINT32_MAX;
|
||||
NativeSocket native = kInvalidSocket;
|
||||
@@ -761,6 +764,14 @@ static int32_t SslHandshakeImpl(SslSession& ssl) {
|
||||
return SSL_OK;
|
||||
}
|
||||
|
||||
static int32_t FailSslWrite(SslSession& ssl) {
|
||||
ssl.failed = true;
|
||||
// Stop transport I/O without deleting the guest descriptor or freeing a
|
||||
// session still referenced by the IOCTLV_NET_SSL_WRITE caller.
|
||||
::shutdown(ssl.native, SHUT_RDWR);
|
||||
return SSL_ERR_FAILED;
|
||||
}
|
||||
|
||||
static int32_t SslWrite(SslSession& ssl, const uint8_t* data, uint32_t size) {
|
||||
if (!data || size == 0) {
|
||||
return SSL_ERR_ZERO;
|
||||
@@ -795,12 +806,11 @@ static int32_t SslWrite(SslSession& ssl, const uint8_t* data, uint32_t size) {
|
||||
}
|
||||
if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
|
||||
if (std::chrono::steady_clock::now() >= writeDeadline) {
|
||||
DeleteWiiSocket(ssl.socketFd);
|
||||
return SSL_ERR_FAILED;
|
||||
return FailSslWrite(ssl);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return SSL_ERR_FAILED;
|
||||
return FailSslWrite(ssl);
|
||||
}
|
||||
return static_cast<int32_t>(totalWritten);
|
||||
}
|
||||
@@ -842,6 +852,9 @@ static int32_t SslRead(SslSession& ssl, uint8_t* out, uint32_t size) {
|
||||
// The handshake runs on every SSL read/write, so a failure repeats for as long
|
||||
// as the session lives; report only the first one.
|
||||
static int32_t SslHandshake(SslSession& ssl) {
|
||||
if (ssl.failed) {
|
||||
return SSL_ERR_FAILED;
|
||||
}
|
||||
const int32_t result = SslHandshakeImpl(ssl);
|
||||
if (result != SSL_OK && !ssl.loggedHandshakeFail) {
|
||||
ssl.loggedHandshakeFail = true;
|
||||
@@ -947,6 +960,11 @@ int32_t HandleSslIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const std
|
||||
WriteSslReturn(in, SSL_ERR_ID);
|
||||
return 0;
|
||||
}
|
||||
// Reject before NAS buffering can acknowledge data on a failed session.
|
||||
if (g_sslSessions[sslId].failed) {
|
||||
WriteSslReturn(in, SSL_ERR_FAILED);
|
||||
return 0;
|
||||
}
|
||||
if (out.size() < 2 || !out[1].address) {
|
||||
WriteSslReturn(in, SSL_ERR_FAILED);
|
||||
return 0;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// Host ISA guard. Every other product target builds with -march=x86-64-v3, so a pre-Haswell
|
||||
// Intel or pre-Excavator AMD machine would otherwise die on an illegal-instruction fault with no
|
||||
// Host ISA guard. Product targets build with an explicit x86-64-v2 or x86-64-v3 profile, so an
|
||||
// unsupported machine would otherwise die on an illegal-instruction fault with no
|
||||
// explanation. This TU alone skips that flag (own CMake object library, excluded from unity
|
||||
// build/PCH) and runs from a priority-101 C initializer, ahead of every C++ dynamic initializer
|
||||
// and thus the first AVX2 code that could execute. Keep it free of anything that could pull in
|
||||
// vectorized code: no iostreams, no std::string, no runtime-wide headers.
|
||||
//
|
||||
// x86-64-v3 is an x86-specific optional-feature baseline (AVX2/BMI2/FMA and friends are not
|
||||
// guaranteed present on every x86_64 chip); nothing here applies on AArch64, where ASIMD/NEON is
|
||||
// mandatory in the base architecture and PublicProducts.cmake never applies an -march=x86-64-v3
|
||||
// equivalent flag to begin with. That branch below is a no-op stub, not a port of this check.
|
||||
// x86-64-v2/v3 are x86-specific optional-feature baselines; nothing here applies on AArch64,
|
||||
// where ASIMD/NEON is mandatory in the base architecture.
|
||||
|
||||
#if defined(__x86_64__)
|
||||
|
||||
@@ -58,27 +56,27 @@ struct CpuFeature {
|
||||
bool isOsXsave;
|
||||
};
|
||||
|
||||
// Everything x86-64-v3 implies, which includes all of x86-64-v2. Spelled out so
|
||||
// the error message can name the exact instruction sets the machine lacks
|
||||
// rather than only "AVX2", which is merely the best known member of the set.
|
||||
// x86-64-v2 requirements. The v3-only extension below retains a useful
|
||||
// feature-by-feature diagnostic rather than reducing a failed v3 check to AVX2.
|
||||
constexpr CpuFeature kRequiredFeatures[] = {
|
||||
{"SSE3", 1, 0, 2, 0, false},
|
||||
{"SSSE3", 1, 0, 2, 9, false},
|
||||
{"FMA", 1, 0, 2, 12, false},
|
||||
{"CMPXCHG16B", 1, 0, 2, 13, false},
|
||||
{"SSE4.1", 1, 0, 2, 19, false},
|
||||
{"SSE4.2", 1, 0, 2, 20, false},
|
||||
{"MOVBE", 1, 0, 2, 22, false},
|
||||
{"POPCNT", 1, 0, 2, 23, false},
|
||||
{"OSXSAVE", 1, 0, 2, 27, true},
|
||||
{"AVX", 1, 0, 2, 28, false},
|
||||
{"F16C", 1, 0, 2, 29, false},
|
||||
{"BMI1", 7, 0, 1, 3, false},
|
||||
{"AVX2", 7, 0, 1, 5, false},
|
||||
{"BMI2", 7, 0, 1, 8, false},
|
||||
{"LAHF-SAHF", 0x80000001u, 0, 2, 0, false},
|
||||
};
|
||||
|
||||
#if !defined(MKW_X86_CPU_PROFILE_V2)
|
||||
constexpr CpuFeature kV3RequiredFeatures[] = {
|
||||
{"FMA", 1, 0, 2, 12, false}, {"MOVBE", 1, 0, 2, 22, false},
|
||||
{"OSXSAVE", 1, 0, 2, 27, true}, {"AVX", 1, 0, 2, 28, false},
|
||||
{"F16C", 1, 0, 2, 29, false}, {"BMI1", 7, 0, 1, 3, false},
|
||||
{"AVX2", 7, 0, 1, 5, false}, {"BMI2", 7, 0, 1, 8, false},
|
||||
{"LZCNT", 0x80000001u, 0, 2, 5, false},
|
||||
};
|
||||
#endif
|
||||
|
||||
// Fixed-capacity text accumulation: no allocation, no exceptions, nothing that
|
||||
// could route through code this file is trying to stay ahead of.
|
||||
@@ -130,6 +128,27 @@ bool CollectMissingBaselineFeatures(TextBuffer& missing) {
|
||||
ok = false;
|
||||
}
|
||||
|
||||
#if !defined(MKW_X86_CPU_PROFILE_V2)
|
||||
for (const CpuFeature& feature : kV3RequiredFeatures) {
|
||||
const bool leafAvailable = (feature.leaf & 0x80000000u) != 0
|
||||
? feature.leaf <= maxExtended
|
||||
: feature.leaf <= maxBasic;
|
||||
bool present = false;
|
||||
if (leafAvailable) {
|
||||
unsigned regs[4] = {0, 0, 0, 0};
|
||||
HostCpuId(feature.leaf, feature.subleaf, regs);
|
||||
present = (regs[feature.reg] & (1u << feature.bit)) != 0;
|
||||
}
|
||||
if (present) {
|
||||
haveOsXsave = haveOsXsave || feature.isOsXsave;
|
||||
continue;
|
||||
}
|
||||
if (!ok) missing.Append(", ");
|
||||
missing.Append(feature.name);
|
||||
ok = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
// CPUID reporting AVX is not sufficient: the OS also has to have enabled
|
||||
// XMM and YMM state saving or every VEX-encoded instruction faults. This is
|
||||
// the same guard a compiler's own runtime feature dispatch applies.
|
||||
@@ -175,13 +194,17 @@ void WriteStdErrEarly(const char* text) {
|
||||
|
||||
[[noreturn]] void ReportUnsupportedCpu(const char* missing) {
|
||||
TextBuffer message;
|
||||
message.Append(
|
||||
"This build needs a processor that supports AVX2 and the rest of the "
|
||||
"x86-64-v3 instruction set.\n\nMissing on this machine: ");
|
||||
#if defined(MKW_X86_CPU_PROFILE_V2)
|
||||
message.Append("This build needs a processor that supports the x86-64-v2 instruction set.\n\nMissing on this machine: ");
|
||||
#else
|
||||
message.Append("This build needs a processor that supports the x86-64-v3 instruction set.\n\nMissing on this machine: ");
|
||||
#endif
|
||||
message.Append(missing);
|
||||
message.Append(
|
||||
"\n\nx86-64-v3 covers Intel Core processors from Haswell (4th "
|
||||
"generation, 2013) onward and AMD processors from Excavator (2015) onward.");
|
||||
#if defined(MKW_X86_CPU_PROFILE_V2)
|
||||
message.Append("\n\nx86-64-v2 covers Intel Core processors from Nehalem (2008) onward and AMD processors from Jaguar (2013) onward.");
|
||||
#else
|
||||
message.Append("\n\nx86-64-v3 covers Intel Core processors from Haswell (4th generation, 2013) onward and AMD processors from Excavator (2015) onward.");
|
||||
#endif
|
||||
|
||||
// The tag matches RT_TAG_RUNTIME in runtime_log.h. It is spelled out here
|
||||
// because this translation unit must not include runtime-wide headers (see
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "platform/host_platform.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
@@ -46,6 +47,18 @@ std::optional<std::filesystem::path> ExecutableDirectory() noexcept {
|
||||
std::error_code ec;
|
||||
const auto resolved = std::filesystem::weakly_canonical(path, ec);
|
||||
return (ec ? std::filesystem::path(path) : resolved).parent_path();
|
||||
#elif defined(__linux__)
|
||||
std::vector<char> buffer(256);
|
||||
for (;;) {
|
||||
const auto length = ::readlink("/proc/self/exe", buffer.data(), buffer.size());
|
||||
if (length < 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (static_cast<std::size_t>(length) < buffer.size()) {
|
||||
return std::filesystem::path(std::string(buffer.data(), length)).parent_path();
|
||||
}
|
||||
buffer.resize(buffer.size() * 2);
|
||||
}
|
||||
#else
|
||||
return std::nullopt;
|
||||
#endif
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <stdexcept>
|
||||
#include <limits>
|
||||
|
||||
@@ -44,11 +45,48 @@ uint32_t g_reservationAddr = 0;
|
||||
bool g_hasReservation = false;
|
||||
const auto g_timeBaseStart = std::chrono::steady_clock::now();
|
||||
|
||||
// Days from 1970-01-01 to the given civil date (Howard Hinnant's algorithm).
|
||||
constexpr int64_t DaysFromCivil(int64_t y, unsigned m, unsigned d)
|
||||
{
|
||||
y -= m <= 2;
|
||||
const int64_t era = (y >= 0 ? y : y - 399) / 400;
|
||||
const unsigned yoe = static_cast<unsigned>(y - era * 400);
|
||||
const unsigned doy = (153 * (m > 2 ? m - 3 : m + 9) + 2) / 5 + d - 1;
|
||||
const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
||||
return era * 146097 + static_cast<int64_t>(doe) - 719468;
|
||||
}
|
||||
|
||||
// The Wii RTC holds local time; the OS counts ticks from 2000-01-01.
|
||||
uint64_t InitialTimeBase()
|
||||
{
|
||||
const std::time_t now = std::time(nullptr);
|
||||
std::tm local{};
|
||||
#if defined(_WIN32)
|
||||
if (localtime_s(&local, &now) != 0) {
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
if (localtime_r(&now, &local) == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
const int64_t days = DaysFromCivil(local.tm_year + 1900, static_cast<unsigned>(local.tm_mon + 1),
|
||||
static_cast<unsigned>(local.tm_mday)) -
|
||||
DaysFromCivil(2000, 1, 1);
|
||||
const int64_t seconds = days * 86400 + local.tm_hour * 3600 + local.tm_min * 60 + local.tm_sec;
|
||||
if (seconds <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return static_cast<uint64_t>(seconds) * TimeBaseContract::kTicksPerSecond;
|
||||
}
|
||||
|
||||
const uint64_t g_timeBaseOrigin = InitialTimeBase();
|
||||
|
||||
uint64_t GetTimeBase() {
|
||||
const auto elapsed = std::chrono::steady_clock::now() - g_timeBaseStart;
|
||||
const auto nanoseconds =
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed).count();
|
||||
return TimeBaseContract::NanosecondsToTicks(static_cast<uint64_t>(nanoseconds));
|
||||
return g_timeBaseOrigin + TimeBaseContract::NanosecondsToTicks(static_cast<uint64_t>(nanoseconds));
|
||||
}
|
||||
|
||||
constexpr uint32_t kFpscrFx = 1u << 31;
|
||||
|
||||
@@ -309,9 +309,8 @@ void DrawWiiRemoteAccelerometer(uint32_t port) {
|
||||
} else {
|
||||
ImGui::TextDisabled("No accelerometer data yet.");
|
||||
}
|
||||
// SDL's read of the remote's calibration block often times out over Bluetooth
|
||||
// and it falls back to a nominal zero point, leaving a small per-axis bias;
|
||||
// measured here with the remote at rest.
|
||||
// Residual per-axis bias left after SDL's factory-calibrated decode, measured
|
||||
// here with the remote at rest.
|
||||
if (WiiRemoteInput::IsAccelCalibrating()) {
|
||||
ImGui::ProgressBar(WiiRemoteInput::AccelCalibrationProgress(), ImVec2(220.0f, 0.0f), "Hold still...");
|
||||
} else if (ImGui::Button("Calibrate (remote lying flat, buttons up)")) {
|
||||
@@ -333,7 +332,7 @@ void DrawWiiRemoteAccelerometer(uint32_t port) {
|
||||
const std::array<double, 3> offset = RuntimeConfigFile::WiiAccelOffset();
|
||||
ImGui::TextDisabled("Stored offset: x %+.3f y %+.3f z %+.3f g", offset[0], offset[1], offset[2]);
|
||||
} else {
|
||||
ImGui::TextDisabled("Not calibrated (using SDL's zero point; see console.log for \"fallback accelerometer calibration\").");
|
||||
ImGui::TextDisabled("No trim stored.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,10 +102,10 @@ bool g_accelOffsetLoaded = false;
|
||||
// sample may stray from the first one before the run is declared "moved".
|
||||
constexpr int kCalibrationSamples = 90;
|
||||
constexpr float kCalibrationMaxDeviationG = 0.15f;
|
||||
// |mean| outside this range means the remote was not at rest or the
|
||||
// accelerometer is far off its nominal scale; either way the offset is useless.
|
||||
constexpr float kCalibrationMinGravityG = 0.8f;
|
||||
constexpr float kCalibrationMaxGravityG = 1.2f;
|
||||
// A resting remote reads about 1 g and lies within 20 degrees of face up.
|
||||
constexpr double kCalibrationMinGravityG = 0.8;
|
||||
constexpr double kCalibrationMaxGravityG = 1.2;
|
||||
constexpr double kCalibrationMinTiltCosine = 0.9396926;
|
||||
|
||||
struct AccelCalibration {
|
||||
bool active = false;
|
||||
@@ -320,7 +320,8 @@ void StepAccelCalibration() {
|
||||
std::array<double, 3> mean{};
|
||||
for (int i = 0; i < 3; ++i) mean[i] = g_calibration.sum[i] / g_calibration.count;
|
||||
const double length = std::sqrt(mean[0] * mean[0] + mean[1] * mean[1] + mean[2] * mean[2]);
|
||||
if (length < kCalibrationMinGravityG || length > kCalibrationMaxGravityG || mean[1] < 0.5) {
|
||||
if (length < kCalibrationMinGravityG || length > kCalibrationMaxGravityG ||
|
||||
mean[1] / length < kCalibrationMinTiltCosine) {
|
||||
FinishAccelCalibration("Failed: the remote was not resting flat with the buttons up.");
|
||||
return;
|
||||
}
|
||||
@@ -602,15 +603,30 @@ void FillGraceSample(uint32_t chan, Kind kind, KpadSample& sample) {
|
||||
sample.hasClassic = kind == Kind::RemoteWithClassic;
|
||||
}
|
||||
|
||||
// SDL's stick axis (-32767..32767, y down) as WPADCLStatus carries it: WPAD
|
||||
// normalises every Classic Controller stick, whatever the report's resolution,
|
||||
// to a signed 10-bit value, -512..511 with 0 at the centre and +y up (RVL SDK
|
||||
// WPAD.h; wut's WPADStatusClassic documents the same range).
|
||||
// The patched SDL Wii driver posts Classic Controller sticks as WPAD's own
|
||||
// value times 64 (aurora-main/cmake/AuroraSDL3Patches.cmake), so WPADCLStatus
|
||||
// is the axis divided back: signed, 0 at the centre, -512..511, +y up.
|
||||
int16_t ClassicStickRaw(Sint16 axis, bool invert) {
|
||||
float value = static_cast<float>(axis) / 32767.0f;
|
||||
if (invert) value = -value;
|
||||
value = std::clamp(value, -1.0f, 1.0f);
|
||||
return static_cast<int16_t>(std::clamp(std::lround(value * 512.0f), -512L, 511L));
|
||||
const long value = invert ? -static_cast<long>(axis) : axis;
|
||||
return static_cast<int16_t>(std::clamp(std::lround(value / 64.0), -512L, 511L));
|
||||
}
|
||||
|
||||
// KPAD's default Classic Controller stick limits; the game uses the cross clamp.
|
||||
constexpr float kKpadStickMin = 60.0f;
|
||||
constexpr float kKpadStickMax = 308.0f;
|
||||
|
||||
// KPAD cross clamp: per-axis dead zone and scale, then the unit circle.
|
||||
void ClassicStickFloat(const int16_t raw[2], float out[2]) {
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const float magnitude = std::fabs(static_cast<float>(raw[i]));
|
||||
const float scaled = std::clamp((magnitude - kKpadStickMin) / (kKpadStickMax - kKpadStickMin), 0.0f, 1.0f);
|
||||
out[i] = raw[i] < 0 ? -scaled : scaled;
|
||||
}
|
||||
const float length = std::sqrt(out[0] * out[0] + out[1] * out[1]);
|
||||
if (length > 1.0f) {
|
||||
out[0] /= length;
|
||||
out[1] /= length;
|
||||
}
|
||||
}
|
||||
|
||||
// Samples buttons, accelerometers and the extension of the remote on a port.
|
||||
@@ -696,14 +712,12 @@ bool ReadKpadSample(uint32_t chan, KpadSample& sample) {
|
||||
const Sint16 ly = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
|
||||
const Sint16 rx = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX);
|
||||
const Sint16 ry = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY);
|
||||
sample.clLStick[0] = std::clamp(static_cast<float>(lx) / 32767.0f, -1.0f, 1.0f);
|
||||
sample.clLStick[1] = std::clamp(-static_cast<float>(ly) / 32767.0f, -1.0f, 1.0f);
|
||||
sample.clRStick[0] = std::clamp(static_cast<float>(rx) / 32767.0f, -1.0f, 1.0f);
|
||||
sample.clRStick[1] = std::clamp(-static_cast<float>(ry) / 32767.0f, -1.0f, 1.0f);
|
||||
sample.clLStickRaw[0] = ClassicStickRaw(lx, false);
|
||||
sample.clLStickRaw[1] = ClassicStickRaw(ly, true);
|
||||
sample.clRStickRaw[0] = ClassicStickRaw(rx, false);
|
||||
sample.clRStickRaw[1] = ClassicStickRaw(ry, true);
|
||||
ClassicStickFloat(sample.clLStickRaw, sample.clLStick);
|
||||
ClassicStickFloat(sample.clRStickRaw, sample.clRStick);
|
||||
// Only the full-press click of L/R reaches SDL; report it as a full pull.
|
||||
sample.clTriggerL = (sample.clHold & kClL) ? 255 : 0;
|
||||
sample.clTriggerR = (sample.clHold & kClR) ? 255 : 0;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#include "isa/ppc_isa_float.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
namespace {
|
||||
|
||||
bool SameBits(double lhs, double rhs)
|
||||
{
|
||||
return PpcBitCastToU64Inline(lhs) == PpcBitCastToU64Inline(rhs);
|
||||
}
|
||||
|
||||
bool CheckPair(double actual, float a0, float a1, float c0, float c1, float b0, float b1,
|
||||
bool subtract, const char* operation)
|
||||
{
|
||||
const float expected0 = subtract ? std::fma(a0, c0, -b0) : std::fma(a0, c0, b0);
|
||||
const float expected1 = subtract ? std::fma(a1, c1, -b1) : std::fma(a1, c1, b1);
|
||||
const double expected = PpcPackPairedInline(expected0, expected1);
|
||||
if (SameBits(actual, expected)) {
|
||||
return true;
|
||||
}
|
||||
std::fprintf(stderr, "%s produced the wrong paired lanes\n", operation);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CheckNegatedPair(double actual, float a0, float a1, float c0, float c1, float b0, float b1,
|
||||
bool subtract, const char* operation)
|
||||
{
|
||||
const float expected0 = -std::fma(a0, c0, subtract ? -b0 : b0);
|
||||
const float expected1 = -std::fma(a1, c1, subtract ? -b1 : b1);
|
||||
const double expected = PpcPackPairedInline(expected0, expected1);
|
||||
if (SameBits(actual, expected)) {
|
||||
return true;
|
||||
}
|
||||
std::fprintf(stderr, "%s produced the wrong paired lanes\n", operation);
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
const double a = PpcPackPairedInline(1.000000119f, -123.25f);
|
||||
const double c = PpcPackPairedInline(33554431.0f, 0.0625f);
|
||||
const double b = PpcPackPairedInline(-33554430.0f, 7.75f);
|
||||
|
||||
bool ok = true;
|
||||
ok &= CheckPair(PPC_PsMaddNoNiInline(a, c, b), 1.000000119f, -123.25f,
|
||||
33554431.0f, 0.0625f, -33554430.0f, 7.75f, false, "ps_madd");
|
||||
ok &= CheckPair(PPC_PsMsubNoNiInline(a, c, b), 1.000000119f, -123.25f,
|
||||
33554431.0f, 0.0625f, -33554430.0f, 7.75f, true, "ps_msub");
|
||||
ok &= CheckNegatedPair(PPC_PsNmaddInline(a, c, b), 1.000000119f, -123.25f,
|
||||
33554431.0f, 0.0625f, -33554430.0f, 7.75f, false, "ps_nmadd");
|
||||
ok &= CheckNegatedPair(PPC_PsNmsubNoNiInline(a, c, b), 1.000000119f, -123.25f,
|
||||
33554431.0f, 0.0625f, -33554430.0f, 7.75f, true, "ps_nmsub");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -225,13 +225,37 @@ public sealed partial class CxxLinearCodeGenerator
|
||||
|
||||
var labelNames = func.Blocks.ToDictionary(b => b.Label, b => SanitizeLabel(b.Label), StringComparer.OrdinalIgnoreCase);
|
||||
var instructionContinuationLabels = new Dictionary<uint, string>();
|
||||
var continuationCallCount = func.Blocks
|
||||
var continuationCallCount = func.Blocks.Sum(block =>
|
||||
{
|
||||
var term = block.Instructions.LastOrDefault();
|
||||
var emittedInstructionCount = term is IrBranch or IrJump or IrReturn or IrJumpTable or IrUndefined
|
||||
? Math.Max(0, block.Instructions.Count - 1)
|
||||
: block.Instructions.Count;
|
||||
var suppressed = suppressedInstructionMasks.TryGetValue(block.Label, out var mask) ? mask : null;
|
||||
return Enumerable.Range(0, emittedInstructionCount).Count(index =>
|
||||
{
|
||||
if ((suppressed is not null && index < suppressed.Length && suppressed[index]) ||
|
||||
block.Instructions[index] is not IrCall call ||
|
||||
!TryParseAddress(call.Target, out var target) ||
|
||||
TryGetInlineGuestThunkSpec(target, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return nonReturningCallTargets.Contains(target) ||
|
||||
(lrContinuationCallTargets.Contains(target) &&
|
||||
TryGetLocalFallthroughLr(block.Instructions, index, nonReturningCallTargets, lrContinuationCallTargets).HasValue);
|
||||
});
|
||||
});
|
||||
// Labels remain available for every recognized continuation target.
|
||||
// Sharing the terminal dispatcher is narrower: it only applies when
|
||||
// more than one emitted call can actually jump there.
|
||||
var needsInstructionContinuationLabels = func.Blocks
|
||||
.SelectMany(static block => block.Instructions)
|
||||
.OfType<IrCall>()
|
||||
.Count(call =>
|
||||
.Any(call =>
|
||||
TryParseAddress(call.Target, out var target) &&
|
||||
(nonReturningCallTargets.Contains(target) || lrContinuationCallTargets.Contains(target)));
|
||||
var needsInstructionContinuationLabels = continuationCallCount > 0;
|
||||
var shareLrContinuationDispatch = continuationCallCount > 1;
|
||||
if (needsInstructionContinuationLabels)
|
||||
{
|
||||
@@ -500,7 +524,7 @@ public sealed partial class CxxLinearCodeGenerator
|
||||
// Every call site has already reloaded the callee's state.
|
||||
// Keep the complete local target set, but emit it only once.
|
||||
body.AppendLine(" return;");
|
||||
body.AppendLine("[[maybe_unused]] lr_continuation_dispatch:");
|
||||
body.AppendLine("lr_continuation_dispatch:");
|
||||
EmitLocalLrContinuationDispatch(body, " ", labelNames);
|
||||
body.AppendLine(" if (TranslatedFunctionRegistry::FindByAddressPtr(ctx->lr) != nullptr) {");
|
||||
body.AppendLine(" InvokeIndirectCpu(ctx->lr, ctx);");
|
||||
|
||||
@@ -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,21 +1944,15 @@ public sealed partial class PpcLifter
|
||||
};
|
||||
|
||||
case "bltl":
|
||||
{
|
||||
var crField = "cr0";
|
||||
if (ops.Count > 0 && ops[0] is PpcConditionRegisterOperand crOp)
|
||||
{
|
||||
crField = NormalizeRegister(crOp.Name);
|
||||
}
|
||||
|
||||
return new IrInstruction[]
|
||||
{
|
||||
new IrAssign("lr", IrValue.Imm((int)ins.EndAddress)),
|
||||
new IrBranch("blt", TargetLabel(ins, validAddresses, preferFallthrough: false),
|
||||
$"0x{ins.EndAddress:X8}", crField)
|
||||
};
|
||||
}
|
||||
|
||||
case "bgtl":
|
||||
case "beql":
|
||||
case "bsol":
|
||||
case "bgel":
|
||||
case "blel":
|
||||
case "bnel":
|
||||
case "bnsl":
|
||||
case "bdnzl":
|
||||
case "bdzl":
|
||||
case "bcl":
|
||||
{
|
||||
var rawInstr = ReadRawInstruction(ins);
|
||||
@@ -1971,6 +1966,7 @@ public sealed partial class PpcLifter
|
||||
{
|
||||
instructions.Add(new IrBinary("ctr", IrValue.Register("ctr"), IrValue.Imm(-1), "add"));
|
||||
}
|
||||
instructions.Add(new IrAssign("lr", IrValue.Imm((int)ins.EndAddress)));
|
||||
instructions.Add(new IrBranch("raw", linkedTarget, fallthrough, BuildBoConditionExpression(bo, bi, allowCtr: true)));
|
||||
return instructions;
|
||||
}
|
||||
@@ -1992,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;
|
||||
}
|
||||
|
||||
@@ -2018,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":
|
||||
@@ -3047,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -184,4 +184,3 @@ public class EmittedOutputShapeTests
|
||||
Assert.DoesNotContain("loc_800E77A0:\n}", code.Replace("\r\n", "\n"), StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -21,14 +22,99 @@ public class PpcLifterAdditionalCoverageTests
|
||||
var ir = Assert.Single(new PpcLifter().Lift(new[] { branch })).Ir;
|
||||
|
||||
Assert.Equal("bltl", branch.Mnemonic);
|
||||
var lr = Assert.IsType<IrAssign>(ir[0]);
|
||||
Assert.Equal("lr", lr.Destination);
|
||||
Assert.Equal(unchecked((int)0x80004398u), lr.Value.Constant);
|
||||
Assert.Equal(2, ir.Count);
|
||||
|
||||
var lrAssign = Assert.IsType<IrAssign>(ir[0]);
|
||||
Assert.Equal("lr", lrAssign.Destination);
|
||||
Assert.Equal(unchecked((int)0x80004398u), lrAssign.Value.Constant);
|
||||
Assert.Equal(unchecked((int)branch.EndAddress), lrAssign.Value.Constant);
|
||||
|
||||
var decision = Assert.IsType<IrBranch>(ir[1]);
|
||||
Assert.Equal("blt", decision.Condition);
|
||||
Assert.Equal("0x800043BC", decision.TrueLabel);
|
||||
Assert.Equal("raw", decision.Condition);
|
||||
Assert.Equal("link_branch_80004398_800043BC", decision.TrueLabel);
|
||||
Assert.Equal("0x80004398", decision.FalseLabel);
|
||||
|
||||
Assert.Equal($"link_branch_{branch.EndAddress:X8}_{branch.BranchTargets.First():X8}", decision.TrueLabel);
|
||||
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]
|
||||
@@ -359,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));
|
||||
|
||||
@@ -9,6 +9,33 @@ namespace Translator.Tests;
|
||||
|
||||
public class SharedLrContinuationCodeGenTests
|
||||
{
|
||||
[Fact]
|
||||
public void OnlyLocalLinkRegisterContinuationsCountTowardSharedDispatch()
|
||||
{
|
||||
var function = new IrFunction("single_lr_continuation", "0x80001000", new[]
|
||||
{
|
||||
new IrBasicBlock("0x80001000", new IrInstruction[]
|
||||
{
|
||||
new IrCall(string.Empty, "0x81800000", Array.Empty<IrValue>()),
|
||||
new IrAssign("lr", IrValue.Imm(unchecked((int)0x80001004u))),
|
||||
new IrCall(string.Empty, "0x81800000", Array.Empty<IrValue>()),
|
||||
new IrReturn(null)
|
||||
})
|
||||
});
|
||||
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
|
||||
{
|
||||
["lr"] = ValueRepresentation.UInt32
|
||||
});
|
||||
|
||||
var code = new CxxLinearCodeGenerator().Emit(0x80001000,
|
||||
new SsaTransformer().Convert(function),
|
||||
new FunctionAbiClassification("single_lr_continuation", ValueRepresentation.Void), types,
|
||||
lrContinuationCallTargets: new HashSet<uint> { 0x81800000u });
|
||||
|
||||
Assert.DoesNotContain("lr_continuation_dispatch:", code);
|
||||
Assert.DoesNotContain("goto lr_continuation_dispatch;", code);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(2)]
|
||||
[InlineData(20)]
|
||||
|
||||
Reference in New Issue
Block a user