mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-10-17 12:48:15 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dbee74218 | |||
| 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
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.Text;
|
||||
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
/// <summary>The disc identity read from an extracted game's <c>sys/boot.bin</c>.</summary>
|
||||
public sealed record ExtractedGameHeader(string GameId, int Revision, string InternalName);
|
||||
|
||||
/// <summary>
|
||||
/// A game Wheel Wizard already extracted, in the layout Dolphin's "Extract Entire Disc" writes:
|
||||
/// <c><root>/DATA/{sys,files}</c>. WiiCompiled reads it in place (<c>dvd_root</c>) instead of extracting a
|
||||
/// copy of its own, so it is only ever read, never written or deleted.
|
||||
/// </summary>
|
||||
public static class ExtractedGameDirectory
|
||||
{
|
||||
private const int BootSize = 0x440;
|
||||
private const uint WiiMagic = 0x5D1C9EA3;
|
||||
|
||||
/// <summary>
|
||||
/// The <c>DATA</c> folder of an extracted game: <paramref name="path"/> may be the game root holding
|
||||
/// <c>DATA/</c>, or the <c>DATA</c> folder itself.
|
||||
/// </summary>
|
||||
public static string ResolveDataDirectory(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentException("--game-dir needs the folder of the extracted game.");
|
||||
var full = FileSystemUtilities.NormalizePath(path);
|
||||
if (!Directory.Exists(full))
|
||||
throw new DirectoryNotFoundException($"The extracted game folder does not exist: {full}");
|
||||
|
||||
foreach (var candidate in new[] { Path.Combine(full, "DATA"), full })
|
||||
{
|
||||
if (File.Exists(Path.Combine(candidate, "sys", "main.dol")) && File.Exists(Path.Combine(candidate, "sys", "boot.bin")))
|
||||
return FileSystemUtilities.NormalizePath(candidate);
|
||||
}
|
||||
|
||||
throw new InvalidDataException(
|
||||
$"{full} is not an extracted Wii game: expected DATA/sys/main.dol and DATA/sys/boot.bin.");
|
||||
}
|
||||
|
||||
/// <summary>Reads the disc header from <c>sys/boot.bin</c>, which starts with the same bytes as the disc.</summary>
|
||||
public static ExtractedGameHeader ReadHeader(string dataDirectory)
|
||||
{
|
||||
var boot = File.ReadAllBytes(Path.Combine(dataDirectory, "sys", "boot.bin"));
|
||||
if (boot.Length < BootSize)
|
||||
throw new InvalidDataException("The extracted game's sys/boot.bin is incomplete.");
|
||||
var magic = (uint)(boot[0x18] << 24 | boot[0x19] << 16 | boot[0x1A] << 8 | boot[0x1B]);
|
||||
if (magic != WiiMagic)
|
||||
throw new InvalidDataException("The extracted game is not a Wii game.");
|
||||
|
||||
var gameId = Encoding.ASCII.GetString(boot, 0, 6).TrimEnd('\0', ' ');
|
||||
var nameBytes = boot.AsSpan(0x20, 0x40);
|
||||
var end = nameBytes.IndexOf((byte)0);
|
||||
var name = Encoding.ASCII.GetString(end < 0 ? nameBytes : nameBytes[..end]).Trim();
|
||||
return new ExtractedGameHeader(gameId, boot[7], name);
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,54 @@ internal static class DiscTool
|
||||
reporter.Progress(InstallStages.ExtractDisc, "Disc validated and extracted", 6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks a game Wheel Wizard already extracted against the pinned clean disc and copies only its two code
|
||||
/// files into <paramref name="assetsDirectory"/> for translation. The game itself stays where it is; the
|
||||
/// runtime reads it in place. Returns its <c>DATA</c> folder.
|
||||
/// </summary>
|
||||
public static string ValidateExtracted(string gameDirectory, ProjectManifest manifest, string assetsDirectory,
|
||||
IInstallReporter reporter)
|
||||
{
|
||||
reporter.Progress(InstallStages.ExtractDisc, "Checking the extracted game", 2);
|
||||
var dataDir = ExtractedGameDirectory.ResolveDataDirectory(gameDirectory);
|
||||
var header = ExtractedGameDirectory.ReadHeader(dataDir);
|
||||
if (!string.Equals(header.GameId, manifest.GameId, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"This game is '{header.GameId}', not the expected '{manifest.GameId}' (Mario Kart Wii, region {manifest.Region}).");
|
||||
}
|
||||
|
||||
var dolPath = Path.Combine(dataDir, "sys", "main.dol");
|
||||
var relPath = Path.Combine(dataDir, "files", "rel", "StaticR.rel");
|
||||
if (!File.Exists(relPath)) throw new FileNotFoundException("The extracted game has no StaticR.rel", relPath);
|
||||
if (!string.Equals(Sha256Of(dolPath), manifest.DolSha256, StringComparison.Ordinal) ||
|
||||
!string.Equals(Sha256Of(relPath), manifest.RelSha256, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The extracted game's main.dol or StaticR.rel is not the clean game code this project is pinned to.");
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(assetsDirectory);
|
||||
File.Copy(dolPath, Path.Combine(assetsDirectory, "main.dol"), overwrite: true);
|
||||
File.Copy(relPath, Path.Combine(assetsDirectory, "StaticR.rel"), overwrite: true);
|
||||
reporter.Progress(InstallStages.ExtractDisc, "Extracted game validated", 6);
|
||||
return dataDir;
|
||||
}
|
||||
|
||||
/// <summary>Removes the workspace's own extraction once the extracted game is read in place instead.</summary>
|
||||
public static void RemoveOwnExtraction(string assetsDirectory, IInstallReporter reporter)
|
||||
{
|
||||
var own = Path.Combine(assetsDirectory, "DATA");
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(own)) Directory.Delete(own, recursive: true);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
reporter.Diagnostic("The game copy WiiCompiled no longer uses could not be removed yet: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> RunInfoAsync(string nodTool, string isoPath, CancellationToken cancellationToken)
|
||||
{
|
||||
var startInfo = new System.Diagnostics.ProcessStartInfo(nodTool)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -32,4 +32,10 @@ internal sealed class InstallState
|
||||
public int SchemaVersion { get; set; } = 1;
|
||||
public string Workspace { get; set; } = "";
|
||||
public List<ProductInstallRecord> Products { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The <c>DATA</c> folder of a game Wheel Wizard already extracted (<c>--game-dir</c>), read in place through
|
||||
/// <c>dvd_root</c>. Empty when the game was extracted into the workspace's own <c>Assets/DATA</c>.
|
||||
/// </summary>
|
||||
public string GameDataDir { get; set; } = "";
|
||||
}
|
||||
|
||||
@@ -11,6 +11,13 @@ internal static class Program
|
||||
// --workspace <cache> ahead of whatever the caller passed, so these can't assume position 0.
|
||||
if (args.Length == 0 || args.Contains("-h") || args.Contains("--help")) { PrintUsage(); return 0; }
|
||||
if (args.Contains("--version")) { Console.WriteLine(ProductInfo.Version); return 0; }
|
||||
// Lets Wheel Wizard tell what this AppImage supports before it asks; an older one rejects the flag.
|
||||
if (args.Contains("--capabilities"))
|
||||
{
|
||||
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(
|
||||
new { type = "capabilities", capabilities = new[] { "game-dir" } }));
|
||||
return 0;
|
||||
}
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
// Replaces CancellationSignal.cs's named-EventWaitHandle IPC (Windows-only): SIGINT/SIGTERM
|
||||
@@ -50,6 +57,9 @@ internal static class Program
|
||||
case "check-products":
|
||||
CheckProducts();
|
||||
break;
|
||||
case "link-game-dir":
|
||||
LinkGameDirectory(flags, reporter);
|
||||
break;
|
||||
default:
|
||||
Console.Error.WriteLine($"Unknown command: {command}");
|
||||
PrintUsage();
|
||||
@@ -99,10 +109,23 @@ internal static class Program
|
||||
var assetsDir = Path.Combine(workspace, "Assets");
|
||||
|
||||
reporter.Progress(InstallStages.Validate, "Checking prerequisites", 1);
|
||||
if (flags.TryGetValue("game", out var isoPath) && !string.IsNullOrEmpty(isoPath))
|
||||
var gameDirectory = flags.GetValueOrDefault("game-dir");
|
||||
flags.TryGetValue("game", out var isoPath);
|
||||
if (!string.IsNullOrEmpty(isoPath) && !string.IsNullOrEmpty(gameDirectory))
|
||||
throw new ArgumentException("Pass either --game or --game-dir, not both.");
|
||||
|
||||
var previousState = JsonState.TryRead<InstallState>(StatePath);
|
||||
// Where the runtime reads the game: an extracted game it was given or already linked, else its own copy.
|
||||
var gameDataDir = previousState?.GameDataDir ?? "";
|
||||
if (!string.IsNullOrEmpty(gameDirectory))
|
||||
{
|
||||
gameDataDir = DiscTool.ValidateExtracted(gameDirectory, manifest, assetsDir, reporter);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(isoPath))
|
||||
{
|
||||
await DiscTool.ValidateAndExtractAsync(isoPath, manifest, assetsDir, workspace,
|
||||
flags.GetValueOrDefault("disc-tool-bin"), reporter, token);
|
||||
gameDataDir = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -116,8 +139,9 @@ internal static class Program
|
||||
}
|
||||
}
|
||||
|
||||
var state = JsonState.TryRead<InstallState>(StatePath) ?? new InstallState { Workspace = workspace };
|
||||
var state = previousState ?? new InstallState { Workspace = workspace };
|
||||
state.Workspace = workspace;
|
||||
state.GameDataDir = gameDataDir;
|
||||
|
||||
var profile = installsRetro ? "both" : "base";
|
||||
var profiles = installsRetro ? new[] { "base", "retro-rewind" } : new[] { "base" };
|
||||
@@ -218,11 +242,13 @@ internal static class Program
|
||||
// per-user Config.toml (RuntimeConfiguration.ResolveConfigPath's Windows-only portable-root
|
||||
// lookup has nothing to find here either way).
|
||||
var configPath = RuntimeConfiguration.ApplicationDataConfigPath;
|
||||
var dataDir = Path.Combine(assetsDir, "DATA");
|
||||
var dataDir = string.IsNullOrEmpty(gameDataDir) ? Path.Combine(assetsDir, "DATA") : gameDataDir;
|
||||
if (Directory.Exists(dataDir))
|
||||
{
|
||||
RuntimeConfiguration.SetDvdRoot(configPath, dataDir);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(gameDataDir))
|
||||
DiscTool.RemoveOwnExtraction(assetsDir, reporter);
|
||||
if (installsRetro)
|
||||
{
|
||||
RuntimeConfiguration.SetRetroRewindRoot(configPath, retroDir!);
|
||||
@@ -231,6 +257,39 @@ internal static class Program
|
||||
reporter.Progress(InstallStages.Shortcuts, "Install complete", 99);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Points the installation at a game Wheel Wizard already extracted, without rebuilding: the game code is
|
||||
/// checked to be exactly what the installed products were built from, then <c>dvd_root</c> and the state name
|
||||
/// it and the workspace's own copy is removed.
|
||||
/// </summary>
|
||||
private static void LinkGameDirectory(Dictionary<string, string?> flags, IInstallReporter reporter)
|
||||
{
|
||||
var gameDirectory = flags.GetValueOrDefault("game-dir");
|
||||
if (string.IsNullOrEmpty(gameDirectory))
|
||||
throw new ArgumentException("link-game-dir needs --game-dir <folder>.");
|
||||
var state = JsonState.TryRead<InstallState>(StatePath);
|
||||
if (state is null || state.Products.Count == 0)
|
||||
throw new InvalidOperationException("WiiCompiled is not installed yet.");
|
||||
|
||||
var workspace = flags.GetValueOrDefault("workspace") ??
|
||||
(string.IsNullOrEmpty(state.Workspace) ? WorkspaceLocator.FindFrom(AppContext.BaseDirectory) : state.Workspace);
|
||||
var manifest = ProjectManifest.Load(Path.Combine(workspace, "projects", "mkwii", "recomp.yml"));
|
||||
var assetsDir = Path.Combine(workspace, "Assets");
|
||||
var dataDir = DiscTool.ValidateExtracted(gameDirectory, manifest, assetsDir, reporter);
|
||||
|
||||
var dolSha = Sha256Of(Path.Combine(dataDir, "sys", "main.dol"));
|
||||
var relSha = Sha256Of(Path.Combine(dataDir, "files", "rel", "StaticR.rel"));
|
||||
if (state.Products.Any(record => record.DolSha256 != dolSha || record.RelSha256 != relSha))
|
||||
throw new InvalidOperationException(
|
||||
"The installed products were built from different game code; install again with --game-dir instead.");
|
||||
|
||||
reporter.Progress(InstallStages.Shortcuts, "Pointing WiiCompiled at the extracted game", 70);
|
||||
RuntimeConfiguration.SetDvdRoot(RuntimeConfiguration.ApplicationDataConfigPath, dataDir);
|
||||
state.GameDataDir = dataDir;
|
||||
JsonState.Write(StatePath, state);
|
||||
DiscTool.RemoveOwnExtraction(assetsDir, reporter);
|
||||
}
|
||||
|
||||
private static void Uninstall()
|
||||
{
|
||||
// Matches Windows: UninstallService.cs removes the whole install directory unconditionally -
|
||||
@@ -351,7 +410,7 @@ internal static class Program
|
||||
Console.WriteLine("""
|
||||
Usage: wiicompiled-setup <command> [options]
|
||||
|
||||
install [--game ISO_PATH] [--install-dir DIR] [--retro-dir DIR
|
||||
install [--game ISO_PATH | --game-dir EXTRACTED_GAME] [--install-dir DIR] [--retro-dir DIR
|
||||
{--download-retro-wfc-payload | --skip-retro-wfc-payload}]
|
||||
[--force-clean-build] [--translator-bin PATH] [--disc-tool-bin PATH]
|
||||
[--cc PATH] [--cxx PATH] [--fuse-ld NAME_OR_PATH] [--cmake PATH] [--ninja PATH]
|
||||
@@ -360,7 +419,9 @@ internal static class Program
|
||||
launch-base
|
||||
launch-retro
|
||||
check-products
|
||||
link-game-dir --game-dir EXTRACTED_GAME [--progress-json]
|
||||
--version
|
||||
--capabilities
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ internal enum AppMode
|
||||
CheckProducts,
|
||||
RepairProducts,
|
||||
Version,
|
||||
EmitPayloadIdentities
|
||||
EmitPayloadIdentities,
|
||||
Capabilities,
|
||||
LinkGameDirectory
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -26,7 +28,12 @@ internal sealed record ModeRules
|
||||
public string Flag { get; init; } = "this command";
|
||||
public bool AcceptsProgressJson { get; init; }
|
||||
public bool AcceptsGame { get; init; }
|
||||
|
||||
/// <summary>The game is required, as either <c>--game <image></c> or <c>--game-dir <folder></c>.</summary>
|
||||
public bool RequiresGame { get; init; }
|
||||
|
||||
public bool AcceptsGameDirectory { get; init; }
|
||||
public bool RequiresGameDirectory { get; init; }
|
||||
public bool AcceptsRetroDirectory { get; init; }
|
||||
public bool RequiresRetroDirectory { get; init; }
|
||||
public bool AcceptsPayloadMode { get; init; }
|
||||
@@ -39,6 +46,7 @@ internal sealed class CommandLine
|
||||
{
|
||||
public AppMode Mode { get; private set; } = AppMode.Help;
|
||||
public string? GamePath { get; private set; }
|
||||
public string? GameDirectoryPath { get; private set; }
|
||||
public string? RetroDirectoryPath { get; private set; }
|
||||
public RetroWfcPayloadMode RetroWfcPayloadMode { get; private set; }
|
||||
public string? InstallDirectory { get; private set; }
|
||||
@@ -70,6 +78,9 @@ internal sealed class CommandLine
|
||||
case "--repair-products": result.Mode = AppMode.RepairProducts; break;
|
||||
case "--portable": result.Portable = true; break;
|
||||
case "--emit-payload-identities": result.Mode = AppMode.EmitPayloadIdentities; break;
|
||||
case "--capabilities": result.Mode = AppMode.Capabilities; break;
|
||||
case "--link-game-dir": result.Mode = AppMode.LinkGameDirectory; break;
|
||||
case "--game-dir": result.GameDirectoryPath = RequireValue(args, ref i); break;
|
||||
case "--payload-root": result.PayloadRootPath = RequireValue(args, ref i); break;
|
||||
case "--quiet": result.Quiet = true; break;
|
||||
case "--progress-json": result.ProgressJson = true; break;
|
||||
@@ -100,16 +111,22 @@ internal sealed class CommandLine
|
||||
[AppMode.SilentInstall] = new ModeRules
|
||||
{
|
||||
Flag = "--silent",
|
||||
AcceptsProgressJson = true, AcceptsGame = true, RequiresGame = true,
|
||||
AcceptsProgressJson = true, AcceptsGame = true, AcceptsGameDirectory = true, RequiresGame = true,
|
||||
AcceptsRetroDirectory = true, AcceptsPayloadMode = true,
|
||||
AcceptsPortable = true
|
||||
},
|
||||
[AppMode.VerifyInputs] = new ModeRules
|
||||
{
|
||||
Flag = "--verify-inputs",
|
||||
AcceptsProgressJson = true, AcceptsGame = true, RequiresGame = true,
|
||||
AcceptsProgressJson = true, AcceptsGame = true, AcceptsGameDirectory = true, RequiresGame = true,
|
||||
AcceptsRetroDirectory = true
|
||||
},
|
||||
[AppMode.LinkGameDirectory] = new ModeRules
|
||||
{
|
||||
Flag = "--link-game-dir",
|
||||
AcceptsProgressJson = true, AcceptsGameDirectory = true, RequiresGameDirectory = true,
|
||||
RequiresInstallDirectory = true
|
||||
},
|
||||
[AppMode.CheckProducts] = new ModeRules
|
||||
{
|
||||
Flag = "--check-products", AcceptsProgressJson = true, AcceptsRetroDirectory = true
|
||||
@@ -147,6 +164,9 @@ internal sealed class CommandLine
|
||||
}
|
||||
|
||||
Reject(GamePath is not null && !rules.AcceptsGame, "--game");
|
||||
Reject(GameDirectoryPath is not null && !rules.AcceptsGameDirectory, "--game-dir");
|
||||
if (GamePath is not null && GameDirectoryPath is not null)
|
||||
throw new ArgumentException("Pass either --game or --game-dir, not both.");
|
||||
Reject(RetroDirectoryPath is not null && !rules.AcceptsRetroDirectory, "--retro-dir");
|
||||
Reject(ProgressJson && !rules.AcceptsProgressJson, "--progress-json");
|
||||
Reject(PayloadRootPath is not null && !rules.RequiresPayloadRoot, "--payload-root");
|
||||
@@ -154,8 +174,10 @@ internal sealed class CommandLine
|
||||
Reject(RetroWfcPayloadMode != RetroWfcPayloadMode.NotApplicable && !rules.AcceptsPayloadMode,
|
||||
"A Retro-WFC payload option");
|
||||
|
||||
if (rules.RequiresGame && string.IsNullOrWhiteSpace(GamePath))
|
||||
throw new ArgumentException("--game is required.");
|
||||
if (rules.RequiresGame && string.IsNullOrWhiteSpace(GamePath) && string.IsNullOrWhiteSpace(GameDirectoryPath))
|
||||
throw new ArgumentException("--game or --game-dir is required.");
|
||||
if (rules.RequiresGameDirectory && string.IsNullOrWhiteSpace(GameDirectoryPath))
|
||||
throw new ArgumentException($"--game-dir is required with {rules.Flag}.");
|
||||
if (rules.RequiresRetroDirectory && string.IsNullOrWhiteSpace(RetroDirectoryPath))
|
||||
throw new ArgumentException(
|
||||
$"--retro-dir is required with {rules.Flag}. Supply Wheel Wizard's canonical Retro Rewind folder.");
|
||||
|
||||
@@ -11,8 +11,10 @@ internal static class ConsoleCommands
|
||||
Console.Out.WriteLine("Wheel Wizard is the graphical interface for installing and launching WiiCompiled.");
|
||||
Console.Out.WriteLine();
|
||||
Console.Out.WriteLine("Commands:");
|
||||
Console.Out.WriteLine(" --silent --game <image> --install-dir <dir> [--retro-dir <folder>] [--portable]");
|
||||
Console.Out.WriteLine(" --verify-inputs --game <image> [--retro-dir <folder>]");
|
||||
Console.Out.WriteLine(" --silent (--game <image> | --game-dir <folder>) --install-dir <dir> [--retro-dir <folder>] [--portable]");
|
||||
Console.Out.WriteLine(" --verify-inputs (--game <image> | --game-dir <folder>) [--retro-dir <folder>]");
|
||||
Console.Out.WriteLine(" --link-game-dir --install-dir <dir> --game-dir <folder> [--progress-json]");
|
||||
Console.Out.WriteLine(" --capabilities");
|
||||
Console.Out.WriteLine(" --check-products [--install-dir <dir>] [--retro-dir <folder>] [--progress-json]");
|
||||
Console.Out.WriteLine(" --repair-products --install-dir <dir> --retro-dir <folder> " +
|
||||
"(--download-retro-wfc-payload | --skip-retro-wfc-payload) [--progress-json]");
|
||||
@@ -28,6 +30,80 @@ internal static class ConsoleCommands
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What this setup supports, so Wheel Wizard can tell before it asks. A setup from before this command rejects
|
||||
/// it as unknown, which the caller reads as supporting nothing new.
|
||||
/// </summary>
|
||||
public static int Capabilities()
|
||||
{
|
||||
Console.Out.WriteLine(JsonSerializer.Serialize(new { type = "capabilities", capabilities = new[] { "game-dir" } }));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Points an existing installation at a game Wheel Wizard already extracted: <c>dvd_root</c> and the install
|
||||
/// state name it, and the installation's own extracted copy is removed. Nothing is rebuilt, since the game
|
||||
/// code it was built from is checked to be the same.
|
||||
/// </summary>
|
||||
public static int LinkGameDirectory(CommandLine command)
|
||||
{
|
||||
var reporter = command.ProgressJson ? new NdjsonInstallReporter() : null;
|
||||
var installation = new Installation(Path.GetFullPath(command.InstallDirectory!));
|
||||
try
|
||||
{
|
||||
if (!installation.IsPresent)
|
||||
throw new InvalidOperationException($"WiiCompiled is not installed at {installation.Root}.");
|
||||
|
||||
using var operationLock = InstallOperationLock.Acquire(installation.Root, reporter);
|
||||
PortableInstallHealing.HealMovedInstall(installation, reporter);
|
||||
var state = installation.ReadInstallState() is { SchemaVersion: 1 } current
|
||||
? current
|
||||
: throw new InvalidDataException("The installation has no current install state. Apply setup again.");
|
||||
|
||||
reporter?.Progress(InstallStages.Validate, "Checking the extracted game...", 20);
|
||||
var dataDirectory = ExtractedGameDirectory.ResolveDataDirectory(command.GameDirectoryPath!);
|
||||
var header = InputValidation.ReadDiscHeaderFromDirectory(dataDirectory);
|
||||
var installedManifest = JsonState.TryRead<PayloadManifest>(
|
||||
Path.Combine(installation.Root, InstalledLayout.PayloadManifestFileName));
|
||||
if (installedManifest is not null)
|
||||
InputValidation.EnsureCompatibleDisc(header, installedManifest);
|
||||
// The products were built from exactly this game code; anything else needs a rebuild, not a link.
|
||||
foreach (var (file, expected) in new[]
|
||||
{
|
||||
(Path.Combine(dataDirectory, "sys", "main.dol"), state.DolSha256),
|
||||
(Path.Combine(dataDirectory, "files", "rel", "StaticR.rel"), state.RelSha256),
|
||||
})
|
||||
{
|
||||
if (!File.Exists(file) ||
|
||||
!InputValidation.Sha256File(file).Equals(expected, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException(
|
||||
$"{Path.GetFileName(file)} in the extracted game is not the clean game code WiiCompiled was built from.");
|
||||
}
|
||||
|
||||
RunningProductGuard.EnsureProductsNotRunning(installation.Root);
|
||||
reporter?.Progress(InstallStages.Publish, "Pointing WiiCompiled at the extracted game...", 70);
|
||||
RuntimeConfiguration.SetDvdRoot(installation.ConfigPath, dataDirectory);
|
||||
state.GameDataDir = dataDirectory;
|
||||
JsonState.Write(installation.InstallStatePath, state);
|
||||
InstallerEngine.RemoveOwnGameAssets(installation, reporter ?? (IInstallReporter)new ConsoleInstallReporter(_ => { }));
|
||||
|
||||
if (reporter is null)
|
||||
Console.Out.WriteLine($"WiiCompiled now reads the game from {dataDirectory}.");
|
||||
reporter?.Success(installation.Root);
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex) when (reporter is not null)
|
||||
{
|
||||
reporter.Failure(ex.Message);
|
||||
Console.Error.WriteLine(ex);
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
reporter?.EnsureFinished("Linking the extracted game stopped before it reached a result.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static int EmitPayloadIdentities(CommandLine command)
|
||||
{
|
||||
@@ -68,7 +144,10 @@ internal static class ConsoleCommands
|
||||
payload.ExtractEntry(InstalledLayout.ToolkitEntryPrefix + "nodtool.exe", tool);
|
||||
payload.ExtractDirectory(InstalledLayout.ToolkitEntryPrefix + "Redist", temp);
|
||||
reporter?.Progress(InstallStages.Validate, "Checking the Wii disc image...", 10);
|
||||
var header = InputValidation.ReadDiscHeaderAsync(tool, command.GamePath!).GetAwaiter().GetResult();
|
||||
var header = command.GameDirectoryPath is null
|
||||
? InputValidation.ReadDiscHeaderAsync(tool, command.GamePath!).GetAwaiter().GetResult()
|
||||
: InputValidation.ReadDiscHeaderFromDirectory(
|
||||
ExtractedGameDirectory.ResolveDataDirectory(command.GameDirectoryPath));
|
||||
InputValidation.EnsureCompatibleDisc(header, manifest);
|
||||
|
||||
if (command.RetroDirectoryPath is not null)
|
||||
@@ -115,7 +194,8 @@ internal static class ConsoleCommands
|
||||
var engine = new InstallerEngine(reporter);
|
||||
await engine.InstallAsync(new InstallOptions
|
||||
{
|
||||
GamePath = command.GamePath!,
|
||||
GamePath = command.GamePath,
|
||||
GameDirectoryPath = command.GameDirectoryPath,
|
||||
RetroDirectoryPath = command.RetroDirectoryPath,
|
||||
RetroWfcPayloadMode = command.RetroWfcPayloadMode,
|
||||
InstallDirectory = installDirectory,
|
||||
|
||||
@@ -42,6 +42,19 @@ internal static class InputValidation
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>The disc header of a game Wheel Wizard already extracted, read from its <c>sys/boot.bin</c>.</summary>
|
||||
public static DiscHeader ReadDiscHeaderFromDirectory(string dataDirectory)
|
||||
{
|
||||
var header = ExtractedGameDirectory.ReadHeader(dataDirectory);
|
||||
return new DiscHeader
|
||||
{
|
||||
GameId = header.GameId,
|
||||
InternalName = header.InternalName,
|
||||
Region = RegionFromGameId(header.GameId),
|
||||
Revision = header.Revision,
|
||||
};
|
||||
}
|
||||
|
||||
private static string RegionFromGameId(string gameId) => gameId.Length >= 4
|
||||
? gameId[3] switch
|
||||
{
|
||||
|
||||
@@ -99,7 +99,17 @@ internal sealed class Installation
|
||||
public string BaseExecutable => Path.Combine(BaseDirectory, "WiiCompiled.exe");
|
||||
public string RetroDirectory => Path.Combine(Root, "RetroRewind");
|
||||
public string RetroExecutable => Path.Combine(RetroDirectory, "RetroRewind.exe");
|
||||
public string GameDataDirectory => Path.Combine(Root, "GameAssets", "DATA");
|
||||
/// <summary>
|
||||
/// Where the game data is read from: Wheel Wizard's extracted game when the installation was pointed at it,
|
||||
/// otherwise the extraction this installation keeps itself.
|
||||
/// </summary>
|
||||
public string GameDataDirectory =>
|
||||
ReadInstallState() is { GameDataDir.Length: > 0 } state ? state.GameDataDir : OwnGameDataDirectory;
|
||||
|
||||
/// <summary>The installation's own extraction, which only an installation given a disc image has.</summary>
|
||||
public string OwnGameDataDirectory => Path.Combine(Root, "GameAssets", "DATA");
|
||||
|
||||
public string OwnGameAssetsDirectory => Path.Combine(Root, "GameAssets");
|
||||
public string InstallStatePath => Path.Combine(Root, InstalledLayout.InstallStateFileName);
|
||||
public string ToolkitStatePath => Path.Combine(Root, ToolkitState.FileName);
|
||||
|
||||
|
||||
@@ -13,7 +13,12 @@ internal sealed class InstallerEngine
|
||||
var installDirectory = Path.GetFullPath(options.InstallDirectory);
|
||||
ValidateInstallDirectory(installDirectory);
|
||||
|
||||
InputValidation.ValidateExtension(options.GamePath);
|
||||
// A game Wheel Wizard already extracted is read in place; only a disc image is extracted here.
|
||||
var externalGameData = options.GameDirectoryPath is null
|
||||
? null
|
||||
: ExtractedGameDirectory.ResolveDataDirectory(options.GameDirectoryPath);
|
||||
if (externalGameData is null)
|
||||
InputValidation.ValidateExtension(options.GamePath ?? throw new ArgumentException("A game is required."));
|
||||
var parent = Directory.GetParent(installDirectory)?.FullName
|
||||
?? throw new InvalidOperationException("The installation directory has no parent.");
|
||||
Directory.CreateDirectory(parent);
|
||||
@@ -49,7 +54,8 @@ internal sealed class InstallerEngine
|
||||
var installedToolkitPackageFingerprint = existing.IsPresent
|
||||
? existing.ResolveToolkitPackageFingerprint()
|
||||
: "";
|
||||
var reusableGameAssets = FindReusableGameAssets(existing, manifest);
|
||||
var reusableGameAssets = externalGameData ?? FindReusableGameAssets(existing, manifest);
|
||||
var gameDataDir = GameDataDirFor(externalGameData, reusableGameAssets, previousState);
|
||||
|
||||
var sameToolkit = existing.IsPresent && reusableGameAssets is not null &&
|
||||
installedToolkitFingerprint.Equals(candidateToolkitFingerprint,
|
||||
@@ -84,9 +90,12 @@ internal sealed class InstallerEngine
|
||||
|
||||
var nodTool = extractToolkit ? Path.Combine(toolkit, "nodtool.exe") : installedNodTool;
|
||||
_reporter.Progress(InstallStages.Validate, "Checking the Wii disc image...", 2);
|
||||
var header = await InputValidation.ReadDiscHeaderAsync(nodTool, options.GamePath,
|
||||
cancellationToken);
|
||||
var header = externalGameData is null
|
||||
? await InputValidation.ReadDiscHeaderAsync(nodTool, options.GamePath!, cancellationToken)
|
||||
: InputValidation.ReadDiscHeaderFromDirectory(externalGameData);
|
||||
InputValidation.EnsureCompatibleDisc(header, manifest);
|
||||
if (externalGameData is not null)
|
||||
ValidateExtractedGame(externalGameData, manifest);
|
||||
var canonicalRetroRoot = options.RetroDirectoryPath is null
|
||||
? null
|
||||
: RetroRewindSource.ResolveRetroRewind6(options.RetroDirectoryPath);
|
||||
@@ -130,6 +139,7 @@ internal sealed class InstallerEngine
|
||||
|
||||
var updatedState = BuildInstallState(existing.ReadInstallState(), installDirectory, manifest,
|
||||
retroCompileInputs, canonicalRetroRoot, options.RetroWfcPayloadMode);
|
||||
updatedState.GameDataDir = gameDataDir;
|
||||
PrepareMetadata(staging, manifest, updatedState);
|
||||
remainingCancellation.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -158,16 +168,30 @@ internal sealed class InstallerEngine
|
||||
|
||||
if (reusableGameAssets is null)
|
||||
{
|
||||
await ExtractGameAssetsAsync(nodTool, options.GamePath,
|
||||
await ExtractGameAssetsAsync(nodTool, options.GamePath!,
|
||||
Path.Combine(staging, "GameAssets"), manifest, cancellationToken);
|
||||
}
|
||||
|
||||
await PublishToolkitAndReconcileProductsAsync(existing, staging, workspace, manifest,
|
||||
previousState, options, canonicalRetroRoot, retroCompileInputs,
|
||||
publishGameAssets: reusableGameAssets is null, createShortcuts: firstInstall,
|
||||
publishGameAssets: reusableGameAssets is null, gameDataDir, createShortcuts: firstInstall,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What <see cref="InstallState.GameDataDir"/> becomes: the extracted game this operation was given, or the
|
||||
/// one a previous operation linked when that is what is being reused; empty for the installation's own copy.
|
||||
/// </summary>
|
||||
private static string GameDataDirFor(string? externalGameData, string? reusableGameAssets, InstallState? previousState)
|
||||
{
|
||||
if (externalGameData is not null)
|
||||
return externalGameData;
|
||||
if (reusableGameAssets is not null && previousState is { GameDataDir.Length: > 0 } previous &&
|
||||
FileSystemUtilities.PathsEqual(reusableGameAssets, previous.GameDataDir))
|
||||
return previous.GameDataDir;
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
internal static bool MustRefreshToolkit(bool sameToolkit, bool samePackageContent,
|
||||
bool nodToolPresent) =>
|
||||
@@ -190,7 +214,7 @@ internal sealed class InstallerEngine
|
||||
string stagedWorkspace, PayloadManifest manifest, InstallState? previousState,
|
||||
InstallOptions options, string? canonicalRetroRoot,
|
||||
RetroRewindCompileInputs? retroCompileInputs,
|
||||
bool publishGameAssets, bool createShortcuts, CancellationToken cancellationToken)
|
||||
bool publishGameAssets, string gameDataDir, bool createShortcuts, CancellationToken cancellationToken)
|
||||
{
|
||||
var installDirectory = existing.Root;
|
||||
|
||||
@@ -208,6 +232,7 @@ internal sealed class InstallerEngine
|
||||
|
||||
var state = BuildInstallState(previousState, installDirectory, manifest,
|
||||
compileInputs: null, canonicalRetroRoot, options.RetroWfcPayloadMode);
|
||||
state.GameDataDir = gameDataDir;
|
||||
PrepareMetadata(staging, manifest, state);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -305,7 +330,9 @@ internal sealed class InstallerEngine
|
||||
using var transaction = InstallTransaction.Begin(installDirectory, _reporter, entries.ToArray());
|
||||
transaction.Publish();
|
||||
transaction.RecordRuntimeConfigurationMutation(configSnapshot);
|
||||
RuntimeConfiguration.SetDvdRoot(configPath, Path.Combine(installDirectory, "GameAssets", "DATA"));
|
||||
RuntimeConfiguration.SetDvdRoot(configPath, string.IsNullOrEmpty(state.GameDataDir)
|
||||
? Path.Combine(installDirectory, "GameAssets", "DATA")
|
||||
: state.GameDataDir);
|
||||
// The runtime scans this directory live for its asset overlay, so it is recorded on every
|
||||
// operation that receives one and is never rewritten by a base-only operation.
|
||||
if (canonicalRetroRoot is not null)
|
||||
@@ -336,9 +363,26 @@ internal sealed class InstallerEngine
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
// Reading Wheel Wizard's extracted game, the installation no longer needs a copy of its own.
|
||||
if (!string.IsNullOrEmpty(state.GameDataDir))
|
||||
RemoveOwnGameAssets(new Installation(installDirectory), _reporter);
|
||||
|
||||
_reporter.Progress(InstallStages.Publish, "Installation complete.", completionPercent);
|
||||
}
|
||||
|
||||
/// <summary>Deletes the installation's own extraction; it is only ever this installation's own folder.</summary>
|
||||
internal static void RemoveOwnGameAssets(Installation installation, IInstallReporter reporter)
|
||||
{
|
||||
try
|
||||
{
|
||||
FileSystemUtilities.DeleteDirectoryIfExists(installation.OwnGameAssetsDirectory);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
reporter.Diagnostic("The game copy WiiCompiled no longer uses could not be removed yet: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrepareMetadata(string staging, PayloadManifest manifest, InstallState state)
|
||||
{
|
||||
JsonState.Write(Path.Combine(staging, ToolkitState.FileName), new ToolkitState
|
||||
|
||||
@@ -12,7 +12,14 @@ internal enum RetroWfcPayloadMode
|
||||
|
||||
internal sealed class InstallOptions
|
||||
{
|
||||
public required string GamePath { get; init; }
|
||||
/// <summary>A disc image to extract. Exactly one of this and <see cref="GameDirectoryPath"/> is set.</summary>
|
||||
public string? GamePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// A game Wheel Wizard already extracted (its root, holding <c>DATA/</c>). It is read in place through
|
||||
/// <c>dvd_root</c>; nothing is extracted and no copy of it is kept in the installation.
|
||||
/// </summary>
|
||||
public string? GameDirectoryPath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The canonical Retro Rewind installation Wheel Wizard owns. Its resolved path is recorded as
|
||||
@@ -102,6 +109,13 @@ internal sealed class InstallState
|
||||
/// <c>[paths] retro_rewind_root</c>; this mirror lets uninstall drop only the setting it owns.
|
||||
/// </summary>
|
||||
public string RetroRewindRoot { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// The extracted game's <c>DATA</c> folder this installation reads in place (<c>--game-dir</c>). Empty for an
|
||||
/// installation that reads its own extraction in <c>GameAssets\DATA</c>. Wheel Wizard reads this to know
|
||||
/// whether WiiCompiled already uses its extracted game.
|
||||
/// </summary>
|
||||
public string GameDataDir { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -39,6 +39,12 @@ internal static class Program
|
||||
if (command.Mode == AppMode.Version)
|
||||
return ConsoleCommands.Version();
|
||||
|
||||
if (command.Mode == AppMode.Capabilities)
|
||||
return ConsoleCommands.Capabilities();
|
||||
|
||||
if (command.Mode == AppMode.LinkGameDirectory)
|
||||
return ConsoleCommands.LinkGameDirectory(command);
|
||||
|
||||
if (command.Mode == AppMode.Help)
|
||||
return ConsoleCommands.Help();
|
||||
|
||||
|
||||
@@ -53,6 +53,10 @@ internal static class SelfTests
|
||||
Test("Runtime configuration key matching", TestRuntimeConfigurationKeyMatching, failures);
|
||||
Test("Install location validation", TestInstallLocationValidation, failures);
|
||||
Test("Command line contract", TestCommandLineContract, failures);
|
||||
Test("Extracted game command line contract", TestGameDirectoryCommandLine, failures);
|
||||
Test("Extracted game directory", TestExtractedGameDirectory, failures);
|
||||
Test("Game data follows the install state", TestGameDataDirectoryFollowsState, failures);
|
||||
Test("Capabilities report", TestCapabilitiesReport, failures);
|
||||
Test("Portable root discovery", TestPortableRootDiscovery, failures);
|
||||
Test("Portable install validation failure leaves no marker",
|
||||
TestPortableValidationFailureLeavesNoMarker, failures);
|
||||
@@ -265,6 +269,139 @@ internal static class SelfTests
|
||||
}
|
||||
}
|
||||
|
||||
private static void TestGameDirectoryCommandLine()
|
||||
{
|
||||
void Rejects(string description, params string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
CommandLine.Parse(args);
|
||||
throw new Exception(description + " was accepted.");
|
||||
}
|
||||
catch (ArgumentException) { }
|
||||
}
|
||||
|
||||
var silent = CommandLine.Parse(["--silent", "--game-dir", "D:\\WW\\GAME", "--install-dir", "C:\\MKW", "--progress-json"]);
|
||||
if (silent.Mode != AppMode.SilentInstall || silent.GameDirectoryPath != "D:\\WW\\GAME" || silent.GamePath is not null)
|
||||
throw new Exception("An install from an extracted game did not parse.");
|
||||
|
||||
var link = CommandLine.Parse(["--link-game-dir", "--install-dir", "C:\\MKW", "--game-dir", "D:\\WW\\GAME", "--progress-json"]);
|
||||
if (link.Mode != AppMode.LinkGameDirectory || link.GameDirectoryPath != "D:\\WW\\GAME" || !link.ProgressJson)
|
||||
throw new Exception("The link command did not parse.");
|
||||
|
||||
if (CommandLine.Parse(["--capabilities"]).Mode != AppMode.Capabilities)
|
||||
throw new Exception("--capabilities was not recognised.");
|
||||
|
||||
Rejects("Both a disc image and an extracted game", "--silent", "--game", "g.iso", "--game-dir", "GAME", "--install-dir", "x");
|
||||
Rejects("An install with no game at all", "--silent", "--install-dir", "x");
|
||||
Rejects("A link without the extracted game", "--link-game-dir", "--install-dir", "x");
|
||||
Rejects("A link without an installation", "--link-game-dir", "--game-dir", "GAME");
|
||||
Rejects("A link from a disc image", "--link-game-dir", "--install-dir", "x", "--game-dir", "GAME", "--game", "g.iso");
|
||||
Rejects("--game-dir where nothing reads it", "--check-products", "--install-dir", "x", "--game-dir", "GAME");
|
||||
}
|
||||
|
||||
private static byte[] BootBin(string gameId)
|
||||
{
|
||||
var boot = new byte[0x440];
|
||||
System.Text.Encoding.ASCII.GetBytes(gameId).CopyTo(boot, 0);
|
||||
boot[7] = 2;
|
||||
BinaryPrimitives.WriteUInt32BigEndian(boot.AsSpan(0x18), 0x5D1C9EA3);
|
||||
System.Text.Encoding.ASCII.GetBytes("MARIO KART WII").CopyTo(boot, 0x20);
|
||||
return boot;
|
||||
}
|
||||
|
||||
private static void TestExtractedGameDirectory()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "mkwc-gamedir-" + Guid.NewGuid().ToString("N"));
|
||||
try
|
||||
{
|
||||
var sys = Path.Combine(root, "GAME", "DATA", "sys");
|
||||
Directory.CreateDirectory(sys);
|
||||
File.WriteAllBytes(Path.Combine(sys, "main.dol"), [1, 2, 3]);
|
||||
File.WriteAllBytes(Path.Combine(sys, "boot.bin"), BootBin("RMCP01"));
|
||||
|
||||
var fromRoot = ExtractedGameDirectory.ResolveDataDirectory(Path.Combine(root, "GAME"));
|
||||
var fromData = ExtractedGameDirectory.ResolveDataDirectory(Path.Combine(root, "GAME", "DATA"));
|
||||
if (!FileSystemUtilities.PathsEqual(fromRoot, Path.Combine(root, "GAME", "DATA")) ||
|
||||
!FileSystemUtilities.PathsEqual(fromData, fromRoot))
|
||||
throw new Exception("The DATA folder was not found from the game root or from itself.");
|
||||
|
||||
var header = InputValidation.ReadDiscHeaderFromDirectory(fromRoot);
|
||||
if (header.GameId != "RMCP01" || header.Revision != 2 || header.InternalName != "MARIO KART WII" ||
|
||||
header.Region != "PAL")
|
||||
throw new Exception("The extracted game's header was misread.");
|
||||
|
||||
try
|
||||
{
|
||||
ExtractedGameDirectory.ResolveDataDirectory(root);
|
||||
throw new Exception("A folder without a game was accepted.");
|
||||
}
|
||||
catch (InvalidDataException) { }
|
||||
|
||||
File.WriteAllBytes(Path.Combine(sys, "boot.bin"), new byte[0x440]);
|
||||
try
|
||||
{
|
||||
ExtractedGameDirectory.ReadHeader(fromRoot);
|
||||
throw new Exception("A boot.bin without the Wii magic was accepted.");
|
||||
}
|
||||
catch (InvalidDataException) { }
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TestGameDataDirectoryFollowsState()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "mkwc-gamedata-" + Guid.NewGuid().ToString("N"));
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(root);
|
||||
var installation = new Installation(root);
|
||||
if (!FileSystemUtilities.PathsEqual(installation.GameDataDirectory, installation.OwnGameDataDirectory))
|
||||
throw new Exception("An installation without state did not read its own game copy.");
|
||||
|
||||
var external = Path.Combine(root, "elsewhere", "GAME", "DATA");
|
||||
JsonState.Write(installation.InstallStatePath, new InstallState { InstallDir = root, GameDataDir = external });
|
||||
if (!FileSystemUtilities.PathsEqual(installation.GameDataDirectory, external))
|
||||
throw new Exception("A linked installation did not read the extracted game.");
|
||||
|
||||
// Removing the installation's own copy never reaches the extracted game it reads.
|
||||
Directory.CreateDirectory(Path.Combine(installation.OwnGameAssetsDirectory, "DATA"));
|
||||
Directory.CreateDirectory(external);
|
||||
File.WriteAllText(Path.Combine(external, "keep.bin"), "keep");
|
||||
InstallerEngine.RemoveOwnGameAssets(installation, new ConsoleInstallReporter(_ => { }));
|
||||
if (Directory.Exists(installation.OwnGameAssetsDirectory) || !File.Exists(Path.Combine(external, "keep.bin")))
|
||||
throw new Exception("Removing the own game copy removed the wrong folder.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TestCapabilitiesReport()
|
||||
{
|
||||
var output = new StringWriter();
|
||||
var original = Console.Out;
|
||||
Console.SetOut(output);
|
||||
try
|
||||
{
|
||||
if (ConsoleCommands.Capabilities() != 0)
|
||||
throw new Exception("--capabilities did not succeed.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(original);
|
||||
}
|
||||
|
||||
using var document = System.Text.Json.JsonDocument.Parse(output.ToString());
|
||||
if (document.RootElement.GetProperty("type").GetString() != "capabilities" ||
|
||||
!document.RootElement.GetProperty("capabilities").EnumerateArray().Any(item => item.GetString() == "game-dir"))
|
||||
throw new Exception("--capabilities did not report game-dir.");
|
||||
}
|
||||
|
||||
private static void TestCommandLineContract()
|
||||
{
|
||||
void Rejects(string description, params string[] args)
|
||||
|
||||
@@ -55,6 +55,10 @@ internal static class UninstallService
|
||||
TryCleanup("remove the installed game path from the runtime configuration", quiet,
|
||||
() => RuntimeConfiguration.RemoveDvdRootIfOwned(configPath,
|
||||
Path.Combine(installDirectory, "GameAssets", "DATA")));
|
||||
// A game Wheel Wizard extracted is only unregistered here; the folder itself is Wheel Wizard's, and stays.
|
||||
if (!string.IsNullOrEmpty(state?.GameDataDir))
|
||||
TryCleanup("remove the extracted game path from the runtime configuration", quiet,
|
||||
() => RuntimeConfiguration.RemoveDvdRootIfOwned(configPath, state.GameDataDir));
|
||||
}
|
||||
|
||||
// Remove only this install's Retro Rewind setting; leave user-owned paths untouched.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+24
-146
@@ -48,6 +48,24 @@ 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()
|
||||
|
||||
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 +103,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 +344,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 +395,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()
|
||||
@@ -85,9 +85,9 @@ target_link_libraries(mkw_runtime_common PRIVATE
|
||||
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 +135,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 +336,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 +354,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]]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);");
|
||||
|
||||
@@ -1943,21 +1943,6 @@ 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 "bcl":
|
||||
{
|
||||
var rawInstr = ReadRawInstruction(ins);
|
||||
@@ -1971,6 +1956,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;
|
||||
}
|
||||
|
||||
@@ -184,4 +184,3 @@ public class EmittedOutputShapeTests
|
||||
Assert.DoesNotContain("loc_800E77A0:\n}", code.Replace("\r\n", "\n"), StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,14 +21,20 @@ 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]
|
||||
|
||||
@@ -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