Compare commits

...

9 Commits

Author SHA1 Message Date
patchzyy 6dbee74218 Add option to give folder 2026-10-16 16:40:32 +02:00
martipops bd219e38a3 fix: drive compile progress from ninja's [done/total] counter (#285)
The compile slice advanced a fixed step per line of build output, so how
far it moved depended on how many lines the build printed. When aurora and
its dependencies are compiled locally (as on macOS) the bar reached its cap
long before the game itself was compiled. Read ninja's edge counter instead.
2026-10-09 22:52:17 +02:00
therenow9 196b48528e Fix display lists dropping draws when the staging batch fills (#288)
* test: cover display lists that overflow the staging batch

GXCallDisplayList and GXCallDisplayListLE decode a list with a single
fifo::process() call. When a draw does not fit the staging batch,
process() stops at that draw, and the rest of the list is never drawn.

The test stubs can now admit N draws, refuse the next one as a full
batch would, and count the split that follows. The new test refuses
the middle or the last of three draws, in big- and little-endian
lists, and expects one split and the last draw's vertices.

Also stub aurora::window::set_force_aspect_16_9, called since #256,
so gx_fifo_tests links again.

Fails without the next commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: submit staging and continue when a display list overflows it

drain() already loops process() -> split_staging_batch() -> retry.
Move that loop into fifo::process_all() and use it for drain() and
both GXCallDisplayList variants, so a display list that fills the
staging batch submits it and draws the rest instead of dropping it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 22:48:24 +02:00
Michael G 54c608ef9c Add experimental x86-64-v2 CPU support for older PCs and Intel Macs (#258)
* runtime: add experimental x86-64-v2 profile

* fix review findings in network and translator

* build: include mbedtls in macos compile audit

* review fixes

* cmake: restore macOS deployment target floor before project()

* fix: Preserve explicit package versions and gate v3 tests on host support

* fix: Prevent TLS session reuse after failed writes

* fix: Gate cross-compiled profile tests on emulator availability

---------

Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
2026-10-09 22:46:11 +02:00
patchzyy f3df63b529 Update README.md 2026-10-09 10:51:22 +02:00
patchzyy dd6f596b45 Update to 0.2.34 2026-10-08 20:40:33 +02:00
Michael G 6fa24737d3 feat(macos): back guest flat memory with anonymous vm_remap aliases (#252)
Replace /tmp unlinked temporary files and file-backed mmap with anonymous host pages and Mach mach_vm_remap shared aliases, eliminating disk I/O, APFS churn, and SSD write amplification on macOS.

Adapted from KartPad (chrissotraidis/kartpad).
2026-10-05 23:47:14 +02:00
theofficialgman 279ce8328f Macos support into main (#228)
* macos: add x86_64 Intel support

* (feat) Apple Silicon macOS CI building and Setup.pkg documentation (#118)

* docs(macos): document Setup.pkg installation

Add Apple Silicon macOS CI coverage for runtime configuration, substrate tests, and Setup.pkg packaging alongside the macOS installation instructions.

* macos: pin Apple Silicon deployment target

* fix(macos): restore Retro Rewind local builds

* macos: package universal setup tools

* ci: build macOS input expression tests

* runtime: Do not force 14.0 minimum anymore

new minimum is 12.0

* macos: support older libc++ algorithms

* macos: allow undefined MTLLogStateDescriptor for older SDKs

* tests: deflake input expression timing window test

* build: Fix mac tests

* translator: emit null statement after continuation labels for C++17 compatibility

* feat(macos): add MetalFX spatial upscaling

* Fix automatic music muting on macOS

Co-Authored-By: Michael G <10155689+DarthMDev@users.noreply.github.com>
Co-Authored-By: Daan Vervacke <23398694+DaanVervacke@users.noreply.github.com>

* Address CodeRabbit review comments and integrate upstream TLS cmake

* Remove version requirement for running package CI

Co-Authored-By: Michael G <10155689+DarthMDev@users.noreply.github.com>

* publish-app: fix dependency discovery from build dir, handle spaces in paths, and fail on missing deps

* publish-app: preserve subdirectory suffix during @rpath dependency lookup

* cmake: synchronize CMAKE_SYSTEM_PROCESSOR with CMAKE_OSX_ARCHITECTURES on macOS

* fix rpaths

---------

Co-authored-by: DarthM <mgracer48@yahoo.com>
Co-authored-by: Michael G <10155689+DarthMDev@users.noreply.github.com>
Co-authored-by: Daan Vervacke <23398694+DaanVervacke@users.noreply.github.com>
Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
2026-10-03 11:07:15 +02:00
patchzyy 9d182f8316 Split PSQ fallbacks and add PSQ ISA tests (#278) 2026-10-02 10:34:51 +02:00
86 changed files with 3921 additions and 470 deletions
+64
View File
@@ -46,3 +46,67 @@ jobs:
- name: Test
run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal
platform_tests:
name: Host platform (${{ matrix.runner }})
strategy:
fail-fast: false
matrix:
runner: [ubuntu-24.04, macos-14, macos-15-intel]
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Configure without graphics dependencies or game assets
run: >-
cmake -S runtime -B build-platform -G Ninja
-DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
-DMKW_BUILD_PRODUCTS=OFF -DMKW_PLATFORM_TESTS_ONLY=ON
- name: Build and run host-platform tests
run: |
cmake --build build-platform --parallel 3
ctest --test-dir build-platform --output-on-failure
macos_substrate:
name: macOS arm64 (configure + substrate tests)
runs-on: macos-14
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Test macOS app dependency packaging
shell: bash
run: bash Launcher/macos/test-publish-app.command
- name: Configure native runtime
shell: bash
run: |
test "$(uname -m)" = arm64
cmake -S runtime -B build-macos -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DMKW_BUILD_PRODUCTS=OFF
grep -qx 'CMAKE_OSX_DEPLOYMENT_TARGET:STRING=12.0' \
build-macos/CMakeCache.txt
- name: Build macOS portability targets
shell: bash
run: |
cmake --build build-macos --target \
mkw_platform_paths_tests \
mkw_runtime_config_tests \
mkw_nand_save_tests \
mkw_nand_settings_tests \
mkw_sc_serial_tests \
mkw_input_expr_tests \
mkw_macos_native_compile \
mkw_macos_context_abi_tests \
mkw_macos_host_context_tests \
mkw_macos_guest_flat_memory_tests \
mkw_macos_external_audio_tests
- name: Test execution substrate
shell: bash
run: ctest --test-dir build-macos --output-on-failure
+136 -6
View File
@@ -1,8 +1,8 @@
name: Package installers
# Builds the per-platform installer/setup tool (WiiCompiled-Setup.exe /
# WiiCompiled-Setup-x86_64.AppImage) via Launcher/Build-Installer.ps1 and
# Launcher/build-appimage.sh respectively - the same scripts a maintainer runs by hand today to
# WiiCompiled-Setup-x86_64.AppImage / WiiCompiled-Setup.pkg) via the platform packaging scripts -
# the same scripts a maintainer runs by hand today to
# produce a GitHub Release asset. This does NOT build the actual translated game executable:
# that step requires the end user's own Mario Kart Wii dump (Assets/main.dol, Assets/StaticR.rel),
# which is proprietary and not present in this repository or in CI.
@@ -11,6 +11,11 @@ on:
tags:
- '*'
workflow_dispatch:
inputs:
version:
description: Package version
required: true
type: string
permissions:
contents: read
@@ -90,14 +95,139 @@ jobs:
if-no-files-found: error
archive: false
macos-setup-package:
name: macOS (universal Setup.pkg)
runs-on: macos-14
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '8.0.x'
- name: Verify Apple Silicon runner
shell: bash
run: |
test "$(uname -m)" = arm64
xcode-select -p
- name: Download pinned Nod tools
shell: bash
run: |
mkdir -p Launcher/artifacts/macos
nodtool_version=v2.0.0-alpha.10
nodtool_arm64_asset=nodtool-macos-arm64
nodtool_arm64_sha256=e23ca466999b720c55e6d29c9683fce8cc74451ba64ead2e543d50129f24528a
nodtool_x86_64_asset=nodtool-macos-x86_64
nodtool_x86_64_sha256=f68f504dc2b72694b468ca78b6a24142c7aa5c8800f77564297f4143682e6575
curl -fsSL --retry 3 \
"https://github.com/encounter/nod/releases/download/${nodtool_version}/${nodtool_arm64_asset}" \
-o Launcher/artifacts/macos/nodtool-arm64
curl -fsSL --retry 3 \
"https://github.com/encounter/nod/releases/download/${nodtool_version}/${nodtool_x86_64_asset}" \
-o Launcher/artifacts/macos/nodtool-x86_64
printf '%s %s\n' "$nodtool_arm64_sha256" Launcher/artifacts/macos/nodtool-arm64 | shasum -a 256 -c -
printf '%s %s\n' "$nodtool_x86_64_sha256" Launcher/artifacts/macos/nodtool-x86_64 | shasum -a 256 -c -
chmod +x Launcher/artifacts/macos/nodtool-arm64 Launcher/artifacts/macos/nodtool-x86_64
- name: Publish self-contained Translator tools
shell: bash
run: |
dotnet publish translator/src/Translator.Cli/Translator.Cli.csproj \
-c Release -r osx-arm64 --self-contained true \
-p:PublishSingleFile=true \
-o Launcher/artifacts/macos/translator-arm64
dotnet publish translator/src/Translator.Cli/Translator.Cli.csproj \
-c Release -r osx-x64 --self-contained true \
-p:PublishSingleFile=true \
-o Launcher/artifacts/macos/translator-x86_64
- name: Download pinned universal Ninja
shell: bash
run: |
ninja_version=1.13.2
ninja_sha256=c99048673aa765960a99cf10c6ddb9f1fad506099ff0a0e137ad8960a88f321b
curl -fsSL --retry 3 "https://github.com/ninja-build/ninja/releases/download/v${ninja_version}/ninja-mac.zip" -o ninja-mac.zip
printf '%s %s\n' "$ninja_sha256" ninja-mac.zip | shasum -a 256 -c -
unzip -q ninja-mac.zip -d Launcher/artifacts/macos/ninja
chmod +x Launcher/artifacts/macos/ninja/ninja
- name: Download pinned portable CMake
shell: bash
run: |
cmake_version=4.4.3
archive="cmake-${cmake_version}-macos-universal.tar.gz"
base_url="https://github.com/Kitware/CMake/releases/download/v${cmake_version}"
expected_sha256=0c5d65251c14cc884bfa16bdbed3c263ce5bffe2e21c0d0d00962cb0610464fa
curl -fsSL --retry 3 "$base_url/$archive" -o "$archive"
printf '%s %s\n' "$expected_sha256" "$archive" | shasum -a 256 -c -
tar -xzf "$archive"
mv "cmake-${cmake_version}-macos-universal/CMake.app/Contents" Launcher/artifacts/macos/cmake
- name: Build Setup.pkg
env:
PACKAGE_VERSION: ${{ inputs.version }}
TAG_VERSION: ${{ github.ref_name }}
shell: bash
run: |
package_version="$PACKAGE_VERSION"
if [[ -z "$package_version" ]]; then
if [[ "${TAG_VERSION:-}" =~ ^v?[0-9] ]]; then
package_version="${TAG_VERSION#v}"
elif [[ -f "Launcher/Directory.Build.props" ]]; then
package_version=$(grep -m1 '<Version>' Launcher/Directory.Build.props | sed -E 's/.*<Version>([^<]+)<\/Version>.*/\1/')
fi
fi
mkdir -p Launcher/dist
Launcher/macos/build-setup-pkg.command \
--nodtool-arm64 Launcher/artifacts/macos/nodtool-arm64 \
--nodtool-x86_64 Launcher/artifacts/macos/nodtool-x86_64 \
--translator-arm64 Launcher/artifacts/macos/translator-arm64/Translator.Cli \
--translator-x86_64 Launcher/artifacts/macos/translator-x86_64/Translator.Cli \
--cmake-root Launcher/artifacts/macos/cmake \
--ninja-arm64 Launcher/artifacts/macos/ninja/ninja \
--ninja-x86_64 Launcher/artifacts/macos/ninja/ninja \
--output Launcher/dist/WiiCompiled-Setup.pkg \
${package_version:+--version "$package_version"}
- name: Verify package layout and architecture-specific tools
shell: bash
run: |
pkgutil --check-signature Launcher/dist/WiiCompiled-Setup.pkg || true
if pkgutil --payload-files Launcher/dist/WiiCompiled-Setup.pkg | \
grep -E '/(Assets|generated|PulsarPacks|WiiCompiled.app|RetroRewind.app)(/|$)'; then
echo "::error::Setup.pkg contains a forbidden payload"
exit 1
fi
expanded="$RUNNER_TEMP/wiicompiled-setup-expanded"
pkgutil --expand-full Launcher/dist/WiiCompiled-Setup.pkg "$expanded"
resources="$expanded/Payload/Applications/WiiCompiled Setup.app/Contents/Resources"
for arch in arm64 x86_64; do
for tool in nodtool Translator.Cli ninja; do
lipo "$resources/tools/$arch/$tool" -verify_arch "$arch"
done
done
lipo "$resources/tools/cmake/bin/cmake" -verify_arch arm64 x86_64
bash "$resources/setup.command" --help
/usr/bin/arch -x86_64 /bin/bash "$resources/setup.command" --help
- uses: actions/upload-artifact@v7
with:
name: WiiCompiled-Setup-macos-universal
path: Launcher/dist/WiiCompiled-Setup.pkg
if-no-files-found: error
archive: false
# Publishes the packaged installers as a GitHub Release whenever a v* tag is pushed. Wheel Wizard
# discovers updates from these releases, so the contract it relies on is enforced here: a full
# (non-prerelease) release whose tag is v<semver>, carrying an asset named exactly
# WiiCompiled-Setup.exe, produced by a setup host that reports that same version.
# (non-prerelease) release whose tag is v<semver>, carrying the expected platform assets,
# produced by setup hosts that report that same version.
release:
name: Publish GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
needs: [linux-appimage, windows-installer, recompilation]
needs: [linux-appimage, windows-installer, macos-setup-package, recompilation]
runs-on: ubuntu-latest
permissions:
contents: write
@@ -141,7 +271,7 @@ jobs:
set -euo pipefail
ls -lR artifacts
assets=()
for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage; do
for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage WiiCompiled-Setup.pkg; do
found="$(find artifacts -type f -name "$name" | head -n 1)"
[ -n "$found" ] && [ -s "$found" ] || { echo "::error::missing release asset $name"; exit 1; }
assets+=("$found")
+14
View File
@@ -45,6 +45,20 @@ jobs:
shell: pwsh
run: ./Launcher/Prepare-PortableTools.ps1
- name: Build and run Windows host-platform tests
shell: pwsh
run: |
$tools = Join-Path $PWD 'Launcher/artifacts/portable-tools'
$env:PATH = "$tools/llvm-mingw/bin;$tools/CMake/bin;$tools/Ninja;$env:PATH"
cmake -S runtime -B build-platform -G Ninja `
-DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ `
-DMKW_BUILD_PRODUCTS=OFF -DMKW_PLATFORM_TESTS_ONLY=ON
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
cmake --build build-platform --parallel 3
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
ctest --test-dir build-platform --output-on-failure
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Prepare pinned native dependencies
shell: pwsh
run: ./Launcher/Prepare-Dependencies.ps1
+1 -1
View File
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<!-- The one product version. Release tags must match it. -->
<Version>0.2.33</Version>
<Version>0.2.34</Version>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
</Project>
@@ -0,0 +1,56 @@
using System.Text;
namespace WiiCompiled.Setup.Common;
/// <summary>The disc identity read from an extracted game's <c>sys/boot.bin</c>.</summary>
public sealed record ExtractedGameHeader(string GameId, int Revision, string InternalName);
/// <summary>
/// A game Wheel Wizard already extracted, in the layout Dolphin's "Extract Entire Disc" writes:
/// <c>&lt;root&gt;/DATA/{sys,files}</c>. WiiCompiled reads it in place (<c>dvd_root</c>) instead of extracting a
/// copy of its own, so it is only ever read, never written or deleted.
/// </summary>
public static class ExtractedGameDirectory
{
private const int BootSize = 0x440;
private const uint WiiMagic = 0x5D1C9EA3;
/// <summary>
/// The <c>DATA</c> folder of an extracted game: <paramref name="path"/> may be the game root holding
/// <c>DATA/</c>, or the <c>DATA</c> folder itself.
/// </summary>
public static string ResolveDataDirectory(string path)
{
if (string.IsNullOrWhiteSpace(path))
throw new ArgumentException("--game-dir needs the folder of the extracted game.");
var full = FileSystemUtilities.NormalizePath(path);
if (!Directory.Exists(full))
throw new DirectoryNotFoundException($"The extracted game folder does not exist: {full}");
foreach (var candidate in new[] { Path.Combine(full, "DATA"), full })
{
if (File.Exists(Path.Combine(candidate, "sys", "main.dol")) && File.Exists(Path.Combine(candidate, "sys", "boot.bin")))
return FileSystemUtilities.NormalizePath(candidate);
}
throw new InvalidDataException(
$"{full} is not an extracted Wii game: expected DATA/sys/main.dol and DATA/sys/boot.bin.");
}
/// <summary>Reads the disc header from <c>sys/boot.bin</c>, which starts with the same bytes as the disc.</summary>
public static ExtractedGameHeader ReadHeader(string dataDirectory)
{
var boot = File.ReadAllBytes(Path.Combine(dataDirectory, "sys", "boot.bin"));
if (boot.Length < BootSize)
throw new InvalidDataException("The extracted game's sys/boot.bin is incomplete.");
var magic = (uint)(boot[0x18] << 24 | boot[0x19] << 16 | boot[0x1A] << 8 | boot[0x1B]);
if (magic != WiiMagic)
throw new InvalidDataException("The extracted game is not a Wii game.");
var gameId = Encoding.ASCII.GetString(boot, 0, 6).TrimEnd('\0', ' ');
var nameBytes = boot.AsSpan(0x20, 0x40);
var end = nameBytes.IndexOf((byte)0);
var name = Encoding.ASCII.GetString(end < 0 ? nameBytes : nameBytes[..end]).Trim();
return new ExtractedGameHeader(gameId, boot[7], name);
}
}
@@ -64,6 +64,54 @@ internal static class DiscTool
reporter.Progress(InstallStages.ExtractDisc, "Disc validated and extracted", 6);
}
/// <summary>
/// Checks a game Wheel Wizard already extracted against the pinned clean disc and copies only its two code
/// files into <paramref name="assetsDirectory"/> for translation. The game itself stays where it is; the
/// runtime reads it in place. Returns its <c>DATA</c> folder.
/// </summary>
public static string ValidateExtracted(string gameDirectory, ProjectManifest manifest, string assetsDirectory,
IInstallReporter reporter)
{
reporter.Progress(InstallStages.ExtractDisc, "Checking the extracted game", 2);
var dataDir = ExtractedGameDirectory.ResolveDataDirectory(gameDirectory);
var header = ExtractedGameDirectory.ReadHeader(dataDir);
if (!string.Equals(header.GameId, manifest.GameId, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"This game is '{header.GameId}', not the expected '{manifest.GameId}' (Mario Kart Wii, region {manifest.Region}).");
}
var dolPath = Path.Combine(dataDir, "sys", "main.dol");
var relPath = Path.Combine(dataDir, "files", "rel", "StaticR.rel");
if (!File.Exists(relPath)) throw new FileNotFoundException("The extracted game has no StaticR.rel", relPath);
if (!string.Equals(Sha256Of(dolPath), manifest.DolSha256, StringComparison.Ordinal) ||
!string.Equals(Sha256Of(relPath), manifest.RelSha256, StringComparison.Ordinal))
{
throw new InvalidOperationException(
"The extracted game's main.dol or StaticR.rel is not the clean game code this project is pinned to.");
}
Directory.CreateDirectory(assetsDirectory);
File.Copy(dolPath, Path.Combine(assetsDirectory, "main.dol"), overwrite: true);
File.Copy(relPath, Path.Combine(assetsDirectory, "StaticR.rel"), overwrite: true);
reporter.Progress(InstallStages.ExtractDisc, "Extracted game validated", 6);
return dataDir;
}
/// <summary>Removes the workspace's own extraction once the extracted game is read in place instead.</summary>
public static void RemoveOwnExtraction(string assetsDirectory, IInstallReporter reporter)
{
var own = Path.Combine(assetsDirectory, "DATA");
try
{
if (Directory.Exists(own)) Directory.Delete(own, recursive: true);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
reporter.Diagnostic("The game copy WiiCompiled no longer uses could not be removed yet: " + ex.Message);
}
}
private static async Task<string> RunInfoAsync(string nodTool, string isoPath, CancellationToken cancellationToken)
{
var startInfo = new System.Diagnostics.ProcessStartInfo(nodTool)
@@ -184,18 +184,31 @@ internal sealed class BuildProgressWindow
}
// Anything else - a plain MKWCBUILD note, or raw tool output - stays a diagnostic and only
// feeds the heartbeat below.
// feeds the compile progress below.
_reporter.Diagnostic(line);
// Compilation announces itself once and then emits thousands of compiler lines. Treat that
// output as a heartbeat so the slice keeps creeping forward, but only publish a progress
// line when the rounded percentage actually changes.
if (_fraction >= CompileFraction)
// Compilation announces itself once and then ninja prefixes every edge with [done/total].
// Follow that counter rather than counting lines: how many lines a build prints depends on
// whether aurora and its dependencies are prebuilt (AppImage) or compiled here (macOS), so a
// fixed per-line step reached the top long before the game itself compiled.
if (_fraction >= CompileFraction && TryParseNinjaProgress(line, out var done, out var total))
{
_fraction = Math.Min(0.97, _fraction + 0.0015);
_fraction = Math.Max(_fraction, CompileFraction + (0.97 - CompileFraction) * done / total);
Emit();
}
}
private static bool TryParseNinjaProgress(string line, out int done, out int total)
{
done = total = 0;
if (!line.StartsWith('[')) return false;
var close = line.IndexOf(']');
var slash = line.IndexOf('/');
return close > 0 && slash > 0 && slash < close
&& int.TryParse(line.AsSpan(1, slash - 1), out done)
&& int.TryParse(line.AsSpan(slash + 1, close - slash - 1), out total)
&& total > 0 && done <= total;
}
private void Emit()
{
var percent = Interpolate(_fraction);
@@ -32,4 +32,10 @@ internal sealed class InstallState
public int SchemaVersion { get; set; } = 1;
public string Workspace { get; set; } = "";
public List<ProductInstallRecord> Products { get; set; } = new();
/// <summary>
/// The <c>DATA</c> folder of a game Wheel Wizard already extracted (<c>--game-dir</c>), read in place through
/// <c>dvd_root</c>. Empty when the game was extracted into the workspace's own <c>Assets/DATA</c>.
/// </summary>
public string GameDataDir { get; set; } = "";
}
+65 -4
View File
@@ -11,6 +11,13 @@ internal static class Program
// --workspace <cache> ahead of whatever the caller passed, so these can't assume position 0.
if (args.Length == 0 || args.Contains("-h") || args.Contains("--help")) { PrintUsage(); return 0; }
if (args.Contains("--version")) { Console.WriteLine(ProductInfo.Version); return 0; }
// Lets Wheel Wizard tell what this AppImage supports before it asks; an older one rejects the flag.
if (args.Contains("--capabilities"))
{
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(
new { type = "capabilities", capabilities = new[] { "game-dir" } }));
return 0;
}
using var cts = new CancellationTokenSource();
// Replaces CancellationSignal.cs's named-EventWaitHandle IPC (Windows-only): SIGINT/SIGTERM
@@ -50,6 +57,9 @@ internal static class Program
case "check-products":
CheckProducts();
break;
case "link-game-dir":
LinkGameDirectory(flags, reporter);
break;
default:
Console.Error.WriteLine($"Unknown command: {command}");
PrintUsage();
@@ -99,10 +109,23 @@ internal static class Program
var assetsDir = Path.Combine(workspace, "Assets");
reporter.Progress(InstallStages.Validate, "Checking prerequisites", 1);
if (flags.TryGetValue("game", out var isoPath) && !string.IsNullOrEmpty(isoPath))
var gameDirectory = flags.GetValueOrDefault("game-dir");
flags.TryGetValue("game", out var isoPath);
if (!string.IsNullOrEmpty(isoPath) && !string.IsNullOrEmpty(gameDirectory))
throw new ArgumentException("Pass either --game or --game-dir, not both.");
var previousState = JsonState.TryRead<InstallState>(StatePath);
// Where the runtime reads the game: an extracted game it was given or already linked, else its own copy.
var gameDataDir = previousState?.GameDataDir ?? "";
if (!string.IsNullOrEmpty(gameDirectory))
{
gameDataDir = DiscTool.ValidateExtracted(gameDirectory, manifest, assetsDir, reporter);
}
else if (!string.IsNullOrEmpty(isoPath))
{
await DiscTool.ValidateAndExtractAsync(isoPath, manifest, assetsDir, workspace,
flags.GetValueOrDefault("disc-tool-bin"), reporter, token);
gameDataDir = "";
}
else
{
@@ -116,8 +139,9 @@ internal static class Program
}
}
var state = JsonState.TryRead<InstallState>(StatePath) ?? new InstallState { Workspace = workspace };
var state = previousState ?? new InstallState { Workspace = workspace };
state.Workspace = workspace;
state.GameDataDir = gameDataDir;
var profile = installsRetro ? "both" : "base";
var profiles = installsRetro ? new[] { "base", "retro-rewind" } : new[] { "base" };
@@ -218,11 +242,13 @@ internal static class Program
// per-user Config.toml (RuntimeConfiguration.ResolveConfigPath's Windows-only portable-root
// lookup has nothing to find here either way).
var configPath = RuntimeConfiguration.ApplicationDataConfigPath;
var dataDir = Path.Combine(assetsDir, "DATA");
var dataDir = string.IsNullOrEmpty(gameDataDir) ? Path.Combine(assetsDir, "DATA") : gameDataDir;
if (Directory.Exists(dataDir))
{
RuntimeConfiguration.SetDvdRoot(configPath, dataDir);
}
if (!string.IsNullOrEmpty(gameDataDir))
DiscTool.RemoveOwnExtraction(assetsDir, reporter);
if (installsRetro)
{
RuntimeConfiguration.SetRetroRewindRoot(configPath, retroDir!);
@@ -231,6 +257,39 @@ internal static class Program
reporter.Progress(InstallStages.Shortcuts, "Install complete", 99);
}
/// <summary>
/// Points the installation at a game Wheel Wizard already extracted, without rebuilding: the game code is
/// checked to be exactly what the installed products were built from, then <c>dvd_root</c> and the state name
/// it and the workspace's own copy is removed.
/// </summary>
private static void LinkGameDirectory(Dictionary<string, string?> flags, IInstallReporter reporter)
{
var gameDirectory = flags.GetValueOrDefault("game-dir");
if (string.IsNullOrEmpty(gameDirectory))
throw new ArgumentException("link-game-dir needs --game-dir <folder>.");
var state = JsonState.TryRead<InstallState>(StatePath);
if (state is null || state.Products.Count == 0)
throw new InvalidOperationException("WiiCompiled is not installed yet.");
var workspace = flags.GetValueOrDefault("workspace") ??
(string.IsNullOrEmpty(state.Workspace) ? WorkspaceLocator.FindFrom(AppContext.BaseDirectory) : state.Workspace);
var manifest = ProjectManifest.Load(Path.Combine(workspace, "projects", "mkwii", "recomp.yml"));
var assetsDir = Path.Combine(workspace, "Assets");
var dataDir = DiscTool.ValidateExtracted(gameDirectory, manifest, assetsDir, reporter);
var dolSha = Sha256Of(Path.Combine(dataDir, "sys", "main.dol"));
var relSha = Sha256Of(Path.Combine(dataDir, "files", "rel", "StaticR.rel"));
if (state.Products.Any(record => record.DolSha256 != dolSha || record.RelSha256 != relSha))
throw new InvalidOperationException(
"The installed products were built from different game code; install again with --game-dir instead.");
reporter.Progress(InstallStages.Shortcuts, "Pointing WiiCompiled at the extracted game", 70);
RuntimeConfiguration.SetDvdRoot(RuntimeConfiguration.ApplicationDataConfigPath, dataDir);
state.GameDataDir = dataDir;
JsonState.Write(StatePath, state);
DiscTool.RemoveOwnExtraction(assetsDir, reporter);
}
private static void Uninstall()
{
// Matches Windows: UninstallService.cs removes the whole install directory unconditionally -
@@ -351,7 +410,7 @@ internal static class Program
Console.WriteLine("""
Usage: wiicompiled-setup <command> [options]
install [--game ISO_PATH] [--install-dir DIR] [--retro-dir DIR
install [--game ISO_PATH | --game-dir EXTRACTED_GAME] [--install-dir DIR] [--retro-dir DIR
{--download-retro-wfc-payload | --skip-retro-wfc-payload}]
[--force-clean-build] [--translator-bin PATH] [--disc-tool-bin PATH]
[--cc PATH] [--cxx PATH] [--fuse-ld NAME_OR_PATH] [--cmake PATH] [--ninja PATH]
@@ -360,7 +419,9 @@ internal static class Program
launch-base
launch-retro
check-products
link-game-dir --game-dir EXTRACTED_GAME [--progress-json]
--version
--capabilities
""");
}
}
@@ -14,7 +14,9 @@ internal enum AppMode
CheckProducts,
RepairProducts,
Version,
EmitPayloadIdentities
EmitPayloadIdentities,
Capabilities,
LinkGameDirectory
}
/// <summary>
@@ -26,7 +28,12 @@ internal sealed record ModeRules
public string Flag { get; init; } = "this command";
public bool AcceptsProgressJson { get; init; }
public bool AcceptsGame { get; init; }
/// <summary>The game is required, as either <c>--game &lt;image&gt;</c> or <c>--game-dir &lt;folder&gt;</c>.</summary>
public bool RequiresGame { get; init; }
public bool AcceptsGameDirectory { get; init; }
public bool RequiresGameDirectory { get; init; }
public bool AcceptsRetroDirectory { get; init; }
public bool RequiresRetroDirectory { get; init; }
public bool AcceptsPayloadMode { get; init; }
@@ -39,6 +46,7 @@ internal sealed class CommandLine
{
public AppMode Mode { get; private set; } = AppMode.Help;
public string? GamePath { get; private set; }
public string? GameDirectoryPath { get; private set; }
public string? RetroDirectoryPath { get; private set; }
public RetroWfcPayloadMode RetroWfcPayloadMode { get; private set; }
public string? InstallDirectory { get; private set; }
@@ -70,6 +78,9 @@ internal sealed class CommandLine
case "--repair-products": result.Mode = AppMode.RepairProducts; break;
case "--portable": result.Portable = true; break;
case "--emit-payload-identities": result.Mode = AppMode.EmitPayloadIdentities; break;
case "--capabilities": result.Mode = AppMode.Capabilities; break;
case "--link-game-dir": result.Mode = AppMode.LinkGameDirectory; break;
case "--game-dir": result.GameDirectoryPath = RequireValue(args, ref i); break;
case "--payload-root": result.PayloadRootPath = RequireValue(args, ref i); break;
case "--quiet": result.Quiet = true; break;
case "--progress-json": result.ProgressJson = true; break;
@@ -100,16 +111,22 @@ internal sealed class CommandLine
[AppMode.SilentInstall] = new ModeRules
{
Flag = "--silent",
AcceptsProgressJson = true, AcceptsGame = true, RequiresGame = true,
AcceptsProgressJson = true, AcceptsGame = true, AcceptsGameDirectory = true, RequiresGame = true,
AcceptsRetroDirectory = true, AcceptsPayloadMode = true,
AcceptsPortable = true
},
[AppMode.VerifyInputs] = new ModeRules
{
Flag = "--verify-inputs",
AcceptsProgressJson = true, AcceptsGame = true, RequiresGame = true,
AcceptsProgressJson = true, AcceptsGame = true, AcceptsGameDirectory = true, RequiresGame = true,
AcceptsRetroDirectory = true
},
[AppMode.LinkGameDirectory] = new ModeRules
{
Flag = "--link-game-dir",
AcceptsProgressJson = true, AcceptsGameDirectory = true, RequiresGameDirectory = true,
RequiresInstallDirectory = true
},
[AppMode.CheckProducts] = new ModeRules
{
Flag = "--check-products", AcceptsProgressJson = true, AcceptsRetroDirectory = true
@@ -147,6 +164,9 @@ internal sealed class CommandLine
}
Reject(GamePath is not null && !rules.AcceptsGame, "--game");
Reject(GameDirectoryPath is not null && !rules.AcceptsGameDirectory, "--game-dir");
if (GamePath is not null && GameDirectoryPath is not null)
throw new ArgumentException("Pass either --game or --game-dir, not both.");
Reject(RetroDirectoryPath is not null && !rules.AcceptsRetroDirectory, "--retro-dir");
Reject(ProgressJson && !rules.AcceptsProgressJson, "--progress-json");
Reject(PayloadRootPath is not null && !rules.RequiresPayloadRoot, "--payload-root");
@@ -154,8 +174,10 @@ internal sealed class CommandLine
Reject(RetroWfcPayloadMode != RetroWfcPayloadMode.NotApplicable && !rules.AcceptsPayloadMode,
"A Retro-WFC payload option");
if (rules.RequiresGame && string.IsNullOrWhiteSpace(GamePath))
throw new ArgumentException("--game is required.");
if (rules.RequiresGame && string.IsNullOrWhiteSpace(GamePath) && string.IsNullOrWhiteSpace(GameDirectoryPath))
throw new ArgumentException("--game or --game-dir is required.");
if (rules.RequiresGameDirectory && string.IsNullOrWhiteSpace(GameDirectoryPath))
throw new ArgumentException($"--game-dir is required with {rules.Flag}.");
if (rules.RequiresRetroDirectory && string.IsNullOrWhiteSpace(RetroDirectoryPath))
throw new ArgumentException(
$"--retro-dir is required with {rules.Flag}. Supply Wheel Wizard's canonical Retro Rewind folder.");
@@ -11,8 +11,10 @@ internal static class ConsoleCommands
Console.Out.WriteLine("Wheel Wizard is the graphical interface for installing and launching WiiCompiled.");
Console.Out.WriteLine();
Console.Out.WriteLine("Commands:");
Console.Out.WriteLine(" --silent --game <image> --install-dir <dir> [--retro-dir <folder>] [--portable]");
Console.Out.WriteLine(" --verify-inputs --game <image> [--retro-dir <folder>]");
Console.Out.WriteLine(" --silent (--game <image> | --game-dir <folder>) --install-dir <dir> [--retro-dir <folder>] [--portable]");
Console.Out.WriteLine(" --verify-inputs (--game <image> | --game-dir <folder>) [--retro-dir <folder>]");
Console.Out.WriteLine(" --link-game-dir --install-dir <dir> --game-dir <folder> [--progress-json]");
Console.Out.WriteLine(" --capabilities");
Console.Out.WriteLine(" --check-products [--install-dir <dir>] [--retro-dir <folder>] [--progress-json]");
Console.Out.WriteLine(" --repair-products --install-dir <dir> --retro-dir <folder> " +
"(--download-retro-wfc-payload | --skip-retro-wfc-payload) [--progress-json]");
@@ -28,6 +30,80 @@ internal static class ConsoleCommands
return 0;
}
/// <summary>
/// What this setup supports, so Wheel Wizard can tell before it asks. A setup from before this command rejects
/// it as unknown, which the caller reads as supporting nothing new.
/// </summary>
public static int Capabilities()
{
Console.Out.WriteLine(JsonSerializer.Serialize(new { type = "capabilities", capabilities = new[] { "game-dir" } }));
return 0;
}
/// <summary>
/// Points an existing installation at a game Wheel Wizard already extracted: <c>dvd_root</c> and the install
/// state name it, and the installation's own extracted copy is removed. Nothing is rebuilt, since the game
/// code it was built from is checked to be the same.
/// </summary>
public static int LinkGameDirectory(CommandLine command)
{
var reporter = command.ProgressJson ? new NdjsonInstallReporter() : null;
var installation = new Installation(Path.GetFullPath(command.InstallDirectory!));
try
{
if (!installation.IsPresent)
throw new InvalidOperationException($"WiiCompiled is not installed at {installation.Root}.");
using var operationLock = InstallOperationLock.Acquire(installation.Root, reporter);
PortableInstallHealing.HealMovedInstall(installation, reporter);
var state = installation.ReadInstallState() is { SchemaVersion: 1 } current
? current
: throw new InvalidDataException("The installation has no current install state. Apply setup again.");
reporter?.Progress(InstallStages.Validate, "Checking the extracted game...", 20);
var dataDirectory = ExtractedGameDirectory.ResolveDataDirectory(command.GameDirectoryPath!);
var header = InputValidation.ReadDiscHeaderFromDirectory(dataDirectory);
var installedManifest = JsonState.TryRead<PayloadManifest>(
Path.Combine(installation.Root, InstalledLayout.PayloadManifestFileName));
if (installedManifest is not null)
InputValidation.EnsureCompatibleDisc(header, installedManifest);
// The products were built from exactly this game code; anything else needs a rebuild, not a link.
foreach (var (file, expected) in new[]
{
(Path.Combine(dataDirectory, "sys", "main.dol"), state.DolSha256),
(Path.Combine(dataDirectory, "files", "rel", "StaticR.rel"), state.RelSha256),
})
{
if (!File.Exists(file) ||
!InputValidation.Sha256File(file).Equals(expected, StringComparison.OrdinalIgnoreCase))
throw new InvalidDataException(
$"{Path.GetFileName(file)} in the extracted game is not the clean game code WiiCompiled was built from.");
}
RunningProductGuard.EnsureProductsNotRunning(installation.Root);
reporter?.Progress(InstallStages.Publish, "Pointing WiiCompiled at the extracted game...", 70);
RuntimeConfiguration.SetDvdRoot(installation.ConfigPath, dataDirectory);
state.GameDataDir = dataDirectory;
JsonState.Write(installation.InstallStatePath, state);
InstallerEngine.RemoveOwnGameAssets(installation, reporter ?? (IInstallReporter)new ConsoleInstallReporter(_ => { }));
if (reporter is null)
Console.Out.WriteLine($"WiiCompiled now reads the game from {dataDirectory}.");
reporter?.Success(installation.Root);
return 0;
}
catch (Exception ex) when (reporter is not null)
{
reporter.Failure(ex.Message);
Console.Error.WriteLine(ex);
return 1;
}
finally
{
reporter?.EnsureFinished("Linking the extracted game stopped before it reached a result.");
}
}
public static int EmitPayloadIdentities(CommandLine command)
{
@@ -68,7 +144,10 @@ internal static class ConsoleCommands
payload.ExtractEntry(InstalledLayout.ToolkitEntryPrefix + "nodtool.exe", tool);
payload.ExtractDirectory(InstalledLayout.ToolkitEntryPrefix + "Redist", temp);
reporter?.Progress(InstallStages.Validate, "Checking the Wii disc image...", 10);
var header = InputValidation.ReadDiscHeaderAsync(tool, command.GamePath!).GetAwaiter().GetResult();
var header = command.GameDirectoryPath is null
? InputValidation.ReadDiscHeaderAsync(tool, command.GamePath!).GetAwaiter().GetResult()
: InputValidation.ReadDiscHeaderFromDirectory(
ExtractedGameDirectory.ResolveDataDirectory(command.GameDirectoryPath));
InputValidation.EnsureCompatibleDisc(header, manifest);
if (command.RetroDirectoryPath is not null)
@@ -115,7 +194,8 @@ internal static class ConsoleCommands
var engine = new InstallerEngine(reporter);
await engine.InstallAsync(new InstallOptions
{
GamePath = command.GamePath!,
GamePath = command.GamePath,
GameDirectoryPath = command.GameDirectoryPath,
RetroDirectoryPath = command.RetroDirectoryPath,
RetroWfcPayloadMode = command.RetroWfcPayloadMode,
InstallDirectory = installDirectory,
@@ -42,6 +42,19 @@ internal static class InputValidation
};
}
/// <summary>The disc header of a game Wheel Wizard already extracted, read from its <c>sys/boot.bin</c>.</summary>
public static DiscHeader ReadDiscHeaderFromDirectory(string dataDirectory)
{
var header = ExtractedGameDirectory.ReadHeader(dataDirectory);
return new DiscHeader
{
GameId = header.GameId,
InternalName = header.InternalName,
Region = RegionFromGameId(header.GameId),
Revision = header.Revision,
};
}
private static string RegionFromGameId(string gameId) => gameId.Length >= 4
? gameId[3] switch
{
@@ -99,7 +99,17 @@ internal sealed class Installation
public string BaseExecutable => Path.Combine(BaseDirectory, "WiiCompiled.exe");
public string RetroDirectory => Path.Combine(Root, "RetroRewind");
public string RetroExecutable => Path.Combine(RetroDirectory, "RetroRewind.exe");
public string GameDataDirectory => Path.Combine(Root, "GameAssets", "DATA");
/// <summary>
/// Where the game data is read from: Wheel Wizard's extracted game when the installation was pointed at it,
/// otherwise the extraction this installation keeps itself.
/// </summary>
public string GameDataDirectory =>
ReadInstallState() is { GameDataDir.Length: > 0 } state ? state.GameDataDir : OwnGameDataDirectory;
/// <summary>The installation's own extraction, which only an installation given a disc image has.</summary>
public string OwnGameDataDirectory => Path.Combine(Root, "GameAssets", "DATA");
public string OwnGameAssetsDirectory => Path.Combine(Root, "GameAssets");
public string InstallStatePath => Path.Combine(Root, InstalledLayout.InstallStateFileName);
public string ToolkitStatePath => Path.Combine(Root, ToolkitState.FileName);
@@ -13,7 +13,12 @@ internal sealed class InstallerEngine
var installDirectory = Path.GetFullPath(options.InstallDirectory);
ValidateInstallDirectory(installDirectory);
InputValidation.ValidateExtension(options.GamePath);
// A game Wheel Wizard already extracted is read in place; only a disc image is extracted here.
var externalGameData = options.GameDirectoryPath is null
? null
: ExtractedGameDirectory.ResolveDataDirectory(options.GameDirectoryPath);
if (externalGameData is null)
InputValidation.ValidateExtension(options.GamePath ?? throw new ArgumentException("A game is required."));
var parent = Directory.GetParent(installDirectory)?.FullName
?? throw new InvalidOperationException("The installation directory has no parent.");
Directory.CreateDirectory(parent);
@@ -49,7 +54,8 @@ internal sealed class InstallerEngine
var installedToolkitPackageFingerprint = existing.IsPresent
? existing.ResolveToolkitPackageFingerprint()
: "";
var reusableGameAssets = FindReusableGameAssets(existing, manifest);
var reusableGameAssets = externalGameData ?? FindReusableGameAssets(existing, manifest);
var gameDataDir = GameDataDirFor(externalGameData, reusableGameAssets, previousState);
var sameToolkit = existing.IsPresent && reusableGameAssets is not null &&
installedToolkitFingerprint.Equals(candidateToolkitFingerprint,
@@ -84,9 +90,12 @@ internal sealed class InstallerEngine
var nodTool = extractToolkit ? Path.Combine(toolkit, "nodtool.exe") : installedNodTool;
_reporter.Progress(InstallStages.Validate, "Checking the Wii disc image...", 2);
var header = await InputValidation.ReadDiscHeaderAsync(nodTool, options.GamePath,
cancellationToken);
var header = externalGameData is null
? await InputValidation.ReadDiscHeaderAsync(nodTool, options.GamePath!, cancellationToken)
: InputValidation.ReadDiscHeaderFromDirectory(externalGameData);
InputValidation.EnsureCompatibleDisc(header, manifest);
if (externalGameData is not null)
ValidateExtractedGame(externalGameData, manifest);
var canonicalRetroRoot = options.RetroDirectoryPath is null
? null
: RetroRewindSource.ResolveRetroRewind6(options.RetroDirectoryPath);
@@ -130,6 +139,7 @@ internal sealed class InstallerEngine
var updatedState = BuildInstallState(existing.ReadInstallState(), installDirectory, manifest,
retroCompileInputs, canonicalRetroRoot, options.RetroWfcPayloadMode);
updatedState.GameDataDir = gameDataDir;
PrepareMetadata(staging, manifest, updatedState);
remainingCancellation.ThrowIfCancellationRequested();
@@ -158,16 +168,30 @@ internal sealed class InstallerEngine
if (reusableGameAssets is null)
{
await ExtractGameAssetsAsync(nodTool, options.GamePath,
await ExtractGameAssetsAsync(nodTool, options.GamePath!,
Path.Combine(staging, "GameAssets"), manifest, cancellationToken);
}
await PublishToolkitAndReconcileProductsAsync(existing, staging, workspace, manifest,
previousState, options, canonicalRetroRoot, retroCompileInputs,
publishGameAssets: reusableGameAssets is null, createShortcuts: firstInstall,
publishGameAssets: reusableGameAssets is null, gameDataDir, createShortcuts: firstInstall,
cancellationToken: cancellationToken);
}
/// <summary>
/// What <see cref="InstallState.GameDataDir"/> becomes: the extracted game this operation was given, or the
/// one a previous operation linked when that is what is being reused; empty for the installation's own copy.
/// </summary>
private static string GameDataDirFor(string? externalGameData, string? reusableGameAssets, InstallState? previousState)
{
if (externalGameData is not null)
return externalGameData;
if (reusableGameAssets is not null && previousState is { GameDataDir.Length: > 0 } previous &&
FileSystemUtilities.PathsEqual(reusableGameAssets, previous.GameDataDir))
return previous.GameDataDir;
return "";
}
internal static bool MustRefreshToolkit(bool sameToolkit, bool samePackageContent,
bool nodToolPresent) =>
@@ -190,7 +214,7 @@ internal sealed class InstallerEngine
string stagedWorkspace, PayloadManifest manifest, InstallState? previousState,
InstallOptions options, string? canonicalRetroRoot,
RetroRewindCompileInputs? retroCompileInputs,
bool publishGameAssets, bool createShortcuts, CancellationToken cancellationToken)
bool publishGameAssets, string gameDataDir, bool createShortcuts, CancellationToken cancellationToken)
{
var installDirectory = existing.Root;
@@ -208,6 +232,7 @@ internal sealed class InstallerEngine
var state = BuildInstallState(previousState, installDirectory, manifest,
compileInputs: null, canonicalRetroRoot, options.RetroWfcPayloadMode);
state.GameDataDir = gameDataDir;
PrepareMetadata(staging, manifest, state);
cancellationToken.ThrowIfCancellationRequested();
@@ -305,7 +330,9 @@ internal sealed class InstallerEngine
using var transaction = InstallTransaction.Begin(installDirectory, _reporter, entries.ToArray());
transaction.Publish();
transaction.RecordRuntimeConfigurationMutation(configSnapshot);
RuntimeConfiguration.SetDvdRoot(configPath, Path.Combine(installDirectory, "GameAssets", "DATA"));
RuntimeConfiguration.SetDvdRoot(configPath, string.IsNullOrEmpty(state.GameDataDir)
? Path.Combine(installDirectory, "GameAssets", "DATA")
: state.GameDataDir);
// The runtime scans this directory live for its asset overlay, so it is recorded on every
// operation that receives one and is never rewritten by a base-only operation.
if (canonicalRetroRoot is not null)
@@ -336,9 +363,26 @@ internal sealed class InstallerEngine
ex.Message);
}
// Reading Wheel Wizard's extracted game, the installation no longer needs a copy of its own.
if (!string.IsNullOrEmpty(state.GameDataDir))
RemoveOwnGameAssets(new Installation(installDirectory), _reporter);
_reporter.Progress(InstallStages.Publish, "Installation complete.", completionPercent);
}
/// <summary>Deletes the installation's own extraction; it is only ever this installation's own folder.</summary>
internal static void RemoveOwnGameAssets(Installation installation, IInstallReporter reporter)
{
try
{
FileSystemUtilities.DeleteDirectoryIfExists(installation.OwnGameAssetsDirectory);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
reporter.Diagnostic("The game copy WiiCompiled no longer uses could not be removed yet: " + ex.Message);
}
}
private static void PrepareMetadata(string staging, PayloadManifest manifest, InstallState state)
{
JsonState.Write(Path.Combine(staging, ToolkitState.FileName), new ToolkitState
+15 -1
View File
@@ -12,7 +12,14 @@ internal enum RetroWfcPayloadMode
internal sealed class InstallOptions
{
public required string GamePath { get; init; }
/// <summary>A disc image to extract. Exactly one of this and <see cref="GameDirectoryPath"/> is set.</summary>
public string? GamePath { get; init; }
/// <summary>
/// A game Wheel Wizard already extracted (its root, holding <c>DATA/</c>). It is read in place through
/// <c>dvd_root</c>; nothing is extracted and no copy of it is kept in the installation.
/// </summary>
public string? GameDirectoryPath { get; init; }
/// <summary>
/// The canonical Retro Rewind installation Wheel Wizard owns. Its resolved path is recorded as
@@ -102,6 +109,13 @@ internal sealed class InstallState
/// <c>[paths] retro_rewind_root</c>; this mirror lets uninstall drop only the setting it owns.
/// </summary>
public string RetroRewindRoot { get; set; } = "";
/// <summary>
/// The extracted game's <c>DATA</c> folder this installation reads in place (<c>--game-dir</c>). Empty for an
/// installation that reads its own extraction in <c>GameAssets\DATA</c>. Wheel Wizard reads this to know
/// whether WiiCompiled already uses its extracted game.
/// </summary>
public string GameDataDir { get; set; } = "";
}
/// <summary>
@@ -39,6 +39,12 @@ internal static class Program
if (command.Mode == AppMode.Version)
return ConsoleCommands.Version();
if (command.Mode == AppMode.Capabilities)
return ConsoleCommands.Capabilities();
if (command.Mode == AppMode.LinkGameDirectory)
return ConsoleCommands.LinkGameDirectory(command);
if (command.Mode == AppMode.Help)
return ConsoleCommands.Help();
@@ -53,6 +53,10 @@ internal static class SelfTests
Test("Runtime configuration key matching", TestRuntimeConfigurationKeyMatching, failures);
Test("Install location validation", TestInstallLocationValidation, failures);
Test("Command line contract", TestCommandLineContract, failures);
Test("Extracted game command line contract", TestGameDirectoryCommandLine, failures);
Test("Extracted game directory", TestExtractedGameDirectory, failures);
Test("Game data follows the install state", TestGameDataDirectoryFollowsState, failures);
Test("Capabilities report", TestCapabilitiesReport, failures);
Test("Portable root discovery", TestPortableRootDiscovery, failures);
Test("Portable install validation failure leaves no marker",
TestPortableValidationFailureLeavesNoMarker, failures);
@@ -265,6 +269,139 @@ internal static class SelfTests
}
}
private static void TestGameDirectoryCommandLine()
{
void Rejects(string description, params string[] args)
{
try
{
CommandLine.Parse(args);
throw new Exception(description + " was accepted.");
}
catch (ArgumentException) { }
}
var silent = CommandLine.Parse(["--silent", "--game-dir", "D:\\WW\\GAME", "--install-dir", "C:\\MKW", "--progress-json"]);
if (silent.Mode != AppMode.SilentInstall || silent.GameDirectoryPath != "D:\\WW\\GAME" || silent.GamePath is not null)
throw new Exception("An install from an extracted game did not parse.");
var link = CommandLine.Parse(["--link-game-dir", "--install-dir", "C:\\MKW", "--game-dir", "D:\\WW\\GAME", "--progress-json"]);
if (link.Mode != AppMode.LinkGameDirectory || link.GameDirectoryPath != "D:\\WW\\GAME" || !link.ProgressJson)
throw new Exception("The link command did not parse.");
if (CommandLine.Parse(["--capabilities"]).Mode != AppMode.Capabilities)
throw new Exception("--capabilities was not recognised.");
Rejects("Both a disc image and an extracted game", "--silent", "--game", "g.iso", "--game-dir", "GAME", "--install-dir", "x");
Rejects("An install with no game at all", "--silent", "--install-dir", "x");
Rejects("A link without the extracted game", "--link-game-dir", "--install-dir", "x");
Rejects("A link without an installation", "--link-game-dir", "--game-dir", "GAME");
Rejects("A link from a disc image", "--link-game-dir", "--install-dir", "x", "--game-dir", "GAME", "--game", "g.iso");
Rejects("--game-dir where nothing reads it", "--check-products", "--install-dir", "x", "--game-dir", "GAME");
}
private static byte[] BootBin(string gameId)
{
var boot = new byte[0x440];
System.Text.Encoding.ASCII.GetBytes(gameId).CopyTo(boot, 0);
boot[7] = 2;
BinaryPrimitives.WriteUInt32BigEndian(boot.AsSpan(0x18), 0x5D1C9EA3);
System.Text.Encoding.ASCII.GetBytes("MARIO KART WII").CopyTo(boot, 0x20);
return boot;
}
private static void TestExtractedGameDirectory()
{
var root = Path.Combine(Path.GetTempPath(), "mkwc-gamedir-" + Guid.NewGuid().ToString("N"));
try
{
var sys = Path.Combine(root, "GAME", "DATA", "sys");
Directory.CreateDirectory(sys);
File.WriteAllBytes(Path.Combine(sys, "main.dol"), [1, 2, 3]);
File.WriteAllBytes(Path.Combine(sys, "boot.bin"), BootBin("RMCP01"));
var fromRoot = ExtractedGameDirectory.ResolveDataDirectory(Path.Combine(root, "GAME"));
var fromData = ExtractedGameDirectory.ResolveDataDirectory(Path.Combine(root, "GAME", "DATA"));
if (!FileSystemUtilities.PathsEqual(fromRoot, Path.Combine(root, "GAME", "DATA")) ||
!FileSystemUtilities.PathsEqual(fromData, fromRoot))
throw new Exception("The DATA folder was not found from the game root or from itself.");
var header = InputValidation.ReadDiscHeaderFromDirectory(fromRoot);
if (header.GameId != "RMCP01" || header.Revision != 2 || header.InternalName != "MARIO KART WII" ||
header.Region != "PAL")
throw new Exception("The extracted game's header was misread.");
try
{
ExtractedGameDirectory.ResolveDataDirectory(root);
throw new Exception("A folder without a game was accepted.");
}
catch (InvalidDataException) { }
File.WriteAllBytes(Path.Combine(sys, "boot.bin"), new byte[0x440]);
try
{
ExtractedGameDirectory.ReadHeader(fromRoot);
throw new Exception("A boot.bin without the Wii magic was accepted.");
}
catch (InvalidDataException) { }
}
finally
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}
private static void TestGameDataDirectoryFollowsState()
{
var root = Path.Combine(Path.GetTempPath(), "mkwc-gamedata-" + Guid.NewGuid().ToString("N"));
try
{
Directory.CreateDirectory(root);
var installation = new Installation(root);
if (!FileSystemUtilities.PathsEqual(installation.GameDataDirectory, installation.OwnGameDataDirectory))
throw new Exception("An installation without state did not read its own game copy.");
var external = Path.Combine(root, "elsewhere", "GAME", "DATA");
JsonState.Write(installation.InstallStatePath, new InstallState { InstallDir = root, GameDataDir = external });
if (!FileSystemUtilities.PathsEqual(installation.GameDataDirectory, external))
throw new Exception("A linked installation did not read the extracted game.");
// Removing the installation's own copy never reaches the extracted game it reads.
Directory.CreateDirectory(Path.Combine(installation.OwnGameAssetsDirectory, "DATA"));
Directory.CreateDirectory(external);
File.WriteAllText(Path.Combine(external, "keep.bin"), "keep");
InstallerEngine.RemoveOwnGameAssets(installation, new ConsoleInstallReporter(_ => { }));
if (Directory.Exists(installation.OwnGameAssetsDirectory) || !File.Exists(Path.Combine(external, "keep.bin")))
throw new Exception("Removing the own game copy removed the wrong folder.");
}
finally
{
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
}
}
private static void TestCapabilitiesReport()
{
var output = new StringWriter();
var original = Console.Out;
Console.SetOut(output);
try
{
if (ConsoleCommands.Capabilities() != 0)
throw new Exception("--capabilities did not succeed.");
}
finally
{
Console.SetOut(original);
}
using var document = System.Text.Json.JsonDocument.Parse(output.ToString());
if (document.RootElement.GetProperty("type").GetString() != "capabilities" ||
!document.RootElement.GetProperty("capabilities").EnumerateArray().Any(item => item.GetString() == "game-dir"))
throw new Exception("--capabilities did not report game-dir.");
}
private static void TestCommandLineContract()
{
void Rejects(string description, params string[] args)
@@ -55,6 +55,10 @@ internal static class UninstallService
TryCleanup("remove the installed game path from the runtime configuration", quiet,
() => RuntimeConfiguration.RemoveDvdRootIfOwned(configPath,
Path.Combine(installDirectory, "GameAssets", "DATA")));
// A game Wheel Wizard extracted is only unregistered here; the folder itself is Wheel Wizard's, and stays.
if (!string.IsNullOrEmpty(state?.GameDataDir))
TryCleanup("remove the extracted game path from the runtime configuration", quiet,
() => RuntimeConfiguration.RemoveDvdRootIfOwned(configPath, state.GameDataDir));
}
// Remove only this install's Retro Rewind setting; leave user-owned paths untouched.
+11 -6
View File
@@ -22,7 +22,7 @@ Usage: local-build-macos.command --output-dir DIR [options]
--retro-rewind-package-dir DIR RetroRewind6 directory (required for Retro Rewind)
--retro-wfc-offline-dir DIR Directory containing binary/payload.RMCPD00.bin
--skip-retro-wfc-payload Build Retro Rewind without the shared Retro-WFC payload
--force-clean-build Delete local generated and native-build-macos caches
--force-clean-build Delete local generated and current-architecture native build caches
--parallel N Pin translation and build parallelism
--cmake PATH --ninja PATH Override build tools
--dotnet PATH Override dotnet
@@ -56,7 +56,9 @@ while (($#)); do
done
[[ $(uname -s) == Darwin ]] || fail 'this build script is for macOS only'
[[ $(uname -m) == arm64 ]] || fail 'the current macOS product target is Apple Silicon only'
macos_arch=$(uname -m)
case "$macos_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $macos_arch" ;; esac
macos_deployment_target=12.0
workspace=$(cd "$workspace" && pwd)
[[ -n "$output_dir" ]] || fail '--output-dir is required'
case "$profile" in base|retro-rewind|both) ;; *) fail '--profile must be base, retro-rewind, or both' ;; esac
@@ -73,7 +75,8 @@ for tool in "$cmake_bin" "$ninja_bin" clang clang++ shasum; do command -v "$tool
project="$workspace/projects/mkwii/recomp.yml"; assets="$workspace/Assets"; generated="$workspace/generated"
functions="$generated/functions"; metadata="$generated/base_translation_output.json"; manifest_dir="$workspace/build/base"
manifest="$manifest_dir/mkwii_base_manifest.json"; shards="$generated/build_shards"; native_build="$workspace/native-build-macos"
manifest="$manifest_dir/mkwii_base_manifest.json"; shards="$generated/build_shards"
native_build="$workspace/native-build-macos-$macos_arch"
assert_file "$project" 'translation project'
if [[ -n "$game" ]]; then "$script_dir/macos/extract-disc.command" --game "$game" --assets-dir "$assets" --nodtool "$nodtool"; fi
assert_file "$assets/main.dol" 'extracted main.dol'; assert_file "$assets/StaticR.rel" 'extracted StaticR.rel'
@@ -131,9 +134,11 @@ if (( builds_retro )); then args+=(--resolved-profile "$mod_out/resolved_dispatc
step emit-build-shards 'Preparing native build shards'; translator "${args[@]}"
step configure-native 'Configuring the native toolchain'
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs"
# Use Aurora's pinned SDL3 source on macOS. A system SDL3 can be older than
# Aurora's required API even when find_package() succeeds.
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DCMAKE_OSX_ARCHITECTURES="$macos_arch" -DCMAKE_OSX_DEPLOYMENT_TARGET="$macos_deployment_target" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs" -DAURORA_SDL3_PROVIDER=vendor
targets=(); [[ "$profile" != retro-rewind ]] && targets+=(WiiCompiled); [[ "$profile" != base ]] && targets+=(RetroRewind)
step compile "Compiling ${targets[*]} locally"; "$cmake_bin" --build "$native_build" --target "${targets[@]}" --parallel "$global_jobs"
if [[ "$profile" != retro-rewind ]]; then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product WiiCompiled --output-dir "${base_output_dir:-$output_dir}"; fi
if (( builds_retro )); then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product RetroRewind --output-dir "$output_dir"; fi
if [[ "$profile" != retro-rewind ]]; then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product WiiCompiled --output-dir "${base_output_dir:-$output_dir}" --architecture "$macos_arch" --minimum-system-version "$macos_deployment_target"; fi
if (( builds_retro )); then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product RetroRewind --output-dir "$output_dir" --architecture "$macos_arch" --minimum-system-version "$macos_deployment_target"; fi
printf 'MKWCBUILD:OUTPUT=%s\n' "$output_dir"
+78 -20
View File
@@ -9,45 +9,97 @@ fail() { printf 'build-setup-pkg.command: error: %s\n' "$*" >&2; exit 1; }
copy_clean() { DITTONORSRC=1 ditto --norsrc --noqtn "$@"; }
usage() {
cat <<'EOF'
Usage: build-setup-pkg.command --nodtool PATH --translator PATH --cmake-root DIR --ninja PATH --output PKG [options]
Usage: build-setup-pkg.command --nodtool-arm64 PATH --nodtool-x86_64 PATH --translator-arm64 PATH --translator-x86_64 PATH --cmake-root DIR --ninja-arm64 PATH --ninja-x86_64 PATH --output PKG [options]
Creates a game-code-free WiiCompiled Setup.pkg. The supplied tools must be
maintainer-verified, redistributable macOS arm64 artifacts. The resulting pkg
is unsigned unless --installer-identity is supplied; releases should sign and
notarize it with a Developer ID Installer certificate.
maintainer-verified, redistributable macOS artifacts for both arm64 and
x86_64. The setup package selects native tools for its host while the game is
compiled locally for that host architecture. CMake must be universal2. The
resulting pkg is unsigned unless
--installer-identity is supplied; releases should sign and notarize it with a
Developer ID Installer certificate.
--workspace DIR Repository root (default: script's grandparent)
--version VERSION Bundle/package version (default: 0.1.0)
--version VERSION Bundle/package version (default: project version, or 0.1.0)
--installer-identity NAME Developer ID Installer identity for productbuild
EOF
}
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace=$(cd "$script_dir/../.." && pwd); nodtool=""; translator=""; cmake_root=""; ninja=""; output=""; version=0.1.0; identity=""
workspace=$(cd "$script_dir/../.." && pwd); nodtool_arm64=""; nodtool_x86_64=""; translator_arm64=""; translator_x86_64=""; cmake_root=""; ninja_arm64=""; ninja_x86_64=""; output=""; version=0.1.0; identity=""
version_supplied=false
while (($#)); do
case "$1" in
--workspace) workspace=${2:-}; shift 2 ;;
--nodtool) nodtool=${2:-}; shift 2 ;;
--translator) translator=${2:-}; shift 2 ;;
--nodtool-arm64) nodtool_arm64=${2:-}; shift 2 ;;
--nodtool-x86_64) nodtool_x86_64=${2:-}; shift 2 ;;
--translator-arm64) translator_arm64=${2:-}; shift 2 ;;
--translator-x86_64) translator_x86_64=${2:-}; shift 2 ;;
--cmake-root) cmake_root=${2:-}; shift 2 ;;
--ninja) ninja=${2:-}; shift 2 ;;
--ninja-arm64) ninja_arm64=${2:-}; shift 2 ;;
--ninja-x86_64) ninja_x86_64=${2:-}; shift 2 ;;
--output) output=${2:-}; shift 2 ;;
--version) version=${2:-}; shift 2 ;;
--version) version=${2:-}; version_supplied=true; shift 2 ;;
--installer-identity) identity=${2:-}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown option: $1" ;;
esac
done
version=${version#v}
if [[ "$version_supplied" == false ]]; then
local_csproj="$workspace/Launcher/Directory.Build.props"
if [[ -f "$local_csproj" ]]; then
detected=$(grep -m1 '<Version>' "$local_csproj" | sed -E 's/.*<Version>([^<]+)<\/Version>.*/\1/' || true)
if [[ -n "$detected" ]]; then
version="$detected"
fi
fi
fi
[[ "$version" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]] || fail '--version must contain one to three period-separated integers'
IFS=. read -r version_major version_minor version_patch <<< "$version"
short_version="$version_major.${version_minor:-0}.${version_patch:-0}"
for tool in pkgbuild productbuild ditto codesign; do command -v "$tool" >/dev/null || fail "required macOS tool unavailable: $tool"; done
[[ -x "$nodtool" ]] || fail '--nodtool must name an executable'
[[ -x "$translator" ]] || fail '--translator must name an executable'
for tool in pkgbuild productbuild ditto codesign lipo; do command -v "$tool" >/dev/null || fail "required macOS tool unavailable: $tool"; done
for tool_path in "$nodtool_arm64" "$nodtool_x86_64" "$translator_arm64" "$translator_x86_64" "$ninja_arm64" "$ninja_x86_64"; do [[ -x "$tool_path" ]] || fail 'each architecture-specific tool must name an executable'; done
[[ -x "$cmake_root/bin/cmake" ]] || fail '--cmake-root must contain bin/cmake'
[[ -x "$ninja" ]] || fail '--ninja must name an executable'
"$nodtool" --version >/dev/null || fail '--nodtool did not run successfully'
require_arch() {
local artifact=$1 arch=$2 label=$3
lipo "$artifact" -verify_arch "$arch" >/dev/null 2>&1 || fail "$label must contain a $arch slice: $artifact"
}
require_arch "$nodtool_arm64" arm64 '--nodtool-arm64'; require_arch "$nodtool_x86_64" x86_64 '--nodtool-x86_64'
require_arch "$translator_arm64" arm64 '--translator-arm64'; require_arch "$translator_x86_64" x86_64 '--translator-x86_64'
require_arch "$ninja_arm64" arm64 '--ninja-arm64'; require_arch "$ninja_x86_64" x86_64 '--ninja-x86_64'
lipo "$cmake_root/bin/cmake" -verify_arch arm64 x86_64 >/dev/null 2>&1 || fail '--cmake-root/bin/cmake must be universal2'
# Slice checks above prevent accidental cross-architecture packaging. Exercise
# each supplied executable as well: an incorrectly bundled runtime can have a
# valid Mach-O header but still fail before the setup app can use it. Apple
# Silicon maintainers validate Intel tools through Rosetta when it is present.
host_arch=$(uname -m)
run_for_arch() {
local arch=$1 label=$2
shift 2
if [[ "$arch" == "$host_arch" ]]; then
"$@" >/dev/null || fail "$label did not run successfully"
elif [[ "$host_arch" == arm64 && "$arch" == x86_64 ]] && /usr/bin/arch -x86_64 /usr/bin/true >/dev/null 2>&1; then
/usr/bin/arch -x86_64 "$@" >/dev/null || fail "$label did not run successfully under Rosetta"
else
# Intel hosts cannot execute arm64 binaries. The slice remains checked
# above; CI or an Apple Silicon maintainer must execute that tool set.
printf 'build-setup-pkg.command: warning: unable to execute %s on %s; architecture slice was verified, but run it in %s CI before release\n' \
"$label" "$host_arch" "$arch" >&2
fi
}
for arch in arm64 x86_64; do
if [[ "$arch" == arm64 ]]; then
nodtool=$nodtool_arm64; translator=$translator_arm64; ninja=$ninja_arm64
else
nodtool=$nodtool_x86_64; translator=$translator_x86_64; ninja=$ninja_x86_64
fi
run_for_arch "$arch" "--nodtool-$arch" "$nodtool" --version
run_for_arch "$arch" "--translator-$arch" "$translator" --help
run_for_arch "$arch" "--ninja-$arch" "$ninja" --version
done
run_for_arch "$host_arch" '--cmake-root/bin/cmake' "$cmake_root/bin/cmake" --version
workspace=$(cd "$workspace" && pwd); output=$(cd "$(dirname "$output")" && pwd)/$(basename "$output")
stage=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-pkg.XXXXXX")
trap 'rm -rf "$stage"' EXIT
@@ -64,7 +116,7 @@ cat > "$app/Contents/Info.plist" <<EOF
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>$short_version</string>
<key>CFBundleVersion</key><string>$version</string>
<key>LSMinimumSystemVersion</key><string>14.0</string>
<key>LSMinimumSystemVersion</key><string>12.0</string>
</dict></plist>
EOF
cat > "$app/Contents/MacOS/WiiCompiledSetup" <<'EOF'
@@ -103,11 +155,17 @@ copy_clean "$workspace/Launcher/local-build-macos.command" "$resources/workspace
copy_clean "$workspace/Launcher/macos/extract-disc.command" "$resources/workspace/Launcher/macos/extract-disc.command"
copy_clean "$workspace/Launcher/macos/publish-app.command" "$resources/workspace/Launcher/macos/publish-app.command"
chmod +x "$resources/workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/macos/"*.command
mkdir -p "$resources/tools/cmake"
copy_clean "$nodtool" "$resources/tools/nodtool"; chmod +x "$resources/tools/nodtool"
copy_clean "$translator" "$resources/tools/Translator.Cli"; chmod +x "$resources/tools/Translator.Cli"
# setup.command uses this marker to update source inputs in an existing user
# workspace without replacing extracted game assets or Retro Rewind files.
printf '%s\n' "$version" > "$resources/workspace/.bundle-version"
mkdir -p "$resources/tools/cmake" "$resources/tools/arm64" "$resources/tools/x86_64"
copy_clean "$nodtool_arm64" "$resources/tools/arm64/nodtool"; chmod +x "$resources/tools/arm64/nodtool"
copy_clean "$nodtool_x86_64" "$resources/tools/x86_64/nodtool"; chmod +x "$resources/tools/x86_64/nodtool"
copy_clean "$translator_arm64" "$resources/tools/arm64/Translator.Cli"; chmod +x "$resources/tools/arm64/Translator.Cli"
copy_clean "$translator_x86_64" "$resources/tools/x86_64/Translator.Cli"; chmod +x "$resources/tools/x86_64/Translator.Cli"
copy_clean "$cmake_root" "$resources/tools/cmake"
copy_clean "$ninja" "$resources/tools/ninja"; chmod +x "$resources/tools/ninja"
copy_clean "$ninja_arm64" "$resources/tools/arm64/ninja"; chmod +x "$resources/tools/arm64/ninja"
copy_clean "$ninja_x86_64" "$resources/tools/x86_64/ninja"; chmod +x "$resources/tools/x86_64/ninja"
copy_clean "$workspace/LICENSE" "$resources/LICENSE"
copy_clean "$workspace/THIRD-PARTY-NOTICES.md" "$resources/THIRD-PARTY-NOTICES.md"
codesign --force --deep --sign - "$app"
@@ -0,0 +1,11 @@
# Cross-compile a thin x86_64 macOS build from an Apple Silicon Mac.
# Pass this file on the first configure with:
# -DCMAKE_TOOLCHAIN_FILE=/absolute/path/to/macos-x86_64-toolchain.cmake
#
# CMAKE_SYSTEM_PROCESSOR is deliberately declared here rather than inferred
# from CMAKE_OSX_ARCHITECTURES, so target-aware CMake dependencies select their
# x86_64 artifacts.
set(CMAKE_SYSTEM_NAME Darwin)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_OSX_ARCHITECTURES x86_64 CACHE STRING
"Target macOS architectures" FORCE)
+93 -14
View File
@@ -5,28 +5,36 @@ set -euo pipefail
fail() { printf 'publish-app.command: error: %s\n' "$*" >&2; exit 1; }
usage() {
cat <<'EOF'
Usage: publish-app.command --build-dir DIR --product {WiiCompiled|RetroRewind} --output-dir DIR
Usage: publish-app.command --build-dir DIR --product {WiiCompiled|RetroRewind} --output-dir DIR [options]
Copies a locally built product and its runtime assets into OUTPUT-DIR/<product>.app.
It bundles non-system dylibs, rewrites their install names, and ad-hoc signs the
result. This is suitable for local use; a release must replace ad-hoc signing
with the project's Developer ID signing and notarization process.
--architecture {arm64|x86_64} Required architecture of the compiled product (default: host)
--minimum-system-version VERSION App bundle minimum macOS version (default: 12.0)
EOF
}
build_dir=""; product=""; output_dir=""
build_dir=""; product=""; output_dir=""; architecture=$(uname -m); minimum_system_version=12.0
while (($#)); do
case "$1" in
--build-dir) build_dir=${2:-}; shift 2 ;;
--product) product=${2:-}; shift 2 ;;
--output-dir) output_dir=${2:-}; shift 2 ;;
--architecture) architecture=${2:-}; shift 2 ;;
--minimum-system-version) minimum_system_version=${2:-}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown option: $1" ;;
esac
done
[[ "$product" == WiiCompiled || "$product" == RetroRewind ]] || fail '--product must be WiiCompiled or RetroRewind'
for tool in codesign ditto install_name_tool otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
[[ "$architecture" == arm64 || "$architecture" == x86_64 ]] || fail '--architecture must be arm64 or x86_64'
[[ "$minimum_system_version" =~ ^[0-9]+(\.[0-9]+){1,2}$ ]] || fail '--minimum-system-version must contain two or three period-separated integers'
for tool in codesign ditto install_name_tool lipo otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
[[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product"
lipo "$build_dir/$product" -verify_arch "$architecture" || fail "compiled product is not $architecture: $build_dir/$product"
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done
app="$output_dir/$product.app"
@@ -47,7 +55,7 @@ cat > "$app/Contents/Info.plist" <<EOF
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>0.1.0</string>
<key>CFBundleVersion</key><string>1</string>
<key>LSMinimumSystemVersion</key><string>14.0</string>
<key>LSMinimumSystemVersion</key><string>$minimum_system_version</string>
<key>NSHighResolutionCapable</key><true/>
</dict></plist>
EOF
@@ -57,25 +65,96 @@ for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do
ln -s "../Resources/$asset" "$macos/$asset"
done
# Build a closure of Homebrew dylibs. System libraries remain system references.
queue=("$macos/$product")
# Expand each image's rpaths before inheriting them, so @loader_path stays
# relative to the image that declared it rather than a descendant library.
expanded_rpaths() {
local target=$1 rpath
while IFS= read -r rpath; do
case "$rpath" in
@loader_path/*) rpath="$(dirname "$target")/${rpath#@loader_path/}" ;;
@loader_path) rpath="$(dirname "$target")" ;;
@executable_path/*) rpath="$build_dir/${rpath#@executable_path/}" ;;
@executable_path) rpath="$build_dir" ;;
esac
printf '%s\n' "$rpath"
done < <(otool -l "$target" | awk '
/LC_RPATH/ { rpath = 1; next }
rpath && /^[[:space:]]*path / {
sub(/^[[:space:]]*path[[:space:]]+/, "");
sub(/[[:space:]]+\(offset[[:space:]]+[0-9]+\)$/, "");
print;
rpath = 0;
}')
}
# Resolve a non-system dependency using the current image's rpaths followed
# by the inherited loader stack, matching dyld's dependency-chain search.
dependency_path() {
local current=$1 dependency=$2 search_rpaths=$3 rpath candidate
case "$dependency" in
/System/Library/*|/usr/lib/*)
return 1
;;
/*)
[[ -f "$dependency" ]] && { printf '%s\n' "$dependency"; return 0; }
;;
@loader_path/*)
candidate="$(dirname "$current")/${dependency#@loader_path/}"
[[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; }
;;
@executable_path/*)
candidate="$build_dir/${dependency#@executable_path/}"
[[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; }
;;
@rpath/*|*.dylib)
local subpath
if [[ "$dependency" == @rpath/* ]]; then
subpath="${dependency#@rpath/}"
else
subpath="$dependency"
fi
while IFS= read -r rpath; do
[[ -n "$rpath" ]] || continue
candidate="$rpath/$subpath"
[[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; }
done <<< "$search_rpaths"
;;
esac
return 1
}
# Build a closure of non-system dylibs. System libraries remain system
# references, while every resolved dependency is copied beside the executable.
queue=("$build_dir/$product")
queue_rpaths=("")
while ((${#queue[@]})); do
current=${queue[0]}
inherited_rpaths=${queue_rpaths[0]}
queue=("${queue[@]:1}")
queue_rpaths=("${queue_rpaths[@]:1}")
current_rpaths=$(expanded_rpaths "$current")
if [[ -n "$inherited_rpaths" ]]; then
current_rpaths="${current_rpaths}${current_rpaths:+$'\n'}$inherited_rpaths"
fi
self_id=$(otool -D "$current" 2>/dev/null | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]*$/\1/p' || true)
while IFS= read -r dependency; do
[[ "$dependency" == /opt/homebrew/* || "$dependency" == /usr/local/* ]] || continue
[[ -f "$dependency" ]] || continue
name=$(basename "$dependency")
[[ -z "$dependency" ]] && continue
[[ -n "$self_id" && "$dependency" == "$self_id" ]] && continue
case "$dependency" in
/System/Library/*|/usr/lib/*) continue ;;
esac
dep_path=$(dependency_path "$current" "$dependency" "$current_rpaths") || fail "unresolved non-system dependency: '$dependency' needed by '$current'"
name=$(basename "$dep_path")
if [[ ! -f "$frameworks/$name" ]]; then
ditto "$dependency" "$frameworks/$name"
ditto "$dep_path" "$frameworks/$name"
install_name_tool -id "@rpath/$name" "$frameworks/$name"
queue+=("$frameworks/$name")
queue+=("$dep_path")
queue_rpaths+=("$current_rpaths")
fi
done < <(otool -L "$current" | tail -n +2 | awk '{print $1}')
done < <(otool -L "$current" | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]+\(compatibility version .*/\1/p')
done
while IFS= read -r binary; do
while IFS= read -r old; do
[[ "$old" == /opt/homebrew/* || "$old" == /usr/local/* ]] || continue
name=$(basename "$old")
[[ -f "$frameworks/$name" ]] || continue
if [[ "$binary" == "$macos/$product" ]]; then
@@ -83,7 +162,7 @@ while IFS= read -r binary; do
else
install_name_tool -change "$old" "@loader_path/$name" "$binary"
fi
done < <(otool -L "$binary" | tail -n +2 | awk '{print $1}')
done < <(otool -L "$binary" | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]+\(compatibility version .*/\1/p')
done < <(find "$frameworks" -type f -print; printf '%s\n' "$macos/$product")
find "$frameworks" -type f -exec codesign --force --sign - {} +
+35 -6
View File
@@ -5,10 +5,7 @@ set -euo pipefail
resources=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace_source="$resources/workspace"
nodtool="$resources/tools/nodtool"
translator="$resources/tools/Translator.Cli"
cmake_bin="$resources/tools/cmake/bin/cmake"
ninja_bin="$resources/tools/ninja"
support_root="$HOME/Library/Application Support/WiiCompiled"
workspace="$support_root/BuildWorkspace"
products="$support_root/Products"
@@ -36,6 +33,13 @@ while (($#)); do
done
[[ "$install_location" == user || "$install_location" == applications ]] || fail '--install-location must be user or applications'
host_arch=$(uname -m)
case "$host_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $host_arch" ;; esac
host_tools="$resources/tools/$host_arch"
nodtool="$host_tools/nodtool"
translator="$host_tools/Translator.Cli"
ninja_bin="$host_tools/ninja"
if [[ -z "$game" ]]; then
game=$(/usr/bin/osascript <<'APPLESCRIPT'
set selectedFile to choose file with prompt "Choose your clean Mario Kart Wii PAL (RMCP01) disc image"
@@ -60,12 +64,37 @@ if ! /usr/bin/xcode-select -p >/dev/null 2>&1; then
/usr/bin/xcode-select --install || true
exit 1
fi
for tool in "$nodtool" "$translator" "$cmake_bin" "$ninja_bin"; do
/usr/bin/lipo "$tool" -verify_arch "$host_arch" >/dev/null 2>&1 || \
fail "the packaged $(basename "$tool") does not support $host_arch"
done
mkdir -p "$support_root" "$products"
if [[ ! -d "$workspace/.git" && ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then
source_bundle_version="$workspace_source/.bundle-version"
workspace_bundle_version="$workspace/.bundle-version"
needs_workspace_refresh=0
if [[ ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then
needs_workspace_refresh=1
elif [[ -f "$source_bundle_version" ]] && [[ ! -f "$workspace_bundle_version" || "$(<"$source_bundle_version")" != "$(<"$workspace_bundle_version")" ]]; then
needs_workspace_refresh=1
fi
if (( needs_workspace_refresh )); then
printf 'Preparing the local build workspace...\n'
rm -rf "$workspace"
/usr/bin/ditto "$workspace_source" "$workspace"
if [[ ! -d "$workspace" ]]; then
/usr/bin/ditto "$workspace_source" "$workspace"
else
# Refresh only packaged source inputs. Assets and the staged Retro
# Rewind package belong to the user and stay in place.
for source in aurora-main projects runtime translator Launcher; do
rm -rf "$workspace/$source"
/usr/bin/ditto "$workspace_source/$source" "$workspace/$source"
done
/usr/bin/ditto "$source_bundle_version" "$workspace_bundle_version"
# A dependency provider can be cached in this directory, so make the
# refreshed sources configure from a clean native build tree.
rm -rf "$workspace/native-build-macos-arm64" "$workspace/native-build-macos-x86_64"
fi
fi
profile=base
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# Exercise a real dyld dependency chain before and after app packaging.
set -euo pipefail
[[ $(uname -s) == Darwin ]] || { printf 'This test requires macOS.\n' >&2; exit 1; }
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
temp_root=$(cd "${TMPDIR:-/tmp}" && pwd)
test_root=$(mktemp -d "$temp_root/wiicompiled-publish-test.XXXXXX")
[[ "$test_root" == "$temp_root"/wiicompiled-publish-test.* ]] || exit 1
trap 'rm -rf "$test_root"' EXIT
build_dir="$test_root/build with spaces"
output_dir="$test_root/output with spaces"
mkdir -p "$build_dir/A/b" "$build_dir/A/c" "$build_dir/wii_bootstrap" "$output_dir"
cat > "$test_root/c.c" <<'EOF'
int value_c(void) { return 7; }
EOF
cat > "$test_root/b.c" <<'EOF'
extern int value_c(void);
int value_b(void) { return 2 * value_c(); }
EOF
cat > "$test_root/a.c" <<'EOF'
extern int value_b(void);
int value_a(void) { return 1 + value_b(); }
EOF
cat > "$test_root/main.c" <<'EOF'
#include <stdio.h>
extern int value_a(void);
int main(void) { printf("%d\n", value_a()); return 0; }
EOF
clang -dynamiclib "$test_root/c.c" -o "$build_dir/A/c/libC.dylib" \
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libC.dylib
clang -dynamiclib "$test_root/b.c" -o "$build_dir/A/b/libB.dylib" \
-L "$build_dir/A/c" -lC \
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libB.dylib
clang -dynamiclib "$test_root/a.c" -o "$build_dir/A/libA.dylib" \
-L "$build_dir/A/b" -lB \
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libA.dylib \
-Wl,-rpath,@loader_path/b -Wl,-rpath,@loader_path/c
clang "$test_root/main.c" -o "$build_dir/WiiCompiled" \
-L "$build_dir/A" -lA \
-Wl,-headerpad_max_install_names -Wl,-rpath,@executable_path/A
# B has no rpaths: its C dependency must inherit A's loader-relative path.
[[ $("$build_dir/WiiCompiled") == 15 ]]
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem; do
: > "$build_dir/$asset"
done
bash "$script_dir/publish-app.command" --build-dir "$build_dir" \
--product WiiCompiled --output-dir "$output_dir" --architecture "$(uname -m)"
for name in libA.dylib libB.dylib libC.dylib; do
[[ -f "$output_dir/WiiCompiled.app/Contents/Frameworks/$name" ]]
done
# Remove access to the original paths and relocate the app before executing.
mv "$build_dir" "$test_root/hidden build"
mkdir "$test_root/relocated app"
mv "$output_dir/WiiCompiled.app" "$test_root/relocated app/WiiCompiled.app"
[[ $("$test_root/relocated app/WiiCompiled.app/Contents/MacOS/WiiCompiled") == 15 ]]
printf 'publish-app dependency-chain test passed\n'
+23 -3
View File
@@ -11,6 +11,7 @@
<a href="#building-from-source"><img alt="PowerPC static recompilation" src="https://img.shields.io/badge/PowerPC-static%20recompilation-FF9F0A"></a>
<a href="#retro-rewind"><img alt="Retro Rewind supported" src="https://img.shields.io/badge/Retro%20Rewind-supported-FF375F"></a>
<a href="https://github.com/TeamWheelWizard/WheelWizard/releases"><img alt="Install with Wheel Wizard" src="https://img.shields.io/badge/install%20with-Wheel%20Wizard-8B5CF6"></a>
<a href="https://github.com/patchzyy/Wiicompiled/releases"><img alt="Total downloads" src="https://img.shields.io/github/downloads/patchzyy/Wiicompiled/total?color=00B8D9&amp;logo=github"></a>
<a href="LICENSE"><img alt="License: GPLv3" src="https://img.shields.io/badge/license-GPLv3-2EA44F?logo=gnu&amp;logoColor=white"></a>
</p>
@@ -52,12 +53,16 @@ Play at several times the console's resolution.
**Music ducking.**
Start playing something else, Spotify, a YouTube video, and
the game automatically mutes its own music until the other audio stops. Optional, if you'd
rather it didn't. All audio that shows in your display media controls on your windows pc fall under this.
rather it didn't. Windows uses system media controls and Linux uses MPRIS players.
On macOS 14.2 or later, this detects other apps with active audio output and excludes
the game's own audio. Apps that keep an output stream running silently can keep
game music muted even when nothing is audible.
**An in-game settings bar.**
Press **F10** while the game window has focus:
- Internal resolution
- FPS counter
- MetalFX spatial upscaling on supported macOS GPUs
- Controller assignment for all four ports
- Full per-controller button mapping, including the bumpers
- Dolphin-syntax input expressions and GCPadNew.ini import
@@ -96,8 +101,8 @@ Known limitations of the Wii Remote path:
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
- About 20 GB of free disk space during installation (Final game size ~5 GB)
- macOS 14 (Sonoma) or later on Apple Silicon
- On macOS, Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
- macOS 12 (Monterey) or later on Apple Silicon (`arm64`) or Intel (`x86_64-v3`); pre-Haswell Intel CPUs are unsupported
- On macOS, a Metal-capable GPU and Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
GCZ, CISO, WBFS, WIA and RVZ are accepted.
@@ -118,6 +123,21 @@ image under Settings, turn on **WiiCompiled (beta)**, and hit install from the H
Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
### macOS
Download `WiiCompiled-Setup.pkg` from this repository's Releases page and open it. The universal
package selects the appropriate bundled tools for the host architecture, supporting both Apple Silicon (`arm64`)
and Intel (`x86_64`) Macs. It installs **WiiCompiled Setup** in Applications; open that app, choose
your clean PAL `RMCP01` disc image, and select either the base game or Retro Rewind. For Retro Rewind, choose the `RetroRewind6` folder
or its parent folder.
Setup verifies and extracts the image locally, then translates and compiles the native app on your
Mac. On a first run it may ask macOS to install Xcode Command Line Tools; complete Apple's installer,
then open Setup again. When the build completes, Setup asks for administrator approval once to install
`WiiCompiled.app` (and, if selected, `RetroRewind.app`) in `/Applications`.
Setup opens Terminal while it works, so the extraction and build progress—and any error that needs
reporting—remain visible.
> [!CAUTION]
> Only take builds from this repository's
+8
View File
@@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.25)
project(aurora LANGUAGES C CXX)
if (APPLE)
enable_language(OBJC)
enable_language(OBJCXX)
endif()
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 20)
@@ -83,3 +84,10 @@ if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND NOT CMAKE_CROSSCOMPIL
enable_testing()
add_subdirectory(tests)
endif ()
option(AURORA_BUILD_METALFX_PRESENTATION_TEST "Build the macOS MetalFX presentation test" OFF)
if (AURORA_BUILD_METALFX_PRESENTATION_TEST AND APPLE AND AURORA_ENABLE_GX AND DAWN_ENABLE_METAL AND AURORA_METALFX_FRAMEWORK)
add_executable(metalfx_presentation_test tests/metalfx_interop/presentation_test.cpp)
target_include_directories(metalfx_presentation_test PRIVATE lib)
target_link_libraries(metalfx_presentation_test PRIVATE aurora::core aurora::gx aurora::vi dawn::webgpu_dawn)
endif ()
+5 -1
View File
@@ -48,7 +48,7 @@ if (_aurora_dawn_provider STREQUAL "auto")
set(_has_package TRUE)
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|aarch64)$")
set(_has_package TRUE)
elseif (APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|x86_64)$")
elseif (APPLE AND (CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|x86_64)$" OR CMAKE_OSX_ARCHITECTURES MATCHES "^(arm64|x86_64)$"))
set(_has_package TRUE)
endif ()
@@ -143,6 +143,10 @@ elseif (_aurora_dawn_provider STREQUAL "package")
if (NOT AURORA_DAWN_PACKAGE_URL)
string(TOLOWER "${CMAKE_SYSTEM_NAME}" _dawn_system)
string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _dawn_arch)
if (APPLE AND CMAKE_OSX_ARCHITECTURES)
list(GET CMAKE_OSX_ARCHITECTURES 0 _dawn_osx_arch)
string(TOLOWER "${_dawn_osx_arch}" _dawn_arch)
endif ()
if (_dawn_system STREQUAL "windows")
if (_dawn_arch STREQUAL "x86_64")
set(_dawn_arch "amd64")
+5 -2
View File
@@ -41,7 +41,10 @@ if (_aurora_sdl3_provider STREQUAL "auto")
set(_aurora_sdl3_provider "package")
else ()
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
find_package(SDL3 QUIET)
# Aurora uses APIs from the SDL version pinned by AURORA_SDL3_VERSION.
# Do not silently select an older system package and fail later while
# compiling its headers.
find_package(SDL3 ${AURORA_SDL3_VERSION} QUIET)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
if (SDL3_FOUND)
set(_aurora_sdl3_provider "system")
@@ -58,7 +61,7 @@ if (_aurora_sdl3_provider STREQUAL "system")
message(STATUS "aurora: Using system SDL3 (provider=system)")
if (NOT SDL3_FOUND)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
find_package(SDL3 REQUIRED)
find_package(SDL3 ${AURORA_SDL3_VERSION} REQUIRED)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
endif ()
_aurora_sdl3_select_target()
+14 -1
View File
@@ -34,6 +34,17 @@ if (AURORA_ENABLE_GX)
target_compile_definitions(aurora_core PUBLIC AURORA_ENABLE_GX WEBGPU_DAWN)
target_sources(aurora_core PRIVATE lib/webgpu/gpu.cpp lib/webgpu/gpu_cache.cpp lib/dawn/BackendBinding.cpp)
target_link_libraries(aurora_core PRIVATE dawn::webgpu_dawn)
if (APPLE AND DAWN_ENABLE_METAL)
find_library(AURORA_METALFX_FRAMEWORK MetalFX)
endif ()
if (APPLE AND DAWN_ENABLE_METAL AND AURORA_METALFX_FRAMEWORK)
target_sources(aurora_core PRIVATE lib/webgpu/metalfx.mm)
set_source_files_properties(lib/webgpu/metalfx.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
target_link_options(aurora_core PUBLIC "LINKER:-weak_framework,MetalFX")
target_link_libraries(aurora_core PRIVATE "-framework IOSurface")
else ()
target_sources(aurora_core PRIVATE lib/webgpu/metalfx_stub.cpp)
endif ()
if (DAWN_ENABLE_VULKAN)
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_VULKAN)
endif ()
@@ -41,7 +52,9 @@ if (AURORA_ENABLE_GX)
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_METAL)
target_sources(aurora_core PRIVATE lib/dawn/MetalBinding.mm)
set_source_files_properties(lib/dawn/MetalBinding.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
target_link_options(aurora_core PUBLIC "LINKER:-weak_framework,Metal")
target_link_options(aurora_core PUBLIC
"LINKER:-weak_framework,Metal"
"LINKER:-U,_OBJC_CLASS_$_MTLLogStateDescriptor")
endif ()
if (DAWN_ENABLE_D3D11)
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_D3D11)
+50
View File
@@ -16,10 +16,60 @@ function(aurora_find_package_global)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ${_PREV_FIND_PACKAGE_TARGETS_GLOBAL})
endfunction()
# A macOS build for another architecture must not discover Homebrew packages
# built for its physical host. CMAKE_CROSSCOMPILING is not sufficient here:
# Apple Clang can target another architecture through CMAKE_OSX_ARCHITECTURES
# without CMake considering the configure a cross-build.
set(_AURORA_EXCLUDE_HOST_HOMEBREW FALSE)
if (APPLE AND CMAKE_OSX_ARCHITECTURES)
list(LENGTH CMAKE_OSX_ARCHITECTURES _AURORA_OSX_ARCH_COUNT)
if (_AURORA_OSX_ARCH_COUNT EQUAL 1)
list(GET CMAKE_OSX_ARCHITECTURES 0 _AURORA_TARGET_ARCH)
string(TOLOWER "${_AURORA_TARGET_ARCH}" _AURORA_TARGET_ARCH)
# hw.optional.arm64 identifies Apple Silicon even when CMake itself runs
# through Rosetta, where CMAKE_HOST_SYSTEM_PROCESSOR reports x86_64.
execute_process(
COMMAND /usr/sbin/sysctl -n hw.optional.arm64
RESULT_VARIABLE _AURORA_ARM64_PROBE_RESULT
OUTPUT_VARIABLE _AURORA_ARM64_PROBE
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE)
if (_AURORA_ARM64_PROBE_RESULT EQUAL 0 AND _AURORA_ARM64_PROBE STREQUAL "1")
set(_AURORA_HOST_ARCH arm64)
else ()
execute_process(
COMMAND /usr/bin/uname -m
OUTPUT_VARIABLE _AURORA_HOST_ARCH
OUTPUT_STRIP_TRAILING_WHITESPACE)
string(TOLOWER "${_AURORA_HOST_ARCH}" _AURORA_HOST_ARCH)
endif ()
if (_AURORA_TARGET_ARCH STREQUAL "x86_64" AND _AURORA_HOST_ARCH MATCHES "^(arm64|aarch64)$")
list(APPEND CMAKE_IGNORE_PREFIX_PATH "/opt/homebrew")
set(_AURORA_EXCLUDE_HOST_HOMEBREW TRUE)
elseif (_AURORA_TARGET_ARCH MATCHES "^(arm64|aarch64)$" AND _AURORA_HOST_ARCH MATCHES "^(x86_64|amd64)$")
list(APPEND CMAKE_IGNORE_PREFIX_PATH "/usr/local")
set(_AURORA_EXCLUDE_HOST_HOMEBREW TRUE)
endif ()
endif ()
endif ()
if (_AURORA_EXCLUDE_HOST_HOMEBREW)
list(REMOVE_DUPLICATES CMAKE_IGNORE_PREFIX_PATH)
message(STATUS "aurora: cross-architecture macOS build; ignoring host Homebrew prefixes")
endif ()
if (AURORA_ENABLE_GX)
include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/AuroraDawnProvider.cmake)
endif ()
# SDL's pkg-config probe can bypass CMake's prefix exclusion. macOS does not
# need libusb for the supported SDL input paths, so keep that host-only library
# out of a cross-architecture configure.
if (_AURORA_EXCLUDE_HOST_HOMEBREW)
set(SDL_HIDAPI_LIBUSB OFF CACHE BOOL "" FORCE)
set(SDL_HIDAPI_LIBUSB_SHARED OFF CACHE BOOL "" FORCE)
endif ()
# Abseil is needed for core libraries. It normally comes via Dawn's vendor build.
# Otherwise prefer a system package and only fetch it as a last resort.
if (NOT TARGET absl::flat_hash_map OR NOT TARGET absl::btree)
+15
View File
@@ -170,6 +170,21 @@ void aurora_set_background_input(bool value);
void aurora_set_display_mode(AuroraDisplayMode mode);
AuroraDisplayMode aurora_get_display_mode();
typedef enum {
AURORA_METALFX_DISABLED,
AURORA_METALFX_UNSUPPORTED,
AURORA_METALFX_NOT_UPSCALING,
AURORA_METALFX_ACTIVE,
AURORA_METALFX_ERROR,
} AuroraMetalFXStatus;
// Changes are consumed at the next sealed frame boundary. MetalFX only applies
// when both source dimensions are smaller than the aspect-fitted output.
void aurora_set_metalfx_spatial(bool enabled);
bool aurora_get_metalfx_spatial();
bool aurora_is_metalfx_spatial_supported();
AuroraMetalFXStatus aurora_get_metalfx_status();
AuroraBackend aurora_get_backend();
const AuroraBackend* aurora_get_available_backends(size_t* count);
+150 -9
View File
@@ -7,6 +7,7 @@
#include "gx/shader_info.hpp"
#include "imgui.hpp"
#include "webgpu/gpu.hpp"
#include "webgpu/metalfx.hpp"
#include <webgpu/webgpu_cpp.h>
#endif
@@ -60,6 +61,9 @@ std::atomic<AuroraFrameWorkerWaitCallback> g_frameWorkerWaitCallback{nullptr};
// deadlines derived from it, so the presenter cannot drift. Zero means present when ready.
std::atomic<uint64_t> g_presentScheduleBaseNanos{0};
std::atomic<uint64_t> g_presentScheduleIntervalNanos{0};
std::atomic<bool> g_metalfxRequested{false};
std::atomic<bool> g_metalfxSupported{false};
std::atomic<AuroraMetalFXStatus> g_metalfxStatus{AURORA_METALFX_DISABLED};
namespace {
Module Log("aurora");
@@ -765,6 +769,9 @@ AuroraInfo initialize(int argc, char* argv[], const AuroraConfig& config) noexce
#ifdef AURORA_ENABLE_GX
gfx::initialize();
g_metalfxSupported.store(webgpu::metalfx::supported(g_device, webgpu::g_backendType));
g_metalfxStatus.store(AURORA_METALFX_DISABLED);
imgui::create_context();
#endif
const auto size = window::get_window_size();
@@ -1187,12 +1194,114 @@ void stop_presenter() noexcept {
g_presenterStarted.store(false, std::memory_order_release);
}
struct MetalFXSlot {
webgpu::metalfx::Size size{};
std::unique_ptr<webgpu::metalfx::SpatialScaler> scaler;
wgpu::BindGroup bindGroup;
};
std::array<MetalFXSlot, gx::MaxInterpolatedFrames + 1> g_metalfxSlots;
size_t g_metalfxNextSlot = 0;
webgpu::metalfx::SpatialScaler* g_metalfxPendingOutput = nullptr;
bool g_metalfxFailed = false;
void metalfx_failed(const std::string& reason) {
Log.warn("MetalFX spatial upscaling disabled: {}; using normal presentation", reason);
g_metalfxFailed = true;
g_metalfxStatus.store(AURORA_METALFX_ERROR);
g_metalfxPendingOutput = nullptr;
g_metalfxSlots = {};
}
wgpu::BindGroup upscale_presentation(wgpu::CommandEncoder& encoder,
const webgpu::PresentSource& source,
const webgpu::Viewport& viewport, bool enabled) {
if (!enabled) {
g_metalfxSlots = {};
g_metalfxFailed = false;
g_metalfxStatus.store(AURORA_METALFX_DISABLED);
return {};
}
if (!g_metalfxSupported.load()) {
g_metalfxStatus.store(AURORA_METALFX_UNSUPPORTED);
return {};
}
if (g_metalfxFailed) return {};
const webgpu::metalfx::Size size{
source.size.width, source.size.height,
static_cast<uint32_t>(viewport.width), static_cast<uint32_t>(viewport.height),
webgpu::g_graphicsConfig.surfaceConfiguration.format,
};
// The existing copy path samples perceptual values from unorm game images.
// Do not introduce implicit sRGB decoding or downscaling into MetalFX.
if (!size.inputWidth || !size.inputHeight || size.inputWidth >= size.outputWidth ||
size.inputHeight >= size.outputHeight ||
(source.format != wgpu::TextureFormat::RGBA8Unorm && source.format != wgpu::TextureFormat::BGRA8Unorm)) {
g_metalfxStatus.store(AURORA_METALFX_NOT_UPSCALING);
return {};
}
auto& slot = g_metalfxSlots[g_metalfxNextSlot++ % g_metalfxSlots.size()];
if (!slot.scaler || !(slot.size == size)) {
slot = {};
std::string error;
slot.scaler = webgpu::metalfx::create(g_instance, g_device, size, error);
if (!slot.scaler) {
if (error.empty()) g_metalfxStatus.store(AURORA_METALFX_NOT_UPSCALING);
else metalfx_failed(error);
return {};
}
slot.size = size;
wgpu::SamplerDescriptor samplerDescriptor{};
samplerDescriptor.magFilter = wgpu::FilterMode::Linear;
samplerDescriptor.minFilter = wgpu::FilterMode::Linear;
slot.bindGroup = webgpu::create_copy_bind_group(slot.scaler->output_view(),
g_device.CreateSampler(&samplerDescriptor));
Log.info("MetalFX spatial slot: {}x{} -> {}x{}", size.inputWidth, size.inputHeight,
size.outputWidth, size.outputHeight);
}
if (!slot.scaler->begin_input()) {
metalfx_failed(slot.scaler->error());
return {};
}
const wgpu::RenderPassColorAttachment attachment{
.view = slot.scaler->input_view(),
.loadOp = wgpu::LoadOp::Clear,
.storeOp = wgpu::StoreOp::Store,
};
const wgpu::RenderPassDescriptor descriptor{
.label = "MetalFX input copy",
.colorAttachmentCount = 1,
.colorAttachments = &attachment,
};
auto pass = encoder.BeginRenderPass(&descriptor);
pass.SetPipeline(webgpu::g_CopyPipeline);
pass.SetBindGroup(0, source.bindGroup);
pass.SetViewport(0, 0, static_cast<float>(size.inputWidth), static_cast<float>(size.inputHeight), 0, 1);
pass.Draw(3);
pass.End();
// Submit the sealed scene and input copy before crossing to the native queue.
// The replacement encoder composites the upscaled image and ImGui normally.
auto buffer = encoder.Finish();
{
std::lock_guard submitLock(g_queueSubmitMutex);
g_queue.Submit(1, &buffer);
}
encoder = g_device.CreateCommandEncoder();
if (!slot.scaler->upscale()) {
metalfx_failed(slot.scaler->error());
return {};
}
g_metalfxPendingOutput = slot.scaler.get();
g_metalfxStatus.store(AURORA_METALFX_ACTIVE);
return slot.bindGroup;
}
// `presentSource` is latched in the seal prologue: by the time this encodes, the producer's next
// gfx::begin_frame() may already have cleared the display-copy override.
void encode_presentation_snapshot(const wgpu::CommandEncoder& encoder,
const webgpu::PresentSource& presentSource,
const PresentationImage& image,
bool includeImGui) {
wgpu::BindGroup encode_presentation_snapshot(wgpu::CommandEncoder& encoder,
const webgpu::PresentSource& presentSource,
const PresentationImage& image,
bool includeImGui, bool metalfxEnabled,
const wgpu::BindGroup* cachedMetalFXOutput = nullptr) {
ZoneScoped;
auto viewport = webgpu::calculate_present_viewport(
image.texture.size.width, image.texture.size.height, presentSource.size.width,
@@ -1203,6 +1312,13 @@ void encode_presentation_snapshot(const wgpu::CommandEncoder& encoder,
image.texture.size.width, image.texture.size.height, presentAspect);
}
wgpu::BindGroup presentBindGroup = presentSource.bindGroup;
wgpu::BindGroup newMetalFXOutput;
if (cachedMetalFXOutput && *cachedMetalFXOutput) {
presentBindGroup = *cachedMetalFXOutput;
} else if (auto upscaled = upscale_presentation(encoder, presentSource, viewport, metalfxEnabled)) {
presentBindGroup = std::move(upscaled);
newMetalFXOutput = presentBindGroup;
}
{
const std::array attachments{
wgpu::RenderPassColorAttachment{
@@ -1243,6 +1359,7 @@ void encode_presentation_snapshot(const wgpu::CommandEncoder& encoder,
imgui::render(pass);
pass.End();
}
return newMetalFXOutput;
}
#endif
@@ -1251,6 +1368,13 @@ void shutdown() noexcept {
#ifdef AURORA_ENABLE_GX
stop_presenter();
g_presentationImagePools = {};
g_metalfxSlots = {};
g_metalfxPendingOutput = nullptr;
g_metalfxNextSlot = 0;
g_metalfxFailed = false;
g_metalfxRequested.store(false);
g_metalfxSupported.store(false);
g_metalfxStatus.store(AURORA_METALFX_DISABLED);
imgui::shutdown();
gfx::shutdown();
webgpu::shutdown();
@@ -1363,6 +1487,7 @@ struct SealedFrameContext {
uint32_t logicalFrame = 0;
bool interpolationActive = false;
bool replayInterpolatedFrames = false;
bool metalfxEnabled = false;
};
// Phase 1: everything that touches producer-shared renderer state. Needs g_rendererGpuMutex and
@@ -1390,6 +1515,7 @@ void seal_frame_locked(gfx::SealedFrame& sealedFrame, SealedFrameContext& ctx) {
ctx.snapshotWidth = (std::max)(windowSize.native_fb_width, 1u);
ctx.snapshotHeight = (std::max)(windowSize.native_fb_height, 1u);
ctx.logicalFrame = gfx::current_frame();
ctx.metalfxEnabled = g_metalfxRequested.load();
// Latched before webgpu::clear_present_source_override() in the producer's
// next gfx::begin_frame().
ctx.presentSource = webgpu::current_present_source();
@@ -1437,10 +1563,14 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
const wgpu::CommandBufferDescriptor cmdBufDescriptor{
.label = "Presentation slot command buffer",
};
const auto submitEncodedSlot = [&](wgpu::CommandEncoder& target) {
const auto submitEncodedSlot = [&](wgpu::CommandEncoder& target, bool releaseMetalFXOutput = true) {
const auto buffer = target.Finish(&cmdBufDescriptor);
std::lock_guard submitLock(g_queueSubmitMutex);
g_queue.Submit(1, &buffer);
if (releaseMetalFXOutput && g_metalfxPendingOutput) {
if (!g_metalfxPendingOutput->end_output()) metalfx_failed(g_metalfxPendingOutput->error());
g_metalfxPendingOutput = nullptr;
}
};
if (ctx.replayInterpolatedFrames) {
@@ -1449,7 +1579,7 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
gfx::render(sealedFrame, encoder, static_cast<int32_t>(interpolatedFrame), false);
auto image =
acquire_presentation_image(interpolatedFrame, ctx.snapshotWidth, ctx.snapshotHeight);
encode_presentation_snapshot(encoder, ctx.presentSource, *image, true);
encode_presentation_snapshot(encoder, ctx.presentSource, *image, true, ctx.metalfxEnabled);
presentationJobs.push_back({
.image = std::move(image),
.logicalFrame = ctx.logicalFrame,
@@ -1467,12 +1597,18 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
// The copy targets now hold this frame's resolves, so queue their readbacks on the same encoder;
// completion is harvested in gfx::after_submit, never waited on here.
gfx::efb_ram::encode_async_downloads(encoder);
wgpu::BindGroup duplicatedMetalFXOutput;
if (!ctx.replayInterpolatedFrames) {
for (uint32_t interpolatedFrame = 0; interpolatedFrame < ctx.interpolatedFrameCount;
++interpolatedFrame) {
auto image =
acquire_presentation_image(interpolatedFrame, ctx.snapshotWidth, ctx.snapshotHeight);
encode_presentation_snapshot(encoder, ctx.presentSource, *image, true);
const auto newMetalFXOutput = encode_presentation_snapshot(
encoder, ctx.presentSource, *image, true, ctx.metalfxEnabled,
duplicatedMetalFXOutput ? &duplicatedMetalFXOutput : nullptr);
if (!duplicatedMetalFXOutput && newMetalFXOutput) {
duplicatedMetalFXOutput = newMetalFXOutput;
}
presentationJobs.push_back({
.image = std::move(image),
.logicalFrame = ctx.logicalFrame,
@@ -1480,13 +1616,14 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
.interpolated = true,
.duplicated = true,
});
submitEncodedSlot(encoder);
submitEncodedSlot(encoder, false);
encoder = g_device.CreateCommandEncoder(&encoderDescriptor);
}
}
auto finalImage =
acquire_presentation_image(ctx.interpolatedFrameCount, ctx.snapshotWidth, ctx.snapshotHeight);
encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true);
encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true, ctx.metalfxEnabled,
duplicatedMetalFXOutput ? &duplicatedMetalFXOutput : nullptr);
auto pendingFrameCapture = encode_frame_capture(encoder, ctx.presentSource);
presentationJobs.push_back({
.image = std::move(finalImage),
@@ -1962,3 +2099,7 @@ void aurora_set_background_input(bool value) {
}
void aurora_set_display_mode(AuroraDisplayMode mode) { aurora::window::set_display_mode(mode); }
AuroraDisplayMode aurora_get_display_mode() { return aurora::window::get_display_mode(); }
void aurora_set_metalfx_spatial(bool enabled) { aurora::g_metalfxRequested.store(enabled); }
bool aurora_get_metalfx_spatial() { return aurora::g_metalfxRequested.load(); }
bool aurora_is_metalfx_spatial_supported() { return aurora::g_metalfxSupported.load(); }
AuroraMetalFXStatus aurora_get_metalfx_status() { return aurora::g_metalfxStatus.load(); }
+2 -2
View File
@@ -66,7 +66,7 @@ void GXCallDisplayList(const void* data, u32 nbytes) {
// Decode the display list immediately while its borrowed resources are valid.
aurora::gx::fifo::drain();
aurora::gx::fifo::process(static_cast<const u8*>(data), nbytes, true);
aurora::gx::fifo::process_all(static_cast<const u8*>(data), nbytes, true);
}
void GXCallDisplayListLE(const void* data, u32 nbytes) {
@@ -84,6 +84,6 @@ void GXCallDisplayListLE(const void* data, u32 nbytes) {
aurora::gx::fifo::drain();
// Process the display list through the command processor (little-endian)
aurora::gx::fifo::process(static_cast<const u8*>(data), nbytes, false);
aurora::gx::fifo::process_all(static_cast<const u8*>(data), nbytes, false);
}
}
+30 -24
View File
@@ -6,10 +6,10 @@
#include <SDL3/SDL_mouse.h>
#include <SDL3/SDL_joystick.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <sys/stat.h>
#include <ranges>
namespace {
constexpr int32_t k_mappingsFileVersion = 3;
@@ -354,7 +354,7 @@ BOOL PADInit() {
}
g_initialized = true;
std::ranges::for_each(g_keyboardBindings, [](auto& state) {
std::for_each(g_keyboardBindings.begin(), g_keyboardBindings.end(), [](auto& state) {
state.m_buttonMapping = g_defaultKeys;
state.m_axisMapping = g_defaultKeyAxis;
});
@@ -647,8 +647,8 @@ static void EnsureMappingLoaded(aurora::input::GameController* controller) {
static Sint16 _get_axis_value(const aurora::input::GameController* controller, // NOLINT(*-reserved-identifier)
PADAxis axis) {
const auto iter =
std::ranges::find_if(controller->m_axisMapping, [axis](const auto& pair) { return pair.padAxis == axis; });
const auto iter = std::find_if(controller->m_axisMapping.begin(), controller->m_axisMapping.end(),
[axis](const auto& pair) { return pair.padAxis == axis; });
if (iter == controller->m_axisMapping.end()) {
return 0;
}
@@ -738,8 +738,8 @@ u32 PADRead(PADStatus* status) {
status[i].err = PAD_ERR_NONE;
if (g_keyboardBindings[i].m_mappingsSet && SDL_GetKeyboardFocus() != nullptr) {
std::ranges::for_each(
g_keyboardBindings[i].m_buttonMapping, [&kbState, &numKeys, &i, &status](const PADKeyButtonBinding& mapping) {
std::for_each(g_keyboardBindings[i].m_buttonMapping.begin(), g_keyboardBindings[i].m_buttonMapping.end(),
[&kbState, &numKeys, &i, &status](const PADKeyButtonBinding& mapping) {
if (mapping.scancode > PAD_KEY_INVALID && mapping.scancode < numKeys && kbState[mapping.scancode]) {
status[i].button |= mapping.padButton;
} else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) {
@@ -846,8 +846,8 @@ u32 PADRead(PADStatus* status) {
bool leftTriggerSet = false;
bool rightTriggerSet = false;
std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet,
&rightTriggerSet](const auto& mapping) {
std::for_each(controller->m_buttonMapping.begin(), controller->m_buttonMapping.end(),
[&controller, &i, &status, &leftTriggerSet, &rightTriggerSet](const auto& mapping) {
if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) {
status[i].button |= mapping.padButton;
}
@@ -860,8 +860,8 @@ u32 PADRead(PADStatus* status) {
}
});
std::ranges::for_each(controller->m_altButtonMapping, [&controller, &i, &status, &leftTriggerSet,
&rightTriggerSet](const auto& mapping) {
std::for_each(controller->m_altButtonMapping.begin(), controller->m_altButtonMapping.end(),
[&controller, &i, &status, &leftTriggerSet, &rightTriggerSet](const auto& mapping) {
if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) {
return;
}
@@ -1190,8 +1190,8 @@ void PADSetButtonMapping(const u32 port, const PADButtonMapping mapping) {
return;
}
const auto iter = std::ranges::find_if(controller->m_buttonMapping,
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
const auto iter = std::find_if(controller->m_buttonMapping.begin(), controller->m_buttonMapping.end(),
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
if (iter == controller->m_buttonMapping.end()) {
return;
}
@@ -1224,8 +1224,8 @@ void PADSetAltButtonMapping(const u32 port, const PADButtonMapping mapping) {
return;
}
const auto iter = std::ranges::find_if(controller->m_altButtonMapping,
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
const auto iter = std::find_if(controller->m_altButtonMapping.begin(), controller->m_altButtonMapping.end(),
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
if (iter == controller->m_altButtonMapping.end()) {
return;
}
@@ -1251,8 +1251,8 @@ void PADSetAxisMapping(const u32 port, const PADAxisMapping mapping) {
return;
}
const auto iter = std::ranges::find_if(controller->m_axisMapping,
[mapping](const auto& pair) { return mapping.padAxis == pair.padAxis; });
const auto iter = std::find_if(controller->m_axisMapping.begin(), controller->m_axisMapping.end(),
[mapping](const auto& pair) { return mapping.padAxis == pair.padAxis; });
if (iter == controller->m_axisMapping.end()) {
return;
}
@@ -1426,9 +1426,10 @@ static void load_keyboard_bindings() {
if (mappingsSet) {
const bool anyBound =
std::ranges::any_of(buttonMapping,
[](const PADKeyButtonBinding& b) { return b.scancode != PAD_KEY_INVALID; }) ||
std::ranges::any_of(axisMapping, [](const PADKeyAxisBinding& b) { return b.scancode != PAD_KEY_INVALID; });
std::any_of(buttonMapping.begin(), buttonMapping.end(),
[](const PADKeyButtonBinding& b) { return b.scancode != PAD_KEY_INVALID; }) ||
std::any_of(axisMapping.begin(), axisMapping.end(),
[](const PADKeyAxisBinding& b) { return b.scancode != PAD_KEY_INVALID; });
if (!anyBound) {
mappingsSet = false;
}
@@ -1468,7 +1469,10 @@ void __PADWriteDeadZones(SDL_IOStream* file, // NOLINT(*-reserved-identifier)
void PADSerializeMappings() {
const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath);
for (auto& controller : aurora::input::g_GameControllers | std::views::values) {
// Avoid std::views::values here: older Apple libc++ releases implement the
// C++20 ranges algorithms we use but not this adaptor.
for (auto& entry : aurora::input::g_GameControllers) {
auto& controller = entry.second;
EnsureMappingLoaded(&controller);
const auto filePath =
basePath / fmt::format("{}_{:04X}_{:04X}.controller", aurora::input::controller_name(controller.m_index),
@@ -1570,8 +1574,8 @@ static constexpr std::array<std::pair<PADButton, std::string_view>, PAD_AXIS_COU
const char* PADGetButtonName(const PADButton button) {
if (const auto iter =
std::ranges::find_if(skButtonNames, [&button](const auto& pair) { return button == pair.first; });
if (const auto iter = std::find_if(skButtonNames.begin(), skButtonNames.end(),
[&button](const auto& pair) { return button == pair.first; });
iter != skButtonNames.end()) {
return iter->second.data();
}
@@ -1584,7 +1588,8 @@ const char* PADGetNativeButtonName(u32 button) {
}
const char* PADGetAxisName(const PADAxis axis) {
if (const auto it = std::ranges::find_if(skAxisNames, [&axis](const auto& pair) { return axis == pair.first; });
if (const auto it = std::find_if(skAxisNames.begin(), skAxisNames.end(),
[&axis](const auto& pair) { return axis == pair.first; });
it != skAxisNames.end()) {
return it->second.data();
}
@@ -1593,7 +1598,8 @@ const char* PADGetAxisName(const PADAxis axis) {
}
const char* PADGetAxisDirectionLabel(const PADAxis axis) {
if (const auto it = std::ranges::find_if(skAxisDirLabels, [&axis](const auto& pair) { return axis == pair.first; });
if (const auto it = std::find_if(skAxisDirLabels.begin(), skAxisDirLabels.end(),
[&axis](const auto& pair) { return axis == pair.first; });
it != skAxisDirLabels.end()) {
return it->second.data();
}
+2 -3
View File
@@ -23,7 +23,6 @@
#include <memory>
#include <mutex>
#include <optional>
#include <ranges>
#include <absl/container/flat_hash_map.h>
#include <magic_enum.hpp>
@@ -1448,8 +1447,8 @@ static void render_impl(std::vector<RenderPass>& renderPasses, wgpu::CommandEnco
#if defined(AURORA_GFX_DEBUG_GROUPS)
if (finalize && !debugFrame.groups.empty()) {
for (auto& it : std::ranges::reverse_view(debugFrame.groups)) {
Log.warn("Debug group was not popped at end of frame: {}", it);
for (auto it = debugFrame.groups.rbegin(); it != debugFrame.groups.rend(); ++it) {
Log.warn("Debug group was not popped at end of frame: {}", *it);
}
debugFrame.groups.clear();
}
+1 -1
View File
@@ -553,7 +553,7 @@ void build_index() noexcept {
continue;
}
s_replacementIndex.try_emplace(*parsed, path);
s_replacementIndex.try_emplace(*parsed, ReplacementIndexEntry{path});
}
Log.info("Indexed {} texture replacements", s_replacementIndex.size());
+8 -4
View File
@@ -82,19 +82,23 @@ void drain() {
if (detail::sBufferSize == 0) {
return;
}
process_all(detail::sBufferData, detail::sBufferSize, true);
detail::sBufferSize = 0;
}
void process_all(const uint8_t* data, uint32_t size, bool bigEndian) {
uint32_t consumed = 0;
bool retried = false;
while (consumed < detail::sBufferSize) {
const auto count = process(detail::sBufferData + consumed, detail::sBufferSize - consumed, true);
while (consumed < size) {
const auto count = process(data + consumed, size - consumed, bigEndian);
if (count == 0 && retried)
throw gfx::StagingCapacityError("FIFO draw does not fit after capacity submission");
consumed += count;
if (consumed == detail::sBufferSize) break;
if (consumed == size) break;
// process returned with its renderer lock released. No recursive drain.
gfx::split_staging_batch();
retried = true;
}
detail::sBufferSize = 0;
}
const uint8_t* get_buffer_data() { return detail::sBufferData; }
+4
View File
@@ -81,6 +81,10 @@ bool in_display_list();
// Drain the internal FIFO buffer through the command processor
void drain();
// Decode a whole command stream, submitting the staging batch whenever a draw does not fit.
// process() alone stops at that draw, so a caller that ignores its count drops the rest.
void process_all(const uint8_t* data, uint32_t size, bool bigEndian);
// Internal buffer inspection
const uint8_t* get_buffer_data();
uint32_t get_buffer_size();
+1 -1
View File
@@ -235,7 +235,7 @@ std::string GetOSVersion() {
constexpr auto name = "iOS";
#elif TARGET_OS_TV
constexpr auto name = "tvOS";
#elif
#else
constexpr auto name = Unknown;
#endif
+8
View File
@@ -642,6 +642,14 @@ bool initialize(AuroraBackend auroraBackend) {
requiredLimits.maxDynamicStorageBuffersPerPipelineLayout, requiredLimits.maxStorageBuffersPerShaderStage,
requiredLimits.minUniformBufferOffsetAlignment, requiredLimits.minStorageBufferOffsetAlignment);
std::vector<wgpu::FeatureName> requiredFeatures;
// Optional native sharing for MetalFX. Devices without either feature keep
// the normal renderer; the upscaler checks the enabled pair at runtime.
if (backend == wgpu::BackendType::Metal &&
g_adapter.HasFeature(wgpu::FeatureName::SharedTextureMemoryIOSurface) &&
g_adapter.HasFeature(wgpu::FeatureName::SharedFenceMTLSharedEvent)) {
requiredFeatures.push_back(wgpu::FeatureName::SharedTextureMemoryIOSurface);
requiredFeatures.push_back(wgpu::FeatureName::SharedFenceMTLSharedEvent);
}
bool implicitDeviceSynchronizationSupported = false;
wgpu::SupportedFeatures supportedFeatures;
g_adapter.GetFeatures(&supportedFeatures);
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <webgpu/webgpu_cpp.h>
#include <memory>
#include <string>
namespace aurora::webgpu::metalfx {
struct Size {
uint32_t inputWidth;
uint32_t inputHeight;
uint32_t outputWidth;
uint32_t outputHeight;
wgpu::TextureFormat format;
bool operator==(const Size&) const = default;
};
// All methods except supported() belong to the serialized frame encoder.
// GPU ownership is explicit: begin_input -> submit input -> upscale ->
// submit output consumption -> end_output. Neither texture may be used by
// Dawn outside its access interval. Destruction retires in-flight resources.
class SpatialScaler {
public:
virtual ~SpatialScaler() = default;
virtual const wgpu::TextureView& input_view() const = 0;
virtual const wgpu::TextureView& output_view() const = 0;
virtual const wgpu::Texture& output_texture() const = 0;
virtual bool begin_input() = 0;
virtual bool upscale() = 0;
virtual bool end_output() = 0;
virtual const std::string& error() const = 0;
};
bool supported(const wgpu::Device& device, wgpu::BackendType backend);
// A null result with no error means the bounded retirement pool is busy;
// skip upscaling for this frame and retry at a later frame boundary.
std::unique_ptr<SpatialScaler> create(const wgpu::Instance& instance,
const wgpu::Device& device, const Size& size,
std::string& error);
} // namespace aurora::webgpu::metalfx
+326
View File
@@ -0,0 +1,326 @@
#include "metalfx.hpp"
#import <Foundation/Foundation.h>
#import <IOSurface/IOSurface.h>
#import <Metal/Metal.h>
#import <MetalFX/MetalFX.h>
#include <dawn/native/MetalBackend.h>
#include <atomic>
#include <string_view>
namespace aurora::webgpu::metalfx {
namespace {
constexpr uint64_t kScheduleTimeoutNs = 1'000'000'000;
// Four current slots plus at most four retiring slots during resize. A busy
// GPU must not allow resize events to allocate unbounded full-resolution images.
constexpr unsigned kMaxLiveResources = 8;
std::atomic<unsigned> g_liveResources{0};
struct SharedImage {
IOSurfaceRef surface = nullptr;
id<MTLTexture> metal;
wgpu::SharedTextureMemory memory;
wgpu::Texture texture;
wgpu::TextureView view;
~SharedImage() { if (surface) CFRelease(surface); }
bool create(const wgpu::Device& device, id<MTLDevice> native, uint32_t width,
uint32_t height, wgpu::TextureFormat format, MTLTextureUsage nativeUsage,
wgpu::TextureUsage usage) {
const bool bgra = format == wgpu::TextureFormat::BGRA8Unorm;
const size_t rowBytes = IOSurfaceAlignProperty(kIOSurfaceBytesPerRow, size_t(width) * 4);
NSDictionary* properties = @{
(id)kIOSurfaceWidth: @(width), (id)kIOSurfaceHeight: @(height),
(id)kIOSurfaceBytesPerElement: @4, (id)kIOSurfaceBytesPerRow: @(rowBytes),
(id)kIOSurfaceAllocSize: @(rowBytes * height),
(id)kIOSurfacePixelFormat: @(bgra ? 0x42475241u : 0x52474241u)
};
surface = IOSurfaceCreate((__bridge CFDictionaryRef)properties);
if (!surface) return false;
auto descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:
bgra ? MTLPixelFormatBGRA8Unorm : MTLPixelFormatRGBA8Unorm
width:width height:height mipmapped:NO];
descriptor.storageMode = MTLStorageModeShared;
descriptor.usage = nativeUsage;
metal = [native newTextureWithDescriptor:descriptor iosurface:surface plane:0];
if (!metal) return false;
wgpu::SharedTextureMemoryIOSurfaceDescriptor io{};
io.ioSurface = surface;
io.allowStorageBinding = false;
wgpu::SharedTextureMemoryDescriptor importDescriptor{};
importDescriptor.nextInChain = &io;
memory = device.ImportSharedTextureMemory(&importDescriptor);
wgpu::SharedTextureMemoryProperties actual{};
if (!memory || memory.GetProperties(&actual) != wgpu::Status::Success ||
actual.format != format || actual.size.width != width || actual.size.height != height ||
(actual.usage & usage) != usage) return false;
wgpu::TextureDescriptor textureDescriptor{};
textureDescriptor.label = "MetalFX shared texture";
textureDescriptor.size = {width, height, 1};
textureDescriptor.format = format;
textureDescriptor.usage = usage;
texture = memory.CreateTexture(&textureDescriptor);
if (!texture) return false;
view = texture.CreateView();
return view != nullptr;
}
};
struct API_AVAILABLE(macos(13.0)) Resources {
SharedImage input, output;
id<MTLFXSpatialScaler> scaler;
id<MTLTexture> privateOutput;
id<MTLCommandQueue> nativeQueue;
id<MTLSharedEvent> event;
wgpu::SharedFence fence;
std::atomic<bool> failed{false};
Resources() { ++g_liveResources; }
~Resources() { --g_liveResources; }
};
class API_AVAILABLE(macos(13.0)) MetalSpatialScaler final : public SpatialScaler {
wgpu::Instance m_instance;
wgpu::Queue m_queue;
std::shared_ptr<Resources> m_resources;
wgpu::SharedTextureMemoryEndAccessState m_outputReleased{};
wgpu::Future m_outputScheduled{};
uint64_t m_value = 0;
std::string m_error;
bool fail(const char* reason) {
m_error = reason;
m_resources->failed = true;
return false;
}
bool wait_scheduled(wgpu::Future future) {
return m_instance.WaitAny(future, kScheduleTimeoutNs) == wgpu::WaitStatus::Success ||
fail("Timed out scheduling MetalFX GPU work");
}
bool end_access(SharedImage& image, wgpu::SharedTextureMemoryEndAccessState& state,
wgpu::Future& scheduled) {
wgpu::SharedTextureMemoryMetalEndAccessState metal{};
state.nextInChain = &metal;
const auto status = image.memory.EndAccess(image.texture, &state);
state.nextInChain = nullptr;
scheduled = metal.commandsScheduledFuture;
return status == wgpu::Status::Success || fail("MetalFX Dawn EndAccess failed");
}
bool begin_access(SharedImage& image, bool initialized, uint64_t value) {
wgpu::SharedTextureMemoryBeginAccessDescriptor access{};
access.initialized = initialized;
if (value) {
access.fenceCount = 1;
access.fences = &m_resources->fence;
access.signaledValueCount = 1;
access.signaledValues = &value;
}
return image.memory.BeginAccess(image.texture, &access) == wgpu::Status::Success ||
fail("MetalFX Dawn BeginAccess failed");
}
bool encode_waits(id<MTLCommandBuffer> commands,
const wgpu::SharedTextureMemoryEndAccessState& state) {
for (size_t i = 0; i < state.fenceCount; ++i) {
wgpu::SharedFenceMTLSharedEventExportInfo metal{};
wgpu::SharedFenceExportInfo info{};
info.nextInChain = &metal;
state.fences[i].ExportInfo(&info);
if (info.type != wgpu::SharedFenceType::MTLSharedEvent || !metal.sharedEvent)
return fail("Dawn did not export a MetalFX shared-event dependency");
[commands encodeWaitForEvent:(__bridge id<MTLSharedEvent>)metal.sharedEvent
value:state.signaledValues[i]];
}
return true;
}
void retain_until_dawn_done() {
// A resize/toggle can destroy this wrapper immediately. The last submitted
// Dawn consumer keeps the IOSurfaces/scaler alive independently of the cache.
m_queue.OnSubmittedWorkDone(wgpu::CallbackMode::AllowSpontaneous,
[resources = m_resources](wgpu::QueueWorkDoneStatus status, wgpu::StringView) {
if (status != wgpu::QueueWorkDoneStatus::Success) resources->failed = true;
});
}
public:
MetalSpatialScaler(const wgpu::Instance& instance, const wgpu::Device& device,
std::shared_ptr<Resources> resources)
: m_instance(instance), m_queue(device.GetQueue()), m_resources(std::move(resources)) {}
const wgpu::TextureView& input_view() const override { return m_resources->input.view; }
const wgpu::TextureView& output_view() const override { return m_resources->output.view; }
const wgpu::Texture& output_texture() const override { return m_resources->output.texture; }
const std::string& error() const override { return m_error; }
bool begin_input() override {
if (m_resources->failed) return fail("Previous MetalFX GPU work failed");
return begin_access(m_resources->input, m_value != 0, m_value);
}
bool upscale() override {
@autoreleasepool {
// Input has already been submitted. Retain it even if an export or native
// allocation fails and the caller immediately falls back to normal copy.
retain_until_dawn_done();
wgpu::SharedTextureMemoryEndAccessState inputReleased{};
wgpu::Future inputScheduled{};
if (!end_access(m_resources->input, inputReleased, inputScheduled) ||
!wait_scheduled(inputScheduled)) return false;
if (m_value && !wait_scheduled(m_outputScheduled)) return false;
id<MTLCommandBuffer> commands = [m_resources->nativeQueue commandBuffer];
if (!commands) return fail("Could not allocate a MetalFX command buffer");
commands.label = @"MetalFX spatial upscale and return to Dawn";
if (!encode_waits(commands, inputReleased) || !encode_waits(commands, m_outputReleased))
return false;
[m_resources->scaler encodeToCommandBuffer:commands];
id<MTLBlitCommandEncoder> blit = [commands blitCommandEncoder];
if (!blit) return fail("Could not allocate the MetalFX output blit");
[blit copyFromTexture:m_resources->privateOutput sourceSlice:0 sourceLevel:0
sourceOrigin:MTLOriginMake(0, 0, 0)
sourceSize:MTLSizeMake(m_resources->privateOutput.width, m_resources->privateOutput.height, 1)
toTexture:m_resources->output.metal destinationSlice:0 destinationLevel:0
destinationOrigin:MTLOriginMake(0, 0, 0)];
[blit endEncoding];
++m_value;
[commands encodeSignalEvent:m_resources->event value:m_value];
const auto resources = m_resources;
[commands addCompletedHandler:^(id<MTLCommandBuffer> completed) {
if (completed.status == MTLCommandBufferStatusError) resources->failed = true;
}];
[commands commit];
// Scheduling is required to order independent Metal queues. Completion
// remains asynchronous; resource reuse is guarded by shared GPU events.
[commands waitUntilScheduled];
if (commands.status == MTLCommandBufferStatusError)
return fail("MetalFX command buffer failed");
return begin_access(m_resources->output, true, m_value);
}
}
bool end_output() override {
retain_until_dawn_done();
m_outputReleased = {};
return end_access(m_resources->output, m_outputReleased, m_outputScheduled);
}
};
// Allocation failures must be caught here rather than reaching Aurora's fatal
// uncaptured-error callback. Scope callbacks own their strings even on timeout.
bool pop_scope(const wgpu::Instance& instance, const wgpu::Device& device, std::string& error) {
auto message = std::make_shared<std::string>();
auto future = device.PopErrorScope(wgpu::CallbackMode::WaitAnyOnly,
[message](wgpu::PopErrorScopeStatus status, wgpu::ErrorType type, wgpu::StringView text) {
if (status != wgpu::PopErrorScopeStatus::Success || type != wgpu::ErrorType::NoError) {
const std::string_view detail{text};
*message = detail.empty() ? "MetalFX texture allocation failed" : std::string(detail);
}
});
if (instance.WaitAny(future, kScheduleTimeoutNs) != wgpu::WaitStatus::Success) {
error = "Timed out checking MetalFX texture allocation";
return false;
}
if (!message->empty()) { error = *message; return false; }
return true;
}
} // namespace
bool supported(const wgpu::Device& device, wgpu::BackendType backend) {
if (@available(macOS 13.0, *)) {
if (!device || backend != wgpu::BackendType::Metal ||
!device.HasFeature(wgpu::FeatureName::SharedTextureMemoryIOSurface) ||
!device.HasFeature(wgpu::FeatureName::SharedFenceMTLSharedEvent)) return false;
auto native = dawn::native::metal::GetMTLDevice(device.Get());
return native && [MTLFXSpatialScalerDescriptor supportsDevice:native];
}
return false;
}
std::unique_ptr<SpatialScaler> create(const wgpu::Instance& instance,
const wgpu::Device& device, const Size& size,
std::string& error) {
error.clear();
if (@available(macOS 13.0, *)) {
@autoreleasepool {
if (!supported(device, wgpu::BackendType::Metal)) {
error = "MetalFX spatial scaling is unsupported";
return {};
}
wgpu::Limits limits{};
device.GetLimits(&limits);
if (!size.inputWidth || !size.inputHeight || size.inputWidth >= size.outputWidth ||
size.inputHeight >= size.outputHeight || size.outputWidth > limits.maxTextureDimension2D ||
size.outputHeight > limits.maxTextureDimension2D ||
(size.format != wgpu::TextureFormat::RGBA8Unorm && size.format != wgpu::TextureFormat::BGRA8Unorm)) {
error = "MetalFX requires smaller input dimensions and an RGBA8/BGRA8 unorm target";
return {};
}
if (g_liveResources.load() >= kMaxLiveResources) {
return {};
}
auto native = dawn::native::metal::GetMTLDevice(device.Get());
auto resources = std::make_shared<Resources>();
auto descriptor = [MTLFXSpatialScalerDescriptor new];
descriptor.inputWidth = size.inputWidth;
descriptor.inputHeight = size.inputHeight;
descriptor.outputWidth = size.outputWidth;
descriptor.outputHeight = size.outputHeight;
descriptor.colorTextureFormat = size.format == wgpu::TextureFormat::BGRA8Unorm
? MTLPixelFormatBGRA8Unorm : MTLPixelFormatRGBA8Unorm;
descriptor.outputTextureFormat = descriptor.colorTextureFormat;
descriptor.colorProcessingMode = MTLFXSpatialScalerColorProcessingModePerceptual;
resources->scaler = [descriptor newSpatialScalerWithDevice:native];
resources->nativeQueue = [native newCommandQueue];
resources->event = [native newSharedEvent];
if (!resources->scaler || !resources->nativeQueue || !resources->event) {
error = "Could not create MetalFX spatial resources";
return {};
}
auto outputDescriptor = [MTLTextureDescriptor
texture2DDescriptorWithPixelFormat:descriptor.outputTextureFormat
width:size.outputWidth height:size.outputHeight mipmapped:NO];
outputDescriptor.storageMode = MTLStorageModePrivate;
outputDescriptor.usage = resources->scaler.outputTextureUsage;
resources->privateOutput = [native newTextureWithDescriptor:outputDescriptor];
if (!resources->privateOutput) { error = "Could not allocate MetalFX private output"; return {}; }
device.PushErrorScope(wgpu::ErrorFilter::Validation);
device.PushErrorScope(wgpu::ErrorFilter::OutOfMemory);
device.PushErrorScope(wgpu::ErrorFilter::Internal);
bool allocated = resources->input.create(device, native, size.inputWidth, size.inputHeight,
size.format, resources->scaler.colorTextureUsage, wgpu::TextureUsage::RenderAttachment);
allocated = allocated && resources->output.create(device, native, size.outputWidth, size.outputHeight,
size.format, MTLTextureUsageShaderRead, wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc);
if (allocated) {
wgpu::SharedFenceMTLSharedEventDescriptor event{};
event.sharedEvent = (__bridge void*)resources->event;
wgpu::SharedFenceDescriptor fence{};
fence.nextInChain = &event;
resources->fence = device.ImportSharedFence(&fence);
allocated = resources->fence != nullptr;
}
for (int i = 0; i < 3; ++i) {
if (!pop_scope(instance, device, error)) allocated = false;
}
if (!allocated) {
if (error.empty()) error = "Could not import MetalFX IOSurface textures into Dawn";
return {};
}
resources->scaler.colorTexture = resources->input.metal;
resources->scaler.outputTexture = resources->privateOutput;
resources->scaler.inputContentWidth = size.inputWidth;
resources->scaler.inputContentHeight = size.inputHeight;
return std::make_unique<MetalSpatialScaler>(instance, device, std::move(resources));
}
}
error = "MetalFX requires macOS 13 or newer";
return {};
}
} // namespace aurora::webgpu::metalfx
+11
View File
@@ -0,0 +1,11 @@
#include "metalfx.hpp"
namespace aurora::webgpu::metalfx {
bool supported(const wgpu::Device&, wgpu::BackendType) { return false; }
std::unique_ptr<SpatialScaler> create(const wgpu::Instance&, const wgpu::Device&,
const Size&, std::string& error) {
error = "MetalFX is not available in this build";
return {};
}
} // namespace aurora::webgpu::metalfx
+4
View File
@@ -50,6 +50,9 @@ void reset_uniform_allocations() noexcept;
const std::vector<uint8_t>& uniform_allocation(size_t index) noexcept;
void use_draw_command_tracking(bool enabled) noexcept;
void use_real_vertex_format_helpers(bool enabled) noexcept;
// Admit this many draws, then refuse one as if the staging batch were full.
void refuse_staging_admission_after(uint32_t admissions) noexcept;
void reset_staging_capacity() noexcept;
} // namespace aurora::gfx::testing
class GXFifoTest : public ::testing::Test {
@@ -65,6 +68,7 @@ protected:
aurora::gfx::testing::reset_resolve_pass_records();
aurora::gfx::testing::reset_vertex_push_record();
aurora::gfx::testing::use_real_vertex_format_helpers(false);
aurora::gfx::testing::reset_staging_capacity();
}
// Copy the internal FIFO buffer contents and clear it
+30 -2
View File
@@ -34,6 +34,9 @@ std::optional<aurora::gx::DrawData> s_lastGxDraw;
bool s_trackDrawCommands = false;
bool s_useRealVertexFormatHelpers = false;
std::deque<std::vector<uint8_t>> s_uniformAllocations;
std::optional<uint32_t> s_stagingAdmissionsBeforeRefusal;
bool s_stagingRefused = false;
uint64_t s_stagingSplitCount = 0;
} // namespace
// --- aurora::g_config ---
@@ -301,8 +304,23 @@ std::pair<ByteBuffer, Range> copy_uniform(Range source) {
uint32_t align_uniform(uint32_t value) { return (value + 255u) & ~255u; }
uint64_t staging_uniform_bytes(uint64_t value) { return staging_padded(value, 256); }
uint64_t staging_storage_bytes(uint64_t value) { return staging_padded(value, 256); }
bool staging_has_space(const StagingSizes&) { return true; }
void split_staging_batch() { throw StagingCapacityError("Unexpected split in FIFO unit test"); }
bool staging_has_space(const StagingSizes&) {
if (!s_stagingAdmissionsBeforeRefusal) return true;
if (*s_stagingAdmissionsBeforeRefusal > 0) {
--*s_stagingAdmissionsBeforeRefusal;
return true;
}
// Refuse once, as a full batch would; the split that follows makes room again.
s_stagingAdmissionsBeforeRefusal.reset();
s_stagingRefused = true;
return false;
}
void split_staging_batch() {
if (!s_stagingRefused) throw StagingCapacityError("Unexpected split in FIFO unit test");
s_stagingRefused = false;
++s_stagingSplitCount;
}
uint64_t staging_split_count() noexcept { return s_stagingSplitCount; }
Vec2<uint32_t> get_render_target_size() noexcept { return s_renderTargetSize; }
Vec2<uint32_t> get_frame_buffer_size() noexcept { return s_renderTargetSize; }
@@ -345,6 +363,15 @@ void use_draw_command_tracking(bool enabled) noexcept {
void use_real_vertex_format_helpers(bool enabled) noexcept {
s_useRealVertexFormatHelpers = enabled;
}
void refuse_staging_admission_after(uint32_t admissions) noexcept {
s_stagingAdmissionsBeforeRefusal = admissions;
s_stagingRefused = false;
}
void reset_staging_capacity() noexcept {
s_stagingAdmissionsBeforeRefusal.reset();
s_stagingRefused = false;
s_stagingSplitCount = 0;
}
} // namespace aurora::gfx::testing
// --- Pipeline/draw command stubs ---
@@ -525,6 +552,7 @@ std::optional<TextureHandle> find_replacement(const GXTexObj_&) noexcept { retur
namespace aurora::window {
AuroraWindowSize get_window_size() { return {640, 480, 640, 480, 640, 480, 1.0f}; }
void set_frame_buffer_aspect_fit(bool) {}
void set_force_aspect_16_9(bool) {}
} // namespace aurora::window
// --- WebGPU C API stubs (prevent linker errors from wgpu:: destructors) ---
@@ -0,0 +1,28 @@
cmake_minimum_required(VERSION 3.25)
project(metalfx_interop LANGUAGES CXX)
if(NOT APPLE)
message(FATAL_ERROR "The MetalFX interoperability probe requires macOS")
endif()
enable_language(OBJCXX)
set(CMAKE_OBJCXX_STANDARD 20)
set(CMAKE_OBJCXX_STANDARD_REQUIRED ON)
set(CMAKE_OSX_DEPLOYMENT_TARGET 13.0)
# Use the same Dawn package as Aurora; no separate download or renderer build.
find_package(Threads REQUIRED)
find_package(Dawn CONFIG REQUIRED)
add_executable(metalfx_interop main.mm ../../lib/webgpu/metalfx.mm)
target_include_directories(metalfx_interop PRIVATE ../../lib)
target_compile_options(metalfx_interop PRIVATE -fobjc-arc -Wall -Wextra)
target_link_libraries(metalfx_interop PRIVATE dawn::webgpu_dawn
"-framework MetalFX" "-framework Metal" "-framework IOSurface" "-framework Foundation")
enable_testing()
add_test(NAME metalfx_interop COMMAND metalfx_interop)
set_tests_properties(metalfx_interop PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
add_executable(metalfx_stub_test stub_test.cpp ../../lib/webgpu/metalfx_stub.cpp)
target_include_directories(metalfx_stub_test PRIVATE ../../lib)
target_compile_features(metalfx_stub_test PRIVATE cxx_std_20)
target_link_libraries(metalfx_stub_test PRIVATE dawn::webgpu_dawn)
add_test(NAME metalfx_stub COMMAND metalfx_stub_test)
+124
View File
@@ -0,0 +1,124 @@
# MetalFX spatial upscaling: renderer integration and tests
Aurora can now upscale the completed game image with MetalFX before aspect-fit
presentation and ImGui composition. It is opt-in and requires macOS 13+, a
supported Metal device, and Dawn IOSurface/shared-event support. Other backends
and builds without the MetalFX SDK use a stub and the existing presentation path.
The MetalFX framework is weak-linked; the game's deployment target is unchanged.
## Trying the renderer integration
Use F10 → Graphics → MetalFX spatial upscaling. Then use the existing
Resolution control to render below the output viewport's size.
Both source dimensions must be smaller than the output dimensions. Equal-size
rendering, supersampling, and unsupported source formats bypass MetalFX. In
particular, Auto (window size) generally offers no upscaling opportunity.
The F10 toggle is saved in `Config.toml` as
`video.metalfx_spatial_upscaling`. It uses these thread-safe Aurora entry points:
- `aurora_set_metalfx_spatial(bool)` requests a change at the next sealed frame.
- `aurora_get_metalfx_spatial()` returns the requested setting.
- `aurora_is_metalfx_spatial_supported()` reports device/build support.
- `aurora_get_metalfx_status()` distinguishes Disabled, Unsupported,
Not Upscaling, Active, and Error. A busy resize-retirement pool temporarily
bypasses upscaling and retries on a later frame. Other upscaler errors log a
reason and use normal presentation until a disabled frame resets the error.
The game's HUD is part of the source image and is upscaled. ImGui/F10/FPS overlays
are composed afterward at output resolution. Existing source-frame captures
still capture the original source image. The interpolation snapshot call sites
all use the same upscaling hook; game-specific interpolation remains untested.
## GPU path and ownership
1. Request Dawn's `SharedTextureMemoryIOSurface` and `SharedFenceMTLSharedEvent`
features when the Metal adapter supports both. Use that Dawn device's native
`MTLDevice`, not a separately selected default device.
2. Cache MaxInterpolatedFrames + 1 upscaling slots with IOSurface-backed input and output textures,
a spatial scaler, a private MetalFX output, and shared-event dependencies.
Check texture formats, dimensions, usages, and device size limits on creation.
3. Begin Dawn input access, copy the completed game image at its source size,
and submit the scene plus copy. End input access and wait for Dawn's
`commandsScheduledFuture` before submitting dependent native Metal work.
4. On the native queue, wait for input rendering and any prior Dawn consumption
of the shared output. Encode MetalFX into its required **private** output
texture, then GPU-blit the result into the output IOSurface and signal an event.
5. After native scheduling, begin Dawn output access with that event/value.
Composite the upscaled image into the existing content viewport, retaining
letterboxing, then draw ImGui. Submit and end output access. Reuse observes
both Dawn-to-Metal and Metal-to-Dawn event dependencies.
6. GPU completion callbacks retain resources after a cache entry is replaced,
disabled, or shut down. At most eight resource sets may exist (four current
plus four retiring); rapid resizing cannot allocate an unbounded queue.
CPU scheduling waits remain, but there are no CPU image transfers or per-frame
GPU-completion waits in the upscaling path. There is one source-size GPU copy
and one full-output GPU blit. Their cost must be measured before promising a
performance gain. The input copy follows the existing perceptual/unorm sampling
path. sRGB texture formats bypass MetalFX to avoid implicit color conversion.
## Standalone GPU regression test
The test builds the actual `lib/webgpu/metalfx.mm` implementation. Point
`Dawn_DIR` at the package used by an existing Aurora build:
```sh
cmake -S aurora-main/tests/metalfx_interop -B build-metalfx-interop \
-DDawn_DIR="/absolute/path/to/dawn_prebuilt-src/lib/cmake/Dawn" \
-DCMAKE_BUILD_TYPE=Release
cmake --build build-metalfx-interop
MTL_DEBUG_LAYER=1 MTL_SHADER_VALIDATION=1 \
ctest --test-dir build-metalfx-interop --output-on-failure -V
```
The GPU test returns 77 (CTest **Skipped**) when no Metal adapter, required
sharing features, or spatial scaler is available. A skip is not evidence of
interoperability. Sandboxed processes may need GPU access. CTest imposes a
60-second timeout. The separate stub test needs no GPU.
Tested on Apple M3, macOS 26.5.1, using Aurora's existing Dawn package
(`v20260603.191052`). Metal API and GPU validation were enabled:
| Formats | Input | Output | Frames per format |
| --- | --- | --- | --- |
| RGBA8Unorm, BGRA8Unorm | 64 × 48 | 128 × 96 | 24 |
| RGBA8Unorm, BGRA8Unorm | 320 × 180 | 480 × 270 | 24 |
| RGBA8Unorm, BGRA8Unorm | 960 × 540 | 1920 × 1080 | 24 |
All 144 frames and 2,304 interior pixel samples passed. Red changes per frame;
green and blue distinguish left/right and top/bottom. All channels are checked
within five 8-bit levels, catching stale images, orientation/channel mistakes,
and missing output. Tests cover 1.5× and 2× scaling, padded readback rows, slot
reuse, dropping wrappers before readback completion, invalid dimensions/sRGB
formats, the eight-set allocation bound, and the unavailable-backend stub.
Readback is only the test oracle and is absent from the game upscaling path.
These samples do not measure reconstruction quality at edges or race performance.
## Windowed presentation test
This optional target exercises Aurora's actual frame submission and presentation
with a synthetic source and an ImGui overlay. It requires no Wii game data and
creates an automatically closing test window. Add the option to an existing
from-source runtime build (the normal dependency/provider options still apply):
```sh
cmake -S runtime -B build-macos -DCMAKE_BUILD_TYPE=Release \
-DAURORA_BUILD_METALFX_PRESENTATION_TEST=ON
cmake --build build-macos --target metalfx_presentation_test
MTL_DEBUG_LAYER=1 MTL_SHADER_VALIDATION=1 \
./build-macos/aurora-build/metalfx_presentation_test
```
On the same M3, all 84 frames passed with Metal API/GPU validation: disabled,
enabled, window resize, 4:3/16:9 aspect changes, native-size bypass, disable, and
re-enable. Assertions check renderer status and errors; this is not a pixel-level
verification of the window image. The core build and macOS 12 deployment-target
availability compilation also passed. Full Mario Kart gameplay, race performance,
visual quality, Intel Macs, other Apple GPUs, older macOS runtime versions, and
non-macOS full builds remain untested.
References: [Apple MetalFX](https://developer.apple.com/documentation/metalfx),
[spatial scaler requirements](https://developer.apple.com/documentation/metalfx/mtlfxspatialscaler),
and the installed Dawn `MetalBackend.h` / `webgpu_cpp.h` APIs.
+269
View File
@@ -0,0 +1,269 @@
#import <Foundation/Foundation.h>
#import <IOSurface/IOSurface.h>
#import <Metal/Metal.h>
#include "webgpu/metalfx.hpp"
#include <dawn/native/MetalBackend.h>
#include <webgpu/webgpu_cpp.h>
#include <array>
#include <atomic>
#include <cmath>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string_view>
#include <vector>
namespace {
constexpr uint64_t kTimeoutNs = 10'000'000'000;
constexpr unsigned kFrames = 24;
std::atomic<unsigned> g_errors{0};
void require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
void wait(const wgpu::Instance& instance, wgpu::Future future) {
require(instance.WaitAny(future, kTimeoutNs) == wgpu::WaitStatus::Success,
"Dawn operation timed out or failed");
}
void runCase(const wgpu::Instance& instance, const wgpu::Device& device,
bool bgra, uint32_t width, uint32_t height,
uint32_t outWidth, uint32_t outHeight) {
const auto format = bgra ? wgpu::TextureFormat::BGRA8Unorm : wgpu::TextureFormat::RGBA8Unorm;
using namespace aurora::webgpu::metalfx;
std::array<std::unique_ptr<SpatialScaler>, 3> slots;
for (auto& slot : slots) {
std::string error;
slot = create(instance, device, {width, height, outWidth, outHeight, format}, error);
if (!slot) {
throw std::runtime_error(error.empty()
? "MetalFX resource pool was still busy retiring earlier slots"
: error);
}
}
// Asymmetric quadrants expose channel swaps, vertical flips, and stale frames.
wgpu::ShaderSourceWGSL source{};
source.code = R"(
@group(0) @binding(0) var<uniform> params: vec4f;
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
let p = array(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
return vec4f(p[i], 0, 1);
}
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
return vec4f(params.x, select(0.2, 0.8, p.x >= params.y / 2),
select(0.3, 0.7, p.y >= params.z / 2), 1);
}
)";
wgpu::ShaderModuleDescriptor shaderDescriptor{};
shaderDescriptor.nextInChain = &source;
auto shader = device.CreateShaderModule(&shaderDescriptor);
wgpu::ColorTargetState target{};
target.format = format;
wgpu::FragmentState fragment{};
fragment.module = shader;
fragment.entryPoint = "fs";
fragment.targetCount = 1;
fragment.targets = &target;
wgpu::RenderPipelineDescriptor pipelineDescriptor{};
pipelineDescriptor.vertex.module = shader;
pipelineDescriptor.vertex.entryPoint = "vs";
pipelineDescriptor.fragment = &fragment;
auto pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
auto dawnQueue = device.GetQueue();
const uint32_t bytesPerRow = (outWidth * 4 + 255) & ~255u;
const uint64_t readbackSize = uint64_t(bytesPerRow) * outHeight;
std::vector<wgpu::Buffer> readbacks;
for (unsigned frame = 0; frame < kFrames; ++frame) {
auto& slot = slots[frame % slots.size()];
require(slot->begin_input(), "Production MetalFX begin_input failed");
const std::array<float, 4> params{0.2f + float(frame % 5) * 0.1f,
float(width), float(height), 0};
wgpu::BufferDescriptor uniformDescriptor{};
uniformDescriptor.size = sizeof(params);
uniformDescriptor.usage = wgpu::BufferUsage::Uniform | wgpu::BufferUsage::CopyDst;
auto uniform = device.CreateBuffer(&uniformDescriptor);
dawnQueue.WriteBuffer(uniform, 0, params.data(), sizeof(params));
wgpu::BindGroupEntry entry{};
entry.binding = 0;
entry.buffer = uniform;
entry.size = sizeof(params);
wgpu::BindGroupDescriptor bindDescriptor{};
bindDescriptor.layout = pipeline.GetBindGroupLayout(0);
bindDescriptor.entryCount = 1;
bindDescriptor.entries = &entry;
auto bindGroup = device.CreateBindGroup(&bindDescriptor);
auto encoder = device.CreateCommandEncoder();
wgpu::RenderPassColorAttachment attachment{};
attachment.view = slot->input_view();
attachment.loadOp = wgpu::LoadOp::Clear;
attachment.storeOp = wgpu::StoreOp::Store;
wgpu::RenderPassDescriptor passDescriptor{};
passDescriptor.colorAttachmentCount = 1;
passDescriptor.colorAttachments = &attachment;
auto pass = encoder.BeginRenderPass(&passDescriptor);
pass.SetPipeline(pipeline);
pass.SetBindGroup(0, bindGroup);
pass.Draw(3);
pass.End();
auto render = encoder.Finish();
dawnQueue.Submit(1, &render);
if (!slot->upscale()) throw std::runtime_error(slot->error());
wgpu::BufferDescriptor readbackDescriptor{};
readbackDescriptor.size = readbackSize;
readbackDescriptor.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead;
auto readback = device.CreateBuffer(&readbackDescriptor);
encoder = device.CreateCommandEncoder();
wgpu::TexelCopyTextureInfo copySource{};
copySource.texture = slot->output_texture();
wgpu::TexelCopyBufferInfo destination{};
destination.buffer = readback;
destination.layout.bytesPerRow = bytesPerRow;
destination.layout.rowsPerImage = outHeight;
const wgpu::Extent3D extent{outWidth, outHeight, 1};
encoder.CopyTextureToBuffer(&copySource, &destination, &extent);
auto copy = encoder.Finish();
dawnQueue.Submit(1, &copy);
if (!slot->end_output()) throw std::runtime_error(slot->error());
readbacks.push_back(std::move(readback));
}
// Model toggle/resize immediately after submission, while either queue may
// still be consuming these textures. Production completion callbacks must
// keep the resources alive after the cache drops its wrappers.
slots = {};
// Readback is only the test oracle. No CPU image transfer or GPU completion
// wait occurs between Dawn rendering, MetalFX, and Dawn consumption above.
for (unsigned frame = 0; frame < kFrames; ++frame) {
bool mapped = false;
auto& readback = readbacks[frame];
wait(instance, readback.MapAsync(wgpu::MapMode::Read, 0, readbackSize,
wgpu::CallbackMode::WaitAnyOnly, [&mapped](wgpu::MapAsyncStatus status, wgpu::StringView) {
mapped = status == wgpu::MapAsyncStatus::Success;
}));
require(mapped, "Output readback mapping failed");
const auto* bytes = static_cast<const uint8_t*>(readback.GetConstMappedRange());
require(bytes != nullptr, "Output readback pointer is null");
for (unsigned y = 0; y < 4; ++y) {
for (unsigned x = 0; x < 4; ++x) {
const unsigned px = (2 * x + 1) * outWidth / 8;
const unsigned py = (2 * y + 1) * outHeight / 8;
const auto* pixel = bytes + py * bytesPerRow + px * 4;
const std::array<float, 4> expected{
0.2f + float(frame % 5) * 0.1f, x >= 2 ? 0.8f : 0.2f,
y >= 2 ? 0.7f : 0.3f, 1};
for (unsigned c = 0; c < 4; ++c) {
const unsigned channel = bgra && c != 1 && c != 3 ? 2 - c : c;
if (std::abs(int(pixel[channel]) - int(std::lround(expected[c] * 255))) > 5) {
std::cerr << "Pixel mismatch: frame=" << frame << " x=" << px << " y=" << py
<< " channel=" << c << " actual=" << int(pixel[channel])
<< " expected=" << std::lround(expected[c] * 255) << '\n';
throw std::runtime_error("MetalFX output failed image validation");
}
}
}
}
readback.Unmap();
}
require(g_errors.load() == 0, "Dawn reported validation errors or device loss");
std::cout << "PASS " << (bgra ? "BGRA8" : "RGBA8") << ' ' << width << 'x' << height
<< " -> " << outWidth << 'x' << outHeight << ": " << kFrames
<< " frames, 3 reused slots, 16 pixel samples/frame\n";
}
int run() {
const wgpu::InstanceFeatureName timedWait = wgpu::InstanceFeatureName::TimedWaitAny;
wgpu::InstanceDescriptor instanceDescriptor{};
instanceDescriptor.requiredFeatureCount = 1;
instanceDescriptor.requiredFeatures = &timedWait;
auto instance = wgpu::CreateInstance(&instanceDescriptor);
require(instance != nullptr, "Dawn instance creation failed");
wgpu::Adapter adapter;
wgpu::RequestAdapterOptions options{};
options.backendType = wgpu::BackendType::Metal;
wait(instance, instance.RequestAdapter(&options, wgpu::CallbackMode::WaitAnyOnly,
[&adapter](wgpu::RequestAdapterStatus status, wgpu::Adapter result, wgpu::StringView message) {
if (status == wgpu::RequestAdapterStatus::Success) adapter = std::move(result);
else std::cerr << "Adapter: " << std::string_view(message) << '\n';
}));
if (!adapter) { std::cout << "SKIP: no Dawn Metal adapter\n"; return 77; }
const std::array features{wgpu::FeatureName::SharedTextureMemoryIOSurface,
wgpu::FeatureName::SharedFenceMTLSharedEvent};
for (auto feature : features) {
if (!adapter.HasFeature(feature)) {
std::cout << "SKIP: Dawn adapter lacks IOSurface/shared-event interoperability\n";
return 77;
}
}
wgpu::DeviceDescriptor descriptor{};
descriptor.requiredFeatureCount = features.size();
descriptor.requiredFeatures = features.data();
descriptor.SetUncapturedErrorCallback(
[](const wgpu::Device&, wgpu::ErrorType, wgpu::StringView message) {
++g_errors;
std::cerr << "Dawn error: " << std::string_view(message) << '\n';
});
descriptor.SetDeviceLostCallback(wgpu::CallbackMode::AllowSpontaneous,
[](const wgpu::Device&, wgpu::DeviceLostReason reason, wgpu::StringView message) {
if (reason != wgpu::DeviceLostReason::Destroyed) {
++g_errors;
std::cerr << "Device lost: " << std::string_view(message) << '\n';
}
});
wgpu::Device device;
wait(instance, adapter.RequestDevice(&descriptor, wgpu::CallbackMode::WaitAnyOnly,
[&device](wgpu::RequestDeviceStatus status, wgpu::Device result, wgpu::StringView message) {
if (status == wgpu::RequestDeviceStatus::Success) device = std::move(result);
else std::cerr << "Device: " << std::string_view(message) << '\n';
}));
require(device != nullptr, "Dawn device creation failed");
id<MTLDevice> native = dawn::native::metal::GetMTLDevice(device.Get());
require(native != nil, "Dawn native Metal device is unavailable");
std::cout << "GPU: " << native.name.UTF8String << '\n';
if (!aurora::webgpu::metalfx::supported(device, wgpu::BackendType::Metal)) {
std::cout << "SKIP: GPU does not support MetalFX spatial scaling\n";
return 77;
}
require(!aurora::webgpu::metalfx::supported(device, wgpu::BackendType::Vulkan),
"MetalFX must reject non-Metal backends");
std::string error;
require(!aurora::webgpu::metalfx::create(instance, device,
{128, 96, 128, 96, wgpu::TextureFormat::RGBA8Unorm}, error) && !error.empty(),
"MetalFX must reject equal-size input/output");
require(!aurora::webgpu::metalfx::create(instance, device,
{128, 96, 256, 192, wgpu::TextureFormat::RGBA8UnormSrgb}, error),
"MetalFX must reject implicit sRGB conversion");
{
using namespace aurora::webgpu::metalfx;
std::array<std::unique_ptr<SpatialScaler>, 8> resources;
for (auto& scaler : resources) {
scaler = create(instance, device, {64, 48, 128, 96, wgpu::TextureFormat::RGBA8Unorm}, error);
require(scaler != nullptr, "Could not fill the MetalFX resource pool");
}
require(!create(instance, device, {64, 48, 128, 96, wgpu::TextureFormat::RGBA8Unorm}, error)
&& error.empty(), "A full retirement pool must defer allocation without a fatal error");
}
for (bool bgra : {false, true}) {
runCase(instance, device, bgra, 64, 48, 128, 96);
runCase(instance, device, bgra, 320, 180, 480, 270);
runCase(instance, device, bgra, 960, 540, 1920, 1080);
}
return 0;
}
} // namespace
int main() {
@autoreleasepool {
try { return run(); }
catch (const std::exception& error) {
std::cerr << "FAIL: " << error.what() << '\n';
return 1;
}
}
}
@@ -0,0 +1,124 @@
// Explicitly opted-in windowed test of Aurora's real frame/presentation path.
// No Wii game data is needed; the source override supplies a synthetic image.
#include <aurora/aurora.h>
#include <imgui.h>
#include <SDL3/SDL_timer.h>
#include "webgpu/gpu.hpp"
#include "window.hpp"
#include <atomic>
#include <chrono>
#include <cstdio>
#include <filesystem>
#include <stdexcept>
#include <string_view>
#include <system_error>
#include <vector>
namespace {
std::atomic<unsigned> g_errors{0};
void log_message(AuroraLogLevel level, const char* module, const char* message, unsigned len) {
if (level >= LOG_ERROR) ++g_errors;
if (level >= LOG_WARNING || std::string_view(message, len).find("MetalFX") != std::string_view::npos)
std::fprintf(stderr, "[%s] %.*s\n", module, static_cast<int>(len), message);
if (level == LOG_FATAL) std::abort();
}
void require(bool value, const char* message) {
if (!value) throw std::runtime_error(message);
}
void draw_frames(uint32_t width, uint32_t height, AuroraMetalFXStatus expected) {
using namespace aurora::webgpu;
auto source = create_render_texture(width, height, false);
auto bindGroup = create_copy_bind_group(source);
std::vector<uint32_t> pixels(size_t(width) * height);
for (uint32_t y = 0; y < height; ++y) {
for (uint32_t x = 0; x < width; ++x) {
pixels[size_t(y) * width + x] = 0xff000000u | ((x / 16 % 2) ? 0x00bb55u : 0xbb5500u);
}
}
wgpu::TexelCopyTextureInfo target{};
target.texture = source.texture;
wgpu::TexelCopyBufferLayout layout{};
layout.bytesPerRow = width * 4;
layout.rowsPerImage = height;
g_queue.WriteTexture(&target, pixels.data(), pixels.size() * sizeof(uint32_t), &layout, &source.size);
unsigned rendered = 0;
unsigned matchingStatus = 0;
for (unsigned attempt = 0; attempt < 300 && rendered < 12; ++attempt) {
aurora_update();
if (!aurora_begin_frame()) { SDL_Delay(5); continue; }
set_present_source_override(bindGroup, source.texture, source.size, source.format);
ImGui::SetNextWindowPos(ImVec2(12, 12), ImGuiCond_Always);
ImGui::Begin("MetalFX presentation test", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
ImGui::TextUnformatted("Output-resolution overlay after game upscaling");
ImGui::Text("Source: %u x %u", width, height);
ImGui::End();
aurora_end_frame();
aurora_wait_for_frame_worker();
const auto status = aurora_get_metalfx_status();
require(status != AURORA_METALFX_ERROR, "MetalFX reported a presentation error");
if (status == expected) ++matchingStatus;
++rendered;
}
require(rendered == 12 && matchingStatus >= 9, "Presentation did not reach the expected MetalFX state");
require(g_errors.load() == 0, "Aurora reported an error");
std::printf("PASS presentation source=%ux%u status=%d frames=%u\n", width, height, expected, rendered);
}
} // namespace
int main(int argc, char** argv) {
const auto cache = std::filesystem::temp_directory_path() /
("aurora-metalfx-presentation-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
std::filesystem::create_directories(cache);
const auto path = cache.string();
AuroraConfig config{};
config.appName = "MetalFX presentation test";
config.userPath = path.c_str();
config.cachePath = path.c_str();
config.resourcesPath = path.c_str();
config.desiredBackend = BACKEND_METAL;
config.windowWidth = 640;
config.windowHeight = 480;
config.msaa = 1;
config.maxTextureAnisotropy = 1;
config.logCallback = log_message;
config.logLevel = LOG_INFO;
aurora_initialize(argc, argv, &config);
int result = 0;
try {
if (!aurora_is_metalfx_spatial_supported()) {
std::puts("SKIP: MetalFX spatial scaling is unavailable");
result = 77;
} else {
aurora::window::lock_present_aspect_ratio(4, 3);
aurora_set_metalfx_spatial(false);
require(!aurora_get_metalfx_spatial(), "Disable request was not retained");
draw_frames(320, 240, AURORA_METALFX_DISABLED);
aurora_set_metalfx_spatial(true);
require(aurora_get_metalfx_spatial(), "Enable request was not retained");
draw_frames(320, 240, AURORA_METALFX_ACTIVE);
aurora::window::set_window_size(800, 500);
draw_frames(320, 240, AURORA_METALFX_ACTIVE);
aurora::window::lock_present_aspect_ratio(16, 9);
draw_frames(320, 240, AURORA_METALFX_ACTIVE);
const auto output = aurora::window::get_window_size();
draw_frames(output.native_fb_width, output.native_fb_height, AURORA_METALFX_NOT_UPSCALING);
aurora_set_metalfx_spatial(false);
draw_frames(320, 240, AURORA_METALFX_DISABLED);
aurora_set_metalfx_spatial(true);
draw_frames(320, 240, AURORA_METALFX_ACTIVE);
}
} catch (const std::exception& error) {
std::fprintf(stderr, "FAIL: %s\n", error.what());
result = 1;
}
aurora_shutdown();
std::error_code cleanupError;
std::filesystem::remove_all(cache, cleanupError);
return result;
}
@@ -0,0 +1,9 @@
#include "webgpu/metalfx.hpp"
int main() {
using namespace aurora::webgpu::metalfx;
if (supported({}, wgpu::BackendType::Vulkan) || supported({}, wgpu::BackendType::Metal)) return 1;
std::string error;
if (create({}, {}, {640, 480, 1280, 960, wgpu::TextureFormat::RGBA8Unorm}, error)) return 1;
return error.empty() ? 1 : 0;
}
@@ -88,6 +88,31 @@ TEST_F(GXFifoTest, SingleExpandedPrimitiveCannotMergeWithTriangles) {
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{0, 1, 2}));
}
TEST_F(GXFifoTest, DisplayListSubmitsStagingWhenADrawDoesNotFit) {
__GXSetDirtyState();
aurora::gx::fifo::clear_buffer();
g_gxState.lastVtxFmt = GX_VTXFMT0;
g_gxState.lastVtxSize = 1;
// admitted=1 refuses the middle draw; admitted=2 refuses the last, so only a replay of the refused draw pushes 0x33.
for (const u32 admitted : {1u, 2u}) for (const bool bigEndian : {true, false}) {
std::vector<u8> list;
for (const u8 fill : {0x11, 0x22, 0x33}) {
auto bytes = draw(GX_TRIANGLES, 3);
if (!bigEndian) std::swap(bytes[1], bytes[2]);
std::fill(bytes.begin() + 3, bytes.end(), fill);
list.insert(list.end(), bytes.begin(), bytes.end());
}
const auto splitsBefore = aurora::gfx::staging_split_count();
aurora::gfx::testing::refuse_staging_admission_after(admitted);
if (bigEndian) GXCallDisplayList(list.data(), static_cast<u32>(list.size()));
else GXCallDisplayListLE(list.data(), static_cast<u32>(list.size()));
EXPECT_EQ(aurora::gfx::staging_split_count() - splitsBefore, 1u)
<< "admitted=" << admitted << " bigEndian=" << bigEndian;
EXPECT_EQ(aurora::gfx::testing::last_pushed_vertices(), (std::vector<u8>{0x33, 0x33, 0x33}))
<< "admitted=" << admitted << " bigEndian=" << bigEndian;
}
}
TEST(StagingMapping, RetiredCallbacksCannotPublishAnotherBuffersReadiness) {
using namespace aurora::gfx;
StagingMapState state;
+79 -107
View File
@@ -1,27 +1,71 @@
cmake_minimum_required(VERSION 3.25)
cmake_minimum_required(VERSION 3.25)
# Dawn's pinned macOS artifacts target 12.0. Set the same floor before project()
# initializes the Apple toolchain so direct developer CMake invocations cannot
# accidentally inherit the running SDK's deployment version. This cache entry
# is harmless on non-Apple platforms and remains overridable by a caller.
if(NOT CMAKE_OSX_DEPLOYMENT_TARGET)
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING
"Minimum macOS version supported by WiiCompiled" FORCE)
endif()
project(mkw_recompiled)
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
message(FATAL_ERROR "WiiCompiled requires a 64-bit Clang toolchain")
endif()
if(APPLE AND CMAKE_OSX_ARCHITECTURES)
list(LENGTH CMAKE_OSX_ARCHITECTURES MKW_OSX_ARCHITECTURE_COUNT)
if(MKW_OSX_ARCHITECTURE_COUNT GREATER 1)
message(FATAL_ERROR
"WiiCompiled supports one macOS architecture per build directory; "
"configure separate arm64 and x86_64 build directories")
endif()
list(GET CMAKE_OSX_ARCHITECTURES 0 _mkw_osx_arch)
set(CMAKE_SYSTEM_PROCESSOR "${_mkw_osx_arch}" CACHE STRING "Target processor architecture" FORCE)
set(CMAKE_SYSTEM_PROCESSOR "${_mkw_osx_arch}")
endif()
if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_PLATFORM_WINDOWS TRUE)
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64)$")
# The first native macOS target is Apple Silicon. Intel and universal
# binaries remain future compatibility work; do not silently claim them.
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|arm64|ARM64)$")
set(MKW_PLATFORM_MACOS TRUE)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_PLATFORM_MACOS_X86_64 TRUE)
else()
set(MKW_PLATFORM_MACOS_ARM64 TRUE)
endif()
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$")
set(MKW_PLATFORM_LINUX TRUE)
else()
message(FATAL_ERROR
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS arm64")
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS x86_64/arm64")
endif()
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
message(FATAL_ERROR "WiiCompiled only supports Release builds")
endif()
option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON)
option(MKW_BUILD_PSQ_TESTS "Build focused PSQ ISA tests" OFF)
set(MKW_X86_CPU_PROFILE "v3" CACHE STRING
"Minimum x86-64 CPU profile for translated products (v2 or v3)")
set_property(CACHE MKW_X86_CPU_PROFILE PROPERTY STRINGS v2 v3)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$" AND
NOT MKW_X86_CPU_PROFILE MATCHES "^(v2|v3)$")
message(FATAL_ERROR "MKW_X86_CPU_PROFILE must be v2 or v3 (got '${MKW_X86_CPU_PROFILE}')")
endif()
option(MKW_PLATFORM_TESTS_ONLY "Build host-platform tests without graphics dependencies or game assets" OFF)
include("${CMAKE_CURRENT_LIST_DIR}/cmake/HostLibraries.cmake")
if(MKW_PLATFORM_TESTS_ONLY)
if(MKW_BUILD_PRODUCTS)
message(FATAL_ERROR "MKW_PLATFORM_TESTS_ONLY requires MKW_BUILD_PRODUCTS=OFF")
endif()
add_compile_definitions(NOMINMAX)
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PlatformTests.cmake")
return()
endif()
# Preprocessor definitions that belong to this project's own code (the runtime,
# the translated shards and the product glue) and to nothing else. They are
@@ -59,20 +103,6 @@ target_include_directories(mkw_pugixml PUBLIC third_party/pugixml)
target_compile_features(mkw_pugixml PUBLIC cxx_std_17)
set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF)
# Linux guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric
# stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
# asymmetric resume/yield coroutine libraries, which would need every call site restructured).
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers
# and macOS uses the project's x18-safe AArch64 assembly backend, so this target is Linux-only.
if(MKW_PLATFORM_LINUX)
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.
@@ -162,6 +192,18 @@ set(MKW_AURORA_DIR "${CMAKE_CURRENT_LIST_DIR}/../aurora-main")
# Fast-math may erase them and change guest-visible integer conversions.
set(MKW_TRANSLATED_PPC_FP_OPTIONS -fno-fast-math -ffp-contract=off)
set(MKW_PPC_SEMANTIC_RUNTIME_SOURCES
"${CMAKE_CURRENT_LIST_DIR}/src/ppc_helpers.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/fpu_helpers.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/ppc_quantized.cpp")
set_source_files_properties(${MKW_PPC_SEMANTIC_RUNTIME_SOURCES} PROPERTIES
SKIP_UNITY_BUILD_INCLUSION ON
SKIP_PRECOMPILE_HEADERS ON
COMPILE_OPTIONS "${MKW_TRANSLATED_PPC_FP_OPTIONS}")
# Match the optimization policy of the translated callers as well as their FP policy.
set_property(SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/ppc_quantized.cpp" APPEND PROPERTY
COMPILE_OPTIONS -O2 -fno-slp-vectorize)
# ----------------------------------------------------------------------
# Third-party: aurora-main (provides SDL3 + GPU backends)
# ----------------------------------------------------------------------
@@ -281,12 +323,14 @@ endif()
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
if(MKW_PLATFORM_MACOS)
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory.cpp")
# HostContext's Apple Silicon backend is implemented in a small assembly
# companion. It must be part of the product runtime as well as the
# standalone context test; otherwise the final executable is missing
# mkw_co_init/mkw_co_switch at link time.
enable_language(ASM)
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
if(MKW_PLATFORM_MACOS_ARM64)
# HostContext's Apple Silicon backend is implemented in a small assembly
# companion. It must be part of the product runtime as well as the
# standalone context test; otherwise the final executable is missing
# mkw_co_init/mkw_co_switch at link time.
enable_language(ASM)
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
endif()
else()
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
endif()
@@ -300,87 +344,7 @@ set(MKW_CPU_BASELINE_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/host_cpu_baseline.cpp
list(REMOVE_ITEM SOURCES ${MKW_BASE_PRODUCT_SOURCE} ${MKW_RETRO_REWIND_PRODUCT_SOURCE}
${MKW_CPU_BASELINE_SOURCE} ${MKW_PLATFORM_SOURCE})
# This deliberately small library contains host services that are safe to
# validate before guest memory and fiber work makes a full runtime build viable.
add_library(mkw_platform STATIC "${MKW_PLATFORM_SOURCE}")
target_include_directories(mkw_platform PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_platform PUBLIC cxx_std_17)
set_target_properties(mkw_platform PROPERTIES UNITY_BUILD OFF)
# Keep these independent from Aurora's BUILD_TESTING option: they validate the
# project's host-platform contracts, not Aurora's third-party test suite.
enable_testing()
add_executable(mkw_platform_paths_tests "${CMAKE_CURRENT_LIST_DIR}/tests/platform_paths_tests.cpp")
target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
add_executable(mkw_nand_save_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_save_tests.cpp")
target_include_directories(mkw_nand_save_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_save_tests COMMAND mkw_nand_save_tests)
add_executable(mkw_nand_settings_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_settings_tests.cpp")
find_package(Threads REQUIRED)
target_link_libraries(mkw_nand_settings_tests PRIVATE Threads::Threads)
target_include_directories(mkw_nand_settings_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
add_executable(mkw_sc_serial_tests "${CMAKE_CURRENT_LIST_DIR}/tests/sc_serial_tests.cpp")
target_include_directories(mkw_sc_serial_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_sc_serial_tests PRIVATE cxx_std_17)
add_test(NAME mkw_sc_serial_tests COMMAND mkw_sc_serial_tests)
# The input expression engine is self-contained, so it can be exercised without
# linking the runtime or SDL.
add_executable(mkw_input_expr_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/test_expr.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/input_expr.cpp")
target_include_directories(mkw_input_expr_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
# HostContext deliberately keeps the platform-specific context primitive out
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
# refactors cannot silently remove its headers, implementation, or link edge.
if(MKW_PLATFORM_LINUX)
add_executable(mkw_linux_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp")
target_include_directories(mkw_linux_host_context_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include"
"${CMAKE_CURRENT_LIST_DIR}/third_party/libco")
target_compile_features(mkw_linux_host_context_tests PRIVATE cxx_std_17)
target_link_libraries(mkw_linux_host_context_tests PRIVATE mkw::libco)
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
endif()
if(MKW_PLATFORM_MACOS)
# Exercise the Apple Silicon context ABI and the public host-memory
# contracts separately from translated products.
enable_language(ASM)
add_executable(mkw_macos_context_abi_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
add_executable(mkw_macos_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
add_executable(mkw_macos_guest_flat_memory_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_guest_flat_memory_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
target_include_directories(mkw_macos_guest_flat_memory_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_macos_guest_flat_memory_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests)
endif()
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PlatformTests.cmake")
# The translator emits the complete, content-addressed source graph. Consuming
# this one manifest keeps configure independent of the 28k generated function
@@ -428,7 +392,15 @@ else()
target_compile_definitions(mkw_macos_native_compile PRIVATE SDL_MAIN_HANDLED TARGET_PC)
target_link_libraries(mkw_macos_native_compile PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
mkw::pugixml mkw::toml11 mkw::cryptopp)
mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
if(MKW_PLATFORM_MACOS_X86_64)
target_link_libraries(mkw_macos_native_compile PRIVATE mkw::libco)
target_compile_options(mkw_macos_native_compile PRIVATE
-march=x86-64-${MKW_X86_CPU_PROFILE})
if(MKW_X86_CPU_PROFILE STREQUAL "v2")
target_compile_definitions(mkw_macos_native_compile PRIVATE MKW_X86_CPU_PROFILE_V2=1)
endif()
endif()
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
endif()
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
+29
View File
@@ -0,0 +1,29 @@
# Shared by the product build and the dependency-free platform test configuration.
get_filename_component(MKW_HOST_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
# POSIX guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric
# stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
# asymmetric resume/yield coroutine libraries, which would need every call site restructured).
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers;
# Apple Silicon uses the project's x18-safe AArch64 assembly backend, while Intel macOS uses
# libco's existing System V AMD64 backend.
if(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
add_library(mkw_libco STATIC "${MKW_HOST_RUNTIME_DIR}/third_party/libco/libco.c")
add_library(mkw::libco ALIAS mkw_libco)
target_include_directories(mkw_libco PUBLIC "${MKW_HOST_RUNTIME_DIR}/third_party/libco")
set_target_properties(mkw_libco PROPERTIES UNITY_BUILD OFF)
endif()
# This deliberately small library contains host services that are safe to
# validate before guest memory and fiber work makes a full runtime build viable.
add_library(mkw_platform STATIC "${MKW_HOST_RUNTIME_DIR}/src/platform/host_platform.cpp")
target_include_directories(mkw_platform PUBLIC "${MKW_HOST_RUNTIME_DIR}/include")
target_compile_features(mkw_platform PUBLIC cxx_std_17)
set_target_properties(mkw_platform PROPERTIES UNITY_BUILD OFF)
if(MKW_PLATFORM_WINDOWS)
target_compile_definitions(mkw_platform PRIVATE NOMINMAX)
target_link_libraries(mkw_platform PUBLIC shell32 ole32 uuid)
endif()
+188
View File
@@ -0,0 +1,188 @@
get_filename_component(MKW_TEST_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
# Keep these independent from Aurora's BUILD_TESTING option: they validate the
# project's host-platform contracts, not Aurora's third-party test suite.
enable_testing()
if(MKW_BUILD_PSQ_TESTS)
add_executable(mkw_psq_helpers_tests
"${MKW_TEST_RUNTIME_DIR}/tests/psq_helpers_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/ppc_quantized.cpp")
target_include_directories(mkw_psq_helpers_tests PRIVATE
"${MKW_TEST_RUNTIME_DIR}/tests/psq_memory"
"${MKW_TEST_RUNTIME_DIR}/include/isa"
"${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_psq_helpers_tests PRIVATE cxx_std_17)
target_compile_options(mkw_psq_helpers_tests PRIVATE
-O2 -fno-fast-math -ffp-contract=off -fno-slp-vectorize)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
target_compile_options(mkw_psq_helpers_tests PRIVATE
-march=x86-64-${MKW_X86_CPU_PROFILE})
endif()
set_target_properties(mkw_psq_helpers_tests PROPERTIES UNITY_BUILD OFF)
add_test(NAME mkw_psq_helpers_tests COMMAND mkw_psq_helpers_tests)
add_test(NAME mkw_psq_reserved_tests COMMAND "${CMAKE_COMMAND}"
"-DPSQ_TEST_EXECUTABLE=$<TARGET_FILE:mkw_psq_helpers_tests>"
-P "${MKW_TEST_RUNTIME_DIR}/tests/psq_reserved_tests.cmake")
endif()
add_executable(mkw_platform_paths_tests "${MKW_TEST_RUNTIME_DIR}/tests/platform_paths_tests.cpp")
target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
add_executable(mkw_runtime_config_tests "${MKW_TEST_RUNTIME_DIR}/tests/runtime_config_tests.cpp")
target_include_directories(mkw_runtime_config_tests PRIVATE
"${MKW_TEST_RUNTIME_DIR}/include"
"${MKW_TEST_RUNTIME_DIR}/third_party/toml11")
target_compile_features(mkw_runtime_config_tests PRIVATE cxx_std_20)
add_test(NAME mkw_runtime_config_tests COMMAND mkw_runtime_config_tests)
add_executable(mkw_nand_save_tests "${MKW_TEST_RUNTIME_DIR}/tests/nand_save_tests.cpp")
target_include_directories(mkw_nand_save_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_save_tests COMMAND mkw_nand_save_tests)
add_executable(mkw_nand_settings_tests "${MKW_TEST_RUNTIME_DIR}/tests/nand_settings_tests.cpp")
find_package(Threads REQUIRED)
target_link_libraries(mkw_nand_settings_tests PRIVATE Threads::Threads)
target_include_directories(mkw_nand_settings_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
add_executable(mkw_sc_serial_tests "${MKW_TEST_RUNTIME_DIR}/tests/sc_serial_tests.cpp")
target_include_directories(mkw_sc_serial_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_sc_serial_tests PRIVATE cxx_std_17)
add_test(NAME mkw_sc_serial_tests COMMAND mkw_sc_serial_tests)
# The input expression engine is self-contained, so it can be exercised without
# linking the runtime or SDL.
add_executable(mkw_input_expr_tests
"${MKW_TEST_RUNTIME_DIR}/tests/test_expr.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/input_expr.cpp")
target_include_directories(mkw_input_expr_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
# HostContext deliberately keeps the platform-specific context primitive out
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
# refactors cannot silently remove its headers, implementation, or link edge.
if(MKW_PLATFORM_LINUX)
add_executable(mkw_linux_host_context_tests
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp")
target_include_directories(mkw_linux_host_context_tests PRIVATE
"${MKW_TEST_RUNTIME_DIR}/include"
"${MKW_TEST_RUNTIME_DIR}/third_party/libco")
target_compile_features(mkw_linux_host_context_tests PRIVATE cxx_std_17)
target_link_libraries(mkw_linux_host_context_tests PRIVATE mkw::libco)
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
endif()
if(MKW_PLATFORM_WINDOWS)
add_executable(mkw_windows_host_context_tests
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp")
target_include_directories(mkw_windows_host_context_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_windows_host_context_tests PRIVATE cxx_std_17)
add_test(NAME mkw_windows_host_context_tests COMMAND mkw_windows_host_context_tests)
endif()
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
# Probe at the base ISA, including OS support for AVX register state. Cross
# builds execute target code only through a configured emulator.
if(NOT CMAKE_CROSSCOMPILING OR CMAKE_CROSSCOMPILING_EMULATOR)
include(CheckCXXSourceRuns)
include(CMakePushCheckState)
cmake_push_check_state(RESET)
set(CMAKE_REQUIRED_FLAGS "-march=x86-64")
check_cxx_source_runs([=[
#include <cpuid.h>
int main() {
unsigned a, b, c, d;
if (__get_cpuid_max(0, nullptr) < 7 ||
__get_cpuid_max(0x80000000u, nullptr) < 0x80000001u)
return 1;
__cpuid_count(1, 0, a, b, c, d);
// v2 plus FMA, MOVBE, XSAVE, OSXSAVE, AVX and F16C.
const unsigned leaf1 = (1u << 0) | (1u << 9) | (1u << 12) |
(1u << 13) | (1u << 19) | (1u << 20) | (1u << 22) |
(1u << 23) | (1u << 26) | (1u << 27) | (1u << 28) | (1u << 29);
if ((c & leaf1) != leaf1) return 1;
__cpuid_count(7, 0, a, b, c, d);
const unsigned leaf7 = (1u << 3) | (1u << 5) | (1u << 8);
if ((b & leaf7) != leaf7) return 1;
__cpuid_count(0x80000001u, 0, a, b, c, d);
if ((c & 0x21u) != 0x21u) return 1; // LAHF-SAHF and LZCNT.
__asm__ __volatile__("xgetbv" : "=a"(a), "=d"(d) : "c"(0));
return (a & 0x6u) == 0x6u ? 0 : 1;
}
]=] MKW_HOST_SUPPORTS_X86_V3)
cmake_pop_check_state()
endif()
# Compile the same oracle cases for both paths. The v2 binary proves the
# scalar fallback stays free of FMA; the v3 binary checks the existing
# vector intrinsic path against the same strict result.
foreach(profile IN ITEMS v2 v3)
add_executable(mkw_ppc_pair_fma_${profile}_tests
"${MKW_TEST_RUNTIME_DIR}/tests/ppc_pair_fma_tests.cpp")
target_include_directories(mkw_ppc_pair_fma_${profile}_tests PRIVATE
"${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_ppc_pair_fma_${profile}_tests PRIVATE cxx_std_17)
target_compile_options(mkw_ppc_pair_fma_${profile}_tests PRIVATE
-march=x86-64-${profile} -fno-fast-math -ffp-contract=off)
if((NOT CMAKE_CROSSCOMPILING OR CMAKE_CROSSCOMPILING_EMULATOR) AND
(profile STREQUAL "v2" OR MKW_HOST_SUPPORTS_X86_V3))
add_test(NAME mkw_ppc_pair_fma_${profile}_tests COMMAND mkw_ppc_pair_fma_${profile}_tests)
endif()
endforeach()
add_library(mkw_cpu_baseline_v2_compile OBJECT
"${MKW_TEST_RUNTIME_DIR}/src/host_cpu_baseline.cpp")
target_compile_features(mkw_cpu_baseline_v2_compile PRIVATE cxx_std_17)
target_compile_definitions(mkw_cpu_baseline_v2_compile PRIVATE MKW_X86_CPU_PROFILE_V2=1)
target_compile_options(mkw_cpu_baseline_v2_compile PRIVATE -march=x86-64-v2 -w)
endif()
if(MKW_PLATFORM_MACOS)
# Exercise the public host-memory contracts separately from translated products.
if(MKW_PLATFORM_MACOS_ARM64)
# Apple Silicon's context ABI is implemented by the local assembly backend.
enable_language(ASM)
add_executable(mkw_macos_context_abi_tests
"${MKW_TEST_RUNTIME_DIR}/tests/macos_context_abi_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/platform/macos/co_switch.S")
target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
add_executable(mkw_macos_host_context_tests
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/platform/macos/co_switch.S")
target_include_directories(mkw_macos_host_context_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
else()
# Intel macOS follows the same System V AMD64 libco path as Linux.
add_executable(mkw_macos_host_context_tests
"${MKW_TEST_RUNTIME_DIR}/tests/host_context_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/host_context.cpp")
target_include_directories(mkw_macos_host_context_tests PRIVATE
"${MKW_TEST_RUNTIME_DIR}/include"
"${MKW_TEST_RUNTIME_DIR}/third_party/libco")
target_link_libraries(mkw_macos_host_context_tests PRIVATE mkw::libco)
endif()
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
add_executable(mkw_macos_guest_flat_memory_tests
"${MKW_TEST_RUNTIME_DIR}/tests/macos_guest_flat_memory_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/guest_flat_memory_macos.cpp")
target_include_directories(mkw_macos_guest_flat_memory_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_macos_guest_flat_memory_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests)
add_executable(mkw_macos_external_audio_tests
"${MKW_TEST_RUNTIME_DIR}/tests/macos_external_audio_tests.cpp"
"${MKW_TEST_RUNTIME_DIR}/src/external_audio_macos.cpp")
target_include_directories(mkw_macos_external_audio_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
target_compile_features(mkw_macos_external_audio_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_external_audio_tests COMMAND mkw_macos_external_audio_tests)
endif()
+35 -24
View File
@@ -1,4 +1,4 @@
# Public WiiCompiled product graph.
# Public WiiCompiled product graph.
#
# The translator owns the translated build graph. Mario Kart's profile-neutral
# functions are compiled once into mkw_base_shared; only callers whose direct
@@ -28,6 +28,7 @@ list(REMOVE_DUPLICATES SOURCES)
if(MKW_PLATFORM_MACOS)
find_library(MKW_IOKIT_FRAMEWORK IOKit REQUIRED)
find_library(MKW_COREFOUNDATION_FRAMEWORK CoreFoundation REQUIRED)
find_library(MKW_COREAUDIO_FRAMEWORK CoreAudio REQUIRED)
endif()
function(mkw_apply_common_compile_options target)
@@ -84,11 +85,22 @@ target_link_libraries(mkw_runtime_common PRIVATE
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
if(MKW_PLATFORM_WINDOWS)
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
elseif(MKW_PLATFORM_LINUX)
elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
# media monitoring). Empty string on glibc >= 2.34 where dl* is in libc.
# media monitoring). Empty on platforms where dl* is already in libc/libSystem.
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS})
endif()
if(MKW_PLATFORM_MACOS)
# CoreAudio framework is required for automatic music muting on macOS.
target_link_libraries(mkw_runtime_common PRIVATE "${MKW_COREAUDIO_FRAMEWORK}")
if(MKW_PLATFORM_MACOS_X86_64)
# libco is used by Intel macOS. Apple Silicon uses the local
# x18-safe assembly backend and therefore does not define mkw::libco.
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco)
endif()
endif()
if(MKW_CPPWINRT_INCLUDE_DIR)
if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
message(FATAL_ERROR
@@ -118,29 +130,23 @@ foreach(source IN LISTS SOURCES)
endif()
set_source_files_properties("${source}" PROPERTIES UNITY_GROUP "${runtime_group}")
endforeach()
# These translation units implement guest-visible floating-point bit
# semantics. Keep them out of the fast-math runtime unity groups and apply
# the same contraction/rounding policy as translated PPC shards.
set(MKW_PPC_SEMANTIC_RUNTIME_SOURCES
"${MKW_RUNTIME_SOURCE_DIR}/src/ppc_helpers.cpp"
"${MKW_RUNTIME_SOURCE_DIR}/src/fpu_helpers.cpp")
set_source_files_properties(${MKW_PPC_SEMANTIC_RUNTIME_SOURCES} PROPERTIES
SKIP_UNITY_BUILD_INCLUSION ON
SKIP_PRECOMPILE_HEADERS ON
COMPILE_OPTIONS "${MKW_TRANSLATED_PPC_FP_OPTIONS}")
# PPC semantic sources are excluded from unity/PCH and configured in CMakeLists.txt.
set_target_properties(mkw_runtime_common PROPERTIES UNITY_BUILD ON UNITY_BUILD_MODE GROUP)
target_precompile_headers(mkw_runtime_common PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
mkw_apply_common_compile_options(mkw_runtime_common)
# Host ISA guard. Windows and Linux x86_64 product targets use x86-64-v3, so
# this object deliberately keeps the plain baseline ISA and checks the CPU
# before any AVX2/FMA code can execute. AArch64 has no equivalent optional ISA
# floor to probe: NEON/FMA are architectural requirements.
# Host ISA guard deliberately keeps the plain baseline ISA and checks the
# selected x86 profile before optional instructions can execute. AArch64 has
# no equivalent optional ISA floor to probe: NEON/FMA are architectural
# requirements.
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}")
target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17)
set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF)
target_compile_options(mkw_cpu_baseline PRIVATE -w)
if(MKW_X86_CPU_PROFILE STREQUAL "v2")
target_compile_definitions(mkw_cpu_baseline PRIVATE MKW_X86_CPU_PROFILE_V2=1)
endif()
endif()
if(NOT MKW_BASE_COMMON_SHARDS)
@@ -205,7 +211,9 @@ function(mkw_configure_product target)
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
if(MKW_PLATFORM_MACOS)
target_link_libraries(${target} PRIVATE
"${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}")
"${MKW_IOKIT_FRAMEWORK}" "${MKW_COREFOUNDATION_FRAMEWORK}" "${MKW_COREAUDIO_FRAMEWORK}")
target_link_options(${target} PRIVATE
"LINKER:-U,_OBJC_CLASS_$_MTLLogStateDescriptor")
endif()
if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
@@ -226,7 +234,7 @@ function(mkw_configure_product target)
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
elseif(MKW_PLATFORM_LINUX)
elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
# 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
@@ -328,12 +336,12 @@ else()
message(STATUS "RetroRewind target disabled (run translate-mod and emit-build-shards)")
endif()
# Windows and Linux x86_64 share the x86-64-v3 floor that the CPU baseline
# object above checks. AArch64 builds are compiled locally for the host that
# will run them, so both Linux and Apple Silicon use the compiler's native CPU
# tuning rather than leaving target-specific performance on the table.
# Windows, Linux and Intel macOS x86_64 use the selected profile. AArch64
# builds are compiled locally for the host that will run them, so both Linux
# and Apple Silicon use the compiler's native CPU tuning rather than leaving
# target-specific performance on the table.
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3)
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-${MKW_X86_CPU_PROFILE})
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
else()
@@ -346,5 +354,8 @@ set(MKW_ALL_BUILD_TARGETS
foreach(target IN LISTS MKW_ALL_BUILD_TARGETS)
if(TARGET ${target} AND MKW_BASELINE_ARCH_FLAG)
target_compile_options(${target} PRIVATE ${MKW_BASELINE_ARCH_FLAG})
if(MKW_X86_CPU_PROFILE STREQUAL "v2")
target_compile_definitions(${target} PRIVATE MKW_X86_CPU_PROFILE_V2=1)
endif()
endif()
endforeach()
+14
View File
@@ -0,0 +1,14 @@
#pragma once
namespace MusicAttenuation {
struct MacOSAudioStatus {
bool available = false;
bool playing = false;
};
// Queries output activity without capturing audio. Requires Core Audio process
// objects (macOS 14.2+); older systems return an unavailable sample.
MacOSAudioStatus QueryMacOSExternalAudio() noexcept;
} // namespace MusicAttenuation
+5 -5
View File
@@ -3,11 +3,11 @@
#include <cstddef>
// HostContext is the deliberately small boundary between the guest scheduler
// and the host's cooperative-context facility. Windows uses native Fibers and
// Linux uses libco; macOS AArch64 uses the local assembly backend because it
// must preserve Darwin's platform-reserved x18 register, which libco's AArch64
// backend does not save. Its handles are only valid on the thread that
// initialized the scheduler.
// and the host's cooperative-context facility. Windows uses native Fibers;
// Linux uses libco's host backend; Intel macOS uses its System V x86-64 backend. macOS AArch64 uses
// the local assembly backend because it must preserve Darwin's platform-reserved
// x18 register, which libco's AArch64 backend does not save. Its handles are
// only valid on the thread that initialized the scheduler.
namespace HostContext {
using Handle = void*;
+20
View File
@@ -607,7 +607,18 @@ inline double PPC_PsMulNoNiInline(double lhs, double rhs)
inline PpcPairVec PpcFmaddPairInline(PpcPairVec multiplicand, PpcPairVec multiplier, PpcPairVec addend)
{
#if defined(__x86_64__)
#if defined(__FMA__)
return _mm_fmadd_ps(multiplicand, multiplier, addend);
#else
// x86-64-v2 has SSE4.2 but no FMA. Preserve the one rounding point per
// lane through the scalar C++ FMA rather than decomposing into mul+add.
const double a = PpcM128ToPsInline(multiplicand);
const double c = PpcM128ToPsInline(multiplier);
const double b = PpcM128ToPsInline(addend);
return PpcPsToM128Inline(PpcPackPairedInline(
PpcAccuratePsMaddLaneNoNiInline<false>(PpcGetPs0Inline(a), PpcGetPs0Inline(c), PpcGetPs0Inline(b)),
PpcAccuratePsMaddLaneNoNiInline<false>(PpcGetPs1Inline(a), PpcGetPs1Inline(c), PpcGetPs1Inline(b))));
#endif
#elif defined(__aarch64__)
const PpcPairVec result = vfma_f32(addend, multiplicand, multiplier);
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
@@ -619,7 +630,16 @@ inline PpcPairVec PpcFmaddPairInline(PpcPairVec multiplicand, PpcPairVec multipl
inline PpcPairVec PpcFmsubPairInline(PpcPairVec multiplicand, PpcPairVec multiplier, PpcPairVec subtractor)
{
#if defined(__x86_64__)
#if defined(__FMA__)
return _mm_fmsub_ps(multiplicand, multiplier, subtractor);
#else
const double a = PpcM128ToPsInline(multiplicand);
const double c = PpcM128ToPsInline(multiplier);
const double b = PpcM128ToPsInline(subtractor);
return PpcPsToM128Inline(PpcPackPairedInline(
PpcAccuratePsMaddLaneNoNiInline<true>(PpcGetPs0Inline(a), PpcGetPs0Inline(c), PpcGetPs0Inline(b)),
PpcAccuratePsMaddLaneNoNiInline<true>(PpcGetPs1Inline(a), PpcGetPs1Inline(c), PpcGetPs1Inline(b))));
#endif
#elif defined(__aarch64__)
const PpcPairVec result = vfma_f32(vneg_f32(subtractor), multiplicand, multiplier);
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
+64 -94
View File
@@ -1385,37 +1385,13 @@ MKW_PPC_FORCE_INLINE void PPC_PsqStStackInline(uint32_t addr, double value)
// Context-free PSQ entries for translated regions which own GQR state as an
// ordinary native value. All architecturally valid quantization encodings are
// handled directly; reserved encodings retain the generic helper's abort.
template <uint32_t W, uint32_t I, bool Stack>
MKW_PPC_NO_INLINE MKW_PPC_COLD inline double PPC_PsqLStateFallback(uint32_t gqr, uint32_t addr)
{
static_assert(W <= 1u && I < 8u);
const uint32_t type = (gqr >> 16) & 0x7u;
const uint32_t scale = (gqr >> 24) & 0x3Fu;
if constexpr (W == 0u)
{
switch (type)
{
case 0u: return Stack ? PpcLoadPairPsqFloatStackInline(addr) : PpcLoadPairPsqFloatFastInline(addr);
case 4u: return Stack ? PpcLoadPairPsqIntegerStackInline<uint8_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<uint8_t>(addr, scale);
case 5u: return Stack ? PpcLoadPairPsqIntegerStackInline<uint16_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<uint16_t>(addr, scale);
case 6u: return Stack ? PpcLoadPairPsqIntegerStackInline<int8_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<int8_t>(addr, scale);
case 7u: return Stack ? PpcLoadPairPsqIntegerStackInline<int16_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<int16_t>(addr, scale);
default: std::abort();
}
}
else
{
switch (type)
{
case 0u: return Stack ? PpcLoadSinglePsqFloatStackInline(addr) : PpcLoadSinglePsqFloatFastInline(addr);
case 4u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<uint8_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<uint8_t>(addr, scale);
case 5u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<uint16_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<uint16_t>(addr, scale);
case 6u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<int8_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<int8_t>(addr, scale);
case 7u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<int16_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<int16_t>(addr, scale);
default: std::abort();
}
}
}
template <uint32_t W, bool Stack>
MKW_PPC_NO_INLINE MKW_PPC_COLD double PPC_PsqLStateFallback(uint32_t gqr, uint32_t addr);
extern template double PPC_PsqLStateFallback<0u, false>(uint32_t, uint32_t);
extern template double PPC_PsqLStateFallback<0u, true>(uint32_t, uint32_t);
extern template double PPC_PsqLStateFallback<1u, false>(uint32_t, uint32_t);
extern template double PPC_PsqLStateFallback<1u, true>(uint32_t, uint32_t);
// Keep the normal explicit-state path small and directly optimizable. Exact
// unscaled encodings cover the SDK's common GQR setup; scaled and reserved
@@ -1442,41 +1418,17 @@ MKW_PPC_FORCE_INLINE double PPC_PsqLStateInline(uint32_t gqr, uint32_t addr)
if constexpr (W == 0u) return Stack ? PpcLoadPairPsqIntegerStackInline<int16_t>(addr, 0u) : PpcLoadPairPsqIntegerFastInline<int16_t>(addr, 0u);
else return Stack ? PpcLoadSinglePsqQuantizedStackInline<int16_t>(addr, 0u) : PpcLoadSinglePsqQuantizedFastInline<int16_t>(addr, 0u);
default:
return PPC_PsqLStateFallback<W, I, Stack>(gqr, addr);
return PPC_PsqLStateFallback<W, Stack>(gqr, addr);
}
}
template <uint32_t W, uint32_t I, bool Stack>
MKW_PPC_NO_INLINE MKW_PPC_COLD inline void PPC_PsqStStateFallback(uint32_t gqr, uint32_t addr, double value)
{
static_assert(W <= 1u && I < 8u);
const uint32_t type = gqr & 0x7u;
const uint32_t scale = (gqr >> 8) & 0x3Fu;
if constexpr (W == 0u)
{
switch (type)
{
case 0u: Stack ? PpcStorePairPsqFloatStackInline(addr, value) : PpcStorePairPsqFloatFastInline(addr, value); return;
case 4u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<uint8_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<uint8_t>(addr, value, scale); return;
case 5u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<uint16_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<uint16_t>(addr, value, scale); return;
case 6u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<int8_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<int8_t>(addr, value, scale); return;
case 7u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<int16_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<int16_t>(addr, value, scale); return;
default: std::abort();
}
}
else
{
switch (type)
{
case 0u: if constexpr (Stack) PpcStoreSinglePsqFloatStackInline(addr, value); else PpcStoreSinglePsqFloatFastInline(addr, value); return;
case 4u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<uint8_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<uint8_t>(addr, value, scale); return;
case 5u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<uint16_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<uint16_t>(addr, value, scale); return;
case 6u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<int8_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<int8_t>(addr, value, scale); return;
case 7u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<int16_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<int16_t>(addr, value, scale); return;
default: std::abort();
}
}
}
template <uint32_t W, bool Stack>
MKW_PPC_NO_INLINE MKW_PPC_COLD void PPC_PsqStStateFallback(uint32_t gqr, uint32_t addr, double value);
extern template void PPC_PsqStStateFallback<0u, false>(uint32_t, uint32_t, double);
extern template void PPC_PsqStStateFallback<0u, true>(uint32_t, uint32_t, double);
extern template void PPC_PsqStStateFallback<1u, false>(uint32_t, uint32_t, double);
extern template void PPC_PsqStStateFallback<1u, true>(uint32_t, uint32_t, double);
template <uint32_t W, uint32_t I, bool Stack>
MKW_PPC_FORCE_INLINE void PPC_PsqStStateInline(uint32_t gqr, uint32_t addr, double value)
@@ -1505,11 +1457,18 @@ MKW_PPC_FORCE_INLINE void PPC_PsqStStateInline(uint32_t gqr, uint32_t addr, doub
else { if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<int16_t>(addr, value, 0u); else PpcStoreSinglePsqQuantizedFastInline<int16_t>(addr, value, 0u); }
return;
default:
PPC_PsqStStateFallback<W, I, Stack>(gqr, addr, value);
PPC_PsqStStateFallback<W, Stack>(gqr, addr, value);
return;
}
}
template <uint32_t W>
MKW_PPC_NO_INLINE MKW_PPC_COLD double PPC_PsqLResolvedStateFallback(
uint32_t gqr, uint8_t* resolvedHost, uint32_t offset, uint32_t addr);
extern template double PPC_PsqLResolvedStateFallback<0u>(uint32_t, uint8_t*, uint32_t, uint32_t);
extern template double PPC_PsqLResolvedStateFallback<1u>(uint32_t, uint8_t*, uint32_t, uint32_t);
template <uint32_t W, uint32_t I>
MKW_PPC_FORCE_INLINE double PPC_PsqLResolvedStateInline(
uint32_t gqr, uint8_t* resolvedHost, uint32_t offset, uint32_t addr)
@@ -1517,33 +1476,41 @@ MKW_PPC_FORCE_INLINE double PPC_PsqLResolvedStateInline(
static_assert(W <= 1u && I < 8u);
if (!resolvedHost) [[unlikely]] return PPC_PsqLStateInline<W, I, false>(gqr, addr);
const uint32_t type = (gqr >> 16) & 0x7u;
const uint32_t scale = (gqr >> 24) & 0x3Fu;
if (type == 0u) {
if constexpr (W == 0u) return PpcLoadPairPsqFloatResolvedInline(resolvedHost, offset, addr);
else return PpcLoadSinglePsqFloatResolvedInline(resolvedHost, offset, addr);
}
if constexpr (W == 0u)
{
switch (type)
switch (gqr & 0x3F070000u)
{
case 0u: return PpcLoadPairPsqFloatResolvedInline(resolvedHost, offset, addr);
case 4u: return PpcLoadPairPsqIntegerResolvedInline<uint8_t>(resolvedHost, offset, addr, scale);
case 5u: return PpcLoadPairPsqIntegerResolvedInline<uint16_t>(resolvedHost, offset, addr, scale);
case 6u: return PpcLoadPairPsqIntegerResolvedInline<int8_t>(resolvedHost, offset, addr, scale);
case 7u: return PpcLoadPairPsqIntegerResolvedInline<int16_t>(resolvedHost, offset, addr, scale);
default: std::abort();
case 0x00040000u: return PpcLoadPairPsqIntegerResolvedInline<uint8_t>(resolvedHost, offset, addr, 0u);
case 0x00050000u: return PpcLoadPairPsqIntegerResolvedInline<uint16_t>(resolvedHost, offset, addr, 0u);
case 0x00060000u: return PpcLoadPairPsqIntegerResolvedInline<int8_t>(resolvedHost, offset, addr, 0u);
case 0x00070000u: return PpcLoadPairPsqIntegerResolvedInline<int16_t>(resolvedHost, offset, addr, 0u);
default: return PPC_PsqLResolvedStateFallback<W>(gqr, resolvedHost, offset, addr);
}
}
else
{
switch (type)
switch (gqr & 0x3F070000u)
{
case 0u: return PpcLoadSinglePsqFloatResolvedInline(resolvedHost, offset, addr);
case 4u: return PpcLoadSinglePsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, scale);
case 5u: return PpcLoadSinglePsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, scale);
case 6u: return PpcLoadSinglePsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, scale);
case 7u: return PpcLoadSinglePsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, scale);
default: std::abort();
case 0x00040000u: return PpcLoadSinglePsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, 0u);
case 0x00050000u: return PpcLoadSinglePsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, 0u);
case 0x00060000u: return PpcLoadSinglePsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, 0u);
case 0x00070000u: return PpcLoadSinglePsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, 0u);
default: return PPC_PsqLResolvedStateFallback<W>(gqr, resolvedHost, offset, addr);
}
}
}
template <uint32_t W>
MKW_PPC_NO_INLINE MKW_PPC_COLD void PPC_PsqStResolvedStateFallback(
uint32_t gqr, uint8_t* resolvedHost, uint32_t offset, uint32_t addr, double value);
extern template void PPC_PsqStResolvedStateFallback<0u>(uint32_t, uint8_t*, uint32_t, uint32_t, double);
extern template void PPC_PsqStResolvedStateFallback<1u>(uint32_t, uint8_t*, uint32_t, uint32_t, double);
template <uint32_t W, uint32_t I>
MKW_PPC_FORCE_INLINE void PPC_PsqStResolvedStateInline(
uint32_t gqr, uint8_t* resolvedHost, uint32_t offset, uint32_t addr, double value)
@@ -1555,29 +1522,32 @@ MKW_PPC_FORCE_INLINE void PPC_PsqStResolvedStateInline(
return;
}
const uint32_t type = gqr & 0x7u;
const uint32_t scale = (gqr >> 8) & 0x3Fu;
if (type == 0u) {
if constexpr (W == 0u) PpcStorePairPsqFloatResolvedInline(resolvedHost, offset, addr, value);
else PpcStoreSinglePsqFloatResolvedInline(resolvedHost, offset, addr, value);
return;
}
if constexpr (W == 0u)
{
switch (type)
switch (gqr & 0x3F07u)
{
case 0u: PpcStorePairPsqFloatResolvedInline(resolvedHost, offset, addr, value); return;
case 4u: PpcStorePairPsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, value, scale); return;
case 5u: PpcStorePairPsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, value, scale); return;
case 6u: PpcStorePairPsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, value, scale); return;
case 7u: PpcStorePairPsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, value, scale); return;
default: std::abort();
case 0x3D04u: PpcStorePairPsqU8Scale61ResolvedInline(resolvedHost, offset, addr, value); return;
case 4u: PpcStorePairPsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, value, 0u); return;
case 5u: PpcStorePairPsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, value, 0u); return;
case 6u: PpcStorePairPsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, value, 0u); return;
case 7u: PpcStorePairPsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, value, 0u); return;
default: PPC_PsqStResolvedStateFallback<W>(gqr, resolvedHost, offset, addr, value); return;
}
}
else
{
switch (type)
switch (gqr & 0x3F07u)
{
case 0u: PpcStoreSinglePsqFloatResolvedInline(resolvedHost, offset, addr, value); return;
case 4u: PpcStoreSinglePsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, value, scale); return;
case 5u: PpcStoreSinglePsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, value, scale); return;
case 6u: PpcStoreSinglePsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, value, scale); return;
case 7u: PpcStoreSinglePsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, value, scale); return;
default: std::abort();
case 4u: PpcStoreSinglePsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, value, 0u); return;
case 5u: PpcStoreSinglePsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, value, 0u); return;
case 6u: PpcStoreSinglePsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, value, 0u); return;
case 7u: PpcStoreSinglePsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, value, 0u); return;
default: PPC_PsqStResolvedStateFallback<W>(gqr, resolvedHost, offset, addr, value); return;
}
}
}
+2 -1
View File
@@ -5,7 +5,8 @@
namespace MusicAttenuation {
// Enables the optional external-media integration (Windows media sessions,
// or MPRIS over D-Bus on Linux). The monitor is started lazily the first time
// Core Audio output activity on macOS, or MPRIS over D-Bus on Linux).
// The monitor is started lazily the first time
// this is enabled.
void SetEnabled(bool enabled) noexcept;
void SetMusicVolume(float volume) noexcept;
+12
View File
@@ -42,6 +42,7 @@ struct RuntimeUserConfig {
std::optional<std::string> graphicsApi;
std::optional<std::string> displayMode;
std::optional<uint32_t> frameInterpolationFps;
std::optional<bool> metalFxSpatialUpscaling;
std::optional<bool> skipUnreadyPipelines;
std::optional<bool> disableCopyFilter;
std::optional<bool> textureReplacements;
@@ -459,6 +460,8 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.frameInterpolationFps = migrated;
}
}
config.metalFxSpatialUpscaling =
FindConfigValue<bool>(document, "video", "metalfx_spatial_upscaling");
config.skipUnreadyPipelines = FindConfigValue<bool>(document, "video", "skip_unready_pipelines");
config.disableCopyFilter = FindConfigValue<bool>(document, "video", "disable_copy_filter");
config.showFps = FindConfigValue<bool>(document, "video", "show_fps");
@@ -660,6 +663,11 @@ inline bool SetFrameInterpolationFps(uint32_t value) {
return WriteSetting("video", "frame_interpolation_fps", std::to_string(value));
}
inline bool SetMetalFxSpatialUpscaling(bool value) {
Mutable().metalFxSpatialUpscaling = value;
return WriteSetting("video", "metalfx_spatial_upscaling", value ? "true" : "false");
}
inline bool SetDisplayMode(std::string value) {
if (!IsSupportedDisplayMode(value)) {
return false;
@@ -815,6 +823,10 @@ inline float ResolutionMultiplier(float fallback = 1.0f) {
return std::max(0.0f, Get().resolutionMultiplier.value_or(fallback));
}
inline bool MetalFxSpatialUpscaling(bool fallback = false) {
return Get().metalFxSpatialUpscaling.value_or(fallback);
}
inline float AudioVolume(float fallback = 1.0f) {
return std::clamp(Get().audioVolume.value_or(fallback), 0.0f, 1.0f);
}
+87
View File
@@ -0,0 +1,87 @@
#if defined(__APPLE__)
#include "external_audio_macos.h"
#include <CoreAudio/CoreAudio.h>
#include <unistd.h>
#include <vector>
namespace MusicAttenuation {
namespace {
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 140200
constexpr auto kProcessObjectList = kAudioHardwarePropertyProcessObjectList;
constexpr auto kProcessPid = kAudioProcessPropertyPID;
constexpr auto kProcessRunningOutput = kAudioProcessPropertyIsRunningOutput;
#else
// Public Core Audio selector ABI values, for SDKs predating process objects.
// The runtime HasProperty check still determines whether the OS supports them.
constexpr AudioObjectPropertySelector kProcessObjectList = 'prs#';
constexpr AudioObjectPropertySelector kProcessPid = 'ppid';
constexpr AudioObjectPropertySelector kProcessRunningOutput = 'piro';
#endif
template <typename T>
bool ReadAudioProcessProperty(AudioObjectID object, AudioObjectPropertySelector selector,
T& value) noexcept {
const AudioObjectPropertyAddress address{
selector, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain};
UInt32 size = sizeof(value);
return AudioObjectGetPropertyData(object, &address, 0, nullptr, &size, &value) == noErr &&
size == sizeof(value);
}
} // namespace
MacOSAudioStatus QueryMacOSExternalAudio() noexcept {
const AudioObjectPropertyAddress address{
kProcessObjectList,
kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain};
// Probe the property instead of raising the game's minimum macOS version.
if (!AudioObjectHasProperty(kAudioObjectSystemObject, &address)) {
return {};
}
// Processes may start between the size and data queries. Retry a changed
// list a bounded number of times; subsequent monitor polls also retry.
for (int attempt = 0; attempt < 3; ++attempt) {
UInt32 size = 0;
if (AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &address,
0, nullptr, &size) != noErr ||
size % sizeof(AudioObjectID) != 0) {
return {};
}
if (size == 0) {
return {true, false};
}
std::vector<AudioObjectID> processes(size / sizeof(AudioObjectID));
const auto result = AudioObjectGetPropertyData(kAudioObjectSystemObject, &address,
0, nullptr, &size, processes.data());
if (result == kAudioHardwareBadPropertySizeError) {
continue;
}
if (result != noErr || size % sizeof(AudioObjectID) != 0 ||
size / sizeof(AudioObjectID) > processes.size()) {
return {};
}
const pid_t ownPid = getpid();
for (size_t index = 0; index < size / sizeof(AudioObjectID); ++index) {
pid_t pid = 0;
UInt32 runningOutput = 0;
// An exiting process may disappear mid-query. Never count an
// unknown PID or our own SDL output as external playback.
if (ReadAudioProcessProperty(processes[index], kProcessPid, pid) &&
pid > 0 && pid != ownPid &&
ReadAudioProcessProperty(processes[index], kProcessRunningOutput,
runningOutput) && runningOutput != 0) {
return {true, true};
}
}
return {true, false};
}
return {};
}
} // namespace MusicAttenuation
#endif
+129 -32
View File
@@ -2,79 +2,176 @@
#include <mach/mach.h>
#include <mach/mach_vm.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <algorithm>
#include <cstdio>
#include <mutex>
#include <stdexcept>
#include <string>
#include <vector>
namespace GuestFlat {
bool g_requiresCheckedAccess = false;
namespace {
struct Mapping { uint32_t base; uint64_t size; uint8_t* host; };
struct Store {
Backing kind = Backing::Owned;
uint32_t owned = 0;
uint64_t size = 0;
uint8_t* host = nullptr;
};
struct Mapping {
uint32_t base = 0;
uint64_t size = 0;
uint8_t* host = nullptr;
};
std::mutex g_mutex;
std::vector<Store> g_stores;
std::vector<Mapping> g_mappings;
std::vector<RegionRequest> g_layout;
uint8_t* g_base = nullptr;
bool g_active = false;
inline uint64_t RoundUp(uint64_t value, uint64_t align) {
return (value + align - 1) & ~(align - 1);
}
uint64_t Offset(const RegionRequest& r) {
if (r.backing == Backing::Mem1) return r.base & 0x1fffffffu;
if (r.backing == Backing::Mem2) return (r.base & 0x1fffffffu) - 0x10000000u;
return 0;
}
bool Same(const std::vector<RegionRequest>& a, const std::vector<RegionRequest>& b) {
return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(),
[](const auto& x, const auto& y) { return x.base == y.base && x.size == y.size && x.backing == y.backing; });
}
int BackingFile(size_t size) {
char name[] = "/tmp/wiicompiled-guest-XXXXXX";
const int fd = mkstemp(name);
if (fd >= 0) { unlink(name); if (ftruncate(fd, static_cast<off_t>(size)) != 0) { close(fd); return -1; } }
return fd;
}
} // namespace
bool IsActive() { return g_active; }
void Initialize(const std::vector<RegionRequest>& regions) {
std::lock_guard lock(g_mutex);
g_requiresCheckedAccess = static_cast<size_t>(getpagesize()) > kGuestPageSize;
if (g_active) { if (!Same(g_layout, regions)) throw std::runtime_error("flat guest layout cannot be remapped"); return; }
const uint64_t pageSize = static_cast<uint64_t>(getpagesize());
g_requiresCheckedAccess = pageSize > kGuestPageSize;
if (g_active) {
if (!Same(g_layout, regions))
throw std::runtime_error("flat guest layout cannot be remapped");
return;
}
for (size_t i = 0; i < regions.size(); ++i) {
const auto& r = regions[i];
if (r.size == 0) continue;
if (r.size > kGuestSpaceSize - r.base ||
r.base % pageSize != 0 || Offset(r) % pageSize != 0) {
throw std::runtime_error("invalid flat guest memory region");
}
const uint64_t end = static_cast<uint64_t>(r.base) + RoundUp(r.size, pageSize);
for (size_t j = 0; j < i; ++j) {
const auto& prior = regions[j];
if (prior.size == 0) continue;
const uint64_t priorEnd = static_cast<uint64_t>(prior.base) + RoundUp(prior.size, pageSize);
if (r.base < priorEnd && prior.base < end) {
throw std::runtime_error("overlapping flat guest memory regions");
}
}
}
mach_vm_address_t address = kFixedFlatGuestBase;
if (mach_vm_allocate(mach_task_self(), &address, kGuestSpaceSize, VM_FLAGS_FIXED) != KERN_SUCCESS || address != kFixedFlatGuestBase)
if (mach_vm_allocate(mach_task_self(), &address, kGuestSpaceSize, VM_FLAGS_FIXED) != KERN_SUCCESS || address != kFixedFlatGuestBase) {
throw std::runtime_error("unable to reserve fixed 4 GiB macOS guest address space");
}
g_base = reinterpret_cast<uint8_t*>(address);
struct Store { Backing kind; uint32_t owned; uint64_t size; int fd; };
std::vector<Store> stores;
for (const auto& r : regions) {
if (!r.size) continue;
const uint32_t owned = r.backing == Backing::Owned ? r.base : 0;
auto it = std::find_if(stores.begin(), stores.end(), [&](const Store& s) { return s.kind == r.backing && s.owned == owned; });
const uint64_t need = Offset(r) + r.size;
if (it == stores.end()) stores.push_back({r.backing, owned, need, -1}); else it->size = std::max(it->size, need);
try {
for (const auto& r : regions) {
if (!r.size) continue;
const uint32_t owned = r.backing == Backing::Owned ? r.base : 0;
auto it = std::find_if(stores.begin(), stores.end(), [&](const Store& s) {
return s.kind == r.backing && s.owned == owned;
});
const uint64_t need = RoundUp(Offset(r) + r.size, pageSize);
if (it == stores.end()) {
stores.push_back({r.backing, owned, need, nullptr});
} else {
it->size = std::max(it->size, need);
}
}
for (auto& s : stores) {
void* host = mmap(nullptr, static_cast<size_t>(s.size), PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANON, -1, 0);
if (host == MAP_FAILED) {
throw std::runtime_error("unable to allocate anonymous macOS guest backing store");
}
s.host = static_cast<uint8_t*>(host);
}
for (const auto& r : regions) {
if (!r.size) continue;
const uint32_t owned = r.backing == Backing::Owned ? r.base : 0;
const auto& s = *std::find_if(stores.begin(), stores.end(), [&](const Store& x) {
return x.kind == r.backing && x.owned == owned;
});
mach_vm_address_t target = reinterpret_cast<mach_vm_address_t>(g_base + r.base);
vm_prot_t cur = VM_PROT_NONE, max = VM_PROT_NONE;
const kern_return_t kr = mach_vm_remap(
mach_task_self(),
&target,
static_cast<mach_vm_size_t>(RoundUp(r.size, pageSize)),
0,
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,
mach_task_self(),
reinterpret_cast<mach_vm_address_t>(s.host + Offset(r)),
FALSE,
&cur,
&max,
VM_INHERIT_NONE
);
if (kr != KERN_SUCCESS) {
throw std::runtime_error(std::string("mach_vm_remap guest alias failed: ") + mach_error_string(kr));
}
g_mappings.push_back({r.base, r.size, s.host + Offset(r)});
}
g_layout = regions;
g_stores = std::move(stores);
g_active = true;
} catch (...) {
for (auto& s : stores) {
if (s.host != nullptr) {
munmap(s.host, static_cast<size_t>(s.size));
}
}
mach_vm_deallocate(mach_task_self(), address, kGuestSpaceSize);
g_mappings.clear();
g_stores.clear();
g_layout.clear();
g_base = nullptr;
throw;
}
for (auto& s : stores) { s.fd = BackingFile(s.size); if (s.fd < 0) throw std::runtime_error("unable to create macOS guest backing store"); }
for (const auto& r : regions) {
if (!r.size) continue;
const uint32_t owned = r.backing == Backing::Owned ? r.base : 0;
const auto& s = *std::find_if(stores.begin(), stores.end(), [&](const Store& x) { return x.kind == r.backing && x.owned == owned; });
auto* host = static_cast<uint8_t*>(mmap(nullptr, r.size, PROT_READ | PROT_WRITE, MAP_SHARED, s.fd, Offset(r)));
auto* guest = mmap(g_base + r.base, r.size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, s.fd, Offset(r));
if (host == MAP_FAILED || guest != g_base + r.base) throw std::runtime_error("unable to map macOS guest alias");
g_mappings.push_back({r.base, r.size, host});
}
for (auto& s : stores) close(s.fd);
g_layout = regions; g_active = true;
}
uint8_t* HostPointer(uint32_t a) { for (const auto& m : g_mappings) if (a >= m.base && uint64_t(a - m.base) < m.size) return m.host + (a - m.base); return nullptr; }
uint8_t* HostPointer(uint32_t a) {
for (const auto& m : g_mappings) {
if (a >= m.base && uint64_t(a - m.base) < m.size) {
return m.host + (a - m.base);
}
}
return nullptr;
}
void ProtectDeferredRange(uint32_t, size_t) {}
void UnprotectDeferredRange(uint32_t, size_t) {}
void RegisterExecutableRange(uint32_t, uint32_t) {}
FaultCounters Counters() { return {}; }
void LogFaultSummary() noexcept {}
bool HandleAccessViolation(void*, bool) noexcept { return false; }
} // namespace GuestFlat
-1
View File
@@ -242,7 +242,6 @@ void WritePollResults(uint32_t outAddress,
const std::vector<NetworkPollContract::CopiedDescriptor>& descriptors);
// network_socket.cpp
int32_t DeleteWiiSocket(uint32_t fd);
void CleanupAllWiiSockets();
sockaddr_in ReadWiiSockAddr(uint32_t addr);
int32_t HandleIpTopIoctl(uint32_t cmd, uint32_t inBuf, uint32_t inLen, uint32_t outBuf,
+21 -3
View File
@@ -54,6 +54,9 @@ constexpr int kMaxSslSessions = 4;
struct SslSession {
bool active = false;
bool handshaked = false;
// A failed POSIX TLS write cannot be resumed with a new guest buffer.
// Keep the slot and socket ownership intact until explicit teardown.
bool failed = false;
bool plaintextWfc = false;
uint32_t socketFd = UINT32_MAX;
NativeSocket native = kInvalidSocket;
@@ -761,6 +764,14 @@ static int32_t SslHandshakeImpl(SslSession& ssl) {
return SSL_OK;
}
static int32_t FailSslWrite(SslSession& ssl) {
ssl.failed = true;
// Stop transport I/O without deleting the guest descriptor or freeing a
// session still referenced by the IOCTLV_NET_SSL_WRITE caller.
::shutdown(ssl.native, SHUT_RDWR);
return SSL_ERR_FAILED;
}
static int32_t SslWrite(SslSession& ssl, const uint8_t* data, uint32_t size) {
if (!data || size == 0) {
return SSL_ERR_ZERO;
@@ -795,12 +806,11 @@ static int32_t SslWrite(SslSession& ssl, const uint8_t* data, uint32_t size) {
}
if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
if (std::chrono::steady_clock::now() >= writeDeadline) {
DeleteWiiSocket(ssl.socketFd);
return SSL_ERR_FAILED;
return FailSslWrite(ssl);
}
continue;
}
return SSL_ERR_FAILED;
return FailSslWrite(ssl);
}
return static_cast<int32_t>(totalWritten);
}
@@ -842,6 +852,9 @@ static int32_t SslRead(SslSession& ssl, uint8_t* out, uint32_t size) {
// The handshake runs on every SSL read/write, so a failure repeats for as long
// as the session lives; report only the first one.
static int32_t SslHandshake(SslSession& ssl) {
if (ssl.failed) {
return SSL_ERR_FAILED;
}
const int32_t result = SslHandshakeImpl(ssl);
if (result != SSL_OK && !ssl.loggedHandshakeFail) {
ssl.loggedHandshakeFail = true;
@@ -947,6 +960,11 @@ int32_t HandleSslIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const std
WriteSslReturn(in, SSL_ERR_ID);
return 0;
}
// Reject before NAS buffering can acknowledge data on a failed session.
if (g_sslSessions[sslId].failed) {
WriteSslReturn(in, SSL_ERR_FAILED);
return 0;
}
if (out.size() < 2 || !out[1].address) {
WriteSslReturn(in, SSL_ERR_FAILED);
return 0;
+2 -2
View File
@@ -14,7 +14,7 @@
extern "C" void mkw_co_switch(void** targetSp, void** sourceSp);
extern "C" void* mkw_co_init(void* stackTop, void (*entry)(void*), void* argument);
#elif defined(__linux__)
#elif defined(__linux__) || (defined(__APPLE__) && defined(__x86_64__))
#include <libco.h>
#include <cstdlib>
@@ -158,7 +158,7 @@ void Switch(Handle target)
g_current = source;
}
#elif defined(__linux__)
#elif defined(__linux__) || (defined(__APPLE__) && defined(__x86_64__))
namespace {
struct Context {
+46 -23
View File
@@ -1,14 +1,12 @@
// Host ISA guard. Every other product target builds with -march=x86-64-v3, so a pre-Haswell
// Intel or pre-Excavator AMD machine would otherwise die on an illegal-instruction fault with no
// Host ISA guard. Product targets build with an explicit x86-64-v2 or x86-64-v3 profile, so an
// unsupported machine would otherwise die on an illegal-instruction fault with no
// explanation. This TU alone skips that flag (own CMake object library, excluded from unity
// build/PCH) and runs from a priority-101 C initializer, ahead of every C++ dynamic initializer
// and thus the first AVX2 code that could execute. Keep it free of anything that could pull in
// vectorized code: no iostreams, no std::string, no runtime-wide headers.
//
// x86-64-v3 is an x86-specific optional-feature baseline (AVX2/BMI2/FMA and friends are not
// guaranteed present on every x86_64 chip); nothing here applies on AArch64, where ASIMD/NEON is
// mandatory in the base architecture and PublicProducts.cmake never applies an -march=x86-64-v3
// equivalent flag to begin with. That branch below is a no-op stub, not a port of this check.
// x86-64-v2/v3 are x86-specific optional-feature baselines; nothing here applies on AArch64,
// where ASIMD/NEON is mandatory in the base architecture.
#if defined(__x86_64__)
@@ -58,27 +56,27 @@ struct CpuFeature {
bool isOsXsave;
};
// Everything x86-64-v3 implies, which includes all of x86-64-v2. Spelled out so
// the error message can name the exact instruction sets the machine lacks
// rather than only "AVX2", which is merely the best known member of the set.
// x86-64-v2 requirements. The v3-only extension below retains a useful
// feature-by-feature diagnostic rather than reducing a failed v3 check to AVX2.
constexpr CpuFeature kRequiredFeatures[] = {
{"SSE3", 1, 0, 2, 0, false},
{"SSSE3", 1, 0, 2, 9, false},
{"FMA", 1, 0, 2, 12, false},
{"CMPXCHG16B", 1, 0, 2, 13, false},
{"SSE4.1", 1, 0, 2, 19, false},
{"SSE4.2", 1, 0, 2, 20, false},
{"MOVBE", 1, 0, 2, 22, false},
{"POPCNT", 1, 0, 2, 23, false},
{"OSXSAVE", 1, 0, 2, 27, true},
{"AVX", 1, 0, 2, 28, false},
{"F16C", 1, 0, 2, 29, false},
{"BMI1", 7, 0, 1, 3, false},
{"AVX2", 7, 0, 1, 5, false},
{"BMI2", 7, 0, 1, 8, false},
{"LAHF-SAHF", 0x80000001u, 0, 2, 0, false},
};
#if !defined(MKW_X86_CPU_PROFILE_V2)
constexpr CpuFeature kV3RequiredFeatures[] = {
{"FMA", 1, 0, 2, 12, false}, {"MOVBE", 1, 0, 2, 22, false},
{"OSXSAVE", 1, 0, 2, 27, true}, {"AVX", 1, 0, 2, 28, false},
{"F16C", 1, 0, 2, 29, false}, {"BMI1", 7, 0, 1, 3, false},
{"AVX2", 7, 0, 1, 5, false}, {"BMI2", 7, 0, 1, 8, false},
{"LZCNT", 0x80000001u, 0, 2, 5, false},
};
#endif
// Fixed-capacity text accumulation: no allocation, no exceptions, nothing that
// could route through code this file is trying to stay ahead of.
@@ -130,6 +128,27 @@ bool CollectMissingBaselineFeatures(TextBuffer& missing) {
ok = false;
}
#if !defined(MKW_X86_CPU_PROFILE_V2)
for (const CpuFeature& feature : kV3RequiredFeatures) {
const bool leafAvailable = (feature.leaf & 0x80000000u) != 0
? feature.leaf <= maxExtended
: feature.leaf <= maxBasic;
bool present = false;
if (leafAvailable) {
unsigned regs[4] = {0, 0, 0, 0};
HostCpuId(feature.leaf, feature.subleaf, regs);
present = (regs[feature.reg] & (1u << feature.bit)) != 0;
}
if (present) {
haveOsXsave = haveOsXsave || feature.isOsXsave;
continue;
}
if (!ok) missing.Append(", ");
missing.Append(feature.name);
ok = false;
}
#endif
// CPUID reporting AVX is not sufficient: the OS also has to have enabled
// XMM and YMM state saving or every VEX-encoded instruction faults. This is
// the same guard a compiler's own runtime feature dispatch applies.
@@ -175,13 +194,17 @@ void WriteStdErrEarly(const char* text) {
[[noreturn]] void ReportUnsupportedCpu(const char* missing) {
TextBuffer message;
message.Append(
"This build needs a processor that supports AVX2 and the rest of the "
"x86-64-v3 instruction set.\n\nMissing on this machine: ");
#if defined(MKW_X86_CPU_PROFILE_V2)
message.Append("This build needs a processor that supports the x86-64-v2 instruction set.\n\nMissing on this machine: ");
#else
message.Append("This build needs a processor that supports the x86-64-v3 instruction set.\n\nMissing on this machine: ");
#endif
message.Append(missing);
message.Append(
"\n\nx86-64-v3 covers Intel Core processors from Haswell (4th "
"generation, 2013) onward and AMD processors from Excavator (2015) onward.");
#if defined(MKW_X86_CPU_PROFILE_V2)
message.Append("\n\nx86-64-v2 covers Intel Core processors from Nehalem (2008) onward and AMD processors from Jaguar (2013) onward.");
#else
message.Append("\n\nx86-64-v3 covers Intel Core processors from Haswell (4th generation, 2013) onward and AMD processors from Excavator (2015) onward.");
#endif
// The tag matches RT_TAG_RUNTIME in runtime_log.h. It is spelled out here
// because this translation unit must not include runtime-wide headers (see
+11 -2
View File
@@ -44,10 +44,15 @@
#include <signal.h>
#if defined(__x86_64__)
// Only the x86 POSIX fault path inspects ucontext_t to recover the page-fault
// write bit. macOS deprecates ucontext and requires _XOPEN_SOURCE just to
// include the header, while the arm64 handler does not use it at all.
// write bit. macOS exposes the signal-handler context through sys/ucontext.h;
// avoid ucontext.h itself because its deprecated user-context APIs require
// _XOPEN_SOURCE. The arm64 handler does not inspect a host context at all.
#if defined(__APPLE__)
#include <sys/ucontext.h>
#else
#include <ucontext.h>
#endif
#endif
#include <unistd.h>
#endif
@@ -1128,7 +1133,11 @@ void PosixMemoryFaultHandler(int sig, siginfo_t* info, void* ucontextVoid) {
// error code x86 pushes on a page fault records whether it was a write.
if (ucontextVoid != nullptr) {
auto* uc = static_cast<ucontext_t*>(ucontextVoid);
#if defined(__APPLE__)
isWrite = uc->uc_mcontext != nullptr && (uc->uc_mcontext->__es.__err & 0x2) != 0;
#else
isWrite = (uc->uc_mcontext.gregs[REG_ERR] & 0x2) != 0;
#endif
}
#endif
+35 -4
View File
@@ -4,9 +4,9 @@
#include <array>
#include <atomic>
#include <bit>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <mutex>
#include <thread>
@@ -16,6 +16,8 @@
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Media.Control.h>
#elif defined(__APPLE__)
#include "external_audio_macos.h"
#elif defined(__linux__)
#include <dlfcn.h>
@@ -52,6 +54,20 @@ std::array<float, kSoundPlayerCount> g_requestedSoundPlayerVolumes{};
std::array<float, kSoundPlayerCount> g_lastAppliedSoundPlayerVolumes{};
std::array<bool, kSoundPlayerCount> g_haveSoundPlayerVolumes{};
uint32_t FloatBits(float value) noexcept {
static_assert(sizeof(float) == sizeof(uint32_t));
uint32_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
return bits;
}
float BitsFloat(uint32_t bits) noexcept {
static_assert(sizeof(float) == sizeof(uint32_t));
float value = 0.0f;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
float ClampSoundPlayerVolume(float volume) noexcept {
// Match nw4r::snd::SoundPlayer::SetVolume at 0x800A35E0 exactly,
// including its NaN behavior (unordered compares select the upper bound).
@@ -63,7 +79,7 @@ float ClampSoundPlayerVolume(float volume) noexcept {
bool WriteGuestFloat(uint32_t address, float value) noexcept {
try {
Memory::Write32(address, std::bit_cast<uint32_t>(value));
Memory::Write32(address, FloatBits(value));
return true;
} catch (const Memory::AccessViolation&) {
return false;
@@ -75,7 +91,7 @@ bool ReadGuestFloat(uint32_t address, float& value) noexcept {
if (!Memory::TryRead32(address, bits)) {
return false;
}
value = std::bit_cast<float>(bits);
value = BitsFloat(bits);
return true;
}
@@ -448,6 +464,19 @@ void MonitorLinuxMprisSessions() noexcept {
}
#endif
#if defined(__APPLE__)
void MonitorMacOSAudio() noexcept {
using namespace std::chrono_literals;
for (;;) {
const auto status = QueryMacOSExternalAudio();
g_externalMediaPlaying.store(status.playing, std::memory_order_release);
g_mediaControlAvailable.store(status.available, std::memory_order_release);
g_mediaControlInitializationComplete.store(true, std::memory_order_release);
std::this_thread::sleep_for(250ms);
}
}
#endif
void StartMonitor() noexcept {
#if defined(_WIN32)
// The process owns this monitor for its remaining lifetime. Keeping it
@@ -456,6 +485,8 @@ void StartMonitor() noexcept {
#elif defined(__linux__)
// Detached so there is no shutdown ordering to manage against static audio state.
std::thread(MonitorLinuxMprisSessions).detach();
#elif defined(__APPLE__)
std::thread(MonitorMacOSAudio).detach();
#else
g_mediaControlAvailable.store(false, std::memory_order_release);
g_mediaControlInitializationComplete.store(true, std::memory_order_release);
@@ -558,7 +589,7 @@ void SetSoundPlayerVolume(uint32_t soundPlayer, float requestedVolume) {
}
// Preserve the original function's access semantics. An invalid player is
// a guest bug and must not be converted into a silent successful call.
Memory::Write32(soundPlayer + kSoundPlayerVolumeOffset, std::bit_cast<uint32_t>(applied));
Memory::Write32(soundPlayer + kSoundPlayerVolumeOffset, FloatBits(applied));
}
} // namespace MusicAttenuation
+13
View File
@@ -1,6 +1,7 @@
#include "platform/host_platform.h"
#include <cstdlib>
#include <vector>
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
@@ -46,6 +47,18 @@ std::optional<std::filesystem::path> ExecutableDirectory() noexcept {
std::error_code ec;
const auto resolved = std::filesystem::weakly_canonical(path, ec);
return (ec ? std::filesystem::path(path) : resolved).parent_path();
#elif defined(__linux__)
std::vector<char> buffer(256);
for (;;) {
const auto length = ::readlink("/proc/self/exe", buffer.data(), buffer.size());
if (length < 0) {
return std::nullopt;
}
if (static_cast<std::size_t>(length) < buffer.size()) {
return std::filesystem::path(std::string(buffer.data(), length)).parent_path();
}
buffer.resize(buffer.size() * 2);
}
#else
return std::nullopt;
#endif
+4 -4
View File
@@ -833,8 +833,8 @@ extern "C" double PPC_PsqL(uint32_t addr, uint32_t w, uint32_t i)
}
const uint32_t gqr = cpu->gqr[i & 7];
return w == 0 ? PPC_PsqLStateFallback<0u, 0u, false>(gqr, addr)
: PPC_PsqLStateFallback<1u, 0u, false>(gqr, addr);
return w == 0 ? PPC_PsqLStateFallback<0u, false>(gqr, addr)
: PPC_PsqLStateFallback<1u, false>(gqr, addr);
}
extern "C" void PPC_PsqSt(uint32_t addr, double value, uint32_t w, uint32_t i)
@@ -848,11 +848,11 @@ extern "C" void PPC_PsqSt(uint32_t addr, double value, uint32_t w, uint32_t i)
const uint32_t gqr = cpu->gqr[i & 7];
if (w == 0)
{
PPC_PsqStStateFallback<0u, 0u, false>(gqr, addr, value);
PPC_PsqStStateFallback<0u, false>(gqr, addr, value);
}
else
{
PPC_PsqStStateFallback<1u, 0u, false>(gqr, addr, value);
PPC_PsqStStateFallback<1u, false>(gqr, addr, value);
}
}
+154
View File
@@ -0,0 +1,154 @@
#include "isa/ppc_isa_quantized.h"
#if defined(__FAST_MATH__) || __FINITE_MATH_ONLY__
#error "PSQ helpers require strict PPC floating-point options"
#endif
template <uint32_t W, bool Stack>
MKW_PPC_NO_INLINE MKW_PPC_COLD double PPC_PsqLStateFallback(uint32_t gqr, uint32_t addr)
{
static_assert(W <= 1u);
const uint32_t type = (gqr >> 16) & 0x7u;
const uint32_t scale = (gqr >> 24) & 0x3Fu;
if constexpr (W == 0u)
{
switch (type)
{
case 0u: return Stack ? PpcLoadPairPsqFloatStackInline(addr) : PpcLoadPairPsqFloatFastInline(addr);
case 4u: return Stack ? PpcLoadPairPsqIntegerStackInline<uint8_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<uint8_t>(addr, scale);
case 5u: return Stack ? PpcLoadPairPsqIntegerStackInline<uint16_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<uint16_t>(addr, scale);
case 6u: return Stack ? PpcLoadPairPsqIntegerStackInline<int8_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<int8_t>(addr, scale);
case 7u: return Stack ? PpcLoadPairPsqIntegerStackInline<int16_t>(addr, scale) : PpcLoadPairPsqIntegerFastInline<int16_t>(addr, scale);
default: std::abort();
}
}
else
{
switch (type)
{
case 0u: return Stack ? PpcLoadSinglePsqFloatStackInline(addr) : PpcLoadSinglePsqFloatFastInline(addr);
case 4u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<uint8_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<uint8_t>(addr, scale);
case 5u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<uint16_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<uint16_t>(addr, scale);
case 6u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<int8_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<int8_t>(addr, scale);
case 7u: return Stack ? PpcLoadSinglePsqQuantizedStackInline<int16_t>(addr, scale) : PpcLoadSinglePsqQuantizedFastInline<int16_t>(addr, scale);
default: std::abort();
}
}
}
template <uint32_t W, bool Stack>
MKW_PPC_NO_INLINE MKW_PPC_COLD void PPC_PsqStStateFallback(uint32_t gqr, uint32_t addr, double value)
{
static_assert(W <= 1u);
const uint32_t type = gqr & 0x7u;
const uint32_t scale = (gqr >> 8) & 0x3Fu;
if constexpr (W == 0u)
{
switch (type)
{
case 0u: Stack ? PpcStorePairPsqFloatStackInline(addr, value) : PpcStorePairPsqFloatFastInline(addr, value); return;
case 4u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<uint8_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<uint8_t>(addr, value, scale); return;
case 5u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<uint16_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<uint16_t>(addr, value, scale); return;
case 6u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<int8_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<int8_t>(addr, value, scale); return;
case 7u: if constexpr (Stack) PpcStorePairPsqQuantizedStackInline<int16_t>(addr, value, scale); else PpcStorePairPsqQuantizedFastInline<int16_t>(addr, value, scale); return;
default: std::abort();
}
}
else
{
switch (type)
{
case 0u: if constexpr (Stack) PpcStoreSinglePsqFloatStackInline(addr, value); else PpcStoreSinglePsqFloatFastInline(addr, value); return;
case 4u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<uint8_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<uint8_t>(addr, value, scale); return;
case 5u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<uint16_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<uint16_t>(addr, value, scale); return;
case 6u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<int8_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<int8_t>(addr, value, scale); return;
case 7u: if constexpr (Stack) PpcStoreSinglePsqQuantizedStackInline<int16_t>(addr, value, scale); else PpcStoreSinglePsqQuantizedFastInline<int16_t>(addr, value, scale); return;
default: std::abort();
}
}
}
template <uint32_t W>
MKW_PPC_NO_INLINE MKW_PPC_COLD double PPC_PsqLResolvedStateFallback(
uint32_t gqr, uint8_t* resolvedHost, uint32_t offset, uint32_t addr)
{
static_assert(W <= 1u);
if (!resolvedHost) [[unlikely]] return PPC_PsqLStateInline<W, 0u, false>(gqr, addr);
const uint32_t type = (gqr >> 16) & 0x7u;
const uint32_t scale = (gqr >> 24) & 0x3Fu;
if constexpr (W == 0u)
{
switch (type)
{
case 0u: return PpcLoadPairPsqFloatResolvedInline(resolvedHost, offset, addr);
case 4u: return PpcLoadPairPsqIntegerResolvedInline<uint8_t>(resolvedHost, offset, addr, scale);
case 5u: return PpcLoadPairPsqIntegerResolvedInline<uint16_t>(resolvedHost, offset, addr, scale);
case 6u: return PpcLoadPairPsqIntegerResolvedInline<int8_t>(resolvedHost, offset, addr, scale);
case 7u: return PpcLoadPairPsqIntegerResolvedInline<int16_t>(resolvedHost, offset, addr, scale);
default: std::abort();
}
}
else
{
switch (type)
{
case 0u: return PpcLoadSinglePsqFloatResolvedInline(resolvedHost, offset, addr);
case 4u: return PpcLoadSinglePsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, scale);
case 5u: return PpcLoadSinglePsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, scale);
case 6u: return PpcLoadSinglePsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, scale);
case 7u: return PpcLoadSinglePsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, scale);
default: std::abort();
}
}
}
template <uint32_t W>
MKW_PPC_NO_INLINE MKW_PPC_COLD void PPC_PsqStResolvedStateFallback(
uint32_t gqr, uint8_t* resolvedHost, uint32_t offset, uint32_t addr, double value)
{
static_assert(W <= 1u);
if (!resolvedHost) [[unlikely]]
{
PPC_PsqStStateInline<W, 0u, false>(gqr, addr, value);
return;
}
const uint32_t type = gqr & 0x7u;
const uint32_t scale = (gqr >> 8) & 0x3Fu;
if constexpr (W == 0u)
{
switch (type)
{
case 0u: PpcStorePairPsqFloatResolvedInline(resolvedHost, offset, addr, value); return;
case 4u: PpcStorePairPsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, value, scale); return;
case 5u: PpcStorePairPsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, value, scale); return;
case 6u: PpcStorePairPsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, value, scale); return;
case 7u: PpcStorePairPsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, value, scale); return;
default: std::abort();
}
}
else
{
switch (type)
{
case 0u: PpcStoreSinglePsqFloatResolvedInline(resolvedHost, offset, addr, value); return;
case 4u: PpcStoreSinglePsqQuantizedResolvedInline<uint8_t>(resolvedHost, offset, addr, value, scale); return;
case 5u: PpcStoreSinglePsqQuantizedResolvedInline<uint16_t>(resolvedHost, offset, addr, value, scale); return;
case 6u: PpcStoreSinglePsqQuantizedResolvedInline<int8_t>(resolvedHost, offset, addr, value, scale); return;
case 7u: PpcStoreSinglePsqQuantizedResolvedInline<int16_t>(resolvedHost, offset, addr, value, scale); return;
default: std::abort();
}
}
}
template double PPC_PsqLStateFallback<0u, false>(uint32_t, uint32_t);
template void PPC_PsqStStateFallback<0u, false>(uint32_t, uint32_t, double);
template double PPC_PsqLStateFallback<0u, true>(uint32_t, uint32_t);
template void PPC_PsqStStateFallback<0u, true>(uint32_t, uint32_t, double);
template double PPC_PsqLResolvedStateFallback<0u>(uint32_t, uint8_t*, uint32_t, uint32_t);
template void PPC_PsqStResolvedStateFallback<0u>(uint32_t, uint8_t*, uint32_t, uint32_t, double);
template double PPC_PsqLStateFallback<1u, false>(uint32_t, uint32_t);
template void PPC_PsqStStateFallback<1u, false>(uint32_t, uint32_t, double);
template double PPC_PsqLStateFallback<1u, true>(uint32_t, uint32_t);
template void PPC_PsqStStateFallback<1u, true>(uint32_t, uint32_t, double);
template double PPC_PsqLResolvedStateFallback<1u>(uint32_t, uint8_t*, uint32_t, uint32_t);
template void PPC_PsqStResolvedStateFallback<1u>(uint32_t, uint8_t*, uint32_t, uint32_t, double);
+48 -1
View File
@@ -111,6 +111,9 @@ bool g_skipUnreadyPipelines = RuntimeConfigFile::SkipUnreadyPipelines(true);
bool g_disableCopyFilter = RuntimeConfigFile::DisableCopyFilter(true);
bool g_showFps = RuntimeConfigFile::ShowFps(true);
bool g_forceAspect169 = RuntimeConfigFile::ForceAspect169Enabled();
#if defined(__APPLE__)
bool g_metalFxSpatialUpscaling = RuntimeConfigFile::MetalFxSpatialUpscaling(false);
#endif
uint32_t g_disabledPostProcessingPaths = RuntimeConfigFile::DisabledPostProcessingPaths(0);
std::array<int32_t, PAD_MAX_CONTROLLERS> g_configuredControllerIndices = [] {
std::array<int32_t, PAD_MAX_CONTROLLERS> indices{};
@@ -998,13 +1001,24 @@ void DrawAudioSettings() {
MusicAttenuation::SetEnabled(g_attenuateMusicWhenMediaPlays);
RuntimeConfigFile::SetAttenuateMusicWhenMediaPlays(g_attenuateMusicWhenMediaPlays);
}
#if defined(__APPLE__)
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Detects other apps with active audio output on macOS 14.2 or later. "
"Apps that keep an output stream running silently may keep game music muted.");
}
#endif
if (g_attenuateMusicWhenMediaPlays) {
if (MusicAttenuation::IsExternalMediaPlaying()) {
ImGui::TextDisabled("External media is playing; game music is muted.");
} else if (!MusicAttenuation::IsMediaControlInitializationComplete()) {
ImGui::TextDisabled("Waiting for media controls...");
ImGui::TextDisabled("Checking external audio...");
} else if (!MusicAttenuation::IsMediaControlAvailable()) {
#if defined(__APPLE__)
ImGui::TextDisabled("External audio detection unavailable (requires macOS 14.2 or later).");
#else
ImGui::TextDisabled("Media controls are unavailable.");
#endif
} else {
ImGui::TextDisabled("No external media is currently playing.");
}
@@ -1093,6 +1107,36 @@ void DrawGraphicsSettings() {
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 380.0f);
ImGui::TextDisabled("Frame interpolation is experimental, you might find visual artifacts");
ImGui::PopTextWrapPos();
#if defined(__APPLE__)
const bool metalFxSupported = aurora_is_metalfx_spatial_supported();
ImGui::BeginDisabled(!metalFxSupported);
if (ImGui::Checkbox("MetalFX spatial upscaling", &g_metalFxSpatialUpscaling)) {
aurora_set_metalfx_spatial(g_metalFxSpatialUpscaling);
RuntimeConfigFile::SetMetalFxSpatialUpscaling(g_metalFxSpatialUpscaling);
}
ImGui::EndDisabled();
switch (aurora_get_metalfx_status()) {
case AURORA_METALFX_ACTIVE:
ImGui::TextDisabled("Active: upscaling the game image before the overlay.");
break;
case AURORA_METALFX_NOT_UPSCALING:
ImGui::TextDisabled("Choose a lower internal resolution to use MetalFX.");
break;
case AURORA_METALFX_UNSUPPORTED:
ImGui::TextDisabled("Requires macOS 13+, Metal, and a MetalFX-capable GPU.");
break;
case AURORA_METALFX_ERROR:
ImGui::TextDisabled("Unavailable after a renderer error; toggle off and on to retry.");
break;
case AURORA_METALFX_DISABLED:
if (metalFxSupported) {
ImGui::TextDisabled("Render below output resolution for sharper lower-cost output.");
} else {
ImGui::TextDisabled("MetalFX spatial upscaling is unavailable on this device.");
}
break;
}
#endif
if (ImGui::Checkbox("Disable copy filter", &g_disableCopyFilter)) {
aurora_set_disable_copy_filter(g_disableCopyFilter);
RuntimeConfigFile::SetDisableCopyFilter(g_disableCopyFilter);
@@ -1397,6 +1441,9 @@ void InitializeRuntimeSettings() noexcept {
MusicAttenuation::SetVoicesVolume(static_cast<float>(g_voicesVolumePercent) / 100.0f);
MusicAttenuation::SetEnabled(g_attenuateMusicWhenMediaPlays);
RuntimeGameGraphicsOptions::SetDisabledPostProcessingPaths(g_disabledPostProcessingPaths);
#if defined(__APPLE__)
aurora_set_metalfx_spatial(g_metalFxSpatialUpscaling);
#endif
const uint32_t targetFps = kFrameInterpolationTargetFps[static_cast<size_t>(g_frameInterpolationMode)];
LimitResolutionForFrameRate();
aurora_set_frame_interpolation_fps(targetFps);
@@ -0,0 +1,109 @@
#include "external_audio_macos.h"
#include <CoreAudio/CoreAudio.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <stdexcept>
#include <vector>
namespace {
struct Process {
pid_t pid;
UInt32 output;
bool disappeared = false;
};
std::vector<Process> processes;
bool supported = true;
bool queryFailed = false;
int sizeRaces = 0;
void Require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
void Expect(bool available, bool playing, const char* message) {
const auto status = MusicAttenuation::QueryMacOSExternalAudio();
Require(status.available == available && status.playing == playing, message);
}
} // namespace
// Fake only the Core Audio boundary so the production enumeration and process
// filtering run unchanged, without depending on other apps on the test machine.
// Use the public selector ABI values so these mocks also build with older SDKs.
extern "C" Boolean AudioObjectHasProperty(AudioObjectID object,
const AudioObjectPropertyAddress* address) {
Require(object == kAudioObjectSystemObject &&
address->mSelector == 'prs#',
"Must query the system process list");
return supported;
}
extern "C" OSStatus AudioObjectGetPropertyDataSize(
AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32* size) {
if (queryFailed) return kAudioHardwareUnspecifiedError;
*size = static_cast<UInt32>(processes.size() * sizeof(AudioObjectID));
return noErr;
}
extern "C" OSStatus AudioObjectGetPropertyData(
AudioObjectID object, const AudioObjectPropertyAddress* address, UInt32,
const void*, UInt32* size, void* data) {
Require(address->mScope == kAudioObjectPropertyScopeGlobal, "Must use global scope");
if (object == kAudioObjectSystemObject) {
if (sizeRaces > 0) {
--sizeRaces;
return kAudioHardwareBadPropertySizeError;
}
for (size_t i = 0; i < processes.size(); ++i) {
static_cast<AudioObjectID*>(data)[i] = static_cast<AudioObjectID>(i + 100);
}
*size = static_cast<UInt32>(processes.size() * sizeof(AudioObjectID));
return noErr;
}
const auto& process = processes.at(object - 100);
if (process.disappeared) return kAudioHardwareBadObjectError;
if (address->mSelector == 'ppid') {
std::memcpy(data, &process.pid, sizeof(process.pid));
*size = sizeof(process.pid);
} else {
Require(address->mSelector == 'piro',
"Input-only activity must not count as playback");
Require(process.pid != getpid(), "Must exclude the game's own output");
std::memcpy(data, &process.output, sizeof(process.output));
*size = sizeof(process.output);
}
return noErr;
}
int main() {
supported = false;
Expect(false, false, "Older macOS must report unavailable");
supported = true;
Expect(true, false, "An empty process list is available but inactive");
processes = {{getpid(), 1}};
Expect(true, false, "Game audio alone must not trigger muting");
processes.push_back({getpid() + 1, 0});
Expect(true, false, "An idle or input-only app must not trigger muting");
processes.back().output = 1;
Expect(true, true, "External playback must trigger muting");
processes.back().output = 0;
Expect(true, false, "Stopping playback must restore music");
processes.back().output = 1;
processes.back().disappeared = true;
Expect(true, false, "Exiting apps must not leave music muted");
processes.push_back({getpid() + 2, 1});
Expect(true, true, "A disappearing app must not hide another active player");
sizeRaces = 1;
Expect(true, true, "A growing process list must be retried");
sizeRaces = 3;
Expect(false, false, "List retries must be bounded");
Expect(true, true, "Later polls must recover from list races");
queryFailed = true;
Expect(false, false, "Query failure must clear playing state");
queryFailed = false;
Expect(true, true, "Monitoring must recover from query failure");
processes = {{0, 1}};
Expect(true, false, "Unknown PIDs must not count as external playback");
std::cout << "macOS external audio tests passed\n";
}
+58
View File
@@ -0,0 +1,58 @@
#include "isa/ppc_isa_float.h"
#include <cmath>
#include <cstdint>
#include <cstdio>
namespace {
bool SameBits(double lhs, double rhs)
{
return PpcBitCastToU64Inline(lhs) == PpcBitCastToU64Inline(rhs);
}
bool CheckPair(double actual, float a0, float a1, float c0, float c1, float b0, float b1,
bool subtract, const char* operation)
{
const float expected0 = subtract ? std::fma(a0, c0, -b0) : std::fma(a0, c0, b0);
const float expected1 = subtract ? std::fma(a1, c1, -b1) : std::fma(a1, c1, b1);
const double expected = PpcPackPairedInline(expected0, expected1);
if (SameBits(actual, expected)) {
return true;
}
std::fprintf(stderr, "%s produced the wrong paired lanes\n", operation);
return false;
}
bool CheckNegatedPair(double actual, float a0, float a1, float c0, float c1, float b0, float b1,
bool subtract, const char* operation)
{
const float expected0 = -std::fma(a0, c0, subtract ? -b0 : b0);
const float expected1 = -std::fma(a1, c1, subtract ? -b1 : b1);
const double expected = PpcPackPairedInline(expected0, expected1);
if (SameBits(actual, expected)) {
return true;
}
std::fprintf(stderr, "%s produced the wrong paired lanes\n", operation);
return false;
}
} // namespace
int main()
{
const double a = PpcPackPairedInline(1.000000119f, -123.25f);
const double c = PpcPackPairedInline(33554431.0f, 0.0625f);
const double b = PpcPackPairedInline(-33554430.0f, 7.75f);
bool ok = true;
ok &= CheckPair(PPC_PsMaddNoNiInline(a, c, b), 1.000000119f, -123.25f,
33554431.0f, 0.0625f, -33554430.0f, 7.75f, false, "ps_madd");
ok &= CheckPair(PPC_PsMsubNoNiInline(a, c, b), 1.000000119f, -123.25f,
33554431.0f, 0.0625f, -33554430.0f, 7.75f, true, "ps_msub");
ok &= CheckNegatedPair(PPC_PsNmaddInline(a, c, b), 1.000000119f, -123.25f,
33554431.0f, 0.0625f, -33554430.0f, 7.75f, false, "ps_nmadd");
ok &= CheckNegatedPair(PPC_PsNmsubNoNiInline(a, c, b), 1.000000119f, -123.25f,
33554431.0f, 0.0625f, -33554430.0f, 7.75f, true, "ps_nmsub");
return ok ? 0 : 1;
}
+140
View File
@@ -0,0 +1,140 @@
#include "isa/ppc_isa_quantized.h"
#include <csignal>
#include <iostream>
#include <stdexcept>
#include <string>
static uint64_t checksum = 0;
static unsigned checks = 0;
static void Require(bool condition, const char* message) {
++checks;
if (!condition) throw std::runtime_error(message);
}
static void Hash(uint64_t value) { checksum = (checksum ^ value) * 1099511628211ull; }
template <uint32_t W> static double ReferenceLoad(uint32_t type, uint32_t scale, uint32_t addr) {
if constexpr (W == 0u) {
switch (type) {
case 0: return PpcLoadPairPsqFloatFastInline(addr);
case 4: return PpcLoadPairPsqIntegerFastInline<uint8_t>(addr, scale);
case 5: return PpcLoadPairPsqIntegerFastInline<uint16_t>(addr, scale);
case 6: return PpcLoadPairPsqIntegerFastInline<int8_t>(addr, scale);
case 7: return PpcLoadPairPsqIntegerFastInline<int16_t>(addr, scale);
}
} else {
switch (type) {
case 0: return PpcLoadSinglePsqFloatFastInline(addr);
case 4: return PpcLoadSinglePsqQuantizedFastInline<uint8_t>(addr, scale);
case 5: return PpcLoadSinglePsqQuantizedFastInline<uint16_t>(addr, scale);
case 6: return PpcLoadSinglePsqQuantizedFastInline<int8_t>(addr, scale);
case 7: return PpcLoadSinglePsqQuantizedFastInline<int16_t>(addr, scale);
}
}
std::abort();
}
template <uint32_t W> static void ReferenceStore(uint32_t type, uint32_t scale, uint32_t addr, double value) {
if constexpr (W == 0u) {
switch (type) {
case 0: PpcStorePairPsqFloatFastInline(addr, value); return;
case 4: PpcStorePairPsqQuantizedFastInline<uint8_t>(addr, value, scale); return;
case 5: PpcStorePairPsqQuantizedFastInline<uint16_t>(addr, value, scale); return;
case 6: PpcStorePairPsqQuantizedFastInline<int8_t>(addr, value, scale); return;
case 7: PpcStorePairPsqQuantizedFastInline<int16_t>(addr, value, scale); return;
}
} else {
switch (type) {
case 0: PpcStoreSinglePsqFloatFastInline(addr, value); return;
case 4: PpcStoreSinglePsqQuantizedFastInline<uint8_t>(addr, value, scale); return;
case 5: PpcStoreSinglePsqQuantizedFastInline<uint16_t>(addr, value, scale); return;
case 6: PpcStoreSinglePsqQuantizedFastInline<int8_t>(addr, value, scale); return;
case 7: PpcStoreSinglePsqQuantizedFastInline<int16_t>(addr, value, scale); return;
}
}
std::abort();
}
template <uint32_t W, uint32_t I> static void Check() {
constexpr uint32_t edges[] = {
0, 0x80000000u, 1, 0x80000001u, 0x007FFFFFu, 0x00800000u,
0x3F000000u, 0xBF000000u, 0x3F800000u, 0xBF800000u,
0x437F0000u, 0x477FFF00u, 0xC7000000u, 0x7F7FFFFFu,
0x7F800000u, 0xFF800000u, 0x7F800001u, 0x7FC01234u, 0xFFC01234u
};
for (uint32_t type : {0u, 4u, 5u, 6u, 7u}) for (uint32_t scale = 0; scale < 64; ++scale)
for (unsigned n = 0; n < std::size(edges); ++n) {
// Include ignored GQR bits and a different type/scale in the unused half.
const uint32_t half = (scale << 8) | type | 0xC0F8u;
const uint32_t loadGqr = (half << 16) | 0x2105u;
const uint32_t storeGqr = half | 0x21050000u;
const uint64_t raw = (uint64_t(edges[n]) << 32) | edges[(n + 7) % std::size(edges)];
const double value = PpcBitCastToDoubleInline(raw);
for (unsigned mode = 0; mode < 5; ++mode) {
const uint32_t addr = mode == 0 ? 32u : mode == 1 ? 0xCC008000u : mode == 2 ? 0u : 0xFFFFFF00u;
constexpr uint32_t offset = 17;
PsqTestMemory::directStack = mode == 4;
PsqTestMemory::bytes.fill(0xCD);
BigEndian::Write64(PsqTestMemory::Pointer(addr), raw);
const auto expected = PpcBitCastToU64Inline(ReferenceLoad<W>(type, scale, addr));
PsqTestMemory::accesses = 0;
double loaded;
if (mode == 0) loaded = PPC_PsqLResolvedStateInline<W, I>(loadGqr, PsqTestMemory::Pointer(addr) - offset, offset, addr);
else if (mode == 1) loaded = PPC_PsqLResolvedStateInline<W, I>(loadGqr, nullptr, offset, addr);
else if (mode == 2) loaded = PPC_PsqLStateInline<W, I, false>(loadGqr, addr);
else loaded = PPC_PsqLStateInline<W, I, true>(loadGqr, addr);
Require(PpcBitCastToU64Inline(loaded) == expected, "load bits / lane order");
Hash(expected);
const size_t width = (type == 0 ? 4u : (type == 4 || type == 6) ? 1u : 2u) * (2u - W);
Require(PsqTestMemory::accesses == ((mode == 0 || mode == 4) ? 0u : 1u), "load route");
if (PsqTestMemory::accesses) Require(PsqTestMemory::address == addr && PsqTestMemory::width == width, "load slow address / width");
PsqTestMemory::bytes.fill(0xCD);
ReferenceStore<W>(type, scale, addr, value);
const auto expectedBytes = PsqTestMemory::bytes;
PsqTestMemory::bytes.fill(0xCD);
PsqTestMemory::accesses = 0;
if (mode == 0) PPC_PsqStResolvedStateInline<W, I>(storeGqr, PsqTestMemory::Pointer(addr) - offset, offset, addr, value);
else if (mode == 1) PPC_PsqStResolvedStateInline<W, I>(storeGqr, nullptr, offset, addr, value);
else if (mode == 2) PPC_PsqStStateInline<W, I, false>(storeGqr, addr, value);
else PPC_PsqStStateInline<W, I, true>(storeGqr, addr, value);
Require(PsqTestMemory::bytes == expectedBytes, "store bits / untouched lanes");
Hash(PsqTestMemory::ReadHost<uint64_t>(PsqTestMemory::Pointer(addr)));
Require(PsqTestMemory::accesses == ((mode == 0 || mode == 4) ? 0u : 1u), "store route");
if (PsqTestMemory::accesses) Require(PsqTestMemory::address == addr && PsqTestMemory::width == width, "store slow address / width");
}
}
}
int main(int argc, char** argv) {
try {
if (argc > 1) {
std::signal(SIGABRT, [](int) { std::_Exit(86); });
const uint32_t type = static_cast<uint32_t>(std::stoul(argv[2]));
const bool single = std::string(argv[3]) == "1";
uint8_t* host = std::string(argv[4]) == "resolved" ? PsqTestMemory::bytes.data() : nullptr;
if (std::string(argv[1]) == "load") {
if (single) PPC_PsqLResolvedStateInline<1, 7>(type << 16, host, 0, 0);
else PPC_PsqLResolvedStateInline<0, 7>(type << 16, host, 0, 0);
} else {
if (single) PPC_PsqStResolvedStateInline<1, 7>(type, host, 0, 0, 0);
else PPC_PsqStResolvedStateInline<0, 7>(type, host, 0, 0, 0);
}
return 0;
}
const auto saved = MkwGetHostFpControl();
CpuContext ctx{};
for (uint32_t ni : {0u, 4u}) for (uint32_t rounding = 0; rounding < 4; ++rounding) {
ctx.fpscr = ni;
CpuContextScope scope(&ctx);
#if defined(__x86_64__)
MkwSetHostFpControl((MkwGetHostFpControl() & ~(3u << 13)) | (rounding << 13));
#endif
Check<0, 0>(); Check<1, 0>(); Check<0, 7>(); Check<1, 7>();
}
MkwRestoreHostMxcsr(saved);
std::cout << "passed " << checks << " checks, checksum " << std::hex << checksum << '\n';
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
+83
View File
@@ -0,0 +1,83 @@
#pragma once
// Instrumented ISA memory seam: no guest VM reservation or GPU is needed.
#include "big_endian.h"
#include <array>
#include <cstdint>
#include <cstring>
namespace PsqTestMemory {
inline std::array<uint8_t, 512> bytes{};
inline uint32_t address = 0;
inline size_t width = 0;
inline unsigned accesses = 0;
inline bool directStack = false;
inline uint8_t* Pointer(uint32_t addr) { return bytes.data() + (addr & 255u); }
inline void Record(uint32_t addr, size_t size) { address = addr; width = size; ++accesses; }
template <typename T> T ReadHost(const uint8_t* host) {
if constexpr (sizeof(T) == 1) return *host;
else if constexpr (sizeof(T) == 2) return BigEndian::Read16(host);
else if constexpr (sizeof(T) == 4) return BigEndian::Read32(host);
else return (uint64_t(BigEndian::Read32(host)) << 32) | BigEndian::Read32(host + 4);
}
template <typename T> void WriteHost(uint8_t* host, T value) {
if constexpr (sizeof(T) == 1) *host = value;
else if constexpr (sizeof(T) == 2) BigEndian::Write16(host, value);
else if constexpr (sizeof(T) == 4) BigEndian::Write32(host, value);
else BigEndian::Write64(host, value);
}
template <typename T> T Read(uint32_t addr) {
Record(addr, sizeof(T)); return ReadHost<T>(Pointer(addr));
}
template <typename T> void Write(uint32_t addr, T value) {
Record(addr, sizeof(T)); WriteHost(Pointer(addr), value);
}
}
namespace GuestFlat {
inline bool RequiresCheckedAccess() noexcept { return true; }
}
#define MKW_FLAT_GUEST_BASE (PsqTestMemory::bytes.data())
class Memory {
public:
static uint8_t Read8(uint32_t a) { return PsqTestMemory::Read<uint8_t>(a); }
static uint16_t Read16(uint32_t a) { return PsqTestMemory::Read<uint16_t>(a); }
static uint32_t Read32(uint32_t a) { return PsqTestMemory::Read<uint32_t>(a); }
static uint64_t Read64(uint32_t a) { return PsqTestMemory::Read<uint64_t>(a); }
static void Write8(uint32_t a, uint8_t v) { PsqTestMemory::Write(a, v); }
static void Write16(uint32_t a, uint16_t v) { PsqTestMemory::Write(a, v); }
static void Write32(uint32_t a, uint32_t v) { PsqTestMemory::Write(a, v); }
static void Write64(uint32_t a, uint64_t v) { PsqTestMemory::Write(a, v); }
};
namespace MemoryInline {
inline bool FlatWriteNeedsPolicy(uint32_t) { return true; }
inline bool TryGetPointerFast(uint32_t a, size_t, uint8_t*& host) {
host = PsqTestMemory::directStack ? PsqTestMemory::Pointer(a) : nullptr;
return host != nullptr;
}
inline bool TryGetWritablePointerFast(uint32_t a, size_t n, uint8_t*& host) {
return TryGetPointerFast(a, n, host);
}
template <typename T> T ReadResolvedFallback(uint32_t a) { return PsqTestMemory::Read<T>(a); }
template <typename T> void WriteResolvedFallback(uint32_t a, T v) { PsqTestMemory::Write(a, v); }
template <typename T> T ReadResolved(uint8_t* host, uint32_t o, uint32_t a) {
return host ? PsqTestMemory::ReadHost<T>(host + o) : ReadResolvedFallback<T>(a);
}
template <typename T> void WriteResolved(uint8_t* host, uint32_t o, uint32_t a, T v) {
if (host) PsqTestMemory::WriteHost(host + o, v); else WriteResolvedFallback(a, v);
}
#define PSQ_TEST_MEMORY_WIDTH(Bits, Type) \
inline Type ReadStack##Bits(uint32_t a) { \
return PsqTestMemory::directStack ? PsqTestMemory::ReadHost<Type>(PsqTestMemory::Pointer(a)) : PsqTestMemory::Read<Type>(a); } \
inline void WriteStack##Bits(uint32_t a, Type v) { \
if (PsqTestMemory::directStack) PsqTestMemory::WriteHost(PsqTestMemory::Pointer(a), v); else PsqTestMemory::Write(a, v); } \
inline Type ReadResolved##Bits(uint8_t* h, uint32_t o, uint32_t a) { return ReadResolved<Type>(h, o, a); } \
inline void WriteResolved##Bits(uint8_t* h, uint32_t o, uint32_t a, Type v) { WriteResolved(h, o, a, v); }
PSQ_TEST_MEMORY_WIDTH(8, uint8_t)
PSQ_TEST_MEMORY_WIDTH(16, uint16_t)
PSQ_TEST_MEMORY_WIDTH(32, uint32_t)
PSQ_TEST_MEMORY_WIDTH(64, uint64_t)
#undef PSQ_TEST_MEMORY_WIDTH
}
+13
View File
@@ -0,0 +1,13 @@
foreach(operation load store)
foreach(type 1 2 3)
foreach(lanes 0 1)
foreach(route resolved null)
execute_process(COMMAND "${PSQ_TEST_EXECUTABLE}" ${operation} ${type} ${lanes} ${route}
RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error)
if(NOT result EQUAL 86)
message(FATAL_ERROR "Reserved PSQ ${operation}/${type}/${lanes}/${route}: ${result} ${output} ${error}")
endif()
endforeach()
endforeach()
endforeach()
endforeach()
+35
View File
@@ -0,0 +1,35 @@
#include "runtime_config.h"
#include <iostream>
#include <sstream>
namespace {
bool Require(bool value, const char* message) {
if (!value) {
std::cerr << message << '\n';
return false;
}
return true;
}
} // namespace
int main() {
{
std::istringstream input("[video]\nmetalfx_spatial_upscaling = true\n");
const RuntimeUserConfig config = RuntimeConfigFile::ParseConfig(input, "enabled.toml");
if (!Require(config.metalFxSpatialUpscaling == std::optional<bool>{true},
"valid MetalFX setting was not loaded")) return 1;
}
{
std::istringstream input("[video]\nmetalfx_spatial_upscaling = false\n");
const RuntimeUserConfig config = RuntimeConfigFile::ParseConfig(input, "disabled.toml");
if (!Require(config.metalFxSpatialUpscaling == std::optional<bool>{false},
"false MetalFX setting was not loaded")) return 1;
}
{
std::istringstream input("[video]\nmetalfx_spatial_upscaling = \"yes\"\n");
const RuntimeUserConfig config = RuntimeConfigFile::ParseConfig(input, "invalid.toml");
if (!Require(!config.metalFxSpatialUpscaling, "invalid MetalFX setting was accepted")) return 1;
}
return 0;
}
+19 -5
View File
@@ -97,15 +97,29 @@ int main() {
Check(!Pressed(pulse), "pulse expires");
// The timing-window idiom seen in shared Dolphin configs.
auto window = Compile("!pulse(`W`, 0.05) & pulse(`W`, 0.15)");
auto window = Compile("!pulse(`W`, 0.05) & pulse(`W`, 0.35)");
g_inputs["W"] = 0.0;
window.Evaluate(Source());
g_inputs["W"] = 1.0;
Check(!Pressed(window), "window closed before its start");
Sleep(90);
Check(Pressed(window), "window open between the two pulses");
Sleep(90);
Check(!Pressed(window), "window closed after its end");
bool windowOpened = false;
for (int i = 0; i < 40; ++i) {
Sleep(10);
if (Pressed(window)) {
windowOpened = true;
break;
}
}
Check(windowOpened, "window open between the two pulses");
bool windowClosed = false;
for (int i = 0; i < 50; ++i) {
Sleep(10);
if (!Pressed(window)) {
windowClosed = true;
break;
}
}
Check(windowClosed, "window closed after its end");
// timer ramps 0..1 and wraps, so a threshold turns it into a square wave.
auto timer = Compile("`X` & timer(0.1)");
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -225,13 +225,37 @@ public sealed partial class CxxLinearCodeGenerator
var labelNames = func.Blocks.ToDictionary(b => b.Label, b => SanitizeLabel(b.Label), StringComparer.OrdinalIgnoreCase);
var instructionContinuationLabels = new Dictionary<uint, string>();
var continuationCallCount = func.Blocks
var continuationCallCount = func.Blocks.Sum(block =>
{
var term = block.Instructions.LastOrDefault();
var emittedInstructionCount = term is IrBranch or IrJump or IrReturn or IrJumpTable or IrUndefined
? Math.Max(0, block.Instructions.Count - 1)
: block.Instructions.Count;
var suppressed = suppressedInstructionMasks.TryGetValue(block.Label, out var mask) ? mask : null;
return Enumerable.Range(0, emittedInstructionCount).Count(index =>
{
if ((suppressed is not null && index < suppressed.Length && suppressed[index]) ||
block.Instructions[index] is not IrCall call ||
!TryParseAddress(call.Target, out var target) ||
TryGetInlineGuestThunkSpec(target, out _))
{
return false;
}
return nonReturningCallTargets.Contains(target) ||
(lrContinuationCallTargets.Contains(target) &&
TryGetLocalFallthroughLr(block.Instructions, index, nonReturningCallTargets, lrContinuationCallTargets).HasValue);
});
});
// Labels remain available for every recognized continuation target.
// Sharing the terminal dispatcher is narrower: it only applies when
// more than one emitted call can actually jump there.
var needsInstructionContinuationLabels = func.Blocks
.SelectMany(static block => block.Instructions)
.OfType<IrCall>()
.Count(call =>
.Any(call =>
TryParseAddress(call.Target, out var target) &&
(nonReturningCallTargets.Contains(target) || lrContinuationCallTargets.Contains(target)));
var needsInstructionContinuationLabels = continuationCallCount > 0;
var shareLrContinuationDispatch = continuationCallCount > 1;
if (needsInstructionContinuationLabels)
{
@@ -380,7 +404,7 @@ public sealed partial class CxxLinearCodeGenerator
instructionContinuationLabels.TryGetValue(trace.Address, out var continuationLabel))
{
RecordEmittedLocalLabel(continuationLabel);
body.AppendLine($"{continuationLabel}:");
body.AppendLine($"{continuationLabel}: ;");
}
var localFallthroughLr = TryGetLocalFallthroughLr(block.Instructions, i, nonReturningCallTargets, lrContinuationCallTargets);
// State-free bodies are cloned after register caching and lose their
@@ -500,7 +524,7 @@ public sealed partial class CxxLinearCodeGenerator
// Every call site has already reloaded the callee's state.
// Keep the complete local target set, but emit it only once.
body.AppendLine(" return;");
body.AppendLine("[[maybe_unused]] lr_continuation_dispatch:");
body.AppendLine("lr_continuation_dispatch:");
EmitLocalLrContinuationDispatch(body, " ", labelNames);
body.AppendLine(" if (TranslatedFunctionRegistry::FindByAddressPtr(ctx->lr) != nullptr) {");
body.AppendLine(" InvokeIndirectCpu(ctx->lr, ctx);");
@@ -1943,21 +1943,6 @@ public sealed partial class PpcLifter
};
case "bltl":
{
var crField = "cr0";
if (ops.Count > 0 && ops[0] is PpcConditionRegisterOperand crOp)
{
crField = NormalizeRegister(crOp.Name);
}
return new IrInstruction[]
{
new IrAssign("lr", IrValue.Imm((int)ins.EndAddress)),
new IrBranch("blt", TargetLabel(ins, validAddresses, preferFallthrough: false),
$"0x{ins.EndAddress:X8}", crField)
};
}
case "bcl":
{
var rawInstr = ReadRawInstruction(ins);
@@ -1971,6 +1956,7 @@ public sealed partial class PpcLifter
{
instructions.Add(new IrBinary("ctr", IrValue.Register("ctr"), IrValue.Imm(-1), "add"));
}
instructions.Add(new IrAssign("lr", IrValue.Imm((int)ins.EndAddress)));
instructions.Add(new IrBranch("raw", linkedTarget, fallthrough, BuildBoConditionExpression(bo, bi, allowCtr: true)));
return instructions;
}
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Translator.Core.Analysis.Representation;
using Translator.Core.Analysis.Ssa;
using Translator.Core.CodeGen;
@@ -153,4 +153,34 @@ public class EmittedOutputShapeTests
Assert.Contains("f3.d = MemoryInline::FlatReadFloat32((r4 + 16));", code, StringComparison.Ordinal);
Assert.Contains("f4.d = MemoryInline::FlatReadFloat64((r4 + 24));", code, StringComparison.Ordinal);
}
[Fact]
public void ContinuationLabelAtBlockEndEmitsValidCxx17Statement()
{
var function = new IrFunction("continuation_at_block_end", "0x800E7798", new[]
{
new IrBasicBlock("0x800E7798", new IrInstruction[]
{
new IrCall(string.Empty, "0x8179B000", System.Array.Empty<IrValue>()),
new IrTracePpc(0x800E77A0u, "nop", "0x60000000"),
new IrJump("0x800E77A4")
}),
new IrBasicBlock("0x800E77A4", new IrInstruction[]
{
new IrReturn(null)
})
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>());
var code = new CxxLinearCodeGenerator().Emit(
0x800E7798,
new SsaTransformer().Convert(function),
new FunctionAbiClassification(function.Name, ValueRepresentation.Void),
types,
lrContinuationCallTargets: new HashSet<uint> { 0x8179B000u });
// In C++17, a label before a closing brace is invalid without an intervening statement.
Assert.Contains("loc_800E77A0: ;", code, StringComparison.Ordinal);
Assert.DoesNotContain("loc_800E77A0:\n}", code.Replace("\r\n", "\n"), StringComparison.Ordinal);
}
}
@@ -21,14 +21,20 @@ public class PpcLifterAdditionalCoverageTests
var ir = Assert.Single(new PpcLifter().Lift(new[] { branch })).Ir;
Assert.Equal("bltl", branch.Mnemonic);
var lr = Assert.IsType<IrAssign>(ir[0]);
Assert.Equal("lr", lr.Destination);
Assert.Equal(unchecked((int)0x80004398u), lr.Value.Constant);
Assert.Equal(2, ir.Count);
var lrAssign = Assert.IsType<IrAssign>(ir[0]);
Assert.Equal("lr", lrAssign.Destination);
Assert.Equal(unchecked((int)0x80004398u), lrAssign.Value.Constant);
Assert.Equal(unchecked((int)branch.EndAddress), lrAssign.Value.Constant);
var decision = Assert.IsType<IrBranch>(ir[1]);
Assert.Equal("blt", decision.Condition);
Assert.Equal("0x800043BC", decision.TrueLabel);
Assert.Equal("raw", decision.Condition);
Assert.Equal("link_branch_80004398_800043BC", decision.TrueLabel);
Assert.Equal("0x80004398", decision.FalseLabel);
Assert.Equal($"link_branch_{branch.EndAddress:X8}_{branch.BranchTargets.First():X8}", decision.TrueLabel);
Assert.Equal($"0x{branch.EndAddress:X8}", decision.FalseLabel);
}
[Fact]
@@ -9,6 +9,33 @@ namespace Translator.Tests;
public class SharedLrContinuationCodeGenTests
{
[Fact]
public void OnlyLocalLinkRegisterContinuationsCountTowardSharedDispatch()
{
var function = new IrFunction("single_lr_continuation", "0x80001000", new[]
{
new IrBasicBlock("0x80001000", new IrInstruction[]
{
new IrCall(string.Empty, "0x81800000", Array.Empty<IrValue>()),
new IrAssign("lr", IrValue.Imm(unchecked((int)0x80001004u))),
new IrCall(string.Empty, "0x81800000", Array.Empty<IrValue>()),
new IrReturn(null)
})
});
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
{
["lr"] = ValueRepresentation.UInt32
});
var code = new CxxLinearCodeGenerator().Emit(0x80001000,
new SsaTransformer().Convert(function),
new FunctionAbiClassification("single_lr_continuation", ValueRepresentation.Void), types,
lrContinuationCallTargets: new HashSet<uint> { 0x81800000u });
Assert.DoesNotContain("lr_continuation_dispatch:", code);
Assert.DoesNotContain("goto lr_continuation_dispatch;", code);
}
[Theory]
[InlineData(2)]
[InlineData(20)]
@@ -15,6 +15,7 @@ internal static class TranslatorCppTestHarness
Path.Combine("runtime", "src", "fpu_helpers.cpp"),
Path.Combine("runtime", "src", "memory.cpp"),
Path.Combine("runtime", "src", "ppc_helpers.cpp"),
Path.Combine("runtime", "src", "ppc_quantized.cpp"),
};
public static string BuildCompileArguments(
@@ -31,6 +32,7 @@ internal static class TranslatorCppTestHarness
args.Append("-std=c++17 ");
args.Append("-D_CRT_SECURE_NO_WARNINGS ");
args.Append("-march=x86-64-v3 ");
args.Append("-fno-fast-math -ffp-contract=off ");
if (!RuntimeHeadersDefineRestrictMacro(repoRoot))
{
// Fallback for the window between an emitter change using MKW_RESTRICT and the runtime