98 Commits

Author SHA1 Message Date
patchzyy e6f9b2197e Fix LLVM path 2026-09-04 21:17:11 +02:00
patchzyy 65047bc7b7 Bump LLVM 2026-09-04 21:12:18 +02:00
patchzyy 989d5e00da Updat eversion 2026-09-04 21:02:02 +02:00
patchzyy efc44b0482 Default Wii remote continuous scanning to off (#137) 2026-09-04 19:35:39 +02:00
patchzyy d3d0de62a6 Throttle SDL logs and gate Wii rescans (#129)
* Throttle SDL logs and gate Wii rescans

* Clarify Wii remote scan state in overlay
2026-09-03 23:29:49 +02:00
patchzyy c6ef17378e Bundle LLVM runtime libs in portable tools 2026-09-03 11:32:15 +02:00
patchzyy 38a11b7b2c CI: download direct-upload artifacts with download-artifact v8 2026-09-03 10:53:45 +02:00
patchzyy 7a13696a8b Bump version to 0.2.26 2026-09-03 10:40:31 +02:00
theofficialgman 2bcfdca199 Linux appimage prebuild aurora (#121)
* Linux Appimage: statically prebuild Aurora (and all its dependencies)

adds symlinks to the compiler locations in a static path that way rebuilds do not think that the compiler path has changed between appimage install commands

* Update package.yml

* Update Launcher/build-appimage.sh

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-09-03 10:20:24 +02:00
patchzyy f2165a5aa6 Update package.yml (#127) 2026-09-03 09:17:48 +02:00
Michael G ffc7244277 Support Kamek v2 Code.pul files (#126) 2026-09-03 07:52:17 +02:00
theofficialgman dec9ed8df1 CI: automatically run packaging CI on every tag (#119) 2026-09-02 17:38:14 +02:00
patchzyy 8dc9bb67b3 Update Build-Installer.ps1 2026-09-01 21:56:28 +02:00
Javier R Bueno ca7d126a13 Bluetooth Wii Remote support: Wii Remote / Wii Wheel, Nunchuk and Classic Controller through KPAD (#73)
* Bluetooth Wii Remote support: the game reads a real Wii Remote through KPAD

Enable SDL3's HIDAPI Wii driver and hand a paired Wii Remote (bare or with
Nunchuk) to the game as a real Wii Remote: WPADProbe reports CORE/FREESTYLE
and KPADRead fills KPADStatus[0] from SDL every frame (buttons, accelerometer
in KPAD's g frame, Nunchuk stick and accelerometer), while the GameCube pad
view of that port reports no controller. The game's own motion code then
handles wheelies, tricks and Wii Wheel steering. Classic Controllers and
Wii U Pro Controllers keep going through the GameCube pad path with a default
button table picked by name.

SDL's Wii driver drops a remote on a failed Bluetooth read or when the
Nunchuk is plugged or unplugged and never re-adds it, so the runtime keeps
rescanning (Dolphin style) while no Wii controller is present by toggling the
driver hint off and, a few frames later, on again; a dropped remote is back
within 1-2 s. Settings live in the F10 overlay under Wii Remotes (Bluetooth)
and in Config.toml (wii_remotes, wii_continuous_scan).

* Fix Wii U Pro / Classic Controller ZL and ZR not registering

SDL's Wii driver reports ZL/ZR as the LEFT_TRIGGER/RIGHT_TRIGGER analog
axes, never as digital shoulder buttons. Binding them to
LEFT_SHOULDER/RIGHT_SHOULDER meant they never fired and also disabled
aurora's own analog-trigger fallback (a button table entry for
PAD_TRIGGER_L/R marks the trigger as "handled", even when the bound
digital button never actually presses). Leaving them unbound lets the
default axis mapping drive them like every other analog-trigger pad.

Reported by an end-to-end tester connecting a real Classic Controller to
a Wii Remote.

* Wii Remotes menu: live raw D-pad/ZL/ZR readout for Classic Controller / Wii U Pro

Diagnostic aid for a reported issue where the Classic Controller's D-pad
does not do anything in-game (no wheelies). Shows what SDL itself sees so
a driver-level problem (nothing lights up) can be told apart from a
mapping problem (it lights up but the game does not react).

* Fix Classic Controller D-pad input

* Address CodeRabbit review on PR #73

- PADRead: hide KPAD-served ports even while input is blocked so the port
  error state does not flip when the overlay opens/closes.
- WPADProbe: run the Wii Remote rescan state machine before probing so a
  reconnect probe before the next PADRead can see the remote.
- EnsureSensors: only cache the gamepad id once every accelerometer enabled,
  so a failed activation is retried.
- ConfigureSdlHints: reset the in-flight rescan bookkeeping.
- Settings overlay: disable "Rescan now" while Wii Remotes are turned off.

* Bluetooth Wii Remote: fix wheel steering, native Classic Controller, extension hot-swap

Accelerometer
- The SDL -> KPAD conversion negated the wrong axis: SDL's z is the remote's
  +Y (towards the user), so KPAD acc is (-wiiX, -wiiZ, +wiiY). Fixes mirrored
  Wii Wheel steering.
- Drop reports whose accelerometer bytes arrive zeroed (+-5.12 g on every axis,
  a few times a minute over Bluetooth) and repeat the last good sample; they
  read as a full-lock steer plus a 9 g shake.
- One-button zero-point calibration in the overlay (remote flat, buttons up),
  stored in Config.toml as wii_accel_offset_x/y/z. SDL's read of the remote's
  factory calibration times out over Bluetooth and falls back to a nominal
  zero point, which left a per-axis bias of up to ~0.3 g on the tested remote.
- Live accelerometer readout and an optional per-frame CSV trace
  (wii_accel_trace = true) for debugging.

Classic Controller through KPAD/WPAD
- WPADProbe reports WPAD_DEV_CLASSIC; KPADRead fills ex_status.cl and
  KPADGetUnifiedWpadStatus the raw WPADCLStatus (WPAD_CL_BUTTON_* bits, sticks
  in the SDK's signed -512..511 range, triggers), so the game shows the Classic
  layout and icons and no button mapping is involved. Ports served through KPAD
  are hidden from PADRead; only the Wii U Pro Controller stays a GameCube pad.

Extension hot-swap
- SDL's Wii driver destroys the joystick on an extension change but keeps the
  HID handle open, and HIDAPI never re-creates a joystick for such a device.
  Patch the vendored SDL at configure time (AuroraSDL3Patches.cmake, wired into
  AuroraSDL3Provider.cmake for both the downloaded tarball and a pre-provided
  FETCHCONTENT_SOURCE_DIR_SDL) so the joystick is rebuilt in place with the new
  extension type, without touching the Bluetooth handle.
- Keep a vanished remote's channel alive with neutral input for up to 3 s while
  SDL re-creates the joystick, so the game never sees a disconnection. The
  driver-hint rescan stays as a fallback for real drops, starting 3 s after
  the loss, and also runs from the overlay's per-frame Draw. Log rescans.

Mappings / overlay
- Do not apply the shared positional [controller] bindings to Wii pads: that
  override is what made a Classic Controller's A/B and X/Y look swapped.
- Raw D-pad fallback also for the Wii U Pro Controller; overlay readouts read
  joystick buttons directly (SDL's generated HIDAPI mapping expects a hat).
- Overlay: Classic Controller readout, accelerometer readout and calibration.
- README: Bluetooth Wii Remote section and known limitations.

* Review pass on the Wii Remote input path

- EffectiveKind: stop bridging an extension swap once a different controller
  has taken the port, and note that everything touching the scanner state runs
  on the guest thread.
- KPADGetUnifiedWpadStatus: fill every requested entry (the SDK returns `count`
  recent samples), capped at KPAD's 16 read buffers.
- IsKpadKind gets internal linkage; the calibration accessors get their
  comments; clarify why Draw() also runs Poll().

* Drop the dead Classic-Controller-as-GameCube-pad matching

A Wii Remote with a Classic Controller is served through KPAD and its port is
hidden from PADRead, so the name matches that once gave it a GameCube button
table and the raw D-pad fallback could never take effect any more. Both now
match only the Wii U Pro Controller, and the default table is renamed
accordingly (g_defaultButtonsWiiUPro).

---------

Co-authored-by: LOL <andresguerra2k26@gmail.com>
Co-authored-by: Nick <89667145+Nick1232345@users.noreply.github.com>
2026-09-01 21:47:31 +02:00
Michael G 5c76e2b0df feature: add apple silicon native macOS support (#81)
* feature: add apple silicon native macOS support - #81

* (macos): Fix crash

This fixes a crash when viewing the rear camera

* fix(macos): keep interpolated presentation on main thread

* fix(macos): supply Retro-WFC payload during setup

* perf(windows): compile out flat-memory fallback check

* remove duplicate smoke test

* test(macos): name and focus host platform tests

* fix(macos): validate Retro-WFC payload cache

* fix(payload): preserve staged file access failures

* Limit flat-page checks to variable-page hosts

---------

Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
2026-09-01 18:57:15 +02:00
theofficialgman ae3096c89b linux appimage: bundle prebuilt clang, ninja, and cmake 2026-09-01 17:01:57 +02:00
theofficialgman 6f4dd2f470 CI: update actions target versions
also upload .appimage and .exe files directly using the new "archive: false" option rather than zipping them
2026-09-01 17:01:13 +02:00
patchzyy 2b5b889249 Add windows CI 2026-09-01 17:01:13 +02:00
theofficialgman fbfb741b49 Create package CI 2026-09-01 17:01:13 +02:00
theofficialgman e630132dd3 Update Build-Installer.ps1 2026-09-01 17:01:13 +02:00
patchzyy a6f3720598 Merge pull request #115 from DarthMDev/security-tls-certificate-validation
security: enforce TLS certificate validation
2026-09-01 16:58:04 +02:00
DarthM abe7cd2b61 security: enforce TLS certificate validation 2026-09-01 03:43:59 -04:00
patchzyy 7aa04bfe80 Delete unneeded note 2026-08-31 16:44:06 +02:00
patchzyy 1af7cd6d9b Merge pull request #109 from DarthMDev/discord-rich-presence
feat: add basic Discord rich presence support
2026-08-31 16:32:44 +02:00
patchzyy d31942c356 Merge pull request #110 from patchzyy/Color-profile-crash-fix
Refine SEH crash handling on Windows
2026-08-31 16:32:18 +02:00
patchzyy 13b6d0b447 Refine SEH crash handling on Windows 2026-08-31 16:28:24 +02:00
patchzyy db576cd5cb Update runtime_config.h 2026-08-31 16:08:32 +02:00
DarthM edacfcb8bc fix: handshake before Discord activity updates 2026-08-31 06:57:19 -04:00
DarthM 36faa76dc0 fix: harden Discord IPC retries 2026-08-31 06:36:45 -04:00
DarthM 3fc1555871 config: set default Discord client ID 2026-08-31 06:15:30 -04:00
DarthM be691bd08d feat: add Discord rich presence support 2026-08-31 06:15:30 -04:00
patchzyy 243eb86021 Merge pull request #50 from theofficialgman/aarch64-main
Add ARM64 Support (with Linux ARM64 packaging implementation)
2026-08-30 22:28:36 +02:00
patchzyy 7b750e96ac Update ppc_isa_float.h 2026-08-30 22:20:58 +02:00
patchzyy e4e388295b Merge branch 'aarch64-main' of https://github.com/theofficialgman/Wiicompiled into pr/50 2026-08-30 22:09:50 +02:00
patchzyy 2c38db4174 Merge pull request #99 from zydezu/add-mpris-support
Add MPRIS for music attenuation support on linux
2026-08-30 21:07:16 +02:00
zydezu 1c7e4e79c5 FEAT: add MPRIS for music attenuation support on linux 2026-08-30 19:17:58 +01:00
patchzyy 9a22de1079 Bump version to 0.2.25 2026-08-30 11:41:22 +02:00
patchzyy c0359b05a7 Merge pull request #92 from patchzyy/proper-WUP
Enable libusb in SDL
2026-08-30 11:39:16 +02:00
patchzyy 6c1d4faaf7 Merge pull request #93 from patchzyy/Remove-f10-controller-block
Remove the f10 menu blocking controller inputs
2026-08-30 11:35:31 +02:00
patchzyy 3fb7373c4f Update AuroraLibUSB.cmake 2026-08-30 11:34:45 +02:00
patchzyy 8dd689c6d2 Update settings_overlay.cpp 2026-08-30 11:27:37 +02:00
patchzyy 69be584cc5 WUP 2026-08-30 11:21:10 +02:00
patchzyy 8d569ef77b Merge pull request #91 from patchzyy/Bring-back-auto-assignment
bring back auto-assignment
2026-08-30 10:49:10 +02:00
patchzyy 36cc6abe24 bring back auto-assignment 2026-08-30 10:39:40 +02:00
theofficialgman 6ddc153e44 add native aarch64 (arm64) support
use -mcpu=native on arm64 targets

build-appimage.sh kernel architecture detection
2026-08-29 23:08:43 -04:00
patchzyy 806f4127bb Merge pull request #37 from theofficialgman/main
Add Real (Native) Linux Support
2026-08-30 01:14:30 +02:00
patchzyy fb9b101de7 Update Test-NativeDependencies.ps1 2026-08-30 01:08:10 +02:00
theofficialgman 64ea7b7401 correct windows tests after refactor 2026-08-29 14:55:02 -04:00
theofficialgman 4f4716be5a remove WUP028 driver which is already handled by SDL3 on Linux
also restores automatic port assignment on non-Windows. This should be the default as https://github.com/patchzyy/Wiicompiled/pull/42 was never a necessary change
2026-08-29 13:54:25 -04:00
theofficialgman cb812c1fd0 add native aarch64 (arm64) support
use -mcpu=native on arm64 targets
2026-08-29 12:12:06 -04:00
theofficialgman c0ed2bfbeb refactor WiiCompiled.Setup into WiiCompiled.Setup.Windows and add WiiCompiled.Setup.Common
the idea behind this is C# code that is OS agnostic can go in WiiCompiled.Setup.Common to be shared by any OS specific code (eg: WiiCompiled.Setup.Windows and WiiCompiled.Setup.Linux).
2026-08-29 12:06:58 -04:00
theofficialgman f09590aa5d switch to standalone nod tool rather than dolphin-tool 2026-08-29 12:06:58 -04:00
theofficialgman edc5fa1dd3 also bundle dotnet in translator for linux appimage 2026-08-29 12:06:58 -04:00
theofficialgman c5db4d0e8e add appimage buildscript for linux 2026-08-29 12:06:58 -04:00
theofficialgman e3c4028b50 partial linux setup/install scripting 2026-08-29 12:06:58 -04:00
theofficialgman 48c4df342f fix non-fatal linker warning on linux 2026-08-29 12:06:13 -04:00
theofficialgman c93f82e938 add linux local-build.sh script 2026-08-29 12:06:13 -04:00
theofficialgman 9f799fe12b final linux support necessary changes to build a working application 2026-08-29 12:06:13 -04:00
theofficialgman 02df7ef9ee correct type-alias difference on linux 2026-08-29 12:06:13 -04:00
theofficialgman 88ab029c74 add linux option for guest memory usage 2026-08-29 12:06:13 -04:00
theofficialgman a0b54b874b use libco on non-Win32 systems for guest OSThread scheduling 2026-08-29 12:04:08 -04:00
theofficialgman f63d0c84b5 add linux support to runtime build 2026-08-29 12:02:59 -04:00
patchzyy 6d836dab3e Bump version to 0.2.24 2026-08-29 15:22:41 +02:00
patchzyy 6bffedf028 Merge pull request #67 from patchzyy/fix-accented-paths
fix accented paths
2026-08-29 15:02:06 +02:00
patchzyy 09ca4e98f4 Update system_bridge.cpp 2026-08-29 15:01:51 +02:00
patchzyy 6dc4e59052 Coderabbit fixes 2026-08-29 11:56:35 +02:00
patchzyy 82b295b20c Merge pull request #77 from patchzyy/Fix-unsupported-locale
Fix paranthesees
2026-08-28 22:19:24 +02:00
patchzyy eb1721fd72 Update NativeBuildFlags.ps1 2026-08-28 20:45:46 +02:00
patchzyy 129c714f02 Update disk space 2026-08-28 20:42:20 +02:00
patchzyy 2794024d3a Fix paranthesees 2026-08-28 20:38:27 +02:00
patchzyy b5e5858e1d Use UTF-8-safe filesystem paths end-to-end 2026-08-28 19:01:14 +02:00
patchzyy 4897e7e27d Use safe UTF-8 path construction 2026-08-28 10:52:01 +02:00
patchzyy 7ebda6bf7f mangle path fix 2026-08-28 10:30:07 +02:00
patchzyy 0403f176bd fix accented paths 2026-08-28 01:25:13 +02:00
patchzyy 1912292c80 Merge pull request #42 from GalaxisBeast/main
add gamecube controller support for all 4 ports
2026-08-27 23:58:32 +02:00
patchzyy e498e62622 Merge pull request #63 from s5bug/fix-translator-readme-formatting
fix formatting of translator/README.md
2026-08-27 23:53:52 +02:00
Aly f73739f248 fix formatting of translator/README.md 2026-08-27 14:56:15 -06:00
patchzyy 3389fe35fc Merge pull request #57 from patchzyy/fix-ghosts
Implement missing ISFS_ReadDir
2026-08-27 19:45:42 +02:00
Cristian Boehm e11062a1ba Merge branch 'patchzyy:main' into main 2026-08-27 13:29:49 -04:00
patchzyy 1d1d064d37 Implement missing ISFS_ReadDir 2026-08-27 19:15:21 +02:00
patchzyy 9d1f3db1d5 Merge pull request #48 from patchzyy/fix-choking-logging
Update os_report.cpp
2026-08-27 14:43:06 +02:00
GalaxisBeast 251529d66e fix issues
fix timeout loop, fixed drift issues, fixed infinite rumble, fixed gamecube controller adapter taking over all ports, require assigning a virtual controller port specifically to the gamecube controller adapter, and block inputs from gamecube controller while settings menu (f10 menu) is open
2026-08-26 20:12:15 -04:00
patchzyy 987b666296 Update os_report.cpp 2026-08-27 00:59:40 +02:00
patchzyy 260022b4ba version update 2026-08-26 23:39:04 +02:00
GalaxisBeast f1c2b60df1 fix mistake where i literally deleted the code 2026-08-26 17:37:14 -04:00
Cristian Boehm 5193dd9749 Merge branch 'main' into main 2026-08-26 17:22:47 -04:00
patchzyy ec9bd92415 Merge pull request #43 from patchzyy/Unknown-Controller-support
Unknown controller support
2026-08-26 23:04:55 +02:00
patchzyy dbf6d05625 SDL cache + output stream 2026-08-26 22:50:20 +02:00
GalaxisBeast 8ba44162fa Update wup028_adapter.cpp 2026-08-26 15:55:57 -04:00
GalaxisBeast 9ee14e582d Update wup028_adapter.cpp 2026-08-26 15:41:00 -04:00
GalaxisBeast 0c349a9296 Update pad.cpp 2026-08-26 15:35:32 -04:00
patchzyy 21b57f08a4 Unknown controller support 2026-08-26 21:32:22 +02:00
GalaxisBeast e146b10af8 add gamecube controller support for all 4 ports 2026-08-26 15:17:28 -04:00
patchzyy edd03f8991 Merge pull request #23 from patchzyy/Premature-success-fix
fix underflow and crash on malformed stream
2026-08-24 21:50:19 +02:00
patchzyy 9c4f4b738a fix underflow and crash on malformed stream 2026-08-24 19:20:20 +02:00
patchzyy 8d5d93f02d version update 2026-08-24 13:40:44 +02:00
patchzyy c75b482224 Merge pull request #17 from patchzyy/decompress-overlow
Fix szs memory corruption
2026-08-24 12:03:50 +02:00
patchzyy e5ae29b27e Create egg_decomp.cpp 2026-08-24 11:30:11 +02:00
169 changed files with 18875 additions and 970 deletions
+2 -2
View File
@@ -18,11 +18,11 @@ jobs:
name: Translator (build + test)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@v4
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '8.0.x'
+156
View File
@@ -0,0 +1,156 @@
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
# 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.
on:
push:
tags:
- '*'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: package-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
linux-appimage:
name: Linux (AppImage, ${{ matrix.arch }})
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-22.04
arch: x86_64
- runner: ubuntu-22.04-arm
arch: aarch64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '8.0.x'
# appimagetool ships as an AppImage itself and needs to self-mount via FUSE to run directly;
# GitHub's ubuntu-latest runners don't have libfuse2 preinstalled.
- name: Install dependencies (required by appimagetool and SDL3 build)
run: |
sudo apt-get update
sudo apt-get install -y libfuse2 build-essential git make \
pkg-config cmake ninja-build gnome-desktop-testing libasound2-dev libpulse-dev \
libaudio-dev libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \
libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev libthai-dev libusb-1.0-0-dev \
libpipewire-0.3-dev libwayland-dev libdecor-0-dev liburing-dev
- name: Build AppImage
run: bash Launcher/build-appimage.sh
- uses: actions/upload-artifact@v7
with:
name: WiiCompiled-Setup-linux-${{ matrix.arch }}
path: Launcher/dist/WiiCompiled-Setup-${{ matrix.arch }}.AppImage
if-no-files-found: error
archive: false
windows-installer:
name: Windows (installer exe)
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '8.0.x'
- name: Build installer
shell: pwsh
run: ./Launcher/Build-Installer.ps1
- uses: actions/upload-artifact@v7
with:
name: WiiCompiled-Setup-windows-x64
path: Launcher/dist/WiiCompiled-Setup.exe
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.
release:
name: Publish GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
needs: [linux-appimage, windows-installer]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
# Wheel Wizard runs the downloaded setup with --version and refuses it when the reported
# version differs from the release tag, so a tag that was pushed without bumping every pinned
# version would ship an update nobody can install. Catch that before anything is published.
- name: Verify the tag matches the pinned setup version
env:
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
version="${TAG#v}"
status=0
check() {
if ! grep -Fq "$2" "$1"; then
echo "::error file=$1::expected '$2' for tag $TAG"
status=1
fi
}
check Launcher/WiiCompiled.Setup.Windows/Program.cs "public const string Version = \"$version\";"
check Launcher/WiiCompiled.Setup.Windows/WiiCompiled.Setup.Windows.csproj "<Version>$version</Version>"
check Launcher/Build-Installer.ps1 "ProductVersion = '$version'"
exit $status
# The build jobs upload with `archive: false`, which stores each installer as a raw file
# rather than a zip (and names the artifact after the file). Only download-artifact v8 and
# later understand such direct uploads; v7 tries to unzip everything and fails on them.
- uses: actions/download-artifact@v8
with:
path: artifacts
merge-multiple: true
- name: Publish release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
ls -lR artifacts
assets=()
for name in WiiCompiled-Setup.exe WiiCompiled-Setup-x86_64.AppImage WiiCompiled-Setup-aarch64.AppImage; 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")
done
# A re-run of a tag whose release already exists only refreshes the assets.
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release upload "$TAG" "${assets[@]}" --repo "$GITHUB_REPOSITORY" --clobber
else
gh release create "$TAG" "${assets[@]}" \
--repo "$GITHUB_REPOSITORY" \
--title "$TAG" \
--verify-tag \
--generate-notes
fi
+1
View File
@@ -25,6 +25,7 @@ Code.pul
# Build output
/build/
/build-*/
/native-build/
/dist/
/out/
[Bb]in/
+50 -21
View File
@@ -1,7 +1,6 @@
[CmdletBinding(PositionalBinding = $false)]
param(
[string]$OutputDirectory = 'Launcher/dist',
[string]$DolphinToolPath,
[string]$PortableToolsDirectory = 'Launcher/artifacts/portable-tools',
[string]$DependencySourceDirectory = 'Launcher/artifacts/dependencies',
[string]$VcRuntimeDirectory,
@@ -15,10 +14,6 @@ Set-StrictMode -Version 3.0
# helpers shared with LocalBuild.ps1 and Prepare-NativePrebuilt.ps1.
. (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1')
if ([string]::IsNullOrWhiteSpace($DolphinToolPath)) {
throw 'Build-Installer.ps1 requires -DolphinToolPath pointing to DolphinTool.exe.'
}
$repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$outputRoot = [IO.Path]::GetFullPath((Join-Path $repoRoot $OutputDirectory))
$portableTools = [IO.Path]::GetFullPath((Join-Path $repoRoot $PortableToolsDirectory))
@@ -26,7 +21,7 @@ $dependencySources = [IO.Path]::GetFullPath((Join-Path $repoRoot $DependencySour
$workRoot = Join-Path $PSScriptRoot 'artifacts\installer-build'
$publish = Join-Path $workRoot 'publish'
$payloadRoot = Join-Path $workRoot 'payload'
$setupProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup\WiiCompiled.Setup.csproj'
$setupProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup.Windows\WiiCompiled.Setup.Windows.csproj'
$translatorProject = Join-Path $repoRoot 'translator\src\Translator.Cli\Translator.Cli.csproj'
$projectFile = Join-Path $repoRoot 'projects\mkwii\recomp.yml'
@@ -91,20 +86,25 @@ function Compress-Zip([string]$Source, [string]$Destination, [string[]]$Entries)
Assert-File $setupProject '.NET setup project'
Assert-File $translatorProject 'Translator CLI project'
Assert-File $DolphinToolPath 'DolphinTool'
if (-not (Test-Path -LiteralPath (Join-Path $portableTools 'llvm-mingw\bin\x86_64-w64-mingw32-clang++.exe'))) {
# A PowerShell script, not a native executable - it never touches $LASTEXITCODE, and its own
# $ErrorActionPreference = 'Stop' + throw already aborts this run on failure.
& (Join-Path $PSScriptRoot 'Prepare-PortableTools.ps1') -Destination $portableTools
if ($LASTEXITCODE -ne 0) { throw 'Portable tool preparation failed.' }
}
Assert-File (Join-Path $portableTools 'CMake\bin\cmake.exe') 'Portable CMake'
Assert-File (Join-Path $portableTools 'Ninja\ninja.exe') 'Portable Ninja'
Assert-Directory $dependencySources 'Pinned offline dependency sources'
# native_prebuilt carries the aurora/third-party archives the user no longer has
# to compile (launcher/Prepare-NativePrebuilt.ps1).
# Kept in step with InstalledLayout.DependencyNames by Test-PinnedFacts.ps1: the installed host
# refuses to call a toolkit complete unless every one of these directories is present.
$requiredDependencies = @('abseil-cpp','cppwinrt','dawn_prebuilt','fmt','freetype','imgui','native_prebuilt','png','SDL','sqlite3','tracy','xxhash','zlib','zstd')
$requiredDependencies = @('abseil-cpp','cppwinrt','dawn_prebuilt','fmt','freetype','imgui','libusb','native_prebuilt','png','SDL','sqlite3','tracy','xxhash','zlib','zstd')
$missingSources = @($requiredDependencies | Where-Object { $_ -ne 'native_prebuilt' } |
Where-Object { -not (Test-Path -LiteralPath (Join-Path $dependencySources $_) -PathType Container) })
if ($missingSources.Count -gt 0) {
& (Join-Path $PSScriptRoot 'Prepare-Dependencies.ps1') -Destination $dependencySources
}
Assert-Directory $dependencySources 'Pinned offline dependency sources'
# The precompiled archives are only interchangeable with what the user's machine
# compiles if both came from this toolchain and this flag set, so a stale package
@@ -169,6 +169,14 @@ $translator = Join-Path $publish 'translator\Translator.Cli.exe'
Assert-File $setupHost 'Published setup host'
Assert-File $translator 'Self-contained translator'
# Resolved via the shared WiiCompiled.Setup.Common.Cli helper (also used by build-appimage.sh on
# Linux) rather than a separate download/version-pin copy here: it downloads and caches the same way
# NodToolProvider.cs always does (Launcher/artifacts/nodtool.exe)
$nodToolCliProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup.Common.Cli'
$nodTool = (& dotnet run --project $nodToolCliProject -c Release -- --workspace $repoRoot | Select-Object -Last 1)
if ($LASTEXITCODE -ne 0) { throw "nodtool resolution failed with exit code $LASTEXITCODE." }
Assert-File $nodTool 'Resolved nodtool'
Write-Host '[2/6] Staging the explicit, game-code-free payload allowlist...'
# The staged layout mirrors the installed layout exactly (Toolkit, BuildWorkspace): payload
# identities hash relative paths, so the names here are part of the fingerprint contract.
@@ -191,7 +199,7 @@ Get-ChildItem -LiteralPath (Join-Path $toolkit 'llvm-mingw\bin') -File |
Remove-Item -Force
[IO.Directory]::CreateDirectory((Join-Path $toolkit 'Translator')) | Out-Null
Copy-Item -LiteralPath $translator -Destination (Join-Path $toolkit 'Translator\Translator.Cli.exe')
Copy-Item -LiteralPath $DolphinToolPath -Destination (Join-Path $toolkit 'DolphinTool.exe')
Copy-Item -LiteralPath $nodTool -Destination (Join-Path $toolkit 'nodtool.exe')
[IO.Directory]::CreateDirectory((Join-Path $toolkit 'Redist')) | Out-Null
Copy-Item -Path (Join-Path $vcRuntime '*.dll') -Destination (Join-Path $toolkit 'Redist')
Copy-Item -Path (Join-Path $vcRuntime '*.dll') -Destination (Join-Path $toolkit 'CMake\bin')
@@ -225,18 +233,38 @@ Copy-Item (Join-Path $dependencySources 'cppwinrt\LICENSE.txt') (Join-Path $payl
# The precompiled aurora/third-party archives are built from the very sources
# already shipped under build-workspace\Dependencies and aurora-main, so they add
# no third-party component and therefore no new license obligation.
$dolphinLicense = Join-Path (Split-Path -Parent $DolphinToolPath) 'COPYING'
if (Test-Path $dolphinLicense) { Copy-Item $dolphinLicense (Join-Path $payloadRoot 'licenses\Dolphin-COPYING.txt') }
@"
DolphinTool source offer
nodtool (disc image extraction)
Project and complete corresponding source: https://github.com/dolphin-emu/dolphin
Dolphin is licensed under GPLv2+ with additional per-file SPDX licenses.
"@ | Set-Content (Join-Path $payloadRoot 'licenses\Dolphin-SOURCE.txt') -Encoding UTF8
Project: https://github.com/encounter/nod
Dual-licensed under MIT OR Apache-2.0.
MIT License
Copyright 2021 Luke Street.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"@ | Set-Content (Join-Path $payloadRoot 'licenses\nodtool-LICENSE-MIT.txt') -Encoding UTF8
@"
Microsoft Visual C++ Runtime
Redistributable x64 runtime DLLs are included app-locally for DolphinTool and third-party renderer DLLs.
Redistributable x64 runtime DLLs are included app-locally for nodtool and third-party renderer DLLs.
Microsoft license terms: https://visualstudio.microsoft.com/license-terms/
"@ | Set-Content (Join-Path $payloadRoot 'licenses\Microsoft-VC-Runtime.txt') -Encoding UTF8
# Compute the payload's content identities once, here, with the same code every installed host
@@ -253,7 +281,7 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
$manifest = [ordered]@{
SchemaVersion = 2
ProductVersion = '0.2.21'
ProductVersion = '0.2.27'
ExpectedGameId = $pins.GameId
ExpectedDolSha256 = $pins.DolSha256
ExpectedRelSha256 = $pins.RelSha256
@@ -269,11 +297,12 @@ $manifest = [ordered]@{
$manifest | ConvertTo-Json | Set-Content (Join-Path $payloadRoot 'payload-manifest.json') -Encoding UTF8
Write-Host '[4/6] Enforcing the copyright and generated-code boundary...'
# Both audits are PowerShell scripts that throw directly on failure (Test-PayloadBoundary.ps1
# never invokes a native command at all, so it never touches $LASTEXITCODE); there is no exit
# code to check here, and doing so risks reading a stale value from an unrelated earlier command.
& (Join-Path $PSScriptRoot 'Test-PayloadBoundary.ps1') -PayloadRoot $payloadRoot
if ($LASTEXITCODE -ne 0) { throw 'Payload boundary audit failed.' }
& (Join-Path $PSScriptRoot 'Test-NativeDependencies.ps1') -PayloadRoot $payloadRoot -SetupHost $setupHost `
-LlvmReadobjPath (Join-Path $portableTools 'llvm-mingw\bin\llvm-readobj.exe')
if ($LASTEXITCODE -ne 0) { throw 'Native dependency audit failed.' }
Write-Host '[5/6] Creating the canonical installer payload...'
$payloadZip = Join-Path $workRoot 'payload.zip'
+5 -4
View File
@@ -151,9 +151,10 @@ if ($Profile -ne 'both' -and -not [string]::IsNullOrWhiteSpace($BaseOutputDirect
throw '-BaseOutputDirectory is valid only with -Profile both.'
}
$translator = Join-Path $Toolkit 'Translator\Translator.Cli.exe'
$cmake = Join-Path $Toolkit 'CMake\bin\cmake.exe'
$ninja = Join-Path $Toolkit 'Ninja\ninja.exe'
$toolchainBin = Join-Path $Toolkit 'llvm-mingw\bin'
$toolchain = Get-MkwShellSafeToolchainRoot $Toolkit
$cmake = Join-Path $toolchain 'CMake\bin\cmake.exe'
$ninja = Join-Path $toolchain 'Ninja\ninja.exe'
$toolchainBin = Join-Path $toolchain 'llvm-mingw\bin'
$cc = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang.exe'
$cxx = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang++.exe'
$windres = Join-Path $toolchainBin 'x86_64-w64-mingw32-windres.exe'
@@ -203,7 +204,7 @@ if ($Parallel -gt 0) {
$oldPath = $env:PATH
$oldDotnet = $env:DOTNET_ROOT
try {
$env:PATH = Get-MkwToolchainPath $Toolkit
$env:PATH = Get-MkwToolchainPath $toolchain
Remove-Item Env:DOTNET_ROOT -ErrorAction SilentlyContinue
Push-Location $Workspace
try {
+37
View File
@@ -40,6 +40,43 @@ function Get-MkwToolchainPath([string]$ToolchainRoot) {
) -join ';')
}
function Get-MkwShellSafeToolchainRoot([string]$ToolchainRoot) {
if ([string]::IsNullOrWhiteSpace($ToolchainRoot)) { throw 'A toolchain root is required.' }
$full = [IO.Path]::GetFullPath($ToolchainRoot)
# A drive root keeps its separator: "C:" is relative to the current directory on that drive.
if ($full -ne [IO.Path]::GetPathRoot($full)) { $full = $full.TrimEnd('\') }
if ($full -notmatch '[()&^%!]') { return $full }
$sha = [Security.Cryptography.SHA256]::Create()
try {
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($full.ToLowerInvariant()))
} finally { $sha.Dispose() }
$linkName = 'toolchain-' + ((($bytes[0..7]) | ForEach-Object { $_.ToString('x2') }) -join '')
$failures = @()
foreach ($base in @($env:ProgramData, $env:PUBLIC)) {
if ([string]::IsNullOrWhiteSpace($base) -or $base -match '[()&^%! ]') { continue }
$link = Join-Path (Join-Path $base 'WiiCompiled') $linkName
try {
[IO.Directory]::CreateDirectory((Split-Path -Parent $link)) | Out-Null
# The name already identifies the target, so an existing junction that still resolves is
# this one; only a broken leftover is replaced. Directory.Delete removes the reparse
# point itself, where Remove-Item -Recurse would delete the toolchain it points at.
if (-not (Test-Path -LiteralPath (Join-Path $link 'CMake\bin\cmake.exe') -PathType Leaf)) {
if (Test-Path -LiteralPath $link) { [IO.Directory]::Delete($link) }
New-Item -ItemType Junction -Path $link -Target $full -ErrorAction Stop | Out-Null
}
Write-Host "MKWCBUILD: Building through $link, because $full contains characters cmd.exe cannot parse"
return $link
} catch {
$failures += "$link ($($_.Exception.Message))"
}
}
throw ("The toolchain path $full contains a character (one of ( ) & ^ % !) that the compiler " +
'cannot be invoked through, and no junction to it could be created: ' + ($failures -join '; ') +
'. Install to a path without those characters.')
}
function Get-MkwProjectPins([string]$ProjectFile) {
<#
The Mario Kart Wii facts pinned by projects/mkwii/recomp.yml (game identity, clean input
+196
View File
@@ -0,0 +1,196 @@
# Populates Launcher/artifacts/dependencies: the pinned, unpatched upstream source trees (plus the
# prebuilt Dawn package and generated C++/WinRT headers) that Build-Installer.ps1 ships inside the
# installer so the user's build runs with FETCHCONTENT_FULLY_DISCONNECTED=ON. Every pin is the one
# aurora-main's own CMake declares; the Pin fields below are asserted against those files so the
# two cannot drift apart silently. native_prebuilt is not fetched here - Prepare-NativePrebuilt.ps1
# compiles it from these trees.
[CmdletBinding()]
param(
[string]$Destination,
# Windows metadata source for cppwinrt.exe: 'local' (this machine's WinMetadata), 'sdk', or an
# installed SDK version such as 10.0.26100.0.
[string]$CppWinRtInput = 'local'
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 3.0
$repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$auroraCMake = Join-Path $repoRoot 'aurora-main\CMakeLists.txt'
$auroraExtern = Join-Path $repoRoot 'aurora-main\extern\CMakeLists.txt'
$auroraDawn = Join-Path $repoRoot 'aurora-main\cmake\AuroraDawnProvider.cmake'
$auroraLibUsb = Join-Path $repoRoot 'aurora-main\cmake\AuroraLibUSB.cmake'
$auroraSdl = Join-Path $repoRoot 'aurora-main\cmake\AuroraSDL3Provider.cmake'
# Name = directory under the destination; FetchContent maps it back through
# FETCHCONTENT_SOURCE_DIR_<UPPERCASE NAME> (NativeBuildFlags.ps1), so the names are the declared
# FetchContent names, not the upstream project names.
$packages = @(
[pscustomobject]@{
Name = 'SDL'; File = 'SDL3-3.4.4.tar.gz'
Uris = @('https://github.com/libsdl-org/SDL/releases/download/release-3.4.4/SDL3-3.4.4.tar.gz')
Pins = @(@{ File = $auroraCMake; Text = 'set(AURORA_SDL3_VERSION "3.4.4"' },
@{ File = $auroraSdl; Text = 'releases/download/release-${AURORA_SDL3_VERSION}/SDL3-${AURORA_SDL3_VERSION}.tar.gz' })
},
[pscustomobject]@{
Name = 'abseil-cpp'; File = 'abseil-cpp-20240722.0.tar.gz'
Uris = @('https://github.com/abseil/abseil-cpp/archive/refs/tags/20240722.0.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/abseil/abseil-cpp/archive/refs/tags/20240722.0.tar.gz' })
},
[pscustomobject]@{
Name = 'dawn_prebuilt'; File = 'dawn-v20260603.191052-windows-amd64.tar.gz'
Uris = @('https://github.com/encounter/dawn-build/releases/download/v20260603.191052/dawn-windows-amd64.tar.gz')
Pins = @(@{ File = $auroraCMake; Text = 'set(AURORA_DAWN_VERSION "v20260603.191052"' },
@{ File = $auroraDawn; Text = 'SHA256=7785373d569b3b0237918ec9c523239f7d0667857c5ea8242e3cdfde95e6aeab' })
},
[pscustomobject]@{
Name = 'fmt'; File = 'fmt-11.1.4.tar.gz'
Uris = @('https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz' })
},
[pscustomobject]@{
Name = 'freetype'; File = 'freetype-2.14.3.tar.gz'
Uris = @('https://files.twilitrealm.dev/freetype-2.14.3.tar.gz',
'https://download.savannah.gnu.org/releases/freetype/freetype-2.14.3.tar.gz',
'https://downloads.sourceforge.net/project/freetype/freetype2/2.14.3/freetype-2.14.3.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://files.twilitrealm.dev/freetype-2.14.3.tar.gz' })
},
[pscustomobject]@{
Name = 'imgui'; File = 'imgui-1.91.9b-docking.tar.gz'
Uris = @('https://github.com/ocornut/imgui/archive/refs/tags/v1.91.9b-docking.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/ocornut/imgui/archive/refs/tags/v1.91.9b-docking.tar.gz' })
},
[pscustomobject]@{
Name = 'libusb'; File = 'libusb-1.0.30.tar.bz2'
Uris = @('https://github.com/libusb/libusb/releases/download/v1.0.30/libusb-1.0.30.tar.bz2')
Pins = @(@{ File = $auroraCMake; Text = 'set(AURORA_LIBUSB_VERSION "1.0.30"' },
@{ File = $auroraLibUsb; Text = 'releases/download/v${AURORA_LIBUSB_VERSION}/libusb-${AURORA_LIBUSB_VERSION}.tar.bz2' })
},
[pscustomobject]@{
Name = 'png'; File = 'libpng-1.6.58.tar.gz'
Uris = @('https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.58.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.58.tar.gz' })
},
[pscustomobject]@{
Name = 'sqlite3'; File = 'sqlite-amalgamation-3510300.zip'
Uris = @('https://sqlite.org/2026/sqlite-amalgamation-3510300.zip')
Pins = @(@{ File = $auroraExtern; Text = 'https://sqlite.org/2026/sqlite-amalgamation-3510300.zip' })
},
[pscustomobject]@{
Name = 'tracy'; File = 'tracy-a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz'
Uris = @('https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz' })
},
[pscustomobject]@{
Name = 'xxhash'; File = 'xxHash-0.8.3.tar.gz'
Uris = @('https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.3.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.3.tar.gz' })
},
[pscustomobject]@{
Name = 'zlib'; File = 'zlib-1.3.2.tar.gz'
Uris = @('https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz' })
},
[pscustomobject]@{
Name = 'zstd'; File = 'zstd-1.5.7.tar.gz'
Uris = @('https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz' })
}
)
# The C++/WinRT compiler (NuGet package = zip) that generates the projection headers.
$cppWinRtTool = [pscustomobject]@{
Name = 'cppwinrt-tool'; File = 'Microsoft.Windows.CppWinRT.3.0.260818.1.nupkg'
Uris = @('https://www.nuget.org/api/v2/package/Microsoft.Windows.CppWinRT/3.0.260818.1')
Pins = @()
}
if (-not $Destination) { $Destination = Join-Path $PSScriptRoot 'artifacts\dependencies' }
$Destination = [IO.Path]::GetFullPath($Destination)
$downloads = Join-Path $PSScriptRoot 'artifacts\downloads'
$tar = Join-Path $env:SystemRoot 'System32\tar.exe'
if (-not (Test-Path -LiteralPath $tar -PathType Leaf)) { throw "Windows archive tool is missing: $tar" }
[IO.Directory]::CreateDirectory($Destination) | Out-Null
[IO.Directory]::CreateDirectory($downloads) | Out-Null
function Assert-Pinned($Package) {
foreach ($pin in $Package.Pins) {
if (-not (Test-Path -LiteralPath $pin.File -PathType Leaf)) { throw "Pin source is missing: $($pin.File)" }
$content = [IO.File]::ReadAllText($pin.File)
if (-not $content.Contains($pin.Text)) {
throw "$($Package.Name) is pinned to '$($pin.Text)' here but $($pin.File) no longer declares it; update both."
}
}
}
function Get-Archive($Package) {
$archive = Join-Path $downloads $Package.File
if (Test-Path -LiteralPath $archive -PathType Leaf) { return $archive }
$temporary = $archive + '.partial'
$failures = @()
foreach ($uri in $Package.Uris) {
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
Write-Host "Downloading $($Package.Name) from $uri..."
try {
Invoke-WebRequest -UseBasicParsing -Uri $uri -OutFile $temporary
} catch {
$failures += "$uri ($($_.Exception.Message))"
continue
}
Move-Item -LiteralPath $temporary -Destination $archive -Force
return $archive
}
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
throw "$($Package.Name) could not be fetched: $($failures -join '; ')"
}
function Expand-Package([string]$Archive, [string]$Target) {
# Release archives wrap everything in one versioned directory (SDL3-3.4.4/, tracy-<sha>/) while
# dawn-build's package is flat; either way the tree lands directly under the target.
$extract = Join-Path (Split-Path -Parent $Target) ('.extract-' + (Split-Path -Leaf $Target))
if (Test-Path -LiteralPath $extract) { Remove-Item -LiteralPath $extract -Recurse -Force }
[IO.Directory]::CreateDirectory($extract) | Out-Null
# Symbolic links need a privilege Windows tar usually lacks; the only ones in these archives
# are zstd test aliases, and none are build inputs.
$listing = & $tar -tvf $Archive
if ($LASTEXITCODE -ne 0) { throw "Listing $Archive failed with exit code $LASTEXITCODE." }
$excludes = @($listing | ForEach-Object { if ($_ -match '^l.*\s(\S+)\s->\s') { '--exclude=' + $Matches[1] } })
& $tar -xf $Archive -C $extract @excludes
if ($LASTEXITCODE -ne 0) { throw "Extracting $Archive failed with exit code $LASTEXITCODE." }
$entries = @(Get-ChildItem -LiteralPath $extract -Force)
$source = $extract
if ($entries.Count -eq 1 -and $entries[0].PSIsContainer) { $source = $entries[0].FullName }
if (Test-Path -LiteralPath $Target) { Remove-Item -LiteralPath $Target -Recurse -Force }
Move-Item -LiteralPath $source -Destination $Target
if (Test-Path -LiteralPath $extract) { Remove-Item -LiteralPath $extract -Recurse -Force }
}
foreach ($package in $packages) {
Assert-Pinned $package
$target = Join-Path $Destination $package.Name
if (Test-Path -LiteralPath $target -PathType Container) { continue }
Expand-Package (Get-Archive $package) $target
Write-Host "Prepared $($package.Name)"
}
$cppWinRt = Join-Path $Destination 'cppwinrt'
if (-not (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType Leaf)) {
$toolRoot = Join-Path $PSScriptRoot 'artifacts\cppwinrt-tool'
$compiler = Join-Path $toolRoot 'bin\cppwinrt.exe'
if (-not (Test-Path -LiteralPath $compiler -PathType Leaf)) {
Expand-Package (Get-Archive $cppWinRtTool) $toolRoot
}
if (-not (Test-Path -LiteralPath $compiler -PathType Leaf)) { throw "cppwinrt.exe is missing: $compiler" }
if (Test-Path -LiteralPath $cppWinRt) { Remove-Item -LiteralPath $cppWinRt -Recurse -Force }
[IO.Directory]::CreateDirectory($cppWinRt) | Out-Null
Write-Host "Generating C++/WinRT headers (-input $CppWinRtInput)..."
& $compiler -input $CppWinRtInput -output $cppWinRt
if ($LASTEXITCODE -ne 0) { throw "cppwinrt.exe failed with exit code $LASTEXITCODE." }
if (-not (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType Leaf)) {
throw "cppwinrt.exe produced no winrt\base.h under $cppWinRt"
}
Copy-Item -LiteralPath (Join-Path $toolRoot 'LICENSE') -Destination (Join-Path $cppWinRt 'LICENSE.txt')
Write-Host 'Prepared cppwinrt'
}
Write-Host "Pinned dependency sources ready: $Destination"
-2
View File
@@ -61,8 +61,6 @@ Assert-File $windres 'Portable resource compiler'
Assert-File $clangBinary 'Portable clang driver binary'
Assert-Directory $dependencies 'Pinned offline dependency sources'
Assert-Directory $auroraSource 'aurora-main source tree'
Assert-File (Join-Path $repoRoot 'generated\build_shards\shards.cmake') `
'Translator shard manifest (run the developer build once so runtime/ can configure)'
if ($Parallel -le 0) { $Parallel = [Environment]::ProcessorCount }
+590
View File
@@ -0,0 +1,590 @@
#!/usr/bin/env bash
# Builds the redistributable precompiled aurora + third-party package for native Linux: aurora
# (~43% of local build CPU time per Prepare-NativePrebuilt.ps1) and vendored Crypto++ are identical
# for every user under the pinned toolchain prepare-portable-tools.sh bundles, so this configures
# runtime/ against that toolchain, builds just that closure, and harvests the archives plus a
# generated CMake description into an output package - the Linux counterpart to
# Launcher/Prepare-NativePrebuilt.ps1, consumed by the same platform-agnostic
# runtime/cmake/NativePrebuilt.cmake either script's package works with unmodified.
#
# Not a byte-for-byte port of the Windows script: Linux needs no offline pinned dependency cache.
# Every FetchContent dependency in aurora-main/extern/CMakeLists.txt is a fixed-version URL already
# (verified directly), and aurora-main/CMakeLists.txt's own _default_linkage is "static" on any
# non-Windows platform. Windows pins Launcher/artifacts/dependencies because its installer ships
# pre-fetched sources to end users for a fully offline build; this harvest only ever runs on a
# maintainer's own machine (network access needed once, here, not shipped to anyone), so a plain
# networked configure is exactly as reproducible - what actually makes the harvested archives safe
# to link into any consumer's own build is the pinned compiler below, not an offline source cache.
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace=$(cd "$script_dir/.." && pwd)
arch=""
output_dir=""
stage_dir="$workspace/build/native-prebuilt-stage"
keep_stage=0
reuse_stage=0
parallel=0
print_fingerprint_only=0
usage() {
cat <<'EOF'
Usage: Prepare-NativePrebuilt.sh --arch {x86_64|aarch64} [options]
--arch ARCH Target architecture; selects the matching bundled toolchain (required)
--output-dir DIR Where the package is written (default: Launcher/artifacts/native-prebuilt-ARCH)
--stage-dir DIR Staging build directory (default: build/native-prebuilt-stage)
--keep-stage Do not delete the staging build directory afterward
--reuse-stage Reuse an existing staging build directory (maintainer iteration aid: a
re-harvest does not recompile aurora from scratch)
--parallel N Ninja build parallelism (default: nproc)
--print-fingerprint-only Print the four provenance inputs (compiler_sha256, flag_fingerprint,
aurora_fingerprint, third_party_fingerprint) as "key=value" lines and
exit, without configuring/building/harvesting anything - lets a caller
(build-appimage.sh) decide whether an existing package is still current
without paying for a full aurora rebuild just to find out.
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--arch) arch=$2; shift 2 ;;
--output-dir) output_dir=$2; shift 2 ;;
--stage-dir) stage_dir=$2; shift 2 ;;
--keep-stage) keep_stage=1; shift ;;
--reuse-stage) reuse_stage=1; shift ;;
--parallel) parallel=$2; shift 2 ;;
--print-fingerprint-only) print_fingerprint_only=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Prepare-NativePrebuilt.sh: unknown argument: $1" >&2; exit 1 ;;
esac
done
case "$arch" in
x86_64|aarch64) ;;
*) echo "Prepare-NativePrebuilt.sh: --arch must be x86_64 or aarch64" >&2; usage; exit 1 ;;
esac
fail() { echo "Prepare-NativePrebuilt.sh: error: $*" >&2; exit 1; }
assert_file() { [[ -f "$1" ]] || fail "$2 is missing: $1"; }
assert_dir() { [[ -d "$1" ]] || fail "$2 is missing: $1"; }
sha256_of() { sha256sum "$1" | awk '{print $1}'; }
normalize() { readlink -f "$1"; }
[[ -n "$output_dir" ]] || output_dir="$script_dir/artifacts/native-prebuilt-$arch"
[[ "$stage_dir" = /* ]] || stage_dir="$workspace/$stage_dir"
toolchain_dir="$script_dir/artifacts/portable-tools/toolchain-$arch"
cc="$toolchain_dir/bin/clang"
cxx="$toolchain_dir/bin/clang++"
cmake_bin="$toolchain_dir/bin/cmake"
ninja_bin="$toolchain_dir/bin/ninja"
runtime_source="$workspace/runtime"
aurora_source="$workspace/aurora-main"
assert_file "$cmake_bin" "Portable CMake (run prepare-portable-tools.sh --arch $arch first)"
assert_file "$ninja_bin" "Portable Ninja"
assert_file "$cc" "Portable C compiler"
assert_file "$cxx" "Portable C++ compiler"
assert_dir "$aurora_source" "aurora-main source tree"
clang_binary=$(normalize "$toolchain_dir/bin/clang-23")
assert_file "$clang_binary" "Portable clang driver binary"
(( parallel > 0 )) || parallel=$(nproc)
fingerprint_tree() {
# $1 = root dir, remaining args = top-level subdirectory names to exclude
local root=$1; shift
root=$(normalize "$root")
[[ -d "$root" ]] || return 0
local find_args=("$root")
local ex
for ex in "$@"; do find_args+=(-path "$root/$ex" -prune -o); done
find_args+=(-type f -print)
find "${find_args[@]}" | LC_ALL=C sort | while IFS= read -r abs; do
printf '%s %s\n' "${abs#$root/}" "$(sha256_of "$abs")"
done | sha256sum | awk '{print $1}'
}
# The fixed (path-independent) half of the configure command line - identical for every
# harvest, which is what lets one package be valid regardless of where it was built.
# -DBUILD_SHARED_LIBS=OFF: aurora-main/extern/CMakeLists.txt derives its own _USE_SHARED from
# whether BUILD_SHARED_LIBS is *defined at all* (not its value), so leaving it unset would default
# zlib/libpng to shared. -DAURORA_SDL3_PROVIDER=vendor: without it, "auto" resolves to "system" on
# any machine that happens to have an SDL3 dev package installed. -DCMAKE_DISABLE_FIND_PACKAGE_*:
# absl/PNG/Freetype each call find_package() unconditionally, with no provider flag to gate them
# (unlike SDL3/Dawn) - verified directly that this machine's system libpng-dev/freetype-dev get
# linked in shared instead of the pinned vendored source otherwise. Disabling find_package for
# these three forces the same FetchContent-vendored, statically-built result regardless of what a
# given maintainer's machine happens to have installed. ZLIB is deliberately NOT disabled the same
# way: libpng's own vendored CMakeLists.txt calls find_package(ZLIB REQUIRED) internally even when
# zlib came from aurora's own FetchContent (extern/CMakeLists.txt writes a redirect config for
# exactly this), and CMAKE_DISABLE_FIND_PACKAGE_ZLIB errors out on any REQUIRED call site outright
# (verified directly) - it cannot be scoped to only aurora's own initial, non-required check.
# Dawn stays a prebuilt package regardless (Linux x86_64/aarch64 always auto-resolve to "package" -
# see AuroraDawnProvider.cmake).
fixed_configure_flags=(
-DCMAKE_BUILD_TYPE=Release
-DBUILD_SHARED_LIBS=OFF
-DAURORA_SDL3_PROVIDER=vendor
-DCMAKE_DISABLE_FIND_PACKAGE_absl=ON
-DCMAKE_DISABLE_FIND_PACKAGE_PNG=ON
-DCMAKE_DISABLE_FIND_PACKAGE_Freetype=ON
# Freetype's own vendored CMakeLists.txt separately probes for system BZip2 (optional
# bzip2-compressed-font support aurora-main never asked for) regardless of the Freetype
# find_package disable above, since that only stops aurora's own outer find_package(Freetype)
# from picking up a system Freetype - it does not reach into the FetchContent-built copy's own
# internal find_package(BZip2) call. FT_DISABLE_BZIP2 is Freetype's own documented flag for
# exactly this (verified in its CMakeLists.txt), unlike the ZLIB/libpng situation where no such
# source-level flag exists.
-DFT_DISABLE_BZIP2=ON
-DCMAKE_POLICY_DEFAULT_CMP0168=NEW
)
flag_fingerprint=$(printf '%s\n' "${fixed_configure_flags[@]}" | sha256sum | awk '{print $1}')
# extern/ is excluded because the payload ships that tree separately (aurora-main/extern is bundled
# whole by build-appimage.sh); build/ is a plain developer build directory.
aurora_fingerprint=$(fingerprint_tree "$aurora_source" extern build)
[[ -n "$aurora_fingerprint" ]] || fail "The aurora source tree could not be fingerprinted: $aurora_source"
# The harvested Crypto++ archive is consumed against this tree's headers, so it is fingerprinted
# for the same reason as aurora above. No exclusions: unlike aurora's extern/, nothing under
# runtime/third_party is shipped separately.
third_party_fingerprint=$(fingerprint_tree "$runtime_source/third_party")
[[ -n "$third_party_fingerprint" ]] || fail "The vendored third-party tree could not be fingerprinted: $runtime_source/third_party"
compiler_sha256=$(sha256_of "$clang_binary")
if [[ "$print_fingerprint_only" -eq 1 ]]; then
printf 'compiler_sha256=%s\n' "$compiler_sha256"
printf 'flag_fingerprint=%s\n' "$flag_fingerprint"
printf 'aurora_fingerprint=%s\n' "$aurora_fingerprint"
printf 'third_party_fingerprint=%s\n' "$third_party_fingerprint"
exit 0
fi
# The package must never contain a stale mixture of two builds.
rm -rf "$output_dir"
mkdir -p "$output_dir/lib" "$output_dir/bin" "$output_dir/include"
# The staging build has to be configured without the package present, otherwise the runtime would
# consume the very package this script is producing.
if [[ "$reuse_stage" -eq 0 ]]; then rm -rf "$stage_dir"; fi
export_dir="$stage_dir/export"
mkdir -p "$export_dir"
# fixed_configure_flags/flag_fingerprint were already computed above (needed before the
# --print-fingerprint-only early exit).
echo "Prepare-NativePrebuilt.sh: configuring the aurora/third-party staging build..."
"$cmake_bin" -S "$runtime_source" -B "$stage_dir" -G Ninja \
"${fixed_configure_flags[@]}" \
-DCMAKE_C_COMPILER="$cc" -DCMAKE_CXX_COMPILER="$cxx" \
-DCMAKE_MAKE_PROGRAM="$ninja_bin" \
-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=lld" \
-DMKW_NATIVE_PREBUILT_EXPORT_DIR="$export_dir"
# mkw_np_probe links exactly what mkw_runtime_common and the public products link, so building it
# compiles the whole redistributable closure and nothing else - and a successful link proves the
# harvested archives are complete.
echo "Prepare-NativePrebuilt.sh: building the aurora/third-party closure..."
"$cmake_bin" --build "$stage_dir" --target mkw_np_probe --parallel "$parallel"
# ---------------------------------------------------------------------------
# Read the description CMake wrote, plus the resolved command lines Ninja holds.
# ---------------------------------------------------------------------------
meta_path="$export_dir/meta.txt"
assert_file "$meta_path" "Native prebuilt export metadata"
get_meta() { awk -F= -v k="$1" '$0 ~ "^" k "=" { sub(/^[^=]*=/, ""); print; exit }' "$meta_path"; }
build_ninja="$stage_dir/build.ninja"
assert_file "$build_ninja" "Generated build.ninja"
extract_ninja_vars() {
# $1 = ninja file, $2 = exact rule name. Prints "KEY\tVALUE" for every " KEY = VALUE" line
# following the first "build ...: RULE ..." statement that uses that rule.
awk -v rule="$2" '
BEGIN { in_stmt = 0 }
/^build / {
if (in_stmt) exit
colon = index($0, ": ")
if (colon == 0) next
rest = substr($0, colon + 2)
sp = index(rest, " ")
stmt_rule = (sp > 0) ? substr(rest, 1, sp - 1) : rest
if (stmt_rule == rule) in_stmt = 1
next
}
in_stmt {
if (substr($0, 1, 2) != " ") exit
line = substr($0, 3)
eq = index(line, " = ")
if (eq == 0) exit
print substr(line, 1, eq - 1) "\t" substr(line, eq + 3)
}
' "$1"
}
declare -A probe_compile=() control_compile=() probe_link=() control_link=()
load_vars() {
local -n dest=$1
local k v
while IFS=$'\t' read -r k v; do dest["$k"]=$v; done < <(extract_ninja_vars "$build_ninja" "$2")
}
load_vars probe_compile "CXX_COMPILER__mkw_np_probe_unscanned_Release"
load_vars control_compile "CXX_COMPILER__mkw_np_probe_control_unscanned_Release"
load_vars probe_link "CXX_EXECUTABLE_LINKER__mkw_np_probe_Release"
load_vars control_link "CXX_EXECUTABLE_LINKER__mkw_np_probe_control_Release"
[[ -n "${probe_compile[INCLUDES]+x}" ]] || fail "No Ninja statement used rule CXX_COMPILER__mkw_np_probe_unscanned_Release."
[[ -n "${probe_link[LINK_LIBRARIES]+x}" ]] || fail "No Ninja statement used rule CXX_EXECUTABLE_LINKER__mkw_np_probe_Release."
split_list() {
# $1 = string value, $2 = name of the array to fill (nameref)
local -n out=$2
out=()
[[ -n "$1" ]] || return 0
read -ra out <<< "$1"
}
subtract_multiset() {
# $1 = "all" array name, $2 = "remove" array name, $3 = result array name (all namerefs)
local -n all_ref=$1 remove_ref=$2 result_ref=$3
local -A pending=()
local item
for item in "${remove_ref[@]}"; do
pending["$item"]=$(( ${pending["$item"]:-0} + 1 ))
done
result_ref=()
for item in "${all_ref[@]}"; do
if [[ "${pending["$item"]:-0}" -gt 0 ]]; then
pending["$item"]=$(( pending["$item"] - 1 ))
continue
fi
result_ref+=("$item")
done
}
split_list "${probe_compile[INCLUDES]:-}" _probe_includes
split_list "${control_compile[INCLUDES]:-}" _control_includes
subtract_multiset _probe_includes _control_includes include_arguments
split_list "${probe_compile[DEFINES]:-}" _probe_defines
split_list "${control_compile[DEFINES]:-}" _control_defines
subtract_multiset _probe_defines _control_defines define_arguments
split_list "${probe_compile[FLAGS]:-}" _probe_flags
split_list "${control_compile[FLAGS]:-}" _control_flags
subtract_multiset _probe_flags _control_flags compile_options_raw
split_list "${probe_link[LINK_LIBRARIES]:-}" _probe_link
split_list "${control_link[LINK_LIBRARIES]:-}" _control_link
subtract_multiset _probe_link _control_link link_items
[[ ${#include_arguments[@]} -gt 0 ]] || fail "The aurora compile interface is empty; the probe did not link aurora."
[[ ${#link_items[@]} -gt 0 ]] || fail "The aurora link interface is empty; the probe did not link aurora."
# ---------------------------------------------------------------------------
# Harvest the built libraries.
# ---------------------------------------------------------------------------
binary_root=$(normalize "$stage_dir")
aurora_root=$(normalize "$(get_meta aurora_dir)")
runtime_root=$(normalize "$(get_meta runtime_dir)")
generated_include_index=0
copy_harvested() {
# $1 = source path, $2 = subdirectory under the package. Echoes "subdir/name".
local source=$1 subdir=$2
local name destination
name=$(basename "$source")
destination="$output_dir/$subdir/$name"
[[ ! -e "$destination" ]] || fail "Two harvested files collide on the name $name: $source"
cp "$source" "$destination"
printf '%s/%s' "$subdir" "$name"
}
declare -A generated_include_tokens=()
convert_to_token() {
# $1 = absolute path, $2 = 'include' or another kind. Sets $TOKEN_RESULT - NOT echoed/command-
# substituted: this needs to mutate generated_include_tokens/generated_include_index, and a
# $(...) call runs in a subshell, silently discarding that mutation once it returns (verified
# directly: every build-tree include directory collided into the same "generated01" until this
# was written this way instead).
local path kind=$2
path=$(normalize "$1")
case "$path" in
"$aurora_root"/*) TOKEN_RESULT="@AURORA@/${path#$aurora_root/}"; return ;;
"$runtime_root"/*) TOKEN_RESULT="@RUNTIME@/${path#$runtime_root/}"; return ;;
"$binary_root"/*)
[[ "$kind" == include ]] || fail "Unhandled build-tree artifact: $path"
if [[ -n "${generated_include_tokens[$path]:-}" ]]; then
TOKEN_RESULT=${generated_include_tokens[$path]}
return
fi
generated_include_index=$((generated_include_index + 1))
local name destination
name=$(printf 'generated%02d' "$generated_include_index")
destination="$output_dir/include/$name"
mkdir -p "$destination"
cp -a "$path/." "$destination/"
TOKEN_RESULT="@PKG@/include/$name"
generated_include_tokens[$path]=$TOKEN_RESULT
return ;;
*) fail "Path escapes every shippable root ($kind): $path" ;;
esac
}
declare -A linker_file_to_reference=()
shared_import_targets=() shared_import_runtime=() shared_import_implib=()
targets_txt="$export_dir/targets.txt"
assert_file "$targets_txt" "Native prebuilt export targets list"
while IFS='|' read -r name type file linkerfile; do
[[ -n "$name" ]] || continue
[[ -f "$linkerfile" ]] || continue # not part of the closure mkw_np_probe pulled in
linkerfile=$(normalize "$linkerfile")
relative_linker=$(copy_harvested "$linkerfile" lib)
if [[ "$type" == "SHARED_LIBRARY" ]]; then
file_abs=$(normalize "$file")
# Unlike Windows (a .dll + a separate .dll.a import library), a Linux shared object's
# TARGET_FILE and TARGET_LINKER_FILE are the same path - one file, harvested once.
if [[ "$file_abs" == "$linkerfile" ]]; then
relative_runtime=$relative_linker
else
[[ -f "$file_abs" ]] || fail "Shared library $name has no runtime file: $file_abs"
relative_runtime=$(copy_harvested "$file_abs" bin)
fi
shared_import_targets+=("mkw_np::$name")
shared_import_runtime+=("$relative_runtime")
shared_import_implib+=("$relative_linker")
linker_file_to_reference["$linkerfile"]="mkw_np::$name"
else
linker_file_to_reference["$linkerfile"]="@PKG@/$relative_linker"
fi
done < "$targets_txt"
# Unlike Windows (SDL/zlib/libpng ship as DLLs by default), everything here was forced static above
# and Dawn's own Linux package (verified directly) ships libwebgpu_dawn.a, also static - so zero
# shared imports is the expected, normal outcome, not a failure.
echo "Prepare-NativePrebuilt.sh: harvested ${#linker_file_to_reference[@]} archive(s) (${#shared_import_targets[@]} shared)"
dawn_linker_file="" dawn_runtime_file=""
dawn_txt="$export_dir/dawn.txt"
if [[ -s "$dawn_txt" ]]; then
IFS='|' read -r _ dawn_runtime_file dawn_linker_file < "$dawn_txt"
dawn_runtime_file=$(normalize "$dawn_runtime_file")
dawn_linker_file=$(normalize "$dawn_linker_file")
fi
[[ -n "$dawn_linker_file" ]] && linker_file_to_reference["$dawn_linker_file"]="dawn::webgpu_dawn"
# ---------------------------------------------------------------------------
# Rewrite the compile/link interface into workspace-relative tokens.
# ---------------------------------------------------------------------------
package_include_dirs=()
include_count=${#include_arguments[@]}
i=0
while (( i < include_count )); do
entry=${include_arguments[i]}
case "$entry" in
-I*) convert_to_token "${entry#-I}" include; package_include_dirs+=("$TOKEN_RESULT") ;;
-isystem)
i=$((i + 1))
(( i < include_count )) || fail "Trailing -isystem with no argument"
convert_to_token "${include_arguments[i]}" include; package_include_dirs+=("$TOKEN_RESULT") ;;
-isystem*) convert_to_token "${entry#-isystem}" include; package_include_dirs+=("$TOKEN_RESULT") ;;
*) fail "Unexpected include argument: $entry" ;;
esac
i=$((i + 1))
done
package_definitions=()
for entry in "${define_arguments[@]}"; do
case "$entry" in
# Ninja records a quoted define's value with its quotes backslash-escaped (verified
# directly: IMGUI_USER_CONFIG=\"aurora/imgui_config.h\" in build.ninja) regardless of
# platform - format_cmake_block below re-escapes for CMake's own string syntax, so the
# literal backslash has to come out here first or the result is double-escaped and the
# define expands to literal backslash-quote characters instead of a quoted string.
-D*) package_definitions+=("${entry#-D}") ;;
*) fail "Unexpected define argument: $entry" ;;
esac
done
package_definitions=("${package_definitions[@]//\\\"/\"}")
# fmt's `-include cstdlib` (and anything like it) is recorded from a C++ compile line, so re-scope
# it to C++. The runtime also assembles generated .S blobs through the same targets, and a forced
# C++ header include would break them.
package_compile_options=()
for entry in "${compile_options_raw[@]}"; do
package_compile_options+=("\$<\$<COMPILE_LANGUAGE:CXX>:$entry>")
done
package_link_items=()
for item in "${link_items[@]}"; do
case "$item" in
-*) package_link_items+=("$item"); continue ;;
esac
if [[ "$item" = /* ]]; then item_abs=$(normalize "$item"); else item_abs=$(normalize "$stage_dir/$item"); fi
ref=${linker_file_to_reference["$item_abs"]:-}
if [[ -n "$ref" ]]; then
package_link_items+=("$ref")
continue
fi
case "$item_abs" in
*/libz.so*)
# The one unavoidable system reference: libpng's own vendored CMakeLists.txt calls
# find_package(ZLIB REQUIRED) internally even when zlib came from aurora's own
# FetchContent (extern/CMakeLists.txt writes a redirect config for exactly this case),
# and CMAKE_DISABLE_FIND_PACKAGE_ZLIB errors out on any REQUIRED call site outright
# (verified directly), so it cannot be forced to vendor/static the way absl/PNG/Freetype
# are above. zlib is as close to a universal baseline as a Linux shared library gets
# (glibc-adjacent - practically every distro has it already), so this is recorded as a
# portable `-lz` link flag instead of the harvesting machine's absolute path.
package_link_items+=("-lz")
continue ;;
esac
fail "Link item is neither a system library nor a harvested archive: $item"
done
# ---------------------------------------------------------------------------
# Emit the consumer-facing CMake description.
# ---------------------------------------------------------------------------
format_cmake_block() {
# $1 = CMake variable name, remaining args = list items
local name=$1; shift
if [[ $# -eq 0 ]]; then
printf 'set(%s "")\n' "$name"
return
fi
printf 'set(%s\n' "$name"
local item escaped
for item in "$@"; do
escaped=${item//\\/\\\\}
escaped=${escaped//\"/\\\"}
printf ' "%s"\n' "$escaped"
done
printf ')\n'
}
dawn_config_dir_meta=$(get_meta dawn_config_dir)
dawn_config_token=""
if [[ -n "$dawn_config_dir_meta" ]]; then
dawn_config_dir_abs=$(normalize "$dawn_config_dir_meta")
case "$dawn_config_dir_abs" in
"$binary_root"/*)
# Dawn's find_package() config directory (lib/cmake/Dawn under its own fetched package
# root) cannot go through the generic single-directory copy other build-tree artifacts
# use: DawnConfig.cmake/DawnTargets.cmake derive _IMPORT_PREFIX/PACKAGE_PREFIX_DIR from
# their OWN file location by walking up three parent directories (verified directly), so
# the whole package root - include/, lib/, lib/cmake/Dawn/ together - must be copied as
# one self-contained unit for that relative navigation to still resolve once moved.
dawn_package_root=$(normalize "$dawn_config_dir_abs/../../..")
dawn_config_relative=${dawn_config_dir_abs#$dawn_package_root/}
mkdir -p "$output_dir/include/dawn_package"
cp -a "$dawn_package_root/." "$output_dir/include/dawn_package/"
dawn_config_token="@PKG@/include/dawn_package/$dawn_config_relative"
;;
*) convert_to_token "$dawn_config_dir_abs" include; dawn_config_token=$TOKEN_RESULT ;;
esac
fi
generated_cmake="$output_dir/native_prebuilt.cmake"
{
echo "# Generated by Launcher/Prepare-NativePrebuilt.sh - do not edit."
echo "#"
echo "# Describes the precompiled aurora + third-party archives that replace a"
echo "# from-source aurora-main build. Every path is a token resolved against the"
echo "# consuming workspace, because the source trees still ship next to this"
echo "# package and the runtime compiles against their headers."
echo ""
echo 'if(NOT MKW_NP_PACKAGE_DIR OR NOT MKW_NP_AURORA_DIR OR NOT MKW_NP_RUNTIME_DIR OR NOT MKW_NP_DEPS_DIR)'
echo ' message(FATAL_ERROR "native_prebuilt.cmake must be included by runtime/cmake/NativePrebuilt.cmake")'
echo 'endif()'
echo ""
format_cmake_block MKW_NP_INCLUDE_DIRECTORIES "${package_include_dirs[@]}"
format_cmake_block MKW_NP_COMPILE_DEFINITIONS "${package_definitions[@]}"
format_cmake_block MKW_NP_COMPILE_OPTIONS "${package_compile_options[@]}"
format_cmake_block MKW_NP_LINK_LIBRARIES "${package_link_items[@]}"
format_cmake_block MKW_NP_AURORA_TARGETS "aurora::gx" "aurora::pad" "aurora::si" "aurora::vi" "aurora::mtx"
echo ""
printf 'set(MKW_NP_DAWN_CONFIG_DIR "%s")\n' "$dawn_config_token"
printf 'set(MKW_NP_DAWN_VERSION "%s")\n' "$(get_meta aurora_dawn_version)"
echo ""
echo "# Shared third-party libraries keep imported SHARED targets so that"
echo '# $<TARGET_RUNTIME_DLLS> still copies them next to the game executable.'
for idx in "${!shared_import_targets[@]}"; do
printf 'add_library(%s SHARED IMPORTED GLOBAL)\n' "${shared_import_targets[$idx]}"
printf 'set_target_properties(%s PROPERTIES\n' "${shared_import_targets[$idx]}"
printf ' IMPORTED_LOCATION "${MKW_NP_PACKAGE_DIR}/%s"\n' "${shared_import_runtime[$idx]}"
printf ' IMPORTED_IMPLIB "${MKW_NP_PACKAGE_DIR}/%s")\n' "${shared_import_implib[$idx]}"
done
} > "$generated_cmake"
# ---------------------------------------------------------------------------
# Provenance: the pinned toolchain is what makes a precompiled archive interchangeable with
# locally compiled objects (bit-for-bit the compiler and flag set the user's machine will use);
# LocalBuild.ps1's Windows equivalent re-checks both before consuming a package, and a Linux
# consumer-side check (once local-build.sh gains --native-prebuilt-dir) should do the same.
# compiler_sha256/flag_fingerprint/aurora_fingerprint/third_party_fingerprint were already computed
# above (needed before the --print-fingerprint-only early exit).
# ---------------------------------------------------------------------------
dawn_runtime_sha256=""
[[ -n "$dawn_runtime_file" && -f "$dawn_runtime_file" ]] && dawn_runtime_sha256=$(sha256_of "$dawn_runtime_file")
[[ -n "$dawn_runtime_sha256" ]] || fail "The Dawn runtime could not be hashed: $dawn_runtime_file"
compiler_version=$(get_meta cxx_compiler_version)
sdl3_target=$(get_meta aurora_sdl3_target)
dawn_version=$(get_meta aurora_dawn_version)
harvested_count=${#linker_file_to_reference[@]}
python3 - "$output_dir" "$compiler_sha256" "$compiler_version" "$flag_fingerprint" \
"$dawn_version" "$dawn_runtime_sha256" "$aurora_fingerprint" "$third_party_fingerprint" \
"$sdl3_target" "$harvested_count" <<'PY'
import hashlib, json, os, sys, datetime
(output_dir, compiler_sha256, compiler_version, flag_fingerprint, dawn_version,
dawn_runtime_sha256, aurora_fingerprint, third_party_fingerprint, sdl3_target,
harvested_count) = sys.argv[1:]
contents = []
for root, dirs, files in os.walk(output_dir):
dirs.sort()
for name in sorted(files):
if name == "provenance.json":
continue
path = os.path.join(root, name)
rel = os.path.relpath(path, output_dir).replace(os.sep, "/")
with open(path, "rb") as fh:
digest = hashlib.sha256(fh.read()).hexdigest()
contents.append({"Path": rel, "Bytes": os.path.getsize(path), "Sha256": digest})
contents.sort(key=lambda c: c["Path"])
provenance = {
"SchemaVersion": 1,
"BuiltUtc": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
"CompilerSha256": compiler_sha256,
"CompilerVersion": compiler_version,
"FlagFingerprint": flag_fingerprint,
"DawnVersion": dawn_version,
"DawnRuntimeSha256": dawn_runtime_sha256,
"AuroraSourceFingerprint": aurora_fingerprint,
"ThirdPartySourceFingerprint": third_party_fingerprint,
"Sdl3Target": sdl3_target,
"HarvestedLibraryCount": int(harvested_count),
"Contents": contents,
}
with open(os.path.join(output_dir, "provenance.json"), "w", encoding="utf-8") as fh:
json.dump(provenance, fh, indent=2)
fh.write("\n")
total_bytes = sum(c["Bytes"] for c in contents)
print(f" files: {len(contents)}; size: {total_bytes / (1024 * 1024):.1f} MiB")
PY
if [[ "$keep_stage" -eq 0 ]]; then rm -rf "$stage_dir"; fi
echo ""
echo "Native prebuilt package: $output_dir"
echo " archives: $(find "$output_dir/lib" -maxdepth 1 -type f | wc -l); shared imports: ${#shared_import_targets[@]}"
echo " flag fingerprint: $flag_fingerprint"
-6
View File
@@ -1,6 +1,5 @@
[CmdletBinding(PositionalBinding = $false)]
param(
[string]$DolphinToolPath,
[string]$PortableToolsDirectory = 'Launcher/artifacts/portable-tools',
[string]$DependencySourceDirectory = 'Launcher/artifacts/dependencies',
[string]$VcRuntimeDirectory,
@@ -10,12 +9,7 @@ param(
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 3.0
if ([string]::IsNullOrWhiteSpace($DolphinToolPath)) {
throw 'Prepare-Release.ps1 requires -DolphinToolPath pointing to DolphinTool.exe.'
}
$arguments = @{
DolphinToolPath = $DolphinToolPath
PortableToolsDirectory = $PortableToolsDirectory
DependencySourceDirectory = $DependencySourceDirectory
ToolkitReleaseTag = $ToolkitReleaseTag
+1 -1
View File
@@ -18,7 +18,7 @@ $llvmReadobj = [System.IO.Path]::GetFullPath($LlvmReadobjPath)
$systemDlls = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
@(
'advapi32.dll', 'authz.dll', 'bcrypt.dll', 'combase.dll', 'comdlg32.dll', 'crypt32.dll',
'advapi32.dll', 'authz.dll', 'bcrypt.dll', 'bcryptprimitives.dll', 'combase.dll', 'comdlg32.dll', 'crypt32.dll',
'd3d11.dll', 'd3d12.dll', 'dbghelp.dll', 'dcomp.dll', 'dwrite.dll', 'dwmapi.dll',
'dxgi.dll', 'gdi32.dll', 'imm32.dll',
'iphlpapi.dll', 'kernel32.dll', 'mf.dll', 'mfplat.dll', 'mfreadwrite.dll', 'mfuuid.dll',
+7 -4
View File
@@ -15,7 +15,8 @@ if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) {
}
$repoRoot = [IO.Path]::GetFullPath($RepositoryRoot)
$launcher = Join-Path $repoRoot 'Launcher'
$setup = Join-Path $launcher 'WiiCompiled.Setup'
$setup = Join-Path $launcher 'WiiCompiled.Setup.Windows'
$common = Join-Path $launcher 'WiiCompiled.Setup.Common'
$failures = [Collections.Generic.List[string]]::new()
function Add-Failure([string]$Message) { $failures.Add($Message) }
@@ -48,9 +49,11 @@ function Compare-Set([string[]]$Expected, [string[]]$Actual, [string]$ExpectedNa
$pins = Get-MkwProjectPins (Join-Path $repoRoot 'projects\mkwii\recomp.yml')
# --- The Retro-WFC endpoint: recomp.yml owns it; the installer host pins the same string so a
# --- redirected or rewritten endpoint cannot be fetched from.
$inputValidation = Read-SourceFile (Join-Path $setup 'InputValidation.cs') 'InputValidation.cs'
$hostUri = Get-CapturedValue $inputValidation 'CurrentRetroWfcPayloadUri\s*=\s*"([^"]+)"' `
# --- redirected or rewritten endpoint cannot be fetched from. The literal lives in
# --- WiiCompiled.Setup.Common (shared with WiiCompiled.Setup.Linux) - InputValidation.cs only
# --- re-exports it as `= RetroWfcPayload.CurrentRetroWfcPayloadUri;`, no literal to capture there.
$retroWfcPayload = Read-SourceFile (Join-Path $common 'RetroWfcPayload.cs') 'RetroWfcPayload.cs'
$hostUri = Get-CapturedValue $retroWfcPayload 'CurrentRetroWfcPayloadUri\s*=\s*"([^"]+)"' `
'The host Retro-WFC endpoint constant'
if ($hostUri -cne $pins.RetroWfcPayloadUri) {
Add-Failure "InputValidation.CurrentRetroWfcPayloadUri is '$hostUri' but recomp.yml pins '$($pins.RetroWfcPayloadUri)'."
@@ -0,0 +1,28 @@
using WiiCompiled.Setup.Common;
// A packaging-time-only helper - never shipped, never run by an end user. Both
// Launcher/build-appimage.sh and Launcher/Build-Installer.ps1 invoke this to obtain the nodtool
// binary they bundle, so there is exactly one place (NodToolProvider) that knows the pinned
// version/URL/platform-asset mapping, instead of a separate copy per packaging script.
//
// Usage: WiiCompiled.Setup.Common.Cli --workspace <repo-root>
// Prints the resolved nodtool path to stdout.
string? workspace = null;
for (var i = 0; i < args.Length; i++)
{
if (args[i] == "--workspace" && i + 1 < args.Length)
{
workspace = args[++i];
}
}
if (workspace is null)
{
Console.Error.WriteLine("Usage: WiiCompiled.Setup.Common.Cli --workspace <repo-root>");
return 1;
}
var path = await NodToolProvider.ResolveAsync(workspace, CancellationToken.None);
Console.WriteLine(path);
return 0;
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
<Version>0.2.22</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description>
<DebugType>embedded</DebugType>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
</ItemGroup>
</Project>
@@ -1,14 +1,14 @@
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Common;
/// <summary>
/// One entry of an exact regular directory tree. Directory topology is part of the content
/// contract everywhere this walker is used: an empty directory can be a runtime-visible asset just
/// as a regular file can be, so it must not disappear from a content identity or a staged copy.
/// </summary>
internal sealed record RegularTreeEntry(string RelativePath, string FullPath, bool IsDirectory,
public sealed record RegularTreeEntry(string RelativePath, string FullPath, bool IsDirectory,
bool IsEmptyDirectory, long Length);
internal static class FileSystemUtilities
public static class FileSystemUtilities
{
public static void CopyDirectory(string source, string destination,
CancellationToken cancellationToken = default)
@@ -1,9 +1,9 @@
using System.Text.Json;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Common;
/// <summary>Reads and atomically writes the small JSON state documents kept inside an installation.</summary>
internal static class JsonState
/// <summary>Reads and atomically writes the small JSON state documents each installer keeps.</summary>
public static class JsonState
{
private static readonly JsonSerializerOptions ReadOptions = new() { PropertyNameCaseInsensitive = true };
private static readonly JsonSerializerOptions WriteOptions = new() { WriteIndented = true };
@@ -17,11 +17,17 @@ internal static class JsonState
catch
{
// A truncated or hand-edited state document must degrade into "unknown", which every
// caller already treats as "assume stale and rebuild", not into a failed launch.
// caller already treats as "assume stale and rebuild", not into a crash.
return null;
}
}
public static void Write<T>(string path, T value) =>
FileSystemUtilities.WriteAtomic(path, JsonSerializer.Serialize(value, WriteOptions));
public static void Write<T>(string path, T value)
{
var directory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(directory)) Directory.CreateDirectory(directory);
var tempPath = path + ".tmp-" + Guid.NewGuid().ToString("N");
File.WriteAllText(tempPath, JsonSerializer.Serialize(value, WriteOptions));
File.Move(tempPath, path, overwrite: true);
}
}
@@ -0,0 +1,38 @@
using System.Text.RegularExpressions;
namespace WiiCompiled.Setup.Common;
/// <summary>Disc metadata parsed from `nodtool info`'s stdout.</summary>
public sealed record NodToolDiscInfo(string GameId, string Title, int Revision);
/// <summary>
/// Parses the plain-text stdout of `nodtool info &lt;iso&gt;`. nodtool has no JSON output mode, but
/// prints one unconditional disc-level Title/Game ID/Disc-Revision block (via its own
/// `print_header`) before any per-partition breakdown - Wii discs also have differently-scoped
/// "Title"/"Game ID" lines per update/channel partition further down, so the first match of each
/// pattern is always the disc-level one both installers want.
/// </summary>
public static partial class NodToolInfoParser
{
public static NodToolDiscInfo Parse(string infoStdout)
{
var gameIdMatch = GameIdLine().Match(infoStdout);
if (!gameIdMatch.Success)
throw new InvalidOperationException("nodtool did not return disc metadata.");
var titleMatch = TitleLine().Match(infoStdout);
var revisionMatch = RevisionLine().Match(infoStdout);
return new NodToolDiscInfo(
GameId: gameIdMatch.Groups[1].Value,
Title: titleMatch.Success ? titleMatch.Groups[1].Value : "",
Revision: revisionMatch.Success ? int.Parse(revisionMatch.Groups[1].Value) : 0);
}
[GeneratedRegex(@"^Game ID: (\S+)", RegexOptions.Multiline)]
private static partial Regex GameIdLine();
[GeneratedRegex(@"^Title: (.+)$", RegexOptions.Multiline)]
private static partial Regex TitleLine();
[GeneratedRegex(@"^Disc \d+, Revision (\d+)", RegexOptions.Multiline)]
private static partial Regex RevisionLine();
}
@@ -0,0 +1,67 @@
using System.Runtime.InteropServices;
namespace WiiCompiled.Setup.Common;
/// <summary>
/// Resolves the `nodtool` binary both installers use for Wii disc validation/extraction (see
/// NodToolInfoParser.cs), replacing the earlier dependency on `dolphin-tool`/`DolphinTool.exe`. A
/// caller can supply one directly; otherwise this downloads the matching prebuilt release binary
/// from encounter/nod and caches it at Launcher/artifacts/nodtool[.exe].
///
/// Shared by: WiiCompiled.Setup.Linux/DiscTool.cs (falls back to this at end-user install time on
/// a plain git checkout), and WiiCompiled.Setup.Common.Cli (invoked once at packaging time by both
/// build-appimage.sh and Build-Installer.ps1 to acquire the copy each bundles).
/// </summary>
public static class NodToolProvider
{
public const string Version = "v2.0.0-alpha.10";
public static async Task<string> ResolveAsync(string workspace, CancellationToken cancellationToken)
{
var cacheName = OperatingSystem.IsWindows() ? "nodtool.exe" : "nodtool";
var cachePath = Path.Combine(workspace, "Launcher", "artifacts", cacheName);
if (File.Exists(cachePath)) return cachePath;
var url = $"https://github.com/encounter/nod/releases/download/{Version}/{AssetName()}";
Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!);
var tempPath = cachePath + ".tmp";
using (var http = new HttpClient())
using (var response = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
{
response.EnsureSuccessStatusCode();
await using var fileStream = File.Create(tempPath);
await response.Content.CopyToAsync(fileStream, cancellationToken);
}
File.Move(tempPath, cachePath, overwrite: true);
if (!OperatingSystem.IsWindows())
{
File.SetUnixFileMode(cachePath,
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
}
return cachePath;
}
private static string AssetName()
{
if (OperatingSystem.IsWindows())
{
return RuntimeInformation.OSArchitecture switch
{
Architecture.X64 => "nodtool-windows-x86_64.exe",
Architecture.Arm64 => "nodtool-windows-arm64.exe",
Architecture.X86 => "nodtool-windows-x86.exe",
var other => throw new PlatformNotSupportedException($"No prebuilt nodtool release for Windows {other}"),
};
}
return RuntimeInformation.OSArchitecture switch
{
Architecture.X64 => "nodtool-linux-x86_64",
Architecture.Arm64 => "nodtool-linux-aarch64",
Architecture.X86 => "nodtool-linux-i686",
var other => throw new PlatformNotSupportedException($"No prebuilt nodtool release for Linux {other}"),
};
}
}
@@ -1,4 +1,4 @@
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Common;
/// <summary>
/// A portable installation is a self-contained directory tree the user can move or carry on removable media:
@@ -9,8 +9,11 @@ namespace WiiCompiled.Setup;
/// </code>
/// The runtime finds the same root independently (<c>runtime/include/runtime_config.h</c>,
/// <c>PortableRootDirectory</c>); this class must keep the same marker name, layout, and depth bound.
/// Shared by both installers - Linux's CLI has no <c>--portable</c> flag, so it only ever calls
/// <see cref="TryFind"/>/<see cref="UserDataDirectory"/>/<see cref="Contains"/> (always missing,
/// since it never creates a marker file), not <see cref="Create"/>.
/// </summary>
internal static class PortableRoot
public static class PortableRoot
{
public const string MarkerFileName = "portable.txt";
public const string UserDataDirectoryName = "UserData";
@@ -72,7 +75,7 @@ internal static class PortableRoot
if (!File.Exists(marker))
{
File.WriteAllText(marker,
$"{ProductInfo.Name} portable installation." + Environment.NewLine +
"WiiCompiled portable installation." + Environment.NewLine +
"This marker makes the runtime keep Config.toml, NAND, Cache, and Logs in UserData\\ " +
"beside it instead of in %LOCALAPPDATA%." + Environment.NewLine +
"Delete it to make this installation use per-user application data again." +
@@ -90,54 +93,3 @@ internal static class PortableRoot
private static string Normalize(string path) => FileSystemUtilities.NormalizePath(path);
}
/// <summary>
/// A portable root can be moved or renamed between operations. Every installed-host operation that
/// reads <c>install-state.json</c> passes through here first so exactly one place decides what a
/// moved installation means, and so a non-portable installation is never touched.
/// </summary>
internal static class PortableInstallHealing
{
/// <summary>
/// Reconciles a moved portable installation with its recorded location: the state file adopts the
/// directory it was actually found in, and the native build tree is discarded because its
/// CMake cache holds absolute paths from the old location. Returns whether anything was healed.
/// </summary>
public static bool HealMovedInstall(Installation installation, IInstallReporter? reporter = null)
{
// Guard: an ordinary installation that disagrees with its state file is a real problem for
// the operation to report, not something to silently rewrite.
if (PortableRoot.TryFind(installation.Root) is null) return false;
var state = installation.ReadInstallState();
if (state is not { SchemaVersion: 1 } || string.IsNullOrWhiteSpace(state.InstallDir)) return false;
string recorded;
try
{
recorded = FileSystemUtilities.NormalizePath(state.InstallDir);
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException)
{
recorded = state.InstallDir;
}
if (recorded.Equals(installation.Root, StringComparison.OrdinalIgnoreCase)) return false;
var previous = state.InstallDir;
state.InstallDir = installation.Root;
JsonState.Write(installation.InstallStatePath, state);
// The configured native build directory bakes absolute source, toolchain, and output paths
// into CMakeCache.txt. After a move it is unusable and would fail the next configure rather
// than being reused, so it is removed and reconfigured from scratch on the next build.
var nativeBuild = Path.Combine(installation.WorkspaceDirectory, "native-build");
var hadNativeBuild = Directory.Exists(nativeBuild);
if (hadNativeBuild) FileSystemUtilities.DeleteDirectoryIfExists(nativeBuild);
reporter?.Diagnostic(
$"This portable installation moved from {previous} to {installation.Root}. " +
"The recorded location was updated" +
(hadNativeBuild ? " and the location-bound native build cache was discarded." : "."));
return true;
}
}
@@ -1,16 +1,17 @@
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Common;
/// <summary>
/// Resolves the one canonical Retro Rewind install. Wheel Wizard owns and passes it as
/// <c>--retro-dir</c>; the backend only resolves, reads, and records it, never packages or copies it.
/// <c>--retro-dir</c>; each installer only resolves, reads, and records it, never packages or
/// copies it.
/// </summary>
internal static class RetroRewindSource
public static class RetroRewindSource
{
/// <summary>
/// Resolves the <c>RetroRewind6</c> folder from a selection that may be the folder itself or a
/// parent containing exactly one <c>RetroRewind6/Binaries/Code.pul</c>.
/// </summary>
internal static string ResolveRetroRewind6(string selected)
public static string ResolveRetroRewind6(string selected)
{
if (string.IsNullOrWhiteSpace(selected))
throw new InvalidDataException("Choose the canonical Retro Rewind folder.");
@@ -1,20 +1,28 @@
using System.Diagnostics;
using System.Buffers.Binary;
using System.Net;
using System.Security.Cryptography;
using System.Text.Json;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Common;
internal static class InputValidation
/// <summary>
/// One validated, content-identified download in operation-owned scratch space. Callers use this
/// exact directory for both the update decision and any resulting build.
/// </summary>
public sealed record RetroWfcPayloadSnapshot(string Directory, string Sha256, long ByteLength);
/// <summary>
/// Downloads and verifies the Retro-WFC payload (a small, RSA-signed blob served from a single
/// fixed endpoint). Shared by both installers - moved here from WiiCompiled.Setup.Windows's
/// InputValidation.cs, which keeps every one of these method names as thin forwarding wrappers so
/// its many existing call sites (ProductRepairService.cs, LocalBuildService.cs, Installation.cs,
/// SelfTests.cs) needed no changes.
/// </summary>
public static class RetroWfcPayload
{
private const long MaximumRetroWfcPayloadBytes = 16L * 1024 * 1024;
// The payload is tens of kilobytes from a single fixed endpoint 30s is good.
private static readonly TimeSpan RetroWfcDownloadTimeout = TimeSpan.FromSeconds(30);
private static readonly TimeSpan RetroWfcRetryDelay = TimeSpan.FromSeconds(1);
private static readonly HashSet<string> SupportedDiscImageExtensions = new(
[".iso", ".gcm", ".gcz", ".ciso", ".wbfs", ".wia", ".rvz"],
StringComparer.OrdinalIgnoreCase);
public const string CurrentRetroWfcPayloadUri = "http://nas.play.rwfc.net/payload?g=RMCPD00";
private static readonly string RetroWfcOfflinePayloadFile =
@@ -37,43 +45,6 @@ internal static class InputValidation
private const int RetroWfcPayloadSignatureOffset = 0x10;
private const int RetroWfcPayloadMinimumBytes = 0x130;
public static void ValidateExtension(string gamePath)
{
if (!File.Exists(gamePath))
throw new FileNotFoundException("The selected game image does not exist.", gamePath);
var extension = Path.GetExtension(gamePath);
if (!SupportedDiscImageExtensions.Contains(extension))
throw new InvalidDataException(
"Select a complete Wii disc image in ISO, GCM, GCZ, CISO, WBFS, WIA, or RVZ format.");
}
public static async Task<DiscHeader> ReadDiscHeaderAsync(string dolphinTool, string gamePath,
CancellationToken cancellationToken = default)
{
ValidateExtension(gamePath);
var result = await ProcessRunner.RunAsync(dolphinTool,
["header", "-i", Path.GetFullPath(gamePath), "-j"], null, cancellationToken);
if (result.ExitCode != 0)
throw new InvalidDataException("DolphinTool could not read this disc image. " + result.CombinedOutput.Trim());
var json = result.StandardOutput.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
.FirstOrDefault(line => line.TrimStart().StartsWith('{'));
if (json is null)
throw new InvalidDataException("DolphinTool did not return disc metadata.");
return JsonSerializer.Deserialize<DiscHeader>(json)
?? throw new InvalidDataException("DolphinTool returned invalid disc metadata.");
}
public static void EnsureCompatibleDisc(DiscHeader header, PayloadManifest manifest)
{
if (!header.GameId.Equals(manifest.ExpectedGameId, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidDataException(
$"This build supports Mario Kart Wii PAL ({manifest.ExpectedGameId}). " +
$"The selected image is {header.GameId} ({header.InternalName}, {header.Region}).");
}
}
public static string ValidateStagedRetroWfcPayloadDirectory(string stagedDirectory,
RSAParameters? signingKey = null)
{
@@ -82,9 +53,22 @@ internal static class InputValidation
var root = Path.GetFullPath(stagedDirectory);
var payload = Path.Combine(root, RetroWfcOfflinePayloadFile);
if (!File.Exists(payload))
try
{
if ((File.GetAttributes(payload) & FileAttributes.Directory) != 0)
throw new InvalidDataException(
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
}
catch (FileNotFoundException)
{
throw new InvalidDataException(
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
}
catch (DirectoryNotFoundException)
{
throw new InvalidDataException(
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
}
ValidateRetroWfcPayloadFile(payload, signingKey);
return root;
}
@@ -186,7 +170,7 @@ internal static class InputValidation
}
}
internal static bool IsTransientRetroWfcDownloadFailure(Exception exception,
public static bool IsTransientRetroWfcDownloadFailure(Exception exception,
CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested) return false;
@@ -232,73 +216,9 @@ internal static class InputValidation
"The Retro-WFC payload is not signed by the pinned Retro-WFC signing key.");
}
public static string Sha256File(string path)
private static string Sha256File(string path)
{
using var stream = File.OpenRead(path);
return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
}
}
internal sealed record ProcessResult(int ExitCode, string StandardOutput, string StandardError)
{
public string CombinedOutput => StandardOutput + Environment.NewLine + StandardError;
}
internal static class ProcessRunner
{
/// <summary>Runs a redirected child process to completion. <paramref name="configure"/> sets up a
/// working directory or scrubbed environment; <paramref name="capture"/> is off for callers that only
/// forward output live, so a build's output isn't buffered in memory for nobody to read.</summary>
public static async Task<ProcessResult> RunAsync(string executable, IReadOnlyList<string> arguments,
Action<string>? output, CancellationToken cancellationToken,
Action<ProcessStartInfo>? configure = null, bool capture = true,
Action<Exception>? onTerminationFailure = null)
{
var info = new ProcessStartInfo
{
FileName = executable,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
foreach (var argument in arguments) info.ArgumentList.Add(argument);
configure?.Invoke(info);
using var process = new Process { StartInfo = info, EnableRaisingEvents = true };
var stdout = new List<string>();
var stderr = new List<string>();
process.OutputDataReceived += (_, e) => { if (e.Data is not null) { if (capture) stdout.Add(e.Data); output?.Invoke(e.Data); } };
process.ErrorDataReceived += (_, e) => { if (e.Data is not null) { if (capture) stderr.Add(e.Data); output?.Invoke(e.Data); } };
if (!process.Start()) throw new InvalidOperationException($"Could not start {executable}.");
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await WaitForExitAsync(process, cancellationToken, onTerminationFailure);
return new ProcessResult(process.ExitCode, string.Join(Environment.NewLine, stdout),
string.Join(Environment.NewLine, stderr));
}
public static async Task WaitForExitAsync(Process process, CancellationToken cancellationToken,
Action<Exception>? onTerminationFailure = null)
{
try
{
await process.WaitForExitAsync(cancellationToken);
}
catch (OperationCanceledException)
{
try
{
if (!process.HasExited) process.Kill(entireProcessTree: true);
}
catch (Exception ex)
{
onTerminationFailure?.Invoke(ex);
}
await process.WaitForExitAsync(CancellationToken.None);
process.WaitForExit();
throw;
}
process.WaitForExit();
}
}
@@ -1,16 +1,19 @@
using System.Globalization;
using System.Text;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Common;
internal sealed record RuntimeConfigSnapshot(bool Existed, byte[] Contents);
public sealed record RuntimeConfigSnapshot(bool Existed, byte[] Contents);
/// <summary>
/// Reads and writes the runtime's <c>Config.toml</c>. Every entry point takes the file it operates on
/// since its location depends on the installation (portable <c>UserData</c> vs. per-user app data);
/// callers obtain it once via <see cref="ResolveConfigPath"/>.
/// callers obtain it once via <see cref="ResolveConfigPath"/>. Shared by both installers - Linux
/// never creates a <see cref="PortableRoot.MarkerFileName"/> marker file, so
/// <see cref="ResolveConfigPath"/>/<see cref="FormatPathValue"/>'s portable-root lookups always miss
/// there and this degrades to the same plain per-user-app-data, always-absolute-path behavior a
/// non-portable Windows install already gets.
/// </summary>
internal static class RuntimeConfiguration
public static class RuntimeConfiguration
{
public const string ConfigFileName = "Config.toml";
@@ -51,7 +54,7 @@ internal static class RuntimeConfiguration
File.Delete(configPath);
}
internal static void SetDvdRoot(string configPath, string dvdRoot) =>
public static void SetDvdRoot(string configPath, string dvdRoot) =>
SetPath(configPath, "dvd_root", dvdRoot);
/// <summary>
@@ -59,21 +62,21 @@ internal static class RuntimeConfiguration
/// asset overlay by scanning this directory live at launch, so an asset-only Retro Rewind update
/// needs no backend work: the next launch simply reads the new files.
/// </summary>
internal static void SetRetroRewindRoot(string configPath, string retroRewindRoot) =>
public static void SetRetroRewindRoot(string configPath, string retroRewindRoot) =>
SetPath(configPath, "retro_rewind_root", retroRewindRoot);
/// <summary>The canonical Retro Rewind root, or null when no installation has recorded one.</summary>
public static string? GetRetroRewindRoot(string configPath) =>
GetResolvedPath(configPath, "retro_rewind_root");
internal static void RemoveRetroRewindRootIfOwned(string configPath, string retroRewindRoot) =>
public static void RemoveRetroRewindRootIfOwned(string configPath, string retroRewindRoot) =>
RemovePathIfOwned(configPath, "retro_rewind_root", retroRewindRoot);
internal static void RemoveDvdRootIfOwned(string configPath, string dvdRoot) =>
public static void RemoveDvdRootIfOwned(string configPath, string dvdRoot) =>
RemovePathIfOwned(configPath, "dvd_root", dvdRoot);
/// <summary>The raw stored text of a <c>[paths]</c> key, exactly as the file holds it.</summary>
internal static string? GetPath(string configPath, string key) =>
public static string? GetPath(string configPath, string key) =>
TryUnquoteToml(GetRawValue(configPath, "paths", key) ?? "", out var value) ? value : null;
/// <summary>
@@ -81,17 +84,17 @@ internal static class RuntimeConfiguration
/// <c>[paths]</c> value against the directory holding <c>Config.toml</c> (never the working
/// directory), so the host must resolve it the same way before comparing or reading it.
/// </summary>
internal static string? GetResolvedPath(string configPath, string key)
public static string? GetResolvedPath(string configPath, string key)
{
var stored = GetPath(configPath, key);
return string.IsNullOrWhiteSpace(stored) ? null : ResolveAgainstConfig(configPath, stored);
}
internal static string ConfigDirectory(string configPath) =>
public static string ConfigDirectory(string configPath) =>
Path.GetDirectoryName(Path.GetFullPath(configPath))
?? throw new InvalidOperationException($"{configPath} has no containing directory.");
internal static string ResolveAgainstConfig(string configPath, string value) =>
public static string ResolveAgainstConfig(string configPath, string value) =>
Path.GetFullPath(value, ConfigDirectory(configPath));
private static void SetPath(string configPath, string key, string value)
@@ -150,7 +153,7 @@ internal static class RuntimeConfiguration
}
/// <summary>The raw TOML literal stored for a key, or null when the section or key is absent.</summary>
internal static string? GetRawValue(string configPath, string section, string key)
public static string? GetRawValue(string configPath, string section, string key)
{
if (!File.Exists(configPath)) return null;
var header = $"[{section}]";
@@ -265,7 +268,7 @@ internal static class RuntimeConfiguration
private static string QuoteToml(string value) =>
"\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
internal static bool TryUnquoteToml(string value, out string result)
public static bool TryUnquoteToml(string value, out string result)
{
result = "";
if (value.Length < 2) return false;
@@ -292,5 +295,4 @@ internal static class RuntimeConfiguration
result = builder.ToString();
return true;
}
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
<Version>0.2.22</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description>
<DebugType>embedded</DebugType>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
</Project>
@@ -0,0 +1,111 @@
using System.Diagnostics;
namespace WiiCompiled.Setup.Linux;
/// <summary>
/// Invokes Launcher/local-build.sh and turns its stdout into progress reports. Replaces
/// LocalBuildService.cs's hardcoded Windows PowerShell 5.1 invocation - there is no PowerShell
/// dependency here at all, just bash.
/// </summary>
internal static class BuildRunner
{
public static async Task RunAsync(
string workspace, string profile, string outputDir, string? baseOutputDir,
string? retroDir, string? retroWfcOfflineDir, bool skipRetroWfcPayload,
bool forceCleanBuild, string? translatorBin, string? ccBin, string? cxxBin, string? fuseLd,
string? cmakeBin, string? ninjaBin, string? nativePrebuiltDir, IInstallReporter reporter,
CancellationToken cancellationToken)
{
var script = Path.Combine(workspace, "Launcher", "local-build.sh");
if (!File.Exists(script)) throw new FileNotFoundException("local-build.sh is missing", script);
var startInfo = new ProcessStartInfo("bash")
{
WorkingDirectory = workspace,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
};
startInfo.ArgumentList.Add(script);
startInfo.ArgumentList.Add("--profile"); startInfo.ArgumentList.Add(profile);
startInfo.ArgumentList.Add("--output-dir"); startInfo.ArgumentList.Add(outputDir);
if (!string.IsNullOrEmpty(baseOutputDir))
{
startInfo.ArgumentList.Add("--base-output-dir"); startInfo.ArgumentList.Add(baseOutputDir);
}
if (!string.IsNullOrEmpty(retroDir))
{
// Still forwarded to local-build.sh under its own internal name -
// --retro-rewind-package-dir - matching LocalBuild.ps1's own -RetroRewindPackageDirectory.
startInfo.ArgumentList.Add("--retro-rewind-package-dir"); startInfo.ArgumentList.Add(retroDir);
}
if (!string.IsNullOrEmpty(retroWfcOfflineDir))
{
startInfo.ArgumentList.Add("--retro-wfc-offline-dir"); startInfo.ArgumentList.Add(retroWfcOfflineDir);
}
if (skipRetroWfcPayload) startInfo.ArgumentList.Add("--skip-retro-wfc-payload");
if (forceCleanBuild) startInfo.ArgumentList.Add("--force-clean-build");
if (!string.IsNullOrEmpty(translatorBin))
{
startInfo.ArgumentList.Add("--translator-bin"); startInfo.ArgumentList.Add(translatorBin);
}
// Forwarded by AppRun so the AppImage's bundled clang/lld (see prepare-portable-clang.sh)
// is used instead of local-build.sh's own default of whatever clang is on $PATH.
if (!string.IsNullOrEmpty(ccBin))
{
startInfo.ArgumentList.Add("--cc"); startInfo.ArgumentList.Add(ccBin);
}
if (!string.IsNullOrEmpty(cxxBin))
{
startInfo.ArgumentList.Add("--cxx"); startInfo.ArgumentList.Add(cxxBin);
}
if (!string.IsNullOrEmpty(fuseLd))
{
startInfo.ArgumentList.Add("--fuse-ld"); startInfo.ArgumentList.Add(fuseLd);
}
if (!string.IsNullOrEmpty(cmakeBin))
{
startInfo.ArgumentList.Add("--cmake"); startInfo.ArgumentList.Add(cmakeBin);
}
if (!string.IsNullOrEmpty(ninjaBin))
{
startInfo.ArgumentList.Add("--ninja"); startInfo.ArgumentList.Add(ninjaBin);
}
// Forwarded by AppRun so the AppImage's bundled precompiled aurora/third-party package (see
// Prepare-NativePrebuilt.sh) is used instead of local-build.sh compiling aurora-main itself.
if (!string.IsNullOrEmpty(nativePrebuiltDir))
{
startInfo.ArgumentList.Add("--native-prebuilt-dir"); startInfo.ArgumentList.Add(nativePrebuiltDir);
}
using var process = new Process { StartInfo = startInfo };
var window = new BuildProgressWindow(reporter, InstallStages.Build, start: 6, end: 96);
process.OutputDataReceived += (_, e) => { if (e.Data is not null) window.Observe(e.Data); };
process.ErrorDataReceived += (_, e) => { if (e.Data is not null) reporter.Diagnostic(e.Data); };
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
try
{
await process.WaitForExitAsync(cancellationToken);
}
catch (OperationCanceledException)
{
KillProcessTree(process);
throw;
}
if (process.ExitCode != 0)
{
throw new InvalidOperationException($"local-build.sh failed (exit {process.ExitCode}). See diagnostics above.");
}
}
private static void KillProcessTree(Process process)
{
try { process.Kill(entireProcessTree: true); } catch { /* best-effort */ }
}
}
@@ -0,0 +1,40 @@
namespace WiiCompiled.Setup.Linux;
/// <summary>
/// freedesktop.org .desktop application-menu entries. Replaces ShellIntegration.cs's registry
/// uninstall entry (no Linux analogue for an unpackaged tool - Windows already skips that step for
/// portable installs, this just applies that same behavior universally) and .lnk shortcuts.
/// </summary>
internal static class DesktopEntry
{
private static string ApplicationsDirectory =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "applications");
private static string PathFor(string profile) =>
Path.Combine(ApplicationsDirectory, $"wiicompiled-{profile}.desktop");
public static void Create(string profile, string displayName, string exePath)
{
// exePath is the installed native runtime binary itself (e.g.
// .../Install/Base/WiiCompiled) - each profile already gets its own .desktop file here,
// so there is no need to route through the setup tool's own launch-base/launch-retro
// subcommand dispatch first. Unquoted: the Desktop Entry spec's Exec grammar doesn't take
// a bare '"'-wrapped path, and none is needed here anyway - the only part of this path
// that varies is the username, which Unix forbids containing whitespace.
Directory.CreateDirectory(ApplicationsDirectory);
var contents =
"[Desktop Entry]\n" +
"Type=Application\n" +
$"Name={displayName}\n" +
$"Exec={exePath}\n" +
"Categories=Game;\n" +
"Terminal=false\n";
File.WriteAllText(PathFor(profile), contents);
}
public static void Remove(string profile)
{
var path = PathFor(profile);
if (File.Exists(path)) File.Delete(path);
}
}
@@ -0,0 +1,116 @@
using System.Security.Cryptography;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Linux;
/// <summary>
/// Validates and extracts the user's own Mario Kart Wii disc via `nodtool` (see
/// WiiCompiled.Setup.Common/NodToolProvider.cs) - a prebuilt, MIT/Apache-2.0-licensed CLI from
/// encounter/nod, replacing the earlier dependency on a system-installed `dolphin-tool`
/// (GPL-2.0-or-later, and not reliably packaged standalone by every distro).
/// </summary>
internal static class DiscTool
{
public static async Task ValidateAndExtractAsync(
string isoPath, ProjectManifest manifest, string assetsDirectory, string workspace,
string? nodToolBin, IInstallReporter reporter, CancellationToken cancellationToken)
{
var nodTool = nodToolBin ?? await NodToolProvider.ResolveAsync(workspace, cancellationToken);
// `nodtool info` only decodes the disc/partition headers (milliseconds); `nodtool extract`
// copies the whole data partition to disk (tens of seconds for a custom-track-heavy MKWii
// ISO). Checking the game ID first, before extracting, means a wrong disc fails fast -
// matching the original dolphin-tool `header` step this replaces.
reporter.Progress(InstallStages.ExtractDisc, "Reading the disc header", 2);
var info = NodToolInfoParser.Parse(await RunInfoAsync(nodTool, isoPath, cancellationToken));
if (!string.Equals(info.GameId, manifest.GameId, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"This disc is '{info.GameId}', not the expected '{manifest.GameId}' (Mario Kart Wii, region {manifest.Region}). " +
"Only your own legally-owned copy of that exact game/region can be used.");
}
// Extracted straight into Assets/DATA (kept, not a scratch dir) - the runtime reads course/
// texture/audio data from this directory live via [paths] dvd_root, not just at translation
// time, so it has to survive past this install (see Program.cs, which points dvd_root here).
reporter.Progress(InstallStages.ExtractDisc, "Extracting the disc image", 4);
var dataDir = Path.Combine(assetsDirectory, "DATA");
if (Directory.Exists(dataDir)) Directory.Delete(dataDir, recursive: true);
await RunExtractAsync(nodTool, isoPath, dataDir, cancellationToken);
var dolPath = Path.Combine(dataDir, "sys", "main.dol");
var relPath = Path.Combine(dataDir, "files", "rel", "StaticR.rel");
if (!File.Exists(dolPath)) throw new FileNotFoundException("nodtool did not produce main.dol", dolPath);
if (!File.Exists(relPath)) throw new FileNotFoundException("nodtool did not produce StaticR.rel", relPath);
var dolSha = Sha256Of(dolPath);
var relSha = Sha256Of(relPath);
if (!string.Equals(dolSha, manifest.DolSha256, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"main.dol sha256 mismatch: expected {manifest.DolSha256}, got {dolSha}. " +
"This disc revision does not match what this project's manifest is pinned to.");
}
if (!string.Equals(relSha, manifest.RelSha256, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"StaticR.rel sha256 mismatch: expected {manifest.RelSha256}, got {relSha}. " +
"This disc revision does not match what this project's manifest is pinned to.");
}
Directory.CreateDirectory(assetsDirectory);
File.Copy(dolPath, Path.Combine(assetsDirectory, "main.dol"), overwrite: true);
File.Copy(relPath, Path.Combine(assetsDirectory, "StaticR.rel"), overwrite: true);
reporter.Progress(InstallStages.ExtractDisc, "Disc validated and extracted", 6);
}
private static async Task<string> RunInfoAsync(string nodTool, string isoPath, CancellationToken cancellationToken)
{
var startInfo = new System.Diagnostics.ProcessStartInfo(nodTool)
{
ArgumentList = { "info", isoPath },
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
};
using var process = System.Diagnostics.Process.Start(startInfo)
?? throw new InvalidOperationException($"Failed to start {nodTool}.");
var stdout = await process.StandardOutput.ReadToEndAsync(cancellationToken);
var stderr = await process.StandardError.ReadToEndAsync(cancellationToken);
await process.WaitForExitAsync(cancellationToken);
if (process.ExitCode != 0)
{
throw new InvalidOperationException(
$"nodtool could not read this disc image (exit {process.ExitCode}): {stderr}{stdout}".Trim());
}
return stdout;
}
private static string Sha256Of(string path)
{
using var stream = File.OpenRead(path);
return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
}
private static async Task RunExtractAsync(string nodTool, string isoPath, string outDir, CancellationToken cancellationToken)
{
var startInfo = new System.Diagnostics.ProcessStartInfo(nodTool)
{
ArgumentList = { "extract", isoPath, outDir, "-q" },
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
};
using var process = System.Diagnostics.Process.Start(startInfo)
?? throw new InvalidOperationException($"Failed to start {nodTool}.");
var stdout = await process.StandardOutput.ReadToEndAsync(cancellationToken);
var stderr = await process.StandardError.ReadToEndAsync(cancellationToken);
await process.WaitForExitAsync(cancellationToken);
if (process.ExitCode != 0)
{
throw new InvalidOperationException(
$"nodtool extract {isoPath} failed (exit {process.ExitCode}): {stderr}{stdout}");
}
}
}
@@ -0,0 +1,209 @@
using System.Text.Json;
namespace WiiCompiled.Setup.Linux;
// Ported near-verbatim from Launcher/WiiCompiled.Setup/InstallProgress.cs: this whole file is
// platform-neutral (System.Text.Json + Console only), so the NDJSON --progress-json wire protocol
// stays byte-for-byte the same shape a future GUI already speaks on Windows.
/// <summary>
/// Stable stage identifiers reported by <c>--progress-json</c>. Kept intentionally small for this
/// lean Linux installer (no toolkit-extraction/publish-transaction stages, since there is no
/// bundled toolkit or staged workspace copy here - see the plan's "operate on a git checkout"
/// scoping decision).
/// </summary>
internal static class InstallStages
{
public const string Validate = "validate";
public const string ExtractDisc = "extract-disc";
public const string Build = "build";
public const string Shortcuts = "shortcuts";
}
/// <summary>
/// Where an installation reports what it is doing. Progress is coarse and monotonic; raw translator
/// and compiler output is a diagnostic, never progress, because it is unbounded and machine-hostile.
/// </summary>
internal interface IInstallReporter
{
void Progress(string stage, string message, int percent);
void Diagnostic(string line);
}
/// <summary>
/// The <c>--progress-json</c> protocol: one JSON object per line on stdout, nothing else on stdout,
/// diagnostics on stderr. The terminal <c>result</c> line is written exactly once.
/// </summary>
internal sealed class NdjsonInstallReporter : IInstallReporter
{
private static readonly JsonSerializerOptions Options = new() { WriteIndented = false };
private readonly object _gate = new();
private int _lastPercent;
private bool _finished;
public void Progress(string stage, string message, int percent)
{
lock (_gate)
{
if (_finished) return;
// Percentages are clamped monotonic: a caller's progress bar must never walk backwards
// because a later stage happened to estimate a lower number.
_lastPercent = Math.Clamp(Math.Max(percent, _lastPercent), 0, 99);
WriteLine(new { type = "progress", stage, message, percent = _lastPercent });
}
}
public void Diagnostic(string line) => Console.Error.WriteLine(line);
public void Success(string installDirectory)
{
lock (_gate)
{
if (_finished) return;
_finished = true;
WriteLine(new { type = "result", success = true, version = ProductInfo.Version, installDir = installDirectory });
}
}
public void Failure(string error)
{
lock (_gate)
{
if (_finished) return;
_finished = true;
WriteLine(new { type = "result", success = false, error });
}
}
/// <summary>
/// The terminal result line is the caller's only completion signal, so no exit path may skip it.
/// Callers invoke this from a finally block; it is a no-op once a result was already written.
/// </summary>
public void EnsureFinished(string errorIfUnfinished) => Failure(errorIfUnfinished);
private static void WriteLine(object value)
{
Console.Out.WriteLine(JsonSerializer.Serialize(value, Options));
Console.Out.Flush();
}
}
/// <summary>Plain-text console reporting for a run without <c>--progress-json</c>.</summary>
internal sealed class ConsoleInstallReporter : IInstallReporter
{
public void Progress(string stage, string message, int percent) =>
Console.Out.WriteLine($"[{percent,3}%] {message}");
public void Diagnostic(string line) => Console.Out.WriteLine(line);
}
/// <summary>
/// Build step identifiers from local-build.sh's <c>MKWCBUILD:STEP:&lt;id&gt;</c> lines - the id is
/// the contract, matched against Launcher/local-build.sh's log_step() call sites.
/// </summary>
internal static class BuildStepIds
{
public const string BuildTranslator = "build-translator";
public const string ReuseBaseTranslation = "reuse-base-translation";
public const string RetranslateBase = "retranslate-base";
public const string TranslateBase = "translate-base";
public const string EmitBaseManifest = "emit-base-manifest";
public const string TranslateMod = "translate-mod";
public const string GenerateDataInit = "generate-data-init";
public const string EmitBuildShards = "emit-build-shards";
public const string ConfigureNative = "configure-native";
public const string Compile = "compile";
}
/// <summary>
/// Maps one local-build.sh run onto a slice of the overall percentage. local-build.sh announces
/// every step it starts with an <c>MKWCBUILD:</c> prefix, so the slice can advance on real events
/// instead of on a timer.
/// </summary>
internal sealed class BuildProgressWindow
{
private const string Marker = "MKWCBUILD:";
private const string StepMarker = "STEP:";
/// <summary>The fraction the compile step reaches; beyond it, compiler output is a heartbeat.</summary>
private const double CompileFraction = 0.58;
private static readonly (string Id, double Fraction, string Message)[] Steps =
[
(BuildStepIds.BuildTranslator, 0.04, "Building the translator"),
(BuildStepIds.ReuseBaseTranslation, 0.30, "Reusing the completed base translation"),
(BuildStepIds.RetranslateBase, 0.08, "The base translation is stale; retranslating it"),
(BuildStepIds.TranslateBase, 0.10, "Translating Mario Kart Wii"),
(BuildStepIds.EmitBaseManifest, 0.34, "Creating the translation manifest"),
(BuildStepIds.TranslateMod, 0.38, "Translating the Retro Rewind Code.pul"),
(BuildStepIds.GenerateDataInit, 0.44, "Generating game data initialization"),
(BuildStepIds.EmitBuildShards, 0.48, "Preparing the native build"),
(BuildStepIds.ConfigureNative, 0.52, "Configuring the compiler"),
(BuildStepIds.Compile, CompileFraction, "Compiling the game. This is the longest step"),
];
private readonly IInstallReporter _reporter;
private readonly string _stage;
private readonly int _start;
private readonly int _end;
private double _fraction;
private string _message = "Preparing the local build";
private int _reportedPercent = -1;
public BuildProgressWindow(IInstallReporter reporter, string stage, int start, int end)
{
_reporter = reporter;
_stage = stage;
_start = start;
_end = end;
}
public void Observe(string line)
{
var index = line.IndexOf(Marker, StringComparison.Ordinal);
if (index >= 0)
{
var text = line[(index + Marker.Length)..].Trim();
if (text.StartsWith(StepMarker, StringComparison.Ordinal))
{
var identifier = text[StepMarker.Length..];
var end = identifier.IndexOf(' ');
if (end >= 0) identifier = identifier[..end];
foreach (var (id, fraction, message) in Steps)
{
if (!id.Equals(identifier, StringComparison.Ordinal)) continue;
if (fraction > _fraction)
{
_fraction = fraction;
_message = message;
Emit();
}
return;
}
}
}
// Anything else - a plain MKWCBUILD note, or raw tool output - stays a diagnostic and only
// feeds the heartbeat below.
_reporter.Diagnostic(line);
// Compilation announces itself once and then emits thousands of compiler lines. Treat that
// output as a heartbeat so the slice keeps creeping forward, but only publish a progress
// line when the rounded percentage actually changes.
if (_fraction >= CompileFraction)
{
_fraction = Math.Min(0.97, _fraction + 0.0015);
Emit();
}
}
private void Emit()
{
var percent = Interpolate(_fraction);
if (percent == _reportedPercent) return;
_reportedPercent = percent;
_reporter.Progress(_stage, _message, percent);
}
private int Interpolate(double fraction) =>
(int)Math.Round(_start + (_end - _start) * Math.Clamp(fraction, 0, 1));
}
@@ -0,0 +1,31 @@
namespace WiiCompiled.Setup.Linux;
internal static class ProductInfo
{
public const string Name = "WiiCompiled";
public const string Version = "0.2.22";
}
/// <summary>One installed product's record inside install-state.json.</summary>
internal sealed class ProductInstallRecord
{
public string Profile { get; set; } = "";
public string InstallDirectory { get; set; } = "";
public string ExecutableName { get; set; } = "";
public string DolSha256 { get; set; } = "";
public string RelSha256 { get; set; } = "";
public string BuiltUtc { get; set; } = "";
}
/// <summary>
/// The whole flat state document this tool keeps at ~/.local/share/WiiCompiled/install-state.json.
/// Deliberately not a fingerprint tree: local-build.sh already does its own incremental-rebuild
/// caching, so this only needs to remember where things were installed and what they were built
/// against, not decide when to rebuild.
/// </summary>
internal sealed class InstallState
{
public int SchemaVersion { get; set; } = 1;
public string Workspace { get; set; } = "";
public List<ProductInstallRecord> Products { get; set; } = new();
}
+335
View File
@@ -0,0 +1,335 @@
using System.Security.Cryptography;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Linux;
internal static class Program
{
private static async Task<int> Main(string[] args)
{
// Checked anywhere in argv, not just args[0]: AppRun (Launcher/build-appimage.sh) prepends
// --workspace <cache> ahead of whatever the caller passed, so these can't assume position 0.
if (args.Length == 0 || args.Contains("-h") || args.Contains("--help")) { PrintUsage(); return 0; }
if (args.Contains("--version")) { Console.WriteLine(ProductInfo.Version); return 0; }
using var cts = new CancellationTokenSource();
// Replaces CancellationSignal.cs's named-EventWaitHandle IPC (Windows-only): SIGINT/SIGTERM
// are the portable, standard way for a parent (Wheel Wizard or a shell) to cancel this
// process and the build it spawned.
using var sigint = System.Runtime.InteropServices.PosixSignalRegistration.Create(
System.Runtime.InteropServices.PosixSignal.SIGINT, context => { context.Cancel = true; cts.Cancel(); });
using var sigterm = System.Runtime.InteropServices.PosixSignalRegistration.Create(
System.Runtime.InteropServices.PosixSignal.SIGTERM, context => { context.Cancel = true; cts.Cancel(); });
return await RunAsync(args, cts);
}
private static async Task<int> RunAsync(string[] args, CancellationTokenSource cts)
{
// AppRun (Launcher/build-appimage.sh) invokes this as `wiicompiled-setup --workspace
// <cache> <command> [options]` - a global flag ahead of the subcommand - so the command
// word is whichever token isn't part of a --flag/value pair, not strictly args[0].
var (command, flags) = ParseArgs(args);
if (command is null) { PrintUsage(); return 1; }
var progressJson = flags.ContainsKey("progress-json");
IInstallReporter reporter = progressJson ? new NdjsonInstallReporter() : new ConsoleInstallReporter();
try
{
switch (command)
{
case "install":
await InstallAsync(flags, reporter, cts.Token);
break;
case "uninstall":
Uninstall();
break;
case "launch-base":
return Launch("base", flags);
case "launch-retro":
return Launch("retro-rewind", flags);
case "check-products":
CheckProducts();
break;
default:
Console.Error.WriteLine($"Unknown command: {command}");
PrintUsage();
return 1;
}
(reporter as NdjsonInstallReporter)?.Success(flags.GetValueOrDefault("install-dir") ?? "");
return 0;
}
catch (OperationCanceledException)
{
Console.Error.WriteLine("Cancelled.");
(reporter as NdjsonInstallReporter)?.Failure("cancelled");
return 130;
}
catch (Exception ex)
{
Console.Error.WriteLine($"error: {ex.Message}");
(reporter as NdjsonInstallReporter)?.Failure(ex.Message);
return 1;
}
}
private static async Task InstallAsync(Dictionary<string, string?> flags, IInstallReporter reporter, CancellationToken token)
{
var retroDir = flags.GetValueOrDefault("retro-dir");
var installsRetro = !string.IsNullOrEmpty(retroDir);
var downloadPayload = flags.ContainsKey("download-retro-wfc-payload");
var skipPayload = flags.ContainsKey("skip-retro-wfc-payload");
if (installsRetro)
{
if (downloadPayload == skipPayload)
throw new ArgumentException(
"Choose exactly one Retro-WFC mode: --download-retro-wfc-payload or --skip-retro-wfc-payload.");
}
else if (downloadPayload || skipPayload)
{
throw new ArgumentException("A Retro-WFC payload option is valid only with --retro-dir.");
}
// Canonicalizes to the exact RetroRewind6 folder (accepting a parent folder or a symlink),
// the same validation Windows applies via this same shared method - local-build.sh's own
// check further down is a simpler backstop, not the primary validation anymore.
if (installsRetro) retroDir = RetroRewindSource.ResolveRetroRewind6(retroDir!);
var workspace = flags.GetValueOrDefault("workspace") ?? WorkspaceLocator.FindFrom(AppContext.BaseDirectory);
var manifest = ProjectManifest.Load(Path.Combine(workspace, "projects", "mkwii", "recomp.yml"));
var assetsDir = Path.Combine(workspace, "Assets");
reporter.Progress(InstallStages.Validate, "Checking prerequisites", 1);
if (flags.TryGetValue("game", out var isoPath) && !string.IsNullOrEmpty(isoPath))
{
await DiscTool.ValidateAndExtractAsync(isoPath, manifest, assetsDir, workspace,
flags.GetValueOrDefault("disc-tool-bin"), reporter, token);
}
else
{
var dol = Path.Combine(assetsDir, "main.dol");
var rel = Path.Combine(assetsDir, "StaticR.rel");
if (!File.Exists(dol) || !File.Exists(rel))
{
throw new InvalidOperationException(
"No --game ISO was given and Assets/main.dol + Assets/StaticR.rel are not already present. " +
"Either pass --game <path-to-iso>, or extract them yourself first (see translator/README.md).");
}
}
var state = JsonState.TryRead<InstallState>(StatePath) ?? new InstallState { Workspace = workspace };
state.Workspace = workspace;
var profile = installsRetro ? "both" : "base";
var profiles = installsRetro ? new[] { "base", "retro-rewind" } : new[] { "base" };
var baseInstallDir = installsRetro ? DefaultInstallDir("base") : null;
var installDir = flags.GetValueOrDefault("install-dir") ?? DefaultInstallDir(installsRetro ? "retro-rewind" : "base");
string? retroWfcOfflineDir = null;
if (downloadPayload)
{
// Reused if a previous install already downloaded and it's still valid - matches
// Windows's own reuse-if-valid behavior instead of re-downloading on every install.
var cacheDir = Path.Combine(workspace, "generated", "retro-wfc-payload");
reporter.Progress(InstallStages.Validate, "Preparing the Retro-WFC payload", 1);
try
{
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(cacheDir);
}
catch (InvalidDataException)
{
await RetroWfcPayload.DownloadRetroWfcPayloadAsync(
RetroWfcPayload.CurrentRetroWfcPayloadUri, cacheDir, token);
}
retroWfcOfflineDir = cacheDir;
}
await BuildRunner.RunAsync(
workspace, profile, installDir, baseInstallDir,
retroDir,
retroWfcOfflineDir,
skipPayload,
flags.ContainsKey("force-clean-build"),
flags.GetValueOrDefault("translator-bin"),
flags.GetValueOrDefault("cc"),
flags.GetValueOrDefault("cxx"),
flags.GetValueOrDefault("fuse-ld"),
flags.GetValueOrDefault("cmake"),
flags.GetValueOrDefault("ninja"),
flags.GetValueOrDefault("native-prebuilt-dir"),
reporter, token);
reporter.Progress(InstallStages.Shortcuts, "Creating shortcuts", 98);
var dolSha = Sha256Of(Path.Combine(assetsDir, "main.dol"));
var relSha = Sha256Of(Path.Combine(assetsDir, "StaticR.rel"));
foreach (var p in profiles)
{
var dir = p == "base" ? (baseInstallDir ?? installDir) : installDir;
var exeName = p == "base" ? "WiiCompiled" : "RetroRewind";
var displayName = p == "base" ? "WiiCompiled (base game)" : "WiiCompiled (Retro Rewind)";
state.Products.RemoveAll(r => r.Profile == p);
state.Products.Add(new ProductInstallRecord
{
Profile = p,
InstallDirectory = dir,
ExecutableName = exeName,
DolSha256 = dolSha,
RelSha256 = relSha,
BuiltUtc = DateTime.UtcNow.ToString("O"),
});
DesktopEntry.Create(p, displayName, Path.Combine(dir, exeName));
}
JsonState.Write(StatePath, state);
// The runtime reads course/texture/audio data live from dvd_root at every launch, not just
// at translation time - without this the game fatally errors the instant it needs any file
// that isn't main.dol/StaticR.rel. Linux has no --portable flag, so this is always the
// per-user Config.toml (RuntimeConfiguration.ResolveConfigPath's Windows-only portable-root
// lookup has nothing to find here either way).
var configPath = RuntimeConfiguration.ApplicationDataConfigPath;
var dataDir = Path.Combine(assetsDir, "DATA");
if (Directory.Exists(dataDir))
{
RuntimeConfiguration.SetDvdRoot(configPath, dataDir);
}
if (installsRetro)
{
RuntimeConfiguration.SetRetroRewindRoot(configPath, retroDir!);
}
reporter.Progress(InstallStages.Shortcuts, "Install complete", 99);
}
private static void Uninstall()
{
// Matches Windows: UninstallService.cs removes the whole install directory unconditionally -
// there is no partial-product uninstall on either platform.
var state = JsonState.TryRead<InstallState>(StatePath) ?? new InstallState();
foreach (var record in state.Products.ToList())
{
if (Directory.Exists(record.InstallDirectory))
{
Directory.Delete(record.InstallDirectory, recursive: true);
}
DesktopEntry.Remove(record.Profile);
state.Products.Remove(record);
Console.WriteLine($"Removed {record.Profile} from {record.InstallDirectory}");
}
JsonState.Write(StatePath, state);
}
private static int Launch(string profile, Dictionary<string, string?> flags)
{
var state = JsonState.TryRead<InstallState>(StatePath);
var record = state?.Products.FirstOrDefault(r => r.Profile == profile);
if (record is null)
{
var installHint = profile == "retro-rewind"
? "install --retro-dir <RetroRewind6> {--download-retro-wfc-payload | --skip-retro-wfc-payload}"
: $"install --profile {profile}";
Console.Error.WriteLine($"{profile} is not installed. Run '{installHint}' first.");
return 1;
}
var exePath = Path.Combine(record.InstallDirectory, record.ExecutableName);
if (!File.Exists(exePath))
{
Console.Error.WriteLine($"Installed executable is missing: {exePath}. Run 'install --profile {profile}' again.");
return 1;
}
var startInfo = new System.Diagnostics.ProcessStartInfo(exePath)
{
WorkingDirectory = record.InstallDirectory,
UseShellExecute = false,
};
using var process = System.Diagnostics.Process.Start(startInfo);
process?.WaitForExit();
return process?.ExitCode ?? 1;
}
private static void CheckProducts()
{
var state = JsonState.TryRead<InstallState>(StatePath);
if (state is null || state.Products.Count == 0)
{
Console.WriteLine("Nothing installed.");
return;
}
var assetsDir = Path.Combine(state.Workspace, "Assets");
var currentDol = Sha256IfExists(Path.Combine(assetsDir, "main.dol"));
var currentRel = Sha256IfExists(Path.Combine(assetsDir, "StaticR.rel"));
foreach (var record in state.Products)
{
var exePath = Path.Combine(record.InstallDirectory, record.ExecutableName);
var present = File.Exists(exePath);
var stale = present && (currentDol != record.DolSha256 || currentRel != record.RelSha256);
var status = !present ? "MISSING" : stale ? "STALE (game assets changed since last build)" : "current";
Console.WriteLine($"{record.Profile,-14} {status,-45} {record.InstallDirectory}");
}
}
private static string Sha256Of(string path)
{
using var stream = File.OpenRead(path);
return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
}
private static string? Sha256IfExists(string path) => File.Exists(path) ? Sha256Of(path) : null;
private static string StatePath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WiiCompiled", "install-state.json");
private static string DefaultInstallDir(string profile) => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WiiCompiled", "Install",
profile == "base" ? "Base" : "RetroRewind");
/// <summary>
/// A single pass that finds both the command word and every --flag[=value] pair, regardless
/// of order - a --flag may appear before or after the command (see the AppRun caller note in
/// RunAsync). The first token that is neither a --flag nor a value already consumed by the
/// preceding --flag is taken as the command.
/// </summary>
private static (string? Command, Dictionary<string, string?> Flags) ParseArgs(string[] args)
{
string? command = null;
var flags = new Dictionary<string, string?>();
for (var i = 0; i < args.Length; i++)
{
var arg = args[i];
if (arg.StartsWith("--", StringComparison.Ordinal))
{
var name = arg[2..];
if (i + 1 < args.Length && !args[i + 1].StartsWith("--", StringComparison.Ordinal))
{
flags[name] = args[++i];
}
else
{
flags[name] = null; // boolean flag
}
}
else if (command is null)
{
command = arg;
}
}
return (command, flags);
}
private static void PrintUsage()
{
Console.WriteLine("""
Usage: wiicompiled-setup <command> [options]
install [--game ISO_PATH] [--install-dir DIR] [--retro-dir DIR
{--download-retro-wfc-payload | --skip-retro-wfc-payload}]
[--force-clean-build] [--translator-bin PATH] [--disc-tool-bin PATH]
[--cc PATH] [--cxx PATH] [--fuse-ld NAME_OR_PATH] [--cmake PATH] [--ninja PATH]
[--native-prebuilt-dir DIR] [--progress-json] [--workspace DIR]
uninstall
launch-base
launch-retro
check-products
--version
""");
}
}
@@ -0,0 +1,70 @@
using System.Text.RegularExpressions;
namespace WiiCompiled.Setup.Linux;
/// <summary>
/// The handful of facts this tool needs out of projects/mkwii/recomp.yml. Parsed literally line by
/// line - the same approach Launcher/NativeBuildFlags.ps1's Get-MkwProjectPins and
/// Launcher/local-build.sh already use - rather than pulling in a YAML library, since the manifest
/// is machine-written with a fixed shape.
/// </summary>
internal sealed class ProjectManifest
{
public required string GameId { get; init; }
public required string Region { get; init; }
public required string DolSha256 { get; init; }
public required string RelSha256 { get; init; }
public static ProjectManifest Load(string path)
{
if (!File.Exists(path)) throw new FileNotFoundException("Translation project file is missing", path);
string? gameId = null, region = null, dolSha = null, relSha = null;
string section = "";
string inputKey = "";
foreach (var raw in File.ReadLines(path))
{
var line = Regex.Replace(raw, "#.*$", "");
if (string.IsNullOrWhiteSpace(line)) continue;
var sectionMatch = Regex.Match(line, "^([A-Za-z0-9_]+):");
if (sectionMatch.Success)
{
section = sectionMatch.Groups[1].Value;
inputKey = "";
continue;
}
if (section == "inputs")
{
var keyMatch = Regex.Match(line, @"^\s{2}([A-Za-z0-9_]+):\s*$");
if (keyMatch.Success) { inputKey = keyMatch.Groups[1].Value; continue; }
var shaMatch = Regex.Match(line, @"^\s*sha256:\s*([0-9a-fA-F]{64})\s*$");
if (shaMatch.Success)
{
var value = shaMatch.Groups[1].Value.ToLowerInvariant();
if (inputKey == "dol") dolSha = value;
else if (inputKey == "rel") relSha = value;
}
}
else if (section == "project")
{
var idMatch = Regex.Match(line, @"^\s*game_id:\s*(\S+)\s*$");
if (idMatch.Success) gameId = idMatch.Groups[1].Value;
var regionMatch = Regex.Match(line, @"^\s*region:\s*(\S+)\s*$");
if (regionMatch.Success) region = regionMatch.Groups[1].Value;
}
}
if (gameId is null || region is null || dolSha is null || relSha is null)
{
throw new InvalidDataException(
$"{path} does not pin game_id/region/dol.sha256/rel.sha256; the project file is not the shape this tool expects.");
}
return new ProjectManifest { GameId = gameId, Region = region, DolSha256 = dolSha, RelSha256 = relSha };
}
}
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>WiiCompiled.Setup.Linux</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Linux</RootNamespace>
<Version>0.2.22</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled on Linux</Description>
<DebugType>embedded</DebugType>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,27 @@
namespace WiiCompiled.Setup.Linux;
/// <summary>
/// Finds the repo checkout this tool is running from by walking up from its own directory looking
/// for Launcher/local-build.sh - this tool operates directly on a git checkout (no bundled/staged
/// workspace copy), so there is no installed "Toolkit" layout to anchor on the way the Windows
/// installer's Installation.cs does.
/// </summary>
internal static class WorkspaceLocator
{
private const int MaxSearchDepth = 6;
public static string FindFrom(string startDirectory)
{
var current = new DirectoryInfo(startDirectory);
for (var level = 0; level <= MaxSearchDepth && current is not null; level++, current = current.Parent)
{
if (File.Exists(Path.Combine(current.FullName, "Launcher", "local-build.sh")))
{
return current.FullName;
}
}
throw new InvalidOperationException(
"Could not find the WiiCompiled repository (looked for Launcher/local-build.sh walking up " +
$"from {startDirectory}). Pass --workspace <path-to-checkout> explicitly.");
}
}
@@ -1,4 +1,4 @@
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// Bridges a frontend-owned, named Windows event into the cancellation token used by setup.
@@ -1,4 +1,4 @@
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal enum AppMode
{
@@ -1,8 +1,9 @@
using System.Buffers.Binary;
using System.Security.Cryptography;
using System.Text;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal sealed record RetroRewindCompileInputs(
string RetroRewindRoot,
@@ -1,6 +1,7 @@
using System.Text.Json;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal static class ConsoleCommands
{
@@ -63,8 +64,8 @@ internal static class ConsoleCommands
{
using var payload = PayloadArchive.OpenCurrent();
var manifest = payload.ReadManifest();
var tool = Path.Combine(temp, "DolphinTool.exe");
payload.ExtractEntry(InstalledLayout.ToolkitEntryPrefix + "DolphinTool.exe", tool);
var tool = Path.Combine(temp, "nodtool.exe");
payload.ExtractEntry(InstalledLayout.ToolkitEntryPrefix + "nodtool.exe", tool);
payload.ExtractDirectory(InstalledLayout.ToolkitEntryPrefix + "Redist", temp);
reporter?.Progress(InstallStages.Validate, "Checking the Wii disc image...", 10);
var header = InputValidation.ReadDiscHeaderAsync(tool, command.GamePath!).GetAwaiter().GetResult();
@@ -1,6 +1,6 @@
using System.Diagnostics;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal static class GameLaunchService
{
@@ -0,0 +1,165 @@
using System.Diagnostics;
using System.Buffers.Binary;
using System.Net;
using System.Security.Cryptography;
using System.Text.Json;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
internal static class InputValidation
{
private static readonly HashSet<string> SupportedDiscImageExtensions = new(
[".iso", ".gcm", ".gcz", ".ciso", ".wbfs", ".wia", ".rvz"],
StringComparer.OrdinalIgnoreCase);
public static void ValidateExtension(string gamePath)
{
if (!File.Exists(gamePath))
throw new FileNotFoundException("The selected game image does not exist.", gamePath);
var extension = Path.GetExtension(gamePath);
if (!SupportedDiscImageExtensions.Contains(extension))
throw new InvalidDataException(
"Select a complete Wii disc image in ISO, GCM, GCZ, CISO, WBFS, WIA, or RVZ format.");
}
public static async Task<DiscHeader> ReadDiscHeaderAsync(string nodTool, string gamePath,
CancellationToken cancellationToken = default)
{
ValidateExtension(gamePath);
var result = await ProcessRunner.RunAsync(nodTool,
["info", Path.GetFullPath(gamePath)], null, cancellationToken);
if (result.ExitCode != 0)
throw new InvalidDataException("nodtool could not read this disc image. " + result.CombinedOutput.Trim());
var info = NodToolInfoParser.Parse(result.StandardOutput);
return new DiscHeader
{
GameId = info.GameId,
InternalName = info.Title,
Region = RegionFromGameId(info.GameId),
Revision = info.Revision,
};
}
private static string RegionFromGameId(string gameId) => gameId.Length >= 4
? gameId[3] switch
{
'P' => "PAL",
'E' => "NTSC-U",
'J' => "NTSC-J",
'K' => "Korea",
'W' => "Taiwan",
_ => gameId[3].ToString(),
}
: "Unknown";
public static void EnsureCompatibleDisc(DiscHeader header, PayloadManifest manifest)
{
if (!header.GameId.Equals(manifest.ExpectedGameId, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidDataException(
$"This build supports Mario Kart Wii PAL ({manifest.ExpectedGameId}). " +
$"The selected image is {header.GameId} ({header.InternalName}, {header.Region}).");
}
}
// Thin forwarding wrappers: the actual download/RSA-verification logic lives in
// WiiCompiled.Setup.Common.RetroWfcPayload (shared with WiiCompiled.Setup.Linux) so there's one
// copy of it, not two. Kept under these names so every existing call site here
// (ProductRepairService.cs, LocalBuildService.cs, Installation.cs, SelfTests.cs) is unchanged.
public const string CurrentRetroWfcPayloadUri = RetroWfcPayload.CurrentRetroWfcPayloadUri;
public static string ValidateStagedRetroWfcPayloadDirectory(string stagedDirectory,
RSAParameters? signingKey = null) =>
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(stagedDirectory, signingKey);
public static string ResolveRetroWfcPayloadFile(string stagedDirectory,
RSAParameters? signingKey = null) =>
RetroWfcPayload.ResolveRetroWfcPayloadFile(stagedDirectory, signingKey);
public static string ComputeRetroWfcPayloadSha256(string stagedDirectory,
RSAParameters? signingKey = null) =>
RetroWfcPayload.ComputeRetroWfcPayloadSha256(stagedDirectory, signingKey);
public static void ValidateRetroWfcPayloadUri(string uriText) =>
RetroWfcPayload.ValidateRetroWfcPayloadUri(uriText);
public static Task<RetroWfcPayloadSnapshot> DownloadRetroWfcPayloadAsync(string uriText,
string destinationDirectory, CancellationToken cancellationToken) =>
RetroWfcPayload.DownloadRetroWfcPayloadAsync(uriText, destinationDirectory, cancellationToken);
internal static bool IsTransientRetroWfcDownloadFailure(Exception exception,
CancellationToken cancellationToken) =>
RetroWfcPayload.IsTransientRetroWfcDownloadFailure(exception, cancellationToken);
public static string Sha256File(string path)
{
using var stream = File.OpenRead(path);
return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
}
}
internal sealed record ProcessResult(int ExitCode, string StandardOutput, string StandardError)
{
public string CombinedOutput => StandardOutput + Environment.NewLine + StandardError;
}
internal static class ProcessRunner
{
/// <summary>Runs a redirected child process to completion. <paramref name="configure"/> sets up a
/// working directory or scrubbed environment; <paramref name="capture"/> is off for callers that only
/// forward output live, so a build's output isn't buffered in memory for nobody to read.</summary>
public static async Task<ProcessResult> RunAsync(string executable, IReadOnlyList<string> arguments,
Action<string>? output, CancellationToken cancellationToken,
Action<ProcessStartInfo>? configure = null, bool capture = true,
Action<Exception>? onTerminationFailure = null)
{
var info = new ProcessStartInfo
{
FileName = executable,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
foreach (var argument in arguments) info.ArgumentList.Add(argument);
configure?.Invoke(info);
using var process = new Process { StartInfo = info, EnableRaisingEvents = true };
var stdout = new List<string>();
var stderr = new List<string>();
process.OutputDataReceived += (_, e) => { if (e.Data is not null) { if (capture) stdout.Add(e.Data); output?.Invoke(e.Data); } };
process.ErrorDataReceived += (_, e) => { if (e.Data is not null) { if (capture) stderr.Add(e.Data); output?.Invoke(e.Data); } };
if (!process.Start()) throw new InvalidOperationException($"Could not start {executable}.");
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await WaitForExitAsync(process, cancellationToken, onTerminationFailure);
return new ProcessResult(process.ExitCode, string.Join(Environment.NewLine, stdout),
string.Join(Environment.NewLine, stderr));
}
public static async Task WaitForExitAsync(Process process, CancellationToken cancellationToken,
Action<Exception>? onTerminationFailure = null)
{
try
{
await process.WaitForExitAsync(cancellationToken);
}
catch (OperationCanceledException)
{
try
{
if (!process.HasExited) process.Kill(entireProcessTree: true);
}
catch (Exception ex)
{
onTerminationFailure?.Invoke(ex);
}
await process.WaitForExitAsync(CancellationToken.None);
process.WaitForExit();
throw;
}
process.WaitForExit();
}
}
@@ -1,7 +1,8 @@
using System.Security.Cryptography;
using System.Text;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// A fail-fast, cross-process lock covering install, repair and launch operations for one install
@@ -1,6 +1,6 @@
using System.Text.Json;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// Stable stage identifiers reported by <c>--progress-json</c>. These are part of the public
@@ -1,4 +1,6 @@
namespace WiiCompiled.Setup;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// Owns one temporary directory for an install operation. The name carries the installation's scope
@@ -1,4 +1,6 @@
namespace WiiCompiled.Setup;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
internal enum InstallTransactionEntryKind
{
@@ -1,4 +1,6 @@
namespace WiiCompiled.Setup;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
/// <summary>Provenance written by the bundled build script next to every product it produces.</summary>
internal sealed class LocalBuildProvenance
@@ -1,4 +1,4 @@
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// Names of the installed/staged layout. Not cosmetic: payload and toolkit identities hash relative paths
@@ -26,7 +26,7 @@ internal static class InstalledLayout
/// </summary>
public static readonly string[] DependencyNames =
[
"abseil-cpp", "cppwinrt", "dawn_prebuilt", "fmt", "freetype", "imgui", "native_prebuilt",
"abseil-cpp", "cppwinrt", "dawn_prebuilt", "fmt", "freetype", "imgui", "libusb", "native_prebuilt",
"png", "SDL", "sqlite3", "tracy", "xxhash", "zlib", "zstd"
];
}
@@ -1,4 +1,6 @@
namespace WiiCompiled.Setup;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
internal sealed class InstallerEngine
{
@@ -57,9 +59,9 @@ internal sealed class InstallerEngine
var runtimeAssetsCurrent = sameToolkit && RuntimeAssetsAreCurrent(existing,
candidateRuntimeAssetsFingerprint, cancellationToken);
var installedDolphinTool = Path.Combine(existing.ToolkitDirectory, "DolphinTool.exe");
var installedNodTool = Path.Combine(existing.ToolkitDirectory, "nodtool.exe");
var extractToolkit = MustRefreshToolkit(sameToolkit, samePackageContent,
File.Exists(installedDolphinTool));
File.Exists(installedNodTool));
var extractWorkspace = !sameToolkit || !runtimeAssetsCurrent;
_reporter.Progress(InstallStages.ExtractToolkit,
@@ -77,9 +79,9 @@ internal sealed class InstallerEngine
payload.ExtractEntry(InstalledLayout.PayloadManifestFileName,
Path.Combine(staging, InstalledLayout.PayloadManifestFileName));
var dolphinTool = extractToolkit ? Path.Combine(toolkit, "DolphinTool.exe") : installedDolphinTool;
var nodTool = extractToolkit ? Path.Combine(toolkit, "nodtool.exe") : installedNodTool;
_reporter.Progress(InstallStages.Validate, "Checking the Wii disc image...", 2);
var header = await InputValidation.ReadDiscHeaderAsync(dolphinTool, options.GamePath,
var header = await InputValidation.ReadDiscHeaderAsync(nodTool, options.GamePath,
cancellationToken);
InputValidation.EnsureCompatibleDisc(header, manifest);
var canonicalRetroRoot = options.RetroDirectoryPath is null
@@ -153,7 +155,7 @@ internal sealed class InstallerEngine
if (reusableGameAssets is null)
{
await ExtractGameAssetsAsync(dolphinTool, options.GamePath,
await ExtractGameAssetsAsync(nodTool, options.GamePath,
Path.Combine(staging, "GameAssets"), manifest, cancellationToken);
}
@@ -164,8 +166,8 @@ internal sealed class InstallerEngine
internal static bool MustRefreshToolkit(bool sameToolkit, bool samePackageContent,
bool dolphinToolPresent) =>
!sameToolkit || !samePackageContent || !dolphinToolPresent;
bool nodToolPresent) =>
!sameToolkit || !samePackageContent || !nodToolPresent;
private static void AddComponent(List<InstallTransactionEntry> entries, string staging,
string installDirectory, string name) =>
@@ -469,18 +471,23 @@ internal sealed class InstallerEngine
}
}
private async Task ExtractGameAssetsAsync(string dolphinTool, string gamePath, string destination,
private async Task ExtractGameAssetsAsync(string nodTool, string gamePath, string destination,
PayloadManifest manifest, CancellationToken cancellationToken)
{
_reporter.Progress(InstallStages.ExtractDisc,
"Extracting the game disc. This is the longest preparation step...", 6);
var extraction = await ProcessRunner.RunAsync(dolphinTool,
["extract", "-i", Path.GetFullPath(gamePath), "-o", destination, "-g", "-q"],
// Extracted straight into a "DATA" subfolder so the on-disk layout matches what
// Installation.GameDataDirectory and every other reader of it already expect - nodtool
// itself has no such wrapper (it extracts sys/+files/ directly to whatever <outdir> is
// given), so this is purely destination-side, not a nodtool convention.
var dataRoot = Path.Combine(destination, "DATA");
var extraction = await ProcessRunner.RunAsync(nodTool,
["extract", Path.GetFullPath(gamePath), dataRoot, "-q"],
line => { if (!string.IsNullOrWhiteSpace(line)) _reporter.Diagnostic(line); },
cancellationToken);
if (extraction.ExitCode != 0)
throw new InvalidDataException("Game extraction failed. " + extraction.CombinedOutput.Trim());
ValidateExtractedGame(Path.Combine(destination, "DATA"), manifest);
ValidateExtractedGame(dataRoot, manifest);
}
private static void ValidateExtractedGame(string dataRoot, PayloadManifest manifest)
@@ -1,6 +1,7 @@
using System.Diagnostics;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// <see cref="Both"/> runs one retro-aware translation and compiles the two products from a single
@@ -1,6 +1,7 @@
using System.Text.Json.Serialization;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal enum RetroWfcPayloadMode
{
@@ -54,12 +55,6 @@ internal sealed class PayloadManifest
public string NativeToolchainFingerprint { get; set; } = "";
}
/// <summary>
/// One validated, content-identified download in operation-owned scratch space. Callers use this
/// exact directory for both the update decision and any resulting build.
/// </summary>
internal sealed record RetroWfcPayloadSnapshot(string Directory, string Sha256, long ByteLength);
internal sealed class DiscHeader
{
[JsonPropertyName("game_id")]
@@ -2,7 +2,7 @@ using System.IO.Compression;
using System.Text;
using System.Text.Json;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal sealed class PayloadArchive : IDisposable
{
@@ -0,0 +1,54 @@
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// A portable root can be moved or renamed between operations. Every installed-host operation that
/// reads <c>install-state.json</c> passes through here first so exactly one place decides what a
/// moved installation means, and so a non-portable installation is never touched.
/// </summary>
internal static class PortableInstallHealing
{
/// <summary>
/// Reconciles a moved portable installation with its recorded location: the state file adopts the
/// directory it was actually found in, and the native build tree is discarded because its
/// CMake cache holds absolute paths from the old location. Returns whether anything was healed.
/// </summary>
public static bool HealMovedInstall(Installation installation, IInstallReporter? reporter = null)
{
// Guard: an ordinary installation that disagrees with its state file is a real problem for
// the operation to report, not something to silently rewrite.
if (PortableRoot.TryFind(installation.Root) is null) return false;
var state = installation.ReadInstallState();
if (state is not { SchemaVersion: 1 } || string.IsNullOrWhiteSpace(state.InstallDir)) return false;
string recorded;
try
{
recorded = FileSystemUtilities.NormalizePath(state.InstallDir);
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException)
{
recorded = state.InstallDir;
}
if (recorded.Equals(installation.Root, StringComparison.OrdinalIgnoreCase)) return false;
var previous = state.InstallDir;
state.InstallDir = installation.Root;
JsonState.Write(installation.InstallStatePath, state);
// The configured native build directory bakes absolute source, toolchain, and output paths
// into CMakeCache.txt. After a move it is unusable and would fail the next configure rather
// than being reused, so it is removed and reconfigured from scratch on the next build.
var nativeBuild = Path.Combine(installation.WorkspaceDirectory, "native-build");
var hadNativeBuild = Directory.Exists(nativeBuild);
if (hadNativeBuild) FileSystemUtilities.DeleteDirectoryIfExists(nativeBuild);
reporter?.Diagnostic(
$"This portable installation moved from {previous} to {installation.Root}. " +
"The recorded location was updated" +
(hadNativeBuild ? " and the location-bound native build cache was discarded." : "."));
return true;
}
}
@@ -1,4 +1,6 @@
namespace WiiCompiled.Setup;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// Reconciles installed products against the canonical Retro Rewind install Wheel Wizard owns: the
@@ -1,4 +1,4 @@
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
using System.Runtime.InteropServices;
@@ -121,7 +121,7 @@ internal static class PlatformChecks
internal static class ProductInfo
{
public const string Name = "WiiCompiled";
public const string Version = "0.2.21";
public const string Version = "0.2.27";
/// <summary>
/// The setup executable is copied into the installation under this name. It is the launcher and
@@ -1,6 +1,7 @@
using System.Diagnostics;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// Refuses to replace installed products while one of them is running: publishing renames the
@@ -1,4 +1,6 @@
namespace WiiCompiled.Setup;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// The one path by which a product receives its copied runtime assets, shared by install and repair.
@@ -1,7 +1,8 @@
using System.Buffers.Binary;
using System.Security.Cryptography;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal static class SelfTests
{
@@ -143,17 +144,17 @@ internal static class SelfTests
private static void TestToolkitRefreshDecision()
{
if (!InstallerEngine.MustRefreshToolkit(sameToolkit: false, samePackageContent: true,
dolphinToolPresent: true))
nodToolPresent: true))
throw new Exception("A republished workspace kept the installed toolkit; the shipped " +
"translator and project file could come from different releases.");
if (!InstallerEngine.MustRefreshToolkit(sameToolkit: true, samePackageContent: false,
dolphinToolPresent: true))
nodToolPresent: true))
throw new Exception("Changed toolkit package content was not extracted.");
if (!InstallerEngine.MustRefreshToolkit(sameToolkit: true, samePackageContent: true,
dolphinToolPresent: false))
throw new Exception("A missing DolphinTool.exe did not force toolkit extraction.");
nodToolPresent: false))
throw new Exception("A missing nodtool.exe did not force toolkit extraction.");
if (InstallerEngine.MustRefreshToolkit(sameToolkit: true, samePackageContent: true,
dolphinToolPresent: true))
nodToolPresent: true))
throw new Exception("An unchanged toolkit was needlessly re-extracted.");
}
@@ -1010,7 +1011,7 @@ internal static class SelfTests
throw new Exception("The toolkit fingerprint is not stable.");
// A file that has nothing to do with generated code must not invalidate every install.
File.WriteAllText(Path.Combine(root, "Toolkit", "DolphinTool.exe"), "irrelevant");
File.WriteAllText(Path.Combine(root, "Toolkit", "nodtool.exe"), "irrelevant");
if (ToolkitFingerprint.Compute(root) != first)
throw new Exception("An unrelated toolkit file changed the fingerprint.");
@@ -1177,7 +1178,7 @@ internal static class SelfTests
"x86_64-w64-mingw32-clang++.exe", "x86_64-w64-mingw32-windres.exe"
})
File.WriteAllText(Path.Combine(root, "Toolkit", "llvm-mingw", "bin", executable), executable);
File.WriteAllText(Path.Combine(root, "Toolkit", "DolphinTool.exe"), "tool");
File.WriteAllText(Path.Combine(root, "Toolkit", "nodtool.exe"), "tool");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "LocalBuild.ps1"), "# build");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "NativeBuildFlags.ps1"), "# flags");
File.WriteAllText(Path.Combine(root, "BuildWorkspace", "projects", "mkwii", "recomp.yml"), "profiles: {}");
@@ -1,6 +1,6 @@
using Microsoft.Win32;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal static class ShellIntegration
{
@@ -1,7 +1,8 @@
using System.Security.Cryptography;
using System.Text;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
/// <summary>
/// Content identity of everything that decides what the locally produced executables contain.
@@ -78,12 +79,12 @@ internal static class ToolkitFingerprint
var workspace = InstalledLayout.Workspace(root);
var entries = new SortedDictionary<string, string>(StringComparer.Ordinal);
// DolphinTool validates/extracts the user disc but does not influence generated products.
// nodtool validates/extracts the user disc but does not influence generated products.
// Everything else in Toolkit can affect translation, compilation, linking, or copied
// runtime support and therefore belongs to the compile identity.
AddDirectory(entries, root, toolkit, null,
cancellationToken,
file => !Path.GetFileName(file).Equals("DolphinTool.exe", StringComparison.OrdinalIgnoreCase));
file => !Path.GetFileName(file).Equals("nodtool.exe", StringComparison.OrdinalIgnoreCase));
AddFile(entries, root, Path.Combine(workspace, "LocalBuild.ps1"), cancellationToken);
AddFile(entries, root, Path.Combine(workspace, "NativeBuildFlags.ps1"), cancellationToken);
AddDirectory(entries, root, Path.Combine(workspace, "projects"), null, cancellationToken);
@@ -1,7 +1,8 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using WiiCompiled.Setup.Common;
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal static class UninstallService
{
@@ -5,13 +5,16 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>WiiCompiled.Setup</AssemblyName>
<RootNamespace>WiiCompiled.Setup</RootNamespace>
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.2.21</Version>
<Version>0.2.27</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled</Description>
<DebugType>embedded</DebugType>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WiiCompiled.Setup.Common\WiiCompiled.Setup.Common.csproj" />
</ItemGroup>
</Project>
@@ -1,4 +1,4 @@
namespace WiiCompiled.Setup;
namespace WiiCompiled.Setup.Windows;
internal static class WorkspaceTimestamps
+303
View File
@@ -0,0 +1,303 @@
#!/usr/bin/env bash
# Packages Launcher/WiiCompiled.Setup.Linux as a self-contained AppImage: a single file Wheel
# Wizard (or anyone else) can fetch and execute with no git clone, no `dotnet` install, and no
# `dolphin-tool` package required at all. The installer and translator are published as
# self-contained binaries, and `nodtool` (a prebuilt MIT/Apache-2.0 CLI from encounter/nod, see
# NodToolProvider.cs) is downloaded and bundled too - AppRun passes --translator-bin and
# --disc-tool-bin so local-build.sh/DiscTool.cs skip their from-source/download fallbacks entirely.
# A pruned native clang/lld/cmake/ninja toolchain (see prepare-portable-tools.sh) is bundled the
# same way - AppRun passes --cc/--cxx/--fuse-ld/--cmake/--ninja so local-build.sh never has to find
# a system compiler, CMake, or Ninja. It still shells out to system pkg-config and Vulkan headers,
# matching Launcher/local-build.sh's own remaining prerequisites. A precompiled aurora +
# third-party package (see Prepare-NativePrebuilt.sh) is bundled the same way too - AppRun passes
# --native-prebuilt-dir so local-build.sh never compiles aurora-main from source at all.
#
# An AppImage mounts read-only, but local-build.sh writes generated/, native-build/, Assets/, etc.
# into the workspace it's given. So AppRun (written below) copies the bundled workspace snapshot
# out to a writable cache directory on first run, and only ever re-syncs the bundled directories
# (runtime/, aurora-main/, projects/, local-build.sh) on a later run whose bundled version changed
# - generated/native-build/Assets/PulsarPacks live only in that writable cache and are never
# touched by the sync, so local-build.sh's own incremental caching survives across runs and across
# AppImage updates. translator/ isn't part of this snapshot at all: it's published as its own
# self-contained binary (usr/bin/translator-cli) below and never needs a writable copy. Neither
# native-prebuilt/ nor the toolchain are copied into the cache either - both are large
# (~90 MiB / ~500 MiB) and local-build.sh only ever reads from them - but AppRun does point
# $CACHE/toolchain and $CACHE/native-prebuilt symlinks at the current mount on every single launch
# (see AppRun's own comment): an AppImage's FUSE mount is at a fresh random /tmp/.mount_XXXXXX
# every run, and CMake bakes whatever compiler/tool path it's given directly into each
# build.ninja rule's command line, so referencing $HERE straight would change that command line -
# and Ninja reruns any rule whose command line changed - forcing a full rebuild on every single
# launch even though the compiler itself never actually changed. The symlink keeps the path
# string CMake/Ninja see identical across runs while what it resolves to tracks the current mount
# underneath.
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace=$(cd "$script_dir/.." && pwd)
# `uname -m` reports the *kernel's* architecture, which can differ from userspace - an aarch64
# kernel can run a 32-bit armhf userland (as shipped by 32-bit Raspberry Pi OS), same as an x86_64
# kernel can run an i686 one. What matters here is which userspace binaries (dotnet, appimagetool)
# will actually run, so this reads the ELF header of this script's own running bash interpreter -
# real userspace - rather than trusting the kernel's self-report. /proc/$$/exe (not /proc/self/exe:
# that would resolve inside the readlink subprocess below, to readlink itself, not to bash) is this
# shell's own PID. EI_CLASS (byte 4: 1=32-bit, 2=64-bit) and e_machine (bytes 18-19: 3=EM_386,
# 40=EM_ARM, 62=EM_X86_64, 183=EM_AARCH64) are read as plain little-endian bytes, which every
# real-world x86/ARM Linux userland uses; ELF's big-endian encoding is a non-issue here since no
# Linux distro ships a big-endian x86 or ARM userland.
elf_exe=$(readlink -f "/proc/$$/exe")
elf_class=$(od -An -t u1 -j 4 -N 1 "$elf_exe" | tr -d ' ')
elf_machine_lo=$(od -An -t u1 -j 18 -N 1 "$elf_exe" | tr -d ' ')
elf_machine_hi=$(od -An -t u1 -j 19 -N 1 "$elf_exe" | tr -d ' ')
elf_machine=$(( elf_machine_hi * 256 + elf_machine_lo ))
# Mirrors the host-architecture detection NodToolProvider.cs already does (RuntimeInformation.
# OSArchitecture) so this script's own dotnet RID and appimagetool selection agree with the
# nodtool binary that same code path resolves below. local-build.sh needs no such mapping itself:
# it just drives the native CMake configure, which already accepts x86_64 or aarch64 natively
# (see runtime/CMakeLists.txt's CMAKE_SYSTEM_PROCESSOR check).
case "$elf_class:$elf_machine" in
2:62)
dotnet_rid=linux-x64
appimagetool_arch=x86_64
;;
2:183)
dotnet_rid=linux-arm64
appimagetool_arch=aarch64
;;
*)
echo "build-appimage.sh: unsupported userspace architecture (ELF class $elf_class, machine $elf_machine) - WiiCompiled requires a 64-bit x86_64 or aarch64 userland" >&2
exit 1
;;
esac
output_dir="$workspace/Launcher/dist"
appimagetool_override=""
while [[ $# -gt 0 ]]; do
case "$1" in
--output-dir) output_dir=$2; shift 2 ;;
--appimagetool) appimagetool_override=$2; shift 2 ;;
-h|--help)
echo "Usage: build-appimage.sh [--output-dir DIR] [--appimagetool PATH]"
exit 0
;;
*) echo "build-appimage.sh: unknown argument: $1" >&2; exit 1 ;;
esac
done
appdir="$workspace/Launcher/artifacts/appimage-build/AppDir"
rm -rf "$appdir"
mkdir -p "$appdir/usr/bin" "$appdir/workspace/Launcher"
echo "Publishing the installer (self-contained $dotnet_rid)..."
publish_tmp="$workspace/Launcher/artifacts/appimage-build/publish"
rm -rf "$publish_tmp"
dotnet publish "$workspace/Launcher/WiiCompiled.Setup.Linux" -c Release -r "$dotnet_rid" \
--self-contained -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \
-o "$publish_tmp"
cp "$publish_tmp/WiiCompiled.Setup.Linux" "$appdir/usr/bin/wiicompiled-setup"
chmod +x "$appdir/usr/bin/wiicompiled-setup"
# Published as a self-contained binary too, so an AppImage user never needs a `dotnet` SDK on
# PATH at all - local-build.sh is told about it via --translator-bin and skips its own
# dotnet-build-from-source step entirely (see local-build.sh's translator resolution branch).
echo "Publishing the translator (self-contained $dotnet_rid)..."
translator_publish_tmp="$workspace/Launcher/artifacts/appimage-build/publish-translator"
rm -rf "$translator_publish_tmp"
dotnet publish "$workspace/translator/src/Translator.Cli" -c Release -r "$dotnet_rid" \
--self-contained -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \
-o "$translator_publish_tmp"
cp "$translator_publish_tmp/Translator.Cli" "$appdir/usr/bin/translator-cli"
chmod +x "$appdir/usr/bin/translator-cli"
# Resolved via the shared WiiCompiled.Setup.Common.Cli helper (also used by Build-Installer.ps1 on
# Windows) rather than a second curl/version-pin copy here: it downloads and caches the same way
# NodToolProvider.cs always does (Launcher/artifacts/nodtool), so there is exactly one place that
# knows the nodtool version/URL/platform-asset mapping.
echo "Resolving nodtool..."
nodtool_path=$(dotnet run --project "$workspace/Launcher/WiiCompiled.Setup.Common.Cli" -c Release -- \
--workspace "$workspace" | tail -n1)
cp "$nodtool_path" "$appdir/usr/bin/nodtool"
chmod +x "$appdir/usr/bin/nodtool"
echo "Preparing the portable clang/lld/cmake/ninja toolchain ($appimagetool_arch)..."
bash "$script_dir/prepare-portable-tools.sh" --arch "$appimagetool_arch"
mkdir -p "$appdir/usr/toolchain"
cp -a "$workspace/Launcher/artifacts/portable-tools/toolchain-$appimagetool_arch"/. "$appdir/usr/toolchain/"
# Precompiled aurora + third-party package (see Prepare-NativePrebuilt.sh) so a user's own
# local-build.sh never has to compile aurora itself (~43% of local build CPU time). Re-harvesting
# recompiles the whole aurora/Crypto++ closure with the toolchain above, so this is skipped unless
# --print-fingerprint-only (a fast, build-free check) says the existing package no longer matches
# the current compiler/flags/aurora/third_party sources.
native_prebuilt_dir="$workspace/Launcher/artifacts/native-prebuilt-$appimagetool_arch"
echo "Checking whether the precompiled aurora + third-party package ($appimagetool_arch) is current..."
current_fingerprint=$(bash "$script_dir/Prepare-NativePrebuilt.sh" --arch "$appimagetool_arch" --print-fingerprint-only)
package_current=0
if [[ -f "$native_prebuilt_dir/provenance.json" ]]; then
package_current=$(CURRENT_FINGERPRINT="$current_fingerprint" python3 - "$native_prebuilt_dir/provenance.json" <<'PY'
import json
import os
import sys
provenance = json.load(open(sys.argv[1], encoding="utf-8"))
current = dict(line.split("=", 1) for line in os.environ["CURRENT_FINGERPRINT"].splitlines() if line)
fields = {
"compiler_sha256": "CompilerSha256",
"flag_fingerprint": "FlagFingerprint",
"aurora_fingerprint": "AuroraSourceFingerprint",
"third_party_fingerprint": "ThirdPartySourceFingerprint",
}
print(1 if all(provenance.get(v) == current.get(k) for k, v in fields.items()) else 0)
PY
)
fi
if [[ "$package_current" == "1" ]]; then
echo "Native prebuilt package is current; reusing $native_prebuilt_dir"
else
echo "Native prebuilt package is missing or stale; harvesting a fresh one (compiles aurora once, can take a while)..."
bash "$script_dir/Prepare-NativePrebuilt.sh" --arch "$appimagetool_arch"
fi
mkdir -p "$appdir/native-prebuilt"
cp -a "$native_prebuilt_dir/." "$appdir/native-prebuilt/"
echo "Staging the bundled workspace snapshot..."
for dir in runtime aurora-main projects; do
cp -r "$workspace/$dir" "$appdir/workspace/$dir"
done
# Mirrors Build-Installer.ps1's own staging exclusions exactly: aurora-main/extern/CMakeLists.txt
# is the real FetchContent driver and must ship, but any already-fetched dependency *subdirectory*
# a developer's local checkout accumulated under extern/ is stale/large build output, not a
# release input - only directories inside extern/ are stripped, never the file itself. runtime/build
# is a plain developer build directory.
find "$appdir/workspace/aurora-main/extern" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} +
rm -rf "$appdir/workspace/runtime/build"
cp "$workspace/Launcher/local-build.sh" "$appdir/workspace/Launcher/local-build.sh"
# AppRun re-syncs runtime/aurora-main/projects/local-build.sh into the writable cache only when
# this changes, so it must change whenever any of those bundled paths actually did - a bare commit
# hash gets this wrong for an uncommitted change (verified directly: rebuilding after editing
# local-build.sh with no commit produced the same hash as the stale cache, so AppRun kept serving
# the old script and failed on a flag that didn't exist yet). `git status --porcelain` catches both
# modified tracked files and new untracked ones; appending a fresh timestamp when it's non-empty
# guarantees this never matches a previous build's stamp, forcing a resync every time the tree is
# dirty. A clean tree (a real tagged release) keeps the stable commit-hash behavior, so identical
# reruns of the same release AppImage don't resync needlessly.
if git -C "$workspace" rev-parse HEAD >/dev/null 2>&1; then
version=$(git -C "$workspace" rev-parse HEAD)
if [[ -n "$(git -C "$workspace" status --porcelain 2>/dev/null)" ]]; then
version="$version-dirty-$(date -u +%s)"
fi
echo "$version" > "$appdir/workspace/.bundle-version"
else
date -u +%s > "$appdir/workspace/.bundle-version"
fi
echo "Writing AppRun..."
cat > "$appdir/AppRun" <<'APPRUN'
#!/bin/bash
set -euo pipefail
HERE="$(dirname "$(readlink -f "$0")")"
CACHE="${XDG_DATA_HOME:-$HOME/.local/share}/WiiCompiled/workspace"
mkdir -p "$CACHE"
if [ ! -f "$CACHE/.bundle-version" ] || \
[ "$(cat "$HERE/workspace/.bundle-version")" != "$(cat "$CACHE/.bundle-version")" ]; then
mkdir -p "$CACHE/Launcher"
for dir in runtime aurora-main projects; do
rm -rf "$CACHE/$dir"
cp -r "$HERE/workspace/$dir" "$CACHE/$dir"
done
cp "$HERE/workspace/Launcher/local-build.sh" "$CACHE/Launcher/local-build.sh"
cp "$HERE/workspace/.bundle-version" "$CACHE/.bundle-version"
fi
# toolchain/ and native-prebuilt/ are NOT copied into the cache (they're large - ~500 MiB /
# ~90 MiB - and local-build.sh only ever reads from them): $CACHE/toolchain and
# $CACHE/native-prebuilt are symlinks re-pointed at the current mount on every single launch
# (unconditionally, not gated on .bundle-version above, since the mount path itself - unlike the
# bundled content - changes every run regardless). CMake bakes a compiler/tool path directly into
# each build.ninja rule's command line and Ninja reruns any rule whose command line changed since
# the last build (verified directly) - an AppImage's FUSE mount is at a fresh random
# /tmp/.mount_XXXXXX every launch, so referencing $HERE straight would change that command line,
# and therefore force a full rebuild, on every single run even though the compiler itself never
# actually changed. A symlink keeps the *path string* CMake/Ninja see identical across runs while
# what it resolves to tracks the current mount underneath (verified directly: CMake records
# whatever path it's given as-is - including a symlink - without resolving it first).
[ -L "$CACHE/toolchain" ] || rm -rf "$CACHE/toolchain"
[ -L "$CACHE/native-prebuilt" ] || rm -rf "$CACHE/native-prebuilt"
ln -sfn "$HERE/usr/toolchain" "$CACHE/toolchain"
ln -sfn "$HERE/native-prebuilt" "$CACHE/native-prebuilt"
exec "$HERE/usr/bin/wiicompiled-setup" --workspace "$CACHE" \
--translator-bin "$HERE/usr/bin/translator-cli" \
--disc-tool-bin "$HERE/usr/bin/nodtool" \
--cc "$CACHE/toolchain/bin/clang" \
--cxx "$CACHE/toolchain/bin/clang++" \
--fuse-ld lld \
--cmake "$CACHE/toolchain/bin/cmake" \
--ninja "$CACHE/toolchain/bin/ninja" \
--native-prebuilt-dir "$CACHE/native-prebuilt" "$@"
APPRUN
chmod +x "$appdir/AppRun"
echo "Writing desktop entry and icon..."
cat > "$appdir/wiicompiled-setup.desktop" <<'DESKTOP'
[Desktop Entry]
Type=Application
Name=WiiCompiled Setup
Comment=Translate, compile, and launch Mario Kart Wii natively on Linux
Exec=AppRun
Icon=wiicompiled-setup
Categories=Game;
Terminal=true
DESKTOP
# No WiiCompiled logo/icon asset exists anywhere in this repo yet. appimagetool refuses to package
# without one, so this is a minimal solid-color placeholder - a one-line swap for real branding
# later (just replace this generated file with a real wiicompiled-setup.png before packaging).
python3 - "$appdir/wiicompiled-setup.png" <<'PY'
import struct
import sys
import zlib
path = sys.argv[1]
def chunk(tag: bytes, data: bytes) -> bytes:
return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data))
width = height = 256
row = b"\x00" + bytes([0x3A, 0x5F, 0x8F, 0xFF]) * width # filter byte + opaque blue-grey pixels
raw = row * height
ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)
idat = zlib.compress(raw, 9)
with open(path, "wb") as handle:
handle.write(b"\x89PNG\r\n\x1a\n")
handle.write(chunk(b"IHDR", ihdr))
handle.write(chunk(b"IDAT", idat))
handle.write(chunk(b"IEND", b""))
PY
echo "Resolving appimagetool..."
appimagetool="$appimagetool_override"
if [[ -z "$appimagetool" ]]; then
# Cache path is arch-tagged so a workspace shared or synced across an x86_64 and an aarch64
# machine never picks up the wrong architecture's cached binary.
appimagetool="$workspace/Launcher/artifacts/appimagetool-$appimagetool_arch"
if [[ ! -x "$appimagetool" ]]; then
echo "Downloading appimagetool ($appimagetool_arch)..."
mkdir -p "$(dirname "$appimagetool")"
curl -fsSL "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-$appimagetool_arch.AppImage" \
-o "$appimagetool"
chmod +x "$appimagetool"
fi
fi
mkdir -p "$output_dir"
echo "Packaging..."
# appimagetool detects the target architecture from the first ELF executable it finds in the
# AppDir; AppRun here is a shell script, not ELF, so ARCH must be set explicitly.
output_name="WiiCompiled-Setup-$appimagetool_arch.AppImage"
ARCH="$appimagetool_arch" "$appimagetool" "$appdir" "$output_dir/$output_name"
echo "Built: $output_dir/$output_name"
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env bash
# Native macOS build automation: optionally extract -> translate -> compile -> publish .app.
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
default_workspace=$(cd "$script_dir/.." && pwd)
fail() { printf 'local-build-macos.command: error: %s\n' "$*" >&2; exit 1; }
step() { printf 'MKWCBUILD:STEP:%s %s\n' "$1" "$2"; }
assert_file() { [[ -f "$1" ]] || fail "$2 is missing: $1"; }
sha256() { shasum -a 256 "$1" | awk '{ print $1 }'; }
usage() {
cat <<'EOF'
Usage: local-build-macos.command --output-dir DIR [options]
--workspace DIR Repository root (default: this script's parent directory)
--profile {base|retro-rewind|both} Product to build (default: base)
--output-dir DIR Output .app directory (required; Retro Rewind for both)
--base-output-dir DIR Base .app directory (required with --profile both)
--game IMAGE --nodtool PATH Extract and verify a clean PAL RMCP01 disc image first
--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
--parallel N Pin translation and build parallelism
--cmake PATH --ninja PATH Override build tools
--dotnet PATH Override dotnet
--translator-bin PATH Use a self-contained Translator.Cli executable
EOF
}
workspace="$default_workspace"; profile=base; output_dir=""; base_output_dir=""
game=""; nodtool=""; retro_root=""; retro_wfc=""; skip_retro_wfc=0; force_clean=0
parallel=0; cmake_bin=cmake; ninja_bin=ninja; dotnet_bin=dotnet; translator_bin=""
while (($#)); do
case "$1" in
--workspace) workspace=${2:-}; shift 2 ;;
--profile) profile=${2:-}; shift 2 ;;
--output-dir) output_dir=${2:-}; shift 2 ;;
--base-output-dir) base_output_dir=${2:-}; shift 2 ;;
--game) game=${2:-}; shift 2 ;;
--nodtool) nodtool=${2:-}; shift 2 ;;
--retro-rewind-package-dir) retro_root=${2:-}; shift 2 ;;
--retro-wfc-offline-dir) retro_wfc=${2:-}; shift 2 ;;
--skip-retro-wfc-payload) skip_retro_wfc=1; shift ;;
--force-clean-build) force_clean=1; shift ;;
--parallel) parallel=${2:-}; shift 2 ;;
--cmake) cmake_bin=${2:-}; shift 2 ;;
--ninja) ninja_bin=${2:-}; shift 2 ;;
--dotnet) dotnet_bin=${2:-}; shift 2 ;;
--translator-bin) translator_bin=${2:-}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown option: $1" ;;
esac
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'
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
builds_retro=0; [[ "$profile" != base ]] && builds_retro=1
if [[ "$profile" == both && -z "$base_output_dir" ]]; then fail '--base-output-dir is required with --profile both'; fi
if [[ "$profile" != both && -n "$base_output_dir" ]]; then fail '--base-output-dir is valid only with --profile both'; fi
if [[ -n "$game" || -n "$nodtool" ]]; then [[ -n "$game" && -n "$nodtool" ]] || fail '--game and --nodtool must be supplied together'; fi
if (( builds_retro )); then
[[ -n "$retro_root" ]] || fail '--retro-rewind-package-dir is required for Retro Rewind'
[[ -n "$retro_wfc" ]] && (( skip_retro_wfc )) && fail 'choose only one Retro-WFC mode'
[[ -n "$retro_wfc" || $skip_retro_wfc -eq 1 ]] || fail 'choose a Retro-WFC payload directory or --skip-retro-wfc-payload'
fi
for tool in "$cmake_bin" "$ninja_bin" clang clang++ shasum; do command -v "$tool" >/dev/null || fail "required tool not found: $tool"; done
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"
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'
if (( force_clean )); then
step force-clean 'Discarding translation and native build caches'
rm -rf "$generated" "$manifest_dir" "$native_build"
fi
if [[ -n "$translator_bin" ]]; then
assert_file "$translator_bin" 'Translator.Cli executable'
translator() { "$translator_bin" "$@"; }
else
command -v "$dotnet_bin" >/dev/null || fail "required tool not found: $dotnet_bin"
translator_dll="$workspace/translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll"
if [[ ! -f "$translator_dll" ]]; then
step build-translator 'Building the translator'
"$dotnet_bin" build "$workspace/translator/src/Translator.Cli/Translator.Cli.csproj" -c Release
fi
translator() { "$dotnet_bin" "$translator_dll" "$@"; }
fi
entry_point=$(awk '/^translation:/{inside=1} inside && /^[[:space:]]*-[[:space:]]*0[xX][0-9a-fA-F]+[[:space:]]*$/{gsub(/^[[:space:]]*-[[:space:]]*/, ""); print; exit}' "$project")
[[ -n "$entry_point" ]] || fail 'could not find the translation entry point'
cpu=$(sysctl -n hw.ncpu); mem_gib=$(( $(sysctl -n hw.memsize) / 1024 / 1024 / 1024 )); (( mem_gib < 1 )) && mem_gib=1
if (( parallel > 0 )); then translator_threads=$parallel; translated_jobs=$parallel; global_jobs=$parallel
else translator_threads=$(( cpu < 16 ? cpu : 16 )); translated_jobs=$(( cpu < mem_gib / 2 ? cpu : mem_gib / 2 )); (( translated_jobs < 1 )) && translated_jobs=1; global_jobs=$cpu; fi
if (( builds_retro )); then
retro_root=$(cd "$retro_root" && pwd)
if [[ ! -f "$retro_root/Binaries/Code.pul" && -f "$retro_root/RetroRewind6/Binaries/Code.pul" ]]; then
retro_root="$retro_root/RetroRewind6"
fi
assert_file "$retro_root/Binaries/Code.pul" 'Retro Rewind Code.pul'
stage="$workspace/PulsarPacks/completed/RetroRewind/RetroRewind6/Binaries"
mkdir -p "$stage"
if [[ ! "$retro_root/Binaries/Code.pul" -ef "$stage/Code.pul" ]]; then
cp -f "$retro_root/Binaries/Code.pul" "$stage/Code.pul"
fi
fi
step translate-base 'Translating the user-owned base game'
rm -rf "$functions" "$metadata" "$manifest_dir"; mkdir -p "$functions" "$manifest_dir"
translator translate-recursive "$entry_point" --project "$project" --outdir "$functions" --output-metadata "$metadata" --production-source-bundle "$generated/base_translation_sources.bin" --no-function-files --prune-stale --threads "$translator_threads"
step emit-base-manifest 'Creating the local base translation manifest'
translator emit-base-manifest --project "$project" --out "$manifest_dir" --functions-dir "$functions" --translation-output-metadata "$metadata" --region P
if (( builds_retro )); then
mod_out="$workspace/build/mods/retro_rewind_full_cpp"; args=(translate-mod --project "$project" --profile retro-rewind --base-manifest "$manifest" --base-translation-output-metadata "$metadata" --code-pul "$retro_root/Binaries/Code.pul" --mod-root "$retro_root" --mod-name 'Retro Rewind' --region P --out "$mod_out" --prefer-cached-inputs --emit-cpp --threads "$translator_threads")
if (( skip_retro_wfc )); then args+=(--skip-retro-wfc); else offline_payload="$retro_wfc/binary/payload.RMCPD00.bin"; assert_file "$offline_payload" 'Offline Retro-WFC payload'; args+=(--retro-wfc-payload "$offline_payload"); fi
step translate-mod 'Translating Retro Rewind'; translator "${args[@]}"
fi
step generate-data-init 'Generating local game data initialization'; translator generate-data-init --project "$project"
args=(emit-build-shards --project "$project" --base-metadata "$metadata" --base-functions-dir "$functions" --native-source-dir "$workspace/runtime/src" --out "$shards")
if (( builds_retro )); then args+=(--resolved-profile "$mod_out/resolved_dispatch_profile.json" --retro-cpp-dir "$mod_out/cpp"); fi
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"
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
printf 'MKWCBUILD:OUTPUT=%s\n' "$output_dir"
+486
View File
@@ -0,0 +1,486 @@
#!/usr/bin/env bash
# Linux build automation: translate -> emit build shards -> configure -> compile -> publish.
#
# This is the native-Linux counterpart to Launcher/LocalBuild.ps1. It is a from-scratch parallel
# implementation, not a port of NativeBuildFlags.ps1: that file's canonical flags exist only for
# the Windows/mingw toolchain's offline pinned dependencies, which this script does not use.
# Without --native-prebuilt-dir, aurora is built from source, letting its own CMake auto-detect
# Vulkan + vendor SDL3/Dawn via FetchContent - the same configuration already verified working by
# hand. With it, aurora is not compiled at all - see Launcher/Prepare-NativePrebuilt.sh, which
# harvests exactly that package (the Linux counterpart to Prepare-NativePrebuilt.ps1).
set -euo pipefail
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
log_step() {
# $1 = machine-readable step id, $2 = human sentence. Mirrors LocalBuild.ps1's
# Write-MkwBuildStep: the id is a stable marker a future installer could parse from the log,
# the sentence is for the human reading the terminal.
printf 'MKWCBUILD:STEP:%s %s\n' "$1" "$2"
}
fail() {
echo "local-build.sh: error: $*" >&2
exit 1
}
assert_file() {
[[ -f "$1" ]] || fail "$2 is missing: $1"
}
assert_dir() {
[[ -d "$1" ]] || fail "$2 is missing: $1"
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "required tool '$1' was not found on PATH (override with --$2)"
}
sha256_of() {
sha256sum "$1" | awk '{print $1}'
}
# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace=$(cd "$script_dir/.." && pwd)
profile=base
output_dir=""
base_output_dir=""
retro_rewind_package_dir=""
retro_wfc_offline_dir=""
skip_retro_wfc_payload=0
force_clean_build=0
parallel_override=0
cc_override=""
cxx_override=""
cmake_override=""
ninja_override=""
dotnet_override=""
translator_dll_override=""
translator_bin_override=""
fuse_ld_override=""
native_prebuilt_dir=""
usage() {
cat <<'EOF'
Usage: local-build.sh --output-dir DIR [options]
--workspace DIR Repository root (default: this script's parent directory)
--profile {base|retro-rewind|both} Build profile (default: base)
--output-dir DIR Where the built product is published (required)
--base-output-dir DIR Second output directory; required with --profile both
--retro-rewind-package-dir DIR Retro Rewind source tree (default: PulsarPacks/completed/RetroRewind/RetroRewind6)
--retro-wfc-offline-dir DIR Offline Retro-WFC payload directory
--skip-retro-wfc-payload Build Retro Rewind without a Retro-WFC payload
--force-clean-build Discard every translation/build cache first
--parallel N Pin translator threads, translated-shard job pool, and Ninja parallelism to N
--cc PATH / --cxx PATH C/C++ compiler (default: cc/c++ on PATH)
--fuse-ld NAME_OR_PATH Linker passed to clang as -fuse-ld=NAME_OR_PATH (default: clang's own default linker)
--cmake PATH / --ninja PATH Build tools (default: on PATH)
--dotnet PATH dotnet executable (default: on PATH)
--translator-dll PATH Pre-built Translator.Cli.dll (skips building the translator; still needs --dotnet to run it)
--translator-bin PATH Self-contained Translator.Cli executable (skips building AND needs no dotnet at all)
--native-prebuilt-dir DIR Precompiled aurora/third-party package (see Prepare-NativePrebuilt.sh);
skips compiling aurora-main from source entirely
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--workspace) workspace=$(cd "$2" && pwd); shift 2 ;;
--profile) profile=$2; shift 2 ;;
--output-dir) output_dir=$2; shift 2 ;;
--base-output-dir) base_output_dir=$2; shift 2 ;;
--retro-rewind-package-dir) retro_rewind_package_dir=$2; shift 2 ;;
--retro-wfc-offline-dir) retro_wfc_offline_dir=$2; shift 2 ;;
--skip-retro-wfc-payload) skip_retro_wfc_payload=1; shift ;;
--force-clean-build) force_clean_build=1; shift ;;
--parallel) parallel_override=$2; shift 2 ;;
--cc) cc_override=$2; shift 2 ;;
--cxx) cxx_override=$2; shift 2 ;;
--fuse-ld) fuse_ld_override=$2; shift 2 ;;
--cmake) cmake_override=$2; shift 2 ;;
--ninja) ninja_override=$2; shift 2 ;;
--dotnet) dotnet_override=$2; shift 2 ;;
--translator-dll) translator_dll_override=$2; shift 2 ;;
--translator-bin) translator_bin_override=$2; shift 2 ;;
--native-prebuilt-dir) native_prebuilt_dir=$2; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown argument: $1" ;;
esac
done
[[ -n "$output_dir" ]] || { usage; fail "--output-dir is required"; }
case "$profile" in
base|retro-rewind|both) ;;
*) fail "--profile must be base, retro-rewind, or both" ;;
esac
builds_retro=0
[[ "$profile" == "retro-rewind" || "$profile" == "both" ]] && builds_retro=1
has_offline_retro_wfc=0
[[ -n "$retro_wfc_offline_dir" ]] && has_offline_retro_wfc=1
if [[ "$builds_retro" -eq 0 ]]; then
if [[ "$has_offline_retro_wfc" -eq 1 || "$skip_retro_wfc_payload" -eq 1 ]]; then
fail "Retro-WFC payload options are valid only for a Retro Rewind build."
fi
if [[ -n "$retro_rewind_package_dir" ]]; then
fail "--retro-rewind-package-dir is valid only for a Retro Rewind build."
fi
else
if [[ "$has_offline_retro_wfc" -eq "$skip_retro_wfc_payload" ]]; then
fail "Choose exactly one Retro-WFC mode: --retro-wfc-offline-dir or --skip-retro-wfc-payload."
fi
fi
if [[ "$profile" == "both" && -z "$base_output_dir" ]]; then
fail "--base-output-dir is required with --profile both; --output-dir receives the Retro Rewind product."
fi
if [[ "$profile" != "both" && -n "$base_output_dir" ]]; then
fail "--base-output-dir is valid only with --profile both."
fi
# ---------------------------------------------------------------------------
# Tool resolution and prerequisite checks
# ---------------------------------------------------------------------------
dotnet_bin=${dotnet_override:-dotnet}
cmake_bin=${cmake_override:-cmake}
ninja_bin=${ninja_override:-ninja}
cc_bin=${cc_override:-clang}
cxx_bin=${cxx_override:-clang++}
# A self-contained --translator-bin needs no dotnet at all (it bundles its own runtime); dotnet is
# only required when the translator has to be built from source or run as a plain .dll.
if [[ -z "$translator_bin_override" ]]; then
require_command "$dotnet_bin" dotnet
fi
require_command "$cmake_bin" cmake
require_command "$ninja_bin" ninja
require_command "$cc_bin" cc
require_command "$cxx_bin" cxx
if [[ -n "$native_prebuilt_dir" ]]; then
assert_file "$native_prebuilt_dir/native_prebuilt.cmake" "Native prebuilt package"
fi
project=$workspace/projects/mkwii/recomp.yml
assets=$workspace/Assets
generated=$workspace/generated
functions=$generated/functions
base_metadata=$generated/base_translation_output.json
base_manifest_dir=$workspace/build/base
base_manifest=$base_manifest_dir/mkwii_base_manifest.json
shards=$generated/build_shards
build=$workspace/native-build
translation_provenance=$generated/translation-provenance.json
toolchain_provenance=$build/toolchain-provenance.json
retro_root=${retro_rewind_package_dir:-$workspace/PulsarPacks/completed/RetroRewind/RetroRewind6}
assert_file "$project" "Translation project"
assert_file "$assets/main.dol" "Extracted main.dol (see translator/README.md - owning the game is required)"
assert_file "$assets/StaticR.rel" "Extracted StaticR.rel (see translator/README.md - owning the game is required)"
# Literal line matching against the manifest's fixed shape, not a YAML dependency - the same
# approach NativeBuildFlags.ps1's Get-MkwProjectPins uses on Windows, kept here only for the one
# field this script actually needs from the manifest.
entry_point=$(awk '
/^translation:/ { in_translation = 1 }
in_translation && /^[[:space:]]*-[[:space:]]*0[xX][0-9a-fA-F]+[[:space:]]*$/ {
gsub(/^[[:space:]]*-[[:space:]]*/, ""); gsub(/[[:space:]]*$/, ""); print; exit
}
' "$project")
[[ -n "$entry_point" ]] || fail "Could not find a translation entry point in $project"
translator_bin=$translator_bin_override
translator_dll=$translator_dll_override
if [[ -n "$translator_bin" ]]; then
assert_file "$translator_bin" "Translator.Cli executable"
translator() { "$translator_bin" "$@"; }
else
if [[ -z "$translator_dll" ]]; then
translator_dll=$workspace/translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll
log_step build-translator "Building the translator"
"$dotnet_bin" build "$workspace/translator/src/Translator.Cli/Translator.Cli.csproj" -c Release
fi
assert_file "$translator_dll" "Translator.Cli.dll"
translator() { "$dotnet_bin" "$translator_dll" "$@"; }
fi
# ---------------------------------------------------------------------------
# Parallelism: three independent knobs, same reasoning as LocalBuild.ps1 -
# translator_threads (translation's own worker threads), translated_jobs (the real RAM guard,
# capping concurrent compiles of memory-hungry translated TUs via Ninja's MKW_TRANSLATED_COMPILE_JOBS
# pool), global_jobs (Ninja's overall parallelism). --parallel pins all three.
# ---------------------------------------------------------------------------
cpu_count=$(nproc)
mem_gib=$(( $(awk '/^MemTotal:/{print $2}' /proc/meminfo) / 1024 / 1024 ))
(( mem_gib < 1 )) && mem_gib=1
if (( parallel_override > 0 )); then
translator_threads=$parallel_override
translated_jobs=$parallel_override
global_jobs=$parallel_override
else
translator_threads=$(( cpu_count < 16 ? cpu_count : 16 ))
(( translator_threads < 1 )) && translator_threads=1
mem_based_cap=$(( mem_gib / 2 ))
(( mem_based_cap < 1 )) && mem_based_cap=1
translated_jobs=$(( cpu_count < mem_based_cap ? cpu_count : mem_based_cap ))
(( translated_jobs < 1 )) && translated_jobs=1
global_jobs=$(( translated_jobs > cpu_count ? translated_jobs : cpu_count ))
fi
# ---------------------------------------------------------------------------
# Translation cache: this script is the only owner of the reuse decision (unlike LocalBuild.ps1,
# which is handed caller-computed fingerprints by the Windows installer - there is no Linux
# installer yet to supply anything). Hash the game inputs the translation actually depends on;
# a match plus every expected output file present means the previous translation is still good.
# ---------------------------------------------------------------------------
if (( force_clean_build )); then
log_step force-clean "A clean build was requested; discarding every translation and build cache"
rm -rf "$generated" "$base_manifest_dir" "$build"
fi
translation_fingerprint=$(cat "$assets/main.dol" "$assets/StaticR.rel" "$project" | sha256sum | awk '{print $1}')
reuse_base=0
if [[ -f "$translation_provenance" ]]; then
recorded=$(grep -o '"TranslationFingerprint" *: *"[^"]*"' "$translation_provenance" 2>/dev/null | sed 's/.*"\([0-9a-f]*\)"$/\1/' || true)
if [[ "$recorded" == "$translation_fingerprint" && -f "$base_metadata" && -f "$base_manifest" ]]; then
reuse_base=1
fi
fi
if (( builds_retro )); then
# The translator discovers the mod through the project file's workspace-relative profile
# paths, and both the base and mod leg block leaf inlining at every address the profile
# patches - so the selected Code.pul must sit at the profile's mod_root before either leg runs.
source_pul=$retro_root/Binaries/Code.pul
assert_file "$source_pul" "Retro Rewind Code.pul"
staged_binaries=$workspace/PulsarPacks/completed/RetroRewind/RetroRewind6/Binaries
mkdir -p "$staged_binaries"
staged_pul=$staged_binaries/Code.pul
if [[ "$(cd "$(dirname "$source_pul")" && pwd)/$(basename "$source_pul")" != "$(cd "$(dirname "$staged_pul")" && pwd)/$(basename "$staged_pul")" ]]; then
cp -f "$source_pul" "$staged_pul"
fi
fi
if (( reuse_base )) && (( builds_retro )); then
# A base tree that never saw this Code.pul would silently bake vanilla code into the modded
# product - check-base-mod-awareness fails closed (anything but exit 0 forces a retranslation).
retro_code_pul=$retro_root/Binaries/Code.pul
assert_file "$retro_code_pul" "Retro Rewind Code.pul"
pul_sha=$(sha256_of "$retro_code_pul")
if ! grep -q "\"codePulSha256\":\"$pul_sha\"" "$base_metadata"; then
if ! translator check-base-mod-awareness --project "$project" --profile retro-rewind \
--translation-output-metadata "$base_metadata" --code-pul "$retro_code_pul"; then
log_step retranslate-base "The base translation is stale; retranslating the base game for the new Code.pul"
reuse_base=0
fi
fi
fi
if (( reuse_base )); then
log_step reuse-base-translation "Reusing the completed base translation"
else
rm -f "$translation_provenance"
mkdir -p "$generated" "$base_manifest_dir"
log_step translate-base "Translating the user-owned base game"
translator translate-recursive "$entry_point" --project "$project" \
--outdir "$functions" --output-metadata "$base_metadata" \
--production-source-bundle "$generated/base_translation_sources.bin" \
--no-function-files --prune-stale --threads "$translator_threads"
log_step emit-base-manifest "Creating the local base translation manifest"
translator emit-base-manifest --project "$project" --out "$base_manifest_dir" \
--functions-dir "$functions" --translation-output-metadata "$base_metadata" --region P
printf '{"SchemaVersion":1,"TranslationFingerprint":"%s"}' "$translation_fingerprint" \
> "$translation_provenance"
fi
if (( builds_retro )); then
code_pul=$retro_root/Binaries/Code.pul
assert_file "$code_pul" "Retro Rewind Code.pul"
retro_out=$workspace/build/mods/retro_rewind_full_cpp
translate_mod_args=(translate-mod --project "$project" --profile retro-rewind
--base-manifest "$base_manifest" --base-translation-output-metadata "$base_metadata"
--code-pul "$code_pul" --mod-root "$retro_root" --mod-name "Retro Rewind"
--region P --out "$retro_out" --prefer-cached-inputs --emit-cpp
--threads "$translator_threads")
if (( skip_retro_wfc_payload )); then
translate_mod_args+=(--skip-retro-wfc)
else
offline_payload=$retro_wfc_offline_dir/binary/payload.RMCPD00.bin
assert_file "$offline_payload" "Offline Retro-WFC shared payload"
translate_mod_args+=(--retro-wfc-payload "$offline_payload")
fi
log_step translate-mod "Translating the selected Retro Rewind Code.pul"
translator "${translate_mod_args[@]}"
fi
log_step generate-data-init "Generating local game data initialization"
translator generate-data-init --project "$project"
shard_args=(emit-build-shards --project "$project" --base-metadata "$base_metadata"
--base-functions-dir "$functions" --native-source-dir "$workspace/runtime/src" --out "$shards")
if (( builds_retro )); then
retro_out=$workspace/build/mods/retro_rewind_full_cpp
shard_args+=(--resolved-profile "$retro_out/resolved_dispatch_profile.json"
--retro-cpp-dir "$retro_out/cpp")
fi
log_step emit-build-shards "Preparing local native build shards"
translator "${shard_args[@]}"
# ---------------------------------------------------------------------------
# Native configure + build. Deliberately not passing -DAURORA_DAWN_PROVIDER=package or
# -DFETCHCONTENT_FULLY_DISCONNECTED=ON: those exist for the Windows prebuilt-package/offline-
# dependencies workflow this script does not build. aurora's own CMake auto-detects Linux and
# picks Vulkan + vendors SDL3/Dawn via FetchContent, exactly as already verified working by hand.
# ---------------------------------------------------------------------------
keep_native_build=0
stale_reason=""
if [[ -f "$build/CMakeCache.txt" ]]; then
expected_home=$workspace/runtime
cache_home=$(grep '^CMAKE_HOME_DIRECTORY:INTERNAL=' "$build/CMakeCache.txt" | cut -d= -f2- || true)
if [[ -n "$cache_home" && "$(cd "$cache_home" 2>/dev/null && pwd)" == "$expected_home" ]]; then
keep_native_build=1
# CMake auto-detects and caches auxiliary tool paths (CMAKE_AR/RANLIB/LINKER/ASM_COMPILER
# and their per-language *_AR/*_RANLIB variants) only once, the first time a language's
# compiler is checked - unlike CMAKE_C_COMPILER/CMAKE_CXX_COMPILER, which get overwritten by
# the -D flags below on every configure, these are never refreshed on a plain reconfigure.
# An AppImage's own AppRun works around this at the source by keeping --cc/--cxx/--cmake/
# --ninja pointed at a stable symlink it re-targets at the current mount every launch
# (see build-appimage.sh), so the *path string* CMake caches never actually changes run to
# run - but this check stays as a general fallback for any tool path that goes stale some
# other way (a moved/removed system toolchain, a relocated portable-tools directory, etc):
# any :FILEPATH= cache entry whose recorded path no longer exists means this cache belongs
# to a toolchain location that's gone - not just a workspace/path mismatch.
while IFS= read -r tool_path; do
[[ -n "$tool_path" ]] || continue
# CMake's own sentinel for "this optional tool was legitimately never found" (e.g.
# clang-scan-deps, not required here) - not a path at all, and not a sign of anything
# stale. Without this exclusion, every configure wiped the cache unconditionally.
[[ "$tool_path" != *-NOTFOUND ]] || continue
if [[ ! -e "$tool_path" ]]; then
keep_native_build=0
stale_reason=" (cached tool path no longer exists: $tool_path)"
break
fi
done < <(grep -o ':FILEPATH=.*' "$build/CMakeCache.txt" | sed 's/^:FILEPATH=//')
fi
fi
if [[ -d "$build" && "$keep_native_build" -eq 0 ]]; then
echo "MKWCBUILD: The native build cache does not belong to this workspace path or toolchain; rebuilding from scratch$stale_reason"
rm -rf "$build"
elif [[ "$keep_native_build" -eq 1 ]]; then
echo "MKWCBUILD: Reusing the incremental native build directory"
fi
configure_args=(-S "$workspace/runtime" -B "$build" -G Ninja
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_C_COMPILER="$cc_bin" -DCMAKE_CXX_COMPILER="$cxx_bin"
-DCMAKE_MAKE_PROGRAM="$ninja_bin"
-DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs")
# CMAKE_C_COMPILER/CXX_COMPILER stay stable across AppImage runs on their own (they're exactly
# what's passed via -D above, and AppRun points --cc/--cxx at a symlink it re-targets at the
# current mount every launch - see build-appimage.sh). CMAKE_AR/RANLIB/LINKER/ASM_COMPILER do NOT
# inherit that stability just because $cc_bin does: verified directly that even with a stable
# --cc symlink, CMake's own auto-detection of these still resolved to the *real*, ephemeral mount
# path underneath (clang's own driver locates its sibling llvm-ar/ld.lld tools by resolving its own
# invoked path, symlinks included, rather than trusting the symlink CMake invoked it through) -
# each subsequent run's differing command line then made Ninja rebuild every object from scratch
# even though nothing had actually changed. Deriving these from $cc_bin (itself already stable)
# and re-passing them explicitly every configure keeps them pinned to the same stable value too.
if [[ "$cc_bin" == */* ]]; then
toolchain_bin=$(dirname "$cc_bin")
[[ -x "$toolchain_bin/llvm-ar" ]] && configure_args+=(-DCMAKE_AR="$toolchain_bin/llvm-ar" -DCMAKE_ASM_COMPILER_AR="$toolchain_bin/llvm-ar" -DCMAKE_C_COMPILER_AR="$toolchain_bin/llvm-ar" -DCMAKE_CXX_COMPILER_AR="$toolchain_bin/llvm-ar")
[[ -x "$toolchain_bin/llvm-ranlib" ]] && configure_args+=(-DCMAKE_RANLIB="$toolchain_bin/llvm-ranlib" -DCMAKE_ASM_COMPILER_RANLIB="$toolchain_bin/llvm-ranlib" -DCMAKE_C_COMPILER_RANLIB="$toolchain_bin/llvm-ranlib" -DCMAKE_CXX_COMPILER_RANLIB="$toolchain_bin/llvm-ranlib")
[[ -x "$toolchain_bin/ld.lld" ]] && configure_args+=(-DCMAKE_LINKER="$toolchain_bin/ld.lld")
configure_args+=(-DCMAKE_ASM_COMPILER="$cc_bin")
fi
if [[ -n "$fuse_ld_override" ]]; then
configure_args+=(-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=$fuse_ld_override")
fi
if [[ -n "$native_prebuilt_dir" ]]; then
configure_args+=(-DMKW_NATIVE_PREBUILT_DIR="$native_prebuilt_dir")
fi
log_step configure-native "Configuring the native toolchain"
"$cmake_bin" "${configure_args[@]}"
case "$profile" in
base) targets=(WiiCompiled) ;;
retro-rewind) targets=(RetroRewind) ;;
both) targets=(WiiCompiled RetroRewind) ;;
esac
build_args=(--build "$build")
for target in "${targets[@]}"; do build_args+=(--target "$target"); done
build_args+=(--parallel "$global_jobs")
log_step compile "Compiling ${targets[*]} locally"
"$cmake_bin" "${build_args[@]}"
# ---------------------------------------------------------------------------
# Publish: the Linux build statically links SDL3/Dawn/etc (verified this session), so unlike
# LocalBuild.ps1's DLL-copying dance there is nothing to copy beside the binary except the
# runtime's own first-run assets.
# ---------------------------------------------------------------------------
dol_sha=$(sha256_of "$assets/main.dol")
rel_sha=$(sha256_of "$assets/StaticR.rel")
compiler_version=$("$cxx_bin" --version | head -1)
publish_built_product() {
local target=$1 destination=$2 provenance_profile=$3
mkdir -p "$destination"
local exe=$build/$target
assert_file "$exe" "Locally compiled game executable"
cp -f "$exe" "$destination/$target"
for name in dsp_coef.bin initial_pipeline_cache.db; do
[[ -f "$build/$name" ]] && cp -f "$build/$name" "$destination/"
done
[[ -d "$build/wii_bootstrap" ]] && cp -rf "$build/wii_bootstrap" "$destination/"
local is_retro=0 code_pul_sha=null
if [[ "$provenance_profile" == "retro-rewind" ]]; then
is_retro=1
code_pul_sha=\"$(sha256_of "$retro_root/Binaries/Code.pul")\"
fi
local built_utc
built_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)
cat > "$destination/local-build.json" <<JSON
{
"SchemaVersion": 1,
"Profile": "$provenance_profile",
"BuiltUtc": "$built_utc",
"DolSha256": "$dol_sha",
"RelSha256": "$rel_sha",
"CodePulSha256": $code_pul_sha,
"Compiler": "$compiler_version"
}
JSON
}
case "$profile" in
both)
publish_built_product WiiCompiled "$base_output_dir" base
publish_built_product RetroRewind "$output_dir" retro-rewind
;;
retro-rewind)
publish_built_product RetroRewind "$output_dir" retro-rewind
;;
base)
publish_built_product WiiCompiled "$output_dir" base
;;
esac
echo "MKWCBUILD:OUTPUT=$output_dir"
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
# Maintainer release builder. It packages setup/source/tooling only -- never a
# translated executable, extracted DATA tree, disc image, or Retro Rewind data.
set -euo pipefail
fail() { printf 'build-setup-pkg.command: error: %s\n' "$*" >&2; exit 1; }
# Release payloads must not inherit Finder metadata, resource forks, or a
# downloaded-file quarantine bit from a maintainer's working volume.
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]
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.
--workspace DIR Repository root (default: script's grandparent)
--version VERSION Bundle/package version (default: 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=""
while (($#)); do
case "$1" in
--workspace) workspace=${2:-}; shift 2 ;;
--nodtool) nodtool=${2:-}; shift 2 ;;
--translator) translator=${2:-}; shift 2 ;;
--cmake-root) cmake_root=${2:-}; shift 2 ;;
--ninja) ninja=${2:-}; shift 2 ;;
--output) output=${2:-}; shift 2 ;;
--version) version=${2:-}; shift 2 ;;
--installer-identity) identity=${2:-}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown option: $1" ;;
esac
done
version=${version#v}
[[ "$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'
[[ -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'
workspace=$(cd "$workspace" && pwd); output=$(cd "$(dirname "$output")" && pwd)/$(basename "$output")
stage=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-pkg.XXXXXX")
trap 'rm -rf "$stage"' EXIT
app="$stage/root/Applications/WiiCompiled Setup.app"
resources="$app/Contents/Resources"
mkdir -p "$app/Contents/MacOS" "$resources/tools"
cat > "$app/Contents/Info.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleExecutable</key><string>WiiCompiledSetup</string>
<key>CFBundleIdentifier</key><string>org.wiicompiled.setup</string>
<key>CFBundleName</key><string>WiiCompiled Setup</string>
<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>
</dict></plist>
EOF
cat > "$app/Contents/MacOS/WiiCompiledSetup" <<'EOF'
#!/usr/bin/env bash
resources="$(cd "$(dirname "$0")/../Resources" && pwd)"
# Finder launches an app with no terminal attached. The setup work deliberately
# writes human-readable build progress to stdout, so run its .command entry
# point in Terminal instead of discarding that output behind an inert app icon.
exec /usr/bin/osascript - "$resources/setup.command" "$@" <<'APPLESCRIPT'
on run argv
set commandLine to quoted form of (item 1 of argv)
if (count of argv) > 1 then
repeat with argumentIndex from 2 to (count of argv)
set commandLine to commandLine & " " & quoted form of (item argumentIndex of argv)
end repeat
end if
tell application "Terminal"
activate
do script commandLine
end tell
end run
APPLESCRIPT
EOF
chmod +x "$app/Contents/MacOS/WiiCompiledSetup"
copy_clean "$script_dir/setup.command" "$resources/setup.command"; chmod +x "$resources/setup.command"
# Copy only the build inputs. This deliberately avoids a maintainer's ignored
# output directories, local disc extraction, and developer-only packaging.
mkdir -p "$resources/workspace"
for source in aurora-main projects runtime translator; do
[[ -d "$workspace/$source" ]] || fail "required workspace directory is missing: $source"
copy_clean "$workspace/$source" "$resources/workspace/$source"
done
mkdir -p "$resources/workspace/Launcher/macos"
copy_clean "$workspace/Launcher/local-build-macos.command" "$resources/workspace/Launcher/local-build-macos.command"
copy_clean "$workspace/Launcher/macos/extract-disc.command" "$resources/workspace/Launcher/macos/extract-disc.command"
copy_clean "$workspace/Launcher/macos/publish-app.command" "$resources/workspace/Launcher/macos/publish-app.command"
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"
copy_clean "$cmake_root" "$resources/tools/cmake"
copy_clean "$ninja" "$resources/tools/ninja"; chmod +x "$resources/tools/ninja"
copy_clean "$workspace/LICENSE" "$resources/LICENSE"
copy_clean "$workspace/THIRD-PARTY-NOTICES.md" "$resources/THIRD-PARTY-NOTICES.md"
codesign --force --deep --sign - "$app"
pkg="$stage/WiiCompiled-Setup-unsigned.pkg"
DITTONORSRC=1 COPYFILE_DISABLE=1 pkgbuild --root "$stage/root" --identifier org.wiicompiled.setup --version "$version" --install-location / "$pkg"
if [[ -n "$identity" ]]; then productbuild --sign "$identity" --package "$pkg" "$output"; else ditto "$pkg" "$output"; fi
printf 'Created game-code-free package: %s\n' "$output"
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Extract a user-owned Mario Kart Wii PAL (RMCP01) disc image for a local build.
# This script deliberately contains no game data and is intended for the macOS
# setup application and for maintainers testing that setup path.
set -euo pipefail
readonly EXPECTED_DOL_SHA256=80d18895b39c63bd80f457398bfcbb91b7d16ac116a41a88967e954080155b05
readonly EXPECTED_REL_SHA256=16d9d146112541fefea701ecb5bc1a496f9d50e4a752fbb5b6778e7c6399f67d
fail() { printf 'extract-disc.command: error: %s\n' "$*" >&2; exit 1; }
sha256() { shasum -a 256 "$1" | awk '{ print $1 }'; }
usage() {
cat <<'EOF'
Usage: extract-disc.command --game IMAGE --assets-dir DIR --nodtool PATH
Extracts a user-owned Mario Kart Wii PAL RMCP01 image into DIR. The final
layout is DIR/main.dol, DIR/StaticR.rel, and DIR/DATA. Existing data is left
untouched unless the complete new extraction passes both content hash checks.
EOF
}
game=""
assets_dir=""
nodtool=""
while (($#)); do
case "$1" in
--game) game=${2:-}; shift 2 ;;
--assets-dir) assets_dir=${2:-}; shift 2 ;;
--nodtool) nodtool=${2:-}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown option: $1" ;;
esac
done
[[ -f "$game" ]] || fail "disc image does not exist: $game"
[[ -n "$assets_dir" ]] || fail "--assets-dir is required"
[[ -x "$nodtool" ]] || fail "nodtool is not executable: $nodtool"
assets_dir=$(mkdir -p "$assets_dir" && cd "$assets_dir" && pwd)
scratch=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-disc.XXXXXX")
cleanup() { rm -rf "$scratch"; }
trap cleanup EXIT
printf 'MKWCBUILD:STEP:validate-disc Checking the selected disc image\n'
"$nodtool" info "$game" >/dev/null
printf 'MKWCBUILD:STEP:extract-disc Extracting the user-owned disc image\n'
"$nodtool" extract "$game" "$scratch/extracted"
dol=$(find "$scratch/extracted" -type f -path '*/sys/main.dol' -print -quit)
rel=$(find "$scratch/extracted" -type f -path '*/files/rel/StaticR.rel' -print -quit)
[[ -n "$dol" ]] || fail 'nodtool extraction did not contain sys/main.dol'
[[ -n "$rel" ]] || fail 'nodtool extraction did not contain files/rel/StaticR.rel'
[[ $(sha256 "$dol") == "$EXPECTED_DOL_SHA256" ]] || fail 'disc is not the supported clean PAL RMCP01 Mario Kart Wii image'
[[ $(sha256 "$rel") == "$EXPECTED_REL_SHA256" ]] || fail 'disc has an unexpected StaticR.rel; use a clean PAL RMCP01 image'
data_root=$(dirname "$(dirname "$dol")")
[[ -d "$data_root/files" ]] || fail 'nodtool extraction did not contain the Wii files directory'
# Stage beside the destination so the final replacement stays on one volume.
stage="$assets_dir/.extract-stage-$$"
rm -rf "$stage"
mkdir -p "$stage"
ditto "$data_root" "$stage/DATA"
ditto "$dol" "$stage/main.dol"
ditto "$rel" "$stage/StaticR.rel"
backup="$assets_dir/.previous-extraction-$(date +%Y%m%d-%H%M%S)"
if [[ -e "$assets_dir/DATA" || -e "$assets_dir/main.dol" || -e "$assets_dir/StaticR.rel" ]]; then
mkdir -p "$backup"
for item in DATA main.dol StaticR.rel; do
[[ -e "$assets_dir/$item" ]] && mv "$assets_dir/$item" "$backup/$item"
done
fi
mv "$stage/DATA" "$assets_dir/DATA"
mv "$stage/main.dol" "$assets_dir/main.dol"
mv "$stage/StaticR.rel" "$assets_dir/StaticR.rel"
rmdir "$stage"
rm -rf "$backup"
printf 'MKWCBUILD:STEP:disc-ready Verified and extracted clean PAL RMCP01 game assets\n'
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# Turn one locally compiled macOS product into a self-contained .app bundle.
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
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.
EOF
}
build_dir=""; product=""; output_dir=""
while (($#)); do
case "$1" in
--build-dir) build_dir=${2:-}; shift 2 ;;
--product) product=${2:-}; shift 2 ;;
--output-dir) output_dir=${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
[[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product"
for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done
app="$output_dir/$product.app"
macos="$app/Contents/MacOS"
frameworks="$app/Contents/Frameworks"
resources="$app/Contents/Resources"
rm -rf "$app"
mkdir -p "$macos" "$frameworks" "$resources"
cat > "$app/Contents/Info.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleDevelopmentRegion</key><string>en</string>
<key>CFBundleExecutable</key><string>$product</string>
<key>CFBundleIdentifier</key><string>org.wiicompiled.$product</string>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
<key>CFBundleName</key><string>$product</string>
<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>NSHighResolutionCapable</key><true/>
</dict></plist>
EOF
ditto "$build_dir/$product" "$macos/$product"
for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do
ditto "$build_dir/$asset" "$resources/$asset"
ln -s "../Resources/$asset" "$macos/$asset"
done
# Build a closure of Homebrew dylibs. System libraries remain system references.
queue=("$macos/$product")
while ((${#queue[@]})); do
current=${queue[0]}
queue=("${queue[@]:1}")
while IFS= read -r dependency; do
[[ "$dependency" == /opt/homebrew/* || "$dependency" == /usr/local/* ]] || continue
[[ -f "$dependency" ]] || continue
name=$(basename "$dependency")
if [[ ! -f "$frameworks/$name" ]]; then
ditto "$dependency" "$frameworks/$name"
install_name_tool -id "@rpath/$name" "$frameworks/$name"
queue+=("$frameworks/$name")
fi
done < <(otool -L "$current" | tail -n +2 | awk '{print $1}')
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
install_name_tool -change "$old" "@executable_path/../Frameworks/$name" "$binary"
else
install_name_tool -change "$old" "@loader_path/$name" "$binary"
fi
done < <(otool -L "$binary" | tail -n +2 | awk '{print $1}')
done < <(find "$frameworks" -type f -print; printf '%s\n' "$macos/$product")
find "$frameworks" -type f -exec codesign --force --sign - {} +
codesign --force --deep --sign - "$app"
codesign --verify --deep --strict "$app"
printf 'MKWCBUILD:APP=%s\n' "$app"
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
# Entry point bundled in WiiCompiled Setup.app. The package contains source and
# tools only; a user's own verified disc is extracted into Application Support.
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"
fail() { printf 'WiiCompiled Setup: %s\n' "$*" >&2; exit 1; }
notice() { /usr/bin/osascript -e "display dialog \"${1//\"/\\\"}\" buttons {\"OK\"} default button \"OK\" with icon caution" >/dev/null; }
usage() {
cat <<'EOF'
Usage: setup.command --game IMAGE [--retro-dir DIR] [--install-location {user|applications}]
Without arguments this script opens file pickers. It is normally launched by
WiiCompiled Setup.app, not run directly.
EOF
}
game=""; retro_dir=""; install_location=applications
while (($#)); do
case "$1" in
--game) game=${2:-}; shift 2 ;;
--retro-dir) retro_dir=${2:-}; shift 2 ;;
--install-location) install_location=${2:-}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown option: $1" ;;
esac
done
[[ "$install_location" == user || "$install_location" == applications ]] || fail '--install-location must be user or applications'
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"
POSIX path of selectedFile
APPLESCRIPT
) || exit 0
choice=$(/usr/bin/osascript -e 'button returned of (display dialog "Would you like to build Retro Rewind too?" buttons {"Base game only", "Choose Retro Rewind folder"} default button "Base game only")')
if [[ "$choice" == 'Choose Retro Rewind folder' ]]; then
retro_dir=$(/usr/bin/osascript <<'APPLESCRIPT'
set selectedFolder to choose folder with prompt "Choose the RetroRewind6 folder (or its parent folder)"
POSIX path of selectedFolder
APPLESCRIPT
) || exit 0
fi
fi
[[ -x "$nodtool" ]] || fail 'the packaged nodtool is missing or not executable'
[[ -x "$translator" ]] || fail 'the packaged Translator.Cli is missing or not executable'
[[ -x "$cmake_bin" && -x "$ninja_bin" ]] || fail 'the packaged CMake or Ninja tool is missing'
if ! /usr/bin/xcode-select -p >/dev/null 2>&1; then
notice 'Xcode Command Line Tools are required once to compile WiiCompiled. Click OK, complete the Apple installer, then run WiiCompiled Setup again.'
/usr/bin/xcode-select --install || true
exit 1
fi
mkdir -p "$support_root" "$products"
if [[ ! -d "$workspace/.git" && ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then
printf 'Preparing the local build workspace...\n'
rm -rf "$workspace"
/usr/bin/ditto "$workspace_source" "$workspace"
fi
profile=base
build_args=(--workspace "$workspace" --game "$game" --nodtool "$nodtool" --output-dir "$products")
if [[ -n "$retro_dir" ]]; then
profile=both
# Online play needs the shared Retro-WFC payload. Keep it in the per-user
# support directory rather than the packaged app or build workspace, then
# verify its pinned signature before publishing it into the local cache.
retro_wfc_dir="$support_root/RetroWfcPayload"
retro_wfc_payload="$retro_wfc_dir/binary/payload.RMCPD00.bin"
if [[ -f "$retro_wfc_payload" ]] && ! "$translator" validate-retro-wfc-payload --directory "$retro_wfc_dir"; then
printf 'Discarding an invalid cached Retro-WFC payload...\n' >&2
rm -f "$retro_wfc_payload"
fi
if [[ ! -f "$retro_wfc_payload" ]]; then
printf 'Downloading the Retro-WFC payload needed for online play...\n'
mkdir -p "$retro_wfc_dir"
payload_stage=$(mktemp -d "$retro_wfc_dir/.payload-download.XXXXXX")
temporary_payload="$payload_stage/binary/payload.RMCPD00.bin"
mkdir -p "$(dirname "$temporary_payload")"
trap 'rm -rf "$payload_stage"' EXIT
/usr/bin/curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \
--retry 1 --output "$temporary_payload" \
'http://nas.play.rwfc.net/payload?g=RMCPD00' || fail 'could not download the Retro-WFC payload needed for online play'
"$translator" validate-retro-wfc-payload --directory "$payload_stage" || \
fail 'downloaded Retro-WFC payload failed signature validation'
mkdir -p "$retro_wfc_dir/binary"
mv "$temporary_payload" "$retro_wfc_payload"
rmdir "$payload_stage/binary" "$payload_stage"
trap - EXIT
fi
build_args+=(--profile both --base-output-dir "$products" --retro-rewind-package-dir "$retro_dir" --retro-wfc-offline-dir "$retro_wfc_dir")
fi
"$workspace/Launcher/local-build-macos.command" "${build_args[@]}" --profile "$profile" --cmake "$cmake_bin" --ninja "$ninja_bin" --translator-bin "$translator"
config="$support_root/Config.toml"
toml_string() { printf '%s' "$1" | sed -e 's/\\\\/\\\\\\\\/g' -e 's/"/\\\\"/g'; }
set_path() {
local key=$1 value=$2 encoded line temporary
encoded=$(toml_string "$value"); line="$key = \"$encoded\""; temporary="$config.tmp"
touch "$config"
if grep -q '^[[:space:]]*\[paths\][[:space:]]*$' "$config"; then
awk -v key="$key" -v line="$line" '
/^[[:space:]]*\[paths\][[:space:]]*$/ { print; print line; inside = 1; next }
inside && /^[[:space:]]*\[/ { inside = 0 }
inside && $0 ~ "^[[:space:]]*" key "[[:space:]]*=" { next }
{ print }
' "$config" > "$temporary"
else
{ cat "$config"; printf '\n[paths]\n%s\n' "$line"; } > "$temporary"
fi
mv "$temporary" "$config"
}
set_path dvd_root "$workspace/Assets/DATA"
[[ -n "$retro_dir" ]] && set_path retro_rewind_root "$retro_dir"
destination="$HOME/Applications"
if [[ "$install_location" == applications ]]; then destination=/Applications; fi
install_app() {
local app=$1
[[ -d "$products/$app" ]] || return 0
if [[ "$destination" == /Applications ]]; then
command="mkdir -p /Applications && rm -rf '/Applications/$app' && ditto '$products/$app' '/Applications/$app'"
/usr/bin/osascript -e "do shell script \"$command\" with administrator privileges"
else
mkdir -p "$destination"; rm -rf "$destination/$app"; /usr/bin/ditto "$products/$app" "$destination/$app"
fi
}
install_app WiiCompiled.app
[[ "$profile" == both ]] && install_app RetroRewind.app
notice "Installation complete. Your apps are in $destination."
+237
View File
@@ -0,0 +1,237 @@
#!/usr/bin/env bash
# Prepares a self-contained native-Linux build toolchain bundled into the AppImage by
# build-appimage.sh, so a user needs no `clang`/`cmake`/`ninja` of their own to build the
# translated game (mirrors why Windows bundles llvm-mingw + CMake + Ninja via
# Prepare-PortableTools.ps1 - this is that script's Linux counterpart, for the same reason).
#
# clang/lld/llvm-ar: pruned from the official llvm.org GitHub release tarball (NOT llvm-mingw -
# that project targets Windows/mingw, never native Linux) down to just what's needed to compile
# and link: clang, lld, llvm-ar, the clang resource dir (builtin headers + compiler-rt), and
# libc++/libc++abi/libunwind (so the toolchain never has to fall back to the host's system
# libstdc++ headers). The raw release is ~1.9 GiB per arch (every LLVM backend, mlir, flang, lldb,
# docs, tests); pruned it is ~500 MiB uncompressed / ~100 MiB compressed, verified against a real
# build of this project.
#
# cmake: pruned from the official Kitware GitHub release tarball down to bin/cmake (not
# ccmake/cmake-gui/cpack/ctest, which local-build.sh never invokes) plus the Modules/Templates
# CMake needs at runtime (found relative to bin/cmake via CMAKE_ROOT auto-detection - Help/doc/man/
# the desktop-integration files under share/ are documentation/GUI-only and dropped). Verified with
# a real configure+build using the pruned cmake+ninja+clang together.
#
# ninja: the official ninja-build GitHub release zip, used as-is - it is already a single small
# (~130 KiB compressed) static-ish binary with nothing to prune.
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace=$(cd "$script_dir/.." && pwd)
llvm_version=23.1.0
cmake_version=4.3.3
ninja_version=1.13.2
destination="$script_dir/artifacts/portable-tools"
arch=""
usage() {
cat <<'EOF'
Usage: prepare-portable-tools.sh --arch {x86_64|aarch64} [--destination DIR]
--arch ARCH Target architecture (required)
--destination DIR Where the toolchain is written, as DIR/toolchain-ARCH
(default: Launcher/artifacts/portable-tools)
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--arch) arch=$2; shift 2 ;;
--destination) destination=$2; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "prepare-portable-tools.sh: unknown argument: $1" >&2; exit 1 ;;
esac
done
case "$arch" in
x86_64) llvm_release_arch=X64; target_triple=x86_64-unknown-linux-gnu
llvm_release_sha256=18da30f77f475688a18f7704d23f9f155ae007ed9922dbed6850a9419d9fec8c
cmake_release_arch=x86_64
cmake_sha256=927b2368a946c37269c3a66225ab00544e756459cdd0b5d0da438694fb9ff802
ninja_asset=ninja-linux.zip
ninja_sha256=5749cbc4e668273514150a80e387a957f933c6ed3f5f11e03fb30955e2bbead6 ;;
aarch64) llvm_release_arch=ARM64; target_triple=aarch64-unknown-linux-gnu
llvm_release_sha256=cfb31bfc713ef453248bf5bd026312f838ad6c52c25623e987cb6a340f3050d4
cmake_release_arch=aarch64
cmake_sha256=9ea38356dbd3e32e51029a3e09a0f2f8e117ef4fbcaad7a21ffb36409bbd5cb4
ninja_asset=ninja-linux-aarch64.zip
ninja_sha256=fd2cacc8050a7f12a16a2e48f9e06fca5c14fc4c2bee2babb67b58be17a607fc ;;
*) echo "prepare-portable-tools.sh: --arch must be x86_64 or aarch64" >&2; usage; exit 1 ;;
esac
destination=$(mkdir -p "$destination" && cd "$destination" && pwd)
toolchain_dir="$destination/toolchain-$arch"
downloads="$script_dir/artifacts/downloads"
mkdir -p "$downloads"
sha256_of() { sha256sum "$1" | awk '{print $1}'; }
download_verified() {
# $1 = destination path, $2 = URL, $3 = expected sha256
local dest=$1 url=$2 expected=$3
if [[ -f "$dest" ]] && [[ "$(sha256_of "$dest")" == "$expected" ]]; then return; fi
echo "prepare-portable-tools.sh: downloading $(basename "$dest")..."
local tmp="$dest.partial"
rm -f "$tmp"
curl -fL --progress-bar -o "$tmp" "$url"
local actual
actual=$(sha256_of "$tmp")
if [[ "$actual" != "$expected" ]]; then
echo "prepare-portable-tools.sh: $(basename "$dest") hash mismatch: expected $expected, got $actual" >&2
rm -f "$tmp"
exit 1
fi
mv "$tmp" "$dest"
}
if [[ -x "$toolchain_dir/bin/clang" && -x "$toolchain_dir/bin/ninja" && -x "$toolchain_dir/bin/cmake" ]]; then
echo "prepare-portable-tools.sh: reusing existing toolchain at $toolchain_dir"
exit 0
fi
work="$destination/.building-toolchain-$arch"
rm -rf "$work"
mkdir -p "$work/bin" "$work/lib/$target_triple" "$work/include/$target_triple/c++/v1"
# --- clang/lld/llvm-ar, pruned from the official LLVM release ---
llvm_archive_name="LLVM-$llvm_version-Linux-$llvm_release_arch.tar.xz"
llvm_archive="$downloads/$llvm_archive_name"
download_verified "$llvm_archive" \
"https://github.com/llvm/llvm-project/releases/download/llvmorg-$llvm_version/$llvm_archive_name" \
"$llvm_release_sha256"
extract_root="$script_dir/artifacts/.extract-clang-$arch"
rm -rf "$extract_root"
mkdir -p "$extract_root"
echo "prepare-portable-tools.sh: extracting $llvm_archive_name (this is the full ~1.9 GiB release; only a fraction is kept)..."
tar -xf "$llvm_archive" -C "$extract_root"
src="$extract_root/LLVM-$llvm_version-Linux-$llvm_release_arch"
[[ -d "$src" ]] || { echo "prepare-portable-tools.sh: unexpected archive layout, expected $src" >&2; exit 1; }
echo "prepare-portable-tools.sh: pruning to the minimal compile+link toolchain..."
# clang: the real driver executable plus the clang/clang++ symlinks CMake/local-build.sh invoke.
# Stripped: debug symbols are dead weight for a bundled compiler nobody will debug.
cp -a "$src/bin/clang-23" "$work/bin/"
strip "$work/bin/clang-23"
ln -s clang-23 "$work/bin/clang"
ln -s clang "$work/bin/clang++"
# lld: linked via -fuse-ld=lld, which clang resolves by looking for ld.lld next to itself first -
# see local-build.sh's --fuse-ld option.
cp -a "$src/bin/lld" "$work/bin/"
strip "$work/bin/lld"
ln -s lld "$work/bin/ld.lld"
# llvm-ar/llvm-ranlib: CMake's archiver for the many static libraries this project builds
# (aurora, Crypto++, SDL3, Dawn's dependency closure, the translated game shards).
cp -a "$src/bin/llvm-ar" "$work/bin/"
strip "$work/bin/llvm-ar"
ln -s llvm-ar "$work/bin/llvm-ranlib"
# Clang's resource directory: builtin headers (stddef.h, immintrin.h, ...) and compiler-rt
# (builtins, sanitizer runtimes). `clang -print-resource-dir` must find this at lib/clang/<ver>/.
cp -a "$src/lib/clang" "$work/lib/"
# libc++/libc++abi/libunwind: so this toolchain never has to fall back to whatever libstdc++ the
# host distro happens to have installed. Not the default yet (local-build.sh still resolves the
# system libstdc++ unless -stdlib=libc++ is passed), but bundled so that option exists.
cp -a "$src/include/c++" "$work/include/"
cp -a "$src/include/$target_triple/c++/v1/__config_site" "$work/include/$target_triple/c++/v1/"
cp -a "$src/lib/$target_triple"/libc++.a "$src/lib/$target_triple"/libc++abi.a "$src/lib/$target_triple"/libunwind.a "$work/lib/$target_triple/"
cp -a "$src/lib/$target_triple"/libc++.so* "$src/lib/$target_triple"/libc++abi.so* "$src/lib/$target_triple"/libunwind.so* "$work/lib/$target_triple/"
rm -rf "$extract_root"
# --- cmake, pruned from the official Kitware release ---
cmake_share_version=${cmake_version%.*}
cmake_archive_name="cmake-$cmake_version-linux-$cmake_release_arch.tar.gz"
cmake_archive="$downloads/$cmake_archive_name"
download_verified "$cmake_archive" \
"https://github.com/Kitware/CMake/releases/download/v$cmake_version/$cmake_archive_name" \
"$cmake_sha256"
cmake_extract_root="$script_dir/artifacts/.extract-cmake-$arch"
rm -rf "$cmake_extract_root"
mkdir -p "$cmake_extract_root"
echo "prepare-portable-tools.sh: extracting $cmake_archive_name..."
tar -xzf "$cmake_archive" -C "$cmake_extract_root"
cmake_src="$cmake_extract_root/cmake-$cmake_version-linux-$cmake_release_arch"
[[ -d "$cmake_src" ]] || { echo "prepare-portable-tools.sh: unexpected archive layout, expected $cmake_src" >&2; exit 1; }
mkdir -p "$work/share/cmake-$cmake_share_version"
cp -a "$cmake_src/bin/cmake" "$work/bin/"
cp -a "$cmake_src/share/cmake-$cmake_share_version/Modules" "$cmake_src/share/cmake-$cmake_share_version/Templates" \
"$work/share/cmake-$cmake_share_version/"
rm -rf "$cmake_extract_root"
# --- ninja, used as-is ---
ninja_archive="$downloads/ninja-$ninja_version-$arch.zip"
download_verified "$ninja_archive" \
"https://github.com/ninja-build/ninja/releases/download/v$ninja_version/$ninja_asset" \
"$ninja_sha256"
echo "prepare-portable-tools.sh: staging ninja $ninja_version..."
unzip -oq "$ninja_archive" -d "$work/bin"
chmod +x "$work/bin/ninja"
cat > "$work/LICENSE.txt" <<EOF
Portable build tools bundled by WiiCompiled
clang/lld/llvm-ar $llvm_version (pruned from the official LLVM release for Linux/$llvm_release_arch)
https://github.com/llvm/llvm-project/releases/tag/llvmorg-$llvm_version
Apache License v2.0 with LLVM Exceptions:
https://github.com/llvm/llvm-project/blob/llvmorg-$llvm_version/LICENSE.TXT
CMake $cmake_version
https://github.com/Kitware/CMake
BSD 3-Clause License
Ninja $ninja_version
https://github.com/ninja-build/ninja
Apache License 2.0
EOF
echo "prepare-portable-tools.sh: smoke-testing the toolchain..."
smoke_dir=$(mktemp -d)
trap 'rm -rf "$smoke_dir"' EXIT
cat > "$smoke_dir/t.cpp" <<'EOF'
#include <vector>
#include <cstdio>
int main() {
std::vector<int> v{1, 2, 3};
int sum = 0;
for (int x : v) sum += x;
return sum == 6 ? 0 : 1;
}
EOF
"$work/bin/clang++" -std=c++20 -fuse-ld=lld "$smoke_dir/t.cpp" -o "$smoke_dir/t"
"$smoke_dir/t"
# Also exercised together through CMake+Ninja, exactly how local-build.sh drives them - a plain
# clang++ invocation above would not catch a broken CMAKE_ROOT (Modules/Templates) or a Ninja that
# can't find the compiler.
cat > "$smoke_dir/CMakeLists.txt" <<'EOF'
cmake_minimum_required(VERSION 3.16)
project(smoke CXX)
add_executable(smoke t.cpp)
EOF
"$work/bin/cmake" -S "$smoke_dir" -B "$smoke_dir/build" -G Ninja \
-DCMAKE_MAKE_PROGRAM="$work/bin/ninja" -DCMAKE_CXX_COMPILER="$work/bin/clang++" >/dev/null
"$work/bin/cmake" --build "$smoke_dir/build" >/dev/null
"$smoke_dir/build/smoke"
rm -rf "$smoke_dir"
trap - EXIT
mv "$work" "$toolchain_dir"
echo "prepare-portable-tools.sh: toolchain ready at $toolchain_dir ($(du -sh "$toolchain_dir" | cut -f1))"
+33 -2
View File
@@ -53,17 +53,47 @@ Everything you change is saved to `Config.toml` on the spot and restored next la
**Real controller support.**
Controllers are fed to the game as a GameCube controller.
The port does NOT pretend to be a Wii Remote or Classic Controller.
Mappings are positional (`south`, `east`, `west`, `north`) rather than Xbox-labelled, so the
same config makes sense on Xbox, PlayStation, Nintendo and generic SDL pads alike, and extra
inputs like paddles, touchpads and share buttons show up when the hardware reports them.
The official Wii U / Switch GameCube adapter (WUP-028) works too; as with Dolphin, on Windows the
adapter must be switched to the WinUSB driver once (Zadig).
**Real Wii Remotes over Bluetooth.**
Pair a Wii Remote with Windows (Settings > Bluetooth > Add device, press 1+2 or SYNC, leave the
PIN empty) and the game reads it as an actual Wii Remote through KPAD: Wii Remote icons and
prompts, Wii Wheel tilt steering, wheelies and tricks all come from the game's own motion code.
Nunchuk and Classic Controller are real Wii extensions too: the game gets the Nunchuk's stick,
C/Z and accelerometer, and the Classic Controller through `KPADGetUnifiedWpadStatus` with its own
layout and icons, so its buttons do what the game says they do and no mapping is involved. Plug an
extension in or pull it out mid-game and the game switches control scheme like on the console
(the runtime patches SDL's Wii driver, which otherwise loses the remote for good on an extension
change). Only the Wii U Pro Controller, which has no Wii-era equivalent, is fed to the game as a
GameCube pad with Nintendo's layout. If a remote drops out or was switched on after launch, the
runtime keeps rescanning Bluetooth until it comes back (F10 > Controller settings > Wii Remotes). SDL's read of
the remote's factory accelerometer calibration often times out over Bluetooth (`console.log`
then says "Using fallback accelerometer calibration") and it falls back to a nominal zero point,
so the same menu has a one-button calibration (remote flat, buttons up) that removes the small
tilt offset some remotes show.
Known limitations of the Wii Remote path:
- No IR pointer yet: menus are navigated with the D-pad and A (the game treats the remote as
pointing away from the screen).
- Battery level is not reported to the game and the remote's speaker is not implemented.
- Only the Wii Remote's own accelerometer is calibrated; the Nunchuk's uses SDL's fixed zero point.
- The Classic Controller's L/R triggers reach the game as digital (full pull on click): SDL does not
expose their analog travel.
- Turn the Wii Remote support off in that menu if you use a Mayflash DolphinBar, which already
presents the remote as a regular gamepad.
## Requirements
- Windows 10 or 11, 64-bit
- 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
- 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)
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
GCZ, CISO, WBFS, WIA and RVZ are accepted.
@@ -84,6 +114,7 @@ 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.
> [!CAUTION]
> Only take builds from this repository's
> [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an
+21 -12
View File
@@ -26,10 +26,6 @@ Aurora itself vendors:
(shagkur) and Dave Murphy (WinterMute). `aurora-main/lib/card/SRAM.hpp`.
Source: <https://github.com/devkitPro/libogc>
> [!NOTE]
> Upstream aurora ships `assets/screenshot.png`, a rendered frame from a different Nintendo
> title. It is intentionally omitted from this repository.
### Dolphin Emulator data files - GPL-2.0-or-later
Copyright (c) 2003+ Dolphin Emulator Project.
@@ -103,14 +99,26 @@ Source: <https://github.com/ToruNiina/toml11/tree/v4.4.0>. Full license text:
Copyright (c) Antoine Aubry and contributors.
Referenced by `translator/src/Translator.Core`. Source: <https://github.com/aaubry/YamlDotNet>
### libco - ISC (valgrind.h: BSD-style)
Copyright byuu and the higan team.
Non-Windows builds use libco's symmetric stackful coroutines in place of Win32 Fibers for guest
OSThread scheduling (`runtime/src/fiber_manager.cpp`). Vendored in full (all non-Windows
CPU-architecture backends - amd64, x86, arm, aarch64, ppc, ppc64v2, plus the portable sjlj
fallback - though this project's x86_64-only target only ever compiles amd64.c) in
`runtime/third_party/libco` from commit `e18e09d634d612a01781168ad4d76be10a7e3bad`.
Source: <https://github.com/higan-emu/libco>. Full license text:
`runtime/third_party/libco/LICENSE`.
---
## Fetched at build time and redistributed in release builds
These are pinned in `aurora-main/extern/CMakeLists.txt` and
These are pinned in `aurora-main/extern/CMakeLists.txt`, `aurora-main/CMakeLists.txt` and
`aurora-main/cmake/AuroraDawnProvider.cmake`. They are not stored in this repository; the build
downloads them, and release installers carry the resulting binaries. Their license texts are
included in the installer's `licenses/` folder.
included in the installer's `licenses/` folder. The Windows installer bundles the pinned source
trees themselves (fetched by `Launcher/Prepare-Dependencies.ps1`) so end-user builds run offline.
| Component | Version | License | Upstream |
| --- | --- | --- | --- |
@@ -118,6 +126,7 @@ included in the installer's `licenses/` folder.
| Tint (part of Dawn) | with Dawn | BSD-3-Clause | <https://dawn.googlesource.com/dawn> |
| DirectXShaderCompiler (`dxcompiler.dll`) | with Dawn | NCSA / University of Illinois Open Source | <https://github.com/microsoft/DirectXShaderCompiler> |
| SDL | 3.4.4 | zlib | <https://github.com/libsdl-org/SDL> |
| libusb (linked into SDL on Windows) | 1.0.30 | LGPL-2.1-or-later | <https://github.com/libusb/libusb> |
| Abseil | LTS 20240722.0 | Apache-2.0 | <https://github.com/abseil/abseil-cpp> |
| Dear ImGui | 1.91.9b-docking | MIT | <https://github.com/ocornut/imgui> |
| {fmt} | 11.1.4 | MIT | <https://github.com/fmtlib/fmt> |
@@ -129,6 +138,7 @@ included in the installer's `licenses/` folder.
| SQLite | 3.51.3 amalgamation | Public domain | <https://sqlite.org/> |
| Tracy Profiler | pinned commit | BSD-3-Clause | <https://github.com/wolfpld/tracy> |
| C++/WinRT | - | MIT (Microsoft) | <https://github.com/microsoft/cppwinrt> |
| nodtool (disc image extraction) | v2.0.0-alpha.10 | MIT OR Apache-2.0 | <https://github.com/encounter/nod> |
### Dual-licensed components - elections made by this project
@@ -153,16 +163,15 @@ unmodified, with their license texts, in the installer's `licenses/` folder.
| llvm-mingw (Clang, LLD, libc++, libunwind, MinGW-w64 runtime) | Apache-2.0 with LLVM Exception; MinGW-w64 runtime under its own permissive terms; bundled GNU utilities under GPL-2.0-or-later or GPL-3.0-or-later | <https://github.com/mstorsjo/llvm-mingw> |
| CMake | BSD-3-Clause | <https://cmake.org/> |
| Ninja | Apache-2.0 | <https://ninja-build.org/> |
| DolphinTool (disc image extraction) | GPL-2.0-or-later | <https://github.com/dolphin-emu/dolphin> |
| nodtool (disc image extraction) | MIT OR Apache-2.0 | <https://github.com/encounter/nod> |
| Microsoft Visual C++ Runtime (`vcruntime140.dll`, `vcruntime140_1.dll`, `msvcp140.dll`) | Microsoft redistributable terms | Microsoft Visual Studio |
| `dxil.dll` | Microsoft redistributable (proprietary signing library) | Microsoft |
> [!IMPORTANT]
> Several toolkit components are GPL-licensed (DolphinTool, and the GNU utilities inside
> llvm-mingw). Their complete corresponding source is available from the upstream projects linked
> above at their pinned versions, and this project will supply it on request for the exact versions
> shipped in any given release. Pins live in `Launcher/Prepare-PortableTools.ps1` and
> `Launcher/NativeBuildFlags.ps1`.
> The GNU utilities bundled inside llvm-mingw are GPL-licensed. Their complete corresponding source
> is available from the upstream project linked above at its pinned version, and this project will
> supply it on request for the exact version shipped in any given release. Pins live in
> `Launcher/Prepare-PortableTools.ps1` and `Launcher/NativeBuildFlags.ps1`.
---
+2
View File
@@ -16,6 +16,7 @@ option(AURORA_CACHE_USE_ZSTD "Compress WebGPU cache entries with zstd" ON)
set(AURORA_DAWN_VERSION "v20260603.191052" CACHE STRING "Dawn version tag (https://github.com/encounter/dawn-build/releases)")
set(AURORA_SDL3_VERSION "3.4.4" CACHE STRING "SDL3 version tag (https://github.com/libsdl-org/SDL/releases)")
set(AURORA_NOD_VERSION "v2.0.0-alpha.8" CACHE STRING "nod version tag (https://github.com/encounter/nod/releases)")
set(AURORA_LIBUSB_VERSION "1.0.30" CACHE STRING "libusb version tag (https://github.com/libusb/libusb/releases)")
# Platform-specific defaults
if (CMAKE_CROSSCOMPILING)
@@ -44,6 +45,7 @@ set(AURORA_SDL3_PROVIDER "${_default_provider}" CACHE STRING
set_property(CACHE AURORA_SDL3_PROVIDER PROPERTY STRINGS auto vendor system package)
set(AURORA_SDL3_LINKAGE "${_default_linkage}" CACHE STRING "SDL3 linkage type preference")
set_property(CACHE AURORA_SDL3_LINKAGE PROPERTY STRINGS shared static)
option(AURORA_SDL3_LIBUSB "Build the vendored SDL3 with libusb on Windows (official GameCube adapter support)" ON)
# nod (if AURORA_ENABLE_DVD)
set(AURORA_NOD_PROVIDER "${_default_provider}" CACHE STRING
+43
View File
@@ -0,0 +1,43 @@
# libusb for SDL3's HIDAPI joystick drivers on Windows.
#
# The official GameCube adapter (WUP-028) is a vendor-specific USB device, not HID, so SDL3
# can only reach it through libusb - which the official SDL3 packages leave out. The vendored
# SDL3 build compiles libusb from the pinned upstream release and links it in statically.
include(FetchContent)
FetchContent_Declare(libusb
URL "https://github.com/libusb/libusb/releases/download/v${AURORA_LIBUSB_VERSION}/libusb-${AURORA_LIBUSB_VERSION}.tar.bz2"
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
# Upstream ships no CMakeLists.txt, so this only populates the source tree.
FetchContent_MakeAvailable(libusb)
set(_libusb_root "${libusb_SOURCE_DIR}/libusb")
add_library(usb-1.0 STATIC
"${_libusb_root}/core.c"
"${_libusb_root}/descriptor.c"
"${_libusb_root}/hotplug.c"
"${_libusb_root}/io.c"
"${_libusb_root}/strerror.c"
"${_libusb_root}/sync.c"
"${_libusb_root}/os/events_windows.c"
"${_libusb_root}/os/threads_windows.c"
"${_libusb_root}/os/windows_common.c"
"${_libusb_root}/os/windows_usbdk.c"
"${_libusb_root}/os/windows_winusb.c"
)
target_include_directories(usb-1.0
PUBLIC "${_libusb_root}"
PRIVATE "${CMAKE_CURRENT_LIST_DIR}/libusb" "${_libusb_root}/os"
)
set_target_properties(usb-1.0 PROPERTIES UNITY_BUILD OFF)
add_library(LibUSB::LibUSB ALIAS usb-1.0)
# SDL's FindLibUSB expects an installed copy. Satisfy its presence checks with this target
# instead: the alias pre-empts the imported target it would otherwise create, and the link
# probe is answered up front because the archive does not exist until build time.
set(LibUSB_INCLUDE_PATH "${_libusb_root}" CACHE PATH "" FORCE)
set(LibUSB_LIBRARY "usb-1.0" CACHE STRING "" FORCE)
set(HAVE_LIBUSB_H 1 CACHE INTERNAL "" FORCE)
set(SDL_HIDAPI_LIBUSB ON CACHE BOOL "" FORCE)
set(SDL_HIDAPI_LIBUSB_SHARED OFF CACHE BOOL "" FORCE)
+126
View File
@@ -0,0 +1,126 @@
# Source-level fixes applied to the vendored SDL3 tree before it is built.
#
# Each patch is an exact string replacement, checked and idempotent: a tree that
# already carries the fix is left alone, a tree where the anchor text is missing
# (a different SDL version) stops the configure with a clear message instead of
# silently building without the fix.
#
# Used two ways:
# - included by AuroraSDL3Provider.cmake, which calls aurora_sdl3_apply_patches()
# on a pre-provided source tree (FETCHCONTENT_SOURCE_DIR_SDL);
# - run as `cmake -DSDL_SOURCE_DIR=<dir> -P AuroraSDL3Patches.cmake` from the
# FetchContent PATCH_COMMAND for a freshly extracted tarball.
function(_aurora_sdl3_replace file description old new)
file(READ "${file}" _content)
string(FIND "${_content}" "${new}" _already)
if (NOT _already EQUAL -1)
return()
endif ()
string(FIND "${_content}" "${old}" _anchor)
if (_anchor EQUAL -1)
message(FATAL_ERROR "aurora: SDL3 patch '${description}' does not apply to ${file}; "
"the vendored SDL version changed, review AuroraSDL3Patches.cmake")
endif ()
string(REPLACE "${old}" "${new}" _content "${_content}")
file(WRITE "${file}" "${_content}")
message(STATUS "aurora: SDL3 patch applied: ${description}")
endfunction()
function(aurora_sdl3_apply_patches sdl_source_dir)
set(_wii "${sdl_source_dir}/src/joystick/hidapi/SDL_hidapi_wii.c")
if (NOT EXISTS "${_wii}")
message(FATAL_ERROR "aurora: SDL3 source tree at ${sdl_source_dir} has no SDL_hidapi_wii.c")
endif ()
# Wii Remote: rebuild the joystick in place after an extension change.
#
# When a Nunchuk or Classic Controller is plugged in or pulled out, the driver
# flags the joystick as disconnected so it can come back with the new name and
# capabilities. But the HID device stays open, and HIDAPI only removes a
# joystick-less device once its handle is closed, so nothing ever re-creates
# the joystick: the remote is gone for good until the application toggles the
# driver hint (which closes and re-opens the Bluetooth HID handle, something
# some Windows Bluetooth stacks answer by dropping the link). Instead, when the
# device has no joystick, probe the extension again (two bounded attempts, at
# most once a second) and connect a new joystick on the still-open handle.
_aurora_sdl3_replace("${_wii}" "Wii Remote in-place reconnect (context field)"
[==[ Uint64 m_ulNextMotionPlusCheck;
bool m_bDisconnected;
]==]
[==[ Uint64 m_ulNextMotionPlusCheck;
bool m_bDisconnected;
Uint64 m_ulNextReconnect; /* WiiCompiled: see HIDAPI_DriverWii_UpdateDevice */
]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote in-place reconnect (bounded extension probe)"
[==[static EWiiExtensionControllerType ReadExtensionControllerType(SDL_HIDAPI_Device *device)
{
SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)device->context;
EWiiExtensionControllerType eExtensionControllerType = k_eWiiExtensionControllerType_Unknown;
const int MAX_ATTEMPTS = 20;
int attempts = 0;
]==]
[==[static EWiiExtensionControllerType ReadExtensionControllerTypeAttempts(SDL_HIDAPI_Device *device, int MAX_ATTEMPTS)
{
SDL_DriverWii_Context *ctx = (SDL_DriverWii_Context *)device->context;
EWiiExtensionControllerType eExtensionControllerType = k_eWiiExtensionControllerType_Unknown;
int attempts = 0;
]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote in-place reconnect (probe wrapper)"
[==[static void UpdateDeviceIdentity(SDL_HIDAPI_Device *device)
{
]==]
[==[static EWiiExtensionControllerType ReadExtensionControllerType(SDL_HIDAPI_Device *device)
{
return ReadExtensionControllerTypeAttempts(device, 20);
}
static void UpdateDeviceIdentity(SDL_HIDAPI_Device *device)
{
]==])
_aurora_sdl3_replace("${_wii}" "Wii Remote in-place reconnect (UpdateDevice)"
[==[ if (device->num_joysticks > 0) {
joystick = SDL_GetJoystickFromID(device->joysticks[0]);
} else {
return false;
}
now = SDL_GetTicks();
]==]
[==[ if (device->num_joysticks > 0) {
joystick = SDL_GetJoystickFromID(device->joysticks[0]);
} else {
/* WiiCompiled: the joystick was dropped (extension plugged or unplugged,
* or a failed read) but the HID device is still open, and the device
* list only removes a joystick-less device once its handle is closed.
* Rebuild the joystick in place, so an extension change behaves like it
* does on the console: the remote never goes away, it just changes
* type. If the remote does not answer, keep the device and retry. */
now = SDL_GetTicks();
if (ctx->m_ulNextReconnect != 0 && now < ctx->m_ulNextReconnect) {
return false;
}
ctx->m_ulNextReconnect = now + 1000;
{
EWiiExtensionControllerType type = ReadExtensionControllerTypeAttempts(device, 2);
if (type == k_eWiiExtensionControllerType_Unknown) {
return false;
}
ctx->m_bDisconnected = false;
ctx->m_ulLastInput = now;
ctx->m_ulNextReconnect = 0;
ctx->m_eExtensionControllerType = type;
UpdateDeviceIdentity(device);
SDL_LogDebug(SDL_LOG_CATEGORY_INPUT, "HIDAPI Wii: Reconnected joystick as %s", device->name);
return HIDAPI_JoystickConnected(device, NULL);
}
}
now = SDL_GetTicks();
]==])
endfunction()
# Script mode (FetchContent PATCH_COMMAND).
if (CMAKE_SCRIPT_MODE_FILE AND DEFINED SDL_SOURCE_DIR)
aurora_sdl3_apply_patches("${SDL_SOURCE_DIR}")
endif ()
@@ -129,12 +129,24 @@ elseif (_aurora_sdl3_provider STREQUAL "vendor")
endif ()
if (WIN32)
set(SDL_LIBC ON CACHE BOOL "Use the system C library" FORCE)
if (AURORA_SDL3_LIBUSB)
include("${CMAKE_CURRENT_LIST_DIR}/AuroraLibUSB.cmake")
endif ()
endif ()
include(FetchContent)
# Source fixes for the vendored SDL (see AuroraSDL3Patches.cmake). A freshly
# downloaded tarball is patched by the PATCH_COMMAND; a tree handed in through
# FETCHCONTENT_SOURCE_DIR_SDL skips the download steps, so patch it here.
set(_aurora_sdl3_patches "${CMAKE_CURRENT_LIST_DIR}/AuroraSDL3Patches.cmake")
include("${_aurora_sdl3_patches}")
if (DEFINED FETCHCONTENT_SOURCE_DIR_SDL AND EXISTS "${FETCHCONTENT_SOURCE_DIR_SDL}")
aurora_sdl3_apply_patches("${FETCHCONTENT_SOURCE_DIR_SDL}")
endif ()
FetchContent_Declare(SDL
URL "https://github.com/libsdl-org/SDL/releases/download/release-${AURORA_SDL3_VERSION}/SDL3-${AURORA_SDL3_VERSION}.tar.gz"
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
PATCH_COMMAND "${CMAKE_COMMAND}" -DSDL_SOURCE_DIR=<SOURCE_DIR> -P "${_aurora_sdl3_patches}"
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(SDL)
+8
View File
@@ -0,0 +1,8 @@
/* libusb build configuration for the Windows backend (MinGW/Clang). */
#pragma once
#define PLATFORM_WINDOWS 1
#define ENABLE_LOGGING 1
#define DEFAULT_VISIBILITY
#define HAVE_STRUCT_TIMESPEC 1
#define PRINTF_FORMAT(a, b) __attribute__((__format__(__printf__, a, b)))
+18
View File
@@ -255,6 +255,14 @@ FrameWorkerState g_frameWorker;
bool frame_worker_requested() noexcept {
#ifdef AURORA_ENABLE_GX
static const bool enabled = [] {
#if defined(__APPLE__)
// ImGui's SDL backend may raise an SDL window from ImGui::NewFrame(). On
// macOS that reaches AppKit, whose window operations are main-thread-only;
// doing it on the frame worker terminates the process with EXC_BREAKPOINT.
// Keep all SDL/ImGui work on the calling thread until the worker no longer
// owns frame preparation on Apple platforms.
return false;
#endif
#if defined(_WIN32)
// RenderDoc's D3D12 layer is injected before Aurora starts and needs device and command
// ownership on one thread, so keep frame submission synchronous there.
@@ -1516,6 +1524,15 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
// Phase 3: hand the encoded group to whoever owns presentation.
void publish_presentations(std::vector<PresentationJob>&& presentationJobs, bool interpolationActive) {
#if defined(__APPLE__)
(void)interpolationActive;
// Presenting reaches SDL/AppKit, whose window operations must stay on the
// main thread. Interpolation normally starts the presenter worker, so keep
// its jobs synchronous on Apple platforms.
for (const auto& job : presentationJobs) {
present_presentation_job(job);
}
#else
// Keep presentation on the presenter whenever the async frame worker runs, even with
// interpolation off, so every mode shares one surface/resize path. RenderDoc keeps the sync path.
if (frame_worker_requested() || interpolationActive ||
@@ -1526,6 +1543,7 @@ void publish_presentations(std::vector<PresentationJob>&& presentationJobs, bool
present_presentation_job(job);
}
}
#endif
}
void record_frame_telemetry() {
+3 -1
View File
@@ -111,7 +111,9 @@ ECardResult CardGciFolder::createFile(const char* filename, size_t size, FileHan
}
gciFileHeader->swapEndian();
m_files.push_back({*gciFileHeader, fileSize, reinterpret_cast<const char8_t*>(gciFilename.c_str()), false}); // push non-endian swapped header first
// push non-endian swapped header first
m_files.push_back({*gciFileHeader, fileSize,
std::u8string(gciFilename.begin(), gciFilename.end()), false});
handleOut = FileHandle(m_files.size() - 1, 0);
return ECardResult::READY;
+1 -1
View File
@@ -175,7 +175,7 @@ void CARDInit(const char* game, const char* maker) {
std::filesystem::path cardWorkingDir;
if (aurora::g_config.userPath != nullptr)
cardWorkingDir = reinterpret_cast<const char8_t*>(aurora::g_config.userPath);
cardWorkingDir = fs_path_from_string(aurora::g_config.userPath);
else
cardWorkingDir = std::filesystem::current_path();
+108 -16
View File
@@ -1,10 +1,13 @@
#include "../../fs_helper.hpp"
#include "../../input.hpp"
#include "../../internal.hpp"
#include <dolphin/pad.h>
#include <dolphin/si.h>
#include <SDL3/SDL_mouse.h>
#include <SDL3/SDL_joystick.h>
#include <array>
#include <atomic>
#include <sys/stat.h>
#include <ranges>
@@ -191,6 +194,32 @@ std::array<PADButtonMapping, PAD_BUTTON_COUNT> g_defaultButtonsJoyPair{{
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT},
}};
// Wii U Pro Controllers through SDL's HIDAPI Wii driver. No SDL_GamepadType
// singles them out, so they are picked by the name the driver gives them (see
// __PADSetDefaultMapping). Wii Remotes, with or without a Nunchuk or Classic
// Controller, are read by the game through KPAD instead and never get a
// GameCube mapping (the runtime hides those ports from PADRead).
// Nintendo's labelled layout: A on the right accelerates, B at the bottom
// brakes. SDL's Wii driver reports ZL/ZR as the LEFT_TRIGGER/RIGHT_TRIGGER
// axes, never as shoulder buttons, so they are left unbound here and picked up
// by aurora's default axis mapping (g_defaultAxes) the same way every
// analog-trigger pad's L/R is.
std::array<PADButtonMapping, PAD_BUTTON_COUNT> g_defaultButtonsWiiUPro{{
{SDL_GAMEPAD_BUTTON_EAST, PAD_BUTTON_A},
{SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_B},
{SDL_GAMEPAD_BUTTON_NORTH, PAD_BUTTON_X},
{SDL_GAMEPAD_BUTTON_WEST, PAD_BUTTON_Y},
{SDL_GAMEPAD_BUTTON_START, PAD_BUTTON_START},
{SDL_GAMEPAD_BUTTON_BACK, PAD_TRIGGER_Z},
{PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_L},
{PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_R},
{SDL_GAMEPAD_BUTTON_DPAD_UP, PAD_BUTTON_UP},
{SDL_GAMEPAD_BUTTON_DPAD_DOWN, PAD_BUTTON_DOWN},
{SDL_GAMEPAD_BUTTON_DPAD_LEFT, PAD_BUTTON_LEFT},
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT},
}};
std::array<PADKeyButtonBinding, PAD_BUTTON_COUNT> g_defaultKeys{{
{PAD_KEY_INVALID, PAD_BUTTON_A},
{PAD_KEY_INVALID, PAD_BUTTON_B},
@@ -283,7 +312,7 @@ constexpr PADCLampRegion ClampRegion{
bool g_initialized;
bool g_keyboardBindingsLoaded = false;
bool g_blockPAD = false;
std::atomic_bool g_blockPAD{false};
bool g_suppressHeldOnRead = false;
std::array<PADButton, PAD_CHANMAX> g_suppressedButtons{};
std::array<bool, PAD_CHANMAX> g_suppressLeftTrigger{};
@@ -407,8 +436,26 @@ static void reset_alt_button_mapping(aurora::input::GameController* controller)
}
}
// SDL's hidapi Wii driver names the pad "Nintendo Wii U Pro Controller"; the
// other names it produces are Wii Remotes, which the game reads through KPAD
// and which therefore never take a GameCube mapping.
static bool wii_default_mapping(const aurora::input::GameController* controller,
std::array<PADButtonMapping, PAD_BUTTON_COUNT>& out) {
const char* name = SDL_GetGamepadName(controller->m_controller);
if (name == nullptr || SDL_strstr(name, "Wii U Pro Controller") == nullptr) {
return false;
}
out = g_defaultButtonsWiiUPro;
return true;
}
// Picks the default button table for a controller by name (Wii pads) or SDL gamepad type.
void __PADSetDefaultMapping(aurora::input::GameController* controller) /* NOLINT(*-reserved-identifier) */
{
if (wii_default_mapping(controller, controller->m_buttonMapping)) {
reset_alt_button_mapping(controller);
return;
}
switch (SDL_GetGamepadType(controller->m_controller)) {
case SDL_GAMEPAD_TYPE_XBOX360:
controller->m_buttonMapping = g_defaultButtonsXBox360;
@@ -491,7 +538,7 @@ void __PADLoadMapping(aurora::input::GameController* controller) /* NOLINT(*-re
return;
}
std::string basePath{aurora::g_config.userPath};
const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath);
if (!controller->m_mappingLoaded) {
__PADSetDefaultMapping(controller);
controller->m_axisMapping = g_defaultAxes;
@@ -499,8 +546,9 @@ void __PADLoadMapping(aurora::input::GameController* controller) /* NOLINT(*-re
controller->m_mappingLoaded = true;
const auto path = fmt::format("{}/{}_{:04X}_{:04X}.controller", basePath, PADGetName(playerIndex), controller->m_vid,
controller->m_pid);
const auto path = fs_path_to_string(
basePath / fmt::format("{}_{:04X}_{:04X}.controller", PADGetName(playerIndex), controller->m_vid,
controller->m_pid));
SDL_IOStream* file = SDL_IOFromFile(path.c_str(), "rb");
if (file == nullptr) {
return;
@@ -659,7 +707,8 @@ u32 PADRead(PADStatus* status) {
int numKeys = 0;
const bool* kbState = SDL_GetKeyboardState(&numKeys);
const bool captureHeldInput = g_suppressHeldOnRead && !g_blockPAD;
const bool inputBlocked = g_blockPAD.load(std::memory_order_acquire);
const bool captureHeldInput = g_suppressHeldOnRead && !inputBlocked;
g_suppressHeldOnRead = false;
uint32_t rumbleSupport = 0;
@@ -741,6 +790,47 @@ u32 PADRead(PADStatus* status) {
if (controller) {
EnsureMappingLoaded(controller);
// Wii U Pro Controller raw D-pad fallback. SDL's HIDAPI Wii driver posts
// the D-pad as joystick buttons 11-14 (the SDL_GAMEPAD_BUTTON_DPAD_*
// values) and never as a hat, but the mapping SDL generates for HIDAPI
// pads binds the D-pad to hat 0, so SDL_GetGamepadButton(DPAD_*) stays
// false. Keep this restricted to the Wii driver's pad so raw button
// indices don't interfere with other controller types.
const char* name = SDL_GetGamepadName(controller->m_controller);
const bool isWiiUPro = name != nullptr && SDL_strstr(name, "Wii U Pro Controller") != nullptr;
if (isWiiUPro) {
SDL_Joystick* joystick =
SDL_GetGamepadJoystick(controller->m_controller);
uint32_t raw = 0;
const int buttonCount = SDL_GetNumJoystickButtons(joystick);
for (int b = 0; b < buttonCount && b < 32; ++b) {
if (SDL_GetJoystickButton(joystick, b)) {
raw |= (1u << b);
}
}
// Up = button 11
if (raw & (1u << 11)) {
status[i].button |= PAD_BUTTON_UP;
}
// Down = button 12
if (raw & (1u << 12)) {
status[i].button |= PAD_BUTTON_DOWN;
}
// Left = button 13
if (raw & (1u << 13)) {
status[i].button |= PAD_BUTTON_LEFT;
}
// Right = button 14
if (raw & (1u << 14)) {
status[i].button |= PAD_BUTTON_RIGHT;
}
}
bool leftTriggerSet = false;
bool rightTriggerSet = false;
std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet,
@@ -774,6 +864,7 @@ u32 PADRead(PADStatus* status) {
}
});
// TODO: Add serializable mappings for these (probably not necessary)?
static constexpr std::array<std::pair<SDL_GamepadButton, PADExtButton>, PAD_EXT_BUTTON_COUNT> kExtButtonMappings{{
{SDL_GAMEPAD_BUTTON_BACK, PAD_BUTTON_BACK},
@@ -882,7 +973,7 @@ u32 PADRead(PADStatus* status) {
}
}
if (g_blockPAD) {
if (inputBlocked) {
neutralize_status(status[i]);
} else {
apply_unblock_suppression(status[i], i, captureHeldInput);
@@ -1247,8 +1338,8 @@ constexpr uint32_t k_keyboardMagic = SBIG('KBND');
constexpr int32_t k_keyboardVersion = 3;
static void load_keyboard_bindings() {
const auto filePath = std::filesystem::path{aurora::g_config.userPath} / "keyboard_bindings.dat";
SDL_IOStream* file = SDL_IOFromFile(filePath.string().c_str(), "rb");
const auto filePath = fs_path_from_string(aurora::g_config.userPath) / "keyboard_bindings.dat";
SDL_IOStream* file = SDL_IOFromFile(fs_path_to_string(filePath).c_str(), "rb");
if (file == nullptr) {
return;
}
@@ -1317,10 +1408,11 @@ static void load_keyboard_bindings() {
}
static void save_keyboard_bindings() {
const auto filePath = std::filesystem::path{aurora::g_config.userPath} / "keyboard_bindings.dat";
SDL_IOStream* file = SDL_IOFromFile(filePath.string().c_str(), "wb");
const auto filePath = fs_path_from_string(aurora::g_config.userPath) / "keyboard_bindings.dat";
const auto filePathStr = fs_path_to_string(filePath);
SDL_IOStream* file = SDL_IOFromFile(filePathStr.c_str(), "wb");
if (file == nullptr) {
aurora::input::Log.warn("save_keyboard_bindings: failed to open {} for writing", filePath.string());
aurora::input::Log.warn("save_keyboard_bindings: failed to open {} for writing", filePathStr);
return;
}
@@ -1344,14 +1436,14 @@ void __PADWriteDeadZones(SDL_IOStream* file, // NOLINT(*-reserved-identifier)
}
void PADSerializeMappings() {
const std::filesystem::path basePath{aurora::g_config.userPath};
const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath);
for (auto& controller : aurora::input::g_GameControllers | std::views::values) {
EnsureMappingLoaded(&controller);
const auto filePath =
basePath / fmt::format("{}_{:04X}_{:04X}.controller", aurora::input::controller_name(controller.m_index),
controller.m_vid, controller.m_pid);
std::string filePathStr = filePath.string();
std::string filePathStr = fs_path_to_string(filePath);
// don't truncate the file if it already exists
const char* openMode = std::filesystem::exists(filePath) ? "r+b" : "wb";
@@ -1370,7 +1462,7 @@ void PADSerializeMappings() {
// start writing data at next 32-byte aligned offset
const int64_t dataStart = SDL_TellIO(file) + 31 & ~31;
if (dataStart == -1) {
aurora::input::Log.warn("Unable to seek in controller bindings! Path: \"{}\"", filePath.string());
aurora::input::Log.warn("Unable to seek in controller bindings! Path: \"{}\"", filePathStr);
return;
}
SDL_SeekIO(file, dataStart, SDL_IO_SEEK_SET);
@@ -1530,12 +1622,12 @@ void PADRestoreDefaultMapping(const u32 port) {
}
void PADBlockInput(const bool block) {
if (g_blockPAD && !block) {
if (g_blockPAD.exchange(block, std::memory_order_acq_rel) && !block) {
g_suppressHeldOnRead = true;
}
g_blockPAD = block;
}
SDL_Gamepad* PADGetSDLGamepadForIndex(const u32 index) {
const auto* ctrl = __PADGetControllerForIndex(index);
if (ctrl == nullptr) {
+10 -2
View File
@@ -1,11 +1,19 @@
#pragma once
#include <filesystem>
#include <string>
#include <string_view>
/**
* Converts a std::filesystem::path to a std::string, UTF-8, without exploding on Windows.
* Narrow path strings crossing the aurora boundary are UTF-8. path::string() and the
* char path constructor go through the ANSI codepage on Windows, so they must not be
* used for anything the host handed us or hands back to SDL, sqlite or ImGui.
*/
inline std::string fs_path_to_string(const std::filesystem::path& path) {
const auto u8str = path.u8string();
return { reinterpret_cast<const char*>(u8str.c_str()) };
return { reinterpret_cast<const char*>(u8str.c_str()), u8str.size() };
}
inline std::filesystem::path fs_path_from_string(std::string_view utf8) {
return std::filesystem::path(std::u8string(utf8.begin(), utf8.end()));
}
+2 -1
View File
@@ -2,6 +2,7 @@
#include "clear.hpp"
#include "../gx/pipeline.hpp"
#include "../fs_helper.hpp"
#include "../sqlite_utils.hpp"
#include "../webgpu/gpu.hpp"
@@ -715,7 +716,7 @@ static bool prepare_pipeline_cache_db() {
return true;
}
const auto path = (std::filesystem::path{g_config.pipelineCachePath} / "pipeline_cache.db").string();
const auto path = fs_path_to_string(fs_path_from_string(g_config.pipelineCachePath) / "pipeline_cache.db");
auto ret = sqlite3_open(path.c_str(), &g_pipelineCacheDb);
if (ret != SQLITE_OK) {
Log.error("Failed to open pipeline cache database: {}", sqlite3_errmsg(g_pipelineCacheDb));
+2 -2
View File
@@ -506,8 +506,8 @@ void build_index() noexcept {
return;
}
auto userPath = std::filesystem::path{reinterpret_cast<const char8_t*>(g_config.userPath)};
auto cachePath = std::filesystem::path{reinterpret_cast<const char8_t*>(g_config.cachePath)};
auto userPath = fs_path_from_string(g_config.userPath);
auto cachePath = fs_path_from_string(g_config.cachePath);
s_replacementRoot = userPath / "texture_replacements";
s_dumpRoot = cachePath / "texture_dumps";
+2 -1
View File
@@ -10,6 +10,7 @@
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_render.h>
#include "fs_helper.hpp"
#include "internal.hpp"
#include "webgpu/gpu.hpp"
#include "window.hpp"
@@ -37,7 +38,7 @@ void remove_legacy_ini_file(const char* basePath) noexcept {
}
std::error_code ec;
std::filesystem::remove(std::filesystem::path{basePath} / "imgui.ini", ec);
std::filesystem::remove(fs_path_from_string(basePath) / "imgui.ini", ec);
}
void create_context() noexcept {
+64 -4
View File
@@ -255,6 +255,18 @@ IdentityMatch identity_match(const ControllerIdentity& saved, const ControllerId
: IdentityMatch::None;
}
void assign_player_index(GameController& controller, int32_t port) {
SDL_SetGamepadPlayerIndex(controller.m_controller, port);
controller.m_playerIndex = port;
}
// SDL forgets the index for devices mapped after connect, so player_index() falls
// back to the cached copy; both have to move together or a port looks doubly taken.
int32_t effective_player_index(const GameController& controller) {
const int32_t player = SDL_GetGamepadPlayerIndex(controller.m_controller);
return player >= 0 ? player : controller.m_playerIndex;
}
bool is_instance_claimed(const std::array<Uint32, PAD_MAX_CONTROLLERS>& claimedControllers, size_t claimedCount,
Uint32 instance) {
return std::find(claimedControllers.begin(), claimedControllers.begin() + claimedCount, instance) !=
@@ -269,10 +281,10 @@ void apply_port_preferences() noexcept {
}
for (auto& [instance, controller] : g_GameControllers) {
const int32_t player = SDL_GetGamepadPlayerIndex(controller.m_controller);
const int32_t player = effective_player_index(controller);
if (player >= 0 && player < PAD_MAX_CONTROLLERS && g_portPreferences[player].state != PortPreferenceState::Unset) {
// Keep SDL's default player assignment from taking explicitly configured ports
SDL_SetGamepadPlayerIndex(controller.m_controller, -1);
assign_player_index(controller, -1);
}
}
@@ -293,7 +305,7 @@ void apply_port_preferences() noexcept {
switch (identity_match(preference.identity, controller_identity(controller))) {
case IdentityMatch::Exact:
SDL_SetGamepadPlayerIndex(controller.m_controller, static_cast<int32_t>(port));
assign_player_index(controller, static_cast<int32_t>(port));
claimedControllers[claimedCount++] = instance;
fallbackController = nullptr;
break;
@@ -311,11 +323,45 @@ void apply_port_preferences() noexcept {
}
if (fallbackController != nullptr) {
SDL_SetGamepadPlayerIndex(fallbackController->m_controller, static_cast<int32_t>(port));
assign_player_index(*fallbackController, static_cast<int32_t>(port));
claimedControllers[claimedCount++] = fallbackInstance;
}
}
}
// SDL only hands out a player index when the device already had a gamepad mapping
// at connect time, so anything mapped later (the setup wizard) stays at -1.
void ensure_player_index(GameController& controller) noexcept {
const int32_t player = SDL_GetGamepadPlayerIndex(controller.m_controller);
if (player >= 0) {
controller.m_playerIndex = player;
return;
}
if (controller.m_playerIndex >= 0) {
return;
}
ensure_port_preferences_loaded();
const auto claim = [&](bool skipConfiguredPorts) {
for (int32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
if (skipConfiguredPorts && g_portPreferences[port].state != PortPreferenceState::Unset) {
continue;
}
const bool taken = std::any_of(g_GameControllers.begin(), g_GameControllers.end(), [&](const auto& entry) {
return entry.second.m_controller != controller.m_controller && effective_player_index(entry.second) == port;
});
if (!taken) {
assign_player_index(controller, port);
return true;
}
}
return false;
};
// Explicitly configured ports are only used as a last resort so a hot-plugged
// controller cannot steal the port its preferred device will claim.
if (!claim(true)) {
claim(false);
}
}
} // namespace
GameController* get_controller_for_player(uint32_t player) noexcept {
@@ -364,6 +410,7 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
controller.m_hasRgbLed = SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false);
SDL_JoystickID instance = SDL_GetJoystickID(SDL_GetGamepadJoystick(ctrl));
g_GameControllers[instance] = controller;
ensure_player_index(g_GameControllers[instance]);
apply_port_preferences();
return instance;
}
@@ -371,6 +418,19 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
return -1;
}
bool refresh_controller(SDL_JoystickID instance) noexcept {
const auto it = g_GameControllers.find(instance);
if (it == g_GameControllers.end()) {
return false;
}
// The SDL mapping changed underneath us; drop the cached PAD bindings so they
// are rebuilt from the new one.
it->second.m_mappingLoaded = false;
ensure_player_index(it->second);
apply_port_preferences();
return true;
}
void remove_controller(Uint32 instance) noexcept {
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
SDL_CloseGamepad(it->second.m_controller);
+1
View File
@@ -51,6 +51,7 @@ struct GameController {
GameController* get_controller_for_player(uint32_t player) noexcept;
Sint32 get_instance_for_player(uint32_t player) noexcept;
SDL_JoystickID add_controller(SDL_JoystickID which) noexcept;
bool refresh_controller(SDL_JoystickID instance) noexcept;
void remove_controller(Uint32 instance) noexcept;
Sint32 player_index(Uint32 instance) noexcept;
void set_player_index(Uint32 instance, Sint32 index) noexcept;
+7 -3
View File
@@ -137,7 +137,7 @@ static void prune_stale_rows() {
static bool cache_init_core() {
Log.debug("SQLite version {}", sqlite3_libversion());
const auto path = std::filesystem::path{reinterpret_cast<const char8_t*>(g_config.cachePath)} / "dawn_cache.db";
const auto path = fs_path_from_string(g_config.cachePath) / "dawn_cache.db";
std::string file = fs_path_to_string(path);
Log.debug("Using dawn cache at {}", file);
auto ret = sqlite3_open(file.c_str(), &db);
@@ -165,8 +165,12 @@ static bool cache_init_core() {
db = nullptr;
std::error_code ec;
std::filesystem::remove(path, ec);
std::filesystem::remove(std::filesystem::path{file + "-wal"}, ec);
std::filesystem::remove(std::filesystem::path{file + "-shm"}, ec);
auto wal = path;
wal += "-wal";
std::filesystem::remove(wal, ec);
auto shm = path;
shm += "-shm";
std::filesystem::remove(shm, ec);
ret = sqlite3_open(file.c_str(), &db);
if (ret != SQLITE_OK) {
Log.error("Failed to recreate database: {}", sqlite3_errmsg(db));
+9
View File
@@ -294,6 +294,15 @@ void process_event(SDL_Event& event) {
});
break;
}
case SDL_EVENT_GAMEPAD_REMAPPED: {
if (input::refresh_controller(event.gdevice.which)) {
g_events.push_back(AuroraEvent{
.type = AURORA_CONTROLLER_ADDED,
.controller = event.gdevice.which,
});
}
break;
}
case SDL_EVENT_GAMEPAD_REMOVED: {
input::remove_controller(event.gdevice.which);
g_events.push_back(AuroraEvent{
+174 -31
View File
@@ -1,15 +1,28 @@
cmake_minimum_required(VERSION 3.16)
project(mkw_recompiled)
if(NOT WIN32 OR NOT MINGW OR NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR
NOT CMAKE_SIZEOF_VOID_P EQUAL 8 OR
NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
message(FATAL_ERROR "WiiCompiled requires 64-bit LLVM-MinGW Clang on Windows")
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(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.
set(MKW_PLATFORM_MACOS TRUE)
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")
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)
# 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
# applied directory-scoped, immediately after the aurora add_subdirectory() call,
@@ -46,6 +59,20 @@ 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.
@@ -110,21 +137,35 @@ else()
message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing")
endif()
set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE)
set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE)
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
set(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "" FORCE)
if(MKW_PLATFORM_WINDOWS)
set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE)
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE)
set(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "" FORCE)
elseif(MKW_PLATFORM_MACOS)
set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE)
set(DAWN_ENABLE_VULKAN OFF CACHE BOOL "" FORCE)
set(DAWN_ENABLE_METAL ON CACHE BOOL "" FORCE)
set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE)
else()
set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE)
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE)
set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE)
endif()
set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE)
set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(DAWN_USE_WINDOWS_UI OFF CACHE BOOL "" FORCE)
# Provide a tiny stub for DXProgrammableCapture when the SDK/PIX headers are
# missing (common on MinGW). Dawn only includes the header; no symbols are
# referenced when PIX isn't present.
set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs")
if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h")
file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR})
file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h"
"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX is absent.\n")
if(MKW_PLATFORM_WINDOWS)
set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs")
if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h")
file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR})
file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h"
"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX isn't present.\n")
endif()
endif()
# Deliberately NOT injected project-wide. Only a from-source Dawn build ever
# includes DXProgrammableCapture.h, and this tree consumes Dawn as a prebuilt
@@ -153,7 +194,9 @@ else()
if(TARGET ${t})
set_target_properties(${t} PROPERTIES UNITY_BUILD OFF)
target_compile_options(${t} PRIVATE -O3 -ffast-math -w -pipe)
target_include_directories(${t} PRIVATE ${MKW_DX_STUB_DIR})
if(MKW_PLATFORM_WINDOWS)
target_include_directories(${t} PRIVATE ${MKW_DX_STUB_DIR})
endif()
# Aurora's own sources include Windows headers and call std::min/max;
# they relied on the old project-wide NOMINMAX that no longer leaks
# into this subtree, so the define is applied per target here.
@@ -198,6 +241,18 @@ endif()
# a registration file is silently never compiled and never errors. The stale-glob
# failure mode is worth far more than the milliseconds.
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")
else()
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
endif()
set(MKW_PLATFORM_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/platform/host_platform.cpp")
set(MKW_BASE_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/base_product.cpp")
set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro_rewind_product.cpp")
# The host ISA guard is the one translation unit that must not receive the
@@ -205,25 +260,113 @@ set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro
# mkw_runtime_common. See cmake/PublicProducts.cmake and the file's own header.
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_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)
# 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()
# The translator emits the complete, content-addressed source graph. Consuming
# this one manifest keeps configure independent of the 28k generated function
# files and of optional Retro Rewind artifacts such as code.map.
set(MKW_TRANSLATED_SHARD_MANIFEST
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
CACHE FILEPATH "Translator-owned aggregate shard manifest")
if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
message(FATAL_ERROR
"Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. "
"Run Translator.Cli emit-build-shards first; see translator/README.md.")
endif()
include("${MKW_TRANSLATED_SHARD_MANIFEST}")
set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS})
message(STATUS
"Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; "
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; "
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions")
if(MKW_BUILD_PRODUCTS)
set(MKW_TRANSLATED_SHARD_MANIFEST
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
CACHE FILEPATH "Translator-owned aggregate shard manifest")
# The prebuilt export only needs the aurora/third-party closure configured above, so a
# packaging machine without a translation stops here instead of failing.
if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
message(STATUS "No translator shard manifest; configuring the native prebuilt export only")
return()
endif()
if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
message(FATAL_ERROR
"Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. "
"Run Translator.Cli emit-build-shards first; see translator/README.md.")
endif()
include("${MKW_TRANSLATED_SHARD_MANIFEST}")
set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS})
message(STATUS
"Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; "
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; "
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions")
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake")
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake")
else()
if(MKW_PLATFORM_MACOS)
# Compile-only audit of native runtime sources. It deliberately avoids
# translated products until their host dependencies are portable.
# These sources depend on generated/RuntimeConfig.h, which is emitted for
# a particular game by the translator and is intentionally unavailable
# in this platform-only configuration.
set(MKW_MACOS_NATIVE_AUDIT_SOURCES ${SOURCES})
list(REMOVE_ITEM MKW_MACOS_NATIVE_AUDIT_SOURCES
"${CMAKE_CURRENT_LIST_DIR}/src/abi_bridge.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/hle/os/os_alarm.cpp")
add_library(mkw_macos_native_compile OBJECT ${MKW_MACOS_NATIVE_AUDIT_SOURCES})
target_include_directories(mkw_macos_native_compile PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include" "${CMAKE_CURRENT_LIST_DIR}/src"
"${CMAKE_CURRENT_LIST_DIR}/.." "${CMAKE_CURRENT_LIST_DIR}/../aurora-main/include")
target_compile_features(mkw_macos_native_compile PRIVATE cxx_std_20)
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)
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
endif()
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
message(STATUS "Translated product targets disabled (MKW_BUILD_PRODUCTS=OFF)")
endif()
+10
View File
@@ -51,6 +51,16 @@ foreach(_dir IN LISTS _mkw_np_includes)
endif()
endforeach()
# Dawn's own packaged config (DawnTargets.cmake) links dawn::webgpu_dawn against
# Threads::Threads directly. A from-source aurora build resolves that as a side effect of
# add_subdirectory(aurora-main) pulling in Dawn's own CMakeLists.txt; this mode never runs that
# subdirectory at all, so nothing else would ever define it (verified directly: configuring without
# this fails with "The link interface of target dawn::webgpu_dawn contains: Threads::Threads but
# the target was not found"). find_package(Threads) is one of CMake's most basic finder modules and
# is a no-op on Windows (its threading support is already part of the CRT), so this is safe on
# every platform this package format targets, not just the one that first hit the failure.
find_package(Threads REQUIRED)
# Dawn is a prebuilt package on both sides; resolve the same install tree the
# package was built against so its imported target (and therefore
# webgpu_dawn.dll / dxcompiler.dll / dxil.dll) is available exactly as a
+78 -35
View File
@@ -25,6 +25,11 @@ if(EXISTS "${DATA_INIT_BLOB_ASM}")
endif()
list(REMOVE_DUPLICATES SOURCES)
if(MKW_PLATFORM_MACOS)
find_library(MKW_IOKIT_FRAMEWORK IOKit REQUIRED)
find_library(MKW_COREFOUNDATION_FRAMEWORK CoreFoundation REQUIRED)
endif()
function(mkw_apply_common_compile_options target)
target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe)
endfunction()
@@ -76,8 +81,14 @@ target_compile_definitions(mkw_runtime_common PRIVATE
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
target_link_libraries(mkw_runtime_common PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
target_link_libraries(mkw_runtime_common PRIVATE mkw::pugixml mkw::toml11 mkw::cryptopp)
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
if(MKW_PLATFORM_WINDOWS)
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
elseif(MKW_PLATFORM_LINUX)
# ${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.
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco ${CMAKE_DL_LIBS})
endif()
if(MKW_CPPWINRT_INCLUDE_DIR)
if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
message(FATAL_ERROR
@@ -121,16 +132,16 @@ set_target_properties(mkw_runtime_common PROPERTIES UNITY_BUILD ON UNITY_BUILD_M
target_precompile_headers(mkw_runtime_common PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
mkw_apply_common_compile_options(mkw_runtime_common)
# Host ISA guard. Everything in MKW_ALL_BUILD_TARGETS below is compiled with
# -march=x86-64-v3; this object library deliberately is not, which
# is the whole point of keeping it out of mkw_runtime_common. It runs a CPUID
# check from a C initializer so an unsupported machine gets a readable error
# instead of an illegal-instruction crash. Excluded from the unity build and the
# precompiled header because both are produced with the owning target's flags.
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)
# 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.
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)
endif()
if(NOT MKW_BASE_COMMON_SHARDS)
message(FATAL_ERROR "Translator build graph contains no shared base shards")
@@ -170,7 +181,9 @@ function(mkw_configure_product target)
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_runtime_common>)
# Startup CPU check. Must stay a separate object library so it keeps the
# plain baseline ISA while everything around it is built for x86-64-v3.
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_cpu_baseline>)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_cpu_baseline>)
endif()
target_include_directories(${target} PRIVATE
"${MKW_RUNTIME_SOURCE_DIR}/include"
"${MKW_RUNTIME_SOURCE_DIR}/src"
@@ -186,10 +199,14 @@ function(mkw_configure_product target)
# include the same fat translated headers; bound them by the same pool.
mkw_bound_translated_compiles(${target})
target_link_libraries(${target} PRIVATE
mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp)
mkw_platform mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp)
target_link_libraries(${target} PRIVATE
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}")
endif()
if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
aurora_copy_runtime_dlls(${target})
@@ -204,26 +221,40 @@ function(mkw_configure_product target)
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
endif()
target_link_libraries(${target} PRIVATE
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
if(MKW_PLATFORM_WINDOWS)
target_link_libraries(${target} PRIVATE
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
foreach(runtime_dll libc++.dll libunwind.dll)
execute_process(
COMMAND "${CMAKE_CXX_COMPILER}" "--print-file-name=${runtime_dll}"
OUTPUT_VARIABLE runtime_dll_path
OUTPUT_STRIP_TRAILING_WHITESPACE)
if(NOT EXISTS "${runtime_dll_path}")
get_filename_component(mkw_compiler_bin "${CMAKE_CXX_COMPILER}" DIRECTORY)
set(runtime_dll_path "${mkw_compiler_bin}/${runtime_dll}")
endif()
if(NOT EXISTS "${runtime_dll_path}")
message(FATAL_ERROR "llvm-mingw runtime DLL not found: ${runtime_dll}")
endif()
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${runtime_dll_path}" $<TARGET_FILE_DIR:${target}>)
endforeach()
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
elseif(MKW_PLATFORM_LINUX)
# mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o
# files via $<TARGET_OBJECTS:>, which does not propagate mkw_runtime_common's own
# target_link_libraries (object libraries don't carry usage requirements to a consumer
# that isn't itself linked against as a target). fiber_manager.cpp's co_* calls live in
# those objects, so the actual executable link needs mkw::libco directly, same as it
# needs it independently of that first `if(WIN32)` branch above. ${CMAKE_DL_LIBS} is
# here for the same reason: music_attenuation.cpp's dlopen(libdbus-1) lives in those
# objects (empty string on glibc >= 2.34, where dl* is in libc).
target_link_libraries(${target} PRIVATE mkw::libco ${CMAKE_DL_LIBS})
endif()
if(MKW_PLATFORM_WINDOWS)
foreach(runtime_dll libc++.dll libunwind.dll)
execute_process(
COMMAND "${CMAKE_CXX_COMPILER}" "--print-file-name=${runtime_dll}"
OUTPUT_VARIABLE runtime_dll_path
OUTPUT_STRIP_TRAILING_WHITESPACE)
if(NOT EXISTS "${runtime_dll_path}")
get_filename_component(mkw_compiler_bin "${CMAKE_CXX_COMPILER}" DIRECTORY)
set(runtime_dll_path "${mkw_compiler_bin}/${runtime_dll}")
endif()
if(NOT EXISTS "${runtime_dll_path}")
message(FATAL_ERROR "llvm-mingw runtime DLL not found: ${runtime_dll}")
endif()
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${runtime_dll_path}" $<TARGET_FILE_DIR:${target}>)
endforeach()
endif()
set(MKW_WII_BOOTSTRAP_SOURCE_DIR "${MKW_RUNTIME_SOURCE_DIR}/assets/wii")
if(NOT EXISTS "${MKW_WII_BOOTSTRAP_SOURCE_DIR}/shared2/wc24")
@@ -282,11 +313,23 @@ 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.
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
else()
set(MKW_BASELINE_ARCH_FLAG "")
endif()
set(MKW_ALL_BUILD_TARGETS
mkw_runtime_common mkw_base_shared mkw_base_sensitive mkw_retro_sensitive
mkw_retro_rewind_functions WiiCompiled RetroRewind)
foreach(target IN LISTS MKW_ALL_BUILD_TARGETS)
if(TARGET ${target})
target_compile_options(${target} PRIVATE -march=x86-64-v3)
if(TARGET ${target} AND MKW_BASELINE_ARCH_FLAG)
target_compile_options(${target} PRIVATE ${MKW_BASELINE_ARCH_FLAG})
endif()
endforeach()
+2 -1
View File
@@ -81,7 +81,8 @@ inline bool WriteSerial(const std::filesystem::path& path, const std::string& se
return false;
}
const std::filesystem::path temporary = path.string() + ".tmp";
std::filesystem::path temporary = path;
temporary += ".tmp";
{
std::ofstream output(temporary, std::ios::trunc);
if (!output) {
@@ -0,0 +1,21 @@
#pragma once
#include <SDL3/SDL_events.h>
// Press-to-bind setup for joysticks SDL either doesn't recognize as gamepads or
// recognizes with a mapping that lacks the analog stick (e.g. raphnet adapters).
// The wizard produces a standard SDL gamepad mapping, applies it live, and
// persists it to gamecontrollerdb.txt in the user data directory.
namespace controller_mapping_wizard {
void LoadPersistedMappings();
void HandleSdlEvent(const SDL_Event& event);
// Lists devices that need setup inside the controller settings menu.
void DrawSetupList();
// Draws the wizard window when active; call once per overlay frame.
void Draw();
bool IsActive();
} // namespace controller_mapping_wizard
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <cstdint>
#include <string>
// Host implementation of Dolphin's /dev/dolphin Discord contract. The guest
// sends only strings and big-endian integer fields; the IPC wire protocol is
// owned here so translated game code never needs host SDK headers.
namespace DiscordPresence {
struct Activity {
std::string details;
std::string state;
std::string largeImageKey;
std::string largeImageText;
std::string smallImageKey;
std::string smallImageText;
int64_t startTimestamp = 0;
int64_t endTimestamp = 0;
uint32_t partySize = 0;
uint32_t partyMax = 0;
};
void Initialize(const std::string& basicClientId, const std::string& basicTitle);
void SetClient(const std::string& clientId);
void SetActivity(Activity activity);
void Reset();
void Shutdown();
} // namespace DiscordPresence
+11 -4
View File
@@ -6,6 +6,7 @@
#include <mutex>
#include <thread>
#include <unordered_map>
#include <vector>
#if defined(_WIN32)
#ifndef NOMINMAX
@@ -22,7 +23,7 @@
// Forward declarations
struct CpuContext;
// GuestFiberManager: each guest OSThread maps to a Windows Fiber. A scheduler fiber picks
// GuestFiberManager: each guest OSThread maps to a host context. A scheduler context picks
// which guest fiber runs; a real timer thread queues VI retraces at the VI cadence. Guest
// threads only switch at explicit yield points (OSSleepThread, OSYieldThread, ...), matching
// Wii cooperative semantics exactly.
@@ -39,7 +40,7 @@ enum class ThreadState : uint32_t {
// Information about a guest fiber
struct GuestFiber {
void* fiber = nullptr; // Windows fiber handle
void* fiber = nullptr; // Host context handle
uint32_t entryPoint = 0; // Thread entry function
uint32_t entryArg = 0; // Argument to entry function
CpuContext cpuContext{}; // Saved CPU context for this fiber
@@ -103,11 +104,18 @@ private:
#else
static void FiberProc(void* param);
#endif
// Switch from whichever fiber is currently active straight to the scheduler fiber, without
// the SwitchToThread bookkeeping (CPU context save/restore, s_currentGuestThread). Used for
// in-fiber yields that aren't a real guest thread switch: waiting out the EGG::Thread::start
// deferral loop, and returning control on natural thread exit.
static void SwitchToScheduler();
// Internal state
static std::mutex s_mutex;
static std::unordered_map<uint32_t, GuestFiber> s_fibers;
static std::vector<void*> s_fibersPendingDelete;
// The scheduler's own host context. Its opaque handle is supplied by the
// active HostContext backend, so one field serves every supported host.
static void* s_schedulerFiber;
static uint32_t s_currentGuestThread;
static bool s_initialized;
@@ -123,4 +131,3 @@ private:
extern std::atomic<uint32_t> g_viRetracePendingCount;
} // namespace Fiber
+51 -5
View File
@@ -12,10 +12,33 @@
namespace GuestFlat {
// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load
// of a global. 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the
// usual image/heap placement.
// of a global.
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
inline constexpr size_t kGuestPageSize = 0x1000;
#if defined(__x86_64__)
// 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap
// placement.
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'1000'0000'0000ull;
#elif defined(__aarch64__) && defined(__APPLE__)
// Keep this well above the low address ranges that Darwin's ASLR may use for
// a PIE executable and its shared cache. Apple Silicon's user VA is wider
// than Linux's 39-bit minimum, so this 512 GiB region is available while the
// Linux AArch64 target retains its 64 GiB placement below.
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0080'0000'0000ull;
#elif defined(__aarch64__)
// 16 TiB (this arch's x86_64 sibling value) is unreachable on any AArch64
// kernel configured for 39-bit virtual addresses (512 GiB ceiling) - common on
// older/embedded targets (e.g. this project's own tested Jetson/L4T board, kernel
// 4.9). There mmap() silently ignores a hint above the ceiling and hands back an
// address near the top of the real range instead, which this module's caller
// then rejects as "already occupied". 64 GiB is reachable on every AArch64 VA
// width in real use (39-bit minimum and up) and was confirmed via direct mmap
// probing to sit far below where the PIE image, heap, shared libraries and
// stack actually land (all clustered above ~340 GiB on a 39-bit/512 GiB system).
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0010'0000'0000ull;
#else
#error "guest_flat_memory.h has no fixed flat guest base chosen for this architecture"
#endif
#define MKW_FLAT_GUEST_BASE (reinterpret_cast<uint8_t*>(GuestFlat::kFixedFlatGuestBase))
@@ -42,6 +65,25 @@ struct FaultCounters {
// True once the reservation exists and translated code may use the flat path.
bool IsActive();
// True when a host VM page covers more than one 4 KiB Wii page. In that
// configuration, guest-view page protection cannot safely represent per-Wii-
// page MMIO, deferred-read, or executable-write state, so general translated
// accesses must use the checked Memory::* path.
// Windows user mode and x86-64 always use a 4 KiB base page, so those builds
// fold this to a compile-time false: it appears in every flat access and must
// not become a hot-path load. Only AArch64, where the page size is a kernel
// configuration (4/16/64 KiB), has to probe it at runtime.
#if defined(_WIN32) || defined(__x86_64__)
#define MKW_GUEST_FLAT_FIXED_PAGE_SIZE 1
#endif
#if defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
inline constexpr bool RequiresCheckedAccess() noexcept { return false; }
#else
extern bool g_requiresCheckedAccess;
inline bool RequiresCheckedAccess() noexcept { return g_requiresCheckedAccess; }
#endif
// Reserves the 4 GiB space (once per process) and maps every requested region
// into both views. Throws std::runtime_error with a precise diagnosis when the
// reservation, the section objects or a view cannot be created - a silent
@@ -73,8 +115,12 @@ FaultCounters Counters();
void LogFaultSummary() noexcept;
// Returns true when the access violation was a guest-space fault this module
// resolved; the caller must then resume execution. `exceptionPointers` is a
// Windows EXCEPTION_POINTERS*.
bool HandleAccessViolation(void* exceptionPointers) noexcept;
// resolved; the caller must then resume execution. `faultAddress` is the raw
// host pointer the access violation trapped on (Windows: ExceptionInformation[1];
// POSIX: siginfo_t::si_addr) and `isWrite` is whether it was a write access
// (Windows: ExceptionInformation[0] != 0; POSIX: derived from the ucontext).
// The platform-specific handler that calls this is expected to have already
// done that extraction - this function only ever works with the parsed pair.
bool HandleAccessViolation(void* faultAddress, bool isWrite) noexcept;
} // namespace GuestFlat
+2 -2
View File
@@ -20,14 +20,14 @@
namespace DvdFstContract {
struct RegisteredFile {
std::string hostPath;
std::filesystem::path hostPath;
std::string dvdPath;
uint32_t size = 0;
uint32_t discOffsetWords = 0;
};
struct IndexedEntry {
std::string hostPath;
std::filesystem::path hostPath;
std::string dvdPath;
uint32_t size = 0;
uint32_t discOffsetWords = 0;
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#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.
namespace HostContext {
using Handle = void*;
using Entry = void (*)(void*);
bool InitializeScheduler(Handle* scheduler);
void ShutdownScheduler(Handle scheduler);
Handle Create(std::size_t stackSize, Entry entry, void* argument);
void Destroy(Handle context);
bool IsCurrent(Handle context);
void Switch(Handle target);
} // namespace HostContext
+18 -1
View File
@@ -4,18 +4,35 @@
#include <cstdint>
#define MKW_RESTRICT __restrict
#if defined(__x86_64__)
#include <immintrin.h>
#elif defined(__aarch64__)
#include <arm_neon.h>
#else
#error "ppc_isa_config.h has no SIMD intrinsics header for this architecture"
#endif
inline constexpr bool MkwStateFreeAbiEnabled(uint32_t) noexcept
{
return true;
}
#if defined(_WIN32)
#define MKW_PPC_FORCE_INLINE __forceinline
#define MKW_PPC_NO_INLINE __declspec(noinline)
#define MKW_PPC_INTERNAL_CALL __regcall
#else
// __forceinline/__declspec are MS-extension keywords Clang only recognizes when targeting
// Windows (MSVC or mingw); native Linux Clang needs the GNU-attribute spellings instead.
// __regcall has no portable non-Windows equivalent worth chasing here - the extra register
// args it saves matter for the hot PPC interpreter loop on Windows, but plain calls are fine
// elsewhere.
#define MKW_PPC_FORCE_INLINE __attribute__((always_inline)) inline
#define MKW_PPC_NO_INLINE __attribute__((noinline))
#define MKW_PPC_INTERNAL_CALL
#endif
#define MKW_PPC_ALWAYS_INLINE_BODY __attribute__((always_inline))
#define MKW_PPC_COLD __attribute__((cold))
#define MKW_PPC_INTERNAL_CALL __regcall
using MkwStateFreeResult2 = uint64_t __attribute__((ext_vector_type(2)));
+1 -1
View File
@@ -62,7 +62,7 @@ public:
{
g_currentCpuContext = ctx;
savedMxcsr_ = _mm_getcsr();
savedMxcsr_ = MkwGetHostFpControl();
if (ctx != nullptr)
MkwApplyHostNiMode(ctx->fpscr);
}
+229 -31
View File
@@ -78,37 +78,71 @@ inline void PpcSetPairedFprInline(PPC_FPR& fpr, double packed)
fpr.d = packed;
}
// Must stay inside the XMM register domain. Bitcasting through a 64-bit GPR added a movq
#if defined(__x86_64__)
using PpcPairVec = __m128;
#elif defined(__aarch64__)
// Only 2 lanes are ever meaningful (a PPC paired-single register), so a 2-lane float32x2_t
// (one 64-bit D register) is a more natural fit than mirroring x86's 128-bit register usage.
using PpcPairVec = float32x2_t;
#endif
// Must stay inside the vector register domain. Bitcasting through a 64-bit GPR added a movq
// domain crossing on every paired-single op (630 in the THP IDCT region alone); a double
// local already lives in an XMM register, so these casts compile to nothing.
inline __m128 PpcPsToM128Inline(double value)
// local already lives in a vector register, so these casts compile to nothing.
inline PpcPairVec PpcPsToM128Inline(double value)
{
#if defined(__x86_64__)
return _mm_castpd_ps(_mm_set_sd(value));
#elif defined(__aarch64__)
return vreinterpret_f32_f64(vdup_n_f64(value));
#endif
}
inline double PpcM128ToPsInline(__m128 value)
inline double PpcM128ToPsInline(PpcPairVec value)
{
#if defined(__x86_64__)
return _mm_cvtsd_f64(_mm_castps_pd(value));
#elif defined(__aarch64__)
return vget_lane_f64(vreinterpret_f64_f32(value), 0);
#endif
}
inline __m128 PpcBroadcastPs0Inline(double value)
inline PpcPairVec PpcBroadcastPs0Inline(double value)
{
#if defined(__x86_64__)
const __m128 lanes = PpcPsToM128Inline(value);
return _mm_shuffle_ps(lanes, lanes, _MM_SHUFFLE(1, 1, 1, 1));
#elif defined(__aarch64__)
// ps0 lives in lane 1 (see the lane-accessor comment below).
return vdup_lane_f32(PpcPsToM128Inline(value), 1);
#endif
}
inline __m128 PpcBroadcastPs1Inline(double value)
inline PpcPairVec PpcBroadcastPs1Inline(double value)
{
#if defined(__x86_64__)
const __m128 lanes = PpcPsToM128Inline(value);
return _mm_shuffle_ps(lanes, lanes, _MM_SHUFFLE(0, 0, 0, 0));
#elif defined(__aarch64__)
// ps1 is already lane 0.
return vdup_lane_f32(PpcPsToM128Inline(value), 0);
#endif
}
inline __m128 PpcNegateNonNanLanesInline(__m128 value)
inline PpcPairVec PpcNegateNonNanLanesInline(PpcPairVec value)
{
#if defined(__x86_64__)
const __m128 signMask = _mm_castsi128_ps(_mm_set1_epi32(static_cast<int>(0x80000000u)));
const __m128 negated = _mm_xor_ps(value, signMask);
const __m128 ordered = _mm_cmpord_ps(value, value);
return _mm_or_ps(_mm_and_ps(ordered, negated), _mm_andnot_ps(ordered, value));
#elif defined(__aarch64__)
const uint32x2_t signMask = vdup_n_u32(0x80000000u);
const float32x2_t negated = vreinterpret_f32_u32(veor_u32(vreinterpret_u32_f32(value), signMask));
// NEON has no direct "ordered compare"; a value compares equal to itself iff it's not NaN.
const uint32x2_t ordered = vceq_f32(value, value);
return vbsl_f32(ordered, negated, value);
#endif
}
// Paired-single lane accessors. The packed double's LOW 32 bits hold ps1 and HIGH 32 bits
@@ -119,20 +153,34 @@ inline __m128 PpcNegateNonNanLanesInline(__m128 value)
inline float PpcGetPs0Inline(double value)
{
// ps0 lives in lane 1; PpcBroadcastPs0Inline already splats it.
#if defined(__x86_64__)
return _mm_cvtss_f32(PpcBroadcastPs0Inline(value));
#elif defined(__aarch64__)
return vget_lane_f32(PpcBroadcastPs0Inline(value), 0);
#endif
}
inline float PpcGetPs1Inline(double value)
{
// ps1 is already lane 0 of the packed representation.
#if defined(__x86_64__)
return _mm_cvtss_f32(PpcPsToM128Inline(value));
#elif defined(__aarch64__)
return vget_lane_f32(PpcPsToM128Inline(value), 0);
#endif
}
inline double PpcPackPairedInline(float ps0, float ps1)
{
#if defined(__x86_64__)
// _mm_unpacklo_ps(x, y) -> { x[0], y[0], x[1], y[1] }, so lane 0 becomes
// ps1 and lane 1 becomes ps0, matching the union layout bit for bit.
return PpcM128ToPsInline(_mm_unpacklo_ps(_mm_set_ss(ps1), _mm_set_ss(ps0)));
#elif defined(__aarch64__)
// Lane 0 = ps1, lane 1 = ps0, matching the union layout bit for bit.
const float32x2_t lane0 = vdup_n_f32(ps1);
return PpcM128ToPsInline(vset_lane_f32(ps0, lane0, 1));
#endif
}
// FPSCR[NI] is modeled by MXCSR FTZ/DAZ, so arithmetic output flushing compiles to nothing.
@@ -148,14 +196,24 @@ inline float PpcForceSingleValueInline(double value)
// FPSCR[NI] flushes an exact pre-round single-subnormal even when rounding would promote it
// to the smallest normal. g_mkwNiFlushThreshold (2^-126 active, 0.0 inactive) turns the
// flush into a branchless mask: a set compare lane keeps just the sign bit, a clear lane
// passes the value to CVTSD2SS. DAZ (set exactly when NI is) makes the compare itself treat
// a subnormal as zero, matching the mask's answer.
// passes the value to the double->float conversion. DAZ/FZ (set exactly when NI is) makes
// the compare itself treat a subnormal as zero, matching the mask's answer.
#if defined(__x86_64__)
const __m128d v = _mm_set_sd(value);
const __m128d signMask = _mm_set_sd(-0.0);
const __m128d magnitude = _mm_andnot_pd(signMask, v);
const __m128d flush = _mm_cmplt_sd(magnitude, _mm_set_sd(g_mkwNiFlushThreshold));
const __m128d kept = _mm_andnot_pd(_mm_andnot_pd(signMask, flush), v);
return static_cast<float>(_mm_cvtsd_f64(kept));
#elif defined(__aarch64__)
const float64x1_t v = vdup_n_f64(value);
const uint64x1_t signMask = vdup_n_u64(0x8000000000000000ULL);
const float64x1_t magnitude = vreinterpret_f64_u64(vbic_u64(vreinterpret_u64_f64(v), signMask));
const uint64x1_t flush = vclt_f64(magnitude, vdup_n_f64(g_mkwNiFlushThreshold));
const uint64x1_t signOnly = vand_u64(vreinterpret_u64_f64(v), signMask);
const uint64x1_t kept = vbsl_u64(flush, signOnly, vreinterpret_u64_f64(v));
return static_cast<float>(vget_lane_f64(vreinterpret_f64_u64(kept), 0));
#endif
}
inline float PpcFlushSingleForNiInline(float value)
@@ -468,15 +526,72 @@ inline double PpcFnmsubsInline(double a, double c, double b)
return static_cast<double>(std::isnan(result) ? result : -result);
}
#if defined(__aarch64__)
// x86 resolves a single NaN source operand (SNaN or QNaN alike) to that operand quieted with
// sign and payload kept, and an invalid op with no NaN input to 0xFFC00000; NEON prefers SNaNs,
// checks the accumulator first, and yields the positive default NaN 0x7FC00000. NaN result
// lanes (rare: one branch on the pair) are therefore rewritten to the x86 answer, keeping the
// two hosts bit-identical for every case where x86 itself is deterministic. With two or more
// NaN operands even x86 is not: the winning operand depends on which FMA form / commuted
// operand order clang picked per call site, so this resolver's fixed first-operand priority is
// one of the answers a real x86 build can give, not a guaranteed match.
inline uint64_t PpcPairNanLaneBitsInline(PpcPairVec value)
{
return vget_lane_u64(vreinterpret_u64_u32(vmvn_u32(vceq_f32(value, value))), 0);
}
inline PpcPairVec PpcQuietPairInline(PpcPairVec value)
{
return vreinterpret_f32_u32(vorr_u32(vreinterpret_u32_f32(value), vdup_n_u32(0x00400000u)));
}
inline PpcPairVec PpcResolveNanLanesInline(PpcPairVec result, PpcPairVec op1, PpcPairVec op2)
{
const uint32x2_t op1Nan = vmvn_u32(vceq_f32(op1, op1));
const uint32x2_t op2Nan = vmvn_u32(vceq_f32(op2, op2));
PpcPairVec replacement = vreinterpret_f32_u32(vdup_n_u32(0xFFC00000u));
replacement = vbsl_f32(op2Nan, PpcQuietPairInline(op2), replacement);
replacement = vbsl_f32(op1Nan, PpcQuietPairInline(op1), replacement);
const uint32x2_t resultNan = vmvn_u32(vceq_f32(result, result));
return vbsl_f32(resultNan, replacement, result);
}
inline PpcPairVec PpcResolveNanLanes3Inline(
PpcPairVec result, PpcPairVec op1, PpcPairVec op2, PpcPairVec op3)
{
const uint32x2_t op3Nan = vmvn_u32(vceq_f32(op3, op3));
PpcPairVec replacement = vreinterpret_f32_u32(vdup_n_u32(0xFFC00000u));
replacement = vbsl_f32(op3Nan, PpcQuietPairInline(op3), replacement);
const uint32x2_t op2Nan = vmvn_u32(vceq_f32(op2, op2));
replacement = vbsl_f32(op2Nan, PpcQuietPairInline(op2), replacement);
const uint32x2_t op1Nan = vmvn_u32(vceq_f32(op1, op1));
replacement = vbsl_f32(op1Nan, PpcQuietPairInline(op1), replacement);
const uint32x2_t resultNan = vmvn_u32(vceq_f32(result, result));
return vbsl_f32(resultNan, replacement, result);
}
#endif // defined(__aarch64__)
inline PpcPairVec PpcMulPairInline(PpcPairVec lhs, PpcPairVec rhs)
{
#if defined(__x86_64__)
return _mm_mul_ps(lhs, rhs);
#elif defined(__aarch64__)
const PpcPairVec result = vmul_f32(lhs, rhs);
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
return PpcResolveNanLanesInline(result, lhs, rhs);
return result;
#endif
}
inline double PPC_PsMulInline(double lhs, double rhs)
{
return PpcFlushPairedForNiInline(
PpcM128ToPsInline(_mm_mul_ps(PpcPsToM128Inline(lhs), PpcPsToM128Inline(rhs))));
PpcM128ToPsInline(PpcMulPairInline(PpcPsToM128Inline(lhs), PpcPsToM128Inline(rhs))));
}
inline double PPC_PsMulNoNiInline(double lhs, double rhs)
{
return PpcM128ToPsInline(_mm_mul_ps(PpcPsToM128Inline(lhs), PpcPsToM128Inline(rhs)));
return PpcM128ToPsInline(PpcMulPairInline(PpcPsToM128Inline(lhs), PpcPsToM128Inline(rhs)));
}
// The paired madd family lowers to one hardware FMA. Semantics match the scalar lanes exactly: a
@@ -485,9 +600,37 @@ inline double PPC_PsMulNoNiInline(double lhs, double rhs)
// PpcNegateNonNanLanesInline. NI flushing is handled by MXCSR (see
// MkwApplyHostNiMode), so the NI and NoNi entry points are identical here.
// Fused multiply-add/subtract on a pair. NEON's vfma_f32(acc, a, b) = acc + a*b has an
// accumulator-first operand order, unlike x86's _mm_fmadd_ps(a, b, c) = a*b + c - msub is
// therefore an fma against a negated accumulator. That vneg would flip a sole-NaN subtractor's
// sign, which x86's vfmsub does not do, so the NaN resolver receives the original subtractor.
inline PpcPairVec PpcFmaddPairInline(PpcPairVec multiplicand, PpcPairVec multiplier, PpcPairVec addend)
{
#if defined(__x86_64__)
return _mm_fmadd_ps(multiplicand, multiplier, addend);
#elif defined(__aarch64__)
const PpcPairVec result = vfma_f32(addend, multiplicand, multiplier);
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
return PpcResolveNanLanes3Inline(result, multiplicand, multiplier, addend);
return result;
#endif
}
inline PpcPairVec PpcFmsubPairInline(PpcPairVec multiplicand, PpcPairVec multiplier, PpcPairVec subtractor)
{
#if defined(__x86_64__)
return _mm_fmsub_ps(multiplicand, multiplier, subtractor);
#elif defined(__aarch64__)
const PpcPairVec result = vfma_f32(vneg_f32(subtractor), multiplicand, multiplier);
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
return PpcResolveNanLanes3Inline(result, multiplicand, multiplier, subtractor);
return result;
#endif
}
inline double PPC_PsMsubInline(double multiplicand, double multiplier, double subtractor)
{
return PpcM128ToPsInline(_mm_fmsub_ps(
return PpcM128ToPsInline(PpcFmsubPairInline(
PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(subtractor)));
}
@@ -498,7 +641,7 @@ inline double PPC_PsMsubNoNiInline(double multiplicand, double multiplier, doubl
inline double PPC_PsMaddInline(double multiplicand, double multiplier, double addend)
{
return PpcM128ToPsInline(_mm_fmadd_ps(
return PpcM128ToPsInline(PpcFmaddPairInline(
PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(addend)));
}
@@ -509,19 +652,19 @@ inline double PPC_PsMaddNoNiInline(double multiplicand, double multiplier, doubl
inline double PPC_PsMadds0Inline(double multiplicand, double multiplier, double addend)
{
return PpcM128ToPsInline(_mm_fmadd_ps(
return PpcM128ToPsInline(PpcFmaddPairInline(
PpcPsToM128Inline(multiplicand), PpcBroadcastPs0Inline(multiplier), PpcPsToM128Inline(addend)));
}
inline double PPC_PsMadds1Inline(double multiplicand, double multiplier, double addend)
{
return PpcM128ToPsInline(_mm_fmadd_ps(
return PpcM128ToPsInline(PpcFmaddPairInline(
PpcPsToM128Inline(multiplicand), PpcBroadcastPs1Inline(multiplier), PpcPsToM128Inline(addend)));
}
inline double PPC_PsNmsubInline(double multiplicand, double multiplier, double subtractor)
{
return PpcM128ToPsInline(PpcNegateNonNanLanesInline(_mm_fmsub_ps(
return PpcM128ToPsInline(PpcNegateNonNanLanesInline(PpcFmsubPairInline(
PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(subtractor))));
}
@@ -532,20 +675,20 @@ inline double PPC_PsNmsubNoNiInline(double multiplicand, double multiplier, doub
inline double PPC_PsNmaddInline(double multiplicand, double multiplier, double addend)
{
return PpcM128ToPsInline(PpcNegateNonNanLanesInline(_mm_fmadd_ps(
return PpcM128ToPsInline(PpcNegateNonNanLanesInline(PpcFmaddPairInline(
PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(addend))));
}
inline double PPC_PsMuls0Inline(double aValue, double cValue)
{
return PpcFlushPairedForNiInline(PpcM128ToPsInline(
_mm_mul_ps(PpcPsToM128Inline(aValue), PpcBroadcastPs0Inline(cValue))));
PpcMulPairInline(PpcPsToM128Inline(aValue), PpcBroadcastPs0Inline(cValue))));
}
inline double PPC_PsMuls1Inline(double aValue, double cValue)
{
return PpcFlushPairedForNiInline(PpcM128ToPsInline(
_mm_mul_ps(PpcPsToM128Inline(aValue), PpcBroadcastPs1Inline(cValue))));
PpcMulPairInline(PpcPsToM128Inline(aValue), PpcBroadcastPs1Inline(cValue))));
}
inline PPC_FPR PpcMakePairedResultInline(float ps0, float ps1);
@@ -571,50 +714,105 @@ inline double PPC_PsToScalarInline(double value)
return static_cast<double>(PpcGetPs0Inline(value));
}
// ps_merge* are pure lane selections (result.ps0 from frA, result.ps1 from frB); with lane 0
// == ps1 and lane 1 == ps0, two shuffles build the result bit-exact instead of round-tripping
// through the pack helper.
// ps_merge* are pure lane selections (result.ps0 from frA, result.ps1 from frB). On x86 two
// shuffles build the result bit-exact without round-tripping through the pack helper; NEON has
// no equally cheap 2-lane general shuffle, so its branch expresses the exact same selection
// (verified against the x86 comments below, lane for lane) directly in terms of the portable
// Get/Pack accessors instead.
inline double PPC_PsMerge00Inline(double aValue, double bValue)
{
// lane0 = b.ps0 (b lane 1), lane1 = a.ps0 (a lane 1)
// result.ps0 = a.ps0, result.ps1 = b.ps0 (lane0 = b.ps0/b lane1, lane1 = a.ps0/a lane1)
#if defined(__x86_64__)
const __m128 gathered = _mm_shuffle_ps(
PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(1, 1, 1, 1));
return PpcM128ToPsInline(_mm_shuffle_ps(gathered, gathered, _MM_SHUFFLE(0, 0, 2, 0)));
#elif defined(__aarch64__)
return PpcPackPairedInline(PpcGetPs0Inline(aValue), PpcGetPs0Inline(bValue));
#endif
}
inline double PPC_PsMerge01Inline(double aValue, double bValue)
{
// lane0 = b.ps1 (b lane 0), lane1 = a.ps0 (a lane 1)
// result.ps0 = a.ps0, result.ps1 = b.ps1 (lane0 = b.ps1/b lane0, lane1 = a.ps0/a lane1)
#if defined(__x86_64__)
const __m128 gathered = _mm_shuffle_ps(
PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(1, 1, 0, 0));
return PpcM128ToPsInline(_mm_shuffle_ps(gathered, gathered, _MM_SHUFFLE(0, 0, 2, 0)));
#elif defined(__aarch64__)
return PpcPackPairedInline(PpcGetPs0Inline(aValue), PpcGetPs1Inline(bValue));
#endif
}
inline double PPC_PsMerge10Inline(double aValue, double bValue)
{
// lane0 = b.ps0 (b lane 1), lane1 = a.ps1 (a lane 0)
// result.ps0 = a.ps1, result.ps1 = b.ps0 (lane0 = b.ps0/b lane1, lane1 = a.ps1/a lane0)
#if defined(__x86_64__)
const __m128 gathered = _mm_shuffle_ps(
PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(0, 0, 1, 1));
return PpcM128ToPsInline(_mm_shuffle_ps(gathered, gathered, _MM_SHUFFLE(0, 0, 2, 0)));
#elif defined(__aarch64__)
return PpcPackPairedInline(PpcGetPs1Inline(aValue), PpcGetPs0Inline(bValue));
#endif
}
inline double PPC_PsMerge11Inline(double aValue, double bValue)
{
// lane0 = b.ps1 (b lane 0), lane1 = a.ps1 (a lane 0): plain unpcklps.
// result.ps0 = a.ps1, result.ps1 = b.ps1 (lane0 = b.ps1/b lane0, lane1 = a.ps1/a lane0):
// plain unpcklps on x86.
#if defined(__x86_64__)
return PpcM128ToPsInline(
_mm_unpacklo_ps(PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue)));
#elif defined(__aarch64__)
return PpcPackPairedInline(PpcGetPs1Inline(aValue), PpcGetPs1Inline(bValue));
#endif
}
inline PpcPairVec PpcAddPairInline(PpcPairVec lhs, PpcPairVec rhs)
{
#if defined(__x86_64__)
return _mm_add_ps(lhs, rhs);
#elif defined(__aarch64__)
const PpcPairVec result = vadd_f32(lhs, rhs);
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
return PpcResolveNanLanesInline(result, lhs, rhs);
return result;
#endif
}
inline PpcPairVec PpcSubPairInline(PpcPairVec lhs, PpcPairVec rhs)
{
#if defined(__x86_64__)
return _mm_sub_ps(lhs, rhs);
#elif defined(__aarch64__)
const PpcPairVec result = vsub_f32(lhs, rhs);
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
return PpcResolveNanLanesInline(result, lhs, rhs);
return result;
#endif
}
inline PpcPairVec PpcDivPairInline(PpcPairVec lhs, PpcPairVec rhs)
{
#if defined(__x86_64__)
return _mm_div_ps(lhs, rhs);
#elif defined(__aarch64__)
const PpcPairVec result = vdiv_f32(lhs, rhs);
if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]]
return PpcResolveNanLanesInline(result, lhs, rhs);
return result;
#endif
}
inline double PPC_PsAddInline(double aValue, double bValue)
{
return PpcFlushPairedForNiInline(
PpcM128ToPsInline(_mm_add_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
PpcM128ToPsInline(PpcAddPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
}
inline double PPC_PsAddNoNiInline(double aValue, double bValue)
{
return PpcM128ToPsInline(
_mm_add_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)));
PpcAddPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)));
}
inline double PPC_PsSelInline(double lhsValue, double controlValue, double rhsValue)
@@ -633,19 +831,19 @@ inline double PPC_PsSelInline(double lhsValue, double controlValue, double rhsVa
inline double PPC_PsSubInline(double aValue, double bValue)
{
return PpcFlushPairedForNiInline(
PpcM128ToPsInline(_mm_sub_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
PpcM128ToPsInline(PpcSubPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
}
inline double PPC_PsSubNoNiInline(double aValue, double bValue)
{
return PpcM128ToPsInline(
_mm_sub_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)));
PpcSubPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)));
}
inline double PPC_PsDivInline(double aValue, double bValue)
{
return PpcFlushPairedForNiInline(
PpcM128ToPsInline(_mm_div_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
PpcM128ToPsInline(PpcDivPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
}
inline double PPC_PsNegInline(double value)

Some files were not shown because too many files have changed in this diff Show More