mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-10 09:11:52 -04:00
Merge pull request #37 from theofficialgman/main
Add Real (Native) Linux Support
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
[CmdletBinding(PositionalBinding = $false)]
|
||||
param(
|
||||
[string]$OutputDirectory = 'Launcher/dist',
|
||||
[string]$DolphinToolPath,
|
||||
[string]$PortableToolsDirectory = 'Launcher/artifacts/portable-tools',
|
||||
[string]$DependencySourceDirectory = 'Launcher/artifacts/dependencies',
|
||||
[string]$VcRuntimeDirectory,
|
||||
@@ -15,10 +14,6 @@ Set-StrictMode -Version 3.0
|
||||
# helpers shared with LocalBuild.ps1 and Prepare-NativePrebuilt.ps1.
|
||||
. (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1')
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($DolphinToolPath)) {
|
||||
throw 'Build-Installer.ps1 requires -DolphinToolPath pointing to DolphinTool.exe.'
|
||||
}
|
||||
|
||||
$repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
||||
$outputRoot = [IO.Path]::GetFullPath((Join-Path $repoRoot $OutputDirectory))
|
||||
$portableTools = [IO.Path]::GetFullPath((Join-Path $repoRoot $PortableToolsDirectory))
|
||||
@@ -26,7 +21,7 @@ $dependencySources = [IO.Path]::GetFullPath((Join-Path $repoRoot $DependencySour
|
||||
$workRoot = Join-Path $PSScriptRoot 'artifacts\installer-build'
|
||||
$publish = Join-Path $workRoot 'publish'
|
||||
$payloadRoot = Join-Path $workRoot 'payload'
|
||||
$setupProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup\WiiCompiled.Setup.csproj'
|
||||
$setupProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup.Windows\WiiCompiled.Setup.Windows.csproj'
|
||||
$translatorProject = Join-Path $repoRoot 'translator\src\Translator.Cli\Translator.Cli.csproj'
|
||||
$projectFile = Join-Path $repoRoot 'projects\mkwii\recomp.yml'
|
||||
|
||||
@@ -91,7 +86,6 @@ function Compress-Zip([string]$Source, [string]$Destination, [string[]]$Entries)
|
||||
|
||||
Assert-File $setupProject '.NET setup project'
|
||||
Assert-File $translatorProject 'Translator CLI project'
|
||||
Assert-File $DolphinToolPath 'DolphinTool'
|
||||
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $portableTools 'llvm-mingw\bin\x86_64-w64-mingw32-clang++.exe'))) {
|
||||
& (Join-Path $PSScriptRoot 'Prepare-PortableTools.ps1') -Destination $portableTools
|
||||
@@ -169,6 +163,15 @@ $translator = Join-Path $publish 'translator\Translator.Cli.exe'
|
||||
Assert-File $setupHost 'Published setup host'
|
||||
Assert-File $translator 'Self-contained translator'
|
||||
|
||||
# Resolved via the shared WiiCompiled.Setup.Common.Cli helper (also used by build-appimage.sh on
|
||||
# Linux) rather than a separate download/version-pin copy here: it downloads and caches the same way
|
||||
# NodToolProvider.cs always does (Launcher/artifacts/nodtool.exe), replacing the old manual
|
||||
# -DolphinToolPath handoff with an automated, pinned acquisition step.
|
||||
$nodToolCliProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup.Common.Cli'
|
||||
$nodTool = (& dotnet run --project $nodToolCliProject -c Release -- --workspace $repoRoot | Select-Object -Last 1)
|
||||
if ($LASTEXITCODE -ne 0) { throw "nodtool resolution failed with exit code $LASTEXITCODE." }
|
||||
Assert-File $nodTool 'Resolved nodtool'
|
||||
|
||||
Write-Host '[2/6] Staging the explicit, game-code-free payload allowlist...'
|
||||
# The staged layout mirrors the installed layout exactly (Toolkit, BuildWorkspace): payload
|
||||
# identities hash relative paths, so the names here are part of the fingerprint contract.
|
||||
@@ -191,7 +194,7 @@ Get-ChildItem -LiteralPath (Join-Path $toolkit 'llvm-mingw\bin') -File |
|
||||
Remove-Item -Force
|
||||
[IO.Directory]::CreateDirectory((Join-Path $toolkit 'Translator')) | Out-Null
|
||||
Copy-Item -LiteralPath $translator -Destination (Join-Path $toolkit 'Translator\Translator.Cli.exe')
|
||||
Copy-Item -LiteralPath $DolphinToolPath -Destination (Join-Path $toolkit 'DolphinTool.exe')
|
||||
Copy-Item -LiteralPath $nodTool -Destination (Join-Path $toolkit 'nodtool.exe')
|
||||
[IO.Directory]::CreateDirectory((Join-Path $toolkit 'Redist')) | Out-Null
|
||||
Copy-Item -Path (Join-Path $vcRuntime '*.dll') -Destination (Join-Path $toolkit 'Redist')
|
||||
Copy-Item -Path (Join-Path $vcRuntime '*.dll') -Destination (Join-Path $toolkit 'CMake\bin')
|
||||
@@ -225,18 +228,38 @@ Copy-Item (Join-Path $dependencySources 'cppwinrt\LICENSE.txt') (Join-Path $payl
|
||||
# The precompiled aurora/third-party archives are built from the very sources
|
||||
# already shipped under build-workspace\Dependencies and aurora-main, so they add
|
||||
# no third-party component and therefore no new license obligation.
|
||||
$dolphinLicense = Join-Path (Split-Path -Parent $DolphinToolPath) 'COPYING'
|
||||
if (Test-Path $dolphinLicense) { Copy-Item $dolphinLicense (Join-Path $payloadRoot 'licenses\Dolphin-COPYING.txt') }
|
||||
@"
|
||||
DolphinTool source offer
|
||||
nodtool (disc image extraction)
|
||||
|
||||
Project and complete corresponding source: https://github.com/dolphin-emu/dolphin
|
||||
Dolphin is licensed under GPLv2+ with additional per-file SPDX licenses.
|
||||
"@ | Set-Content (Join-Path $payloadRoot 'licenses\Dolphin-SOURCE.txt') -Encoding UTF8
|
||||
Project: https://github.com/encounter/nod
|
||||
Dual-licensed under MIT OR Apache-2.0.
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright 2021 Luke Street.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"@ | Set-Content (Join-Path $payloadRoot 'licenses\nodtool-LICENSE-MIT.txt') -Encoding UTF8
|
||||
@"
|
||||
Microsoft Visual C++ Runtime
|
||||
|
||||
Redistributable x64 runtime DLLs are included app-locally for DolphinTool and third-party renderer DLLs.
|
||||
Redistributable x64 runtime DLLs are included app-locally for nodtool and third-party renderer DLLs.
|
||||
Microsoft license terms: https://visualstudio.microsoft.com/license-terms/
|
||||
"@ | Set-Content (Join-Path $payloadRoot 'licenses\Microsoft-VC-Runtime.txt') -Encoding UTF8
|
||||
# Compute the payload's content identities once, here, with the same code every installed host
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
[CmdletBinding(PositionalBinding = $false)]
|
||||
param(
|
||||
[string]$DolphinToolPath,
|
||||
[string]$PortableToolsDirectory = 'Launcher/artifacts/portable-tools',
|
||||
[string]$DependencySourceDirectory = 'Launcher/artifacts/dependencies',
|
||||
[string]$VcRuntimeDirectory,
|
||||
@@ -10,12 +9,7 @@ param(
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 3.0
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($DolphinToolPath)) {
|
||||
throw 'Prepare-Release.ps1 requires -DolphinToolPath pointing to DolphinTool.exe.'
|
||||
}
|
||||
|
||||
$arguments = @{
|
||||
DolphinToolPath = $DolphinToolPath
|
||||
PortableToolsDirectory = $PortableToolsDirectory
|
||||
DependencySourceDirectory = $DependencySourceDirectory
|
||||
ToolkitReleaseTag = $ToolkitReleaseTag
|
||||
|
||||
@@ -18,7 +18,7 @@ $llvmReadobj = [System.IO.Path]::GetFullPath($LlvmReadobjPath)
|
||||
|
||||
$systemDlls = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||
@(
|
||||
'advapi32.dll', 'authz.dll', 'bcrypt.dll', 'combase.dll', 'comdlg32.dll', 'crypt32.dll',
|
||||
'advapi32.dll', 'authz.dll', 'bcrypt.dll', 'bcryptprimitives.dll', 'combase.dll', 'comdlg32.dll', 'crypt32.dll',
|
||||
'd3d11.dll', 'd3d12.dll', 'dbghelp.dll', 'dcomp.dll', 'dwrite.dll', 'dwmapi.dll',
|
||||
'dxgi.dll', 'gdi32.dll', 'imm32.dll',
|
||||
'iphlpapi.dll', 'kernel32.dll', 'mf.dll', 'mfplat.dll', 'mfreadwrite.dll', 'mfuuid.dll',
|
||||
|
||||
@@ -15,7 +15,8 @@ if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) {
|
||||
}
|
||||
$repoRoot = [IO.Path]::GetFullPath($RepositoryRoot)
|
||||
$launcher = Join-Path $repoRoot 'Launcher'
|
||||
$setup = Join-Path $launcher 'WiiCompiled.Setup'
|
||||
$setup = Join-Path $launcher 'WiiCompiled.Setup.Windows'
|
||||
$common = Join-Path $launcher 'WiiCompiled.Setup.Common'
|
||||
|
||||
$failures = [Collections.Generic.List[string]]::new()
|
||||
function Add-Failure([string]$Message) { $failures.Add($Message) }
|
||||
@@ -48,9 +49,11 @@ function Compare-Set([string[]]$Expected, [string[]]$Actual, [string]$ExpectedNa
|
||||
$pins = Get-MkwProjectPins (Join-Path $repoRoot 'projects\mkwii\recomp.yml')
|
||||
|
||||
# --- The Retro-WFC endpoint: recomp.yml owns it; the installer host pins the same string so a
|
||||
# --- redirected or rewritten endpoint cannot be fetched from.
|
||||
$inputValidation = Read-SourceFile (Join-Path $setup 'InputValidation.cs') 'InputValidation.cs'
|
||||
$hostUri = Get-CapturedValue $inputValidation 'CurrentRetroWfcPayloadUri\s*=\s*"([^"]+)"' `
|
||||
# --- redirected or rewritten endpoint cannot be fetched from. The literal lives in
|
||||
# --- WiiCompiled.Setup.Common (shared with WiiCompiled.Setup.Linux) - InputValidation.cs only
|
||||
# --- re-exports it as `= RetroWfcPayload.CurrentRetroWfcPayloadUri;`, no literal to capture there.
|
||||
$retroWfcPayload = Read-SourceFile (Join-Path $common 'RetroWfcPayload.cs') 'RetroWfcPayload.cs'
|
||||
$hostUri = Get-CapturedValue $retroWfcPayload 'CurrentRetroWfcPayloadUri\s*=\s*"([^"]+)"' `
|
||||
'The host Retro-WFC endpoint constant'
|
||||
if ($hostUri -cne $pins.RetroWfcPayloadUri) {
|
||||
Add-Failure "InputValidation.CurrentRetroWfcPayloadUri is '$hostUri' but recomp.yml pins '$($pins.RetroWfcPayloadUri)'."
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
// A packaging-time-only helper - never shipped, never run by an end user. Both
|
||||
// Launcher/build-appimage.sh and Launcher/Build-Installer.ps1 invoke this to obtain the nodtool
|
||||
// binary they bundle, so there is exactly one place (NodToolProvider) that knows the pinned
|
||||
// version/URL/platform-asset mapping, instead of a separate copy per packaging script.
|
||||
//
|
||||
// Usage: WiiCompiled.Setup.Common.Cli --workspace <repo-root>
|
||||
// Prints the resolved nodtool path to stdout.
|
||||
|
||||
string? workspace = null;
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
if (args[i] == "--workspace" && i + 1 < args.Length)
|
||||
{
|
||||
workspace = args[++i];
|
||||
}
|
||||
}
|
||||
|
||||
if (workspace is null)
|
||||
{
|
||||
Console.Error.WriteLine("Usage: WiiCompiled.Setup.Common.Cli --workspace <repo-root>");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var path = await NodToolProvider.ResolveAsync(workspace, CancellationToken.None);
|
||||
Console.WriteLine(path);
|
||||
return 0;
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace>
|
||||
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
|
||||
<Version>0.2.22</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description>
|
||||
<DebugType>embedded</DebugType>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
/// <summary>
|
||||
/// One entry of an exact regular directory tree. Directory topology is part of the content
|
||||
/// contract everywhere this walker is used: an empty directory can be a runtime-visible asset just
|
||||
/// as a regular file can be, so it must not disappear from a content identity or a staged copy.
|
||||
/// </summary>
|
||||
internal sealed record RegularTreeEntry(string RelativePath, string FullPath, bool IsDirectory,
|
||||
public sealed record RegularTreeEntry(string RelativePath, string FullPath, bool IsDirectory,
|
||||
bool IsEmptyDirectory, long Length);
|
||||
|
||||
internal static class FileSystemUtilities
|
||||
public static class FileSystemUtilities
|
||||
{
|
||||
public static void CopyDirectory(string source, string destination,
|
||||
CancellationToken cancellationToken = default)
|
||||
+12
-6
@@ -1,9 +1,9 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
/// <summary>Reads and atomically writes the small JSON state documents kept inside an installation.</summary>
|
||||
internal static class JsonState
|
||||
/// <summary>Reads and atomically writes the small JSON state documents each installer keeps.</summary>
|
||||
public static class JsonState
|
||||
{
|
||||
private static readonly JsonSerializerOptions ReadOptions = new() { PropertyNameCaseInsensitive = true };
|
||||
private static readonly JsonSerializerOptions WriteOptions = new() { WriteIndented = true };
|
||||
@@ -17,11 +17,17 @@ internal static class JsonState
|
||||
catch
|
||||
{
|
||||
// A truncated or hand-edited state document must degrade into "unknown", which every
|
||||
// caller already treats as "assume stale and rebuild", not into a failed launch.
|
||||
// caller already treats as "assume stale and rebuild", not into a crash.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Write<T>(string path, T value) =>
|
||||
FileSystemUtilities.WriteAtomic(path, JsonSerializer.Serialize(value, WriteOptions));
|
||||
public static void Write<T>(string path, T value)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrEmpty(directory)) Directory.CreateDirectory(directory);
|
||||
var tempPath = path + ".tmp-" + Guid.NewGuid().ToString("N");
|
||||
File.WriteAllText(tempPath, JsonSerializer.Serialize(value, WriteOptions));
|
||||
File.Move(tempPath, path, overwrite: true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
/// <summary>Disc metadata parsed from `nodtool info`'s stdout.</summary>
|
||||
public sealed record NodToolDiscInfo(string GameId, string Title, int Revision);
|
||||
|
||||
/// <summary>
|
||||
/// Parses the plain-text stdout of `nodtool info <iso>`. nodtool has no JSON output mode, but
|
||||
/// prints one unconditional disc-level Title/Game ID/Disc-Revision block (via its own
|
||||
/// `print_header`) before any per-partition breakdown - Wii discs also have differently-scoped
|
||||
/// "Title"/"Game ID" lines per update/channel partition further down, so the first match of each
|
||||
/// pattern is always the disc-level one both installers want.
|
||||
/// </summary>
|
||||
public static partial class NodToolInfoParser
|
||||
{
|
||||
public static NodToolDiscInfo Parse(string infoStdout)
|
||||
{
|
||||
var gameIdMatch = GameIdLine().Match(infoStdout);
|
||||
if (!gameIdMatch.Success)
|
||||
throw new InvalidOperationException("nodtool did not return disc metadata.");
|
||||
var titleMatch = TitleLine().Match(infoStdout);
|
||||
var revisionMatch = RevisionLine().Match(infoStdout);
|
||||
return new NodToolDiscInfo(
|
||||
GameId: gameIdMatch.Groups[1].Value,
|
||||
Title: titleMatch.Success ? titleMatch.Groups[1].Value : "",
|
||||
Revision: revisionMatch.Success ? int.Parse(revisionMatch.Groups[1].Value) : 0);
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"^Game ID: (\S+)", RegexOptions.Multiline)]
|
||||
private static partial Regex GameIdLine();
|
||||
|
||||
[GeneratedRegex(@"^Title: (.+)$", RegexOptions.Multiline)]
|
||||
private static partial Regex TitleLine();
|
||||
|
||||
[GeneratedRegex(@"^Disc \d+, Revision (\d+)", RegexOptions.Multiline)]
|
||||
private static partial Regex RevisionLine();
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the `nodtool` binary both installers use for Wii disc validation/extraction (see
|
||||
/// NodToolInfoParser.cs), replacing the earlier dependency on `dolphin-tool`/`DolphinTool.exe`. A
|
||||
/// caller can supply one directly; otherwise this downloads the matching prebuilt release binary
|
||||
/// from encounter/nod and caches it at Launcher/artifacts/nodtool[.exe].
|
||||
///
|
||||
/// Shared by: WiiCompiled.Setup.Linux/DiscTool.cs (falls back to this at end-user install time on
|
||||
/// a plain git checkout), and WiiCompiled.Setup.Common.Cli (invoked once at packaging time by both
|
||||
/// build-appimage.sh and Build-Installer.ps1 to acquire the copy each bundles).
|
||||
/// </summary>
|
||||
public static class NodToolProvider
|
||||
{
|
||||
public const string Version = "v2.0.0-alpha.10";
|
||||
|
||||
public static async Task<string> ResolveAsync(string workspace, CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheName = OperatingSystem.IsWindows() ? "nodtool.exe" : "nodtool";
|
||||
var cachePath = Path.Combine(workspace, "Launcher", "artifacts", cacheName);
|
||||
if (File.Exists(cachePath)) return cachePath;
|
||||
|
||||
var url = $"https://github.com/encounter/nod/releases/download/{Version}/{AssetName()}";
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!);
|
||||
var tempPath = cachePath + ".tmp";
|
||||
using (var http = new HttpClient())
|
||||
using (var response = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var fileStream = File.Create(tempPath);
|
||||
await response.Content.CopyToAsync(fileStream, cancellationToken);
|
||||
}
|
||||
File.Move(tempPath, cachePath, overwrite: true);
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
File.SetUnixFileMode(cachePath,
|
||||
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
|
||||
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
|
||||
UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
|
||||
}
|
||||
return cachePath;
|
||||
}
|
||||
|
||||
private static string AssetName()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return RuntimeInformation.OSArchitecture switch
|
||||
{
|
||||
Architecture.X64 => "nodtool-windows-x86_64.exe",
|
||||
Architecture.Arm64 => "nodtool-windows-arm64.exe",
|
||||
Architecture.X86 => "nodtool-windows-x86.exe",
|
||||
var other => throw new PlatformNotSupportedException($"No prebuilt nodtool release for Windows {other}"),
|
||||
};
|
||||
}
|
||||
return RuntimeInformation.OSArchitecture switch
|
||||
{
|
||||
Architecture.X64 => "nodtool-linux-x86_64",
|
||||
Architecture.Arm64 => "nodtool-linux-aarch64",
|
||||
Architecture.X86 => "nodtool-linux-i686",
|
||||
var other => throw new PlatformNotSupportedException($"No prebuilt nodtool release for Linux {other}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
+6
-54
@@ -1,4 +1,4 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
/// <summary>
|
||||
/// A portable installation is a self-contained directory tree the user can move or carry on removable media:
|
||||
@@ -9,8 +9,11 @@ namespace WiiCompiled.Setup;
|
||||
/// </code>
|
||||
/// The runtime finds the same root independently (<c>runtime/include/runtime_config.h</c>,
|
||||
/// <c>PortableRootDirectory</c>); this class must keep the same marker name, layout, and depth bound.
|
||||
/// Shared by both installers - Linux's CLI has no <c>--portable</c> flag, so it only ever calls
|
||||
/// <see cref="TryFind"/>/<see cref="UserDataDirectory"/>/<see cref="Contains"/> (always missing,
|
||||
/// since it never creates a marker file), not <see cref="Create"/>.
|
||||
/// </summary>
|
||||
internal static class PortableRoot
|
||||
public static class PortableRoot
|
||||
{
|
||||
public const string MarkerFileName = "portable.txt";
|
||||
public const string UserDataDirectoryName = "UserData";
|
||||
@@ -72,7 +75,7 @@ internal static class PortableRoot
|
||||
if (!File.Exists(marker))
|
||||
{
|
||||
File.WriteAllText(marker,
|
||||
$"{ProductInfo.Name} portable installation." + Environment.NewLine +
|
||||
"WiiCompiled portable installation." + Environment.NewLine +
|
||||
"This marker makes the runtime keep Config.toml, NAND, Cache, and Logs in UserData\\ " +
|
||||
"beside it instead of in %LOCALAPPDATA%." + Environment.NewLine +
|
||||
"Delete it to make this installation use per-user application data again." +
|
||||
@@ -90,54 +93,3 @@ internal static class PortableRoot
|
||||
|
||||
private static string Normalize(string path) => FileSystemUtilities.NormalizePath(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A portable root can be moved or renamed between operations. Every installed-host operation that
|
||||
/// reads <c>install-state.json</c> passes through here first so exactly one place decides what a
|
||||
/// moved installation means, and so a non-portable installation is never touched.
|
||||
/// </summary>
|
||||
internal static class PortableInstallHealing
|
||||
{
|
||||
/// <summary>
|
||||
/// Reconciles a moved portable installation with its recorded location: the state file adopts the
|
||||
/// directory it was actually found in, and the native build tree is discarded because its
|
||||
/// CMake cache holds absolute paths from the old location. Returns whether anything was healed.
|
||||
/// </summary>
|
||||
public static bool HealMovedInstall(Installation installation, IInstallReporter? reporter = null)
|
||||
{
|
||||
// Guard: an ordinary installation that disagrees with its state file is a real problem for
|
||||
// the operation to report, not something to silently rewrite.
|
||||
if (PortableRoot.TryFind(installation.Root) is null) return false;
|
||||
|
||||
var state = installation.ReadInstallState();
|
||||
if (state is not { SchemaVersion: 1 } || string.IsNullOrWhiteSpace(state.InstallDir)) return false;
|
||||
|
||||
string recorded;
|
||||
try
|
||||
{
|
||||
recorded = FileSystemUtilities.NormalizePath(state.InstallDir);
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException)
|
||||
{
|
||||
recorded = state.InstallDir;
|
||||
}
|
||||
if (recorded.Equals(installation.Root, StringComparison.OrdinalIgnoreCase)) return false;
|
||||
|
||||
var previous = state.InstallDir;
|
||||
state.InstallDir = installation.Root;
|
||||
JsonState.Write(installation.InstallStatePath, state);
|
||||
|
||||
// The configured native build directory bakes absolute source, toolchain, and output paths
|
||||
// into CMakeCache.txt. After a move it is unusable and would fail the next configure rather
|
||||
// than being reused, so it is removed and reconfigured from scratch on the next build.
|
||||
var nativeBuild = Path.Combine(installation.WorkspaceDirectory, "native-build");
|
||||
var hadNativeBuild = Directory.Exists(nativeBuild);
|
||||
if (hadNativeBuild) FileSystemUtilities.DeleteDirectoryIfExists(nativeBuild);
|
||||
|
||||
reporter?.Diagnostic(
|
||||
$"This portable installation moved from {previous} to {installation.Root}. " +
|
||||
"The recorded location was updated" +
|
||||
(hadNativeBuild ? " and the location-bound native build cache was discarded." : "."));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -1,16 +1,17 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the one canonical Retro Rewind install. Wheel Wizard owns and passes it as
|
||||
/// <c>--retro-dir</c>; the backend only resolves, reads, and records it, never packages or copies it.
|
||||
/// <c>--retro-dir</c>; each installer only resolves, reads, and records it, never packages or
|
||||
/// copies it.
|
||||
/// </summary>
|
||||
internal static class RetroRewindSource
|
||||
public static class RetroRewindSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves the <c>RetroRewind6</c> folder from a selection that may be the folder itself or a
|
||||
/// parent containing exactly one <c>RetroRewind6/Binaries/Code.pul</c>.
|
||||
/// </summary>
|
||||
internal static string ResolveRetroRewind6(string selected)
|
||||
public static string ResolveRetroRewind6(string selected)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(selected))
|
||||
throw new InvalidDataException("Choose the canonical Retro Rewind folder.");
|
||||
+17
-110
@@ -1,20 +1,28 @@
|
||||
using System.Diagnostics;
|
||||
using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
internal static class InputValidation
|
||||
/// <summary>
|
||||
/// One validated, content-identified download in operation-owned scratch space. Callers use this
|
||||
/// exact directory for both the update decision and any resulting build.
|
||||
/// </summary>
|
||||
public sealed record RetroWfcPayloadSnapshot(string Directory, string Sha256, long ByteLength);
|
||||
|
||||
/// <summary>
|
||||
/// Downloads and verifies the Retro-WFC payload (a small, RSA-signed blob served from a single
|
||||
/// fixed endpoint). Shared by both installers - moved here from WiiCompiled.Setup.Windows's
|
||||
/// InputValidation.cs, which keeps every one of these method names as thin forwarding wrappers so
|
||||
/// its many existing call sites (ProductRepairService.cs, LocalBuildService.cs, Installation.cs,
|
||||
/// SelfTests.cs) needed no changes.
|
||||
/// </summary>
|
||||
public static class RetroWfcPayload
|
||||
{
|
||||
private const long MaximumRetroWfcPayloadBytes = 16L * 1024 * 1024;
|
||||
// The payload is tens of kilobytes from a single fixed endpoint 30s is good.
|
||||
private static readonly TimeSpan RetroWfcDownloadTimeout = TimeSpan.FromSeconds(30);
|
||||
private static readonly TimeSpan RetroWfcRetryDelay = TimeSpan.FromSeconds(1);
|
||||
private static readonly HashSet<string> SupportedDiscImageExtensions = new(
|
||||
[".iso", ".gcm", ".gcz", ".ciso", ".wbfs", ".wia", ".rvz"],
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public const string CurrentRetroWfcPayloadUri = "http://nas.play.rwfc.net/payload?g=RMCPD00";
|
||||
private static readonly string RetroWfcOfflinePayloadFile =
|
||||
@@ -37,43 +45,6 @@ internal static class InputValidation
|
||||
private const int RetroWfcPayloadSignatureOffset = 0x10;
|
||||
private const int RetroWfcPayloadMinimumBytes = 0x130;
|
||||
|
||||
public static void ValidateExtension(string gamePath)
|
||||
{
|
||||
if (!File.Exists(gamePath))
|
||||
throw new FileNotFoundException("The selected game image does not exist.", gamePath);
|
||||
var extension = Path.GetExtension(gamePath);
|
||||
if (!SupportedDiscImageExtensions.Contains(extension))
|
||||
throw new InvalidDataException(
|
||||
"Select a complete Wii disc image in ISO, GCM, GCZ, CISO, WBFS, WIA, or RVZ format.");
|
||||
}
|
||||
|
||||
public static async Task<DiscHeader> ReadDiscHeaderAsync(string dolphinTool, string gamePath,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ValidateExtension(gamePath);
|
||||
var result = await ProcessRunner.RunAsync(dolphinTool,
|
||||
["header", "-i", Path.GetFullPath(gamePath), "-j"], null, cancellationToken);
|
||||
if (result.ExitCode != 0)
|
||||
throw new InvalidDataException("DolphinTool could not read this disc image. " + result.CombinedOutput.Trim());
|
||||
|
||||
var json = result.StandardOutput.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
|
||||
.FirstOrDefault(line => line.TrimStart().StartsWith('{'));
|
||||
if (json is null)
|
||||
throw new InvalidDataException("DolphinTool did not return disc metadata.");
|
||||
return JsonSerializer.Deserialize<DiscHeader>(json)
|
||||
?? throw new InvalidDataException("DolphinTool returned invalid disc metadata.");
|
||||
}
|
||||
|
||||
public static void EnsureCompatibleDisc(DiscHeader header, PayloadManifest manifest)
|
||||
{
|
||||
if (!header.GameId.Equals(manifest.ExpectedGameId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"This build supports Mario Kart Wii PAL ({manifest.ExpectedGameId}). " +
|
||||
$"The selected image is {header.GameId} ({header.InternalName}, {header.Region}).");
|
||||
}
|
||||
}
|
||||
|
||||
public static string ValidateStagedRetroWfcPayloadDirectory(string stagedDirectory,
|
||||
RSAParameters? signingKey = null)
|
||||
{
|
||||
@@ -186,7 +157,7 @@ internal static class InputValidation
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsTransientRetroWfcDownloadFailure(Exception exception,
|
||||
public static bool IsTransientRetroWfcDownloadFailure(Exception exception,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested) return false;
|
||||
@@ -232,73 +203,9 @@ internal static class InputValidation
|
||||
"The Retro-WFC payload is not signed by the pinned Retro-WFC signing key.");
|
||||
}
|
||||
|
||||
public static string Sha256File(string path)
|
||||
private static string Sha256File(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record ProcessResult(int ExitCode, string StandardOutput, string StandardError)
|
||||
{
|
||||
public string CombinedOutput => StandardOutput + Environment.NewLine + StandardError;
|
||||
}
|
||||
|
||||
internal static class ProcessRunner
|
||||
{
|
||||
/// <summary>Runs a redirected child process to completion. <paramref name="configure"/> sets up a
|
||||
/// working directory or scrubbed environment; <paramref name="capture"/> is off for callers that only
|
||||
/// forward output live, so a build's output isn't buffered in memory for nobody to read.</summary>
|
||||
public static async Task<ProcessResult> RunAsync(string executable, IReadOnlyList<string> arguments,
|
||||
Action<string>? output, CancellationToken cancellationToken,
|
||||
Action<ProcessStartInfo>? configure = null, bool capture = true,
|
||||
Action<Exception>? onTerminationFailure = null)
|
||||
{
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
FileName = executable,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
foreach (var argument in arguments) info.ArgumentList.Add(argument);
|
||||
configure?.Invoke(info);
|
||||
|
||||
using var process = new Process { StartInfo = info, EnableRaisingEvents = true };
|
||||
var stdout = new List<string>();
|
||||
var stderr = new List<string>();
|
||||
process.OutputDataReceived += (_, e) => { if (e.Data is not null) { if (capture) stdout.Add(e.Data); output?.Invoke(e.Data); } };
|
||||
process.ErrorDataReceived += (_, e) => { if (e.Data is not null) { if (capture) stderr.Add(e.Data); output?.Invoke(e.Data); } };
|
||||
if (!process.Start()) throw new InvalidOperationException($"Could not start {executable}.");
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
await WaitForExitAsync(process, cancellationToken, onTerminationFailure);
|
||||
return new ProcessResult(process.ExitCode, string.Join(Environment.NewLine, stdout),
|
||||
string.Join(Environment.NewLine, stderr));
|
||||
}
|
||||
|
||||
public static async Task WaitForExitAsync(Process process, CancellationToken cancellationToken,
|
||||
Action<Exception>? onTerminationFailure = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!process.HasExited) process.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
onTerminationFailure?.Invoke(ex);
|
||||
}
|
||||
await process.WaitForExitAsync(CancellationToken.None);
|
||||
process.WaitForExit();
|
||||
throw;
|
||||
}
|
||||
process.WaitForExit();
|
||||
}
|
||||
}
|
||||
+18
-16
@@ -1,16 +1,19 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Common;
|
||||
|
||||
internal sealed record RuntimeConfigSnapshot(bool Existed, byte[] Contents);
|
||||
public sealed record RuntimeConfigSnapshot(bool Existed, byte[] Contents);
|
||||
|
||||
/// <summary>
|
||||
/// Reads and writes the runtime's <c>Config.toml</c>. Every entry point takes the file it operates on
|
||||
/// since its location depends on the installation (portable <c>UserData</c> vs. per-user app data);
|
||||
/// callers obtain it once via <see cref="ResolveConfigPath"/>.
|
||||
/// callers obtain it once via <see cref="ResolveConfigPath"/>. Shared by both installers - Linux
|
||||
/// never creates a <see cref="PortableRoot.MarkerFileName"/> marker file, so
|
||||
/// <see cref="ResolveConfigPath"/>/<see cref="FormatPathValue"/>'s portable-root lookups always miss
|
||||
/// there and this degrades to the same plain per-user-app-data, always-absolute-path behavior a
|
||||
/// non-portable Windows install already gets.
|
||||
/// </summary>
|
||||
internal static class RuntimeConfiguration
|
||||
public static class RuntimeConfiguration
|
||||
{
|
||||
public const string ConfigFileName = "Config.toml";
|
||||
|
||||
@@ -51,7 +54,7 @@ internal static class RuntimeConfiguration
|
||||
File.Delete(configPath);
|
||||
}
|
||||
|
||||
internal static void SetDvdRoot(string configPath, string dvdRoot) =>
|
||||
public static void SetDvdRoot(string configPath, string dvdRoot) =>
|
||||
SetPath(configPath, "dvd_root", dvdRoot);
|
||||
|
||||
/// <summary>
|
||||
@@ -59,21 +62,21 @@ internal static class RuntimeConfiguration
|
||||
/// asset overlay by scanning this directory live at launch, so an asset-only Retro Rewind update
|
||||
/// needs no backend work: the next launch simply reads the new files.
|
||||
/// </summary>
|
||||
internal static void SetRetroRewindRoot(string configPath, string retroRewindRoot) =>
|
||||
public static void SetRetroRewindRoot(string configPath, string retroRewindRoot) =>
|
||||
SetPath(configPath, "retro_rewind_root", retroRewindRoot);
|
||||
|
||||
/// <summary>The canonical Retro Rewind root, or null when no installation has recorded one.</summary>
|
||||
public static string? GetRetroRewindRoot(string configPath) =>
|
||||
GetResolvedPath(configPath, "retro_rewind_root");
|
||||
|
||||
internal static void RemoveRetroRewindRootIfOwned(string configPath, string retroRewindRoot) =>
|
||||
public static void RemoveRetroRewindRootIfOwned(string configPath, string retroRewindRoot) =>
|
||||
RemovePathIfOwned(configPath, "retro_rewind_root", retroRewindRoot);
|
||||
|
||||
internal static void RemoveDvdRootIfOwned(string configPath, string dvdRoot) =>
|
||||
public static void RemoveDvdRootIfOwned(string configPath, string dvdRoot) =>
|
||||
RemovePathIfOwned(configPath, "dvd_root", dvdRoot);
|
||||
|
||||
/// <summary>The raw stored text of a <c>[paths]</c> key, exactly as the file holds it.</summary>
|
||||
internal static string? GetPath(string configPath, string key) =>
|
||||
public static string? GetPath(string configPath, string key) =>
|
||||
TryUnquoteToml(GetRawValue(configPath, "paths", key) ?? "", out var value) ? value : null;
|
||||
|
||||
/// <summary>
|
||||
@@ -81,17 +84,17 @@ internal static class RuntimeConfiguration
|
||||
/// <c>[paths]</c> value against the directory holding <c>Config.toml</c> (never the working
|
||||
/// directory), so the host must resolve it the same way before comparing or reading it.
|
||||
/// </summary>
|
||||
internal static string? GetResolvedPath(string configPath, string key)
|
||||
public static string? GetResolvedPath(string configPath, string key)
|
||||
{
|
||||
var stored = GetPath(configPath, key);
|
||||
return string.IsNullOrWhiteSpace(stored) ? null : ResolveAgainstConfig(configPath, stored);
|
||||
}
|
||||
|
||||
internal static string ConfigDirectory(string configPath) =>
|
||||
public static string ConfigDirectory(string configPath) =>
|
||||
Path.GetDirectoryName(Path.GetFullPath(configPath))
|
||||
?? throw new InvalidOperationException($"{configPath} has no containing directory.");
|
||||
|
||||
internal static string ResolveAgainstConfig(string configPath, string value) =>
|
||||
public static string ResolveAgainstConfig(string configPath, string value) =>
|
||||
Path.GetFullPath(value, ConfigDirectory(configPath));
|
||||
|
||||
private static void SetPath(string configPath, string key, string value)
|
||||
@@ -150,7 +153,7 @@ internal static class RuntimeConfiguration
|
||||
}
|
||||
|
||||
/// <summary>The raw TOML literal stored for a key, or null when the section or key is absent.</summary>
|
||||
internal static string? GetRawValue(string configPath, string section, string key)
|
||||
public static string? GetRawValue(string configPath, string section, string key)
|
||||
{
|
||||
if (!File.Exists(configPath)) return null;
|
||||
var header = $"[{section}]";
|
||||
@@ -265,7 +268,7 @@ internal static class RuntimeConfiguration
|
||||
private static string QuoteToml(string value) =>
|
||||
"\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
|
||||
|
||||
internal static bool TryUnquoteToml(string value, out string result)
|
||||
public static bool TryUnquoteToml(string value, out string result)
|
||||
{
|
||||
result = "";
|
||||
if (value.Length < 2) return false;
|
||||
@@ -292,5 +295,4 @@ internal static class RuntimeConfiguration
|
||||
result = builder.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
|
||||
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
|
||||
<Version>0.2.22</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description>
|
||||
<DebugType>embedded</DebugType>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,81 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
/// <summary>
|
||||
/// Invokes Launcher/local-build.sh and turns its stdout into progress reports. Replaces
|
||||
/// LocalBuildService.cs's hardcoded Windows PowerShell 5.1 invocation - there is no PowerShell
|
||||
/// dependency here at all, just bash.
|
||||
/// </summary>
|
||||
internal static class BuildRunner
|
||||
{
|
||||
public static async Task RunAsync(
|
||||
string workspace, string profile, string outputDir, string? baseOutputDir,
|
||||
string? retroDir, string? retroWfcOfflineDir, bool skipRetroWfcPayload,
|
||||
bool forceCleanBuild, string? translatorBin, IInstallReporter reporter, CancellationToken cancellationToken)
|
||||
{
|
||||
var script = Path.Combine(workspace, "Launcher", "local-build.sh");
|
||||
if (!File.Exists(script)) throw new FileNotFoundException("local-build.sh is missing", script);
|
||||
|
||||
var startInfo = new ProcessStartInfo("bash")
|
||||
{
|
||||
WorkingDirectory = workspace,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
startInfo.ArgumentList.Add(script);
|
||||
startInfo.ArgumentList.Add("--profile"); startInfo.ArgumentList.Add(profile);
|
||||
startInfo.ArgumentList.Add("--output-dir"); startInfo.ArgumentList.Add(outputDir);
|
||||
if (!string.IsNullOrEmpty(baseOutputDir))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--base-output-dir"); startInfo.ArgumentList.Add(baseOutputDir);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(retroDir))
|
||||
{
|
||||
// Still forwarded to local-build.sh under its own internal name -
|
||||
// --retro-rewind-package-dir - matching LocalBuild.ps1's own -RetroRewindPackageDirectory.
|
||||
startInfo.ArgumentList.Add("--retro-rewind-package-dir"); startInfo.ArgumentList.Add(retroDir);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(retroWfcOfflineDir))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--retro-wfc-offline-dir"); startInfo.ArgumentList.Add(retroWfcOfflineDir);
|
||||
}
|
||||
if (skipRetroWfcPayload) startInfo.ArgumentList.Add("--skip-retro-wfc-payload");
|
||||
if (forceCleanBuild) startInfo.ArgumentList.Add("--force-clean-build");
|
||||
if (!string.IsNullOrEmpty(translatorBin))
|
||||
{
|
||||
startInfo.ArgumentList.Add("--translator-bin"); startInfo.ArgumentList.Add(translatorBin);
|
||||
}
|
||||
|
||||
using var process = new Process { StartInfo = startInfo };
|
||||
var window = new BuildProgressWindow(reporter, InstallStages.Build, start: 6, end: 96);
|
||||
|
||||
process.OutputDataReceived += (_, e) => { if (e.Data is not null) window.Observe(e.Data); };
|
||||
process.ErrorDataReceived += (_, e) => { if (e.Data is not null) reporter.Diagnostic(e.Data); };
|
||||
|
||||
process.Start();
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
KillProcessTree(process);
|
||||
throw;
|
||||
}
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"local-build.sh failed (exit {process.ExitCode}). See diagnostics above.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void KillProcessTree(Process process)
|
||||
{
|
||||
try { process.Kill(entireProcessTree: true); } catch { /* best-effort */ }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
/// <summary>
|
||||
/// freedesktop.org .desktop application-menu entries. Replaces ShellIntegration.cs's registry
|
||||
/// uninstall entry (no Linux analogue for an unpackaged tool - Windows already skips that step for
|
||||
/// portable installs, this just applies that same behavior universally) and .lnk shortcuts.
|
||||
/// </summary>
|
||||
internal static class DesktopEntry
|
||||
{
|
||||
private static string ApplicationsDirectory =>
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "applications");
|
||||
|
||||
private static string PathFor(string profile) =>
|
||||
Path.Combine(ApplicationsDirectory, $"wiicompiled-{profile}.desktop");
|
||||
|
||||
public static void Create(string profile, string displayName, string exePath)
|
||||
{
|
||||
// exePath is the installed native runtime binary itself (e.g.
|
||||
// .../Install/Base/WiiCompiled) - each profile already gets its own .desktop file here,
|
||||
// so there is no need to route through the setup tool's own launch-base/launch-retro
|
||||
// subcommand dispatch first. Unquoted: the Desktop Entry spec's Exec grammar doesn't take
|
||||
// a bare '"'-wrapped path, and none is needed here anyway - the only part of this path
|
||||
// that varies is the username, which Unix forbids containing whitespace.
|
||||
Directory.CreateDirectory(ApplicationsDirectory);
|
||||
var contents =
|
||||
"[Desktop Entry]\n" +
|
||||
"Type=Application\n" +
|
||||
$"Name={displayName}\n" +
|
||||
$"Exec={exePath}\n" +
|
||||
"Categories=Game;\n" +
|
||||
"Terminal=false\n";
|
||||
File.WriteAllText(PathFor(profile), contents);
|
||||
}
|
||||
|
||||
public static void Remove(string profile)
|
||||
{
|
||||
var path = PathFor(profile);
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using System.Security.Cryptography;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
/// <summary>
|
||||
/// Validates and extracts the user's own Mario Kart Wii disc via `nodtool` (see
|
||||
/// WiiCompiled.Setup.Common/NodToolProvider.cs) - a prebuilt, MIT/Apache-2.0-licensed CLI from
|
||||
/// encounter/nod, replacing the earlier dependency on a system-installed `dolphin-tool`
|
||||
/// (GPL-2.0-or-later, and not reliably packaged standalone by every distro).
|
||||
/// </summary>
|
||||
internal static class DiscTool
|
||||
{
|
||||
public static async Task ValidateAndExtractAsync(
|
||||
string isoPath, ProjectManifest manifest, string assetsDirectory, string workspace,
|
||||
string? nodToolBin, IInstallReporter reporter, CancellationToken cancellationToken)
|
||||
{
|
||||
var nodTool = nodToolBin ?? await NodToolProvider.ResolveAsync(workspace, cancellationToken);
|
||||
|
||||
// `nodtool info` only decodes the disc/partition headers (milliseconds); `nodtool extract`
|
||||
// copies the whole data partition to disk (tens of seconds for a custom-track-heavy MKWii
|
||||
// ISO). Checking the game ID first, before extracting, means a wrong disc fails fast -
|
||||
// matching the original dolphin-tool `header` step this replaces.
|
||||
reporter.Progress(InstallStages.ExtractDisc, "Reading the disc header", 2);
|
||||
var info = NodToolInfoParser.Parse(await RunInfoAsync(nodTool, isoPath, cancellationToken));
|
||||
if (!string.Equals(info.GameId, manifest.GameId, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"This disc is '{info.GameId}', not the expected '{manifest.GameId}' (Mario Kart Wii, region {manifest.Region}). " +
|
||||
"Only your own legally-owned copy of that exact game/region can be used.");
|
||||
}
|
||||
|
||||
// Extracted straight into Assets/DATA (kept, not a scratch dir) - the runtime reads course/
|
||||
// texture/audio data from this directory live via [paths] dvd_root, not just at translation
|
||||
// time, so it has to survive past this install (see Program.cs, which points dvd_root here).
|
||||
reporter.Progress(InstallStages.ExtractDisc, "Extracting the disc image", 4);
|
||||
var dataDir = Path.Combine(assetsDirectory, "DATA");
|
||||
if (Directory.Exists(dataDir)) Directory.Delete(dataDir, recursive: true);
|
||||
await RunExtractAsync(nodTool, isoPath, dataDir, cancellationToken);
|
||||
|
||||
var dolPath = Path.Combine(dataDir, "sys", "main.dol");
|
||||
var relPath = Path.Combine(dataDir, "files", "rel", "StaticR.rel");
|
||||
if (!File.Exists(dolPath)) throw new FileNotFoundException("nodtool did not produce main.dol", dolPath);
|
||||
if (!File.Exists(relPath)) throw new FileNotFoundException("nodtool did not produce StaticR.rel", relPath);
|
||||
|
||||
var dolSha = Sha256Of(dolPath);
|
||||
var relSha = Sha256Of(relPath);
|
||||
if (!string.Equals(dolSha, manifest.DolSha256, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"main.dol sha256 mismatch: expected {manifest.DolSha256}, got {dolSha}. " +
|
||||
"This disc revision does not match what this project's manifest is pinned to.");
|
||||
}
|
||||
if (!string.Equals(relSha, manifest.RelSha256, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"StaticR.rel sha256 mismatch: expected {manifest.RelSha256}, got {relSha}. " +
|
||||
"This disc revision does not match what this project's manifest 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, "Disc validated and extracted", 6);
|
||||
}
|
||||
|
||||
private static async Task<string> RunInfoAsync(string nodTool, string isoPath, CancellationToken cancellationToken)
|
||||
{
|
||||
var startInfo = new System.Diagnostics.ProcessStartInfo(nodTool)
|
||||
{
|
||||
ArgumentList = { "info", isoPath },
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
using var process = System.Diagnostics.Process.Start(startInfo)
|
||||
?? throw new InvalidOperationException($"Failed to start {nodTool}.");
|
||||
var stdout = await process.StandardOutput.ReadToEndAsync(cancellationToken);
|
||||
var stderr = await process.StandardError.ReadToEndAsync(cancellationToken);
|
||||
await process.WaitForExitAsync(cancellationToken);
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"nodtool could not read this disc image (exit {process.ExitCode}): {stderr}{stdout}".Trim());
|
||||
}
|
||||
return stdout;
|
||||
}
|
||||
|
||||
private static string Sha256Of(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static async Task RunExtractAsync(string nodTool, string isoPath, string outDir, CancellationToken cancellationToken)
|
||||
{
|
||||
var startInfo = new System.Diagnostics.ProcessStartInfo(nodTool)
|
||||
{
|
||||
ArgumentList = { "extract", isoPath, outDir, "-q" },
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
|
||||
using var process = System.Diagnostics.Process.Start(startInfo)
|
||||
?? throw new InvalidOperationException($"Failed to start {nodTool}.");
|
||||
var stdout = await process.StandardOutput.ReadToEndAsync(cancellationToken);
|
||||
var stderr = await process.StandardError.ReadToEndAsync(cancellationToken);
|
||||
await process.WaitForExitAsync(cancellationToken);
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"nodtool extract {isoPath} failed (exit {process.ExitCode}): {stderr}{stdout}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
// Ported near-verbatim from Launcher/WiiCompiled.Setup/InstallProgress.cs: this whole file is
|
||||
// platform-neutral (System.Text.Json + Console only), so the NDJSON --progress-json wire protocol
|
||||
// stays byte-for-byte the same shape a future GUI already speaks on Windows.
|
||||
|
||||
/// <summary>
|
||||
/// Stable stage identifiers reported by <c>--progress-json</c>. Kept intentionally small for this
|
||||
/// lean Linux installer (no toolkit-extraction/publish-transaction stages, since there is no
|
||||
/// bundled toolkit or staged workspace copy here - see the plan's "operate on a git checkout"
|
||||
/// scoping decision).
|
||||
/// </summary>
|
||||
internal static class InstallStages
|
||||
{
|
||||
public const string Validate = "validate";
|
||||
public const string ExtractDisc = "extract-disc";
|
||||
public const string Build = "build";
|
||||
public const string Shortcuts = "shortcuts";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Where an installation reports what it is doing. Progress is coarse and monotonic; raw translator
|
||||
/// and compiler output is a diagnostic, never progress, because it is unbounded and machine-hostile.
|
||||
/// </summary>
|
||||
internal interface IInstallReporter
|
||||
{
|
||||
void Progress(string stage, string message, int percent);
|
||||
void Diagnostic(string line);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <c>--progress-json</c> protocol: one JSON object per line on stdout, nothing else on stdout,
|
||||
/// diagnostics on stderr. The terminal <c>result</c> line is written exactly once.
|
||||
/// </summary>
|
||||
internal sealed class NdjsonInstallReporter : IInstallReporter
|
||||
{
|
||||
private static readonly JsonSerializerOptions Options = new() { WriteIndented = false };
|
||||
private readonly object _gate = new();
|
||||
private int _lastPercent;
|
||||
private bool _finished;
|
||||
|
||||
public void Progress(string stage, string message, int percent)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_finished) return;
|
||||
// Percentages are clamped monotonic: a caller's progress bar must never walk backwards
|
||||
// because a later stage happened to estimate a lower number.
|
||||
_lastPercent = Math.Clamp(Math.Max(percent, _lastPercent), 0, 99);
|
||||
WriteLine(new { type = "progress", stage, message, percent = _lastPercent });
|
||||
}
|
||||
}
|
||||
|
||||
public void Diagnostic(string line) => Console.Error.WriteLine(line);
|
||||
|
||||
public void Success(string installDirectory)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_finished) return;
|
||||
_finished = true;
|
||||
WriteLine(new { type = "result", success = true, version = ProductInfo.Version, installDir = installDirectory });
|
||||
}
|
||||
}
|
||||
|
||||
public void Failure(string error)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_finished) return;
|
||||
_finished = true;
|
||||
WriteLine(new { type = "result", success = false, error });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The terminal result line is the caller's only completion signal, so no exit path may skip it.
|
||||
/// Callers invoke this from a finally block; it is a no-op once a result was already written.
|
||||
/// </summary>
|
||||
public void EnsureFinished(string errorIfUnfinished) => Failure(errorIfUnfinished);
|
||||
|
||||
private static void WriteLine(object value)
|
||||
{
|
||||
Console.Out.WriteLine(JsonSerializer.Serialize(value, Options));
|
||||
Console.Out.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Plain-text console reporting for a run without <c>--progress-json</c>.</summary>
|
||||
internal sealed class ConsoleInstallReporter : IInstallReporter
|
||||
{
|
||||
public void Progress(string stage, string message, int percent) =>
|
||||
Console.Out.WriteLine($"[{percent,3}%] {message}");
|
||||
|
||||
public void Diagnostic(string line) => Console.Out.WriteLine(line);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build step identifiers from local-build.sh's <c>MKWCBUILD:STEP:<id></c> lines - the id is
|
||||
/// the contract, matched against Launcher/local-build.sh's log_step() call sites.
|
||||
/// </summary>
|
||||
internal static class BuildStepIds
|
||||
{
|
||||
public const string BuildTranslator = "build-translator";
|
||||
public const string ReuseBaseTranslation = "reuse-base-translation";
|
||||
public const string RetranslateBase = "retranslate-base";
|
||||
public const string TranslateBase = "translate-base";
|
||||
public const string EmitBaseManifest = "emit-base-manifest";
|
||||
public const string TranslateMod = "translate-mod";
|
||||
public const string GenerateDataInit = "generate-data-init";
|
||||
public const string EmitBuildShards = "emit-build-shards";
|
||||
public const string ConfigureNative = "configure-native";
|
||||
public const string Compile = "compile";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps one local-build.sh run onto a slice of the overall percentage. local-build.sh announces
|
||||
/// every step it starts with an <c>MKWCBUILD:</c> prefix, so the slice can advance on real events
|
||||
/// instead of on a timer.
|
||||
/// </summary>
|
||||
internal sealed class BuildProgressWindow
|
||||
{
|
||||
private const string Marker = "MKWCBUILD:";
|
||||
private const string StepMarker = "STEP:";
|
||||
|
||||
/// <summary>The fraction the compile step reaches; beyond it, compiler output is a heartbeat.</summary>
|
||||
private const double CompileFraction = 0.58;
|
||||
|
||||
private static readonly (string Id, double Fraction, string Message)[] Steps =
|
||||
[
|
||||
(BuildStepIds.BuildTranslator, 0.04, "Building the translator"),
|
||||
(BuildStepIds.ReuseBaseTranslation, 0.30, "Reusing the completed base translation"),
|
||||
(BuildStepIds.RetranslateBase, 0.08, "The base translation is stale; retranslating it"),
|
||||
(BuildStepIds.TranslateBase, 0.10, "Translating Mario Kart Wii"),
|
||||
(BuildStepIds.EmitBaseManifest, 0.34, "Creating the translation manifest"),
|
||||
(BuildStepIds.TranslateMod, 0.38, "Translating the Retro Rewind Code.pul"),
|
||||
(BuildStepIds.GenerateDataInit, 0.44, "Generating game data initialization"),
|
||||
(BuildStepIds.EmitBuildShards, 0.48, "Preparing the native build"),
|
||||
(BuildStepIds.ConfigureNative, 0.52, "Configuring the compiler"),
|
||||
(BuildStepIds.Compile, CompileFraction, "Compiling the game. This is the longest step"),
|
||||
];
|
||||
|
||||
private readonly IInstallReporter _reporter;
|
||||
private readonly string _stage;
|
||||
private readonly int _start;
|
||||
private readonly int _end;
|
||||
private double _fraction;
|
||||
private string _message = "Preparing the local build";
|
||||
private int _reportedPercent = -1;
|
||||
|
||||
public BuildProgressWindow(IInstallReporter reporter, string stage, int start, int end)
|
||||
{
|
||||
_reporter = reporter;
|
||||
_stage = stage;
|
||||
_start = start;
|
||||
_end = end;
|
||||
}
|
||||
|
||||
public void Observe(string line)
|
||||
{
|
||||
var index = line.IndexOf(Marker, StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
{
|
||||
var text = line[(index + Marker.Length)..].Trim();
|
||||
if (text.StartsWith(StepMarker, StringComparison.Ordinal))
|
||||
{
|
||||
var identifier = text[StepMarker.Length..];
|
||||
var end = identifier.IndexOf(' ');
|
||||
if (end >= 0) identifier = identifier[..end];
|
||||
foreach (var (id, fraction, message) in Steps)
|
||||
{
|
||||
if (!id.Equals(identifier, StringComparison.Ordinal)) continue;
|
||||
if (fraction > _fraction)
|
||||
{
|
||||
_fraction = fraction;
|
||||
_message = message;
|
||||
Emit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Anything else - a plain MKWCBUILD note, or raw tool output - stays a diagnostic and only
|
||||
// feeds the heartbeat 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)
|
||||
{
|
||||
_fraction = Math.Min(0.97, _fraction + 0.0015);
|
||||
Emit();
|
||||
}
|
||||
}
|
||||
|
||||
private void Emit()
|
||||
{
|
||||
var percent = Interpolate(_fraction);
|
||||
if (percent == _reportedPercent) return;
|
||||
_reportedPercent = percent;
|
||||
_reporter.Progress(_stage, _message, percent);
|
||||
}
|
||||
|
||||
private int Interpolate(double fraction) =>
|
||||
(int)Math.Round(_start + (_end - _start) * Math.Clamp(fraction, 0, 1));
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
internal static class ProductInfo
|
||||
{
|
||||
public const string Name = "WiiCompiled";
|
||||
public const string Version = "0.2.22";
|
||||
}
|
||||
|
||||
/// <summary>One installed product's record inside install-state.json.</summary>
|
||||
internal sealed class ProductInstallRecord
|
||||
{
|
||||
public string Profile { get; set; } = "";
|
||||
public string InstallDirectory { get; set; } = "";
|
||||
public string ExecutableName { get; set; } = "";
|
||||
public string DolSha256 { get; set; } = "";
|
||||
public string RelSha256 { get; set; } = "";
|
||||
public string BuiltUtc { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The whole flat state document this tool keeps at ~/.local/share/WiiCompiled/install-state.json.
|
||||
/// Deliberately not a fingerprint tree: local-build.sh already does its own incremental-rebuild
|
||||
/// caching, so this only needs to remember where things were installed and what they were built
|
||||
/// against, not decide when to rebuild.
|
||||
/// </summary>
|
||||
internal sealed class InstallState
|
||||
{
|
||||
public int SchemaVersion { get; set; } = 1;
|
||||
public string Workspace { get; set; } = "";
|
||||
public List<ProductInstallRecord> Products { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
using System.Security.Cryptography;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
private static async Task<int> Main(string[] args)
|
||||
{
|
||||
// Checked anywhere in argv, not just args[0]: AppRun (Launcher/build-appimage.sh) prepends
|
||||
// --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; }
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
// Replaces CancellationSignal.cs's named-EventWaitHandle IPC (Windows-only): SIGINT/SIGTERM
|
||||
// are the portable, standard way for a parent (Wheel Wizard or a shell) to cancel this
|
||||
// process and the build it spawned.
|
||||
using var sigint = System.Runtime.InteropServices.PosixSignalRegistration.Create(
|
||||
System.Runtime.InteropServices.PosixSignal.SIGINT, context => { context.Cancel = true; cts.Cancel(); });
|
||||
using var sigterm = System.Runtime.InteropServices.PosixSignalRegistration.Create(
|
||||
System.Runtime.InteropServices.PosixSignal.SIGTERM, context => { context.Cancel = true; cts.Cancel(); });
|
||||
return await RunAsync(args, cts);
|
||||
}
|
||||
|
||||
private static async Task<int> RunAsync(string[] args, CancellationTokenSource cts)
|
||||
{
|
||||
// AppRun (Launcher/build-appimage.sh) invokes this as `wiicompiled-setup --workspace
|
||||
// <cache> <command> [options]` - a global flag ahead of the subcommand - so the command
|
||||
// word is whichever token isn't part of a --flag/value pair, not strictly args[0].
|
||||
var (command, flags) = ParseArgs(args);
|
||||
if (command is null) { PrintUsage(); return 1; }
|
||||
var progressJson = flags.ContainsKey("progress-json");
|
||||
IInstallReporter reporter = progressJson ? new NdjsonInstallReporter() : new ConsoleInstallReporter();
|
||||
|
||||
try
|
||||
{
|
||||
switch (command)
|
||||
{
|
||||
case "install":
|
||||
await InstallAsync(flags, reporter, cts.Token);
|
||||
break;
|
||||
case "uninstall":
|
||||
Uninstall();
|
||||
break;
|
||||
case "launch-base":
|
||||
return Launch("base", flags);
|
||||
case "launch-retro":
|
||||
return Launch("retro-rewind", flags);
|
||||
case "check-products":
|
||||
CheckProducts();
|
||||
break;
|
||||
default:
|
||||
Console.Error.WriteLine($"Unknown command: {command}");
|
||||
PrintUsage();
|
||||
return 1;
|
||||
}
|
||||
(reporter as NdjsonInstallReporter)?.Success(flags.GetValueOrDefault("install-dir") ?? "");
|
||||
return 0;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Console.Error.WriteLine("Cancelled.");
|
||||
(reporter as NdjsonInstallReporter)?.Failure("cancelled");
|
||||
return 130;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"error: {ex.Message}");
|
||||
(reporter as NdjsonInstallReporter)?.Failure(ex.Message);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task InstallAsync(Dictionary<string, string?> flags, IInstallReporter reporter, CancellationToken token)
|
||||
{
|
||||
var retroDir = flags.GetValueOrDefault("retro-dir");
|
||||
var installsRetro = !string.IsNullOrEmpty(retroDir);
|
||||
var downloadPayload = flags.ContainsKey("download-retro-wfc-payload");
|
||||
var skipPayload = flags.ContainsKey("skip-retro-wfc-payload");
|
||||
if (installsRetro)
|
||||
{
|
||||
if (downloadPayload == skipPayload)
|
||||
throw new ArgumentException(
|
||||
"Choose exactly one Retro-WFC mode: --download-retro-wfc-payload or --skip-retro-wfc-payload.");
|
||||
}
|
||||
else if (downloadPayload || skipPayload)
|
||||
{
|
||||
throw new ArgumentException("A Retro-WFC payload option is valid only with --retro-dir.");
|
||||
}
|
||||
|
||||
// Canonicalizes to the exact RetroRewind6 folder (accepting a parent folder or a symlink),
|
||||
// the same validation Windows applies via this same shared method - local-build.sh's own
|
||||
// check further down is a simpler backstop, not the primary validation anymore.
|
||||
if (installsRetro) retroDir = RetroRewindSource.ResolveRetroRewind6(retroDir!);
|
||||
|
||||
var workspace = flags.GetValueOrDefault("workspace") ?? WorkspaceLocator.FindFrom(AppContext.BaseDirectory);
|
||||
var manifest = ProjectManifest.Load(Path.Combine(workspace, "projects", "mkwii", "recomp.yml"));
|
||||
var assetsDir = Path.Combine(workspace, "Assets");
|
||||
|
||||
reporter.Progress(InstallStages.Validate, "Checking prerequisites", 1);
|
||||
if (flags.TryGetValue("game", out var isoPath) && !string.IsNullOrEmpty(isoPath))
|
||||
{
|
||||
await DiscTool.ValidateAndExtractAsync(isoPath, manifest, assetsDir, workspace,
|
||||
flags.GetValueOrDefault("disc-tool-bin"), reporter, token);
|
||||
}
|
||||
else
|
||||
{
|
||||
var dol = Path.Combine(assetsDir, "main.dol");
|
||||
var rel = Path.Combine(assetsDir, "StaticR.rel");
|
||||
if (!File.Exists(dol) || !File.Exists(rel))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"No --game ISO was given and Assets/main.dol + Assets/StaticR.rel are not already present. " +
|
||||
"Either pass --game <path-to-iso>, or extract them yourself first (see translator/README.md).");
|
||||
}
|
||||
}
|
||||
|
||||
var state = JsonState.TryRead<InstallState>(StatePath) ?? new InstallState { Workspace = workspace };
|
||||
state.Workspace = workspace;
|
||||
|
||||
var profile = installsRetro ? "both" : "base";
|
||||
var profiles = installsRetro ? new[] { "base", "retro-rewind" } : new[] { "base" };
|
||||
var baseInstallDir = installsRetro ? DefaultInstallDir("base") : null;
|
||||
var installDir = flags.GetValueOrDefault("install-dir") ?? DefaultInstallDir(installsRetro ? "retro-rewind" : "base");
|
||||
|
||||
string? retroWfcOfflineDir = null;
|
||||
if (downloadPayload)
|
||||
{
|
||||
// Reused if a previous install already downloaded and it's still valid - matches
|
||||
// Windows's own reuse-if-valid behavior instead of re-downloading on every install.
|
||||
var cacheDir = Path.Combine(workspace, "generated", "retro-wfc-payload");
|
||||
reporter.Progress(InstallStages.Validate, "Preparing the Retro-WFC payload", 1);
|
||||
try
|
||||
{
|
||||
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(cacheDir);
|
||||
}
|
||||
catch (InvalidDataException)
|
||||
{
|
||||
await RetroWfcPayload.DownloadRetroWfcPayloadAsync(
|
||||
RetroWfcPayload.CurrentRetroWfcPayloadUri, cacheDir, token);
|
||||
}
|
||||
retroWfcOfflineDir = cacheDir;
|
||||
}
|
||||
|
||||
await BuildRunner.RunAsync(
|
||||
workspace, profile, installDir, baseInstallDir,
|
||||
retroDir,
|
||||
retroWfcOfflineDir,
|
||||
skipPayload,
|
||||
flags.ContainsKey("force-clean-build"),
|
||||
flags.GetValueOrDefault("translator-bin"),
|
||||
reporter, token);
|
||||
|
||||
reporter.Progress(InstallStages.Shortcuts, "Creating shortcuts", 98);
|
||||
var dolSha = Sha256Of(Path.Combine(assetsDir, "main.dol"));
|
||||
var relSha = Sha256Of(Path.Combine(assetsDir, "StaticR.rel"));
|
||||
|
||||
foreach (var p in profiles)
|
||||
{
|
||||
var dir = p == "base" ? (baseInstallDir ?? installDir) : installDir;
|
||||
var exeName = p == "base" ? "WiiCompiled" : "RetroRewind";
|
||||
var displayName = p == "base" ? "WiiCompiled (base game)" : "WiiCompiled (Retro Rewind)";
|
||||
state.Products.RemoveAll(r => r.Profile == p);
|
||||
state.Products.Add(new ProductInstallRecord
|
||||
{
|
||||
Profile = p,
|
||||
InstallDirectory = dir,
|
||||
ExecutableName = exeName,
|
||||
DolSha256 = dolSha,
|
||||
RelSha256 = relSha,
|
||||
BuiltUtc = DateTime.UtcNow.ToString("O"),
|
||||
});
|
||||
DesktopEntry.Create(p, displayName, Path.Combine(dir, exeName));
|
||||
}
|
||||
JsonState.Write(StatePath, state);
|
||||
|
||||
// The runtime reads course/texture/audio data live from dvd_root at every launch, not just
|
||||
// at translation time - without this the game fatally errors the instant it needs any file
|
||||
// that isn't main.dol/StaticR.rel. Linux has no --portable flag, so this is always the
|
||||
// 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");
|
||||
if (Directory.Exists(dataDir))
|
||||
{
|
||||
RuntimeConfiguration.SetDvdRoot(configPath, dataDir);
|
||||
}
|
||||
if (installsRetro)
|
||||
{
|
||||
RuntimeConfiguration.SetRetroRewindRoot(configPath, retroDir!);
|
||||
}
|
||||
|
||||
reporter.Progress(InstallStages.Shortcuts, "Install complete", 99);
|
||||
}
|
||||
|
||||
private static void Uninstall()
|
||||
{
|
||||
// Matches Windows: UninstallService.cs removes the whole install directory unconditionally -
|
||||
// there is no partial-product uninstall on either platform.
|
||||
var state = JsonState.TryRead<InstallState>(StatePath) ?? new InstallState();
|
||||
foreach (var record in state.Products.ToList())
|
||||
{
|
||||
if (Directory.Exists(record.InstallDirectory))
|
||||
{
|
||||
Directory.Delete(record.InstallDirectory, recursive: true);
|
||||
}
|
||||
DesktopEntry.Remove(record.Profile);
|
||||
state.Products.Remove(record);
|
||||
Console.WriteLine($"Removed {record.Profile} from {record.InstallDirectory}");
|
||||
}
|
||||
JsonState.Write(StatePath, state);
|
||||
}
|
||||
|
||||
private static int Launch(string profile, Dictionary<string, string?> flags)
|
||||
{
|
||||
var state = JsonState.TryRead<InstallState>(StatePath);
|
||||
var record = state?.Products.FirstOrDefault(r => r.Profile == profile);
|
||||
if (record is null)
|
||||
{
|
||||
var installHint = profile == "retro-rewind"
|
||||
? "install --retro-dir <RetroRewind6> {--download-retro-wfc-payload | --skip-retro-wfc-payload}"
|
||||
: $"install --profile {profile}";
|
||||
Console.Error.WriteLine($"{profile} is not installed. Run '{installHint}' first.");
|
||||
return 1;
|
||||
}
|
||||
var exePath = Path.Combine(record.InstallDirectory, record.ExecutableName);
|
||||
if (!File.Exists(exePath))
|
||||
{
|
||||
Console.Error.WriteLine($"Installed executable is missing: {exePath}. Run 'install --profile {profile}' again.");
|
||||
return 1;
|
||||
}
|
||||
var startInfo = new System.Diagnostics.ProcessStartInfo(exePath)
|
||||
{
|
||||
WorkingDirectory = record.InstallDirectory,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
using var process = System.Diagnostics.Process.Start(startInfo);
|
||||
process?.WaitForExit();
|
||||
return process?.ExitCode ?? 1;
|
||||
}
|
||||
|
||||
private static void CheckProducts()
|
||||
{
|
||||
var state = JsonState.TryRead<InstallState>(StatePath);
|
||||
if (state is null || state.Products.Count == 0)
|
||||
{
|
||||
Console.WriteLine("Nothing installed.");
|
||||
return;
|
||||
}
|
||||
var assetsDir = Path.Combine(state.Workspace, "Assets");
|
||||
var currentDol = Sha256IfExists(Path.Combine(assetsDir, "main.dol"));
|
||||
var currentRel = Sha256IfExists(Path.Combine(assetsDir, "StaticR.rel"));
|
||||
foreach (var record in state.Products)
|
||||
{
|
||||
var exePath = Path.Combine(record.InstallDirectory, record.ExecutableName);
|
||||
var present = File.Exists(exePath);
|
||||
var stale = present && (currentDol != record.DolSha256 || currentRel != record.RelSha256);
|
||||
var status = !present ? "MISSING" : stale ? "STALE (game assets changed since last build)" : "current";
|
||||
Console.WriteLine($"{record.Profile,-14} {status,-45} {record.InstallDirectory}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string Sha256Of(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string? Sha256IfExists(string path) => File.Exists(path) ? Sha256Of(path) : null;
|
||||
|
||||
private static string StatePath => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WiiCompiled", "install-state.json");
|
||||
|
||||
private static string DefaultInstallDir(string profile) => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WiiCompiled", "Install",
|
||||
profile == "base" ? "Base" : "RetroRewind");
|
||||
|
||||
/// <summary>
|
||||
/// A single pass that finds both the command word and every --flag[=value] pair, regardless
|
||||
/// of order - a --flag may appear before or after the command (see the AppRun caller note in
|
||||
/// RunAsync). The first token that is neither a --flag nor a value already consumed by the
|
||||
/// preceding --flag is taken as the command.
|
||||
/// </summary>
|
||||
private static (string? Command, Dictionary<string, string?> Flags) ParseArgs(string[] args)
|
||||
{
|
||||
string? command = null;
|
||||
var flags = new Dictionary<string, string?>();
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
var arg = args[i];
|
||||
if (arg.StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
var name = arg[2..];
|
||||
if (i + 1 < args.Length && !args[i + 1].StartsWith("--", StringComparison.Ordinal))
|
||||
{
|
||||
flags[name] = args[++i];
|
||||
}
|
||||
else
|
||||
{
|
||||
flags[name] = null; // boolean flag
|
||||
}
|
||||
}
|
||||
else if (command is null)
|
||||
{
|
||||
command = arg;
|
||||
}
|
||||
}
|
||||
return (command, flags);
|
||||
}
|
||||
|
||||
private static void PrintUsage()
|
||||
{
|
||||
Console.WriteLine("""
|
||||
Usage: wiicompiled-setup <command> [options]
|
||||
|
||||
install [--game ISO_PATH] [--install-dir DIR] [--retro-dir DIR
|
||||
{--download-retro-wfc-payload | --skip-retro-wfc-payload}]
|
||||
[--force-clean-build] [--translator-bin PATH] [--disc-tool-bin PATH]
|
||||
[--progress-json] [--workspace DIR]
|
||||
uninstall
|
||||
launch-base
|
||||
launch-retro
|
||||
check-products
|
||||
--version
|
||||
""");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
/// <summary>
|
||||
/// The handful of facts this tool needs out of projects/mkwii/recomp.yml. Parsed literally line by
|
||||
/// line - the same approach Launcher/NativeBuildFlags.ps1's Get-MkwProjectPins and
|
||||
/// Launcher/local-build.sh already use - rather than pulling in a YAML library, since the manifest
|
||||
/// is machine-written with a fixed shape.
|
||||
/// </summary>
|
||||
internal sealed class ProjectManifest
|
||||
{
|
||||
public required string GameId { get; init; }
|
||||
public required string Region { get; init; }
|
||||
public required string DolSha256 { get; init; }
|
||||
public required string RelSha256 { get; init; }
|
||||
|
||||
public static ProjectManifest Load(string path)
|
||||
{
|
||||
if (!File.Exists(path)) throw new FileNotFoundException("Translation project file is missing", path);
|
||||
|
||||
string? gameId = null, region = null, dolSha = null, relSha = null;
|
||||
string section = "";
|
||||
string inputKey = "";
|
||||
|
||||
foreach (var raw in File.ReadLines(path))
|
||||
{
|
||||
var line = Regex.Replace(raw, "#.*$", "");
|
||||
if (string.IsNullOrWhiteSpace(line)) continue;
|
||||
|
||||
var sectionMatch = Regex.Match(line, "^([A-Za-z0-9_]+):");
|
||||
if (sectionMatch.Success)
|
||||
{
|
||||
section = sectionMatch.Groups[1].Value;
|
||||
inputKey = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (section == "inputs")
|
||||
{
|
||||
var keyMatch = Regex.Match(line, @"^\s{2}([A-Za-z0-9_]+):\s*$");
|
||||
if (keyMatch.Success) { inputKey = keyMatch.Groups[1].Value; continue; }
|
||||
|
||||
var shaMatch = Regex.Match(line, @"^\s*sha256:\s*([0-9a-fA-F]{64})\s*$");
|
||||
if (shaMatch.Success)
|
||||
{
|
||||
var value = shaMatch.Groups[1].Value.ToLowerInvariant();
|
||||
if (inputKey == "dol") dolSha = value;
|
||||
else if (inputKey == "rel") relSha = value;
|
||||
}
|
||||
}
|
||||
else if (section == "project")
|
||||
{
|
||||
var idMatch = Regex.Match(line, @"^\s*game_id:\s*(\S+)\s*$");
|
||||
if (idMatch.Success) gameId = idMatch.Groups[1].Value;
|
||||
|
||||
var regionMatch = Regex.Match(line, @"^\s*region:\s*(\S+)\s*$");
|
||||
if (regionMatch.Success) region = regionMatch.Groups[1].Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (gameId is null || region is null || dolSha is null || relSha is null)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"{path} does not pin game_id/region/dol.sha256/rel.sha256; the project file is not the shape this tool expects.");
|
||||
}
|
||||
|
||||
return new ProjectManifest { GameId = gameId, Region = region, DolSha256 = dolSha, RelSha256 = relSha };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>WiiCompiled.Setup.Linux</AssemblyName>
|
||||
<RootNamespace>WiiCompiled.Setup.Linux</RootNamespace>
|
||||
<Version>0.2.22</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Command-line installer and launcher for WiiCompiled on Linux</Description>
|
||||
<DebugType>embedded</DebugType>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace WiiCompiled.Setup.Linux;
|
||||
|
||||
/// <summary>
|
||||
/// Finds the repo checkout this tool is running from by walking up from its own directory looking
|
||||
/// for Launcher/local-build.sh - this tool operates directly on a git checkout (no bundled/staged
|
||||
/// workspace copy), so there is no installed "Toolkit" layout to anchor on the way the Windows
|
||||
/// installer's Installation.cs does.
|
||||
/// </summary>
|
||||
internal static class WorkspaceLocator
|
||||
{
|
||||
private const int MaxSearchDepth = 6;
|
||||
|
||||
public static string FindFrom(string startDirectory)
|
||||
{
|
||||
var current = new DirectoryInfo(startDirectory);
|
||||
for (var level = 0; level <= MaxSearchDepth && current is not null; level++, current = current.Parent)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "Launcher", "local-build.sh")))
|
||||
{
|
||||
return current.FullName;
|
||||
}
|
||||
}
|
||||
throw new InvalidOperationException(
|
||||
"Could not find the WiiCompiled repository (looked for Launcher/local-build.sh walking up " +
|
||||
$"from {startDirectory}). Pass --workspace <path-to-checkout> explicitly.");
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Bridges a frontend-owned, named Windows event into the cancellation token used by setup.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal enum AppMode
|
||||
{
|
||||
+2
-1
@@ -1,8 +1,9 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal sealed record RetroRewindCompileInputs(
|
||||
string RetroRewindRoot,
|
||||
+4
-3
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal static class ConsoleCommands
|
||||
{
|
||||
@@ -63,8 +64,8 @@ internal static class ConsoleCommands
|
||||
{
|
||||
using var payload = PayloadArchive.OpenCurrent();
|
||||
var manifest = payload.ReadManifest();
|
||||
var tool = Path.Combine(temp, "DolphinTool.exe");
|
||||
payload.ExtractEntry(InstalledLayout.ToolkitEntryPrefix + "DolphinTool.exe", tool);
|
||||
var tool = Path.Combine(temp, "nodtool.exe");
|
||||
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();
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal static class GameLaunchService
|
||||
{
|
||||
@@ -0,0 +1,165 @@
|
||||
using System.Diagnostics;
|
||||
using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal static class InputValidation
|
||||
{
|
||||
private static readonly HashSet<string> SupportedDiscImageExtensions = new(
|
||||
[".iso", ".gcm", ".gcz", ".ciso", ".wbfs", ".wia", ".rvz"],
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public static void ValidateExtension(string gamePath)
|
||||
{
|
||||
if (!File.Exists(gamePath))
|
||||
throw new FileNotFoundException("The selected game image does not exist.", gamePath);
|
||||
var extension = Path.GetExtension(gamePath);
|
||||
if (!SupportedDiscImageExtensions.Contains(extension))
|
||||
throw new InvalidDataException(
|
||||
"Select a complete Wii disc image in ISO, GCM, GCZ, CISO, WBFS, WIA, or RVZ format.");
|
||||
}
|
||||
|
||||
public static async Task<DiscHeader> ReadDiscHeaderAsync(string nodTool, string gamePath,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ValidateExtension(gamePath);
|
||||
var result = await ProcessRunner.RunAsync(nodTool,
|
||||
["info", Path.GetFullPath(gamePath)], null, cancellationToken);
|
||||
if (result.ExitCode != 0)
|
||||
throw new InvalidDataException("nodtool could not read this disc image. " + result.CombinedOutput.Trim());
|
||||
|
||||
var info = NodToolInfoParser.Parse(result.StandardOutput);
|
||||
return new DiscHeader
|
||||
{
|
||||
GameId = info.GameId,
|
||||
InternalName = info.Title,
|
||||
Region = RegionFromGameId(info.GameId),
|
||||
Revision = info.Revision,
|
||||
};
|
||||
}
|
||||
|
||||
private static string RegionFromGameId(string gameId) => gameId.Length >= 4
|
||||
? gameId[3] switch
|
||||
{
|
||||
'P' => "PAL",
|
||||
'E' => "NTSC-U",
|
||||
'J' => "NTSC-J",
|
||||
'K' => "Korea",
|
||||
'W' => "Taiwan",
|
||||
_ => gameId[3].ToString(),
|
||||
}
|
||||
: "Unknown";
|
||||
|
||||
public static void EnsureCompatibleDisc(DiscHeader header, PayloadManifest manifest)
|
||||
{
|
||||
if (!header.GameId.Equals(manifest.ExpectedGameId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"This build supports Mario Kart Wii PAL ({manifest.ExpectedGameId}). " +
|
||||
$"The selected image is {header.GameId} ({header.InternalName}, {header.Region}).");
|
||||
}
|
||||
}
|
||||
|
||||
// Thin forwarding wrappers: the actual download/RSA-verification logic lives in
|
||||
// WiiCompiled.Setup.Common.RetroWfcPayload (shared with WiiCompiled.Setup.Linux) so there's one
|
||||
// copy of it, not two. Kept under these names so every existing call site here
|
||||
// (ProductRepairService.cs, LocalBuildService.cs, Installation.cs, SelfTests.cs) is unchanged.
|
||||
public const string CurrentRetroWfcPayloadUri = RetroWfcPayload.CurrentRetroWfcPayloadUri;
|
||||
|
||||
public static string ValidateStagedRetroWfcPayloadDirectory(string stagedDirectory,
|
||||
RSAParameters? signingKey = null) =>
|
||||
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(stagedDirectory, signingKey);
|
||||
|
||||
public static string ResolveRetroWfcPayloadFile(string stagedDirectory,
|
||||
RSAParameters? signingKey = null) =>
|
||||
RetroWfcPayload.ResolveRetroWfcPayloadFile(stagedDirectory, signingKey);
|
||||
|
||||
public static string ComputeRetroWfcPayloadSha256(string stagedDirectory,
|
||||
RSAParameters? signingKey = null) =>
|
||||
RetroWfcPayload.ComputeRetroWfcPayloadSha256(stagedDirectory, signingKey);
|
||||
|
||||
public static void ValidateRetroWfcPayloadUri(string uriText) =>
|
||||
RetroWfcPayload.ValidateRetroWfcPayloadUri(uriText);
|
||||
|
||||
public static Task<RetroWfcPayloadSnapshot> DownloadRetroWfcPayloadAsync(string uriText,
|
||||
string destinationDirectory, CancellationToken cancellationToken) =>
|
||||
RetroWfcPayload.DownloadRetroWfcPayloadAsync(uriText, destinationDirectory, cancellationToken);
|
||||
|
||||
internal static bool IsTransientRetroWfcDownloadFailure(Exception exception,
|
||||
CancellationToken cancellationToken) =>
|
||||
RetroWfcPayload.IsTransientRetroWfcDownloadFailure(exception, cancellationToken);
|
||||
|
||||
public static string Sha256File(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record ProcessResult(int ExitCode, string StandardOutput, string StandardError)
|
||||
{
|
||||
public string CombinedOutput => StandardOutput + Environment.NewLine + StandardError;
|
||||
}
|
||||
|
||||
internal static class ProcessRunner
|
||||
{
|
||||
/// <summary>Runs a redirected child process to completion. <paramref name="configure"/> sets up a
|
||||
/// working directory or scrubbed environment; <paramref name="capture"/> is off for callers that only
|
||||
/// forward output live, so a build's output isn't buffered in memory for nobody to read.</summary>
|
||||
public static async Task<ProcessResult> RunAsync(string executable, IReadOnlyList<string> arguments,
|
||||
Action<string>? output, CancellationToken cancellationToken,
|
||||
Action<ProcessStartInfo>? configure = null, bool capture = true,
|
||||
Action<Exception>? onTerminationFailure = null)
|
||||
{
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
FileName = executable,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
foreach (var argument in arguments) info.ArgumentList.Add(argument);
|
||||
configure?.Invoke(info);
|
||||
|
||||
using var process = new Process { StartInfo = info, EnableRaisingEvents = true };
|
||||
var stdout = new List<string>();
|
||||
var stderr = new List<string>();
|
||||
process.OutputDataReceived += (_, e) => { if (e.Data is not null) { if (capture) stdout.Add(e.Data); output?.Invoke(e.Data); } };
|
||||
process.ErrorDataReceived += (_, e) => { if (e.Data is not null) { if (capture) stderr.Add(e.Data); output?.Invoke(e.Data); } };
|
||||
if (!process.Start()) throw new InvalidOperationException($"Could not start {executable}.");
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
await WaitForExitAsync(process, cancellationToken, onTerminationFailure);
|
||||
return new ProcessResult(process.ExitCode, string.Join(Environment.NewLine, stdout),
|
||||
string.Join(Environment.NewLine, stderr));
|
||||
}
|
||||
|
||||
public static async Task WaitForExitAsync(Process process, CancellationToken cancellationToken,
|
||||
Action<Exception>? onTerminationFailure = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!process.HasExited) process.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
onTerminationFailure?.Invoke(ex);
|
||||
}
|
||||
await process.WaitForExitAsync(CancellationToken.None);
|
||||
process.WaitForExit();
|
||||
throw;
|
||||
}
|
||||
process.WaitForExit();
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// A fail-fast, cross-process lock covering install, repair and launch operations for one install
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Stable stage identifiers reported by <c>--progress-json</c>. These are part of the public
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Owns one temporary directory for an install operation. The name carries the installation's scope
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal enum InstallTransactionEntryKind
|
||||
{
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>Provenance written by the bundled build script next to every product it produces.</summary>
|
||||
internal sealed class LocalBuildProvenance
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Names of the installed/staged layout. Not cosmetic: payload and toolkit identities hash relative paths
|
||||
+19
-12
@@ -1,4 +1,6 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal sealed class InstallerEngine
|
||||
{
|
||||
@@ -57,9 +59,9 @@ internal sealed class InstallerEngine
|
||||
|
||||
var runtimeAssetsCurrent = sameToolkit && RuntimeAssetsAreCurrent(existing,
|
||||
candidateRuntimeAssetsFingerprint, cancellationToken);
|
||||
var installedDolphinTool = Path.Combine(existing.ToolkitDirectory, "DolphinTool.exe");
|
||||
var installedNodTool = Path.Combine(existing.ToolkitDirectory, "nodtool.exe");
|
||||
var extractToolkit = MustRefreshToolkit(sameToolkit, samePackageContent,
|
||||
File.Exists(installedDolphinTool));
|
||||
File.Exists(installedNodTool));
|
||||
var extractWorkspace = !sameToolkit || !runtimeAssetsCurrent;
|
||||
|
||||
_reporter.Progress(InstallStages.ExtractToolkit,
|
||||
@@ -77,9 +79,9 @@ internal sealed class InstallerEngine
|
||||
payload.ExtractEntry(InstalledLayout.PayloadManifestFileName,
|
||||
Path.Combine(staging, InstalledLayout.PayloadManifestFileName));
|
||||
|
||||
var dolphinTool = extractToolkit ? Path.Combine(toolkit, "DolphinTool.exe") : installedDolphinTool;
|
||||
var nodTool = extractToolkit ? Path.Combine(toolkit, "nodtool.exe") : installedNodTool;
|
||||
_reporter.Progress(InstallStages.Validate, "Checking the Wii disc image...", 2);
|
||||
var header = await InputValidation.ReadDiscHeaderAsync(dolphinTool, options.GamePath,
|
||||
var header = await InputValidation.ReadDiscHeaderAsync(nodTool, options.GamePath,
|
||||
cancellationToken);
|
||||
InputValidation.EnsureCompatibleDisc(header, manifest);
|
||||
var canonicalRetroRoot = options.RetroDirectoryPath is null
|
||||
@@ -153,7 +155,7 @@ internal sealed class InstallerEngine
|
||||
|
||||
if (reusableGameAssets is null)
|
||||
{
|
||||
await ExtractGameAssetsAsync(dolphinTool, options.GamePath,
|
||||
await ExtractGameAssetsAsync(nodTool, options.GamePath,
|
||||
Path.Combine(staging, "GameAssets"), manifest, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -164,8 +166,8 @@ internal sealed class InstallerEngine
|
||||
|
||||
|
||||
internal static bool MustRefreshToolkit(bool sameToolkit, bool samePackageContent,
|
||||
bool dolphinToolPresent) =>
|
||||
!sameToolkit || !samePackageContent || !dolphinToolPresent;
|
||||
bool nodToolPresent) =>
|
||||
!sameToolkit || !samePackageContent || !nodToolPresent;
|
||||
|
||||
private static void AddComponent(List<InstallTransactionEntry> entries, string staging,
|
||||
string installDirectory, string name) =>
|
||||
@@ -469,18 +471,23 @@ internal sealed class InstallerEngine
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExtractGameAssetsAsync(string dolphinTool, string gamePath, string destination,
|
||||
private async Task ExtractGameAssetsAsync(string nodTool, string gamePath, string destination,
|
||||
PayloadManifest manifest, CancellationToken cancellationToken)
|
||||
{
|
||||
_reporter.Progress(InstallStages.ExtractDisc,
|
||||
"Extracting the game disc. This is the longest preparation step...", 6);
|
||||
var extraction = await ProcessRunner.RunAsync(dolphinTool,
|
||||
["extract", "-i", Path.GetFullPath(gamePath), "-o", destination, "-g", "-q"],
|
||||
// Extracted straight into a "DATA" subfolder so the on-disk layout matches what
|
||||
// Installation.GameDataDirectory and every other reader of it already expect - nodtool
|
||||
// itself has no such wrapper (it extracts sys/+files/ directly to whatever <outdir> is
|
||||
// given), so this is purely destination-side, not a nodtool convention.
|
||||
var dataRoot = Path.Combine(destination, "DATA");
|
||||
var extraction = await ProcessRunner.RunAsync(nodTool,
|
||||
["extract", Path.GetFullPath(gamePath), dataRoot, "-q"],
|
||||
line => { if (!string.IsNullOrWhiteSpace(line)) _reporter.Diagnostic(line); },
|
||||
cancellationToken);
|
||||
if (extraction.ExitCode != 0)
|
||||
throw new InvalidDataException("Game extraction failed. " + extraction.CombinedOutput.Trim());
|
||||
ValidateExtractedGame(Path.Combine(destination, "DATA"), manifest);
|
||||
ValidateExtractedGame(dataRoot, manifest);
|
||||
}
|
||||
|
||||
private static void ValidateExtractedGame(string dataRoot, PayloadManifest manifest)
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Both"/> runs one retro-aware translation and compiles the two products from a single
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal enum RetroWfcPayloadMode
|
||||
{
|
||||
@@ -54,12 +55,6 @@ internal sealed class PayloadManifest
|
||||
public string NativeToolchainFingerprint { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One validated, content-identified download in operation-owned scratch space. Callers use this
|
||||
/// exact directory for both the update decision and any resulting build.
|
||||
/// </summary>
|
||||
internal sealed record RetroWfcPayloadSnapshot(string Directory, string Sha256, long ByteLength);
|
||||
|
||||
internal sealed class DiscHeader
|
||||
{
|
||||
[JsonPropertyName("game_id")]
|
||||
+1
-1
@@ -2,7 +2,7 @@ using System.IO.Compression;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal sealed class PayloadArchive : IDisposable
|
||||
{
|
||||
@@ -0,0 +1,54 @@
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// A portable root can be moved or renamed between operations. Every installed-host operation that
|
||||
/// reads <c>install-state.json</c> passes through here first so exactly one place decides what a
|
||||
/// moved installation means, and so a non-portable installation is never touched.
|
||||
/// </summary>
|
||||
internal static class PortableInstallHealing
|
||||
{
|
||||
/// <summary>
|
||||
/// Reconciles a moved portable installation with its recorded location: the state file adopts the
|
||||
/// directory it was actually found in, and the native build tree is discarded because its
|
||||
/// CMake cache holds absolute paths from the old location. Returns whether anything was healed.
|
||||
/// </summary>
|
||||
public static bool HealMovedInstall(Installation installation, IInstallReporter? reporter = null)
|
||||
{
|
||||
// Guard: an ordinary installation that disagrees with its state file is a real problem for
|
||||
// the operation to report, not something to silently rewrite.
|
||||
if (PortableRoot.TryFind(installation.Root) is null) return false;
|
||||
|
||||
var state = installation.ReadInstallState();
|
||||
if (state is not { SchemaVersion: 1 } || string.IsNullOrWhiteSpace(state.InstallDir)) return false;
|
||||
|
||||
string recorded;
|
||||
try
|
||||
{
|
||||
recorded = FileSystemUtilities.NormalizePath(state.InstallDir);
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException)
|
||||
{
|
||||
recorded = state.InstallDir;
|
||||
}
|
||||
if (recorded.Equals(installation.Root, StringComparison.OrdinalIgnoreCase)) return false;
|
||||
|
||||
var previous = state.InstallDir;
|
||||
state.InstallDir = installation.Root;
|
||||
JsonState.Write(installation.InstallStatePath, state);
|
||||
|
||||
// The configured native build directory bakes absolute source, toolchain, and output paths
|
||||
// into CMakeCache.txt. After a move it is unusable and would fail the next configure rather
|
||||
// than being reused, so it is removed and reconfigured from scratch on the next build.
|
||||
var nativeBuild = Path.Combine(installation.WorkspaceDirectory, "native-build");
|
||||
var hadNativeBuild = Directory.Exists(nativeBuild);
|
||||
if (hadNativeBuild) FileSystemUtilities.DeleteDirectoryIfExists(nativeBuild);
|
||||
|
||||
reporter?.Diagnostic(
|
||||
$"This portable installation moved from {previous} to {installation.Root}. " +
|
||||
"The recorded location was updated" +
|
||||
(hadNativeBuild ? " and the location-bound native build cache was discarded." : "."));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Reconciles installed products against the canonical Retro Rewind install Wheel Wizard owns: the
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Refuses to replace installed products while one of them is running: publishing renames the
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// The one path by which a product receives its copied runtime assets, shared by install and repair.
|
||||
+9
-8
@@ -1,7 +1,8 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Security.Cryptography;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal static class SelfTests
|
||||
{
|
||||
@@ -143,17 +144,17 @@ internal static class SelfTests
|
||||
private static void TestToolkitRefreshDecision()
|
||||
{
|
||||
if (!InstallerEngine.MustRefreshToolkit(sameToolkit: false, samePackageContent: true,
|
||||
dolphinToolPresent: true))
|
||||
nodToolPresent: true))
|
||||
throw new Exception("A republished workspace kept the installed toolkit; the shipped " +
|
||||
"translator and project file could come from different releases.");
|
||||
if (!InstallerEngine.MustRefreshToolkit(sameToolkit: true, samePackageContent: false,
|
||||
dolphinToolPresent: true))
|
||||
nodToolPresent: true))
|
||||
throw new Exception("Changed toolkit package content was not extracted.");
|
||||
if (!InstallerEngine.MustRefreshToolkit(sameToolkit: true, samePackageContent: true,
|
||||
dolphinToolPresent: false))
|
||||
throw new Exception("A missing DolphinTool.exe did not force toolkit extraction.");
|
||||
nodToolPresent: false))
|
||||
throw new Exception("A missing nodtool.exe did not force toolkit extraction.");
|
||||
if (InstallerEngine.MustRefreshToolkit(sameToolkit: true, samePackageContent: true,
|
||||
dolphinToolPresent: true))
|
||||
nodToolPresent: true))
|
||||
throw new Exception("An unchanged toolkit was needlessly re-extracted.");
|
||||
}
|
||||
|
||||
@@ -1010,7 +1011,7 @@ internal static class SelfTests
|
||||
throw new Exception("The toolkit fingerprint is not stable.");
|
||||
|
||||
// A file that has nothing to do with generated code must not invalidate every install.
|
||||
File.WriteAllText(Path.Combine(root, "Toolkit", "DolphinTool.exe"), "irrelevant");
|
||||
File.WriteAllText(Path.Combine(root, "Toolkit", "nodtool.exe"), "irrelevant");
|
||||
if (ToolkitFingerprint.Compute(root) != first)
|
||||
throw new Exception("An unrelated toolkit file changed the fingerprint.");
|
||||
|
||||
@@ -1177,7 +1178,7 @@ internal static class SelfTests
|
||||
"x86_64-w64-mingw32-clang++.exe", "x86_64-w64-mingw32-windres.exe"
|
||||
})
|
||||
File.WriteAllText(Path.Combine(root, "Toolkit", "llvm-mingw", "bin", executable), executable);
|
||||
File.WriteAllText(Path.Combine(root, "Toolkit", "DolphinTool.exe"), "tool");
|
||||
File.WriteAllText(Path.Combine(root, "Toolkit", "nodtool.exe"), "tool");
|
||||
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "LocalBuild.ps1"), "# build");
|
||||
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "NativeBuildFlags.ps1"), "# flags");
|
||||
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "projects", "mkwii", "recomp.yml"), "profiles: {}");
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal static class ShellIntegration
|
||||
{
|
||||
+4
-3
@@ -1,7 +1,8 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Content identity of everything that decides what the locally produced executables contain.
|
||||
@@ -78,12 +79,12 @@ internal static class ToolkitFingerprint
|
||||
var workspace = InstalledLayout.Workspace(root);
|
||||
var entries = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||
|
||||
// DolphinTool validates/extracts the user disc but does not influence generated products.
|
||||
// nodtool validates/extracts the user disc but does not influence generated products.
|
||||
// Everything else in Toolkit can affect translation, compilation, linking, or copied
|
||||
// runtime support and therefore belongs to the compile identity.
|
||||
AddDirectory(entries, root, toolkit, null,
|
||||
cancellationToken,
|
||||
file => !Path.GetFileName(file).Equals("DolphinTool.exe", StringComparison.OrdinalIgnoreCase));
|
||||
file => !Path.GetFileName(file).Equals("nodtool.exe", StringComparison.OrdinalIgnoreCase));
|
||||
AddFile(entries, root, Path.Combine(workspace, "LocalBuild.ps1"), cancellationToken);
|
||||
AddFile(entries, root, Path.Combine(workspace, "NativeBuildFlags.ps1"), cancellationToken);
|
||||
AddDirectory(entries, root, Path.Combine(workspace, "projects"), null, cancellationToken);
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using WiiCompiled.Setup.Common;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
internal static class UninstallService
|
||||
{
|
||||
+4
-1
@@ -5,7 +5,7 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>WiiCompiled.Setup</AssemblyName>
|
||||
<RootNamespace>WiiCompiled.Setup</RootNamespace>
|
||||
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Version>0.2.24</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
@@ -14,4 +14,7 @@
|
||||
<DebugType>embedded</DebugType>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
namespace WiiCompiled.Setup.Windows;
|
||||
|
||||
|
||||
internal static class WorkspaceTimestamps
|
||||
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env bash
|
||||
# Packages Launcher/WiiCompiled.Setup.Linux as a self-contained AppImage: a single file Wheel
|
||||
# Wizard (or anyone else) can fetch and execute with no git clone, no `dotnet` install, and no
|
||||
# `dolphin-tool` package required at all. The installer and translator are published as
|
||||
# self-contained binaries, and `nodtool` (a prebuilt MIT/Apache-2.0 CLI from encounter/nod, see
|
||||
# NodToolProvider.cs) is downloaded and bundled too - AppRun passes --translator-bin and
|
||||
# --disc-tool-bin so local-build.sh/DiscTool.cs skip their from-source/download fallbacks entirely.
|
||||
# It still shells out to system clang/cmake/ninja - no C/C++ toolchain is bundled, matching
|
||||
# Launcher/local-build.sh's own remaining prerequisites.
|
||||
#
|
||||
# An AppImage mounts read-only, but local-build.sh writes generated/, native-build/, Assets/, etc.
|
||||
# into the workspace it's given. So AppRun (written below) copies the bundled workspace snapshot
|
||||
# out to a writable cache directory on first run, and only ever re-syncs the bundled directories
|
||||
# (runtime/, aurora-main/, projects/, local-build.sh) on a later run whose bundled version changed
|
||||
# - generated/native-build/Assets/PulsarPacks live only in that writable cache and are never
|
||||
# touched by the sync, so local-build.sh's own incremental caching survives across runs and across
|
||||
# AppImage updates. translator/ isn't part of this snapshot at all: it's published as its own
|
||||
# self-contained binary (usr/bin/translator-cli) below and never needs a writable copy.
|
||||
set -euo pipefail
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
workspace=$(cd "$script_dir/.." && pwd)
|
||||
|
||||
output_dir="$workspace/Launcher/dist"
|
||||
appimagetool_override=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--output-dir) output_dir=$2; shift 2 ;;
|
||||
--appimagetool) appimagetool_override=$2; shift 2 ;;
|
||||
-h|--help)
|
||||
echo "Usage: build-appimage.sh [--output-dir DIR] [--appimagetool PATH]"
|
||||
exit 0
|
||||
;;
|
||||
*) echo "build-appimage.sh: unknown argument: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
appdir="$workspace/Launcher/artifacts/appimage-build/AppDir"
|
||||
rm -rf "$appdir"
|
||||
mkdir -p "$appdir/usr/bin" "$appdir/workspace/Launcher"
|
||||
|
||||
echo "Publishing the installer (self-contained linux-x64)..."
|
||||
publish_tmp="$workspace/Launcher/artifacts/appimage-build/publish"
|
||||
rm -rf "$publish_tmp"
|
||||
dotnet publish "$workspace/Launcher/WiiCompiled.Setup.Linux" -c Release -r linux-x64 \
|
||||
--self-contained -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \
|
||||
-o "$publish_tmp"
|
||||
cp "$publish_tmp/WiiCompiled.Setup.Linux" "$appdir/usr/bin/wiicompiled-setup"
|
||||
chmod +x "$appdir/usr/bin/wiicompiled-setup"
|
||||
|
||||
# Published as a self-contained binary too, so an AppImage user never needs a `dotnet` SDK on
|
||||
# PATH at all - local-build.sh is told about it via --translator-bin and skips its own
|
||||
# dotnet-build-from-source step entirely (see local-build.sh's translator resolution branch).
|
||||
echo "Publishing the translator (self-contained linux-x64)..."
|
||||
translator_publish_tmp="$workspace/Launcher/artifacts/appimage-build/publish-translator"
|
||||
rm -rf "$translator_publish_tmp"
|
||||
dotnet publish "$workspace/translator/src/Translator.Cli" -c Release -r linux-x64 \
|
||||
--self-contained -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \
|
||||
-o "$translator_publish_tmp"
|
||||
cp "$translator_publish_tmp/Translator.Cli" "$appdir/usr/bin/translator-cli"
|
||||
chmod +x "$appdir/usr/bin/translator-cli"
|
||||
|
||||
# Resolved via the shared WiiCompiled.Setup.Common.Cli helper (also used by Build-Installer.ps1 on
|
||||
# Windows) rather than a second curl/version-pin copy here: it downloads and caches the same way
|
||||
# NodToolProvider.cs always does (Launcher/artifacts/nodtool), so there is exactly one place that
|
||||
# knows the nodtool version/URL/platform-asset mapping.
|
||||
echo "Resolving nodtool..."
|
||||
nodtool_path=$(dotnet run --project "$workspace/Launcher/WiiCompiled.Setup.Common.Cli" -c Release -- \
|
||||
--workspace "$workspace" | tail -n1)
|
||||
cp "$nodtool_path" "$appdir/usr/bin/nodtool"
|
||||
chmod +x "$appdir/usr/bin/nodtool"
|
||||
|
||||
echo "Staging the bundled workspace snapshot..."
|
||||
for dir in runtime aurora-main projects; do
|
||||
cp -r "$workspace/$dir" "$appdir/workspace/$dir"
|
||||
done
|
||||
# Mirrors Build-Installer.ps1's own staging exclusions exactly: aurora-main/extern/CMakeLists.txt
|
||||
# is the real FetchContent driver and must ship, but any already-fetched dependency *subdirectory*
|
||||
# a developer's local checkout accumulated under extern/ is stale/large build output, not a
|
||||
# release input - only directories inside extern/ are stripped, never the file itself. runtime/build
|
||||
# is a plain developer build directory.
|
||||
find "$appdir/workspace/aurora-main/extern" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} +
|
||||
rm -rf "$appdir/workspace/runtime/build"
|
||||
cp "$workspace/Launcher/local-build.sh" "$appdir/workspace/Launcher/local-build.sh"
|
||||
|
||||
if git -C "$workspace" rev-parse HEAD >/dev/null 2>&1; then
|
||||
git -C "$workspace" rev-parse HEAD > "$appdir/workspace/.bundle-version"
|
||||
else
|
||||
date -u +%s > "$appdir/workspace/.bundle-version"
|
||||
fi
|
||||
|
||||
echo "Writing AppRun..."
|
||||
cat > "$appdir/AppRun" <<'APPRUN'
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
HERE="$(dirname "$(readlink -f "$0")")"
|
||||
CACHE="${XDG_DATA_HOME:-$HOME/.local/share}/WiiCompiled/workspace"
|
||||
if [ ! -f "$CACHE/.bundle-version" ] || \
|
||||
[ "$(cat "$HERE/workspace/.bundle-version")" != "$(cat "$CACHE/.bundle-version")" ]; then
|
||||
mkdir -p "$CACHE/Launcher"
|
||||
for dir in runtime aurora-main projects; do
|
||||
rm -rf "$CACHE/$dir"
|
||||
cp -r "$HERE/workspace/$dir" "$CACHE/$dir"
|
||||
done
|
||||
cp "$HERE/workspace/Launcher/local-build.sh" "$CACHE/Launcher/local-build.sh"
|
||||
cp "$HERE/workspace/.bundle-version" "$CACHE/.bundle-version"
|
||||
fi
|
||||
exec "$HERE/usr/bin/wiicompiled-setup" --workspace "$CACHE" \
|
||||
--translator-bin "$HERE/usr/bin/translator-cli" \
|
||||
--disc-tool-bin "$HERE/usr/bin/nodtool" "$@"
|
||||
APPRUN
|
||||
chmod +x "$appdir/AppRun"
|
||||
|
||||
echo "Writing desktop entry and icon..."
|
||||
cat > "$appdir/wiicompiled-setup.desktop" <<'DESKTOP'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=WiiCompiled Setup
|
||||
Comment=Translate, compile, and launch Mario Kart Wii natively on Linux
|
||||
Exec=AppRun
|
||||
Icon=wiicompiled-setup
|
||||
Categories=Game;
|
||||
Terminal=true
|
||||
DESKTOP
|
||||
|
||||
# No WiiCompiled logo/icon asset exists anywhere in this repo yet. appimagetool refuses to package
|
||||
# without one, so this is a minimal solid-color placeholder - a one-line swap for real branding
|
||||
# later (just replace this generated file with a real wiicompiled-setup.png before packaging).
|
||||
python3 - "$appdir/wiicompiled-setup.png" <<'PY'
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
path = sys.argv[1]
|
||||
|
||||
|
||||
def chunk(tag: bytes, data: bytes) -> bytes:
|
||||
return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data))
|
||||
|
||||
|
||||
width = height = 256
|
||||
row = b"\x00" + bytes([0x3A, 0x5F, 0x8F, 0xFF]) * width # filter byte + opaque blue-grey pixels
|
||||
raw = row * height
|
||||
ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)
|
||||
idat = zlib.compress(raw, 9)
|
||||
|
||||
with open(path, "wb") as handle:
|
||||
handle.write(b"\x89PNG\r\n\x1a\n")
|
||||
handle.write(chunk(b"IHDR", ihdr))
|
||||
handle.write(chunk(b"IDAT", idat))
|
||||
handle.write(chunk(b"IEND", b""))
|
||||
PY
|
||||
|
||||
echo "Resolving appimagetool..."
|
||||
appimagetool="$appimagetool_override"
|
||||
if [[ -z "$appimagetool" ]]; then
|
||||
appimagetool="$workspace/Launcher/artifacts/appimagetool"
|
||||
if [[ ! -x "$appimagetool" ]]; then
|
||||
echo "Downloading appimagetool..."
|
||||
mkdir -p "$(dirname "$appimagetool")"
|
||||
curl -fsSL "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" \
|
||||
-o "$appimagetool"
|
||||
chmod +x "$appimagetool"
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$output_dir"
|
||||
echo "Packaging..."
|
||||
# appimagetool detects the target architecture from the first ELF executable it finds in the
|
||||
# AppDir; AppRun here is a shell script, not ELF, so ARCH must be set explicitly.
|
||||
ARCH=x86_64 "$appimagetool" "$appdir" "$output_dir/WiiCompiled-Setup-x86_64.AppImage"
|
||||
echo "Built: $output_dir/WiiCompiled-Setup-x86_64.AppImage"
|
||||
Executable
+425
@@ -0,0 +1,425 @@
|
||||
#!/usr/bin/env bash
|
||||
# Linux build automation: translate -> emit build shards -> configure -> compile -> publish.
|
||||
#
|
||||
# This is the native-Linux counterpart to Launcher/LocalBuild.ps1. It is a from-scratch parallel
|
||||
# implementation, not a port of NativeBuildFlags.ps1: that file's canonical flags and
|
||||
# prebuilt-package fingerprinting exist only for the Windows/mingw toolchain (a precompiled
|
||||
# aurora/third-party package, offline pinned dependencies) that this script does not build.
|
||||
# Linux always builds aurora from source, letting its own CMake auto-detect Vulkan + vendor
|
||||
# SDL3/Dawn via FetchContent - the same configuration already verified working by hand.
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
log_step() {
|
||||
# $1 = machine-readable step id, $2 = human sentence. Mirrors LocalBuild.ps1's
|
||||
# Write-MkwBuildStep: the id is a stable marker a future installer could parse from the log,
|
||||
# the sentence is for the human reading the terminal.
|
||||
printf 'MKWCBUILD:STEP:%s %s\n' "$1" "$2"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "local-build.sh: error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_file() {
|
||||
[[ -f "$1" ]] || fail "$2 is missing: $1"
|
||||
}
|
||||
|
||||
assert_dir() {
|
||||
[[ -d "$1" ]] || fail "$2 is missing: $1"
|
||||
}
|
||||
|
||||
require_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "required tool '$1' was not found on PATH (override with --$2)"
|
||||
}
|
||||
|
||||
sha256_of() {
|
||||
sha256sum "$1" | awk '{print $1}'
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Argument parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
workspace=$(cd "$script_dir/.." && pwd)
|
||||
profile=base
|
||||
output_dir=""
|
||||
base_output_dir=""
|
||||
retro_rewind_package_dir=""
|
||||
retro_wfc_offline_dir=""
|
||||
skip_retro_wfc_payload=0
|
||||
force_clean_build=0
|
||||
parallel_override=0
|
||||
cc_override=""
|
||||
cxx_override=""
|
||||
cmake_override=""
|
||||
ninja_override=""
|
||||
dotnet_override=""
|
||||
translator_dll_override=""
|
||||
translator_bin_override=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: local-build.sh --output-dir DIR [options]
|
||||
|
||||
--workspace DIR Repository root (default: this script's parent directory)
|
||||
--profile {base|retro-rewind|both} Build profile (default: base)
|
||||
--output-dir DIR Where the built product is published (required)
|
||||
--base-output-dir DIR Second output directory; required with --profile both
|
||||
--retro-rewind-package-dir DIR Retro Rewind source tree (default: PulsarPacks/completed/RetroRewind/RetroRewind6)
|
||||
--retro-wfc-offline-dir DIR Offline Retro-WFC payload directory
|
||||
--skip-retro-wfc-payload Build Retro Rewind without a Retro-WFC payload
|
||||
--force-clean-build Discard every translation/build cache first
|
||||
--parallel N Pin translator threads, translated-shard job pool, and Ninja parallelism to N
|
||||
--cc PATH / --cxx PATH C/C++ compiler (default: cc/c++ on PATH)
|
||||
--cmake PATH / --ninja PATH Build tools (default: on PATH)
|
||||
--dotnet PATH dotnet executable (default: on PATH)
|
||||
--translator-dll PATH Pre-built Translator.Cli.dll (skips building the translator; still needs --dotnet to run it)
|
||||
--translator-bin PATH Self-contained Translator.Cli executable (skips building AND needs no dotnet at all)
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--workspace) workspace=$(cd "$2" && pwd); shift 2 ;;
|
||||
--profile) profile=$2; shift 2 ;;
|
||||
--output-dir) output_dir=$2; shift 2 ;;
|
||||
--base-output-dir) base_output_dir=$2; shift 2 ;;
|
||||
--retro-rewind-package-dir) retro_rewind_package_dir=$2; shift 2 ;;
|
||||
--retro-wfc-offline-dir) retro_wfc_offline_dir=$2; shift 2 ;;
|
||||
--skip-retro-wfc-payload) skip_retro_wfc_payload=1; shift ;;
|
||||
--force-clean-build) force_clean_build=1; shift ;;
|
||||
--parallel) parallel_override=$2; shift 2 ;;
|
||||
--cc) cc_override=$2; shift 2 ;;
|
||||
--cxx) cxx_override=$2; shift 2 ;;
|
||||
--cmake) cmake_override=$2; shift 2 ;;
|
||||
--ninja) ninja_override=$2; shift 2 ;;
|
||||
--dotnet) dotnet_override=$2; shift 2 ;;
|
||||
--translator-dll) translator_dll_override=$2; shift 2 ;;
|
||||
--translator-bin) translator_bin_override=$2; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) fail "unknown argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$output_dir" ]] || { usage; fail "--output-dir is required"; }
|
||||
case "$profile" in
|
||||
base|retro-rewind|both) ;;
|
||||
*) fail "--profile must be base, retro-rewind, or both" ;;
|
||||
esac
|
||||
|
||||
builds_retro=0
|
||||
[[ "$profile" == "retro-rewind" || "$profile" == "both" ]] && builds_retro=1
|
||||
has_offline_retro_wfc=0
|
||||
[[ -n "$retro_wfc_offline_dir" ]] && has_offline_retro_wfc=1
|
||||
|
||||
if [[ "$builds_retro" -eq 0 ]]; then
|
||||
if [[ "$has_offline_retro_wfc" -eq 1 || "$skip_retro_wfc_payload" -eq 1 ]]; then
|
||||
fail "Retro-WFC payload options are valid only for a Retro Rewind build."
|
||||
fi
|
||||
if [[ -n "$retro_rewind_package_dir" ]]; then
|
||||
fail "--retro-rewind-package-dir is valid only for a Retro Rewind build."
|
||||
fi
|
||||
else
|
||||
if [[ "$has_offline_retro_wfc" -eq "$skip_retro_wfc_payload" ]]; then
|
||||
fail "Choose exactly one Retro-WFC mode: --retro-wfc-offline-dir or --skip-retro-wfc-payload."
|
||||
fi
|
||||
fi
|
||||
if [[ "$profile" == "both" && -z "$base_output_dir" ]]; then
|
||||
fail "--base-output-dir is required with --profile both; --output-dir receives the Retro Rewind product."
|
||||
fi
|
||||
if [[ "$profile" != "both" && -n "$base_output_dir" ]]; then
|
||||
fail "--base-output-dir is valid only with --profile both."
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool resolution and prerequisite checks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
dotnet_bin=${dotnet_override:-dotnet}
|
||||
cmake_bin=${cmake_override:-cmake}
|
||||
ninja_bin=${ninja_override:-ninja}
|
||||
cc_bin=${cc_override:-clang}
|
||||
cxx_bin=${cxx_override:-clang++}
|
||||
|
||||
# A self-contained --translator-bin needs no dotnet at all (it bundles its own runtime); dotnet is
|
||||
# only required when the translator has to be built from source or run as a plain .dll.
|
||||
if [[ -z "$translator_bin_override" ]]; then
|
||||
require_command "$dotnet_bin" dotnet
|
||||
fi
|
||||
require_command "$cmake_bin" cmake
|
||||
require_command "$ninja_bin" ninja
|
||||
require_command "$cc_bin" cc
|
||||
require_command "$cxx_bin" cxx
|
||||
|
||||
project=$workspace/projects/mkwii/recomp.yml
|
||||
assets=$workspace/Assets
|
||||
generated=$workspace/generated
|
||||
functions=$generated/functions
|
||||
base_metadata=$generated/base_translation_output.json
|
||||
base_manifest_dir=$workspace/build/base
|
||||
base_manifest=$base_manifest_dir/mkwii_base_manifest.json
|
||||
shards=$generated/build_shards
|
||||
build=$workspace/native-build
|
||||
translation_provenance=$generated/translation-provenance.json
|
||||
toolchain_provenance=$build/toolchain-provenance.json
|
||||
retro_root=${retro_rewind_package_dir:-$workspace/PulsarPacks/completed/RetroRewind/RetroRewind6}
|
||||
|
||||
assert_file "$project" "Translation project"
|
||||
assert_file "$assets/main.dol" "Extracted main.dol (see translator/README.md - owning the game is required)"
|
||||
assert_file "$assets/StaticR.rel" "Extracted StaticR.rel (see translator/README.md - owning the game is required)"
|
||||
|
||||
# Literal line matching against the manifest's fixed shape, not a YAML dependency - the same
|
||||
# approach NativeBuildFlags.ps1's Get-MkwProjectPins uses on Windows, kept here only for the one
|
||||
# field this script actually needs from the manifest.
|
||||
entry_point=$(awk '
|
||||
/^translation:/ { in_translation = 1 }
|
||||
in_translation && /^[[:space:]]*-[[:space:]]*0[xX][0-9a-fA-F]+[[:space:]]*$/ {
|
||||
gsub(/^[[:space:]]*-[[:space:]]*/, ""); gsub(/[[:space:]]*$/, ""); print; exit
|
||||
}
|
||||
' "$project")
|
||||
[[ -n "$entry_point" ]] || fail "Could not find a translation entry point in $project"
|
||||
|
||||
translator_bin=$translator_bin_override
|
||||
translator_dll=$translator_dll_override
|
||||
if [[ -n "$translator_bin" ]]; then
|
||||
assert_file "$translator_bin" "Translator.Cli executable"
|
||||
translator() { "$translator_bin" "$@"; }
|
||||
else
|
||||
if [[ -z "$translator_dll" ]]; then
|
||||
translator_dll=$workspace/translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll
|
||||
log_step build-translator "Building the translator"
|
||||
"$dotnet_bin" build "$workspace/translator/src/Translator.Cli/Translator.Cli.csproj" -c Release
|
||||
fi
|
||||
assert_file "$translator_dll" "Translator.Cli.dll"
|
||||
translator() { "$dotnet_bin" "$translator_dll" "$@"; }
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parallelism: three independent knobs, same reasoning as LocalBuild.ps1 -
|
||||
# translator_threads (translation's own worker threads), translated_jobs (the real RAM guard,
|
||||
# capping concurrent compiles of memory-hungry translated TUs via Ninja's MKW_TRANSLATED_COMPILE_JOBS
|
||||
# pool), global_jobs (Ninja's overall parallelism). --parallel pins all three.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
cpu_count=$(nproc)
|
||||
mem_gib=$(( $(awk '/^MemTotal:/{print $2}' /proc/meminfo) / 1024 / 1024 ))
|
||||
(( mem_gib < 1 )) && mem_gib=1
|
||||
|
||||
if (( parallel_override > 0 )); then
|
||||
translator_threads=$parallel_override
|
||||
translated_jobs=$parallel_override
|
||||
global_jobs=$parallel_override
|
||||
else
|
||||
translator_threads=$(( cpu_count < 16 ? cpu_count : 16 ))
|
||||
(( translator_threads < 1 )) && translator_threads=1
|
||||
mem_based_cap=$(( mem_gib / 2 ))
|
||||
(( mem_based_cap < 1 )) && mem_based_cap=1
|
||||
translated_jobs=$(( cpu_count < mem_based_cap ? cpu_count : mem_based_cap ))
|
||||
(( translated_jobs < 1 )) && translated_jobs=1
|
||||
global_jobs=$(( translated_jobs > cpu_count ? translated_jobs : cpu_count ))
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Translation cache: this script is the only owner of the reuse decision (unlike LocalBuild.ps1,
|
||||
# which is handed caller-computed fingerprints by the Windows installer - there is no Linux
|
||||
# installer yet to supply anything). Hash the game inputs the translation actually depends on;
|
||||
# a match plus every expected output file present means the previous translation is still good.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if (( force_clean_build )); then
|
||||
log_step force-clean "A clean build was requested; discarding every translation and build cache"
|
||||
rm -rf "$generated" "$base_manifest_dir" "$build"
|
||||
fi
|
||||
|
||||
translation_fingerprint=$(cat "$assets/main.dol" "$assets/StaticR.rel" "$project" | sha256sum | awk '{print $1}')
|
||||
reuse_base=0
|
||||
if [[ -f "$translation_provenance" ]]; then
|
||||
recorded=$(grep -o '"TranslationFingerprint" *: *"[^"]*"' "$translation_provenance" 2>/dev/null | sed 's/.*"\([0-9a-f]*\)"$/\1/' || true)
|
||||
if [[ "$recorded" == "$translation_fingerprint" && -f "$base_metadata" && -f "$base_manifest" ]]; then
|
||||
reuse_base=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( builds_retro )); then
|
||||
# The translator discovers the mod through the project file's workspace-relative profile
|
||||
# paths, and both the base and mod leg block leaf inlining at every address the profile
|
||||
# patches - so the selected Code.pul must sit at the profile's mod_root before either leg runs.
|
||||
source_pul=$retro_root/Binaries/Code.pul
|
||||
assert_file "$source_pul" "Retro Rewind Code.pul"
|
||||
staged_binaries=$workspace/PulsarPacks/completed/RetroRewind/RetroRewind6/Binaries
|
||||
mkdir -p "$staged_binaries"
|
||||
staged_pul=$staged_binaries/Code.pul
|
||||
if [[ "$(cd "$(dirname "$source_pul")" && pwd)/$(basename "$source_pul")" != "$(cd "$(dirname "$staged_pul")" && pwd)/$(basename "$staged_pul")" ]]; then
|
||||
cp -f "$source_pul" "$staged_pul"
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( reuse_base )) && (( builds_retro )); then
|
||||
# A base tree that never saw this Code.pul would silently bake vanilla code into the modded
|
||||
# product - check-base-mod-awareness fails closed (anything but exit 0 forces a retranslation).
|
||||
retro_code_pul=$retro_root/Binaries/Code.pul
|
||||
assert_file "$retro_code_pul" "Retro Rewind Code.pul"
|
||||
pul_sha=$(sha256_of "$retro_code_pul")
|
||||
if ! grep -q "\"codePulSha256\":\"$pul_sha\"" "$base_metadata"; then
|
||||
if ! translator check-base-mod-awareness --project "$project" --profile retro-rewind \
|
||||
--translation-output-metadata "$base_metadata" --code-pul "$retro_code_pul"; then
|
||||
log_step retranslate-base "The base translation is stale; retranslating the base game for the new Code.pul"
|
||||
reuse_base=0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( reuse_base )); then
|
||||
log_step reuse-base-translation "Reusing the completed base translation"
|
||||
else
|
||||
rm -f "$translation_provenance"
|
||||
mkdir -p "$generated" "$base_manifest_dir"
|
||||
|
||||
log_step translate-base "Translating the user-owned base game"
|
||||
translator translate-recursive "$entry_point" --project "$project" \
|
||||
--outdir "$functions" --output-metadata "$base_metadata" \
|
||||
--production-source-bundle "$generated/base_translation_sources.bin" \
|
||||
--no-function-files --prune-stale --threads "$translator_threads"
|
||||
|
||||
log_step emit-base-manifest "Creating the local base translation manifest"
|
||||
translator emit-base-manifest --project "$project" --out "$base_manifest_dir" \
|
||||
--functions-dir "$functions" --translation-output-metadata "$base_metadata" --region P
|
||||
|
||||
printf '{"SchemaVersion":1,"TranslationFingerprint":"%s"}' "$translation_fingerprint" \
|
||||
> "$translation_provenance"
|
||||
fi
|
||||
|
||||
if (( builds_retro )); then
|
||||
code_pul=$retro_root/Binaries/Code.pul
|
||||
assert_file "$code_pul" "Retro Rewind Code.pul"
|
||||
retro_out=$workspace/build/mods/retro_rewind_full_cpp
|
||||
translate_mod_args=(translate-mod --project "$project" --profile retro-rewind
|
||||
--base-manifest "$base_manifest" --base-translation-output-metadata "$base_metadata"
|
||||
--code-pul "$code_pul" --mod-root "$retro_root" --mod-name "Retro Rewind"
|
||||
--region P --out "$retro_out" --prefer-cached-inputs --emit-cpp
|
||||
--threads "$translator_threads")
|
||||
if (( skip_retro_wfc_payload )); then
|
||||
translate_mod_args+=(--skip-retro-wfc)
|
||||
else
|
||||
offline_payload=$retro_wfc_offline_dir/binary/payload.RMCPD00.bin
|
||||
assert_file "$offline_payload" "Offline Retro-WFC shared payload"
|
||||
translate_mod_args+=(--retro-wfc-payload "$offline_payload")
|
||||
fi
|
||||
log_step translate-mod "Translating the selected Retro Rewind Code.pul"
|
||||
translator "${translate_mod_args[@]}"
|
||||
fi
|
||||
|
||||
log_step generate-data-init "Generating local game data initialization"
|
||||
translator generate-data-init --project "$project"
|
||||
|
||||
shard_args=(emit-build-shards --project "$project" --base-metadata "$base_metadata"
|
||||
--base-functions-dir "$functions" --native-source-dir "$workspace/runtime/src" --out "$shards")
|
||||
if (( builds_retro )); then
|
||||
retro_out=$workspace/build/mods/retro_rewind_full_cpp
|
||||
shard_args+=(--resolved-profile "$retro_out/resolved_dispatch_profile.json"
|
||||
--retro-cpp-dir "$retro_out/cpp")
|
||||
fi
|
||||
log_step emit-build-shards "Preparing local native build shards"
|
||||
translator "${shard_args[@]}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Native configure + build. Deliberately not passing -DAURORA_DAWN_PROVIDER=package or
|
||||
# -DFETCHCONTENT_FULLY_DISCONNECTED=ON: those exist for the Windows prebuilt-package/offline-
|
||||
# dependencies workflow this script does not build. aurora's own CMake auto-detects Linux and
|
||||
# picks Vulkan + vendors SDL3/Dawn via FetchContent, exactly as already verified working by hand.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
keep_native_build=0
|
||||
if [[ -f "$build/CMakeCache.txt" ]]; then
|
||||
expected_home=$workspace/runtime
|
||||
cache_home=$(grep '^CMAKE_HOME_DIRECTORY:INTERNAL=' "$build/CMakeCache.txt" | cut -d= -f2- || true)
|
||||
if [[ -n "$cache_home" && "$(cd "$cache_home" 2>/dev/null && pwd)" == "$expected_home" ]]; then
|
||||
keep_native_build=1
|
||||
fi
|
||||
fi
|
||||
if [[ -d "$build" && "$keep_native_build" -eq 0 ]]; then
|
||||
echo "MKWCBUILD: The native build cache does not belong to this workspace path; rebuilding from scratch"
|
||||
rm -rf "$build"
|
||||
elif [[ "$keep_native_build" -eq 1 ]]; then
|
||||
echo "MKWCBUILD: Reusing the incremental native build directory"
|
||||
fi
|
||||
|
||||
log_step configure-native "Configuring the native toolchain"
|
||||
"$cmake_bin" -S "$workspace/runtime" -B "$build" -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_COMPILER="$cc_bin" -DCMAKE_CXX_COMPILER="$cxx_bin" \
|
||||
-DCMAKE_MAKE_PROGRAM="$ninja_bin" \
|
||||
-DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs"
|
||||
|
||||
case "$profile" in
|
||||
base) targets=(WiiCompiled) ;;
|
||||
retro-rewind) targets=(RetroRewind) ;;
|
||||
both) targets=(WiiCompiled RetroRewind) ;;
|
||||
esac
|
||||
build_args=(--build "$build")
|
||||
for target in "${targets[@]}"; do build_args+=(--target "$target"); done
|
||||
build_args+=(--parallel "$global_jobs")
|
||||
log_step compile "Compiling ${targets[*]} locally"
|
||||
"$cmake_bin" "${build_args[@]}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Publish: the Linux build statically links SDL3/Dawn/etc (verified this session), so unlike
|
||||
# LocalBuild.ps1's DLL-copying dance there is nothing to copy beside the binary except the
|
||||
# runtime's own first-run assets.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
dol_sha=$(sha256_of "$assets/main.dol")
|
||||
rel_sha=$(sha256_of "$assets/StaticR.rel")
|
||||
compiler_version=$("$cxx_bin" --version | head -1)
|
||||
|
||||
publish_built_product() {
|
||||
local target=$1 destination=$2 provenance_profile=$3
|
||||
mkdir -p "$destination"
|
||||
local exe=$build/$target
|
||||
assert_file "$exe" "Locally compiled game executable"
|
||||
cp -f "$exe" "$destination/$target"
|
||||
for name in dsp_coef.bin initial_pipeline_cache.db; do
|
||||
[[ -f "$build/$name" ]] && cp -f "$build/$name" "$destination/"
|
||||
done
|
||||
[[ -d "$build/wii_bootstrap" ]] && cp -rf "$build/wii_bootstrap" "$destination/"
|
||||
|
||||
local is_retro=0 code_pul_sha=null
|
||||
if [[ "$provenance_profile" == "retro-rewind" ]]; then
|
||||
is_retro=1
|
||||
code_pul_sha=\"$(sha256_of "$retro_root/Binaries/Code.pul")\"
|
||||
fi
|
||||
local built_utc
|
||||
built_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
cat > "$destination/local-build.json" <<JSON
|
||||
{
|
||||
"SchemaVersion": 1,
|
||||
"Profile": "$provenance_profile",
|
||||
"BuiltUtc": "$built_utc",
|
||||
"DolSha256": "$dol_sha",
|
||||
"RelSha256": "$rel_sha",
|
||||
"CodePulSha256": $code_pul_sha,
|
||||
"Compiler": "$compiler_version"
|
||||
}
|
||||
JSON
|
||||
}
|
||||
|
||||
case "$profile" in
|
||||
both)
|
||||
publish_built_product WiiCompiled "$base_output_dir" base
|
||||
publish_built_product RetroRewind "$output_dir" retro-rewind
|
||||
;;
|
||||
retro-rewind)
|
||||
publish_built_product RetroRewind "$output_dir" retro-rewind
|
||||
;;
|
||||
base)
|
||||
publish_built_product WiiCompiled "$output_dir" base
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "MKWCBUILD:OUTPUT=$output_dir"
|
||||
+17
-6
@@ -103,6 +103,17 @@ Source: <https://github.com/ToruNiina/toml11/tree/v4.4.0>. Full license text:
|
||||
Copyright (c) Antoine Aubry and contributors.
|
||||
Referenced by `translator/src/Translator.Core`. Source: <https://github.com/aaubry/YamlDotNet>
|
||||
|
||||
### libco - ISC (valgrind.h: BSD-style)
|
||||
|
||||
Copyright byuu and the higan team.
|
||||
Non-Windows builds use libco's symmetric stackful coroutines in place of Win32 Fibers for guest
|
||||
OSThread scheduling (`runtime/src/fiber_manager.cpp`). Vendored in full (all non-Windows
|
||||
CPU-architecture backends - amd64, x86, arm, aarch64, ppc, ppc64v2, plus the portable sjlj
|
||||
fallback - though this project's x86_64-only target only ever compiles amd64.c) in
|
||||
`runtime/third_party/libco` from commit `e18e09d634d612a01781168ad4d76be10a7e3bad`.
|
||||
Source: <https://github.com/higan-emu/libco>. Full license text:
|
||||
`runtime/third_party/libco/LICENSE`.
|
||||
|
||||
---
|
||||
|
||||
## Fetched at build time and redistributed in release builds
|
||||
@@ -129,6 +140,7 @@ included in the installer's `licenses/` folder.
|
||||
| SQLite | 3.51.3 amalgamation | Public domain | <https://sqlite.org/> |
|
||||
| Tracy Profiler | pinned commit | BSD-3-Clause | <https://github.com/wolfpld/tracy> |
|
||||
| C++/WinRT | - | MIT (Microsoft) | <https://github.com/microsoft/cppwinrt> |
|
||||
| nodtool (disc image extraction) | v2.0.0-alpha.10 | MIT OR Apache-2.0 | <https://github.com/encounter/nod> |
|
||||
|
||||
### Dual-licensed components - elections made by this project
|
||||
|
||||
@@ -153,16 +165,15 @@ unmodified, with their license texts, in the installer's `licenses/` folder.
|
||||
| llvm-mingw (Clang, LLD, libc++, libunwind, MinGW-w64 runtime) | Apache-2.0 with LLVM Exception; MinGW-w64 runtime under its own permissive terms; bundled GNU utilities under GPL-2.0-or-later or GPL-3.0-or-later | <https://github.com/mstorsjo/llvm-mingw> |
|
||||
| CMake | BSD-3-Clause | <https://cmake.org/> |
|
||||
| Ninja | Apache-2.0 | <https://ninja-build.org/> |
|
||||
| DolphinTool (disc image extraction) | GPL-2.0-or-later | <https://github.com/dolphin-emu/dolphin> |
|
||||
| nodtool (disc image extraction) | MIT OR Apache-2.0 | <https://github.com/encounter/nod> |
|
||||
| Microsoft Visual C++ Runtime (`vcruntime140.dll`, `vcruntime140_1.dll`, `msvcp140.dll`) | Microsoft redistributable terms | Microsoft Visual Studio |
|
||||
| `dxil.dll` | Microsoft redistributable (proprietary signing library) | Microsoft |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Several toolkit components are GPL-licensed (DolphinTool, and the GNU utilities inside
|
||||
> llvm-mingw). Their complete corresponding source is available from the upstream projects linked
|
||||
> above at their pinned versions, and this project will supply it on request for the exact versions
|
||||
> shipped in any given release. Pins live in `Launcher/Prepare-PortableTools.ps1` and
|
||||
> `Launcher/NativeBuildFlags.ps1`.
|
||||
> The GNU utilities bundled inside llvm-mingw are GPL-licensed. Their complete corresponding source
|
||||
> is available from the upstream project linked above at its pinned version, and this project will
|
||||
> supply it on request for the exact version shipped in any given release. Pins live in
|
||||
> `Launcher/Prepare-PortableTools.ps1` and `Launcher/NativeBuildFlags.ps1`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -329,12 +329,52 @@ void apply_port_preferences() noexcept {
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
// Ports are explicit assignments. SDL may choose a player index at connection
|
||||
// time, but accepting it would make a newly connected controller silently take
|
||||
// over a game port before the user assigns it in the controller menu.
|
||||
// over a game port before the user assigns it in the controller menu - which
|
||||
// matters here because a manually-assigned WUP-028 adapter port (see
|
||||
// wup028_adapter.cpp, Windows-only) could otherwise collide with one SDL
|
||||
// auto-claimed. Elsewhere, with no WUP-028 port to collide with, the original
|
||||
// auto-claim behavior below is restored instead.
|
||||
void ensure_player_index(GameController& controller) noexcept {
|
||||
assign_player_index(controller, -1);
|
||||
}
|
||||
#else
|
||||
// SDL only hands out a player index when the device already had a gamepad mapping
|
||||
// at connect time, so anything mapped later (the setup wizard) stays at -1.
|
||||
void ensure_player_index(GameController& controller) noexcept {
|
||||
const int32_t player = SDL_GetGamepadPlayerIndex(controller.m_controller);
|
||||
if (player >= 0) {
|
||||
controller.m_playerIndex = player;
|
||||
return;
|
||||
}
|
||||
if (controller.m_playerIndex >= 0) {
|
||||
return;
|
||||
}
|
||||
ensure_port_preferences_loaded();
|
||||
const auto claim = [&](bool skipConfiguredPorts) {
|
||||
for (int32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
||||
if (skipConfiguredPorts && g_portPreferences[port].state != PortPreferenceState::Unset) {
|
||||
continue;
|
||||
}
|
||||
const bool taken = std::any_of(g_GameControllers.begin(), g_GameControllers.end(), [&](const auto& entry) {
|
||||
return entry.second.m_controller != controller.m_controller && effective_player_index(entry.second) == port;
|
||||
});
|
||||
if (!taken) {
|
||||
assign_player_index(controller, port);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// Explicitly configured ports are only used as a last resort so a hot-plugged
|
||||
// controller cannot steal the port its preferred device will claim.
|
||||
if (!claim(true)) {
|
||||
claim(false);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
GameController* get_controller_for_player(uint32_t player) noexcept {
|
||||
|
||||
+44
-5
@@ -1,10 +1,13 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(mkw_recompiled)
|
||||
|
||||
if(NOT WIN32 OR NOT MINGW OR NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR
|
||||
if((NOT (WIN32 AND MINGW)) AND (NOT CMAKE_SYSTEM_NAME STREQUAL "Linux"))
|
||||
message(FATAL_ERROR "WiiCompiled requires Windows (LLVM-MinGW) or native Linux")
|
||||
endif()
|
||||
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR
|
||||
NOT CMAKE_SIZEOF_VOID_P EQUAL 8 OR
|
||||
NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
|
||||
message(FATAL_ERROR "WiiCompiled requires 64-bit LLVM-MinGW Clang on Windows")
|
||||
message(FATAL_ERROR "WiiCompiled requires 64-bit Clang targeting x86_64")
|
||||
endif()
|
||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
message(FATAL_ERROR "WiiCompiled only supports Release builds")
|
||||
@@ -46,6 +49,23 @@ 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)
|
||||
|
||||
# Non-Windows guest-fiber scheduling (runtime/src/fiber_manager.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) - all of libco's non-Windows
|
||||
# CPU-architecture backends are kept, even though libco.c's own preprocessor dispatch
|
||||
# (__amd64__/__i386__/__arm__/__aarch64__/etc.) only ever selects amd64.c for this project's
|
||||
# x86_64-only target (see the platform/arch check above). Windows keeps using native Fibers
|
||||
# untouched, so this target is never built there.
|
||||
if(NOT WIN32)
|
||||
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.
|
||||
@@ -110,12 +130,22 @@ else()
|
||||
message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing")
|
||||
endif()
|
||||
set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE)
|
||||
if(WIN32)
|
||||
set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE)
|
||||
set(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_USE_WINDOWS_UI OFF CACHE BOOL "" FORCE)
|
||||
else()
|
||||
# Non-Windows (Linux): mirrors aurora-main's own
|
||||
# _aurora_dawn_set_platform_backends() choice for this platform - Vulkan only, no
|
||||
# D3D/HLSL. Kept in sync here because this project's own CMake FORCEs these cache
|
||||
# variables before aurora-main's add_subdirectory() runs, which pre-empts aurora's
|
||||
# auto-detection (CACHE ... INTERNAL "" without FORCE never overrides an existing value).
|
||||
set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE)
|
||||
set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
|
||||
set(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "" FORCE)
|
||||
set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(DAWN_USE_WINDOWS_UI OFF CACHE BOOL "" FORCE)
|
||||
|
||||
# Provide a tiny stub for DXProgrammableCapture when the SDK/PIX headers are
|
||||
# missing (common on MinGW). Dawn only includes the header; no symbols are
|
||||
@@ -207,6 +237,15 @@ 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})
|
||||
|
||||
# WUP-028 (official GameCube adapter) support talks to the adapter over WinUSB, which only
|
||||
# exists on Windows - SDL3 already exposes the same hardware as a normal joystick on Linux/macOS,
|
||||
# so this file has nothing to do there. Every call site into it is separately gated behind
|
||||
# #if defined(_WIN32) (see wup028_adapter.cpp's own header comment), so it's safe to simply not
|
||||
# compile it at all on other platforms rather than build a stub implementation.
|
||||
if(NOT WIN32)
|
||||
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/wup028_adapter.cpp")
|
||||
endif()
|
||||
|
||||
# The translator emits the complete, content-addressed source graph. Consuming
|
||||
# this one manifest keeps configure independent of the 28k generated function
|
||||
# files and of optional Retro Rewind artifacts such as code.map.
|
||||
|
||||
@@ -77,7 +77,11 @@ target_compile_definitions(mkw_runtime_common PRIVATE
|
||||
target_link_libraries(mkw_runtime_common PRIVATE
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw::pugixml mkw::toml11 mkw::cryptopp)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
|
||||
if(WIN32)
|
||||
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
|
||||
else()
|
||||
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco)
|
||||
endif()
|
||||
if(MKW_CPPWINRT_INCLUDE_DIR)
|
||||
if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
|
||||
message(FATAL_ERROR
|
||||
@@ -204,26 +208,38 @@ function(mkw_configure_product target)
|
||||
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
|
||||
endif()
|
||||
|
||||
target_link_libraries(${target} PRIVATE
|
||||
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp setupapi winusb)
|
||||
if(WIN32)
|
||||
target_link_libraries(${target} PRIVATE
|
||||
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp setupapi winusb)
|
||||
|
||||
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
foreach(runtime_dll libc++.dll libunwind.dll)
|
||||
execute_process(
|
||||
COMMAND "${CMAKE_CXX_COMPILER}" "--print-file-name=${runtime_dll}"
|
||||
OUTPUT_VARIABLE runtime_dll_path
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
if(NOT EXISTS "${runtime_dll_path}")
|
||||
get_filename_component(mkw_compiler_bin "${CMAKE_CXX_COMPILER}" DIRECTORY)
|
||||
set(runtime_dll_path "${mkw_compiler_bin}/${runtime_dll}")
|
||||
endif()
|
||||
if(NOT EXISTS "${runtime_dll_path}")
|
||||
message(FATAL_ERROR "llvm-mingw runtime DLL not found: ${runtime_dll}")
|
||||
endif()
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${runtime_dll_path}" $<TARGET_FILE_DIR:${target}>)
|
||||
endforeach()
|
||||
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
else()
|
||||
# mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o
|
||||
# files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own
|
||||
# target_link_libraries (object libraries don't carry usage requirements to a consumer
|
||||
# that isn't itself linked against as a target). fiber_manager.cpp's co_* calls live in
|
||||
# those objects, so the actual executable link needs mkw::libco directly, same as it
|
||||
# needs it independently of that first `if(WIN32)` branch above.
|
||||
target_link_libraries(${target} PRIVATE mkw::libco)
|
||||
endif()
|
||||
if(WIN32)
|
||||
foreach(runtime_dll libc++.dll libunwind.dll)
|
||||
execute_process(
|
||||
COMMAND "${CMAKE_CXX_COMPILER}" "--print-file-name=${runtime_dll}"
|
||||
OUTPUT_VARIABLE runtime_dll_path
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
if(NOT EXISTS "${runtime_dll_path}")
|
||||
get_filename_component(mkw_compiler_bin "${CMAKE_CXX_COMPILER}" DIRECTORY)
|
||||
set(runtime_dll_path "${mkw_compiler_bin}/${runtime_dll}")
|
||||
endif()
|
||||
if(NOT EXISTS "${runtime_dll_path}")
|
||||
message(FATAL_ERROR "llvm-mingw runtime DLL not found: ${runtime_dll}")
|
||||
endif()
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${runtime_dll_path}" $<TARGET_FILE_DIR:${target}>)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
set(MKW_WII_BOOTSTRAP_SOURCE_DIR "${MKW_RUNTIME_SOURCE_DIR}/assets/wii")
|
||||
if(NOT EXISTS "${MKW_WII_BOOTSTRAP_SOURCE_DIR}/shared2/wc24")
|
||||
|
||||
@@ -102,12 +102,24 @@ private:
|
||||
static void CALLBACK FiberProc(void* param);
|
||||
#else
|
||||
static void FiberProc(void* param);
|
||||
// libco's co_create() entry points take no argument (unlike CreateFiber's FiberProc(void*)),
|
||||
// so this trampoline reads the guest thread address staged by CreateGuestFiber() and forwards
|
||||
// into the (platform-neutral-bodied) FiberProc above. See fiber_manager.cpp.
|
||||
static void FiberProcTrampoline();
|
||||
#endif
|
||||
|
||||
// Switch from whichever fiber is currently active straight to the scheduler fiber, without
|
||||
// the SwitchToThread bookkeeping (CPU context save/restore, s_currentGuestThread). Used for
|
||||
// in-fiber yields that aren't a real guest thread switch: waiting out the EGG::Thread::start
|
||||
// deferral loop, and returning control on natural thread exit.
|
||||
static void SwitchToScheduler();
|
||||
|
||||
// Internal state
|
||||
static std::mutex s_mutex;
|
||||
static std::unordered_map<uint32_t, GuestFiber> s_fibers;
|
||||
static std::vector<void*> s_fibersPendingDelete;
|
||||
// The scheduler's own "fiber": a Windows HFIBER, or (non-Windows) libco's cothread_t for
|
||||
// whichever native call stack first called GuestFiberManager::Initialize() - both are
|
||||
// plain void* handles, so one field serves both platforms.
|
||||
static void* s_schedulerFiber;
|
||||
static uint32_t s_currentGuestThread;
|
||||
static bool s_initialized;
|
||||
|
||||
@@ -73,8 +73,12 @@ FaultCounters Counters();
|
||||
void LogFaultSummary() noexcept;
|
||||
|
||||
// Returns true when the access violation was a guest-space fault this module
|
||||
// resolved; the caller must then resume execution. `exceptionPointers` is a
|
||||
// Windows EXCEPTION_POINTERS*.
|
||||
bool HandleAccessViolation(void* exceptionPointers) noexcept;
|
||||
// resolved; the caller must then resume execution. `faultAddress` is the raw
|
||||
// host pointer the access violation trapped on (Windows: ExceptionInformation[1];
|
||||
// POSIX: siginfo_t::si_addr) and `isWrite` is whether it was a write access
|
||||
// (Windows: ExceptionInformation[0] != 0; POSIX: derived from the ucontext).
|
||||
// The platform-specific handler that calls this is expected to have already
|
||||
// done that extraction - this function only ever works with the parsed pair.
|
||||
bool HandleAccessViolation(void* faultAddress, bool isWrite) noexcept;
|
||||
|
||||
} // namespace GuestFlat
|
||||
|
||||
@@ -11,11 +11,22 @@ inline constexpr bool MkwStateFreeAbiEnabled(uint32_t) noexcept
|
||||
return true;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define MKW_PPC_FORCE_INLINE __forceinline
|
||||
#define MKW_PPC_NO_INLINE __declspec(noinline)
|
||||
#define MKW_PPC_INTERNAL_CALL __regcall
|
||||
#else
|
||||
// __forceinline/__declspec are MS-extension keywords Clang only recognizes when targeting
|
||||
// Windows (MSVC or mingw); native Linux Clang needs the GNU-attribute spellings instead.
|
||||
// __regcall has no portable non-Windows equivalent worth chasing here - the extra register
|
||||
// args it saves matter for the hot PPC interpreter loop on Windows, but plain calls are fine
|
||||
// elsewhere.
|
||||
#define MKW_PPC_FORCE_INLINE __attribute__((always_inline)) inline
|
||||
#define MKW_PPC_NO_INLINE __attribute__((noinline))
|
||||
#define MKW_PPC_INTERNAL_CALL
|
||||
#endif
|
||||
#define MKW_PPC_ALWAYS_INLINE_BODY __attribute__((always_inline))
|
||||
#define MKW_PPC_COLD __attribute__((cold))
|
||||
#define MKW_PPC_INTERNAL_CALL __regcall
|
||||
|
||||
|
||||
using MkwStateFreeResult2 = uint64_t __attribute__((ext_vector_type(2)));
|
||||
|
||||
@@ -18,8 +18,15 @@ extern "C" {
|
||||
}
|
||||
|
||||
namespace MemoryInline {
|
||||
#if defined(_WIN32)
|
||||
#define MKW_MEMORY_FORCE_INLINE __forceinline
|
||||
#define MKW_MEMORY_NO_INLINE __declspec(noinline)
|
||||
#else
|
||||
// See runtime/include/isa/ppc_isa_config.h for why non-Windows Clang needs the GNU-attribute
|
||||
// spellings instead of the MS-extension keywords.
|
||||
#define MKW_MEMORY_FORCE_INLINE __attribute__((always_inline)) inline
|
||||
#define MKW_MEMORY_NO_INLINE __attribute__((noinline))
|
||||
#endif
|
||||
#define MKW_MEMORY_COLD __attribute__((cold))
|
||||
inline constexpr uint32_t kPageShift = 20;
|
||||
inline constexpr uint32_t kPageSize = 1u << kPageShift;
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <shlobj.h>
|
||||
#else
|
||||
#include <cstdlib>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
struct RuntimeUserConfig {
|
||||
@@ -60,9 +63,11 @@ struct RuntimeUserConfig {
|
||||
// comma-separated SDL-style physical button names ("south", or
|
||||
// "dpad_up,left_shoulder") as values; pressing either bound button counts.
|
||||
std::array<std::optional<std::string>, 12> controllerButtons;
|
||||
#ifdef _WIN32
|
||||
// One-based physical WUP-028 adapter port assigned to each game port.
|
||||
// Zero or a missing value means the adapter does not own that game port.
|
||||
std::array<uint32_t, 4> gameCubeAdapterPorts{};
|
||||
#endif
|
||||
};
|
||||
|
||||
namespace RuntimeConfigFile {
|
||||
@@ -168,7 +173,22 @@ inline std::optional<std::filesystem::path> ExecutableDirectory() {
|
||||
buffer.resize(buffer.size() * 2);
|
||||
}
|
||||
#else
|
||||
return std::nullopt;
|
||||
// /proc/self/exe is a Linux-specific magic symlink to the running executable; readlink()
|
||||
// does not NUL-terminate and silently truncates if the buffer is too small, so this grows
|
||||
// the buffer until the result no longer fills it completely, the same doubling strategy as
|
||||
// the Windows branch above uses for GetModuleFileNameW.
|
||||
std::string buffer(256, '\0');
|
||||
for (;;) {
|
||||
const ssize_t length = readlink("/proc/self/exe", buffer.data(), buffer.size());
|
||||
if (length < 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (static_cast<size_t>(length) < buffer.size()) {
|
||||
buffer.resize(static_cast<size_t>(length));
|
||||
return std::filesystem::path(buffer).parent_path();
|
||||
}
|
||||
buffer.resize(buffer.size() * 2);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -209,6 +229,15 @@ inline std::filesystem::path ApplicationDataDirectory() {
|
||||
CoTaskMemFree(rawPath);
|
||||
return directory;
|
||||
}
|
||||
#else
|
||||
// XDG Base Directory spec equivalent of FOLDERID_LocalAppData: $XDG_DATA_HOME if set and
|
||||
// non-empty, otherwise its default of $HOME/.local/share.
|
||||
if (const char* xdgDataHome = std::getenv("XDG_DATA_HOME"); xdgDataHome && *xdgDataHome) {
|
||||
return std::filesystem::path(xdgDataHome) / kApplicationDirectoryName;
|
||||
}
|
||||
if (const char* home = std::getenv("HOME"); home && *home) {
|
||||
return std::filesystem::path(home) / ".local" / "share" / kApplicationDirectoryName;
|
||||
}
|
||||
#endif
|
||||
return std::filesystem::current_path() / kApplicationDirectoryName;
|
||||
}
|
||||
@@ -338,12 +367,14 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
config.controllerButtons[index] =
|
||||
FindConfigValue<std::string>(document, "controller", buttonKeys[index]);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
for (size_t index = 0; index < config.gameCubeAdapterPorts.size(); ++index) {
|
||||
const std::string key = "adapter_port_" + std::to_string(index + 1);
|
||||
if (auto value = FindConfigUint(document, "controller", key); value && *value <= 4) {
|
||||
config.gameCubeAdapterPorts[index] = *value;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
|
||||
config.windowPosX = FindConfigInt(document, "video", "window_x");
|
||||
@@ -607,6 +638,7 @@ inline bool SetControllerButton(size_t index, std::string value) {
|
||||
return WriteSetting("controller", kControllerButtonKeys[index], FormatString(value));
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
inline int GameCubeAdapterPort(size_t gamePort) {
|
||||
if (gamePort >= Get().gameCubeAdapterPorts.size()) return -1;
|
||||
const uint32_t physicalPort = Get().gameCubeAdapterPorts[gamePort];
|
||||
@@ -619,6 +651,7 @@ inline bool SetGameCubeAdapterPort(size_t gamePort, int physicalPort) {
|
||||
Mutable().gameCubeAdapterPorts[gamePort] = storedPort;
|
||||
return WriteSetting("controller", "adapter_port_" + std::to_string(gamePort + 1), std::to_string(storedPort));
|
||||
}
|
||||
#endif
|
||||
|
||||
inline bool SetAudioVolume(float value) {
|
||||
value = std::clamp(value, 0.0f, 1.0f);
|
||||
|
||||
@@ -11,10 +11,23 @@
|
||||
|
||||
#include "memory.h"
|
||||
|
||||
// Windows' SehLogger longjmps out of a vectored exception handler, where plain setjmp/longjmp is
|
||||
// the norm. A POSIX signal handler jumping back to here must use the sig-prefixed pair instead:
|
||||
// only sigsetjmp/siglongjmp save and restore the process signal mask, which is what keeps SIGSEGV
|
||||
// from staying blocked (and a second fault during the same ctor loop from escalating instead of
|
||||
// trapping) after the first recovered fault.
|
||||
#if defined(_WIN32)
|
||||
using MkwJmpBuf = jmp_buf;
|
||||
#define MKW_SETJMP(buf) setjmp(buf)
|
||||
#else
|
||||
using MkwJmpBuf = sigjmp_buf;
|
||||
#define MKW_SETJMP(buf) sigsetjmp(buf, 1)
|
||||
#endif
|
||||
|
||||
// Global flag to suppress SEH reporting (caught by system_bridge)
|
||||
extern bool g_suppressSehReporting;
|
||||
// Jump buffer for SEH recovery
|
||||
extern thread_local jmp_buf* g_sehJumpTarget;
|
||||
extern thread_local MkwJmpBuf* g_sehJumpTarget;
|
||||
// SEH details for the most recent trapped exception (used during ctor execution).
|
||||
extern thread_local uint32_t g_sehLastExceptionCode;
|
||||
extern thread_local uintptr_t g_sehLastExceptionAddress;
|
||||
|
||||
+106
-26
@@ -13,8 +13,24 @@
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#include "libco.h"
|
||||
#endif
|
||||
|
||||
namespace Fiber {
|
||||
|
||||
#if !defined(_WIN32)
|
||||
namespace {
|
||||
// libco's co_create() entry points take no argument, unlike CreateFiber(size, FiberProc, param).
|
||||
// CreateGuestFiber() stages the guest thread address here immediately before the first co_switch
|
||||
// into a freshly created cothread; FiberProcTrampoline reads it exactly once, at the top of the
|
||||
// fiber's very first activation. Safe because guest fibers are strictly cooperative on a single
|
||||
// OS thread: nothing else can run (and so nothing else can overwrite this) between the staging
|
||||
// write and the trampoline's read of it.
|
||||
thread_local uint32_t s_pendingFiberArg = 0;
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
std::mutex GuestFiberManager::s_mutex;
|
||||
std::unordered_map<uint32_t, GuestFiber> GuestFiberManager::s_fibers;
|
||||
std::vector<void*> GuestFiberManager::s_fibersPendingDelete;
|
||||
@@ -24,18 +40,25 @@ bool GuestFiberManager::s_initialized = false;
|
||||
thread_local CpuContext* GuestFiberManager::s_cpuContext = nullptr;
|
||||
|
||||
void GuestFiberManager::PurgePendingFibers() {
|
||||
#if defined(_WIN32)
|
||||
std::vector<void*> toDelete;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_mutex);
|
||||
toDelete.swap(s_fibersPendingDelete);
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
const void* current = GetCurrentFiber();
|
||||
for (void* f : toDelete) {
|
||||
if (f && f != current) {
|
||||
DeleteFiber(f);
|
||||
}
|
||||
}
|
||||
#else
|
||||
const void* current = co_active();
|
||||
for (void* f : toDelete) {
|
||||
if (f && f != current) {
|
||||
co_delete(static_cast<cothread_t>(f));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -204,10 +227,12 @@ void GuestFiberManager::Initialize() {
|
||||
}
|
||||
|
||||
#else
|
||||
RT_LOG(RT_TAG_OS) << "WARNING: Fiber support not available on this platform!" << std::endl;
|
||||
s_schedulerFiber = nullptr;
|
||||
// co_active() returns a handle for whichever native stack is currently running, creating one
|
||||
// on first call if needed - the libco analogue of ConvertThreadToFiber(nullptr): it converts
|
||||
// this call's own stack into a switchable target without altering control flow.
|
||||
s_schedulerFiber = co_active();
|
||||
#endif
|
||||
|
||||
|
||||
s_currentGuestThread = 0;
|
||||
s_initialized = true;
|
||||
}
|
||||
@@ -223,14 +248,25 @@ void GuestFiberManager::Shutdown() {
|
||||
}
|
||||
}
|
||||
s_fibers.clear();
|
||||
|
||||
|
||||
// Convert scheduler fiber back to thread
|
||||
if (s_schedulerFiber) {
|
||||
ConvertFiberToThread();
|
||||
s_schedulerFiber = nullptr;
|
||||
}
|
||||
#else
|
||||
for (auto& [addr, fiber] : s_fibers) {
|
||||
if (fiber.fiber && !fiber.isSchedulerFiber) {
|
||||
co_delete(static_cast<cothread_t>(fiber.fiber));
|
||||
fiber.fiber = nullptr;
|
||||
}
|
||||
}
|
||||
s_fibers.clear();
|
||||
// Unlike ConvertFiberToThread, libco has no "undo" for co_active(): the scheduler's own
|
||||
// stack was never separately allocated, so there is nothing to release here.
|
||||
s_schedulerFiber = nullptr;
|
||||
#endif
|
||||
|
||||
|
||||
s_initialized = false;
|
||||
}
|
||||
|
||||
@@ -250,12 +286,14 @@ bool GuestFiberManager::CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entr
|
||||
// Check if fiber already exists for this thread - if so, reset it
|
||||
auto existingIt = s_fibers.find(guestThreadAddr);
|
||||
if (existingIt != s_fibers.end()) {
|
||||
#if defined(_WIN32)
|
||||
// Delete the old fiber if it exists and is not the scheduler fiber
|
||||
if (existingIt->second.fiber && !existingIt->second.isSchedulerFiber) {
|
||||
#if defined(_WIN32)
|
||||
DeleteFiber(existingIt->second.fiber);
|
||||
}
|
||||
#else
|
||||
co_delete(static_cast<cothread_t>(existingIt->second.fiber));
|
||||
#endif
|
||||
}
|
||||
s_fibers.erase(existingIt);
|
||||
}
|
||||
|
||||
@@ -288,9 +326,18 @@ bool GuestFiberManager::CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entr
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
gf.fiber = nullptr;
|
||||
// libco's co_create() entry point takes no argument; SwitchToThread() stages guestThreadAddr
|
||||
// into s_pendingFiberArg immediately before the co_switch that first activates this handle.
|
||||
constexpr unsigned int kHostStackSize = 64 * 1024;
|
||||
gf.fiber = co_create(kHostStackSize, &FiberProcTrampoline);
|
||||
|
||||
if (!gf.fiber) {
|
||||
RT_LOG(RT_TAG_OS) << "co_create failed for thread 0x"
|
||||
<< std::hex << guestThreadAddr << std::dec << std::endl;
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
s_fibers[guestThreadAddr] = gf;
|
||||
|
||||
|
||||
@@ -342,17 +389,27 @@ void GuestFiberManager::ExitGuestThread(uint32_t guestThreadAddr, ThreadState fi
|
||||
s_currentGuestThread = 0;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
if (it->second.fiber && !it->second.isSchedulerFiber) {
|
||||
#if defined(_WIN32)
|
||||
const void* current = GetCurrentFiber();
|
||||
if (it->second.fiber == current) {
|
||||
s_fibersPendingDelete.push_back(it->second.fiber);
|
||||
} else {
|
||||
DeleteFiber(it->second.fiber);
|
||||
}
|
||||
#else
|
||||
const void* current = co_active();
|
||||
if (it->second.fiber == current) {
|
||||
// Deleting the coroutine we're currently executing on would free the very stack
|
||||
// this call is running on; defer it (PurgePendingFibers) until some other fiber is
|
||||
// active, exactly like the Windows branch above.
|
||||
s_fibersPendingDelete.push_back(it->second.fiber);
|
||||
} else {
|
||||
co_delete(static_cast<cothread_t>(it->second.fiber));
|
||||
}
|
||||
#endif
|
||||
it->second.fiber = nullptr;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void GuestFiberManager::SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu) {
|
||||
@@ -415,10 +472,13 @@ void GuestFiberManager::SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu
|
||||
// Store CPU context pointer for the target fiber to use
|
||||
s_cpuContext = cpu;
|
||||
|
||||
#if defined(_WIN32)
|
||||
// Check if we're already on the target fiber (e.g., switching to main thread
|
||||
// when we're already on the scheduler fiber)
|
||||
#if defined(_WIN32)
|
||||
void* currentFiber = GetCurrentFiber();
|
||||
#else
|
||||
void* currentFiber = co_active();
|
||||
#endif
|
||||
if (currentFiber == fiberHandle) {
|
||||
// Already executing on the target host fiber. This is common for the
|
||||
// default guest thread, which also owns the scheduler fiber. Keep the
|
||||
@@ -426,7 +486,7 @@ void GuestFiberManager::SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu
|
||||
// from before the guest thread slept.
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (cpu && haveTargetContext) {
|
||||
*cpu = targetContext;
|
||||
// FPSCR travels with the guest-thread context, and its NI bit is
|
||||
@@ -436,8 +496,16 @@ void GuestFiberManager::SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu
|
||||
}
|
||||
|
||||
// Switch to the target fiber (the target fiber will load its own context)
|
||||
#if defined(_WIN32)
|
||||
SwitchToFiber(fiberHandle);
|
||||
|
||||
#else
|
||||
// Staged for FiberProcTrampoline's first (and only) read; a no-op for a fiber that has
|
||||
// already started, since resuming it re-enters mid-function rather than through the
|
||||
// trampoline's entry point.
|
||||
s_pendingFiberArg = guestThreadAddr;
|
||||
co_switch(static_cast<cothread_t>(fiberHandle));
|
||||
#endif
|
||||
|
||||
// When we return here, the fiber that issued SwitchToThread has resumed.
|
||||
// That does not automatically mean the previous guest thread became runnable
|
||||
// again; a different thread may simply have yielded back to the scheduler.
|
||||
@@ -476,7 +544,6 @@ void GuestFiberManager::SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu
|
||||
s_currentGuestThread = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t GuestFiberManager::GetCurrentGuestThread() {
|
||||
@@ -551,6 +618,14 @@ void GuestFiberManager::ProcessTimerEvents(CpuContext* cpu) {
|
||||
}
|
||||
}
|
||||
|
||||
void GuestFiberManager::SwitchToScheduler() {
|
||||
#if defined(_WIN32)
|
||||
SwitchToFiber(s_schedulerFiber);
|
||||
#else
|
||||
co_switch(static_cast<cothread_t>(s_schedulerFiber));
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
void CALLBACK GuestFiberManager::FiberProc(void* param)
|
||||
#else
|
||||
@@ -558,9 +633,7 @@ void GuestFiberManager::FiberProc(void* param)
|
||||
#endif
|
||||
{
|
||||
uint32_t guestThreadAddr = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(param));
|
||||
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
// Get our fiber info
|
||||
GuestFiber* fiber = nullptr;
|
||||
uint32_t entryPoint = 0;
|
||||
@@ -571,7 +644,7 @@ void GuestFiberManager::FiberProc(void* param)
|
||||
auto it = s_fibers.find(guestThreadAddr);
|
||||
if (it == s_fibers.end()) {
|
||||
RT_LOG(RT_TAG_OS) << "FiberProc: fiber not found!" << std::endl;
|
||||
SwitchToFiber(s_schedulerFiber);
|
||||
SwitchToScheduler();
|
||||
return;
|
||||
}
|
||||
fiber = &it->second;
|
||||
@@ -634,7 +707,7 @@ void GuestFiberManager::FiberProc(void* param)
|
||||
<< ", fn=0x" << startFn << ") after retries; continuing anyway." << std::dec << std::endl;
|
||||
break;
|
||||
}
|
||||
SwitchToFiber(s_schedulerFiber);
|
||||
SwitchToScheduler();
|
||||
}
|
||||
|
||||
// The deferral loop above yields to the scheduler and therefore can resume
|
||||
@@ -691,11 +764,18 @@ void GuestFiberManager::FiberProc(void* param)
|
||||
}
|
||||
|
||||
// Return to scheduler
|
||||
SwitchToFiber(s_schedulerFiber);
|
||||
#else
|
||||
(void)guestThreadAddr;
|
||||
RT_LOG(RT_TAG_OS) << "Fibers not supported on this platform!" << std::endl;
|
||||
#endif
|
||||
SwitchToScheduler();
|
||||
}
|
||||
|
||||
#if !defined(_WIN32)
|
||||
void GuestFiberManager::FiberProcTrampoline() {
|
||||
const uint32_t guestThreadAddr = s_pendingFiberArg;
|
||||
FiberProc(reinterpret_cast<void*>(static_cast<uintptr_t>(guestThreadAddr)));
|
||||
// FiberProc always calls SwitchToScheduler() on every exit path and never falls off its own
|
||||
// end; this is only a safety net in case that ever changes; falling off co_create's entry
|
||||
// function is otherwise undefined behavior (libco's own crash() fallback aborts instead).
|
||||
SwitchToScheduler();
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace Fiber
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "runtime_log.h"
|
||||
#include "system_bridge.h"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
@@ -26,28 +27,57 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace GuestFlat {
|
||||
namespace {
|
||||
|
||||
#if defined(_WIN32)
|
||||
// Placeholder / view constants. Declared here so the build does not depend on
|
||||
// the exact Windows SDK version that first shipped them.
|
||||
constexpr DWORD kMemReplacePlaceholder = 0x00004000;
|
||||
constexpr DWORD kMemReservePlaceholder = 0x00040000;
|
||||
constexpr DWORD kMemPreservePlaceholder = 0x00000002;
|
||||
#endif
|
||||
constexpr size_t kAllocationGranularity = 0x10000; // 64 KiB
|
||||
constexpr size_t kHostPageSize = 0x1000;
|
||||
|
||||
// Named, platform-neutral protection modes so every fault-interception call site below (the
|
||||
// MMIO window, the executable-write guard, deferred-EFB-read protection, the on-demand
|
||||
// unmapped-block commit) can stay identical text on both platforms; only ProtectRange() and
|
||||
// CommitPlaceholder() below branch on VirtualProtect vs. mprotect.
|
||||
#if defined(_WIN32)
|
||||
using ProtectionFlags = DWORD;
|
||||
constexpr ProtectionFlags kProtNone = PAGE_NOACCESS;
|
||||
constexpr ProtectionFlags kProtRead = PAGE_READONLY;
|
||||
constexpr ProtectionFlags kProtReadWrite = PAGE_READWRITE;
|
||||
#else
|
||||
using ProtectionFlags = int;
|
||||
constexpr ProtectionFlags kProtNone = PROT_NONE;
|
||||
constexpr ProtectionFlags kProtRead = PROT_READ;
|
||||
constexpr ProtectionFlags kProtReadWrite = PROT_READ | PROT_WRITE;
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32)
|
||||
using VirtualAlloc2Fn = PVOID(WINAPI*)(HANDLE, PVOID, SIZE_T, ULONG, ULONG, void*, ULONG);
|
||||
using MapViewOfFile3Fn = PVOID(WINAPI*)(HANDLE, HANDLE, PVOID, ULONG64, SIZE_T, ULONG, ULONG, void*, ULONG);
|
||||
|
||||
VirtualAlloc2Fn g_virtualAlloc2 = nullptr;
|
||||
MapViewOfFile3Fn g_mapViewOfFile3 = nullptr;
|
||||
#endif
|
||||
|
||||
uint8_t* g_base = nullptr;
|
||||
bool g_initialized = false;
|
||||
std::vector<RegionRequest> g_activeRegions;
|
||||
#if defined(_WIN32)
|
||||
PVOID g_vectoredHandle = nullptr;
|
||||
#endif
|
||||
|
||||
std::mutex& StateMutex() {
|
||||
static std::mutex mutex;
|
||||
@@ -70,7 +100,11 @@ struct SectionKeyHash {
|
||||
};
|
||||
|
||||
struct Section {
|
||||
#if defined(_WIN32)
|
||||
HANDLE handle = nullptr;
|
||||
#else
|
||||
int fd = -1;
|
||||
#endif
|
||||
uint64_t size = 0;
|
||||
uint8_t* hostView = nullptr;
|
||||
};
|
||||
@@ -114,6 +148,18 @@ std::vector<GuardedRange>& DeferredRanges() {
|
||||
return ranges;
|
||||
}
|
||||
|
||||
#if !defined(_WIN32)
|
||||
// Windows disambiguates a racing "unmapped touch" fault via VirtualQuery (did some other thread
|
||||
// already commit this 64 KiB block, and is it actually accessible enough to satisfy this access).
|
||||
// mprotect has no query counterpart, so this tracks the same fact ourselves: one bit per 64 KiB
|
||||
// block, set the first time this module ever commits it, checked-and-set under StateMutex() so
|
||||
// two threads racing on the same never-yet-committed block still report/commit exactly once.
|
||||
std::vector<uint8_t>& UnmappedCommittedBlocks() {
|
||||
static std::vector<uint8_t> blocks(1u << 16, 0); // 2^32 / 64 KiB
|
||||
return blocks;
|
||||
}
|
||||
#endif
|
||||
|
||||
std::atomic<uint32_t> g_countMmio{0};
|
||||
std::atomic<uint32_t> g_countEfb{0};
|
||||
std::atomic<uint32_t> g_countXGuard{0};
|
||||
@@ -133,10 +179,26 @@ uint64_t RoundUp(uint64_t value, uint64_t alignment) {
|
||||
|
||||
std::string LastErrorText(const char* what) {
|
||||
std::ostringstream oss;
|
||||
#if defined(_WIN32)
|
||||
oss << what << " failed (GetLastError=" << GetLastError() << ")";
|
||||
#else
|
||||
oss << what << " failed (" << std::strerror(errno) << ")";
|
||||
#endif
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
// Protects [address, address+size) with `protection`, bridging VirtualProtect (Windows) and
|
||||
// mprotect (POSIX) so every fault-interception call site below can stay platform-neutral.
|
||||
bool ProtectRange(uint8_t* address, uint64_t size, ProtectionFlags protection) {
|
||||
#if defined(_WIN32)
|
||||
DWORD previous = 0;
|
||||
return VirtualProtect(address, static_cast<SIZE_T>(size), protection, &previous) != FALSE;
|
||||
#else
|
||||
return mprotect(address, static_cast<size_t>(size), protection) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
void ResolvePlacementApi() {
|
||||
if (g_virtualAlloc2 != nullptr && g_mapViewOfFile3 != nullptr) return;
|
||||
HMODULE kernelBase = GetModuleHandleW(L"kernelbase.dll");
|
||||
@@ -153,9 +215,11 @@ void ResolvePlacementApi() {
|
||||
"are unavailable on this system).");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void EnsureReservation() {
|
||||
if (g_base != nullptr) return;
|
||||
#if defined(_WIN32)
|
||||
ResolvePlacementApi();
|
||||
|
||||
void* requested = reinterpret_cast<void*>(kFixedFlatGuestBase);
|
||||
@@ -178,25 +242,59 @@ void EnsureReservation() {
|
||||
"usual cause.";
|
||||
throw std::runtime_error(oss.str());
|
||||
}
|
||||
|
||||
if (reserved != requested) {
|
||||
throw std::runtime_error(
|
||||
"The flat guest reservation did not land on the fixed base the translated code was "
|
||||
"compiled against.");
|
||||
}
|
||||
#else
|
||||
void* requested = reinterpret_cast<void*>(kFixedFlatGuestBase);
|
||||
|
||||
// No MAP_FIXED here (and deliberately no MAP_FIXED_NOREPLACE, which needs Linux 4.17+ -
|
||||
// this must work on kernels as old as 4.9): `requested` is only a hint. The kernel's
|
||||
// get_unmapped_area honors a page-aligned hint when the whole range is free, so this lands
|
||||
// on the fixed base in the normal case; if anything already occupies part of the range, the
|
||||
// kernel silently picks a different address instead of clobbering it, which the check below
|
||||
// catches - same "something got there first" contract as the Windows path, without needing
|
||||
// a specific kernel version.
|
||||
void* reserved = mmap(requested, kGuestSpaceSize + kAllocationGranularity, kProtNone,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
|
||||
if (reserved == MAP_FAILED) {
|
||||
std::ostringstream oss;
|
||||
oss << "Unable to reserve the 4 GiB flat guest address space at 0x" << std::hex
|
||||
<< reinterpret_cast<uintptr_t>(requested) << std::dec
|
||||
<< " (" << std::strerror(errno)
|
||||
<< "). The translated code addresses guest memory through this fixed base, so it "
|
||||
"cannot fall back to another one.";
|
||||
throw std::runtime_error(oss.str());
|
||||
}
|
||||
if (reserved != requested) {
|
||||
munmap(reserved, kGuestSpaceSize + kAllocationGranularity);
|
||||
std::ostringstream oss;
|
||||
oss << "Unable to reserve the 4 GiB flat guest address space at 0x" << std::hex
|
||||
<< reinterpret_cast<uintptr_t>(requested) << std::dec
|
||||
<< ". Something else in this process already occupies part of the 16 TiB region - "
|
||||
"an injected library, an overlay or a debugging tool is the usual cause.";
|
||||
throw std::runtime_error(oss.str());
|
||||
}
|
||||
#endif
|
||||
|
||||
g_base = static_cast<uint8_t*>(reserved);
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
// Carves `size` bytes out of the enclosing placeholder so a view or a private
|
||||
// commit can replace it. Splitting an exact-size placeholder is a no-op that
|
||||
// reports ERROR_INVALID_PARAMETER; the caller validates the replacement.
|
||||
void SplitPlaceholder(uint8_t* address, uint64_t size) {
|
||||
VirtualFree(address, static_cast<SIZE_T>(size), MEM_RELEASE | kMemPreservePlaceholder);
|
||||
}
|
||||
#endif
|
||||
|
||||
void MapGuestView(const Section& section, uint64_t sectionOffset, uint32_t guestBase,
|
||||
uint64_t mappedSize) {
|
||||
uint8_t* target = g_base + guestBase;
|
||||
#if defined(_WIN32)
|
||||
SplitPlaceholder(target, mappedSize);
|
||||
void* view = g_mapViewOfFile3(section.handle, GetCurrentProcess(), target, sectionOffset,
|
||||
static_cast<SIZE_T>(mappedSize), kMemReplacePlaceholder,
|
||||
@@ -208,16 +306,37 @@ void MapGuestView(const Section& section, uint64_t sectionOffset, uint32_t guest
|
||||
<< ")";
|
||||
throw std::runtime_error(oss.str());
|
||||
}
|
||||
#else
|
||||
// MAP_FIXED is safe (and needs no particular kernel version) here specifically because we're
|
||||
// deliberately overwriting a sub-range of the PROT_NONE reservation this module already owns
|
||||
// exclusively (see EnsureReservation) - unlike the initial reservation itself, there's no
|
||||
// "something else might already be there" concern to guard against.
|
||||
void* view = mmap(target, static_cast<size_t>(mappedSize), kProtReadWrite,
|
||||
MAP_SHARED | MAP_FIXED, section.fd, static_cast<off_t>(sectionOffset));
|
||||
if (view == MAP_FAILED) {
|
||||
std::ostringstream oss;
|
||||
oss << "Unable to map guest region 0x" << std::hex << guestBase << " (+0x" << mappedSize
|
||||
<< ") into the flat reservation" << std::dec << " (" << std::strerror(errno) << ")";
|
||||
throw std::runtime_error(oss.str());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Replaces a placeholder with private committed memory. Used for the MMIO
|
||||
// window (read-only zeros) and for on-demand commits of stray guest pages.
|
||||
bool CommitPlaceholder(uint8_t* address, uint64_t size, DWORD protection) {
|
||||
bool CommitPlaceholder(uint8_t* address, uint64_t size, ProtectionFlags protection) {
|
||||
#if defined(_WIN32)
|
||||
SplitPlaceholder(address, size);
|
||||
void* result = g_virtualAlloc2(GetCurrentProcess(), address, static_cast<SIZE_T>(size),
|
||||
MEM_RESERVE | MEM_COMMIT | kMemReplacePlaceholder, protection,
|
||||
nullptr, 0);
|
||||
return result != nullptr;
|
||||
#else
|
||||
// No separate reserve-vs-commit step is needed: the anonymous PROT_NONE reservation this
|
||||
// range came from is already demand-zero backed, so mprotect() alone both "commits" and
|
||||
// protects it.
|
||||
return ProtectRange(address, size, protection);
|
||||
#endif
|
||||
}
|
||||
|
||||
// One definition of the two windows lives in memory_access.h; these are the
|
||||
@@ -237,8 +356,7 @@ void ApplyExecutableProtectionLocked() {
|
||||
for (uint64_t page = first; page < last; page += kHostPageSize) {
|
||||
const uint32_t pageIndex = static_cast<uint32_t>(page >> 12);
|
||||
if (protectedPages[pageIndex] != 0) continue;
|
||||
DWORD previous = 0;
|
||||
if (VirtualProtect(g_base + page, kHostPageSize, PAGE_READONLY, &previous) != FALSE) {
|
||||
if (ProtectRange(g_base + page, kHostPageSize, kProtRead)) {
|
||||
protectedPages[pageIndex] = 1;
|
||||
}
|
||||
}
|
||||
@@ -284,8 +402,16 @@ void ZeroMappedStorage() {
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
LONG CALLBACK FlatGuestVectoredHandler(EXCEPTION_POINTERS* info) {
|
||||
if (HandleAccessViolation(info)) {
|
||||
const auto* record = info->ExceptionRecord;
|
||||
if (record == nullptr || record->ExceptionCode != EXCEPTION_ACCESS_VIOLATION ||
|
||||
record->NumberParameters < 2) {
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
void* faultAddress = reinterpret_cast<void*>(record->ExceptionInformation[1]);
|
||||
const bool isWrite = record->ExceptionInformation[0] != 0;
|
||||
if (HandleAccessViolation(faultAddress, isWrite)) {
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
@@ -298,6 +424,7 @@ void InstallVectoredHandler() {
|
||||
throw std::runtime_error(LastErrorText("AddVectoredExceptionHandler"));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void ReportFatalGuestFault(const char* category, uint32_t guestAddress, bool isWrite,
|
||||
const char* detail) {
|
||||
@@ -377,9 +504,7 @@ void Initialize(const std::vector<RegionRequest>& regions) {
|
||||
for (const auto& range : DeferredRanges()) {
|
||||
const uint64_t first = static_cast<uint64_t>(range.start) & ~(kHostPageSize - 1u);
|
||||
const uint64_t last = RoundUp(range.end, kHostPageSize);
|
||||
DWORD previous = 0;
|
||||
VirtualProtect(g_base + first, static_cast<SIZE_T>(last - first), PAGE_READWRITE,
|
||||
&previous);
|
||||
ProtectRange(g_base + first, last - first, kProtReadWrite);
|
||||
}
|
||||
DeferredRanges().clear();
|
||||
ZeroMappedStorage();
|
||||
@@ -408,6 +533,7 @@ void Initialize(const std::vector<RegionRequest>& regions) {
|
||||
const uint64_t rounded = RoundUp(size, kAllocationGranularity);
|
||||
Section section;
|
||||
section.size = rounded;
|
||||
#if defined(_WIN32)
|
||||
section.handle = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE,
|
||||
static_cast<DWORD>(rounded >> 32),
|
||||
static_cast<DWORD>(rounded & 0xFFFFFFFFu), nullptr);
|
||||
@@ -419,6 +545,25 @@ void Initialize(const std::vector<RegionRequest>& regions) {
|
||||
if (section.hostView == nullptr) {
|
||||
throw std::runtime_error(LastErrorText("MapViewOfFile for the host guest-RAM alias"));
|
||||
}
|
||||
#else
|
||||
// The section is an anonymous shared-memory object: the SAME physical pages get mapped
|
||||
// twice below (once here as the always-accessible host view, once per-region as the
|
||||
// guest view whose protection the fault handler controls), the same "one backing store,
|
||||
// two VA aliases" trick CreateFileMapping/MapViewOfFile(3) gives Windows.
|
||||
section.fd = memfd_create("wiicompiled-guest-ram", MFD_CLOEXEC);
|
||||
if (section.fd < 0) {
|
||||
throw std::runtime_error(LastErrorText("memfd_create for guest RAM"));
|
||||
}
|
||||
if (ftruncate(section.fd, static_cast<off_t>(rounded)) != 0) {
|
||||
throw std::runtime_error(LastErrorText("ftruncate for guest RAM"));
|
||||
}
|
||||
section.hostView = static_cast<uint8_t*>(
|
||||
mmap(nullptr, static_cast<size_t>(rounded), kProtReadWrite, MAP_SHARED, section.fd, 0));
|
||||
if (section.hostView == MAP_FAILED) {
|
||||
section.hostView = nullptr;
|
||||
throw std::runtime_error(LastErrorText("mmap for the host guest-RAM alias"));
|
||||
}
|
||||
#endif
|
||||
Sections()[key] = section;
|
||||
}
|
||||
|
||||
@@ -435,12 +580,14 @@ void Initialize(const std::vector<RegionRequest>& regions) {
|
||||
|
||||
// MMIO stays inaccessible in both directions so the vectored handler can report missing HLE; the old
|
||||
// PAGE_READONLY read window that returned zero turned missing devices into silent hangs instead.
|
||||
if (!CommitPlaceholder(g_base + 0xCC000000u, 0x02000000u, PAGE_NOACCESS)) {
|
||||
if (!CommitPlaceholder(g_base + 0xCC000000u, 0x02000000u, kProtNone)) {
|
||||
throw std::runtime_error(LastErrorText("committing the no-access MMIO window"));
|
||||
}
|
||||
|
||||
ApplyExecutableProtectionLocked();
|
||||
#if defined(_WIN32)
|
||||
InstallVectoredHandler();
|
||||
#endif
|
||||
|
||||
// Freshly created section objects are demand-zero, so no explicit clear is
|
||||
// needed on the first mapping (that would fault in all 152 MiB at startup).
|
||||
@@ -470,9 +617,7 @@ void ProtectDeferredRange(uint32_t address, size_t length) {
|
||||
std::lock_guard<std::mutex> lock(StateMutex());
|
||||
const uint64_t first = static_cast<uint64_t>(address) & ~(kHostPageSize - 1u);
|
||||
const uint64_t last = RoundUp(end, kHostPageSize);
|
||||
DWORD previous = 0;
|
||||
if (VirtualProtect(g_base + first, static_cast<SIZE_T>(last - first), PAGE_NOACCESS,
|
||||
&previous) == FALSE) {
|
||||
if (!ProtectRange(g_base + first, last - first, kProtNone)) {
|
||||
// An unmapped destination cannot be trapped; the checked path still
|
||||
// clears the readable bias, so nothing silently reads stale bytes.
|
||||
return;
|
||||
@@ -492,8 +637,7 @@ void UnprotectDeferredRange(uint32_t address, size_t length) {
|
||||
ranges.erase(it);
|
||||
const uint64_t first = static_cast<uint64_t>(address) & ~(kHostPageSize - 1u);
|
||||
const uint64_t last = RoundUp(end, kHostPageSize);
|
||||
DWORD previous = 0;
|
||||
VirtualProtect(g_base + first, static_cast<SIZE_T>(last - first), PAGE_READWRITE, &previous);
|
||||
ProtectRange(g_base + first, last - first, kProtReadWrite);
|
||||
}
|
||||
|
||||
void RegisterExecutableRange(uint32_t start, uint32_t end) {
|
||||
@@ -541,21 +685,14 @@ void LogFaultSummary() noexcept {
|
||||
std::cerr.flush();
|
||||
}
|
||||
|
||||
bool HandleAccessViolation(void* exceptionPointers) noexcept {
|
||||
if (!g_initialized || exceptionPointers == nullptr) return false;
|
||||
auto* info = static_cast<EXCEPTION_POINTERS*>(exceptionPointers);
|
||||
const auto* record = info->ExceptionRecord;
|
||||
if (record == nullptr || record->ExceptionCode != EXCEPTION_ACCESS_VIOLATION ||
|
||||
record->NumberParameters < 2) {
|
||||
return false;
|
||||
}
|
||||
bool HandleAccessViolation(void* faultAddress, bool isWrite) noexcept {
|
||||
if (!g_initialized || faultAddress == nullptr) return false;
|
||||
|
||||
const uintptr_t fault = static_cast<uintptr_t>(record->ExceptionInformation[1]);
|
||||
const uintptr_t fault = reinterpret_cast<uintptr_t>(faultAddress);
|
||||
const uintptr_t base = reinterpret_cast<uintptr_t>(g_base);
|
||||
if (fault < base || fault - base >= kGuestSpaceSize) return false;
|
||||
|
||||
const uint32_t guestAddress = static_cast<uint32_t>(fault - base);
|
||||
const bool isWrite = record->ExceptionInformation[0] != 0;
|
||||
|
||||
// 1) Deferred (EFB) read: materialize the pending copy and drop the trap for the whole 4 KiB page span,
|
||||
// not just the registered range, since protection is page-granular. Leaving a range registered but
|
||||
@@ -581,9 +718,7 @@ bool HandleAccessViolation(void* exceptionPointers) noexcept {
|
||||
ranges.erase(it);
|
||||
spanFirst = static_cast<uint64_t>(rangeStart) & ~(kHostPageSize - 1u);
|
||||
spanLast = RoundUp(rangeEnd, kHostPageSize);
|
||||
DWORD previous = 0;
|
||||
VirtualProtect(g_base + spanFirst, static_cast<SIZE_T>(spanLast - spanFirst),
|
||||
PAGE_READWRITE, &previous);
|
||||
ProtectRange(g_base + spanFirst, spanLast - spanFirst, kProtReadWrite);
|
||||
}
|
||||
}
|
||||
if (covered) {
|
||||
@@ -618,9 +753,8 @@ bool HandleAccessViolation(void* exceptionPointers) noexcept {
|
||||
// Those arrive in bulk, so the page is opened permanently
|
||||
// rather than trapping every relocation.
|
||||
std::lock_guard<std::mutex> lock(StateMutex());
|
||||
DWORD previous = 0;
|
||||
if (VirtualProtect(g_base + (static_cast<uint64_t>(pageIndex) << 12), kHostPageSize,
|
||||
PAGE_READWRITE, &previous) != FALSE) {
|
||||
if (ProtectRange(g_base + (static_cast<uint64_t>(pageIndex) << 12), kHostPageSize,
|
||||
kProtReadWrite)) {
|
||||
ExecutableProtectedPages()[pageIndex] = 0;
|
||||
}
|
||||
}
|
||||
@@ -665,6 +799,7 @@ bool HandleAccessViolation(void* exceptionPointers) noexcept {
|
||||
bool committed = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(StateMutex());
|
||||
#if defined(_WIN32)
|
||||
MEMORY_BASIC_INFORMATION mbi{};
|
||||
if (VirtualQuery(g_base + blockBase, &mbi, sizeof(mbi)) == 0) return false;
|
||||
if (mbi.State == MEM_COMMIT) {
|
||||
@@ -678,9 +813,24 @@ bool HandleAccessViolation(void* exceptionPointers) noexcept {
|
||||
PAGE_EXECUTE)) != 0;
|
||||
return isWrite ? writable : readable;
|
||||
}
|
||||
if (!CommitPlaceholder(g_base + blockBase, kAllocationGranularity, PAGE_READWRITE)) {
|
||||
#else
|
||||
// mprotect has no VirtualQuery counterpart to ask "is this block already committed and
|
||||
// how", so this module tracks the same fact itself (UnmappedCommittedBlocks, checked and
|
||||
// set under this same lock): once a block has been committed READ|WRITE by an earlier
|
||||
// call here (this thread's or a racing one's), every subsequent fault on it is a no-op
|
||||
// resume - there is no POSIX equivalent of "committed but insufficiently permissioned"
|
||||
// for a block only this function ever touches.
|
||||
const uint32_t blockIndex = static_cast<uint32_t>(blockBase / kAllocationGranularity);
|
||||
if (UnmappedCommittedBlocks()[blockIndex] != 0) {
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (!CommitPlaceholder(g_base + blockBase, kAllocationGranularity, kProtReadWrite)) {
|
||||
return false;
|
||||
}
|
||||
#if !defined(_WIN32)
|
||||
UnmappedCommittedBlocks()[blockIndex] = 1;
|
||||
#endif
|
||||
committed = true;
|
||||
}
|
||||
if (committed) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "hle_stubs.h"
|
||||
#include "memory.h"
|
||||
#include "hle/controller_status_contract.h"
|
||||
#ifdef _WIN32
|
||||
#include "wup028_adapter.h"
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
@@ -34,7 +36,9 @@ void WritePadStatus(uint32_t base, const PADStatus& status) {
|
||||
|
||||
extern "C" uint32_t PAD__Init_HLE()
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
Wup028Adapter::Initialize();
|
||||
#endif
|
||||
return PADInit() ? 1u : 0u;
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801AF2F0, PAD__Init_HLE, uint32_t, (), ());
|
||||
@@ -46,8 +50,9 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
|
||||
}
|
||||
|
||||
PADStatus statuses[PAD_CHANMAX]{};
|
||||
std::array<PADStatus, PAD_CHANMAX> adapterStatuses{};
|
||||
uint32_t rumbleMask = PADRead(statuses);
|
||||
#if defined(_WIN32)
|
||||
std::array<PADStatus, PAD_CHANMAX> adapterStatuses{};
|
||||
if (Wup028Adapter::Read(adapterStatuses) && !PADIsInputBlocked()) {
|
||||
for (uint32_t port = 0; port < PAD_CHANMAX; ++port) {
|
||||
if (adapterStatuses[port].err == PAD_ERR_NONE) {
|
||||
@@ -56,6 +61,7 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
try {
|
||||
for (uint32_t i = 0; i < PAD_CHANMAX; ++i) {
|
||||
@@ -84,8 +90,12 @@ PPC_NATIVE_OVERRIDE(801AF1E4, PAD__Recalibrate_HLE, uint32_t, (uint32_t mask), (
|
||||
|
||||
extern "C" void PAD__ControlMotor_HLE(int32_t chan, uint32_t command)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
if (!Wup028Adapter::SetRumble(static_cast<uint32_t>(chan), command == PAD_MOTOR_RUMBLE)) {
|
||||
PADControlMotor(chan, command);
|
||||
}
|
||||
#else
|
||||
PADControlMotor(chan, command);
|
||||
#endif
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801AF908, PAD__ControlMotor_HLE, (int32_t chan, uint32_t command), (chan, command));
|
||||
|
||||
@@ -635,7 +635,8 @@ static void BuildAndPublishRuntimeFst() {
|
||||
for (const DVDFileEntry& entry : g_fileEntries) {
|
||||
if (entry.discOffsetWords != 0) {
|
||||
nextFreeBytes = std::max(
|
||||
nextFreeBytes, static_cast<uint64_t>(entry.discOffsetWords) * 4ull + entry.size);
|
||||
nextFreeBytes, static_cast<uint64_t>(entry.discOffsetWords) * UINT64_C(4) +
|
||||
static_cast<uint64_t>(entry.size));
|
||||
}
|
||||
}
|
||||
for (DVDFileEntry& entry : g_fileEntries) {
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
#include <cstdlib>
|
||||
|
||||
#include <cpuid.h>
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -137,7 +141,12 @@ bool CollectMissingBaselineFeatures(TextBuffer& missing) {
|
||||
// up yet, and fprintf(stderr, ...) faults there. Verified on this toolchain:
|
||||
// WriteFile on the raw standard-error handle and MessageBoxA both work, printf
|
||||
// does not. Anything added to this reporting path has to respect that.
|
||||
//
|
||||
// The POSIX path runs from an __attribute__((constructor)) instead, ahead of libc's own startup
|
||||
// guarantees; ::write() on the raw fd is the same kind of allocation-free, libc-init-independent
|
||||
// primitive as WriteFile is on Windows, so the same restriction is honored here.
|
||||
void WriteStdErrEarly(const char* text) {
|
||||
#if defined(_WIN32)
|
||||
const HANDLE handle = ::GetStdHandle(STD_ERROR_HANDLE);
|
||||
if (handle == nullptr || handle == INVALID_HANDLE_VALUE) {
|
||||
return;
|
||||
@@ -148,6 +157,13 @@ void WriteStdErrEarly(const char* text) {
|
||||
}
|
||||
DWORD written = 0;
|
||||
::WriteFile(handle, text, static_cast<DWORD>(length), &written, nullptr);
|
||||
#else
|
||||
size_t length = 0;
|
||||
while (text[length] != '\0') {
|
||||
++length;
|
||||
}
|
||||
(void)::write(STDERR_FILENO, text, length);
|
||||
#endif
|
||||
}
|
||||
|
||||
[[noreturn]] void ReportUnsupportedCpu(const char* missing) {
|
||||
@@ -168,12 +184,18 @@ void WriteStdErrEarly(const char* text) {
|
||||
WriteStdErrEarly(message.data);
|
||||
WriteStdErrEarly("\n");
|
||||
|
||||
#if defined(_WIN32)
|
||||
::MessageBoxA(nullptr, message.data, "WiiCompiled - Unsupported Processor",
|
||||
MB_OK | MB_ICONERROR | MB_SETFOREGROUND | MB_TASKMODAL);
|
||||
// Leave through the OS rather than exit(): the C++ dynamic initializers
|
||||
// have not run yet, so there is no constructed program state to unwind and
|
||||
// the teardown path itself lives in AVX2 translation units.
|
||||
::ExitProcess(1u);
|
||||
#else
|
||||
// Same reasoning as the Windows path above: no C++ dynamic initializer has run yet, so
|
||||
// _exit() (skips atexit/global destructors, unlike exit()) is the correct way out.
|
||||
::_exit(1);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
+114
-1
@@ -37,6 +37,8 @@
|
||||
#include <mmsystem.h>
|
||||
#include <dbghelp.h>
|
||||
#else
|
||||
#include <signal.h>
|
||||
#include <ucontext.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
@@ -988,7 +990,12 @@ LONG CALLBACK SehLogger(EXCEPTION_POINTERS* info) {
|
||||
// flat module registers its own handler first, but registration order is
|
||||
// not guaranteed once another VEH is installed later, so consult it here
|
||||
// too - resolving a fault twice is a no-op.
|
||||
if (GuestFlat::HandleAccessViolation(info)) {
|
||||
if (info->ExceptionRecord != nullptr &&
|
||||
info->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION &&
|
||||
info->ExceptionRecord->NumberParameters >= 2 &&
|
||||
GuestFlat::HandleAccessViolation(
|
||||
reinterpret_cast<void*>(info->ExceptionRecord->ExceptionInformation[1]),
|
||||
info->ExceptionRecord->ExceptionInformation[0] != 0)) {
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
if (g_suppressSehReporting && g_sehJumpTarget) {
|
||||
@@ -1059,6 +1066,102 @@ void InstallSehLogger() {
|
||||
g_vectoredSehHandle = AddVectoredExceptionHandler(1, SehLogger);
|
||||
}
|
||||
}
|
||||
#else
|
||||
// POSIX counterpart to SehLogger above. Unlike Windows' AddVectoredExceptionHandler, which lets
|
||||
// GuestFlat and this module each install their own handler and defensively re-check each other,
|
||||
// sigaction only allows one handler per signal - the second registration replaces the first
|
||||
// instead of chaining. So this is the single SIGSEGV/SIGBUS handler for the whole process, and it
|
||||
// owns checking GuestFlat's fault-interception logic first, exactly mirroring the order SehLogger
|
||||
// already uses on Windows.
|
||||
void ReportUnhandledSignalFault(int sig, void* faultAddress) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "Signal " << sig << " (fault address 0x" << std::hex
|
||||
<< reinterpret_cast<uintptr_t>(faultAddress) << std::dec << ")";
|
||||
if (!g_lastEntryLabel.empty()) {
|
||||
std::cerr << " while executing " << g_lastEntryLabel;
|
||||
}
|
||||
std::cerr << std::endl;
|
||||
if (const CpuContext* cpu = TryGetCpuContext()) {
|
||||
RT_LOG(RT_TAG_RUNTIME) << "===== DUMPING CPU STATE =====" << std::endl;
|
||||
SystemBridge::DumpCpuState(cpu);
|
||||
}
|
||||
std::cerr.flush();
|
||||
}
|
||||
|
||||
void PosixMemoryFaultHandler(int sig, siginfo_t* info, void* ucontextVoid) {
|
||||
void* faultAddress = info != nullptr ? info->si_addr : nullptr;
|
||||
bool isWrite = false;
|
||||
#if defined(__x86_64__)
|
||||
// Standard glibc technique for a POSIX fastmem-style handler: bit 1 (0x2) of the hardware
|
||||
// error code x86 pushes on a page fault records whether it was a write.
|
||||
if (ucontextVoid != nullptr) {
|
||||
auto* uc = static_cast<ucontext_t*>(ucontextVoid);
|
||||
isWrite = (uc->uc_mcontext.gregs[REG_ERR] & 0x2) != 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Guest-space faults are the flat memory interception mechanism (MMIO, deferred EFB reads,
|
||||
// the executable-write guard, unmapped pages). Resolving one here means resuming the
|
||||
// faulting instruction, which just returning from the handler does.
|
||||
if (faultAddress != nullptr && GuestFlat::HandleAccessViolation(faultAddress, isWrite)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_suppressSehReporting && g_sehJumpTarget) {
|
||||
g_sehLastExceptionCode = static_cast<uint32_t>(sig);
|
||||
g_sehLastExceptionAddress = reinterpret_cast<uintptr_t>(faultAddress);
|
||||
g_sehLastAccessType = isWrite ? 1u : 0u;
|
||||
g_sehLastAccessedAddress = reinterpret_cast<uintptr_t>(faultAddress);
|
||||
siglongjmp(*g_sehJumpTarget, 1);
|
||||
}
|
||||
if (g_suppressSehReporting) {
|
||||
// Reporting suppressed but nobody armed a recovery jump: restore the default disposition
|
||||
// and re-raise so the process still terminates, instead of returning into the same fault.
|
||||
signal(sig, SIG_DFL);
|
||||
raise(sig);
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard against re-entrancy: if we crash while reporting, don't recurse.
|
||||
static std::atomic_flag s_inCrashHandler = ATOMIC_FLAG_INIT;
|
||||
if (s_inCrashHandler.test_and_set()) {
|
||||
std::_Exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
ReportUnhandledSignalFault(sig, faultAddress);
|
||||
std::ostringstream popupDetails;
|
||||
popupDetails << "A native signal (" << sig << ") occurred";
|
||||
if (!g_lastEntryLabel.empty()) {
|
||||
popupDetails << " while executing " << g_lastEntryLabel;
|
||||
}
|
||||
if (faultAddress != nullptr) {
|
||||
popupDetails << ".\n\nThe game attempted a " << (isWrite ? "write" : "read")
|
||||
<< " at host address 0x" << std::hex
|
||||
<< reinterpret_cast<uintptr_t>(faultAddress) << std::dec;
|
||||
}
|
||||
popupDetails << ".\n\nThe process transcript and crash log contain the full CPU and stack "
|
||||
"diagnostics.";
|
||||
ShowRuntimeFatalPopup("a native crash occurred", popupDetails.str());
|
||||
DumpHostStackTrace();
|
||||
WriteFatalLogImpl(sig == SIGBUS ? "sigbus" : "sigsegv");
|
||||
|
||||
std::cerr.flush();
|
||||
std::cout.flush();
|
||||
std::fflush(stdout);
|
||||
std::fflush(stderr);
|
||||
std::_Exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
void InstallPosixMemoryFaultHandler() {
|
||||
struct sigaction action {};
|
||||
action.sa_sigaction = PosixMemoryFaultHandler;
|
||||
action.sa_flags = SA_SIGINFO;
|
||||
sigemptyset(&action.sa_mask);
|
||||
sigaction(SIGSEGV, &action, nullptr);
|
||||
// A touch beyond a memfd-backed mapping's ftruncate()'d size raises SIGBUS rather than
|
||||
// SIGSEGV on Linux; region sizing should make this unreachable, but routing it to the same
|
||||
// handler costs nothing and avoids a silent gap if it ever isn't.
|
||||
sigaction(SIGBUS, &action, nullptr);
|
||||
}
|
||||
#endif
|
||||
|
||||
void AbortSignalHandler(int signum) {
|
||||
@@ -1174,6 +1277,8 @@ int RuntimeMain(int argc, char** argv) {
|
||||
ConfigureWindowsFatalDialogBehavior();
|
||||
InstallSehLogger();
|
||||
WindowsTimerResolutionGuard timerResolutionGuard;
|
||||
#else
|
||||
InstallPosixMemoryFaultHandler();
|
||||
#endif
|
||||
InitializeProcessTranscript(argc, argv);
|
||||
std::signal(SIGABRT, AbortSignalHandler);
|
||||
@@ -1272,7 +1377,9 @@ int RuntimeMain(int argc, char** argv) {
|
||||
}
|
||||
aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait);
|
||||
GxGuestWrite::InstallAuroraHooks();
|
||||
#if defined(_WIN32)
|
||||
Wup028Adapter::Initialize();
|
||||
#endif
|
||||
UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width,
|
||||
auroraInfo.windowSize.native_fb_height);
|
||||
settings_overlay::InitializeRuntimeSettings();
|
||||
@@ -1314,7 +1421,9 @@ int RuntimeMain(int argc, char** argv) {
|
||||
// Shutdown fiber system
|
||||
Fiber::GuestFiberManager::Shutdown();
|
||||
WindowPlacementPersistence::Flush(true);
|
||||
#if defined(_WIN32)
|
||||
Wup028Adapter::Shutdown();
|
||||
#endif
|
||||
aurora_shutdown();
|
||||
SetRuntimeExitCodeImpl(0);
|
||||
ShutdownProcessTranscript();
|
||||
@@ -1332,7 +1441,9 @@ int RuntimeMain(int argc, char** argv) {
|
||||
SetRuntimeExitCodeImpl(1);
|
||||
Fiber::GuestFiberManager::Shutdown();
|
||||
WindowPlacementPersistence::Flush(true);
|
||||
#if defined(_WIN32)
|
||||
Wup028Adapter::Shutdown();
|
||||
#endif
|
||||
aurora_shutdown();
|
||||
ShutdownProcessTranscript();
|
||||
return 1;
|
||||
@@ -1344,7 +1455,9 @@ int RuntimeMain(int argc, char** argv) {
|
||||
SetRuntimeExitCodeImpl(1);
|
||||
Fiber::GuestFiberManager::Shutdown();
|
||||
WindowPlacementPersistence::Flush(true);
|
||||
#if defined(_WIN32)
|
||||
Wup028Adapter::Shutdown();
|
||||
#endif
|
||||
aurora_shutdown();
|
||||
ShutdownProcessTranscript();
|
||||
return 1;
|
||||
|
||||
@@ -20,11 +20,13 @@
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <dbghelp.h>
|
||||
#endif
|
||||
|
||||
MemoryInline::PageEntry MemoryInline::g_pageTable[MemoryInline::kPageCount]{};
|
||||
uintptr_t MemoryInline::g_fullPageBias[MemoryInline::kPageCount]{};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "settings_overlay.h"
|
||||
#ifdef _WIN32
|
||||
#include "wup028_adapter.h"
|
||||
#endif
|
||||
#include "audio_backend.h"
|
||||
#include "controller_mapping_wizard.h"
|
||||
#include "game_graphics_options.h"
|
||||
@@ -313,6 +315,12 @@ void ApplyConfiguredMappings() {
|
||||
}
|
||||
|
||||
void DrawGameCubeAdapterInfo() {
|
||||
// The official GameCube adapter is Windows-only (see wup028_adapter.cpp); on Linux/macOS,
|
||||
// Wup028Adapter is a permanently-disconnected stub, and SDL3 already exposes the same
|
||||
// hardware as a normal joystick, so this menu would only ever show "Searching" and four
|
||||
// perpetually-empty adapter ports - confusing clutter for a feature that can't do anything
|
||||
// on this platform. Skip it entirely rather than render a menu that never has content.
|
||||
#if defined(_WIN32)
|
||||
ImGui::Separator();
|
||||
if (!ImGui::BeginMenu("GameCube adapter info")) return;
|
||||
|
||||
@@ -338,6 +346,7 @@ void DrawGameCubeAdapterInfo() {
|
||||
}
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
#endif
|
||||
}
|
||||
|
||||
void DrawControllerSettings() {
|
||||
@@ -351,13 +360,20 @@ void DrawControllerSettings() {
|
||||
|
||||
ImGui::Separator();
|
||||
const uint32_t selectedGamePort = static_cast<uint32_t>(g_controllerPort);
|
||||
#if defined(_WIN32)
|
||||
const int adapterAssignment = Wup028Adapter::GetPortAssignment(selectedGamePort);
|
||||
if (adapterAssignment >= 0) {
|
||||
ImGui::Text("Assigned: GameCube adapter port %d", adapterAssignment + 1);
|
||||
} else {
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
const char* currentName = PADGetName(selectedGamePort);
|
||||
ImGui::Text("Assigned: %s", currentName != nullptr ? currentName : "None");
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
// Windows-only, same reasoning as DrawGameCubeAdapterInfo() above: on other platforms
|
||||
// adapterAssignment is always -1 and every port would always read "(empty)", so this submenu
|
||||
// would never have anything real to offer.
|
||||
if (ImGui::BeginMenu("Assign GameCube adapter port")) {
|
||||
if (ImGui::MenuItem("None", nullptr, adapterAssignment < 0)) {
|
||||
Wup028Adapter::SetPortAssignment(selectedGamePort, -1);
|
||||
@@ -381,10 +397,13 @@ void DrawControllerSettings() {
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
#endif
|
||||
if (ImGui::MenuItem("Unassign controller")) {
|
||||
PADClearPort(selectedGamePort);
|
||||
#if defined(_WIN32)
|
||||
Wup028Adapter::SetPortAssignment(selectedGamePort, -1);
|
||||
RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, -1);
|
||||
#endif
|
||||
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
|
||||
}
|
||||
ImGui::Separator();
|
||||
@@ -392,7 +411,9 @@ void DrawControllerSettings() {
|
||||
const uint32_t controllerCount = PADCount();
|
||||
if (controllerCount == 0) {
|
||||
ImGui::TextDisabled("No controller connected");
|
||||
#if defined(_WIN32)
|
||||
DrawGameCubeAdapterInfo();
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -401,8 +422,10 @@ void DrawControllerSettings() {
|
||||
const char* name = PADGetNameForControllerIndex(index);
|
||||
ImGui::PushID(static_cast<int>(index));
|
||||
if (ImGui::MenuItem(name != nullptr ? name : "Unknown controller")) {
|
||||
#if defined(_WIN32)
|
||||
Wup028Adapter::SetPortAssignment(selectedGamePort, -1);
|
||||
RuntimeConfigFile::SetGameCubeAdapterPort(selectedGamePort, -1);
|
||||
#endif
|
||||
PADSetPortForIndex(index, selectedGamePort);
|
||||
g_configuredControllerIndices.fill(std::numeric_limits<int32_t>::min());
|
||||
ApplyConfiguredMappings();
|
||||
@@ -416,7 +439,9 @@ void DrawControllerSettings() {
|
||||
PADButtonMapping* mappings = PADGetButtonMappings(static_cast<uint32_t>(g_controllerPort), &mappingCount);
|
||||
if (mappings == nullptr || mappingCount != PAD_BUTTON_COUNT) {
|
||||
ImGui::TextDisabled("Assign a controller to edit its buttons");
|
||||
#if defined(_WIN32)
|
||||
DrawGameCubeAdapterInfo();
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -556,7 +581,9 @@ void DrawControllerSettings() {
|
||||
ImGui::TextUnformatted(kControllerButtons[i].label);
|
||||
ImGui::PopID();
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
DrawGameCubeAdapterInfo();
|
||||
#endif
|
||||
}
|
||||
|
||||
void DrawAudioSettings() {
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
|
||||
#include "abi_bridge.h"
|
||||
@@ -26,7 +28,7 @@
|
||||
|
||||
// Global flag to suppress SEH reporting during static constructor execution
|
||||
bool g_suppressSehReporting = false;
|
||||
thread_local jmp_buf* g_sehJumpTarget = nullptr;
|
||||
thread_local MkwJmpBuf* g_sehJumpTarget = nullptr;
|
||||
thread_local uint32_t g_sehLastExceptionCode = 0;
|
||||
thread_local uintptr_t g_sehLastExceptionAddress = 0;
|
||||
thread_local uintptr_t g_sehLastAccessedAddress = 0;
|
||||
@@ -347,9 +349,9 @@ void SystemBridge::Initialize() {
|
||||
if (funcAddr == 0 || funcAddr == 0xFFFFFFFF) continue;
|
||||
|
||||
if (TranslatedFunctionRegistry::FindByAddressPtr(funcAddr)) {
|
||||
jmp_buf jumpBuf;
|
||||
MkwJmpBuf jumpBuf;
|
||||
g_sehJumpTarget = &jumpBuf;
|
||||
if (setjmp(jumpBuf) == 0) {
|
||||
if (MKW_SETJMP(jumpBuf) == 0) {
|
||||
cpu.gpr[1] = 0x81700000u;
|
||||
InvokeIndirectCpu(funcAddr, &cpu);
|
||||
dolCount++;
|
||||
@@ -387,9 +389,9 @@ void SystemBridge::Initialize() {
|
||||
if (funcAddr == 0 || funcAddr == 0xFFFFFFFF) continue;
|
||||
|
||||
if (TranslatedFunctionRegistry::FindByAddressPtr(funcAddr)) {
|
||||
jmp_buf jumpBuf;
|
||||
MkwJmpBuf jumpBuf;
|
||||
g_sehJumpTarget = &jumpBuf;
|
||||
if (setjmp(jumpBuf) == 0) {
|
||||
if (MKW_SETJMP(jumpBuf) == 0) {
|
||||
cpu.gpr[1] = 0x81700000u;
|
||||
InvokeIndirectCpu(funcAddr, &cpu);
|
||||
count++;
|
||||
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
ISC License (ISC)
|
||||
|
||||
Copyright byuu and the higan team
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
The above applies to all files in this project except valgrind.h which is licensed under a BSD-style license. See the license text and copyright notice contained within that file.
|
||||
Vendored
+112
@@ -0,0 +1,112 @@
|
||||
#define LIBCO_C
|
||||
#include "libco.h"
|
||||
#include "settings.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#ifdef LIBCO_MPROTECT
|
||||
#include <unistd.h>
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
|
||||
#include "valgrind.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
static thread_local unsigned long co_active_buffer[64];
|
||||
static thread_local cothread_t co_active_handle = 0;
|
||||
static void (*co_swap)(cothread_t, cothread_t) = 0;
|
||||
|
||||
#ifdef LIBCO_MPROTECT
|
||||
alignas(4096)
|
||||
#else
|
||||
section(text)
|
||||
#endif
|
||||
static const uint32_t co_swap_function[1024] = {
|
||||
0x910003f0, /* mov x16,sp */
|
||||
0xa9007830, /* stp x16,x30,[x1] */
|
||||
0xa9407810, /* ldp x16,x30,[x0] */
|
||||
0x9100021f, /* mov sp,x16 */
|
||||
0xa9015033, /* stp x19,x20,[x1, 16] */
|
||||
0xa9415013, /* ldp x19,x20,[x0, 16] */
|
||||
0xa9025835, /* stp x21,x22,[x1, 32] */
|
||||
0xa9425815, /* ldp x21,x22,[x0, 32] */
|
||||
0xa9036037, /* stp x23,x24,[x1, 48] */
|
||||
0xa9436017, /* ldp x23,x24,[x0, 48] */
|
||||
0xa9046839, /* stp x25,x26,[x1, 64] */
|
||||
0xa9446819, /* ldp x25,x26,[x0, 64] */
|
||||
0xa905703b, /* stp x27,x28,[x1, 80] */
|
||||
0xa945701b, /* ldp x27,x28,[x0, 80] */
|
||||
0xf900303d, /* str x29, [x1, 96] */
|
||||
0xf940301d, /* ldr x29, [x0, 96] */
|
||||
0x6d072428, /* stp d8, d9, [x1,112] */
|
||||
0x6d472408, /* ldp d8, d9, [x0,112] */
|
||||
0x6d082c2a, /* stp d10,d11,[x1,128] */
|
||||
0x6d482c0a, /* ldp d10,d11,[x0,128] */
|
||||
0x6d09342c, /* stp d12,d13,[x1,144] */
|
||||
0x6d49340c, /* ldp d12,d13,[x0,144] */
|
||||
0x6d0a3c2e, /* stp d14,d15,[x1,160] */
|
||||
0x6d4a3c0e, /* ldp d14,d15,[x0,160] */
|
||||
0xd61f03c0, /* br x30 */
|
||||
};
|
||||
|
||||
static void co_init(void) {
|
||||
#ifdef LIBCO_MPROTECT
|
||||
unsigned long addr = (unsigned long)co_swap_function;
|
||||
unsigned long base = addr - (addr % sysconf(_SC_PAGESIZE));
|
||||
unsigned long size = (addr - base) + sizeof co_swap_function;
|
||||
mprotect((void*)base, size, PROT_READ | PROT_EXEC);
|
||||
#endif
|
||||
}
|
||||
|
||||
cothread_t co_active(void) {
|
||||
if(!co_active_handle) co_active_handle = &co_active_buffer;
|
||||
return co_active_handle;
|
||||
}
|
||||
|
||||
cothread_t co_derive(void* memory, unsigned int size, void (*entrypoint)(void)) {
|
||||
unsigned long* handle;
|
||||
if(!co_swap) {
|
||||
co_init();
|
||||
co_swap = (void (*)(cothread_t, cothread_t))co_swap_function;
|
||||
}
|
||||
if(!co_active_handle) co_active_handle = &co_active_buffer;
|
||||
|
||||
VALGRIND_STACK_REGISTER(memory, memory + size);
|
||||
|
||||
if((handle = (unsigned long*)memory)) {
|
||||
unsigned long stack_top = (unsigned long)handle + size;
|
||||
unsigned long *p;
|
||||
stack_top &= ~((unsigned long) 15);
|
||||
p = (unsigned long*)(stack_top);
|
||||
handle[0] = (unsigned long)p; /* x16 (stack pointer) */
|
||||
handle[1] = (unsigned long)entrypoint; /* x30 (link register) */
|
||||
handle[12] = (unsigned long)p; /* x29 (frame pointer) */
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
cothread_t co_create(unsigned int size, void (*entrypoint)(void)) {
|
||||
void* memory = LIBCO_MALLOC(size);
|
||||
if(!memory) return (cothread_t)0;
|
||||
return co_derive(memory, size, entrypoint);
|
||||
}
|
||||
|
||||
void co_delete(cothread_t handle) {
|
||||
LIBCO_FREE(handle);
|
||||
}
|
||||
|
||||
void co_switch(cothread_t handle) {
|
||||
cothread_t co_previous_handle = co_active_handle;
|
||||
co_swap(co_active_handle = handle, co_previous_handle);
|
||||
}
|
||||
|
||||
int co_serializable(void) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Vendored
+175
@@ -0,0 +1,175 @@
|
||||
#define LIBCO_C
|
||||
#include "libco.h"
|
||||
#include "settings.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
static thread_local long long co_active_buffer[64];
|
||||
static thread_local cothread_t co_active_handle = 0;
|
||||
static void (*co_swap)(cothread_t, cothread_t) = 0;
|
||||
|
||||
#ifdef LIBCO_MPROTECT
|
||||
alignas(4096)
|
||||
#else
|
||||
section(text)
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
/* ABI: Win64 */
|
||||
static const unsigned char co_swap_function[4096] = {
|
||||
0x48, 0x89, 0x22, /* mov [rdx],rsp */
|
||||
0x48, 0x8b, 0x21, /* mov rsp,[rcx] */
|
||||
0x58, /* pop rax */
|
||||
0x48, 0x89, 0x6a, 0x08, /* mov [rdx+ 8],rbp */
|
||||
0x48, 0x89, 0x72, 0x10, /* mov [rdx+16],rsi */
|
||||
0x48, 0x89, 0x7a, 0x18, /* mov [rdx+24],rdi */
|
||||
0x48, 0x89, 0x5a, 0x20, /* mov [rdx+32],rbx */
|
||||
0x4c, 0x89, 0x62, 0x28, /* mov [rdx+40],r12 */
|
||||
0x4c, 0x89, 0x6a, 0x30, /* mov [rdx+48],r13 */
|
||||
0x4c, 0x89, 0x72, 0x38, /* mov [rdx+56],r14 */
|
||||
0x4c, 0x89, 0x7a, 0x40, /* mov [rdx+64],r15 */
|
||||
#if !defined(LIBCO_NO_SSE)
|
||||
0x0f, 0x29, 0x72, 0x50, /* movaps [rdx+ 80],xmm6 */
|
||||
0x0f, 0x29, 0x7a, 0x60, /* movaps [rdx+ 96],xmm7 */
|
||||
0x44, 0x0f, 0x29, 0x42, 0x70, /* movaps [rdx+112],xmm8 */
|
||||
0x48, 0x83, 0xc2, 0x70, /* add rdx,112 */
|
||||
0x44, 0x0f, 0x29, 0x4a, 0x10, /* movaps [rdx+ 16],xmm9 */
|
||||
0x44, 0x0f, 0x29, 0x52, 0x20, /* movaps [rdx+ 32],xmm10 */
|
||||
0x44, 0x0f, 0x29, 0x5a, 0x30, /* movaps [rdx+ 48],xmm11 */
|
||||
0x44, 0x0f, 0x29, 0x62, 0x40, /* movaps [rdx+ 64],xmm12 */
|
||||
0x44, 0x0f, 0x29, 0x6a, 0x50, /* movaps [rdx+ 80],xmm13 */
|
||||
0x44, 0x0f, 0x29, 0x72, 0x60, /* movaps [rdx+ 96],xmm14 */
|
||||
0x44, 0x0f, 0x29, 0x7a, 0x70, /* movaps [rdx+112],xmm15 */
|
||||
#endif
|
||||
0x48, 0x8b, 0x69, 0x08, /* mov rbp,[rcx+ 8] */
|
||||
0x48, 0x8b, 0x71, 0x10, /* mov rsi,[rcx+16] */
|
||||
0x48, 0x8b, 0x79, 0x18, /* mov rdi,[rcx+24] */
|
||||
0x48, 0x8b, 0x59, 0x20, /* mov rbx,[rcx+32] */
|
||||
0x4c, 0x8b, 0x61, 0x28, /* mov r12,[rcx+40] */
|
||||
0x4c, 0x8b, 0x69, 0x30, /* mov r13,[rcx+48] */
|
||||
0x4c, 0x8b, 0x71, 0x38, /* mov r14,[rcx+56] */
|
||||
0x4c, 0x8b, 0x79, 0x40, /* mov r15,[rcx+64] */
|
||||
#if !defined(LIBCO_NO_SSE)
|
||||
0x0f, 0x28, 0x71, 0x50, /* movaps xmm6, [rcx+ 80] */
|
||||
0x0f, 0x28, 0x79, 0x60, /* movaps xmm7, [rcx+ 96] */
|
||||
0x44, 0x0f, 0x28, 0x41, 0x70, /* movaps xmm8, [rcx+112] */
|
||||
0x48, 0x83, 0xc1, 0x70, /* add rcx,112 */
|
||||
0x44, 0x0f, 0x28, 0x49, 0x10, /* movaps xmm9, [rcx+ 16] */
|
||||
0x44, 0x0f, 0x28, 0x51, 0x20, /* movaps xmm10,[rcx+ 32] */
|
||||
0x44, 0x0f, 0x28, 0x59, 0x30, /* movaps xmm11,[rcx+ 48] */
|
||||
0x44, 0x0f, 0x28, 0x61, 0x40, /* movaps xmm12,[rcx+ 64] */
|
||||
0x44, 0x0f, 0x28, 0x69, 0x50, /* movaps xmm13,[rcx+ 80] */
|
||||
0x44, 0x0f, 0x28, 0x71, 0x60, /* movaps xmm14,[rcx+ 96] */
|
||||
0x44, 0x0f, 0x28, 0x79, 0x70, /* movaps xmm15,[rcx+112] */
|
||||
#endif
|
||||
0xff, 0xe0, /* jmp rax */
|
||||
};
|
||||
|
||||
/* Valgrind is available on MINGW but not on MSVC. */
|
||||
#if defined(__GNUC__)
|
||||
#include "valgrind.h"
|
||||
#endif
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
static void co_init(void) {
|
||||
#ifdef LIBCO_MPROTECT
|
||||
DWORD old_privileges;
|
||||
VirtualProtect((void*)co_swap_function, sizeof co_swap_function, PAGE_EXECUTE_READ, &old_privileges);
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
/* ABI: SystemV */
|
||||
static const unsigned char co_swap_function[4096] = {
|
||||
0x48, 0x89, 0x26, /* mov [rsi],rsp */
|
||||
0x48, 0x8b, 0x27, /* mov rsp,[rdi] */
|
||||
0x58, /* pop rax */
|
||||
0x48, 0x89, 0x6e, 0x08, /* mov [rsi+ 8],rbp */
|
||||
0x48, 0x89, 0x5e, 0x10, /* mov [rsi+16],rbx */
|
||||
0x4c, 0x89, 0x66, 0x18, /* mov [rsi+24],r12 */
|
||||
0x4c, 0x89, 0x6e, 0x20, /* mov [rsi+32],r13 */
|
||||
0x4c, 0x89, 0x76, 0x28, /* mov [rsi+40],r14 */
|
||||
0x4c, 0x89, 0x7e, 0x30, /* mov [rsi+48],r15 */
|
||||
0x48, 0x8b, 0x6f, 0x08, /* mov rbp,[rdi+ 8] */
|
||||
0x48, 0x8b, 0x5f, 0x10, /* mov rbx,[rdi+16] */
|
||||
0x4c, 0x8b, 0x67, 0x18, /* mov r12,[rdi+24] */
|
||||
0x4c, 0x8b, 0x6f, 0x20, /* mov r13,[rdi+32] */
|
||||
0x4c, 0x8b, 0x77, 0x28, /* mov r14,[rdi+40] */
|
||||
0x4c, 0x8b, 0x7f, 0x30, /* mov r15,[rdi+48] */
|
||||
0xff, 0xe0, /* jmp rax */
|
||||
};
|
||||
|
||||
#include "valgrind.h"
|
||||
#ifdef LIBCO_MPROTECT
|
||||
#include <unistd.h>
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
|
||||
static void co_init(void) {
|
||||
#ifdef LIBCO_MPROTECT
|
||||
unsigned long long addr = (unsigned long long)co_swap_function;
|
||||
unsigned long long base = addr - (addr % sysconf(_SC_PAGESIZE));
|
||||
unsigned long long size = (addr - base) + sizeof co_swap_function;
|
||||
mprotect((void*)base, size, PROT_READ | PROT_EXEC);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
static void crash(void) {
|
||||
LIBCO_ASSERT(0); /* called only if cothread_t entrypoint returns */
|
||||
}
|
||||
|
||||
cothread_t co_active(void) {
|
||||
if(!co_active_handle) co_active_handle = &co_active_buffer;
|
||||
return co_active_handle;
|
||||
}
|
||||
|
||||
cothread_t co_derive(void* memory, unsigned int size, void (*entrypoint)(void)) {
|
||||
cothread_t handle;
|
||||
if(!co_swap) {
|
||||
co_init();
|
||||
co_swap = (void (*)(cothread_t, cothread_t))co_swap_function;
|
||||
}
|
||||
if(!co_active_handle) co_active_handle = &co_active_buffer;
|
||||
|
||||
#if defined(__VALGRIND_MAJOR__)
|
||||
VALGRIND_STACK_REGISTER(memory, memory + size);
|
||||
#endif
|
||||
|
||||
if((handle = (cothread_t)memory)) {
|
||||
unsigned long long stack_top = (unsigned long long)handle + size;
|
||||
long long *p;
|
||||
stack_top -= 32;
|
||||
stack_top &= ~((unsigned long long) 15);
|
||||
p = (long long*)(stack_top); /* seek to top of stack */
|
||||
*--p = (long long)crash; /* crash if entrypoint returns */
|
||||
*--p = (long long)entrypoint; /* start of function */
|
||||
*(long long*)handle = (long long)p; /* stack pointer */
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
cothread_t co_create(unsigned int size, void (*entrypoint)(void)) {
|
||||
void* memory = LIBCO_MALLOC(size);
|
||||
if(!memory) return (cothread_t)0;
|
||||
return co_derive(memory, size, entrypoint);
|
||||
}
|
||||
|
||||
void co_delete(cothread_t handle) {
|
||||
LIBCO_FREE(handle);
|
||||
}
|
||||
|
||||
void co_switch(cothread_t handle) {
|
||||
register cothread_t co_previous_handle = co_active_handle;
|
||||
co_swap(co_active_handle = handle, co_previous_handle);
|
||||
}
|
||||
|
||||
int co_serializable(void) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Vendored
+87
@@ -0,0 +1,87 @@
|
||||
#define LIBCO_C
|
||||
#include "libco.h"
|
||||
#include "settings.h"
|
||||
#include "valgrind.h"
|
||||
|
||||
#ifdef LIBCO_MPROTECT
|
||||
#include <unistd.h>
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
static thread_local unsigned long co_active_buffer[64];
|
||||
static thread_local cothread_t co_active_handle = 0;
|
||||
static void (*co_swap)(cothread_t, cothread_t) = 0;
|
||||
|
||||
#ifdef LIBCO_MPROTECT
|
||||
alignas(4096)
|
||||
#else
|
||||
section(text)
|
||||
#endif
|
||||
static const unsigned long co_swap_function[1024] = {
|
||||
0xe8a16ff0, /* stmia r1!, {r4-r11,sp,lr} */
|
||||
0xe8b0aff0, /* ldmia r0!, {r4-r11,sp,pc} */
|
||||
0xe12fff1e, /* bx lr */
|
||||
};
|
||||
|
||||
static void co_init(void) {
|
||||
#ifdef LIBCO_MPROTECT
|
||||
unsigned long addr = (unsigned long)co_swap_function;
|
||||
unsigned long base = addr - (addr % sysconf(_SC_PAGESIZE));
|
||||
unsigned long size = (addr - base) + sizeof co_swap_function;
|
||||
mprotect((void*)base, size, PROT_READ | PROT_EXEC);
|
||||
#endif
|
||||
}
|
||||
|
||||
cothread_t co_active(void) {
|
||||
if(!co_active_handle) co_active_handle = &co_active_buffer;
|
||||
return co_active_handle;
|
||||
}
|
||||
|
||||
cothread_t co_derive(void* memory, unsigned int size, void (*entrypoint)(void)) {
|
||||
unsigned long* handle;
|
||||
if(!co_swap) {
|
||||
co_init();
|
||||
co_swap = (void (*)(cothread_t, cothread_t))co_swap_function;
|
||||
}
|
||||
if(!co_active_handle) co_active_handle = &co_active_buffer;
|
||||
|
||||
VALGRIND_STACK_REGISTER(memory, memory + size);
|
||||
|
||||
if((handle = (unsigned long*)memory)) {
|
||||
unsigned long stack_top = (unsigned long)handle + size;
|
||||
unsigned long *p;
|
||||
stack_top &= ~((unsigned long) 15);
|
||||
p = (unsigned long*)(stack_top);
|
||||
handle[8] = (unsigned long)p;
|
||||
handle[9] = (unsigned long)entrypoint;
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
cothread_t co_create(unsigned int size, void (*entrypoint)(void)) {
|
||||
void* memory = LIBCO_MALLOC(size);
|
||||
if(!memory) return (cothread_t)0;
|
||||
return co_derive(memory, size, entrypoint);
|
||||
}
|
||||
|
||||
void co_delete(cothread_t handle) {
|
||||
LIBCO_FREE(handle);
|
||||
}
|
||||
|
||||
void co_switch(cothread_t handle) {
|
||||
cothread_t co_previous_handle = co_active_handle;
|
||||
co_swap(co_active_handle = handle, co_previous_handle);
|
||||
}
|
||||
|
||||
int co_serializable(void) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Vendored
+55
@@ -0,0 +1,55 @@
|
||||
#define LIBCO_C
|
||||
#include "libco.h"
|
||||
#include "settings.h"
|
||||
|
||||
#define WINVER 0x0400
|
||||
#define _WIN32_WINNT 0x0400
|
||||
#include <windows.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
static thread_local cothread_t co_active_ = 0;
|
||||
|
||||
static void __stdcall co_thunk(void* coentry) {
|
||||
((void (*)(void))coentry)();
|
||||
}
|
||||
|
||||
cothread_t co_active(void) {
|
||||
if(!co_active_) {
|
||||
ConvertThreadToFiber(0);
|
||||
co_active_ = GetCurrentFiber();
|
||||
}
|
||||
return co_active_;
|
||||
}
|
||||
|
||||
cothread_t co_derive(void* memory, unsigned int heapsize, void (*coentry)(void)) {
|
||||
/* Windows fibers do not allow users to supply their own memory */
|
||||
return (cothread_t)0;
|
||||
}
|
||||
|
||||
cothread_t co_create(unsigned int heapsize, void (*coentry)(void)) {
|
||||
if(!co_active_) {
|
||||
ConvertThreadToFiber(0);
|
||||
co_active_ = GetCurrentFiber();
|
||||
}
|
||||
return (cothread_t)CreateFiber(heapsize, co_thunk, (void*)coentry);
|
||||
}
|
||||
|
||||
void co_delete(cothread_t cothread) {
|
||||
DeleteFiber(cothread);
|
||||
}
|
||||
|
||||
void co_switch(cothread_t cothread) {
|
||||
co_active_ = cothread;
|
||||
SwitchToFiber(cothread);
|
||||
}
|
||||
|
||||
int co_serializable(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic ignored "-Wparentheses"
|
||||
|
||||
/* placing code in section(text) does not mark it executable with Clang. */
|
||||
#undef LIBCO_MPROTECT
|
||||
#define LIBCO_MPROTECT
|
||||
#endif
|
||||
|
||||
#if defined(__clang__) || defined(__GNUC__)
|
||||
#if defined(__i386__)
|
||||
#include "x86.c"
|
||||
#elif defined(__amd64__)
|
||||
#include "amd64.c"
|
||||
#elif defined(__arm__)
|
||||
#include "arm.c"
|
||||
#elif defined(__aarch64__)
|
||||
#include "aarch64.c"
|
||||
#elif defined(__powerpc64__) && defined(_CALL_ELF) && _CALL_ELF == 2
|
||||
#include "ppc64v2.c"
|
||||
#elif defined(_ARCH_PPC) && !defined(__LITTLE_ENDIAN__)
|
||||
#include "ppc.c"
|
||||
#elif defined(_WIN32)
|
||||
#include "fiber.c"
|
||||
#else
|
||||
#include "sjlj.c"
|
||||
#endif
|
||||
#elif defined(_MSC_VER)
|
||||
#if defined(_M_IX86)
|
||||
#include "x86.c"
|
||||
#elif defined(_M_AMD64)
|
||||
#include "amd64.c"
|
||||
#else
|
||||
#include "fiber.c"
|
||||
#endif
|
||||
#else
|
||||
#error "libco: unsupported processor, compiler or operating system"
|
||||
#endif
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
libco v20 (2019-10-16)
|
||||
author: byuu
|
||||
license: ISC
|
||||
*/
|
||||
|
||||
#ifndef LIBCO_H
|
||||
#define LIBCO_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void* cothread_t;
|
||||
|
||||
cothread_t co_active(void);
|
||||
cothread_t co_derive(void*, unsigned int, void (*)(void));
|
||||
cothread_t co_create(unsigned int, void (*)(void));
|
||||
void co_delete(cothread_t);
|
||||
void co_switch(cothread_t);
|
||||
int co_serializable(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ifndef LIBCO_H */
|
||||
#endif
|
||||
Vendored
+435
@@ -0,0 +1,435 @@
|
||||
/* ppc64le (ELFv2) is not currently supported */
|
||||
|
||||
#define LIBCO_C
|
||||
#include "libco.h"
|
||||
#include "settings.h"
|
||||
#include "valgrind.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef LIBCO_MPROTECT
|
||||
#include <unistd.h>
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
|
||||
/* state format (offsets in 32-bit words)
|
||||
|
||||
+0 pointer to swap code
|
||||
rest of function descriptor for entry function
|
||||
+8 PC
|
||||
+10 SP
|
||||
special registers
|
||||
GPRs
|
||||
FPRs
|
||||
VRs
|
||||
stack
|
||||
*/
|
||||
|
||||
enum { state_size = 1024 };
|
||||
enum { above_stack = 2048 };
|
||||
enum { stack_align = 256 };
|
||||
|
||||
static thread_local cothread_t co_active_handle = 0;
|
||||
|
||||
/* determine environment */
|
||||
|
||||
#define LIBCO_PPC64 (_ARCH_PPC64 || __PPC64__ || __ppc64__ || __powerpc64__)
|
||||
|
||||
/* whether function calls are indirect through a descriptor, or are directly to function */
|
||||
#ifndef LIBCO_PPCDESC
|
||||
#if !_CALL_SYSV && (_CALL_AIX || _CALL_AIXDESC || (LIBCO_PPC64 && (!defined(_CALL_ELF) || _CALL_ELF == 1)))
|
||||
#define LIBCO_PPCDESC 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef LIBCO_MPROTECT
|
||||
alignas(4096)
|
||||
#else
|
||||
section(text)
|
||||
#endif
|
||||
static const uint32_t libco_ppc_code[1024] = {
|
||||
#if LIBCO_PPC64
|
||||
0x7d000026, /* mfcr r8 */
|
||||
0xf8240028, /* std r1,40(r4) */
|
||||
0x7d2802a6, /* mflr r9 */
|
||||
0xf9c40048, /* std r14,72(r4) */
|
||||
0xf9e40050, /* std r15,80(r4) */
|
||||
0xfa040058, /* std r16,88(r4) */
|
||||
0xfa240060, /* std r17,96(r4) */
|
||||
0xfa440068, /* std r18,104(r4) */
|
||||
0xfa640070, /* std r19,112(r4) */
|
||||
0xfa840078, /* std r20,120(r4) */
|
||||
0xfaa40080, /* std r21,128(r4) */
|
||||
0xfac40088, /* std r22,136(r4) */
|
||||
0xfae40090, /* std r23,144(r4) */
|
||||
0xfb040098, /* std r24,152(r4) */
|
||||
0xfb2400a0, /* std r25,160(r4) */
|
||||
0xfb4400a8, /* std r26,168(r4) */
|
||||
0xfb6400b0, /* std r27,176(r4) */
|
||||
0xfb8400b8, /* std r28,184(r4) */
|
||||
0xfba400c0, /* std r29,192(r4) */
|
||||
0xfbc400c8, /* std r30,200(r4) */
|
||||
0xfbe400d0, /* std r31,208(r4) */
|
||||
0xf9240020, /* std r9,32(r4) */
|
||||
0xe8e30020, /* ld r7,32(r3) */
|
||||
0xe8230028, /* ld r1,40(r3) */
|
||||
0x48000009, /* bl 1 */
|
||||
0x7fe00008, /* trap */
|
||||
0x91040030, /*1:stw r8,48(r4) */
|
||||
0x80c30030, /* lwz r6,48(r3) */
|
||||
0x7ce903a6, /* mtctr r7 */
|
||||
0xe9c30048, /* ld r14,72(r3) */
|
||||
0xe9e30050, /* ld r15,80(r3) */
|
||||
0xea030058, /* ld r16,88(r3) */
|
||||
0xea230060, /* ld r17,96(r3) */
|
||||
0xea430068, /* ld r18,104(r3) */
|
||||
0xea630070, /* ld r19,112(r3) */
|
||||
0xea830078, /* ld r20,120(r3) */
|
||||
0xeaa30080, /* ld r21,128(r3) */
|
||||
0xeac30088, /* ld r22,136(r3) */
|
||||
0xeae30090, /* ld r23,144(r3) */
|
||||
0xeb030098, /* ld r24,152(r3) */
|
||||
0xeb2300a0, /* ld r25,160(r3) */
|
||||
0xeb4300a8, /* ld r26,168(r3) */
|
||||
0xeb6300b0, /* ld r27,176(r3) */
|
||||
0xeb8300b8, /* ld r28,184(r3) */
|
||||
0xeba300c0, /* ld r29,192(r3) */
|
||||
0xebc300c8, /* ld r30,200(r3) */
|
||||
0xebe300d0, /* ld r31,208(r3) */
|
||||
0x7ccff120, /* mtcr r6 */
|
||||
#else
|
||||
0x7d000026, /* mfcr r8 */
|
||||
0x90240028, /* stw r1,40(r4) */
|
||||
0x7d2802a6, /* mflr r9 */
|
||||
0x91a4003c, /* stw r13,60(r4) */
|
||||
0x91c40040, /* stw r14,64(r4) */
|
||||
0x91e40044, /* stw r15,68(r4) */
|
||||
0x92040048, /* stw r16,72(r4) */
|
||||
0x9224004c, /* stw r17,76(r4) */
|
||||
0x92440050, /* stw r18,80(r4) */
|
||||
0x92640054, /* stw r19,84(r4) */
|
||||
0x92840058, /* stw r20,88(r4) */
|
||||
0x92a4005c, /* stw r21,92(r4) */
|
||||
0x92c40060, /* stw r22,96(r4) */
|
||||
0x92e40064, /* stw r23,100(r4) */
|
||||
0x93040068, /* stw r24,104(r4) */
|
||||
0x9324006c, /* stw r25,108(r4) */
|
||||
0x93440070, /* stw r26,112(r4) */
|
||||
0x93640074, /* stw r27,116(r4) */
|
||||
0x93840078, /* stw r28,120(r4) */
|
||||
0x93a4007c, /* stw r29,124(r4) */
|
||||
0x93c40080, /* stw r30,128(r4) */
|
||||
0x93e40084, /* stw r31,132(r4) */
|
||||
0x91240020, /* stw r9,32(r4) */
|
||||
0x80e30020, /* lwz r7,32(r3) */
|
||||
0x80230028, /* lwz r1,40(r3) */
|
||||
0x48000009, /* bl 1 */
|
||||
0x7fe00008, /* trap */
|
||||
0x91040030, /*1:stw r8,48(r4) */
|
||||
0x80c30030, /* lwz r6,48(r3) */
|
||||
0x7ce903a6, /* mtctr r7 */
|
||||
0x81a3003c, /* lwz r13,60(r3) */
|
||||
0x81c30040, /* lwz r14,64(r3) */
|
||||
0x81e30044, /* lwz r15,68(r3) */
|
||||
0x82030048, /* lwz r16,72(r3) */
|
||||
0x8223004c, /* lwz r17,76(r3) */
|
||||
0x82430050, /* lwz r18,80(r3) */
|
||||
0x82630054, /* lwz r19,84(r3) */
|
||||
0x82830058, /* lwz r20,88(r3) */
|
||||
0x82a3005c, /* lwz r21,92(r3) */
|
||||
0x82c30060, /* lwz r22,96(r3) */
|
||||
0x82e30064, /* lwz r23,100(r3) */
|
||||
0x83030068, /* lwz r24,104(r3) */
|
||||
0x8323006c, /* lwz r25,108(r3) */
|
||||
0x83430070, /* lwz r26,112(r3) */
|
||||
0x83630074, /* lwz r27,116(r3) */
|
||||
0x83830078, /* lwz r28,120(r3) */
|
||||
0x83a3007c, /* lwz r29,124(r3) */
|
||||
0x83c30080, /* lwz r30,128(r3) */
|
||||
0x83e30084, /* lwz r31,132(r3) */
|
||||
0x7ccff120, /* mtcr r6 */
|
||||
#endif
|
||||
|
||||
#ifndef LIBCO_PPC_NOFP
|
||||
0xd9c400e0, /* stfd f14,224(r4) */
|
||||
0xd9e400e8, /* stfd f15,232(r4) */
|
||||
0xda0400f0, /* stfd f16,240(r4) */
|
||||
0xda2400f8, /* stfd f17,248(r4) */
|
||||
0xda440100, /* stfd f18,256(r4) */
|
||||
0xda640108, /* stfd f19,264(r4) */
|
||||
0xda840110, /* stfd f20,272(r4) */
|
||||
0xdaa40118, /* stfd f21,280(r4) */
|
||||
0xdac40120, /* stfd f22,288(r4) */
|
||||
0xdae40128, /* stfd f23,296(r4) */
|
||||
0xdb040130, /* stfd f24,304(r4) */
|
||||
0xdb240138, /* stfd f25,312(r4) */
|
||||
0xdb440140, /* stfd f26,320(r4) */
|
||||
0xdb640148, /* stfd f27,328(r4) */
|
||||
0xdb840150, /* stfd f28,336(r4) */
|
||||
0xdba40158, /* stfd f29,344(r4) */
|
||||
0xdbc40160, /* stfd f30,352(r4) */
|
||||
0xdbe40168, /* stfd f31,360(r4) */
|
||||
0xc9c300e0, /* lfd f14,224(r3) */
|
||||
0xc9e300e8, /* lfd f15,232(r3) */
|
||||
0xca0300f0, /* lfd f16,240(r3) */
|
||||
0xca2300f8, /* lfd f17,248(r3) */
|
||||
0xca430100, /* lfd f18,256(r3) */
|
||||
0xca630108, /* lfd f19,264(r3) */
|
||||
0xca830110, /* lfd f20,272(r3) */
|
||||
0xcaa30118, /* lfd f21,280(r3) */
|
||||
0xcac30120, /* lfd f22,288(r3) */
|
||||
0xcae30128, /* lfd f23,296(r3) */
|
||||
0xcb030130, /* lfd f24,304(r3) */
|
||||
0xcb230138, /* lfd f25,312(r3) */
|
||||
0xcb430140, /* lfd f26,320(r3) */
|
||||
0xcb630148, /* lfd f27,328(r3) */
|
||||
0xcb830150, /* lfd f28,336(r3) */
|
||||
0xcba30158, /* lfd f29,344(r3) */
|
||||
0xcbc30160, /* lfd f30,352(r3) */
|
||||
0xcbe30168, /* lfd f31,360(r3) */
|
||||
#endif
|
||||
|
||||
#ifdef __ALTIVEC__
|
||||
0x7ca042a6, /* mfvrsave r5 */
|
||||
0x39040180, /* addi r8,r4,384 */
|
||||
0x39240190, /* addi r9,r4,400 */
|
||||
0x70a00fff, /* andi. r0,r5,4095 */
|
||||
0x90a40034, /* stw r5,52(r4) */
|
||||
0x4182005c, /* beq- 2 */
|
||||
0x7e8041ce, /* stvx v20,r0,r8 */
|
||||
0x39080020, /* addi r8,r8,32 */
|
||||
0x7ea049ce, /* stvx v21,r0,r9 */
|
||||
0x39290020, /* addi r9,r9,32 */
|
||||
0x7ec041ce, /* stvx v22,r0,r8 */
|
||||
0x39080020, /* addi r8,r8,32 */
|
||||
0x7ee049ce, /* stvx v23,r0,r9 */
|
||||
0x39290020, /* addi r9,r9,32 */
|
||||
0x7f0041ce, /* stvx v24,r0,r8 */
|
||||
0x39080020, /* addi r8,r8,32 */
|
||||
0x7f2049ce, /* stvx v25,r0,r9 */
|
||||
0x39290020, /* addi r9,r9,32 */
|
||||
0x7f4041ce, /* stvx v26,r0,r8 */
|
||||
0x39080020, /* addi r8,r8,32 */
|
||||
0x7f6049ce, /* stvx v27,r0,r9 */
|
||||
0x39290020, /* addi r9,r9,32 */
|
||||
0x7f8041ce, /* stvx v28,r0,r8 */
|
||||
0x39080020, /* addi r8,r8,32 */
|
||||
0x7fa049ce, /* stvx v29,r0,r9 */
|
||||
0x39290020, /* addi r9,r9,32 */
|
||||
0x7fc041ce, /* stvx v30,r0,r8 */
|
||||
0x7fe049ce, /* stvx v31,r0,r9 */
|
||||
0x80a30034, /*2:lwz r5,52(r3) */
|
||||
0x39030180, /* addi r8,r3,384 */
|
||||
0x39230190, /* addi r9,r3,400 */
|
||||
0x70a00fff, /* andi. r0,r5,4095 */
|
||||
0x7ca043a6, /* mtvrsave r5 */
|
||||
0x4d820420, /* beqctr */
|
||||
0x7e8040ce, /* lvx v20,r0,r8 */
|
||||
0x39080020, /* addi r8,r8,32 */
|
||||
0x7ea048ce, /* lvx v21,r0,r9 */
|
||||
0x39290020, /* addi r9,r9,32 */
|
||||
0x7ec040ce, /* lvx v22,r0,r8 */
|
||||
0x39080020, /* addi r8,r8,32 */
|
||||
0x7ee048ce, /* lvx v23,r0,r9 */
|
||||
0x39290020, /* addi r9,r9,32 */
|
||||
0x7f0040ce, /* lvx v24,r0,r8 */
|
||||
0x39080020, /* addi r8,r8,32 */
|
||||
0x7f2048ce, /* lvx v25,r0,r9 */
|
||||
0x39290020, /* addi r9,r9,32 */
|
||||
0x7f4040ce, /* lvx v26,r0,r8 */
|
||||
0x39080020, /* addi r8,r8,32 */
|
||||
0x7f6048ce, /* lvx v27,r0,r9 */
|
||||
0x39290020, /* addi r9,r9,32 */
|
||||
0x7f8040ce, /* lvx v28,r0,r8 */
|
||||
0x39080020, /* addi r8,r8,32 */
|
||||
0x7fa048ce, /* lvx v29,r0,r9 */
|
||||
0x39290020, /* addi r9,r9,32 */
|
||||
0x7fc040ce, /* lvx v30,r0,r8 */
|
||||
0x7fe048ce, /* lvx v31,r0,r9 */
|
||||
#endif
|
||||
|
||||
0x4e800420, /* bctr */
|
||||
};
|
||||
|
||||
#if LIBCO_PPCDESC
|
||||
/* function call goes through indirect descriptor */
|
||||
#define CO_SWAP_ASM(x, y) ((void (*)(cothread_t, cothread_t))(uintptr_t)x)(x, y)
|
||||
#else
|
||||
/* function call goes directly to code */
|
||||
#define CO_SWAP_ASM(x, y) ((void (*)(cothread_t, cothread_t))(uintptr_t)libco_ppc_code)(x, y)
|
||||
#endif
|
||||
|
||||
static uint32_t* co_derive_(void* memory, unsigned size, uintptr_t entry) {
|
||||
uint32_t* t = (uint32_t*)memory;
|
||||
|
||||
(void)entry;
|
||||
|
||||
#if LIBCO_PPCDESC
|
||||
if(t) {
|
||||
memcpy(t, (void*)entry, sizeof(void*) * 3); /* copy entry's descriptor */
|
||||
*(const void**)t = libco_ppc_code; /* set function pointer to swap routine */
|
||||
}
|
||||
#endif
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
cothread_t co_derive(void* memory, unsigned int size, void (*entry_)(void)) {
|
||||
uintptr_t entry = (uintptr_t)entry_;
|
||||
uint32_t* t = 0;
|
||||
|
||||
/* be sure main thread was successfully allocated */
|
||||
if(co_active()) {
|
||||
t = co_derive_(memory, size, entry);
|
||||
}
|
||||
|
||||
if(t) {
|
||||
uintptr_t sp;
|
||||
int shift;
|
||||
|
||||
VALGRIND_STACK_REGISTER(t, (char*)t + size);
|
||||
|
||||
/* save current registers into new thread, so that any special ones will have proper values when thread is begun */
|
||||
CO_SWAP_ASM(t, t);
|
||||
|
||||
#if LIBCO_PPCDESC
|
||||
entry = (uintptr_t)*(void**)entry; /* get real address */
|
||||
#endif
|
||||
|
||||
/* put stack near end of block, and align */
|
||||
sp = (uintptr_t)t + size - above_stack;
|
||||
sp -= sp % stack_align;
|
||||
|
||||
/* on PPC32, we save and restore GPRs as 32 bits. for PPC64, we
|
||||
save and restore them as 64 bits, regardless of the size the ABI
|
||||
uses. so, we manually write pointers at the proper size. we always
|
||||
save and restore at the same address, and since PPC is big-endian,
|
||||
we must put the low byte first on PPC32. */
|
||||
|
||||
/* if uintptr_t is 32 bits, >>32 is undefined behavior,
|
||||
so we do two shifts and don't have to care how many bits uintptr_t is. */
|
||||
#if LIBCO_PPC64
|
||||
shift = 16;
|
||||
#else
|
||||
shift = 0;
|
||||
#endif
|
||||
|
||||
/* set up so entry will be called on next swap */
|
||||
t[ 8] = (uint32_t)(entry >> shift >> shift);
|
||||
t[ 9] = (uint32_t)entry;
|
||||
|
||||
t[10] = (uint32_t)(sp >> shift >> shift);
|
||||
t[11] = (uint32_t)sp;
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
static uint32_t* co_create_(unsigned size, uintptr_t entry) {
|
||||
uint32_t* t = (uint32_t*)LIBCO_MALLOC(size);
|
||||
|
||||
(void)entry;
|
||||
|
||||
#if LIBCO_PPCDESC
|
||||
if(t) {
|
||||
memcpy(t, (void*)entry, sizeof(void*) * 3); /* copy entry's descriptor */
|
||||
*(const void**)t = libco_ppc_code; /* set function pointer to swap routine */
|
||||
}
|
||||
#endif
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
cothread_t co_create(unsigned int size, void (*entry_)(void)) {
|
||||
uintptr_t entry = (uintptr_t)entry_;
|
||||
uint32_t* t = 0;
|
||||
|
||||
/* be sure main thread was successfully allocated */
|
||||
if(co_active()) {
|
||||
size += state_size + above_stack + stack_align;
|
||||
t = co_create_(size, entry);
|
||||
}
|
||||
|
||||
if(t) {
|
||||
uintptr_t sp;
|
||||
int shift;
|
||||
|
||||
VALGRIND_STACK_REGISTER(t, (char*)t + size);
|
||||
|
||||
/* save current registers into new thread, so that any special ones will have proper values when thread is begun */
|
||||
CO_SWAP_ASM(t, t);
|
||||
|
||||
#if LIBCO_PPCDESC
|
||||
entry = (uintptr_t)*(void**)entry; /* get real address */
|
||||
#endif
|
||||
|
||||
/* put stack near end of block, and align */
|
||||
sp = (uintptr_t)t + size - above_stack;
|
||||
sp -= sp % stack_align;
|
||||
|
||||
/* on PPC32, we save and restore GPRs as 32 bits. for PPC64, we
|
||||
save and restore them as 64 bits, regardless of the size the ABI
|
||||
uses. so, we manually write pointers at the proper size. we always
|
||||
save and restore at the same address, and since PPC is big-endian,
|
||||
we must put the low byte first on PPC32. */
|
||||
|
||||
/* if uintptr_t is 32 bits, >>32 is undefined behavior,
|
||||
so we do two shifts and don't have to care how many bits uintptr_t is. */
|
||||
#if LIBCO_PPC64
|
||||
shift = 16;
|
||||
#else
|
||||
shift = 0;
|
||||
#endif
|
||||
|
||||
/* set up so entry will be called on next swap */
|
||||
t[ 8] = (uint32_t)(entry >> shift >> shift);
|
||||
t[ 9] = (uint32_t)entry;
|
||||
|
||||
t[10] = (uint32_t)(sp >> shift >> shift);
|
||||
t[11] = (uint32_t)sp;
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
void co_delete(cothread_t t) {
|
||||
LIBCO_FREE(t);
|
||||
}
|
||||
|
||||
static void co_init_(void) {
|
||||
#if LIBCO_MPROTECT
|
||||
long page_size = sysconf(_SC_PAGESIZE);
|
||||
if(page_size > 0) {
|
||||
uintptr_t align = page_size;
|
||||
uintptr_t begin = (uintptr_t)libco_ppc_code;
|
||||
uintptr_t end = begin + sizeof libco_ppc_code;
|
||||
|
||||
/* align beginning and end */
|
||||
end += align - 1;
|
||||
end -= end % align;
|
||||
begin -= begin % align;
|
||||
|
||||
mprotect((void*)begin, end - begin, PROT_READ | PROT_EXEC);
|
||||
}
|
||||
#endif
|
||||
|
||||
co_active_handle = co_create_(state_size, (uintptr_t)&co_switch);
|
||||
}
|
||||
|
||||
cothread_t co_active(void) {
|
||||
if(!co_active_handle) co_init_();
|
||||
|
||||
return co_active_handle;
|
||||
}
|
||||
|
||||
void co_switch(cothread_t t) {
|
||||
cothread_t old = co_active_handle;
|
||||
co_active_handle = t;
|
||||
|
||||
CO_SWAP_ASM(t, old);
|
||||
}
|
||||
|
||||
int co_serializable(void) {
|
||||
return 0;
|
||||
}
|
||||
Vendored
+281
@@ -0,0 +1,281 @@
|
||||
/* author: Shawn Anastasio */
|
||||
|
||||
#define LIBCO_C
|
||||
#include "libco.h"
|
||||
#include "settings.h"
|
||||
#include "valgrind.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct ppc64_context {
|
||||
/* GPRs */
|
||||
uint64_t gprs[32];
|
||||
uint64_t lr;
|
||||
uint64_t ccr;
|
||||
|
||||
/* FPRs */
|
||||
uint64_t fprs[32];
|
||||
|
||||
#ifdef __ALTIVEC__
|
||||
/* Altivec (VMX) */
|
||||
uint64_t vmx[12 * 2];
|
||||
uint32_t vrsave;
|
||||
#endif
|
||||
};
|
||||
|
||||
static thread_local struct ppc64_context* co_active_handle = 0;
|
||||
|
||||
#define MAX(x, y) ((x) > (y) ? (x) : (y))
|
||||
#define ALIGN(p, x) ((void*)((uintptr_t)(p) & ~((x) - 1)))
|
||||
|
||||
#define MIN_STACK 0x10000lu
|
||||
#define MIN_STACK_FRAME 0x20lu
|
||||
#define STACK_ALIGN 0x10lu
|
||||
|
||||
void swap_context(struct ppc64_context* read, struct ppc64_context* write);
|
||||
__asm__(
|
||||
".text\n"
|
||||
".align 4\n"
|
||||
".type swap_context @function\n"
|
||||
"swap_context:\n"
|
||||
".cfi_startproc\n"
|
||||
|
||||
/* save GPRs */
|
||||
"std 1, 8(4)\n"
|
||||
"std 2, 16(4)\n"
|
||||
"std 12, 96(4)\n"
|
||||
"std 13, 104(4)\n"
|
||||
"std 14, 112(4)\n"
|
||||
"std 15, 120(4)\n"
|
||||
"std 16, 128(4)\n"
|
||||
"std 17, 136(4)\n"
|
||||
"std 18, 144(4)\n"
|
||||
"std 19, 152(4)\n"
|
||||
"std 20, 160(4)\n"
|
||||
"std 21, 168(4)\n"
|
||||
"std 22, 176(4)\n"
|
||||
"std 23, 184(4)\n"
|
||||
"std 24, 192(4)\n"
|
||||
"std 25, 200(4)\n"
|
||||
"std 26, 208(4)\n"
|
||||
"std 27, 216(4)\n"
|
||||
"std 28, 224(4)\n"
|
||||
"std 29, 232(4)\n"
|
||||
"std 30, 240(4)\n"
|
||||
"std 31, 248(4)\n"
|
||||
|
||||
/* save LR */
|
||||
"mflr 5\n"
|
||||
"std 5, 256(4)\n"
|
||||
|
||||
/* save CCR */
|
||||
"mfcr 5\n"
|
||||
"std 5, 264(4)\n"
|
||||
|
||||
/* save FPRs */
|
||||
"stfd 14, 384(4)\n"
|
||||
"stfd 15, 392(4)\n"
|
||||
"stfd 16, 400(4)\n"
|
||||
"stfd 17, 408(4)\n"
|
||||
"stfd 18, 416(4)\n"
|
||||
"stfd 19, 424(4)\n"
|
||||
"stfd 20, 432(4)\n"
|
||||
"stfd 21, 440(4)\n"
|
||||
"stfd 22, 448(4)\n"
|
||||
"stfd 23, 456(4)\n"
|
||||
"stfd 24, 464(4)\n"
|
||||
"stfd 25, 472(4)\n"
|
||||
"stfd 26, 480(4)\n"
|
||||
"stfd 27, 488(4)\n"
|
||||
"stfd 28, 496(4)\n"
|
||||
"stfd 29, 504(4)\n"
|
||||
"stfd 30, 512(4)\n"
|
||||
"stfd 31, 520(4)\n"
|
||||
|
||||
#ifdef __ALTIVEC__
|
||||
/* save VMX */
|
||||
"li 5, 528\n"
|
||||
"stvxl 20, 4, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"stvxl 21, 4, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"stvxl 22, 4, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"stvxl 23, 4, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"stvxl 24, 4, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"stvxl 25, 4, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"stvxl 26, 4, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"stvxl 27, 4, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"stvxl 28, 4, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"stvxl 29, 4, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"stvxl 30, 4, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"stvxl 31, 4, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
|
||||
/* save VRSAVE */
|
||||
"mfvrsave 5\n"
|
||||
"stw 5, 736(4)\n"
|
||||
#endif
|
||||
|
||||
/* restore GPRs */
|
||||
"ld 1, 8(3)\n"
|
||||
"ld 2, 16(3)\n"
|
||||
"ld 12, 96(3)\n"
|
||||
"ld 13, 104(3)\n"
|
||||
"ld 14, 112(3)\n"
|
||||
"ld 15, 120(3)\n"
|
||||
"ld 16, 128(3)\n"
|
||||
"ld 17, 136(3)\n"
|
||||
"ld 18, 144(3)\n"
|
||||
"ld 19, 152(3)\n"
|
||||
"ld 20, 160(3)\n"
|
||||
"ld 21, 168(3)\n"
|
||||
"ld 22, 176(3)\n"
|
||||
"ld 23, 184(3)\n"
|
||||
"ld 24, 192(3)\n"
|
||||
"ld 25, 200(3)\n"
|
||||
"ld 26, 208(3)\n"
|
||||
"ld 27, 216(3)\n"
|
||||
"ld 28, 224(3)\n"
|
||||
"ld 29, 232(3)\n"
|
||||
"ld 30, 240(3)\n"
|
||||
"ld 31, 248(3)\n"
|
||||
|
||||
/* restore LR */
|
||||
"ld 5, 256(3)\n"
|
||||
"mtlr 5\n"
|
||||
|
||||
/* restore CCR */
|
||||
"ld 5, 264(3)\n"
|
||||
"mtcr 5\n"
|
||||
|
||||
/* restore FPRs */
|
||||
"lfd 14, 384(3)\n"
|
||||
"lfd 15, 392(3)\n"
|
||||
"lfd 16, 400(3)\n"
|
||||
"lfd 17, 408(3)\n"
|
||||
"lfd 18, 416(3)\n"
|
||||
"lfd 19, 424(3)\n"
|
||||
"lfd 20, 432(3)\n"
|
||||
"lfd 21, 440(3)\n"
|
||||
"lfd 22, 448(3)\n"
|
||||
"lfd 23, 456(3)\n"
|
||||
"lfd 24, 464(3)\n"
|
||||
"lfd 25, 472(3)\n"
|
||||
"lfd 26, 480(3)\n"
|
||||
"lfd 27, 488(3)\n"
|
||||
"lfd 28, 496(3)\n"
|
||||
"lfd 29, 504(3)\n"
|
||||
"lfd 30, 512(3)\n"
|
||||
"lfd 31, 520(3)\n"
|
||||
|
||||
#ifdef __ALTIVEC__
|
||||
/* restore VMX */
|
||||
"li 5, 528\n"
|
||||
"lvxl 20, 3, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"lvxl 21, 3, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"lvxl 22, 3, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"lvxl 23, 3, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"lvxl 24, 3, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"lvxl 25, 3, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"lvxl 26, 3, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"lvxl 27, 3, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"lvxl 28, 3, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"lvxl 29, 3, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"lvxl 30, 3, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
"lvxl 31, 3, 5\n"
|
||||
"addi 5, 5, 16\n"
|
||||
|
||||
/* restore VRSAVE */
|
||||
"lwz 5, 720(3)\n"
|
||||
"mtvrsave 5\n"
|
||||
#endif
|
||||
|
||||
/* branch to LR */
|
||||
"blr\n"
|
||||
|
||||
".cfi_endproc\n"
|
||||
".size swap_context, .-swap_context\n"
|
||||
);
|
||||
|
||||
cothread_t co_active(void) {
|
||||
if(!co_active_handle) {
|
||||
co_active_handle = (struct ppc64_context*)LIBCO_MALLOC(MIN_STACK + sizeof(struct ppc64_context));
|
||||
}
|
||||
return (cothread_t)co_active_handle;
|
||||
}
|
||||
|
||||
cothread_t co_derive(void* memory, unsigned int size, void (*coentry)(void)) {
|
||||
uint8_t* sp;
|
||||
struct ppc64_context* context = (struct ppc64_context*)memory;
|
||||
|
||||
VALGRIND_STACK_REGISTER(memory, memory + size);
|
||||
|
||||
/* save current context into new context to initialize it */
|
||||
swap_context(context, context);
|
||||
|
||||
/* align stack */
|
||||
sp = (uint8_t*)memory + size - STACK_ALIGN;
|
||||
sp = (uint8_t*)ALIGN(sp, STACK_ALIGN);
|
||||
|
||||
/* write 0 for initial backchain */
|
||||
*(uint64_t*)sp = 0;
|
||||
|
||||
/* create new frame with backchain */
|
||||
sp -= MIN_STACK_FRAME;
|
||||
*(uint64_t*)sp = (uint64_t)(sp + MIN_STACK_FRAME);
|
||||
|
||||
/* update context with new stack (r1) and entrypoint (r12, lr) */
|
||||
context->gprs[ 1] = (uint64_t)sp;
|
||||
context->gprs[12] = (uint64_t)coentry;
|
||||
context->lr = (uint64_t)coentry;
|
||||
|
||||
return (cothread_t)memory;
|
||||
}
|
||||
|
||||
cothread_t co_create(unsigned int size, void (*coentry)(void)) {
|
||||
void* memory = LIBCO_MALLOC(size);
|
||||
if(!memory) return (cothread_t)0;
|
||||
return co_derive(memory, size, coentry);
|
||||
}
|
||||
|
||||
void co_delete(cothread_t handle) {
|
||||
LIBCO_FREE(handle);
|
||||
}
|
||||
|
||||
void co_switch(cothread_t to) {
|
||||
struct ppc64_context* from = co_active_handle;
|
||||
co_active_handle = (struct ppc64_context*)to;
|
||||
swap_context((struct ppc64_context*)to, from);
|
||||
}
|
||||
|
||||
int co_serializable(void) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Vendored
+131
@@ -0,0 +1,131 @@
|
||||
#if defined(LIBCO_C)
|
||||
|
||||
/*[amd64, arm, ppc, x86]:
|
||||
by default, co_swap_function is marked as a text (code) section
|
||||
if not supported, uncomment the below line to use mprotect instead */
|
||||
/* #define LIBCO_MPROTECT */
|
||||
|
||||
/*[amd64]:
|
||||
Win64 only: provides a substantial speed-up, but will thrash XMM regs
|
||||
do not use this unless you are certain your application won't use SSE */
|
||||
/* #define LIBCO_NO_SSE */
|
||||
|
||||
#if !defined(thread_local) /* User can override thread_local for obscure compilers */
|
||||
#if !defined(LIBCO_MP) /* Running in single-threaded environment */
|
||||
#define thread_local
|
||||
#else /* Running in multi-threaded environment */
|
||||
#if defined(__STDC__) /* Compiling as C Language */
|
||||
#if defined(_MSC_VER) /* Don't rely on MSVC's C11 support */
|
||||
#define thread_local __declspec(thread)
|
||||
#elif __STDC_VERSION__ < 201112L /* If we are on C90/99 */
|
||||
#if defined(__clang__) || defined(__GNUC__) /* Clang and GCC */
|
||||
#define thread_local __thread
|
||||
#else /* Otherwise, we ignore the directive (unless user provides their own) */
|
||||
#define thread_local
|
||||
#endif
|
||||
#else /* C11 and newer define thread_local in threads.h */
|
||||
#include <threads.h>
|
||||
#endif
|
||||
#elif defined(__cplusplus) /* Compiling as C++ Language */
|
||||
#if __cplusplus < 201103L /* thread_local is a C++11 feature */
|
||||
#if defined(_MSC_VER)
|
||||
#define thread_local __declspec(thread)
|
||||
#elif defined(__clang__) || defined(__GNUC__)
|
||||
#define thread_local __thread
|
||||
#else /* Otherwise, we ignore the directive (unless user provides their own) */
|
||||
#define thread_local
|
||||
#endif
|
||||
#else /* In C++ >= 11, thread_local in a builtin keyword */
|
||||
/* Don't do anything */
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* In alignas(a), 'a' should be a power of two that is at least the type's
|
||||
alignment and at most the implementation's alignment limit. This limit is
|
||||
2**13 on MSVC. To be portable to MSVC through at least version 10.0,
|
||||
'a' should be an integer constant, as MSVC does not support expressions
|
||||
such as 1 << 3.
|
||||
|
||||
The following C11 requirements are NOT supported on MSVC:
|
||||
|
||||
- If 'a' is zero, alignas has no effect.
|
||||
- alignas can be used multiple times; the strictest one wins.
|
||||
- alignas (TYPE) is equivalent to alignas (alignof (TYPE)).
|
||||
*/
|
||||
#if !defined(alignas)
|
||||
#if defined(__STDC__) /* C Language */
|
||||
#if defined(_MSC_VER) /* Don't rely on MSVC's C11 support */
|
||||
#define alignas(bytes) __declspec(align(bytes))
|
||||
#elif __STDC_VERSION__ >= 201112L /* C11 and above */
|
||||
#include <stdalign.h>
|
||||
#elif defined(__clang__) || defined(__GNUC__) /* C90/99 on Clang/GCC */
|
||||
#define alignas(bytes) __attribute__ ((aligned (bytes)))
|
||||
#else /* Otherwise, we ignore the directive (user should provide their own) */
|
||||
#define alignas(bytes)
|
||||
#endif
|
||||
#elif defined(__cplusplus) /* C++ Language */
|
||||
#if __cplusplus < 201103L
|
||||
#if defined(_MSC_VER)
|
||||
#define alignas(bytes) __declspec(align(bytes))
|
||||
#elif defined(__clang__) || defined(__GNUC__) /* C++98/03 on Clang/GCC */
|
||||
#define alignas(bytes) __attribute__ ((aligned (bytes)))
|
||||
#else /* Otherwise, we ignore the directive (unless user provides their own) */
|
||||
#define alignas(bytes)
|
||||
#endif
|
||||
#else /* C++ >= 11 has alignas keyword */
|
||||
/* Do nothing */
|
||||
#endif
|
||||
#endif /* = !defined(__STDC_VERSION__) && !defined(__cplusplus) */
|
||||
#endif
|
||||
|
||||
#if !defined(LIBCO_ASSERT)
|
||||
#include <assert.h>
|
||||
#define LIBCO_ASSERT assert
|
||||
#endif
|
||||
|
||||
#if defined (__OpenBSD__)
|
||||
#if !defined(LIBCO_MALLOC) || !defined(LIBCO_FREE)
|
||||
#include <unistd.h>
|
||||
#include <sys/mman.h>
|
||||
|
||||
static void* malloc_obsd(size_t size) {
|
||||
long pagesize = sysconf(_SC_PAGESIZE);
|
||||
char* memory = (char*)mmap(NULL, size + pagesize, PROT_READ|PROT_WRITE, MAP_STACK|MAP_PRIVATE|MAP_ANON, -1, 0);
|
||||
if (memory == MAP_FAILED) return NULL;
|
||||
*(size_t*)memory = size + pagesize;
|
||||
memory += pagesize;
|
||||
return (void*)memory;
|
||||
}
|
||||
|
||||
static void free_obsd(void *ptr) {
|
||||
char* memory = (char*)ptr - sysconf(_SC_PAGESIZE);
|
||||
munmap(memory, *(size_t*)memory);
|
||||
}
|
||||
|
||||
#define LIBCO_MALLOC malloc_obsd
|
||||
#define LIBCO_FREE free_obsd
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if !defined(LIBCO_MALLOC) || !defined(LIBCO_FREE)
|
||||
#include <stdlib.h>
|
||||
#define LIBCO_MALLOC malloc
|
||||
#define LIBCO_FREE free
|
||||
#endif
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
/* workaround for msvc preprocessor stringification behavior */
|
||||
#define LIBCO_STRINGIFY(x) #x
|
||||
#define LIBCO_TOSTRING(x) LIBCO_STRINGIFY(x)
|
||||
#define section(name) __pragma(code_seg(LIBCO_TOSTRING("." #name))) __declspec(allocate(LIBCO_TOSTRING("." #name)))
|
||||
#elif defined(__APPLE__)
|
||||
#define section(name) __attribute__((section("__TEXT,__" #name)))
|
||||
#else
|
||||
#define section(name) __attribute__((section("." #name "#")))
|
||||
#endif
|
||||
|
||||
|
||||
/* if defined(LIBCO_C) */
|
||||
#endif
|
||||
Vendored
+155
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
note this was designed for UNIX systems. Based on ideas expressed in a paper by Ralf Engelschall.
|
||||
for SJLJ on other systems, one would want to rewrite springboard() and co_create() and hack the jmb_buf stack pointer.
|
||||
*/
|
||||
|
||||
/* for sigsetjmp(), sigjmp_buf, and stack_t */
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
/* for SA_ONSTACK */
|
||||
#define _XOPEN_SOURCE 600
|
||||
|
||||
#define LIBCO_C
|
||||
#include "libco.h"
|
||||
#include "settings.h"
|
||||
#include "valgrind.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <signal.h>
|
||||
#include <setjmp.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
sigjmp_buf context;
|
||||
void (*coentry)(void);
|
||||
void* stack;
|
||||
} cothread_struct;
|
||||
|
||||
static thread_local cothread_struct co_primary;
|
||||
static thread_local cothread_struct* creating;
|
||||
static thread_local cothread_struct* co_running = 0;
|
||||
|
||||
static void springboard(int ignored) {
|
||||
if(sigsetjmp(creating->context, 0)) {
|
||||
co_running->coentry();
|
||||
}
|
||||
}
|
||||
|
||||
cothread_t co_active(void) {
|
||||
if(!co_running) co_running = &co_primary;
|
||||
return (cothread_t)co_running;
|
||||
}
|
||||
|
||||
cothread_t co_derive(void* memory, unsigned int size, void (*coentry)(void)) {
|
||||
cothread_struct* thread;
|
||||
if(!co_running) co_running = &co_primary;
|
||||
|
||||
thread = (cothread_struct*)memory;
|
||||
memory = (unsigned char*)memory + sizeof(cothread_struct);
|
||||
size -= sizeof(cothread_struct);
|
||||
if(thread) {
|
||||
struct sigaction handler;
|
||||
struct sigaction old_handler;
|
||||
|
||||
stack_t stack;
|
||||
stack_t old_stack;
|
||||
|
||||
thread->coentry = thread->stack = 0;
|
||||
|
||||
stack.ss_flags = 0;
|
||||
stack.ss_size = size;
|
||||
thread->stack = stack.ss_sp = memory;
|
||||
if(stack.ss_sp && !sigaltstack(&stack, &old_stack)) {
|
||||
handler.sa_handler = springboard;
|
||||
handler.sa_flags = SA_ONSTACK;
|
||||
sigemptyset(&handler.sa_mask);
|
||||
creating = thread;
|
||||
|
||||
if(!sigaction(SIGUSR1, &handler, &old_handler)) {
|
||||
if(!raise(SIGUSR1)) {
|
||||
thread->coentry = coentry;
|
||||
}
|
||||
sigaltstack(&old_stack, 0);
|
||||
sigaction(SIGUSR1, &old_handler, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if(thread->coentry != coentry) {
|
||||
co_delete(thread);
|
||||
thread = 0;
|
||||
} else {
|
||||
VALGRIND_STACK_REGISTER(stack.ss_sp, stack.ss_sp + size);
|
||||
}
|
||||
}
|
||||
|
||||
return (cothread_t)thread;
|
||||
}
|
||||
|
||||
cothread_t co_create(unsigned int size, void (*coentry)(void)) {
|
||||
cothread_struct* thread;
|
||||
if(!co_running) co_running = &co_primary;
|
||||
|
||||
thread = (cothread_struct*)malloc(sizeof(cothread_struct));
|
||||
if(thread) {
|
||||
struct sigaction handler;
|
||||
struct sigaction old_handler;
|
||||
|
||||
stack_t stack;
|
||||
stack_t old_stack;
|
||||
|
||||
thread->coentry = thread->stack = 0;
|
||||
|
||||
stack.ss_flags = 0;
|
||||
stack.ss_size = size;
|
||||
thread->stack = stack.ss_sp = malloc(size);
|
||||
if(stack.ss_sp && !sigaltstack(&stack, &old_stack)) {
|
||||
handler.sa_handler = springboard;
|
||||
handler.sa_flags = SA_ONSTACK;
|
||||
sigemptyset(&handler.sa_mask);
|
||||
creating = thread;
|
||||
|
||||
if(!sigaction(SIGUSR1, &handler, &old_handler)) {
|
||||
if(!raise(SIGUSR1)) {
|
||||
thread->coentry = coentry;
|
||||
}
|
||||
sigaltstack(&old_stack, 0);
|
||||
sigaction(SIGUSR1, &old_handler, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if(thread->coentry != coentry) {
|
||||
co_delete(thread);
|
||||
thread = 0;
|
||||
} else {
|
||||
VALGRIND_STACK_REGISTER(stack.ss_sp, stack.ss_sp + size);
|
||||
}
|
||||
}
|
||||
|
||||
return (cothread_t)thread;
|
||||
}
|
||||
|
||||
void co_delete(cothread_t cothread) {
|
||||
if(cothread) {
|
||||
if(((cothread_struct*)cothread)->stack) {
|
||||
free(((cothread_struct*)cothread)->stack);
|
||||
}
|
||||
free(cothread);
|
||||
}
|
||||
}
|
||||
|
||||
void co_switch(cothread_t cothread) {
|
||||
if(!sigsetjmp(co_running->context, 0)) {
|
||||
co_running = (cothread_struct*)cothread;
|
||||
siglongjmp(co_running->context, 1);
|
||||
}
|
||||
}
|
||||
|
||||
int co_serializable(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Vendored
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
WARNING: the overhead of POSIX ucontext is very high,
|
||||
assembly versions of libco or libco_sjlj should be much faster
|
||||
|
||||
this library only exists for two reasons:
|
||||
1: as an initial test for the viability of a ucontext implementation
|
||||
2: to demonstrate the power and speed of libco over existing implementations,
|
||||
such as pth (which defaults to wrapping ucontext on unix targets)
|
||||
|
||||
use this library only as a *last resort*
|
||||
*/
|
||||
|
||||
#define _POSIX_C_SOURCE 200112L
|
||||
#define LIBCO_C
|
||||
#include "libco.h"
|
||||
#include "settings.h"
|
||||
#include "valgrind.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <ucontext.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
static thread_local ucontext_t co_primary;
|
||||
static thread_local ucontext_t* co_running = 0;
|
||||
|
||||
cothread_t co_active(void) {
|
||||
if(!co_running) co_running = &co_primary;
|
||||
return (cothread_t)co_running;
|
||||
}
|
||||
|
||||
cothread_t co_derive(void* memory, unsigned int heapsize, void (*coentry)(void)) {
|
||||
ucontext_t* thread;
|
||||
if(!co_running) co_running = &co_primary;
|
||||
thread = (ucontext_t*)memory;
|
||||
memory = (unsigned char*)memory + sizeof(ucontext_t);
|
||||
heapsize -= sizeof(ucontext_t);
|
||||
if(thread) {
|
||||
if((!getcontext(thread) && !(thread->uc_stack.ss_sp = 0)) && (thread->uc_stack.ss_sp = memory)) {
|
||||
thread->uc_link = co_running;
|
||||
thread->uc_stack.ss_size = heapsize;
|
||||
makecontext(thread, coentry, 0);
|
||||
VALGRIND_STACK_REGISTER(thread->uc_stack.ss_sp, thread->uc_stack.ss_sp + heapsize);
|
||||
} else {
|
||||
thread = 0;
|
||||
}
|
||||
}
|
||||
return (cothread_t)thread;
|
||||
}
|
||||
|
||||
cothread_t co_create(unsigned int heapsize, void (*coentry)(void)) {
|
||||
ucontext_t* thread;
|
||||
if(!co_running) co_running = &co_primary;
|
||||
thread = (ucontext_t*)malloc(sizeof(ucontext_t));
|
||||
if(thread) {
|
||||
if((!getcontext(thread) && !(thread->uc_stack.ss_sp = 0)) && (thread->uc_stack.ss_sp = malloc(heapsize))) {
|
||||
thread->uc_link = co_running;
|
||||
thread->uc_stack.ss_size = heapsize;
|
||||
makecontext(thread, coentry, 0);
|
||||
VALGRIND_STACK_REGISTER(thread->uc_stack.ss_sp, thread->uc_stack.ss_sp + heapsize);
|
||||
} else {
|
||||
co_delete((cothread_t)thread);
|
||||
thread = 0;
|
||||
}
|
||||
}
|
||||
return (cothread_t)thread;
|
||||
}
|
||||
|
||||
void co_delete(cothread_t cothread) {
|
||||
if(cothread) {
|
||||
if(((ucontext_t*)cothread)->uc_stack.ss_sp) { free(((ucontext_t*)cothread)->uc_stack.ss_sp); }
|
||||
free(cothread);
|
||||
}
|
||||
}
|
||||
|
||||
void co_switch(cothread_t cothread) {
|
||||
ucontext_t* old_thread = co_running;
|
||||
co_running = (ucontext_t*)cothread;
|
||||
swapcontext(old_thread, co_running);
|
||||
}
|
||||
|
||||
int co_serializable(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Vendored
+7165
File diff suppressed because it is too large
Load Diff
Vendored
+131
@@ -0,0 +1,131 @@
|
||||
#define LIBCO_C
|
||||
#include "libco.h"
|
||||
#include "settings.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#if defined(__clang__) || defined(__GNUC__)
|
||||
#define fastcall __attribute__((fastcall))
|
||||
#elif defined(_MSC_VER)
|
||||
#define fastcall __fastcall
|
||||
#else
|
||||
#error "libco: please define fastcall macro"
|
||||
#endif
|
||||
|
||||
static thread_local long co_active_buffer[64];
|
||||
static thread_local cothread_t co_active_handle = 0;
|
||||
static void (fastcall *co_swap)(cothread_t, cothread_t) = 0;
|
||||
|
||||
#ifdef LIBCO_MPROTECT
|
||||
alignas(4096)
|
||||
#else
|
||||
section(text)
|
||||
#endif
|
||||
/* ABI: fastcall */
|
||||
static const unsigned char co_swap_function[4096] = {
|
||||
0x89, 0x22, /* mov [edx],esp */
|
||||
0x8b, 0x21, /* mov esp,[ecx] */
|
||||
0x58, /* pop eax */
|
||||
0x89, 0x6a, 0x04, /* mov [edx+ 4],ebp */
|
||||
0x89, 0x72, 0x08, /* mov [edx+ 8],esi */
|
||||
0x89, 0x7a, 0x0c, /* mov [edx+12],edi */
|
||||
0x89, 0x5a, 0x10, /* mov [edx+16],ebx */
|
||||
0x8b, 0x69, 0x04, /* mov ebp,[ecx+ 4] */
|
||||
0x8b, 0x71, 0x08, /* mov esi,[ecx+ 8] */
|
||||
0x8b, 0x79, 0x0c, /* mov edi,[ecx+12] */
|
||||
0x8b, 0x59, 0x10, /* mov ebx,[ecx+16] */
|
||||
0xff, 0xe0, /* jmp eax */
|
||||
};
|
||||
|
||||
#ifdef _WIN32
|
||||
/* The macro logic below matches what valgrind.h is able to handle. Although
|
||||
* there's no Valgrind on Windows, it's possible to run a Windows exe on Linux
|
||||
* with Wine and Valgrind. See https://wiki.winehq.org/Wine_and_Valgrind. */
|
||||
#if defined(__GNUC__) || defined(_MSC_VER)
|
||||
#include "valgrind.h"
|
||||
#endif
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
static void co_init(void) {
|
||||
#ifdef LIBCO_MPROTECT
|
||||
DWORD old_privileges;
|
||||
VirtualProtect((void*)co_swap_function, sizeof co_swap_function, PAGE_EXECUTE_READ, &old_privileges);
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
#include "valgrind.h"
|
||||
#ifdef LIBCO_MPROTECT
|
||||
#include <unistd.h>
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
|
||||
static void co_init(void) {
|
||||
#ifdef LIBCO_MPROTECT
|
||||
unsigned long addr = (unsigned long)co_swap_function;
|
||||
unsigned long base = addr - (addr % sysconf(_SC_PAGESIZE));
|
||||
unsigned long size = (addr - base) + sizeof co_swap_function;
|
||||
mprotect((void*)base, size, PROT_READ | PROT_EXEC);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
static void crash(void) {
|
||||
LIBCO_ASSERT(0); /* called only if cothread_t entrypoint returns */
|
||||
}
|
||||
|
||||
cothread_t co_active(void) {
|
||||
if(!co_active_handle) co_active_handle = &co_active_buffer;
|
||||
return co_active_handle;
|
||||
}
|
||||
|
||||
cothread_t co_derive(void* memory, unsigned int size, void (*entrypoint)(void)) {
|
||||
cothread_t handle;
|
||||
if(!co_swap) {
|
||||
co_init();
|
||||
co_swap = (void (fastcall*)(cothread_t, cothread_t))co_swap_function;
|
||||
}
|
||||
if(!co_active_handle) co_active_handle = &co_active_buffer;
|
||||
|
||||
#if defined(__VALGRIND_MAJOR__)
|
||||
VALGRIND_STACK_REGISTER(memory, (char*)memory + size);
|
||||
#endif
|
||||
|
||||
if((handle = (cothread_t)memory)) {
|
||||
unsigned long stack_top = (unsigned long)handle + size;
|
||||
long *p;
|
||||
stack_top -= 32;
|
||||
stack_top &= ~((unsigned long) 15);
|
||||
p = (long*)(stack_top); /* seek to top of stack */
|
||||
*--p = (long)crash; /* crash if entrypoint returns */
|
||||
*--p = (long)entrypoint; /* start of function */
|
||||
*(long*)handle = (long)p; /* stack pointer */
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
cothread_t co_create(unsigned int size, void (*entrypoint)(void)) {
|
||||
void* memory = LIBCO_MALLOC(size);
|
||||
if(!memory) return (cothread_t)0;
|
||||
return co_derive(memory, size, entrypoint);
|
||||
}
|
||||
|
||||
void co_delete(cothread_t handle) {
|
||||
LIBCO_FREE(handle);
|
||||
}
|
||||
|
||||
void co_switch(cothread_t handle) {
|
||||
register cothread_t co_previous_handle = co_active_handle;
|
||||
co_swap(co_active_handle = handle, co_previous_handle);
|
||||
}
|
||||
|
||||
int co_serializable(void) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -22,7 +22,14 @@ internal static class AssemblyBlobWriter
|
||||
var expectedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var assembly = new StringBuilder();
|
||||
foreach (var header in headerLines) assembly.AppendLine(header);
|
||||
assembly.AppendLine(".section .rdata,\"dr\"");
|
||||
// PE/COFF (Windows) and ELF (Linux) spell a read-only data section differently in GNU-as
|
||||
// syntax - COFF section flags ("dr" = data, read-only) versus an ELF section needing an
|
||||
// allocatable-only flag plus an explicit @progbits type. The build always targets
|
||||
// whichever platform the translator itself runs on (there is no cross-compilation
|
||||
// support), so that's what this picks the section syntax from.
|
||||
assembly.AppendLine(OperatingSystem.IsWindows()
|
||||
? ".section .rdata,\"dr\""
|
||||
: ".section .rodata,\"a\",@progbits");
|
||||
assembly.AppendLine();
|
||||
|
||||
foreach (var blob in blobs)
|
||||
@@ -44,6 +51,16 @@ internal static class AssemblyBlobWriter
|
||||
{
|
||||
if (!expectedFiles.Contains(Path.GetFullPath(stalePath))) File.Delete(stalePath);
|
||||
}
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
// Absence of a .note.GNU-stack section makes the linker assume the oldest, most
|
||||
// conservative default for this object (an executable stack) and warn about it; this
|
||||
// file has no code needing one, so mark it explicitly like every other GNU-as ELF
|
||||
// object linked into the binary already does (the norm on modern toolchains, just not
|
||||
// producible without an explicit section since this file is hand-assembled, not
|
||||
// compiler-emitted).
|
||||
assembly.AppendLine(".section .note.GNU-stack,\"\",@progbits");
|
||||
}
|
||||
FileOutput.WriteTextIfChanged(assemblyPath, assembly.ToString());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user