67 Commits

Author SHA1 Message Date
patchzyy 53d8f71c68 Update README.md 2026-09-12 09:54:51 +02:00
Cristian Boehm 149cfef608 keyboard and mouse support, rebinding overhaul, analog triggers to digital inputs (#162)
* add keyboard support, analog triggers to digital input, and rebinding overhaul

* Update README.md

* Update README.md

readme typo

* implemented code rabbits suggestions

- Preserved NSO GameCube analog triggers.
  - Made modal closure and Escape cancel every rebind kind.
  - Deduced the native button array size; <array> already existed.
  - Centralized axis/sign decoding.
  - Kept threshold updates live, saving only when editing ends.

* add dimming when in settings and add clear mapping button

* Update settings_overlay.cpp

---------

Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
2026-09-10 17:37:38 +02:00
Michael G 25c69ae28e Fix crash from Kamek skip-return hooks (Item Rain crash) (#182)
* fix: Kamek LR-continuation hook discovery and dispatch

* test: cover branching Kamek LR continuations

* review fix

* another review fix

fix: get the new tests to pass
test: expose LR restore and loop continuation regressions

* Update translator/src/Translator.Core/Mods/ContinuationPlanner.cs

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

* test: cover continuation regressions from the new path-sensitive planner

* Update translator/src/Translator.Core/Mods/ContinuationPlanner.cs

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

* test: cover continuation regressions from the new path-sensitive planner

* fix: preserve LR continuation analysis across large handlers and clobbers

* fix: track LR-relative r1 across update-form stack stores

* Harden LR-relative continuation test coverage

* Fix LR/SP continuation state tracking

---------

Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-09-10 15:51:16 +02:00
patchzyy 0bb15f0a44 Update building-macos.md 2026-09-10 09:48:03 +02:00
patchzyy 466d06d7db Update README with image and credit modifications
Added an image to the README and updated credits section.
2026-09-10 08:38:04 +02:00
Michael G 8769cf6dea docs: add macOS source build guide for WiiCompiled and Retro Rewind (#177)
* docs: add macOS source build guide for WiiCompiled and Retro Rewind

* review fixes

* Change Retro-WFC payload download URL

Updated the URL for downloading Retro-WFC payload.
2026-09-09 18:23:15 +02:00
theofficialgman 452b478bb3 Resolve z fighting (#134)
* Fix already downloaded toolchain re-use

the following mv command would move $work into $toolchain_dir if the $toolchain_dir folder already existed.

* resolve z-fighting
2026-09-09 14:47:14 +02:00
patchzyy 407f8a7190 https payload (#198) 2026-09-09 14:41:58 +02:00
Jordan Blake c2289e4ba4 os_sleep: process due sleep timers one at a time to stop stranding parked threads (#195)
ProcessSleepTimers popped every due timer into a private vector and then
resumed the sleepers in a loop. OSResumeThread re-enters SelectThread, which
can switch fibers away mid-loop, so the timers still in that vector were
gone from gSleepTimers while their threads stayed parked (Ready, suspended,
no timer). The reconciler healed them 100ms later and the stale-timer drop
fired when the original fiber eventually resumed.

Pop one due timer at a time straight from the shared table instead, so any
timer not yet processed stays visible to every other pump while this call
is switched away.

Co-authored-by: jordanblakepp <slamuelrose2002@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 08:30:16 +02:00
Wubbzee a135beb201 Reduce CI time using caching (#180)
* caching a little bit

* provide CMake with the explicit path to sccache.exe

* map ACTIONS_RESULTS_URL to ACTIONS_CACHE_URL so sccache can upload the
files...

* i removed the parallel oops

* small change

* doing a little bit of flag editing

* update sccache and cache nuget stuff
2026-09-07 09:18:53 +02:00
patchzyy 88b990b060 update 2026-09-06 15:38:14 +02:00
patchzyy e0e362bd99 csnum fix 2026-09-06 15:28:56 +02:00
patchzyy 5654d8f21b Merge branch 'main' of https://github.com/patchzyy/Wiicompiled 2026-09-06 11:23:13 +02:00
patchzyy 730e3122d5 version 0.2.30 2026-09-06 11:22:57 +02:00
Wubbzee f424536d3b Treat empty MKW save as missing rather than corrupt (#168)
* treat empty mkw save as missing

first-run format zero-fills rksys.dat before any real save; a quit before
the first save left an all-zero file that read back as corrupt and trapped
the user in a delete/recreate loop. read opens now treat an all-zero
rksys.dat as absent (a real save always begins with the RKSD0006 header),
so the game recreates it from scratch. also ignore native build output.

* shorten

* I dont really want to change this to be honest.

* extra safety

---------

Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
2026-09-06 11:20:51 +02:00
patchzyy 2d9dc4e0f2 import setting.txt (#169)
* import setting.txt

* coderabbit ugh
2026-09-06 11:14:54 +02:00
patchzyy 8e57cc162f version 2026-09-06 00:48:41 +02:00
patchzyy 1c0a3edee9 Add synthetic recompilation CI workflow (#161) 2026-09-05 23:00:47 +02:00
patchzyy d1d80613cc Load console identity from NAND setting.txt (#164) 2026-09-05 23:00:23 +02:00
patchzyy 5d67b229f6 Update settings_overlay.cpp 2026-09-05 16:54:24 +02:00
patchzyy 56db6ba641 v0.2.28 2026-09-05 16:09:34 +02:00
Michael G a67069afd3 fix(macos): increase guest fiber stack size (#154) 2026-09-05 15:45:29 +02:00
patchzyy 009697fb97 Update network_socket.cpp 2026-09-05 14:31:53 +02:00
Cristian Boehm 3f7fed48c9 Fix GC Pocket+ rumble stop handling (#148) 2026-09-05 10:20:15 +02:00
theofficialgman 6eba523d70 Switch back to LLVM 22 so build can succeed on at least some systems (#141)
two upstream LLVM bugs currently prevent building on some of the newest distros. There is no current LLVM release that works on them so we are pending fixes from LLVM
https://github.com/patchzyy/Wiicompiled/issues/136
2026-09-05 10:17:21 +02:00
patchzyy 8c6c177857 Simplify README by removing redundant input details 2026-09-05 10:11:48 +02:00
Nicholas Bly be153e0fa0 Add Dolphin-compatible input expressions and GCPadNew.ini import, DualSense L/R remapping, vibration toggle (#89)
* Add Dolphin-compatible input expressions and GCPadNew.ini import

Rebased onto current main; addresses both CodeRabbit reviews on #89.

- Expression engine matching Dolphin's semantics: doubles rather than
  booleans, 0.5 press threshold, & as min, | as max, and the functions if,
  min, max, clamp, abs, sqrt, pow, sin, cos, tan, deadzone, timer, toggle,
  hold, tap, pulse and smooth. Timing uses a steady clock in seconds, as
  Dolphin does, so a copied expression behaves identically.

- Expressions bind to the GameCube buttons and triggers, combined with the
  existing button mapping rather than replacing it, and are skipped while
  the settings overlay holds input.

- Import reads [GCPadN] from the Dolphin config directory or from
  GCPadNew.ini beside the executable. Stick axes are not expression driven
  and keep their normal mapping.

- Fixes #74: a digital button bound to L or R now reports a fully pulled
  analog trigger, plus a PlayStation preset and a vibration toggle.

Review fixes: config paths round-trip through RuntimeConfigFile::PathToUtf8
and PathFromUtf8 so non-ASCII paths open correctly on Windows, and the
duplicated exists branch is gone; the tap count is clamped before the
unsigned conversion; the expression editor uses resizable storage via
ImGuiInputTextFlags_CallbackResize so a long expression cannot be saved
truncated; clamp bounds are ordered before std::clamp; <cstdlib> is included
for std::strtod; non-finite values are rejected at the evaluator boundary as
well as at the deadzone and timer divisions; and InputBindings::Reload() runs
from InitializeRuntimeSettings rather than the vibration handler.

runtime/tests/test_expr.cpp covers operator precedence, each stateful
function and every case raised in review.

Third review round: smooth() guards NaN as well as infinity so a zero rate
cannot latch a non-finite value in node state; division evaluates both operands
so stateful functions in the left subtree still update when the divisor is zero;
the expression editor clears stale errors when the port changes; and
runtime/tests/test_expr.cpp is registered with CTest as mkw_input_expr_tests,
following the existing test targets.

* Update runtime/src/input_expr.cpp

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

* Update runtime/src/input_expr.cpp

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

* Update runtime/src/input_expr.cpp

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-05 10:07:05 +02:00
patchzyy e34f055b3a Shortcut (#155) 2026-09-05 09:55:09 +02:00
patchzyy 602348f905 Update Models.cs 2026-09-04 22:00:21 +02:00
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
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
theofficialgman cb812c1fd0 add native aarch64 (arm64) support
use -mcpu=native on arm64 targets
2026-08-29 12:12:06 -04:00
125 changed files with 11053 additions and 807 deletions
+1
View File
@@ -3,3 +3,4 @@
# Patch files must stay LF: git apply matches context bytes against LF upstream sources # Patch files must stay LF: git apply matches context bytes against LF upstream sources
*.patch -text *.patch -text
translator/tests/Translator.Tests/TestAssets/**/*.bin binary
+14 -2
View File
@@ -14,18 +14,30 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
recompilation:
name: Recompilation test
uses: ./.github/workflows/recomp-test.yml
translator: translator:
name: Translator (build + test) name: Translator (build + test)
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-dotnet@v4 - uses: actions/setup-dotnet@v6
with: with:
dotnet-version: '8.0.x' dotnet-version: '8.0.x'
- name: Cache NuGet packages
uses: actions/cache@v5
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('translator/Translator.sln', '**/*.csproj', '**/*.props', '**/*.targets', '**/packages.lock.json', 'global.json', 'NuGet.config', 'nuget.config') }}
restore-keys: |
${{ runner.os }}-nuget-
- name: Restore - name: Restore
run: dotnet restore translator/Translator.sln run: dotnet restore translator/Translator.sln
+160
View File
@@ -0,0 +1,160 @@
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:
recompilation:
name: Recompilation test
uses: ./.github/workflows/recomp-test.yml
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, recompilation]
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
+61
View File
@@ -0,0 +1,61 @@
name: Synthetic recompilation
on:
workflow_call:
workflow_dispatch:
permissions:
contents: read
jobs:
windows:
name: Windows runtime (synthetic DOL)
runs-on: windows-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '8.0.x'
# Cache downloads only. Preparation still validates pins, and every run
# compiles current Aurora, runtime, and generated sources from scratch.
- uses: actions/cache@v5
with:
path: Launcher/artifacts/downloads
key: windows-recomp-downloads-${{ hashFiles('Launcher/Prepare-PortableTools.ps1', 'Launcher/Prepare-Dependencies.ps1') }}
# Install sccache.
- name: Run sccache-action
uses: mozilla/sccache-action@v0.0.11
# Tell CMake to use sccache and use GitHub's API.
- name: Configure sccache environment
shell: pwsh
run: |
"SCCACHE_GHA_ENABLED=true" | Add-Content -Path $env:GITHUB_ENV
"ACTIONS_CACHE_SERVICE_V2=on" | Add-Content -Path $env:GITHUB_ENV
"CMAKE_C_COMPILER_LAUNCHER=$env:SCCACHE_PATH" | Add-Content -Path $env:GITHUB_ENV
"CMAKE_CXX_COMPILER_LAUNCHER=$env:SCCACHE_PATH" | Add-Content -Path $env:GITHUB_ENV
- name: Prepare the shipped Windows toolchain
shell: pwsh
run: ./Launcher/Prepare-PortableTools.ps1
- name: Prepare pinned native dependencies
shell: pwsh
run: ./Launcher/Prepare-Dependencies.ps1
# Expose cache token context to the build script.
- name: Translate, compile the full runtime, and link
shell: pwsh
run: ./Launcher/Test-Recompilation.ps1 -Parallel 4
# Print cache results (even if the build fails)
- name: Show sccache stats
if: always()
shell: pwsh
run: sccache --show-stats
+5
View File
@@ -25,6 +25,9 @@ Code.pul
# Build output # Build output
/build/ /build/
/build-*/ /build-*/
/native-build/
/native-build-macos/
/local-products/
/dist/ /dist/
/out/ /out/
[Bb]in/ [Bb]in/
@@ -69,3 +72,5 @@ project.lock.json
*.log *.log
output.txt output.txt
# Operating System
.DS_Store
+13 -7
View File
@@ -88,17 +88,23 @@ Assert-File $setupProject '.NET setup project'
Assert-File $translatorProject 'Translator CLI project' Assert-File $translatorProject 'Translator CLI project'
if (-not (Test-Path -LiteralPath (Join-Path $portableTools 'llvm-mingw\bin\x86_64-w64-mingw32-clang++.exe'))) { 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 & (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 'CMake\bin\cmake.exe') 'Portable CMake'
Assert-File (Join-Path $portableTools 'Ninja\ninja.exe') 'Portable Ninja' 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 # native_prebuilt carries the aurora/third-party archives the user no longer has
# to compile (launcher/Prepare-NativePrebuilt.ps1). # to compile (launcher/Prepare-NativePrebuilt.ps1).
# Kept in step with InstalledLayout.DependencyNames by Test-PinnedFacts.ps1: the installed host # 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. # refuses to call a toolkit complete unless every one of these directories is present.
$requiredDependencies = @('abseil-cpp','cppwinrt','dawn_prebuilt','fmt','freetype','imgui','libusb','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 # 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 # compiles if both came from this toolchain and this flag set, so a stale package
@@ -165,8 +171,7 @@ Assert-File $translator 'Self-contained translator'
# Resolved via the shared WiiCompiled.Setup.Common.Cli helper (also used by build-appimage.sh on # 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 # Linux) rather than a separate download/version-pin copy here: it downloads and caches the same way
# NodToolProvider.cs always does (Launcher/artifacts/nodtool.exe), replacing the old manual # NodToolProvider.cs always does (Launcher/artifacts/nodtool.exe)
# -DolphinToolPath handoff with an automated, pinned acquisition step.
$nodToolCliProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup.Common.Cli' $nodToolCliProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup.Common.Cli'
$nodTool = (& dotnet run --project $nodToolCliProject -c Release -- --workspace $repoRoot | Select-Object -Last 1) $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." } if ($LASTEXITCODE -ne 0) { throw "nodtool resolution failed with exit code $LASTEXITCODE." }
@@ -276,7 +281,7 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
$manifest = [ordered]@{ $manifest = [ordered]@{
SchemaVersion = 2 SchemaVersion = 2
ProductVersion = '0.2.25' ProductVersion = '0.2.31'
ExpectedGameId = $pins.GameId ExpectedGameId = $pins.GameId
ExpectedDolSha256 = $pins.DolSha256 ExpectedDolSha256 = $pins.DolSha256
ExpectedRelSha256 = $pins.RelSha256 ExpectedRelSha256 = $pins.RelSha256
@@ -292,11 +297,12 @@ $manifest = [ordered]@{
$manifest | ConvertTo-Json | Set-Content (Join-Path $payloadRoot 'payload-manifest.json') -Encoding UTF8 $manifest | ConvertTo-Json | Set-Content (Join-Path $payloadRoot 'payload-manifest.json') -Encoding UTF8
Write-Host '[4/6] Enforcing the copyright and generated-code boundary...' 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 & (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 ` & (Join-Path $PSScriptRoot 'Test-NativeDependencies.ps1') -PayloadRoot $payloadRoot -SetupHost $setupHost `
-LlvmReadobjPath (Join-Path $portableTools 'llvm-mingw\bin\llvm-readobj.exe') -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...' Write-Host '[5/6] Creating the canonical installer payload...'
$payloadZip = Join-Path $workRoot 'payload.zip' $payloadZip = Join-Path $workRoot 'payload.zip'
+14 -8
View File
@@ -117,12 +117,13 @@ function Get-MkwProjectPins([string]$ProjectFile) {
} }
function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Description, function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Description,
[string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '') { [string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '', [bool]$WaitForProcessTree = $true) {
<# <#
Runs a build tool and turns a non-zero exit code into a described failure. Start-Process -Wait Runs a build tool and turns a non-zero exit code into a described failure. By default,
is deliberate: it waits for the whole process tree, since a .NET single-file bundle host may Start-Process -Wait waits for the whole process tree, since a .NET single-file bundle host may
hand off to an extracted child that PowerShell's call operator would not wait for. Start-Process hand off to an extracted child that PowerShell's call operator would not wait for. Callers that
doesn't publish $LASTEXITCODE, so this sets it manually for callers that check it. need to avoid waiting on unrelated descendants can opt into the call-operator path.
Start-Process doesn't publish $LASTEXITCODE, so this sets it manually for callers that check it.
-StepId emits the machine-readable form the installer's progress bar consumes (BuildStepIds in -StepId emits the machine-readable form the installer's progress bar consumes (BuildStepIds in
WiiCompiled.Setup/InstallProgress.cs); the human sentence stays on the same log line. WiiCompiled.Setup/InstallProgress.cs); the human sentence stays on the same log line.
#> #>
@@ -132,9 +133,14 @@ function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Descri
if ($_.Contains('"')) { throw "A native build argument contains an unsupported quote: $_" } if ($_.Contains('"')) { throw "A native build argument contains an unsupported quote: $_" }
'"' + $_ + '"' '"' + $_ + '"'
}) })
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments ` if ($WaitForProcessTree) {
-NoNewWindow -Wait -PassThru $process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
$exitCode = $process.ExitCode -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
} else {
& $FilePath @Arguments
$exitCode = $LASTEXITCODE
}
$global:LASTEXITCODE = $exitCode $global:LASTEXITCODE = $exitCode
if ($exitCode -ne 0) { throw "$Description failed with exit code $exitCode." } if ($exitCode -ne 0) { throw "$Description failed with exit code $exitCode." }
} }
+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-File $clangBinary 'Portable clang driver binary'
Assert-Directory $dependencies 'Pinned offline dependency sources' Assert-Directory $dependencies 'Pinned offline dependency sources'
Assert-Directory $auroraSource 'aurora-main source tree' 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 } 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-22")
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"
+7 -1
View File
@@ -1,6 +1,6 @@
# Fails the release build when a fact duplicated across the repo stops agreeing with the copy # Fails the release build when a fact duplicated across the repo stops agreeing with the copy
# that owns it (recomp.yml). Scripts read pinned facts through Get-MkwProjectPins, but three # that owns it (recomp.yml). Scripts read pinned facts through Get-MkwProjectPins, but three
# consumers can't read YAML (the C++ runtime header, the C# constants, hand-written lists on # consumers can't read YAML (the C++ runtime header, the C# constants, shell scripts, and hand-written lists on
# both sides of the C#/PowerShell boundary), so those are checked here instead. # both sides of the C#/PowerShell boundary), so those are checked here instead.
[CmdletBinding()] [CmdletBinding()]
param([string]$RepositoryRoot) param([string]$RepositoryRoot)
@@ -58,6 +58,12 @@ $hostUri = Get-CapturedValue $retroWfcPayload 'CurrentRetroWfcPayloadUri\s*=\s*"
if ($hostUri -cne $pins.RetroWfcPayloadUri) { if ($hostUri -cne $pins.RetroWfcPayloadUri) {
Add-Failure "InputValidation.CurrentRetroWfcPayloadUri is '$hostUri' but recomp.yml pins '$($pins.RetroWfcPayloadUri)'." Add-Failure "InputValidation.CurrentRetroWfcPayloadUri is '$hostUri' but recomp.yml pins '$($pins.RetroWfcPayloadUri)'."
} }
$macosSetup = Read-SourceFile (Join-Path $launcher 'macos\setup.command') 'macOS setup.command'
$macosUri = Get-CapturedValue $macosSetup "'([^']*/api/wfc/payload\?g=RMCPD00)'" `
'The macOS Retro-WFC endpoint'
if ($macosUri -cne $pins.RetroWfcPayloadUri) {
Add-Failure "macOS setup.command downloads '$macosUri' but recomp.yml pins '$($pins.RetroWfcPayloadUri)'."
}
# --- The game identity: the manifest carries it, but the host also compiles a fallback for a # --- The game identity: the manifest carries it, but the host also compiles a fallback for a
# --- manifest that predates the field, and that fallback decides which disc is accepted. # --- manifest that predates the field, and that fallback decides which disc is accepted.
+148
View File
@@ -0,0 +1,148 @@
# Build the real Windows runtime with translated, entirely synthetic PowerPC code.
# No game dump, game symbol map, REL, mod download, or existing generated/ output is used.
[CmdletBinding()]
param(
[string]$PortableToolsDirectory = 'Launcher/artifacts/portable-tools',
[string]$DependencySourceDirectory = 'Launcher/artifacts/dependencies',
[string]$StageDirectory = 'build/recomp-test',
[ValidateRange(1, 64)] [int]$Parallel = 3
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 3.0
. (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1')
$repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
function Full([string]$Path) {
if ([IO.Path]::IsPathRooted($Path)) { return [IO.Path]::GetFullPath($Path) }
return [IO.Path]::GetFullPath((Join-Path $repoRoot $Path))
}
$portableTools = Full $PortableToolsDirectory
$dependencies = Full $DependencySourceDirectory
$stage = Full $StageDirectory
$dotnet = (Get-Command dotnet -CommandType Application).Source
$cmake = Join-Path $portableTools 'CMake/bin/cmake.exe'
$ninja = Join-Path $portableTools 'Ninja/ninja.exe'
$compilerBin = Join-Path $portableTools 'llvm-mingw/bin'
Assert-File $cmake 'Pinned CMake (run Prepare-PortableTools.ps1 first)'
Assert-File $ninja 'Pinned Ninja'
Assert-File (Join-Path $dependencies 'cppwinrt/winrt/base.h') 'Pinned dependencies (run Prepare-Dependencies.ps1 first)'
# Refuse reuse so a developer's game translation or an earlier build cannot make
# the test pass. Keep the staging tree after the run for diagnostics.
if (Test-Path -LiteralPath $stage) { throw "Test stage already exists; choose a fresh -StageDirectory: $stage" }
[IO.Directory]::CreateDirectory($stage) | Out-Null
Write-Host "Synthetic recompilation workspace: $stage"
# Copy current sources, including uncommitted edits, but no ignored build output.
# An isolated workspace preserves the developer's real generated/ directory.
$sourceFiles = & git -C $repoRoot -c core.quotepath=false ls-files --cached --others --exclude-standard -- runtime aurora-main
if ($LASTEXITCODE -ne 0) { throw 'Could not enumerate runtime and Aurora sources.' }
foreach ($relative in $sourceFiles | Sort-Object -Unique) {
$destination = Join-Path $stage $relative
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination)) | Out-Null
Copy-Item -LiteralPath (Join-Path $repoRoot $relative) -Destination $destination
}
# One synthetic text section: li r3,40; addi r3,r3,2; nop; blr.
# Native HLE wrappers also call these eight guest symbols directly. Give each
# its own generated blr function so the real product can link without game code.
# Keep this list explicit: a new unresolved guest dependency must fail the test.
[uint32]$entry = 0x80001000L
[uint32[]]$guestCallbacks = @(
0x8012B830L, 0x801A0620L, 0x801A1ED8L, 0x801A961CL,
0x801AADE0L, 0x801D8D30L, 0x801D9E94L, 0x8055531CL
)
$textSize = [int]($guestCallbacks[-1] - $entry + 4)
$dataOffset = 0x100 + $textSize
$dol = [byte[]]::new($dataOffset + 4)
function Write-BigEndian32([int]$Offset, [uint32]$Value) {
$dol[$Offset] = [byte](($Value -shr 24) -band 255)
$dol[$Offset + 1] = [byte](($Value -shr 16) -band 255)
$dol[$Offset + 2] = [byte](($Value -shr 8) -band 255)
$dol[$Offset + 3] = [byte]($Value -band 255)
}
Write-BigEndian32 0x00 0x100 # text[0] file offset
Write-BigEndian32 0x48 $entry # text[0] guest address
Write-BigEndian32 0x90 $textSize # text[0] length (unreachable gaps are zero)
Write-BigEndian32 0x1C $dataOffset # data[0] file offset
Write-BigEndian32 0x64 0x80600000L # data[0] guest address
Write-BigEndian32 0xAC 4 # data[0] length
Write-BigEndian32 0xD8 0x80601000L # BSS address
Write-BigEndian32 0xDC 32 # BSS length
Write-BigEndian32 0xE0 $entry # entry point
Write-BigEndian32 0x100 0x38600028 # li r3,40
Write-BigEndian32 0x104 0x38630002 # addi r3,r3,2
Write-BigEndian32 0x108 0x60000000 # nop
Write-BigEndian32 0x10C 0x4E800020 # blr
foreach ($address in $guestCallbacks) {
Write-BigEndian32 ([int](0x100 + $address - $entry)) 0x4E800020
}
Write-BigEndian32 $dataOffset 0x12345678
[IO.File]::WriteAllBytes((Join-Path $stage 'synthetic.dol'), $dol)
$entryPoints = (@($entry) + $guestCallbacks | ForEach-Object { '0x{0:X8}' -f $_ }) -join ', '
$functionMap = (@($entry) + $guestCallbacks | ForEach-Object { '{0:X8} func_{0:X8}' -f $_ }) -join "`n"
[IO.File]::WriteAllText((Join-Path $stage 'synthetic-functions.txt'), $functionMap)
$manifest = Join-Path $stage 'recomp.yml'
[IO.File]::WriteAllText($manifest, @"
schema_version: 1
workspace_root: .
project:
id: ci-synthetic-dol
display_name: CI Synthetic DOL
memory:
base: 0x80000000
size: 0x01800000
sda_base: 0x80600000
sda2_base: 0x80600000
inputs:
dol:
path: synthetic.dol
translation:
entry_points: [$entryPoints]
function_map:
path: synthetic-functions.txt
allow_unsupported_instructions: false
runtime:
native_abi_directories: []
native_registration_root: runtime/src
output:
root: generated
"@)
$translatorProject = Join-Path $repoRoot 'translator/src/Translator.Cli/Translator.Cli.csproj'
Invoke-Checked $dotnet @('build', $translatorProject, '-c', 'Release', '--disable-build-servers') 'Building the translator'
$translator = Join-Path $repoRoot 'translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll'
$metadata = Join-Path $stage 'generated/base_translation_output.json'
Invoke-Checked $dotnet @($translator, 'translate-recursive', '0x80001000', '--project', $manifest,
'--output-metadata', $metadata, '--threads', "$Parallel") `
'Translating the synthetic DOL'
# Function-map seeds can be skipped by discovery; do not accept a partial fixture.
$translated = Get-Content -LiteralPath $metadata -Raw | ConvertFrom-Json
foreach ($address in @($entry) + $guestCallbacks) {
if ($address -notin $translated.functions.entryPoint) {
throw ('Synthetic function 0x{0:X8} was not translated.' -f $address)
}
}
Invoke-Checked $dotnet @($translator, 'generate-data-init', '--project', $manifest) 'Generating synthetic data and runtime configuration'
Invoke-Checked $dotnet @($translator, 'emit-build-shards', '--project', $manifest) 'Emitting the production build graph'
$nativeBuild = Join-Path $stage 'native-build'
$oldPath = $env:PATH
try {
$env:PATH = Get-MkwToolchainPath $portableTools
$configure = Get-MkwNativeConfigureArguments -SourceDirectory (Join-Path $stage 'runtime') -BuildDirectory $nativeBuild `
-Ninja $ninja -CCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang.exe') `
-CxxCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-clang++.exe') `
-ResourceCompiler (Join-Path $compilerBin 'x86_64-w64-mingw32-windres.exe') `
-DependenciesDirectory $dependencies -AdditionalArguments @('-DMKW_BUILD_PRODUCTS=ON')
Invoke-Checked $cmake $configure 'Configuring the production Windows runtime' `
-WaitForProcessTree $false
Invoke-Checked $cmake @('--build', $nativeBuild, '--target', 'WiiCompiled', '--parallel', "$Parallel") `
'Compiling and linking the synthetic product with the full runtime' `
-WaitForProcessTree $false
Assert-File (Join-Path $nativeBuild 'WiiCompiled.exe') 'Linked synthetic product'
} finally {
$env:PATH = $oldPath
}
Write-Host 'Synthetic recompilation passed (translation, data generation, runtime compilation, and product link).'
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace> <RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName> <AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
<Version>0.2.22</Version> <Version>0.2.31</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description> <Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description>
@@ -24,7 +24,7 @@ public static class RetroWfcPayload
private static readonly TimeSpan RetroWfcDownloadTimeout = TimeSpan.FromSeconds(30); private static readonly TimeSpan RetroWfcDownloadTimeout = TimeSpan.FromSeconds(30);
private static readonly TimeSpan RetroWfcRetryDelay = TimeSpan.FromSeconds(1); private static readonly TimeSpan RetroWfcRetryDelay = TimeSpan.FromSeconds(1);
public const string CurrentRetroWfcPayloadUri = "http://nas.play.rwfc.net/payload?g=RMCPD00"; public const string CurrentRetroWfcPayloadUri = "https://rwfc.net/api/wfc/payload?g=RMCPD00";
private static readonly string RetroWfcOfflinePayloadFile = private static readonly string RetroWfcOfflinePayloadFile =
Path.Combine("binary", "payload.RMCPD00.bin"); Path.Combine("binary", "payload.RMCPD00.bin");
@@ -53,9 +53,22 @@ public static class RetroWfcPayload
var root = Path.GetFullPath(stagedDirectory); var root = Path.GetFullPath(stagedDirectory);
var payload = Path.Combine(root, RetroWfcOfflinePayloadFile); 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( throw new InvalidDataException(
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin."); "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); ValidateRetroWfcPayloadFile(payload, signingKey);
return root; return root;
} }
@@ -5,7 +5,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace> <RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName> <AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
<Version>0.2.22</Version> <Version>0.2.31</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description> <Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description>
@@ -12,7 +12,9 @@ internal static class BuildRunner
public static async Task RunAsync( public static async Task RunAsync(
string workspace, string profile, string outputDir, string? baseOutputDir, string workspace, string profile, string outputDir, string? baseOutputDir,
string? retroDir, string? retroWfcOfflineDir, bool skipRetroWfcPayload, string? retroDir, string? retroWfcOfflineDir, bool skipRetroWfcPayload,
bool forceCleanBuild, string? translatorBin, IInstallReporter reporter, CancellationToken cancellationToken) 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"); var script = Path.Combine(workspace, "Launcher", "local-build.sh");
if (!File.Exists(script)) throw new FileNotFoundException("local-build.sh is missing", script); if (!File.Exists(script)) throw new FileNotFoundException("local-build.sh is missing", script);
@@ -47,6 +49,34 @@ internal static class BuildRunner
{ {
startInfo.ArgumentList.Add("--translator-bin"); startInfo.ArgumentList.Add(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 }; using var process = new Process { StartInfo = startInfo };
var window = new BuildProgressWindow(reporter, InstallStages.Build, start: 6, end: 96); var window = new BuildProgressWindow(reporter, InstallStages.Build, start: 6, end: 96);
+1 -1
View File
@@ -3,7 +3,7 @@ namespace WiiCompiled.Setup.Linux;
internal static class ProductInfo internal static class ProductInfo
{ {
public const string Name = "WiiCompiled"; public const string Name = "WiiCompiled";
public const string Version = "0.2.22"; public const string Version = "0.2.31";
} }
/// <summary>One installed product's record inside install-state.json.</summary> /// <summary>One installed product's record inside install-state.json.</summary>
+8 -1
View File
@@ -150,6 +150,12 @@ internal static class Program
skipPayload, skipPayload,
flags.ContainsKey("force-clean-build"), flags.ContainsKey("force-clean-build"),
flags.GetValueOrDefault("translator-bin"), 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, token);
reporter.Progress(InstallStages.Shortcuts, "Creating shortcuts", 98); reporter.Progress(InstallStages.Shortcuts, "Creating shortcuts", 98);
@@ -317,7 +323,8 @@ internal static class Program
install [--game ISO_PATH] [--install-dir DIR] [--retro-dir DIR install [--game ISO_PATH] [--install-dir DIR] [--retro-dir DIR
{--download-retro-wfc-payload | --skip-retro-wfc-payload}] {--download-retro-wfc-payload | --skip-retro-wfc-payload}]
[--force-clean-build] [--translator-bin PATH] [--disc-tool-bin PATH] [--force-clean-build] [--translator-bin PATH] [--disc-tool-bin PATH]
[--progress-json] [--workspace DIR] [--cc PATH] [--cxx PATH] [--fuse-ld NAME_OR_PATH] [--cmake PATH] [--ninja PATH]
[--native-prebuilt-dir DIR] [--progress-json] [--workspace DIR]
uninstall uninstall
launch-base launch-base
launch-retro launch-retro
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyName>WiiCompiled.Setup.Linux</AssemblyName> <AssemblyName>WiiCompiled.Setup.Linux</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Linux</RootNamespace> <RootNamespace>WiiCompiled.Setup.Linux</RootNamespace>
<Version>0.2.22</Version> <Version>0.2.31</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled on Linux</Description> <Description>Command-line installer and launcher for WiiCompiled on Linux</Description>
@@ -22,6 +22,9 @@ internal sealed class InstallerEngine
var existing = new Installation(installDirectory); var existing = new Installation(installDirectory);
PortableInstallHealing.HealMovedInstall(existing, _reporter); PortableInstallHealing.HealMovedInstall(existing, _reporter);
// Capture this before publishing anything. An existing installation may be repaired or
// updated by this invocation, but those operations must not recreate the user's shortcuts.
var firstInstall = !existing.IsPresent;
var previousState = existing.ReadInstallState(); var previousState = existing.ReadInstallState();
using var scratch = Directory.Exists(installDirectory) using var scratch = Directory.Exists(installDirectory)
? InstallScratchSpace.CreateInsideInstall(installDirectory, _reporter) ? InstallScratchSpace.CreateInsideInstall(installDirectory, _reporter)
@@ -139,7 +142,7 @@ internal sealed class InstallerEngine
updatedState.RetroRewindInstalled, candidateRuntimeAssetsFingerprint, updatedState.RetroRewindInstalled, candidateRuntimeAssetsFingerprint,
remainingCancellation); remainingCancellation);
Publish(staging, installDirectory, canonicalRetroRoot, updatedState, Publish(staging, installDirectory, canonicalRetroRoot, updatedState,
releaseEntries, remainingCancellation); releaseEntries, remainingCancellation, createShortcuts: firstInstall);
return; return;
} }
@@ -161,7 +164,8 @@ internal sealed class InstallerEngine
await PublishToolkitAndReconcileProductsAsync(existing, staging, workspace, manifest, await PublishToolkitAndReconcileProductsAsync(existing, staging, workspace, manifest,
previousState, options, canonicalRetroRoot, retroCompileInputs, previousState, options, canonicalRetroRoot, retroCompileInputs,
publishGameAssets: reusableGameAssets is null, cancellationToken); publishGameAssets: reusableGameAssets is null, createShortcuts: firstInstall,
cancellationToken: cancellationToken);
} }
@@ -186,7 +190,7 @@ internal sealed class InstallerEngine
string stagedWorkspace, PayloadManifest manifest, InstallState? previousState, string stagedWorkspace, PayloadManifest manifest, InstallState? previousState,
InstallOptions options, string? canonicalRetroRoot, InstallOptions options, string? canonicalRetroRoot,
RetroRewindCompileInputs? retroCompileInputs, RetroRewindCompileInputs? retroCompileInputs,
bool publishGameAssets, CancellationToken cancellationToken) bool publishGameAssets, bool createShortcuts, CancellationToken cancellationToken)
{ {
var installDirectory = existing.Root; var installDirectory = existing.Root;
@@ -214,7 +218,8 @@ internal sealed class InstallerEngine
if (publishGameAssets) AddComponent(entries, staging, installDirectory, "GameAssets"); if (publishGameAssets) AddComponent(entries, staging, installDirectory, "GameAssets");
Publish(staging, installDirectory, canonicalRetroRoot, state, Publish(staging, installDirectory, canonicalRetroRoot, state,
entries, cancellationToken, progressPercent: 8, completionPercent: 10); entries, cancellationToken, progressPercent: 8, completionPercent: 10,
createShortcuts: createShortcuts);
_reporter.Progress(InstallStages.BuildBase, _reporter.Progress(InstallStages.BuildBase,
"Producing the installed products with the published toolkit...", 11); "Producing the installed products with the published toolkit...", 11);
@@ -277,7 +282,7 @@ internal sealed class InstallerEngine
private void Publish(string staging, string installDirectory, private void Publish(string staging, string installDirectory,
string? canonicalRetroRoot, InstallState state, string? canonicalRetroRoot, InstallState state,
List<InstallTransactionEntry> entries, CancellationToken cancellationToken, List<InstallTransactionEntry> entries, CancellationToken cancellationToken,
int progressPercent = 95, int completionPercent = 99) int progressPercent = 95, int completionPercent = 99, bool createShortcuts = false)
{ {
entries.Add(InstallTransactionEntry.Directory(Path.Combine(staging, "licenses"), entries.Add(InstallTransactionEntry.Directory(Path.Combine(staging, "licenses"),
Path.Combine(installDirectory, "licenses"))); Path.Combine(installDirectory, "licenses")));
@@ -322,7 +327,8 @@ internal sealed class InstallerEngine
{ {
ShellIntegration.RegisterUninstaller(installDirectory, state.RetroRewindInstalled); ShellIntegration.RegisterUninstaller(installDirectory, state.RetroRewindInstalled);
} }
ShellIntegration.CreateShortcuts(installDirectory); if (createShortcuts)
ShellIntegration.CreateShortcuts(installDirectory);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -121,7 +121,7 @@ internal static class PlatformChecks
internal static class ProductInfo internal static class ProductInfo
{ {
public const string Name = "WiiCompiled"; public const string Name = "WiiCompiled";
public const string Version = "0.2.25"; public const string Version = "0.2.31";
/// <summary> /// <summary>
/// The setup executable is copied into the installation under this name. It is the launcher and /// The setup executable is copied into the installation under this name. It is the launcher and
@@ -7,7 +7,7 @@
<AssemblyName>WiiCompiled.Setup</AssemblyName> <AssemblyName>WiiCompiled.Setup</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace> <RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.2.25</Version> <Version>0.2.31</Version>
<Authors>patchzy</Authors> <Authors>patchzy</Authors>
<Product>WiiCompiled</Product> <Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled</Description> <Description>Command-line installer and launcher for WiiCompiled</Description>
Regular → Executable
+144 -14
View File
@@ -5,8 +5,12 @@
# self-contained binaries, and `nodtool` (a prebuilt MIT/Apache-2.0 CLI from encounter/nod, see # 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 # 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. # --disc-tool-bin so local-build.sh/DiscTool.cs skip their from-source/download fallbacks entirely.
# It still shells out to system clang/cmake/ninja - no C/C++ toolchain is bundled, matching # A pruned native clang/lld/cmake/ninja toolchain (see prepare-portable-tools.sh) is bundled the
# Launcher/local-build.sh's own remaining prerequisites. # 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. # 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 # into the workspace it's given. So AppRun (written below) copies the bundled workspace snapshot
@@ -15,12 +19,58 @@
# - generated/native-build/Assets/PulsarPacks live only in that writable cache and are never # - 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 # 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 # 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. # 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 set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace=$(cd "$script_dir/.." && 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" output_dir="$workspace/Launcher/dist"
appimagetool_override="" appimagetool_override=""
@@ -40,10 +90,10 @@ appdir="$workspace/Launcher/artifacts/appimage-build/AppDir"
rm -rf "$appdir" rm -rf "$appdir"
mkdir -p "$appdir/usr/bin" "$appdir/workspace/Launcher" mkdir -p "$appdir/usr/bin" "$appdir/workspace/Launcher"
echo "Publishing the installer (self-contained linux-x64)..." echo "Publishing the installer (self-contained $dotnet_rid)..."
publish_tmp="$workspace/Launcher/artifacts/appimage-build/publish" publish_tmp="$workspace/Launcher/artifacts/appimage-build/publish"
rm -rf "$publish_tmp" rm -rf "$publish_tmp"
dotnet publish "$workspace/Launcher/WiiCompiled.Setup.Linux" -c Release -r linux-x64 \ dotnet publish "$workspace/Launcher/WiiCompiled.Setup.Linux" -c Release -r "$dotnet_rid" \
--self-contained -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \ --self-contained -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \
-o "$publish_tmp" -o "$publish_tmp"
cp "$publish_tmp/WiiCompiled.Setup.Linux" "$appdir/usr/bin/wiicompiled-setup" cp "$publish_tmp/WiiCompiled.Setup.Linux" "$appdir/usr/bin/wiicompiled-setup"
@@ -52,10 +102,10 @@ chmod +x "$appdir/usr/bin/wiicompiled-setup"
# Published as a self-contained binary too, so an AppImage user never needs a `dotnet` SDK on # 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 # 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). # dotnet-build-from-source step entirely (see local-build.sh's translator resolution branch).
echo "Publishing the translator (self-contained linux-x64)..." echo "Publishing the translator (self-contained $dotnet_rid)..."
translator_publish_tmp="$workspace/Launcher/artifacts/appimage-build/publish-translator" translator_publish_tmp="$workspace/Launcher/artifacts/appimage-build/publish-translator"
rm -rf "$translator_publish_tmp" rm -rf "$translator_publish_tmp"
dotnet publish "$workspace/translator/src/Translator.Cli" -c Release -r linux-x64 \ dotnet publish "$workspace/translator/src/Translator.Cli" -c Release -r "$dotnet_rid" \
--self-contained -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \ --self-contained -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \
-o "$translator_publish_tmp" -o "$translator_publish_tmp"
cp "$translator_publish_tmp/Translator.Cli" "$appdir/usr/bin/translator-cli" cp "$translator_publish_tmp/Translator.Cli" "$appdir/usr/bin/translator-cli"
@@ -71,6 +121,47 @@ nodtool_path=$(dotnet run --project "$workspace/Launcher/WiiCompiled.Setup.Commo
cp "$nodtool_path" "$appdir/usr/bin/nodtool" cp "$nodtool_path" "$appdir/usr/bin/nodtool"
chmod +x "$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..." echo "Staging the bundled workspace snapshot..."
for dir in runtime aurora-main projects; do for dir in runtime aurora-main projects; do
cp -r "$workspace/$dir" "$appdir/workspace/$dir" cp -r "$workspace/$dir" "$appdir/workspace/$dir"
@@ -84,8 +175,21 @@ find "$appdir/workspace/aurora-main/extern" -mindepth 1 -maxdepth 1 -type d -exe
rm -rf "$appdir/workspace/runtime/build" rm -rf "$appdir/workspace/runtime/build"
cp "$workspace/Launcher/local-build.sh" "$appdir/workspace/Launcher/local-build.sh" 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 if git -C "$workspace" rev-parse HEAD >/dev/null 2>&1; then
git -C "$workspace" rev-parse HEAD > "$appdir/workspace/.bundle-version" 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 else
date -u +%s > "$appdir/workspace/.bundle-version" date -u +%s > "$appdir/workspace/.bundle-version"
fi fi
@@ -96,6 +200,7 @@ cat > "$appdir/AppRun" <<'APPRUN'
set -euo pipefail set -euo pipefail
HERE="$(dirname "$(readlink -f "$0")")" HERE="$(dirname "$(readlink -f "$0")")"
CACHE="${XDG_DATA_HOME:-$HOME/.local/share}/WiiCompiled/workspace" CACHE="${XDG_DATA_HOME:-$HOME/.local/share}/WiiCompiled/workspace"
mkdir -p "$CACHE"
if [ ! -f "$CACHE/.bundle-version" ] || \ if [ ! -f "$CACHE/.bundle-version" ] || \
[ "$(cat "$HERE/workspace/.bundle-version")" != "$(cat "$CACHE/.bundle-version")" ]; then [ "$(cat "$HERE/workspace/.bundle-version")" != "$(cat "$CACHE/.bundle-version")" ]; then
mkdir -p "$CACHE/Launcher" mkdir -p "$CACHE/Launcher"
@@ -106,9 +211,31 @@ if [ ! -f "$CACHE/.bundle-version" ] || \
cp "$HERE/workspace/Launcher/local-build.sh" "$CACHE/Launcher/local-build.sh" cp "$HERE/workspace/Launcher/local-build.sh" "$CACHE/Launcher/local-build.sh"
cp "$HERE/workspace/.bundle-version" "$CACHE/.bundle-version" cp "$HERE/workspace/.bundle-version" "$CACHE/.bundle-version"
fi 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" \ exec "$HERE/usr/bin/wiicompiled-setup" --workspace "$CACHE" \
--translator-bin "$HERE/usr/bin/translator-cli" \ --translator-bin "$HERE/usr/bin/translator-cli" \
--disc-tool-bin "$HERE/usr/bin/nodtool" "$@" --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 APPRUN
chmod +x "$appdir/AppRun" chmod +x "$appdir/AppRun"
@@ -155,11 +282,13 @@ PY
echo "Resolving appimagetool..." echo "Resolving appimagetool..."
appimagetool="$appimagetool_override" appimagetool="$appimagetool_override"
if [[ -z "$appimagetool" ]]; then if [[ -z "$appimagetool" ]]; then
appimagetool="$workspace/Launcher/artifacts/appimagetool" # 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 if [[ ! -x "$appimagetool" ]]; then
echo "Downloading appimagetool..." echo "Downloading appimagetool ($appimagetool_arch)..."
mkdir -p "$(dirname "$appimagetool")" mkdir -p "$(dirname "$appimagetool")"
curl -fsSL "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" \ curl -fsSL "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-$appimagetool_arch.AppImage" \
-o "$appimagetool" -o "$appimagetool"
chmod +x "$appimagetool" chmod +x "$appimagetool"
fi fi
@@ -169,5 +298,6 @@ mkdir -p "$output_dir"
echo "Packaging..." echo "Packaging..."
# appimagetool detects the target architecture from the first ELF executable it finds in the # 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. # AppDir; AppRun here is a shell script, not ELF, so ARCH must be set explicitly.
ARCH=x86_64 "$appimagetool" "$appdir" "$output_dir/WiiCompiled-Setup-x86_64.AppImage" output_name="WiiCompiled-Setup-$appimagetool_arch.AppImage"
echo "Built: $output_dir/WiiCompiled-Setup-x86_64.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"
+72 -11
View File
@@ -2,11 +2,12 @@
# Linux build automation: translate -> emit build shards -> configure -> compile -> publish. # 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 # This is the native-Linux counterpart to Launcher/LocalBuild.ps1. It is a from-scratch parallel
# implementation, not a port of NativeBuildFlags.ps1: that file's canonical flags and # implementation, not a port of NativeBuildFlags.ps1: that file's canonical flags exist only for
# prebuilt-package fingerprinting exist only for the Windows/mingw toolchain (a precompiled # the Windows/mingw toolchain's offline pinned dependencies, which this script does not use.
# aurora/third-party package, offline pinned dependencies) that this script does not build. # Without --native-prebuilt-dir, aurora is built from source, letting its own CMake auto-detect
# Linux always builds aurora from source, letting its own CMake auto-detect Vulkan + vendor # Vulkan + vendor SDL3/Dawn via FetchContent - the same configuration already verified working by
# SDL3/Dawn via FetchContent - the same configuration already verified working by hand. # 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 set -euo pipefail
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -62,6 +63,8 @@ ninja_override=""
dotnet_override="" dotnet_override=""
translator_dll_override="" translator_dll_override=""
translator_bin_override="" translator_bin_override=""
fuse_ld_override=""
native_prebuilt_dir=""
usage() { usage() {
cat <<'EOF' cat <<'EOF'
@@ -77,10 +80,13 @@ Usage: local-build.sh --output-dir DIR [options]
--force-clean-build Discard every translation/build cache first --force-clean-build Discard every translation/build cache first
--parallel N Pin translator threads, translated-shard job pool, and Ninja parallelism to N --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) --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) --cmake PATH / --ninja PATH Build tools (default: on PATH)
--dotnet PATH dotnet executable (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-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) --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 EOF
} }
@@ -97,11 +103,13 @@ while [[ $# -gt 0 ]]; do
--parallel) parallel_override=$2; shift 2 ;; --parallel) parallel_override=$2; shift 2 ;;
--cc) cc_override=$2; shift 2 ;; --cc) cc_override=$2; shift 2 ;;
--cxx) cxx_override=$2; shift 2 ;; --cxx) cxx_override=$2; shift 2 ;;
--fuse-ld) fuse_ld_override=$2; shift 2 ;;
--cmake) cmake_override=$2; shift 2 ;; --cmake) cmake_override=$2; shift 2 ;;
--ninja) ninja_override=$2; shift 2 ;; --ninja) ninja_override=$2; shift 2 ;;
--dotnet) dotnet_override=$2; shift 2 ;; --dotnet) dotnet_override=$2; shift 2 ;;
--translator-dll) translator_dll_override=$2; shift 2 ;; --translator-dll) translator_dll_override=$2; shift 2 ;;
--translator-bin) translator_bin_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 ;; -h|--help) usage; exit 0 ;;
*) fail "unknown argument: $1" ;; *) fail "unknown argument: $1" ;;
esac esac
@@ -157,6 +165,10 @@ require_command "$ninja_bin" ninja
require_command "$cc_bin" cc require_command "$cc_bin" cc
require_command "$cxx_bin" cxx 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 project=$workspace/projects/mkwii/recomp.yml
assets=$workspace/Assets assets=$workspace/Assets
generated=$workspace/generated generated=$workspace/generated
@@ -336,26 +348,75 @@ translator "${shard_args[@]}"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
keep_native_build=0 keep_native_build=0
stale_reason=""
if [[ -f "$build/CMakeCache.txt" ]]; then if [[ -f "$build/CMakeCache.txt" ]]; then
expected_home=$workspace/runtime expected_home=$workspace/runtime
cache_home=$(grep '^CMAKE_HOME_DIRECTORY:INTERNAL=' "$build/CMakeCache.txt" | cut -d= -f2- || true) 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 if [[ -n "$cache_home" && "$(cd "$cache_home" 2>/dev/null && pwd)" == "$expected_home" ]]; then
keep_native_build=1 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
fi fi
if [[ -d "$build" && "$keep_native_build" -eq 0 ]]; then if [[ -d "$build" && "$keep_native_build" -eq 0 ]]; then
echo "MKWCBUILD: The native build cache does not belong to this workspace path; rebuilding from scratch" echo "MKWCBUILD: The native build cache does not belong to this workspace path or toolchain; rebuilding from scratch$stale_reason"
rm -rf "$build" rm -rf "$build"
elif [[ "$keep_native_build" -eq 1 ]]; then elif [[ "$keep_native_build" -eq 1 ]]; then
echo "MKWCBUILD: Reusing the incremental native build directory" echo "MKWCBUILD: Reusing the incremental native build directory"
fi 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" log_step configure-native "Configuring the native toolchain"
"$cmake_bin" -S "$workspace/runtime" -B "$build" -G Ninja \ "$cmake_bin" "${configure_args[@]}"
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER="$cc_bin" -DCMAKE_CXX_COMPILER="$cxx_bin" \
-DCMAKE_MAKE_PROGRAM="$ninja_bin" \
-DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs"
case "$profile" in case "$profile" in
base) targets=(WiiCompiled) ;; base) targets=(WiiCompiled) ;;
+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" \
'https://rwfc.net/api/wfc/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."
+238
View File
@@ -0,0 +1,238 @@
#!/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=22.1.8
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=df0e1ecf16caf3489a272a5eea4eec9b0d82878f6477fa309504f918a0006384
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=805efad2bb91cb4967fa569e0881d10c0f69c04461cf671cccbae19f547acc34
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-22" "$work/bin/"
strip "$work/bin/clang-22"
ln -s clang-22 "$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: testing the toolchain..."
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
cat > "$test_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 "$test_dir/t.cpp" -o "$test_dir/t"
"$test_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 > "$test_dir/CMakeLists.txt" <<'EOF'
cmake_minimum_required(VERSION 3.16)
project(test CXX)
add_executable(test t.cpp)
EOF
"$work/bin/cmake" -S "$test_dir" -B "$test_dir/build" -G Ninja \
-DCMAKE_MAKE_PROGRAM="$work/bin/ninja" -DCMAKE_CXX_COMPILER="$work/bin/clang++" >/dev/null
"$work/bin/cmake" --build "$test_dir/build" >/dev/null
"$test_dir/build/test"
rm -rf "$test_dir"
trap - EXIT
rm -rf "$toolchain_dir"
mv "$work" "$toolchain_dir"
echo "prepare-portable-tools.sh: toolchain ready at $toolchain_dir ($(du -sh "$toolchain_dir" | cut -f1))"
+59 -4
View File
@@ -1,6 +1,19 @@
<img width="4190" height="1232" alt="wiicomplogofinalfinalfinalev2MADEBY_INKWRECK_plzcredit" src="https://github.com/user-attachments/assets/df7a3f2e-5336-479a-b4c0-968dd578726d" />
# WiiCompiled # WiiCompiled
<p align="center">
<a href="https://github.com/patchzyy/Wiicompiled/releases"><img alt="Windows 10 / 11, x64" src="https://img.shields.io/badge/Windows-10%20%2F%2011%20%C2%B7%20x64-0078D4"></a>
<a href="https://github.com/patchzyy/Wiicompiled/releases"><img alt="Linux, x64 / ARM64" src="https://img.shields.io/badge/Linux-x64%20%2F%20ARM64-FCC624?logo=linux&amp;logoColor=white"></a>
<a href="https://github.com/patchzyy/Wiicompiled/releases"><img alt="macOS 14+, Apple Silicon" src="https://img.shields.io/badge/macOS-14%2B%20%C2%B7%20Apple%20Silicon-0A84FF?logo=apple&amp;logoColor=white"></a>
</p>
<p align="center">
<a href="#building-from-source"><img alt="PowerPC static recompilation" src="https://img.shields.io/badge/PowerPC-static%20recompilation-FF9F0A"></a>
<a href="#retro-rewind"><img alt="Retro Rewind supported" src="https://img.shields.io/badge/Retro%20Rewind-supported-FF375F"></a>
<a href="https://github.com/TeamWheelWizard/WheelWizard/releases"><img alt="Install with Wheel Wizard" src="https://img.shields.io/badge/install%20with-Wheel%20Wizard-8B5CF6"></a>
<a href="LICENSE"><img alt="License: GPLv3" src="https://img.shields.io/badge/license-GPLv3-2EA44F?logo=gnu&amp;logoColor=white"></a>
</p>
A native PC port of Mario Kart Wii, made with static recompilation. A native PC port of Mario Kart Wii, made with static recompilation.
There's no emulator in the loop, no interpreter, no JIT, no PowerPC There's no emulator in the loop, no interpreter, no JIT, no PowerPC
@@ -46,26 +59,65 @@ Press **F10** while the game window has focus:
- Internal resolution - Internal resolution
- FPS counter - FPS counter
- Controller assignment for all four ports - Controller assignment for all four ports
- Full per-controller button mapping - Full per-controller button mapping, including the bumpers
- Dolphin-syntax input expressions and GCPadNew.ini import
- Controller vibration on/off
- Volume, instant mute, and the music ducking toggle - Volume, instant mute, and the music ducking toggle
Everything you change is saved to `Config.toml` on the spot and restored next launch. Everything you change is saved to `Config.toml` on the spot and restored next launch.
**Real controller support.** **Real controller support.**
Controllers are fed to the game as a GameCube controller. 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 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 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. inputs like paddles, touchpads and share buttons show up when the hardware reports them.
Both button-binding slots also accept SDL triggers and stick directions. Selecting an analog
input shows a threshold slider beneath it (1100%, default 50%); reaching that amount of travel
holds the chosen digital button. Each binding's threshold is saved independently in `Config.toml`
(for example, `a = "right_trigger@35,south"`).
**Keyboard and Mouse support.**
Keyboard and mouse are also available through **F10 > Controller settings > Keyboard and mouse**
for each port. Enabling this replaces that port's gamepad input. The default preset uses WASD
for the main stick, left mouse for A (accelerate), Space for B (brake), right mouse for R
(drift), middle mouse for Z (item), arrow keys for the D-pad (tricks), and Enter for Start.
Keys and mouse buttons can be remapped, including both sticks and triggers; mouse movement
is not used. These settings are saved in `keyboard_bindings.dat` and restored next launch.
**Dolphin-compatible input expressions.**
Each GameCube control can carry an expression in Dolphin's input syntax, with the same operators
and the same functions.
A Dolphin `GCPadNew.ini` can be imported directly from the F10 bar.
**Vibration toggle.**
Force feedback can be turned off for every port at once.
The official Wii U / Switch GameCube adapter (WUP-028) works too; as with Dolphin, on Windows the 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). 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)
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 ## Requirements
- Windows 10 or 11, 64-bit - Windows 10 or 11, 64-bit
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher - GPU: GTX 1650 / RX 6400 / Arc A310 or higher
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher - CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
- About 20 GB of free disk space during installation (Final game size ~5 GB) - 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, - A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
GCZ, CISO, WBFS, WIA and RVZ are accepted. GCZ, CISO, WBFS, WIA and RVZ are accepted.
@@ -86,6 +138,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 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. launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
> [!CAUTION] > [!CAUTION]
> Only take builds from this repository's > Only take builds from this repository's
> [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an > [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an
@@ -124,7 +177,9 @@ The default test suite needs no binaries and no host C++ compiler, so you can ha
translator without any game data around. translator without any game data around.
For everything beyond that, feeding in your own `main.dol`/`StaticR.rel`, running the For everything beyond that, feeding in your own `main.dol`/`StaticR.rel`, running the
translation, generating the manifest and build graph, and compiling. see [`translator/README.md`](translator/README.md). translation, generating the manifest and build graph, and compiling, see [`translator/README.md`](translator/README.md).
For a step-by-step guide on compiling both WiiCompiled and Retro Rewind from source on macOS (Apple Silicon), see the [macOS Build Guide](docs/building-macos.md).
## FAQ ## FAQ
@@ -173,7 +228,7 @@ AI coding tools were used during development of this project.
All translated output is verified against real hardware behavior and most importantly, physics accuracy is proven synced across Wii, Dolphin, and WiiCompiled (see FAQ). All translated output is verified against real hardware behavior and most importantly, physics accuracy is proven synced across Wii, Dolphin, and WiiCompiled (see FAQ).
## Credits ## Credits
- **inkwreck** - making the logo
- **[aurora](https://github.com/encounter/aurora)** - the GX rendering/windowing backend this - **[aurora](https://github.com/encounter/aurora)** - the GX rendering/windowing backend this
project's whole graphics layer sits on. MIT licensed. project's whole graphics layer sits on. MIT licensed.
- **[Dawn](https://dawn.googlesource.com/dawn)** - Google's WebGPU implementation, powering - **[Dawn](https://dawn.googlesource.com/dawn)** - Google's WebGPU implementation, powering
+2 -5
View File
@@ -26,10 +26,6 @@ Aurora itself vendors:
(shagkur) and Dave Murphy (WinterMute). `aurora-main/lib/card/SRAM.hpp`. (shagkur) and Dave Murphy (WinterMute). `aurora-main/lib/card/SRAM.hpp`.
Source: <https://github.com/devkitPro/libogc> 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 ### Dolphin Emulator data files - GPL-2.0-or-later
Copyright (c) 2003+ Dolphin Emulator Project. Copyright (c) 2003+ Dolphin Emulator Project.
@@ -121,7 +117,8 @@ Source: <https://github.com/higan-emu/libco>. Full license text:
These are pinned in `aurora-main/extern/CMakeLists.txt`, `aurora-main/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 `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 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 | | Component | Version | License | Upstream |
| --- | --- | --- | --- | | --- | --- | --- | --- |
+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 ()
@@ -135,9 +135,18 @@ elseif (_aurora_sdl3_provider STREQUAL "vendor")
endif () endif ()
include(FetchContent) 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 FetchContent_Declare(SDL
URL "https://github.com/libsdl-org/SDL/releases/download/release-${AURORA_SDL3_VERSION}/SDL3-${AURORA_SDL3_VERSION}.tar.gz" URL "https://github.com/libsdl-org/SDL/releases/download/release-${AURORA_SDL3_VERSION}/SDL3-${AURORA_SDL3_VERSION}.tar.gz"
DOWNLOAD_EXTRACT_TIMESTAMP TRUE DOWNLOAD_EXTRACT_TIMESTAMP TRUE
PATCH_COMMAND "${CMAKE_COMMAND}" -DSDL_SOURCE_DIR=<SOURCE_DIR> -P "${_aurora_sdl3_patches}"
EXCLUDE_FROM_ALL EXCLUDE_FROM_ALL
) )
FetchContent_MakeAvailable(SDL) FetchContent_MakeAvailable(SDL)
+16
View File
@@ -171,6 +171,22 @@ typedef struct PADButtonMapping {
PADButton padButton; PADButton padButton;
} PADButtonMapping; } PADButtonMapping;
// Explicitly disabled, unlike INVALID which permits default L/R trigger input.
#define PAD_NATIVE_BUTTON_DISABLED 0xfffffffeu
// Axis-to-button bindings share the persisted nativeButton field without
// changing the binary layout of existing controller mapping files.
constexpr u32 PADEncodeAxisButton(u32 axis, bool negative, u32 threshold = 50) {
return 0x10000u | axis | (negative ? 0x80u : 0u) | (threshold << 8);
}
constexpr bool PADIsAxisButton(u32 binding) { return (binding & 0xffff0000u) == 0x10000u; }
constexpr u32 PADAxisButtonThreshold(u32 binding) { return (binding >> 8) & 0xffu; }
constexpr u32 PADAxisButtonAxis(u32 binding) { return binding & 0x7fu; }
constexpr bool PADAxisButtonNegative(u32 binding) { return (binding & 0x80u) != 0; }
constexpr u32 PADAxisButtonIdentity(u32 binding) {
return PADIsAxisButton(binding) ? (binding & ~0xff00u) : binding;
}
typedef struct PADAxisMapping { typedef struct PADAxisMapping {
PADSignedNativeAxis nativeAxis; PADSignedNativeAxis nativeAxis;
s32 nativeButton; s32 nativeButton;
+18
View File
@@ -255,6 +255,14 @@ FrameWorkerState g_frameWorker;
bool frame_worker_requested() noexcept { bool frame_worker_requested() noexcept {
#ifdef AURORA_ENABLE_GX #ifdef AURORA_ENABLE_GX
static const bool enabled = [] { 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) #if defined(_WIN32)
// RenderDoc's D3D12 layer is injected before Aurora starts and needs device and command // RenderDoc's D3D12 layer is injected before Aurora starts and needs device and command
// ownership on one thread, so keep frame submission synchronous there. // 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. // Phase 3: hand the encoded group to whoever owns presentation.
void publish_presentations(std::vector<PresentationJob>&& presentationJobs, bool interpolationActive) { 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 // 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. // interpolation off, so every mode shares one surface/resize path. RenderDoc keeps the sync path.
if (frame_worker_requested() || interpolationActive || if (frame_worker_requested() || interpolationActive ||
@@ -1526,6 +1543,7 @@ void publish_presentations(std::vector<PresentationJob>&& presentationJobs, bool
present_presentation_job(job); present_presentation_job(job);
} }
} }
#endif
} }
void record_frame_telemetry() { void record_frame_telemetry() {
+126 -10
View File
@@ -4,6 +4,7 @@
#include <dolphin/pad.h> #include <dolphin/pad.h>
#include <dolphin/si.h> #include <dolphin/si.h>
#include <SDL3/SDL_mouse.h> #include <SDL3/SDL_mouse.h>
#include <SDL3/SDL_joystick.h>
#include <array> #include <array>
#include <atomic> #include <atomic>
@@ -193,6 +194,32 @@ std::array<PADButtonMapping, PAD_BUTTON_COUNT> g_defaultButtonsJoyPair{{
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT}, {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{{ std::array<PADKeyButtonBinding, PAD_BUTTON_COUNT> g_defaultKeys{{
{PAD_KEY_INVALID, PAD_BUTTON_A}, {PAD_KEY_INVALID, PAD_BUTTON_A},
{PAD_KEY_INVALID, PAD_BUTTON_B}, {PAD_KEY_INVALID, PAD_BUTTON_B},
@@ -292,6 +319,18 @@ std::array<bool, PAD_CHANMAX> g_suppressLeftTrigger{};
std::array<bool, PAD_CHANMAX> g_suppressRightTrigger{}; std::array<bool, PAD_CHANMAX> g_suppressRightTrigger{};
bool is_mouse_scancode(const s32 scancode) { return scancode < PAD_KEY_INVALID; } bool is_mouse_scancode(const s32 scancode) { return scancode < PAD_KEY_INVALID; }
bool is_native_binding_pressed(SDL_Gamepad* gamepad, u32 binding) {
if (PADIsAxisButton(binding)) {
const u32 axis = PADAxisButtonAxis(binding);
const u32 threshold = PADAxisButtonThreshold(binding);
if (axis >= SDL_GAMEPAD_AXIS_COUNT || threshold < 1 || threshold > 100) return false;
int value = SDL_GetGamepadAxis(gamepad, static_cast<SDL_GamepadAxis>(axis));
if (PADAxisButtonNegative(binding)) value = -value;
return value > 0 && value * 100 >= static_cast<int>(threshold) * 32767;
}
return binding < SDL_GAMEPAD_BUTTON_COUNT &&
SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(binding));
}
bool is_mouse_button_pressed(const s32 scancode) { bool is_mouse_button_pressed(const s32 scancode) {
const int32_t buttonNum = -(scancode + 1); const int32_t buttonNum = -(scancode + 1);
if (buttonNum < 1 || buttonNum > 5) { if (buttonNum < 1 || buttonNum > 5) {
@@ -409,8 +448,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) */ 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)) { switch (SDL_GetGamepadType(controller->m_controller)) {
case SDL_GAMEPAD_TYPE_XBOX360: case SDL_GAMEPAD_TYPE_XBOX360:
controller->m_buttonMapping = g_defaultButtonsXBox360; controller->m_buttonMapping = g_defaultButtonsXBox360;
@@ -679,10 +736,10 @@ u32 PADRead(PADStatus* status) {
} }
status[i].err = PAD_ERR_NONE; status[i].err = PAD_ERR_NONE;
if (g_keyboardBindings[i].m_mappingsSet) { if (g_keyboardBindings[i].m_mappingsSet && SDL_GetKeyboardFocus() != nullptr) {
std::ranges::for_each( std::ranges::for_each(
g_keyboardBindings[i].m_buttonMapping, [&kbState, &i, &status](const PADKeyButtonBinding& mapping) { g_keyboardBindings[i].m_buttonMapping, [&kbState, &numKeys, &i, &status](const PADKeyButtonBinding& mapping) {
if (mapping.scancode > PAD_KEY_INVALID && kbState[mapping.scancode]) { if (mapping.scancode > PAD_KEY_INVALID && mapping.scancode < numKeys && kbState[mapping.scancode]) {
status[i].button |= mapping.padButton; status[i].button |= mapping.padButton;
} else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) { } else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) {
status[i].button |= mapping.padButton; status[i].button |= mapping.padButton;
@@ -743,13 +800,54 @@ u32 PADRead(PADStatus* status) {
status[i].triggerRight = static_cast<u8>(std::min(static_cast<int>(status[i].triggerRight) + tr, 255)); status[i].triggerRight = static_cast<u8>(std::min(static_cast<int>(status[i].triggerRight) + tr, 255));
} }
if (controller) { if (controller && !g_keyboardBindings[i].m_mappingsSet) {
EnsureMappingLoaded(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 leftTriggerSet = false;
bool rightTriggerSet = false; bool rightTriggerSet = false;
std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet, std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet,
&rightTriggerSet](const auto& mapping) { &rightTriggerSet](const auto& mapping) {
if (SDL_GetGamepadButton(controller->m_controller, static_cast<SDL_GamepadButton>(mapping.nativeButton))) { if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) {
status[i].button |= mapping.padButton; status[i].button |= mapping.padButton;
} }
@@ -766,7 +864,7 @@ u32 PADRead(PADStatus* status) {
if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) { if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) {
return; return;
} }
if (SDL_GetGamepadButton(controller->m_controller, static_cast<SDL_GamepadButton>(mapping.nativeButton))) { if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) {
status[i].button |= mapping.padButton; status[i].button |= mapping.padButton;
} }
@@ -778,6 +876,7 @@ u32 PADRead(PADStatus* status) {
} }
}); });
// TODO: Add serializable mappings for these (probably not necessary)? // TODO: Add serializable mappings for these (probably not necessary)?
static constexpr std::array<std::pair<SDL_GamepadButton, PADExtButton>, PAD_EXT_BUTTON_COUNT> kExtButtonMappings{{ static constexpr std::array<std::pair<SDL_GamepadButton, PADExtButton>, PAD_EXT_BUTTON_COUNT> kExtButtonMappings{{
{SDL_GAMEPAD_BUTTON_BACK, PAD_BUTTON_BACK}, {SDL_GAMEPAD_BUTTON_BACK, PAD_BUTTON_BACK},
@@ -859,6 +958,17 @@ u32 PADRead(PADStatus* status) {
Sint16 tl = std::max(static_cast<Sint16>(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_L)); Sint16 tl = std::max(static_cast<Sint16>(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_L));
Sint16 tr = std::max(static_cast<Sint16>(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_R)); Sint16 tr = std::max(static_cast<Sint16>(0), _get_axis_value(controller, PAD_AXIS_TRIGGER_R));
// Games can read either the digital L/R bits or their analog pressure.
// An explicit button binding must drive both, otherwise the original
// L2/R2 axis still activates L/R even when it was rebound to L1/R1.
// Real GC pads retain independent analog travel and end-stop clicks.
if (!(controller->m_isGameCube ||
(SDL_GetGamepadType(controller->m_controller) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO &&
controller->m_pid == 0x2073))) {
if (leftTriggerSet) tl = (status[i].button & PAD_TRIGGER_L) != 0 ? 32767 : 0;
if (rightTriggerSet) tr = (status[i].button & PAD_TRIGGER_R) != 0 ? 32767 : 0;
}
if (controller->m_deadZones.emulateTriggers) { if (controller->m_deadZones.emulateTriggers) {
if (!leftTriggerSet && tl > controller->m_deadZones.leftTriggerActivationZone) { if (!leftTriggerSet && tl > controller->m_deadZones.leftTriggerActivationZone) {
status[i].button |= PAD_TRIGGER_L; status[i].button |= PAD_TRIGGER_L;
@@ -903,12 +1013,13 @@ void PADControlMotor(const u32 chan, const u32 cmd) {
} }
if (controller->m_isGameCube) { if (controller->m_isGameCube) {
if (cmd == PAD_MOTOR_STOP) { if (cmd == PAD_MOTOR_STOP || cmd == PAD_MOTOR_STOP_HARD) {
aurora::input::controller_rumble(instance, 0, 1, 0); // Use an unambiguous motor-off request. The (0, 1) coast encoding
// requires SDL's GameCube brake mode; other backends or an overridden
// hint interpret it as rumble and can leave the controller vibrating.
aurora::input::controller_rumble(instance, 0, 0, 0);
} else if (cmd == PAD_MOTOR_RUMBLE) { } else if (cmd == PAD_MOTOR_RUMBLE) {
aurora::input::controller_rumble(instance, 1, 1, 0); aurora::input::controller_rumble(instance, 1, 1, 0);
} else if (cmd == PAD_MOTOR_STOP_HARD) {
aurora::input::controller_rumble(instance, 0, 0, 0);
} }
} else { } else {
if (cmd == PAD_MOTOR_STOP) { if (cmd == PAD_MOTOR_STOP) {
@@ -1191,6 +1302,11 @@ BOOL PADSetKeyButtonBindings(const u32 port, PADKeyButtonBinding bindings[PAD_BU
} }
PADKeyButtonBinding* PADGetKeyButtonBindings(const u32 port, u32* buttonCount) { PADKeyButtonBinding* PADGetKeyButtonBindings(const u32 port, u32* buttonCount) {
PADInit();
if (!g_keyboardBindingsLoaded) {
g_keyboardBindingsLoaded = true;
load_keyboard_bindings();
}
if (port >= PAD_MAX_CONTROLLERS || !g_keyboardBindings[port].m_mappingsSet) { if (port >= PAD_MAX_CONTROLLERS || !g_keyboardBindings[port].m_mappingsSet) {
return nullptr; return nullptr;
} }
+22 -6
View File
@@ -168,7 +168,12 @@ struct RenderPass {
Range resolveUniformRange; Range resolveUniformRange;
std::array<u32, 3> resolveCopyFilterCoefficients{0, 64, 0}; std::array<u32, 3> resolveCopyFilterCoefficients{0, 64, 0};
Vec4<float> clearColorValue{0.f, 0.f, 0.f, 0.f}; Vec4<float> clearColorValue{0.f, 0.f, 0.f, 0.f};
float clearDepthValue = 1.f; // 1.f is the forward-Z "farthest" clear value; under UseReversedZ farthest is 0.f instead (see
// gx::clear_depth_value(), which the main render pass explicitly overrides this default with -
// any OTHER pass that keeps this default, e.g. an offscreen render-to-texture pass composited
// later, needs the same reversed-Z-aware value or its depth buffer starts "already nearest",
// failing every subsequent depth test and making whatever's drawn into it vanish).
float clearDepthValue = gx::UseReversedZ ? 0.f : 1.f;
CommandList commands; CommandList commands;
bool clearColor = true; bool clearColor = true;
bool clearDepth = true; bool clearDepth = true;
@@ -757,7 +762,9 @@ void begin_offscreen(uint32_t width, uint32_t height) {
.targetSize = {width, height, 1}, .targetSize = {width, height, 1},
.msaaSamples = 1, .msaaSamples = 1,
.clearColorValue = {0.f, 0.f, 0.f, 0.f}, .clearColorValue = {0.f, 0.f, 0.f, 0.f},
.clearDepthValue = 1.f, // See the RenderPass::clearDepthValue default's comment: this offscreen pass gets its own
// depth buffer, and the farthest clear value is 0.f, not 1.f, under UseReversedZ.
.clearDepthValue = gx::UseReversedZ ? 0.f : 1.f,
.clearColor = true, .clearColor = true,
.clearDepth = true, .clearDepth = true,
}; };
@@ -1410,10 +1417,19 @@ static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vec
switch (cmd.type) { switch (cmd.type) {
case CommandType::SetViewport: { case CommandType::SetViewport: {
const auto& vp = cmd.data.setViewport; const auto& vp = cmd.data.setViewport;
// WebGPU requires 0 <= minDepth <= maxDepth <= 1, and the guest's (near, far) order is already // WebGPU requires 0 <= minDepth <= maxDepth <= 1. vp.znear/vp.zfar are in GX's own distance
// reproduced in clip space. Passing the raw swapped pair diverged per backend in release builds. // terms (0 = near); under UseReversedZ the host depth-buffer storage direction is flipped
const float minDepth = std::clamp(std::min(vp.znear, vp.zfar), 0.0f, 1.0f); // (near = 1, far = 0), so this range has to be remapped through 1-x the same way the
const float maxDepth = std::clamp(std::max(vp.znear, vp.zfar), 0.0f, 1.0f); // projection matrix, depth compare function, and clear value all are - a plain min/max clamp
// (the previous code here) maps a *restricted* range (e.g. a viewport deliberately narrowed
// to force something to draw "in front of everything") to the wrong end of the buffer: what
// should land near the near-storage-extreme (1.0) instead lands near the far-storage-extreme
// (0.0), so anything else drawn afterward at its true depth wins the compare test and the
// "in front" geometry silently vanishes. A full [0,1] viewport is unaffected either way,
// which is why this only broke specific elements, not the whole scene. Matches upstream
// aurora's apply_viewport (lib/gfx/encoding.cpp) exactly.
const float minDepth = gx::UseReversedZ ? 1.0f - vp.zfar : vp.znear;
const float maxDepth = gx::UseReversedZ ? 1.0f - vp.znear : vp.zfar;
pass.SetViewport(vp.left, vp.top, vp.width, vp.height, minDepth, maxDepth); pass.SetViewport(vp.left, vp.top, vp.width, vp.height, minDepth, maxDepth);
} break; } break;
case CommandType::SetScissor: { case CommandType::SetScissor: {
+1 -1
View File
@@ -92,7 +92,7 @@ struct Params {
constexpr std::string_view ReversedZBody = R"( constexpr std::string_view ReversedZBody = R"(
fn gx_z24(depth: f32) -> u32 { fn gx_z24(depth: f32) -> u32 {
return min(u32(clamp(depth, 0.0, 1.0) * 16777216.0), 0x00ffffffu); return min(u32(clamp(1.0 - depth, 0.0, 1.0) * 16777215.0 + 0.5), 0x00ffffffu);
} }
)"sv; )"sv;
+1 -1
View File
@@ -137,7 +137,7 @@ fn gx_z24_at_coord(unclamped_coord: vec2i) -> u32 {
let tex_size = vec2i(textureDimensions(src)); let tex_size = vec2i(textureDimensions(src));
let coord = clamp(unclamped_coord, vec2i(0), tex_size - vec2i(1)); let coord = clamp(unclamped_coord, vec2i(0), tex_size - vec2i(1));
let depth = textureLoad(src, coord, 0); let depth = textureLoad(src, coord, 0);
return min(u32(clamp(depth, 0.0, 1.0) * 16777216.0), 0x00ffffffu); return min(u32(clamp(1.0 - depth, 0.0, 1.0) * 16777215.0 + 0.5), 0x00ffffffu);
} }
)"s )"s
: R"( : R"(
+13 -4
View File
@@ -1416,23 +1416,32 @@ static inline GXBlendFactor remove_dst_alpha_usage(GXBlendFactor fac) {
} }
} }
// GX_LEQUAL etc. describe "pass if this pixel is closer than/equal to what's stored" in GX's own
// distance terms, independent of how that distance is encoded as a host depth value. Under
// UseReversedZ the encoding is flipped (near=1, far=0), so "closer" now corresponds to a *larger*
// stored value, not a smaller one - the ordered compare functions (LESS/LEQUAL/GREATER/GEQUAL)
// must invert to match, or the depth test silently runs backwards (verified directly: this was
// the actual cause of a bug report after the projection/shader half of the reverse-Z fix
// eliminated the double-negation that used to accidentally keep the unreversed comparisons
// correct - LEQUAL now needs GreaterEqual, not LessEqual, once the encoding it's testing against
// is genuinely reversed). Matches upstream aurora's to_compare_function exactly.
static inline wgpu::CompareFunction to_compare_function(GXCompare func) { static inline wgpu::CompareFunction to_compare_function(GXCompare func) {
switch (func) { switch (func) {
DEFAULT_FATAL("invalid depth fn {}", underlying(func)); DEFAULT_FATAL("invalid depth fn {}", underlying(func));
case GX_NEVER: case GX_NEVER:
return wgpu::CompareFunction::Never; return wgpu::CompareFunction::Never;
case GX_LESS: case GX_LESS:
return wgpu::CompareFunction::Less; return UseReversedZ ? wgpu::CompareFunction::Greater : wgpu::CompareFunction::Less;
case GX_EQUAL: case GX_EQUAL:
return wgpu::CompareFunction::Equal; return wgpu::CompareFunction::Equal;
case GX_LEQUAL: case GX_LEQUAL:
return wgpu::CompareFunction::LessEqual; return UseReversedZ ? wgpu::CompareFunction::GreaterEqual : wgpu::CompareFunction::LessEqual;
case GX_GREATER: case GX_GREATER:
return wgpu::CompareFunction::Greater; return UseReversedZ ? wgpu::CompareFunction::Less : wgpu::CompareFunction::Greater;
case GX_NEQUAL: case GX_NEQUAL:
return wgpu::CompareFunction::NotEqual; return wgpu::CompareFunction::NotEqual;
case GX_GEQUAL: case GX_GEQUAL:
return wgpu::CompareFunction::GreaterEqual; return UseReversedZ ? wgpu::CompareFunction::LessEqual : wgpu::CompareFunction::GreaterEqual;
case GX_ALWAYS: case GX_ALWAYS:
return wgpu::CompareFunction::Always; return wgpu::CompareFunction::Always;
} }
+8 -1
View File
@@ -485,7 +485,14 @@ const gfx::TextureBind& get_texture(GXTexMapID id) noexcept;
void resolve_sampled_textures(const ShaderInfo& info) noexcept; void resolve_sampled_textures(const ShaderInfo& info) noexcept;
inline float clear_depth_value() { inline float clear_depth_value() {
return std::min(static_cast<float>(g_gxState.clearDepth) / 16777216.f, 16777215.f / 16777216.f); // g_gxState.clearDepth is in GX's own distance terms (0 = near, larger = farther), independent of
// how UseReversedZ encodes that as a host depth value - it must be re-mapped the same way the
// projection matrix and depth compare function are, or the buffer clears to the wrong extreme
// (verified directly: matches upstream aurora's clear_depth_value, which does this same inversion
// and was the second missing piece alongside to_compare_function's compare-op inversion).
const float normalizedDepth =
std::min(static_cast<float>(g_gxState.clearDepth) / 16777216.f, 16777215.f / 16777216.f);
return UseReversedZ ? (1.f - normalizedDepth) : normalizedDepth;
} }
inline bool render_target_has_alpha(GXPixelFmt pixelFmt) noexcept { return pixelFmt == GX_PF_RGBA6_Z24; } inline bool render_target_has_alpha(GXPixelFmt pixelFmt) noexcept { return pixelFmt == GX_PF_RGBA6_Z24; }
+23 -8
View File
@@ -993,11 +993,13 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
"\n let clip_base = select(clip_a, clip_b, use_b);" "\n let clip_base = select(clip_a, clip_b, use_b);"
"\n out.pos = vec4f(clip_base.xy + offset_ndc * clip_base.w, clip_base.zw);"; "\n out.pos = vec4f(clip_base.xy + offset_ndc * clip_base.w, clip_base.zw);";
} }
if constexpr (UseReversedZ) { // The near/far depth correction used to be applied here per-vertex (out.pos.z = -out.pos.z for
vtxXfrAttrsPre += "\n out.pos.z = -out.pos.z;"; // reversed, or += out.pos.w for forward), redundantly on top of the same correction already
} else { // folded into ubuf.proj by effective_projection() (shader_info.cpp) - applying it twice canceled
vtxXfrAttrsPre += "\n out.pos.z += out.pos.w;"; // out for the common case (any draw where effective_projection() decides to flip), silently
} // making "reversed" Z behave identically to forward Z. It is now applied exactly once, in the
// projection matrix alone (matching upstream aurora commit 1dde08fa: "Move depth correction to
// projection matrix"), so nothing needs to happen to out.pos.z here.
// GX rasterizes at a 7/12 pixel center when antialiasing is disabled, while WebGPU rasterizes at 1/2. // GX rasterizes at a 7/12 pixel center when antialiasing is disabled, while WebGPU rasterizes at 1/2.
vtxXfrAttrsPre += vtxXfrAttrsPre +=
"\n let gx_pixel_center_correction = " "\n let gx_pixel_center_correction = "
@@ -1465,7 +1467,14 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
textureDependency.texMapId, uvIn); textureDependency.texMapId, uvIn);
} }
std::string fogDepthExpr = UseReversedZ ? "in.pos.z" : "(1.0 - in.pos.z)"; // in.pos.z is the host NDC z (forward: 0=near/1=far; reversed: 1=near/0=far post-fix), but this
// expression needs to produce GX's own native distance term (always 0=near/1=far, matching how
// g_gxState.clearDepth/clear_depth_value() are interpreted before their own UseReversedZ
// inversion) - forward already matches directly; reversed needs the same 1-x flip everything
// else reversed-Z-aware uses. This was backwards (verified directly against upstream aurora's
// identical expression in build_shader_source), which fed both fog density and the GX_ZT_ADD
// z-texture path the wrong distance value.
std::string fogDepthExpr = UseReversedZ ? "(1.0 - in.pos.z)" : "in.pos.z";
std::string fogZCoordExpr = std::string fogZCoordExpr =
fmt::format("u32(round(clamp({}, 0.0, 1.0) * 16777216.0))", fogDepthExpr); fmt::format("u32(round(clamp({}, 0.0, 1.0) * 16777216.0))", fogDepthExpr);
if (usesZTextureDepth) { if (usesZTextureDepth) {
@@ -1498,7 +1507,7 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
fragmentFn += fmt::format( fragmentFn += fmt::format(
"\n let oldZ = u32(round(clamp({0}, 0.0, 1.0) * 16777216.0));" "\n let oldZ = u32(round(clamp({0}, 0.0, 1.0) * 16777216.0));"
"\n ztexCoord = (ztexCoord + oldZ) & 0x00ffffffu;", "\n ztexCoord = (ztexCoord + oldZ) & 0x00ffffffu;",
UseReversedZ ? "in.pos.z" : "(1.0 - in.pos.z)"); UseReversedZ ? "(1.0 - in.pos.z)" : "in.pos.z");
} }
fragmentFn += "\n let ztexDepth = f32(ztexCoord) / 16777216.0;"; fragmentFn += "\n let ztexDepth = f32(ztexCoord) / 16777216.0;";
fogZCoordExpr = "ztexCoord"; fogZCoordExpr = "ztexCoord";
@@ -1639,7 +1648,13 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
" @builtin(frag_depth) depth: f32,\n" " @builtin(frag_depth) depth: f32,\n"
"};"; "};";
fragmentFn += fmt::format("\n let fragDepth = {}ztexDepth;", UseReversedZ ? "" : "1.0 - "); // ztexDepth is in GX's native distance terms (0=near/1=far, see fogDepthExpr's comment above),
// but frag_depth must be written in the same host NDC-z convention in.pos.z itself uses -
// forward matches directly (no change), reversed needs the same 1-x flip. This was backwards
// the same way fogDepthExpr was (verified by the same derivation, since aurora upstream has no
// directly equivalent line here to cross-check against - this z-texture-depth-output path
// appears to be specific to this fork).
fragmentFn += fmt::format("\n let fragDepth = {}ztexDepth;", UseReversedZ ? "1.0 - " : "");
fragmentReturnType = "FragmentOutput"; fragmentReturnType = "FragmentOutput";
fragmentReturn = fragmentReturn =
" var out: FragmentOutput;\n" " var out: FragmentOutput;\n"
+12 -4
View File
@@ -548,14 +548,22 @@ constexpr size_t kStagedUniformBytes =
96 + sizeof(Mat4x4<float>) + sizeof(Mat3x4<float>) * (MaxPostexMtx + MaxPnMtx); 96 + sizeof(Mat4x4<float>) + sizeof(Mat3x4<float>) * (MaxPostexMtx + MaxPnMtx);
// The host viewport always receives the normalized GX depth window (render_pass_impl clamps to minDepth <= maxDepth). // The host viewport always receives the normalized GX depth window (render_pass_impl clamps to minDepth <= maxDepth).
//
// Folds the near/far depth correction the vertex shader used to apply per-vertex directly into the
// projection matrix instead (matching upstream aurora commit 1dde08fa, "Move depth correction to
// projection matrix") - valid because the correction is a linear combination of the z/w rows, so
// applying it once here to the row is equivalent to applying it once per-vertex to the dot product,
// and it must be applied exactly once: doing it here AND in the shader (the previous bug) canceled
// the negation out for `flip`, silently making "reversed" Z behave identically to forward Z.
// `flip` decides which of the two single-application forms this draw needs: true bakes in the
// reversed-Z inversion (z' = -z), false bakes in the forward-Z near/far combination (z' = z + w) -
// exactly one always applies, never both, and never neither.
static Mat4x4<float> effective_projection() noexcept { static Mat4x4<float> effective_projection() noexcept {
const auto& vp = g_gxState.renderViewport; const auto& vp = g_gxState.renderViewport;
const bool flip = (vp.znear <= vp.zfar) == UseReversedZ; const bool flip = (vp.znear <= vp.zfar) == UseReversedZ;
Mat4x4<float> proj = g_gxState.proj; Mat4x4<float> proj = g_gxState.proj;
if (flip) { for (size_t i = 0; i < 4; ++i) {
for (size_t i = 0; i < 4; ++i) { proj.m2.m[i] = flip ? -proj.m2.m[i] : (proj.m2.m[i] + proj.m3.m[i]);
proj.m2.m[i] = -(proj.m2.m[i] + proj.m3.m[i]);
}
} }
return proj; return proj;
} }
+9
View File
@@ -401,6 +401,8 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
return -1; return -1;
} }
controller.m_isGameCube = controller.m_vid == 0x057E && controller.m_pid == 0x0337; controller.m_isGameCube = controller.m_vid == 0x057E && controller.m_pid == 0x0337;
const char* serial = SDL_GetGamepadSerial(ctrl);
controller.m_gameCubeUseOrdinaryStop = controller.m_isGameCube && serial && "GCP+"sv == serial;
if (controller.m_isGameCube || if (controller.m_isGameCube ||
(SDL_GetGamepadType(ctrl) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO && controller.m_pid == 0x2073)) { (SDL_GetGamepadType(ctrl) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO && controller.m_pid == 0x2073)) {
controller.m_deadZones.emulateTriggers = false; controller.m_deadZones.emulateTriggers = false;
@@ -481,6 +483,13 @@ bool controller_has_rumble(Uint32 instance) noexcept {
void controller_rumble(uint32_t instance, uint16_t low_freq_intensity, uint16_t high_freq_intensity, void controller_rumble(uint32_t instance, uint16_t low_freq_intensity, uint16_t high_freq_intensity,
uint16_t duration_ms) noexcept { uint16_t duration_ms) noexcept {
if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) { if (auto it = g_GameControllers.find(instance); it != g_GameControllers.end()) {
// GC Pocket+ has been observed continuing to vibrate after a hard stop;
// an ordinary stop cleared it. With GAMECUBE_RUMBLE_BRAKE enabled, SDL
// encodes (0, 1) as adapter command 0, whereas (0, 0) sends command 2.
// Apply the workaround here so shutdown uses the same stop as PAD calls.
if (it->second.m_gameCubeUseOrdinaryStop && low_freq_intensity == 0 && high_freq_intensity == 0) {
high_freq_intensity = 1;
}
SDL_RumbleGamepad(it->second.m_controller, low_freq_intensity, high_freq_intensity, duration_ms); SDL_RumbleGamepad(it->second.m_controller, low_freq_intensity, high_freq_intensity, duration_ms);
} }
} }
+1
View File
@@ -17,6 +17,7 @@ extern Module Log;
struct GameController { struct GameController {
SDL_Gamepad* m_controller = nullptr; SDL_Gamepad* m_controller = nullptr;
bool m_isGameCube = false; bool m_isGameCube = false;
bool m_gameCubeUseOrdinaryStop = false;
Sint32 m_index = -1; Sint32 m_index = -1;
Sint32 m_playerIndex = -1; Sint32 m_playerIndex = -1;
bool m_hasRumble = false; bool m_hasRumble = false;
+349
View File
@@ -0,0 +1,349 @@
# Building WiiCompiled and Retro Rewind on macOS
This guide covers building **WiiCompiled** (base game) and **Retro Rewind** from source on macOS for Apple Silicon (`arm64`). Follow these instructions to compile the native executables directly.
> [!NOTE]
> If you only want to build the base game (**WiiCompiled**), look for sections marked **`(Skip if only building WiiCompiled)`** to bypass Retro Rewind and online payload steps.
---
## 1. Prerequisites
### System Requirements
- **Hardware**: Apple Silicon Mac (M1/M2/M3/M4)
- **Operating System**: macOS 14 (Sonoma) or later
- **Xcode Command Line Tools**:
```bash
xcode-select --install
```
### Toolchain Dependencies
Install the required tools using [Homebrew](https://brew.sh):
```bash
brew install cmake ninja
brew install --cask dotnet-sdk@8
```
Verify that Clang, CMake, Ninja, and the .NET 8 runtime are available:
```bash
clang --version
cmake --version
ninja --version
dotnet --list-runtimes # Must list Microsoft.NETCore.App 8.x
```
---
## 2. Required Game and Mod Assets
Due to legal requirements, no proprietary Nintendo assets or code are included in this repository. You must provide your own legally dumped game files.
1. **Mario Kart Wii PAL (`RMCP01`) Disc Image** *(Required)*:
- Supported formats: `.iso`, `.wbfs`, `.ciso`, `.rvz`, `.gcm`, `.gcz`.
2. **nodtool** *(Required for disc extraction)*:
- Download the macOS Apple Silicon binary of [nodtool](https://github.com/encounter/nod/releases):
```bash
curl -fsSL "https://github.com/encounter/nod/releases/download/v2.0.0-alpha.10/nodtool-macos-arm64" -o nodtool
chmod +x nodtool
```
3. **Retro Rewind Distribution** *(Skip if only building WiiCompiled)*:
- Download the [Retro Rewind](https://wiki.tockdom.com/wiki/Retro_Rewind) release package. You will need the `RetroRewind6` folder (which contains `Binaries/Code.pul`).
4. **Retro-WFC Payload** *(Skip if only building WiiCompiled or building offline)*:
- Required for online multiplayer on Retro Rewind. Downloaded during setup from `https://rwfc.net/api/wfc/payload?g=RMCPD00`.
---
## 3. Step 1: Extract Disc Assets
Extract your clean PAL `RMCP01` disc into the `Assets/` directory of the repository:
```bash
# Using nodtool directly into a temporary scratch directory
mkdir -p /tmp/mkw-extract
./nodtool extract /path/to/RMCP01.iso /tmp/mkw-extract
# Copy extracted assets into the repository Assets directory
rm -rf Assets/DATA/files Assets/DATA/sys
mkdir -p Assets/DATA
cp /tmp/mkw-extract/*/sys/main.dol Assets/main.dol
cp /tmp/mkw-extract/*/files/rel/StaticR.rel Assets/StaticR.rel
cp -R /tmp/mkw-extract/*/files Assets/DATA/files
cp -R /tmp/mkw-extract/*/sys Assets/DATA/sys
# Clean up temporary files
rm -rf /tmp/mkw-extract
```
> [!TIP]
> Alternatively, you can use the repository's helper script:
> ```bash
> Launcher/macos/extract-disc.command --game /path/to/RMCP01.iso --assets-dir Assets --nodtool ./nodtool
> ```
### Verify Extracted Asset Hashes
Confirm that the extracted files match the expected clean PAL revision:
```bash
shasum -a 256 Assets/main.dol Assets/StaticR.rel
```
- `Assets/main.dol`: `80d18895b39c63bd80f457398bfcbb91b7d16ac116a41a88967e954080155b05`
- `Assets/StaticR.rel`: `16d9d146112541fefea701ecb5bc1a496f9d50e4a752fbb5b6778e7c6399f67d`
---
## 4. Step 2: Build the Translator CLI
Compile the static recompiler CLI:
```bash
dotnet build translator/src/Translator.Cli/Translator.Cli.csproj -c Release
```
Define a shell function to invoke the translator (ensuring paths with spaces are handled safely):
```bash
translator() {
dotnet "$(pwd)/translator/src/Translator.Cli/bin/Release/net8.0/Translator.Cli.dll" "$@"
}
```
---
## 5. Step 3: Translation
### A. Translate Base Game Functions
```bash
mkdir -p generated/functions build/base
translator translate-recursive 0x8000629c \
--project projects/mkwii/recomp.yml \
--outdir generated/functions \
--output-metadata generated/base_translation_output.json \
--production-source-bundle generated/base_translation_sources.bin \
--no-function-files \
--prune-stale \
--threads $(sysctl -n hw.ncpu)
```
### B. Emit Base Manifest
```bash
translator emit-base-manifest \
--project projects/mkwii/recomp.yml \
--out build/base \
--functions-dir generated/functions \
--translation-output-metadata generated/base_translation_output.json \
--region P
```
---
### C. Stage and Translate Retro Rewind *(Skip this step if you only want to build WiiCompiled)*
1. Stage `Code.pul`:
```bash
RETRO_DIR="/path/to/RetroRewind6"
mkdir -p PulsarPacks/completed/RetroRewind/RetroRewind6/Binaries
cp "$RETRO_DIR/Binaries/Code.pul" PulsarPacks/completed/RetroRewind/RetroRewind6/Binaries/Code.pul
```
2. **Retro-WFC Payload Setup (for Online Multiplayer)**:
Online play in Retro Rewind requires the shared Retro-WFC payload. Download and validate it:
```bash
mkdir -p build/retro-wfc/binary
curl -fsSL --retry 3 "https://nas.play.rwfc.net/payload?g=RMCPD00" \
-o build/retro-wfc/binary/payload.RMCPD00.bin
# Validate payload signature and integrity
translator validate-retro-wfc-payload --directory build/retro-wfc
```
3. Run Retro Rewind translation:
```bash
mkdir -p build/mods/retro_rewind_full_cpp
translator translate-mod \
--project projects/mkwii/recomp.yml \
--profile retro-rewind \
--base-manifest build/base/mkwii_base_manifest.json \
--base-translation-output-metadata generated/base_translation_output.json \
--code-pul "$RETRO_DIR/Binaries/Code.pul" \
--mod-root "$RETRO_DIR" \
--mod-name "Retro Rewind" \
--region P \
--out build/mods/retro_rewind_full_cpp \
--prefer-cached-inputs \
--emit-cpp \
--threads $(sysctl -n hw.ncpu) \
--retro-wfc-payload build/retro-wfc/binary/payload.RMCPD00.bin
```
> [!TIP]
> If you do not want online play or do not have an internet connection, replace `--retro-wfc-payload ...` with `--skip-retro-wfc`.
---
### D. Generate Data Initialization and Build Shards
First, generate the embedded game data initializer:
```bash
translator generate-data-init --project projects/mkwii/recomp.yml
```
Next, generate the CMake build shards using **one** of the following options:
#### Option 1: Base Game Only (WiiCompiled)
```bash
mkdir -p generated/build_shards
translator emit-build-shards \
--project projects/mkwii/recomp.yml \
--base-metadata generated/base_translation_output.json \
--base-functions-dir generated/functions \
--native-source-dir runtime/src \
--out generated/build_shards
```
#### Option 2: Base Game + Retro Rewind
```bash
mkdir -p generated/build_shards
translator emit-build-shards \
--project projects/mkwii/recomp.yml \
--base-metadata generated/base_translation_output.json \
--base-functions-dir generated/functions \
--native-source-dir runtime/src \
--out generated/build_shards \
--resolved-profile build/mods/retro_rewind_full_cpp/resolved_dispatch_profile.json \
--retro-cpp-dir build/mods/retro_rewind_full_cpp/cpp
```
---
## 6. Step 4: Configure and Compile with CMake & Ninja
Configure the native C++ build targeting Apple Silicon:
```bash
cmake -S runtime -B build-macos -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DAURORA_SDL3_PROVIDER=vendor
```
Compile the desired target:
```bash
# To build WiiCompiled only:
cmake --build build-macos --target WiiCompiled --parallel $(sysctl -n hw.ncpu)
# OR to build both WiiCompiled and Retro Rewind:
cmake --build build-macos --target WiiCompiled RetroRewind --parallel $(sysctl -n hw.ncpu)
```
Once compilation completes, the executables are ready in your build directory:
- `build-macos/WiiCompiled`
- `build-macos/RetroRewind` (if built)
During the build, CMake automatically copies the required runtime assets into `build-macos/`:
- `build-macos/dsp_coef.bin`
- `build-macos/initial_pipeline_cache.db`
- `build-macos/wii_bootstrap/`
---
## 7. Step 5: Running Executables from the Build Folder
### Configure `Config.toml`
The runtime reads configuration from `~/Library/Application Support/WiiCompiled/Config.toml`.
Create the directory and configuration file:
```bash
mkdir -p "$HOME/Library/Application Support/WiiCompiled"
```
#### For Base Game Only (WiiCompiled):
```toml
# ~/Library/Application Support/WiiCompiled/Config.toml
[video]
widescreen = true
resolution_multiplier = 1.0
graphics_api = "metal"
[paths]
dvd_root = "/absolute/path/to/Wiicompiled/Assets/DATA"
```
#### For Base Game and Retro Rewind:
```toml
# ~/Library/Application Support/WiiCompiled/Config.toml
[video]
widescreen = true
resolution_multiplier = 1.0
graphics_api = "metal"
[paths]
dvd_root = "/absolute/path/to/Wiicompiled/Assets/DATA"
retro_rewind_root = "/path/to/RetroRewind6"
```
> [!NOTE]
> Ensure `dvd_root` points to the directory containing `files` and `sys/fst.bin`.
### Launching the Game
Run the compiled binaries directly from your terminal or by double clicking:
```bash
# Run base WiiCompiled
./build-macos/WiiCompiled
# Run Retro Rewind
./build-macos/RetroRewind
```
Press **F10** in-game at any time to open the configuration bar (controls, resolution, display settings, audio).
---
## Quick Reference: Automated Helper Script
The repository provides a script (`Launcher/local-build-macos.command`) that handles extraction, translation, and compilation in a single command.
### Building Base Game Only:
```bash
Launcher/local-build-macos.command \
--profile base \
--output-dir build-macos/Products \
--game /path/to/RMCP01.iso \
--nodtool ./nodtool
```
### Building Both (with Online Retro-WFC Payload):
```bash
# 1. Download Retro-WFC payload into a staging directory:
mkdir -p build/retro-wfc/binary
curl -fsSL --retry 3 "https://rwfc.net/api/wfc/payload?g=RMCPD00" \
-o build/retro-wfc/binary/payload.RMCPD00.bin
# 2. Run the automated build with the payload directory:
Launcher/local-build-macos.command \
--profile both \
--output-dir build-macos/Products \
--base-output-dir build-macos/Products \
--game /path/to/RMCP01.iso \
--nodtool ./nodtool \
--retro-rewind-package-dir /path/to/RetroRewind6 \
--retro-wfc-offline-dir build/retro-wfc
```
### Building Both (Offline, Skipping Payload):
```bash
Launcher/local-build-macos.command \
--profile both \
--output-dir build-macos/Products \
--base-output-dir build-macos/Products \
--game /path/to/RMCP01.iso \
--nodtool ./nodtool \
--retro-rewind-package-dir /path/to/RetroRewind6 \
--skip-retro-wfc-payload
```
When finished, the compiled executables reside in `native-build-macos/` and the bundled `.app` packages are placed in `build-macos/Products/`.
+1 -1
View File
@@ -58,7 +58,7 @@ profiles:
module_link_base: 0x803992E0 module_link_base: 0x803992E0
output: build/mods/retro_rewind_full_cpp output: build/mods/retro_rewind_full_cpp
enable_retro_wfc: true enable_retro_wfc: true
retro_wfc_payload: http://nas.play.rwfc.net/payload?g=RMCPD00 retro_wfc_payload: https://rwfc.net/api/wfc/payload?g=RMCPD00
retro_wfc_legacy_bootstrap_hook: 0x800ED6E8 retro_wfc_legacy_bootstrap_hook: 0x800ED6E8
riivolution: riivolution:
xml: xml/RetroRewind6.xml xml: xml/RetroRewind6.xml
+184 -45
View File
@@ -1,18 +1,28 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(mkw_recompiled) project(mkw_recompiled)
if((NOT (WIN32 AND MINGW)) AND (NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")) if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
message(FATAL_ERROR "WiiCompiled requires Windows (LLVM-MinGW) or native Linux") message(FATAL_ERROR "WiiCompiled requires a 64-bit Clang toolchain")
endif() endif()
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR
NOT CMAKE_SIZEOF_VOID_P EQUAL 8 OR if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$") set(MKW_PLATFORM_WINDOWS TRUE)
message(FATAL_ERROR "WiiCompiled requires 64-bit Clang targeting x86_64") 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() endif()
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release") if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
message(FATAL_ERROR "WiiCompiled only supports Release builds") message(FATAL_ERROR "WiiCompiled only supports Release builds")
endif() endif()
option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON)
# Preprocessor definitions that belong to this project's own code (the runtime, # 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 # the translated shards and the product glue) and to nothing else. They are
# applied directory-scoped, immediately after the aurora add_subdirectory() call, # applied directory-scoped, immediately after the aurora add_subdirectory() call,
@@ -49,17 +59,14 @@ target_include_directories(mkw_pugixml PUBLIC third_party/pugixml)
target_compile_features(mkw_pugixml PUBLIC cxx_std_17) target_compile_features(mkw_pugixml PUBLIC cxx_std_17)
set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF) set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF)
# Non-Windows guest-fiber scheduling (runtime/src/fiber_manager.cpp) needs a symmetric # 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 # 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 # directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
# asymmetric resume/yield coroutine libraries, which would need every call site restructured). # 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 # Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
# separately BSD-style licensed, see third_party/libco/LICENSE) - all of libco's non-Windows # separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers
# CPU-architecture backends are kept, even though libco.c's own preprocessor dispatch # and macOS uses the project's x18-safe AArch64 assembly backend, so this target is Linux-only.
# (__amd64__/__i386__/__arm__/__aarch64__/etc.) only ever selects amd64.c for this project's if(MKW_PLATFORM_LINUX)
# x86_64-only target (see the platform/arch check above). Windows keeps using native Fibers
# untouched, so this target is never built there.
if(NOT WIN32)
add_library(mkw_libco STATIC third_party/libco/libco.c) add_library(mkw_libco STATIC third_party/libco/libco.c)
add_library(mkw::libco ALIAS mkw_libco) add_library(mkw::libco ALIAS mkw_libco)
target_include_directories(mkw_libco PUBLIC third_party/libco) target_include_directories(mkw_libco PUBLIC third_party/libco)
@@ -130,31 +137,35 @@ else()
message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing") message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing")
endif() endif()
set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE) set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE)
if(WIN32) if(MKW_PLATFORM_WINDOWS)
set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE) 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) set(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "" FORCE)
set(DAWN_USE_WINDOWS_UI OFF CACHE BOOL "" FORCE) elseif(MKW_PLATFORM_MACOS)
else()
# Non-Windows (Linux): mirrors aurora-main's own
# _aurora_dawn_set_platform_backends() choice for this platform - Vulkan only, no
# D3D/HLSL. Kept in sync here because this project's own CMake FORCEs these cache
# variables before aurora-main's add_subdirectory() runs, which pre-empts aurora's
# auto-detection (CACHE ... INTERNAL "" without FORCE never overrides an existing value).
set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE) set(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) set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE)
endif() endif()
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE) set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE)
set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE)
# Provide a tiny stub for DXProgrammableCapture when the SDK/PIX headers are # Provide a tiny stub for DXProgrammableCapture when the SDK/PIX headers are
# missing (common on MinGW). Dawn only includes the header; no symbols are # missing (common on MinGW). Dawn only includes the header; no symbols are
# referenced when PIX isn't present. # referenced when PIX isn't present.
set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs") if(MKW_PLATFORM_WINDOWS)
if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h") set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs")
file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR}) if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h")
file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h" file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR})
"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX is absent.\n") 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() endif()
# Deliberately NOT injected project-wide. Only a from-source Dawn build ever # Deliberately NOT injected project-wide. Only a from-source Dawn build ever
# includes DXProgrammableCapture.h, and this tree consumes Dawn as a prebuilt # includes DXProgrammableCapture.h, and this tree consumes Dawn as a prebuilt
@@ -183,7 +194,9 @@ else()
if(TARGET ${t}) if(TARGET ${t})
set_target_properties(${t} PROPERTIES UNITY_BUILD OFF) set_target_properties(${t} PROPERTIES UNITY_BUILD OFF)
target_compile_options(${t} PRIVATE -O3 -ffast-math -w -pipe) 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; # Aurora's own sources include Windows headers and call std::min/max;
# they relied on the old project-wide NOMINMAX that no longer leaks # they relied on the old project-wide NOMINMAX that no longer leaks
# into this subtree, so the define is applied per target here. # into this subtree, so the define is applied per target here.
@@ -228,6 +241,18 @@ endif()
# a registration file is silently never compiled and never errors. The stale-glob # a registration file is silently never compiled and never errors. The stale-glob
# failure mode is worth far more than the milliseconds. # failure mode is worth far more than the milliseconds.
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp") 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_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") 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 # The host ISA guard is the one translation unit that must not receive the
@@ -235,25 +260,139 @@ 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. # 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") 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} 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)
add_executable(mkw_nand_save_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_save_tests.cpp")
target_include_directories(mkw_nand_save_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_save_tests COMMAND mkw_nand_save_tests)
add_executable(mkw_nand_settings_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_settings_tests.cpp")
find_package(Threads REQUIRED)
target_link_libraries(mkw_nand_settings_tests PRIVATE Threads::Threads)
target_include_directories(mkw_nand_settings_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
add_executable(mkw_sc_serial_tests "${CMAKE_CURRENT_LIST_DIR}/tests/sc_serial_tests.cpp")
target_include_directories(mkw_sc_serial_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_sc_serial_tests PRIVATE cxx_std_17)
add_test(NAME mkw_sc_serial_tests COMMAND mkw_sc_serial_tests)
# The input expression engine is self-contained, so it can be exercised without
# linking the runtime or SDL.
add_executable(mkw_input_expr_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/test_expr.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/input_expr.cpp")
target_include_directories(mkw_input_expr_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
# HostContext deliberately keeps the platform-specific context primitive out
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
# refactors cannot silently remove its headers, implementation, or link edge.
if(MKW_PLATFORM_LINUX)
add_executable(mkw_linux_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp")
target_include_directories(mkw_linux_host_context_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include"
"${CMAKE_CURRENT_LIST_DIR}/third_party/libco")
target_compile_features(mkw_linux_host_context_tests PRIVATE cxx_std_17)
target_link_libraries(mkw_linux_host_context_tests PRIVATE mkw::libco)
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
endif()
if(MKW_PLATFORM_MACOS)
# Exercise the Apple Silicon context ABI and the public host-memory
# contracts separately from translated products.
enable_language(ASM)
add_executable(mkw_macos_context_abi_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
add_executable(mkw_macos_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
add_executable(mkw_macos_guest_flat_memory_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_guest_flat_memory_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
target_include_directories(mkw_macos_guest_flat_memory_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_macos_guest_flat_memory_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests)
endif()
# The translator emits the complete, content-addressed source graph. Consuming # The translator emits the complete, content-addressed source graph. Consuming
# this one manifest keeps configure independent of the 28k generated function # this one manifest keeps configure independent of the 28k generated function
# files and of optional Retro Rewind artifacts such as code.map. # files and of optional Retro Rewind artifacts such as code.map.
set(MKW_TRANSLATED_SHARD_MANIFEST if(MKW_BUILD_PRODUCTS)
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake" set(MKW_TRANSLATED_SHARD_MANIFEST
CACHE FILEPATH "Translator-owned aggregate shard manifest") "${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}") CACHE FILEPATH "Translator-owned aggregate shard manifest")
message(FATAL_ERROR # The prebuilt export only needs the aurora/third-party closure configured above, so a
"Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. " # packaging machine without a translation stops here instead of failing.
"Run Translator.Cli emit-build-shards first; see translator/README.md.") if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
endif() message(STATUS "No translator shard manifest; configuring the native prebuilt export only")
include("${MKW_TRANSLATED_SHARD_MANIFEST}") return()
set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS}) endif()
message(STATUS if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
"Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; " message(FATAL_ERROR
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; " "Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. "
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions") "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}") set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake") 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() endif()
endforeach() 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 # Dawn is a prebuilt package on both sides; resolve the same install tree the
# package was built against so its imported target (and therefore # package was built against so its imported target (and therefore
# webgpu_dawn.dll / dxcompiler.dll / dxil.dll) is available exactly as a # webgpu_dawn.dll / dxcompiler.dll / dxil.dll) is available exactly as a
+50 -23
View File
@@ -25,6 +25,11 @@ if(EXISTS "${DATA_INIT_BLOB_ASM}")
endif() endif()
list(REMOVE_DUPLICATES SOURCES) 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) function(mkw_apply_common_compile_options target)
target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe) target_compile_options(${target} PRIVATE -O3 -ffast-math -w -pipe)
endfunction() endfunction()
@@ -76,11 +81,13 @@ target_compile_definitions(mkw_runtime_common PRIVATE
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION) _DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
target_link_libraries(mkw_runtime_common PRIVATE target_link_libraries(mkw_runtime_common PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx) 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 mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
if(WIN32) if(MKW_PLATFORM_WINDOWS)
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp) target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
else() elseif(MKW_PLATFORM_LINUX)
target_link_libraries(mkw_runtime_common PRIVATE mkw::libco) # ${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() endif()
if(MKW_CPPWINRT_INCLUDE_DIR) if(MKW_CPPWINRT_INCLUDE_DIR)
if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h") if(NOT EXISTS "${MKW_CPPWINRT_INCLUDE_DIR}/winrt/base.h")
@@ -125,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") target_precompile_headers(mkw_runtime_common PRIVATE "${MKW_RUNTIME_SOURCE_DIR}/include/mkw_pch.h")
mkw_apply_common_compile_options(mkw_runtime_common) mkw_apply_common_compile_options(mkw_runtime_common)
# Host ISA guard. Everything in MKW_ALL_BUILD_TARGETS below is compiled with # Host ISA guard. Windows and Linux x86_64 product targets use x86-64-v3, so
# -march=x86-64-v3; this object library deliberately is not, which # this object deliberately keeps the plain baseline ISA and checks the CPU
# is the whole point of keeping it out of mkw_runtime_common. It runs a CPUID # before any AVX2/FMA code can execute. AArch64 has no equivalent optional ISA
# check from a C initializer so an unsupported machine gets a readable error # floor to probe: NEON/FMA are architectural requirements.
# instead of an illegal-instruction crash. Excluded from the unity build and the if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
# precompiled header because both are produced with the owning target's flags. add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}")
add_library(mkw_cpu_baseline OBJECT "${MKW_CPU_BASELINE_SOURCE}") target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17)
target_compile_features(mkw_cpu_baseline PRIVATE cxx_std_17) set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF)
set_target_properties(mkw_cpu_baseline PROPERTIES UNITY_BUILD OFF) target_compile_options(mkw_cpu_baseline PRIVATE -w)
target_compile_options(mkw_cpu_baseline PRIVATE -w) endif()
if(NOT MKW_BASE_COMMON_SHARDS) if(NOT MKW_BASE_COMMON_SHARDS)
message(FATAL_ERROR "Translator build graph contains no shared base shards") message(FATAL_ERROR "Translator build graph contains no shared base shards")
@@ -174,7 +181,9 @@ function(mkw_configure_product target)
target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_runtime_common>) target_sources(${target} PRIVATE $<TARGET_OBJECTS:mkw_runtime_common>)
# Startup CPU check. Must stay a separate object library so it keeps the # 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. # 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 target_include_directories(${target} PRIVATE
"${MKW_RUNTIME_SOURCE_DIR}/include" "${MKW_RUNTIME_SOURCE_DIR}/include"
"${MKW_RUNTIME_SOURCE_DIR}/src" "${MKW_RUNTIME_SOURCE_DIR}/src"
@@ -190,10 +199,14 @@ function(mkw_configure_product target)
# include the same fat translated headers; bound them by the same pool. # include the same fat translated headers; bound them by the same pool.
mkw_bound_translated_compiles(${target}) mkw_bound_translated_compiles(${target})
target_link_libraries(${target} PRIVATE 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 target_link_libraries(${target} PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx) 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") if(EXISTS "${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake") include("${MKW_AURORA_DIR}/cmake/AuroraCopyRuntimeDLLs.cmake")
aurora_copy_runtime_dlls(${target}) aurora_copy_runtime_dlls(${target})
@@ -208,21 +221,23 @@ function(mkw_configure_product target)
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>) $<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
endif() endif()
if(WIN32) if(MKW_PLATFORM_WINDOWS)
target_link_libraries(${target} PRIVATE target_link_libraries(${target} PRIVATE
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp) dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE) set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
else() elseif(MKW_PLATFORM_LINUX)
# mkw_runtime_common is an OBJECT library: WiiCompiled/RetroRewind only pull in its .o # 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 # 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 # 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 # 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 # those objects, so the actual executable link needs mkw::libco directly, same as it
# needs it independently of that first `if(WIN32)` branch above. # needs it independently of that first `if(WIN32)` branch above. ${CMAKE_DL_LIBS} is
target_link_libraries(${target} PRIVATE mkw::libco) # 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() endif()
if(WIN32) if(MKW_PLATFORM_WINDOWS)
foreach(runtime_dll libc++.dll libunwind.dll) foreach(runtime_dll libc++.dll libunwind.dll)
execute_process( execute_process(
COMMAND "${CMAKE_CXX_COMPILER}" "--print-file-name=${runtime_dll}" COMMAND "${CMAKE_CXX_COMPILER}" "--print-file-name=${runtime_dll}"
@@ -298,11 +313,23 @@ else()
message(STATUS "RetroRewind target disabled (run translate-mod and emit-build-shards)") message(STATUS "RetroRewind target disabled (run translate-mod and emit-build-shards)")
endif() 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 set(MKW_ALL_BUILD_TARGETS
mkw_runtime_common mkw_base_shared mkw_base_sensitive mkw_retro_sensitive mkw_runtime_common mkw_base_shared mkw_base_sensitive mkw_retro_sensitive
mkw_retro_rewind_functions WiiCompiled RetroRewind) mkw_retro_rewind_functions WiiCompiled RetroRewind)
foreach(target IN LISTS MKW_ALL_BUILD_TARGETS) foreach(target IN LISTS MKW_ALL_BUILD_TARGETS)
if(TARGET ${target}) if(TARGET ${target} AND MKW_BASELINE_ARCH_FLAG)
target_compile_options(${target} PRIVATE -march=x86-64-v3) target_compile_options(${target} PRIVATE ${MKW_BASELINE_ARCH_FLAG})
endif() endif()
endforeach() endforeach()
+20 -89
View File
@@ -1,38 +1,28 @@
#pragma once #pragma once
#include "runtime_config.h" #include "nand_path.h"
#include "nand_settings.h"
#include <algorithm>
#include <array> #include <array>
#include <cctype>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <filesystem>
#include <fstream>
#include <iomanip> #include <iomanip>
#include <optional>
#include <random>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <string_view>
#include <utility> #include <utility>
namespace RuntimeConsoleIdentity { namespace RuntimeConsoleIdentity {
struct Identity { struct Identity {
std::string serial; std::string serial;
std::string productCode;
std::string area;
std::string gameRegion;
std::array<uint8_t, 6> mac; std::array<uint8_t, 6> mac;
}; };
inline bool IsValidSerial(const std::string& serial) {
return serial.size() == 9 &&
serial != "000000000" &&
std::all_of(serial.begin(), serial.end(),
[](unsigned char value) { return std::isdigit(value) != 0; });
}
inline Identity FromSerial(std::string serial) { inline Identity FromSerial(std::string serial) {
// Keep Nintendo's Wii OUI. The suffix is derived from the persisted serial // Keep Nintendo's Wii OUI. The suffix is derived from the NAND serial
// so every API exposes one coherent, stable virtual-console identity. // so every API exposes one coherent, stable virtual-console identity.
uint32_t hash = 2166136261u; uint32_t hash = 2166136261u;
for (const unsigned char value : serial) { for (const unsigned char value : serial) {
@@ -46,6 +36,7 @@ inline Identity FromSerial(std::string serial) {
return { return {
std::move(serial), std::move(serial),
{}, {}, {},
{ {
0x00, 0x00,
0x09, 0x09,
@@ -57,83 +48,23 @@ inline Identity FromSerial(std::string serial) {
}; };
} }
inline std::optional<std::string> ReadSerial(const std::filesystem::path& path) { inline Identity LoadFromNand() {
std::ifstream input(path); const auto root = RuntimeNandPath::DiscoverNandRootPath();
std::string line; const auto settings = RuntimeNandSettings::Read(root);
if (!input || !std::getline(input, line)) { if (!settings || !RuntimeNandSettings::HasIdentity(*settings)) {
return std::nullopt; RuntimeNandPath::FailNandRoot(
"NAND setting.txt is missing or has invalid console identity fields (SERNO, CODE, AREA, GAME)",
root / "title/00000001/00000002/data/setting.txt");
} }
constexpr std::string_view prefix = "serial="; Identity identity = FromSerial(settings->at("SERNO"));
if (line.rfind(prefix, 0) != 0) { identity.productCode = settings->at("CODE");
return std::nullopt; identity.area = settings->at("AREA");
} identity.gameRegion = settings->at("GAME");
std::string serial = line.substr(prefix.size()); return identity;
if (!IsValidSerial(serial)) {
return std::nullopt;
}
return serial;
}
inline bool WriteSerial(const std::filesystem::path& path, const std::string& serial) {
std::error_code ec;
std::filesystem::create_directories(path.parent_path(), ec);
if (ec) {
return false;
}
std::filesystem::path temporary = path;
temporary += ".tmp";
{
std::ofstream output(temporary, std::ios::trunc);
if (!output) {
return false;
}
output << "serial=" << serial << '\n';
output.close();
if (!output) {
return false;
}
}
std::filesystem::rename(temporary, path, ec);
if (!ec) {
return true;
}
std::filesystem::remove(temporary, ec);
return false;
}
inline std::string GenerateSerial() {
std::random_device entropy;
std::seed_seq seed{
entropy(),
entropy(),
entropy(),
entropy(),
};
std::mt19937 generator(seed);
std::uniform_int_distribution<uint32_t> distribution(100000000u, 999999999u);
return std::to_string(distribution(generator));
}
inline Identity LoadOrCreate(const std::filesystem::path& path) {
if (const auto serial = ReadSerial(path)) {
return FromSerial(*serial);
}
const std::string generated = GenerateSerial();
if (WriteSerial(path, generated)) {
return FromSerial(generated);
}
// Remain operational in a read-only environment. This fallback matches
// Dolphin's deterministic serial while keeping the same valid identity shape.
return FromSerial("123456789");
} }
inline const Identity& Current() { inline const Identity& Current() {
static const Identity identity = static const Identity identity = LoadFromNand();
LoadOrCreate(RuntimeConfigFile::ApplicationDataDirectory() / "ConsoleIdentity.txt");
return identity; return identity;
} }
+170
View File
@@ -0,0 +1,170 @@
#pragma once
// The single vocabulary shared by everything that has to turn a Config.toml
// controller name into a real button: the F10 settings bar, the macro engine,
// and the startup mapping pass. Keeping one table here means a name that the
// settings bar offers is always a name the config parser accepts, and vice
// versa; the two used to drift because each side carried its own copy.
#include <algorithm>
#include <array>
#include <cstdint>
#include <string>
#include <string_view>
#include <SDL3/SDL_gamepad.h>
#include <dolphin/pad.h>
namespace ControllerNames {
// A GameCube button as the game sees it, with the Config.toml key that selects
// it. Order matches RuntimeConfigFile::kControllerButtonKeys.
struct GameCubeButtonItem {
const char* configKey;
const char* label;
PADButton padButton;
};
inline constexpr std::array<GameCubeButtonItem, PAD_BUTTON_COUNT> kGameCubeButtons = {{
{"a", "A", PAD_BUTTON_A},
{"b", "B", PAD_BUTTON_B},
{"x", "X", PAD_BUTTON_X},
{"y", "Y", PAD_BUTTON_Y},
{"start", "Start", PAD_BUTTON_START},
{"z", "Z", PAD_TRIGGER_Z},
{"l", "L", PAD_TRIGGER_L},
{"r", "R", PAD_TRIGGER_R},
{"up", "D-pad Up", PAD_BUTTON_UP},
{"down", "D-pad Down", PAD_BUTTON_DOWN},
{"left", "D-pad Left", PAD_BUTTON_LEFT},
{"right", "D-pad Right", PAD_BUTTON_RIGHT},
}};
// A physical button on the host pad. Names are positional (south/east/...)
// rather than Xbox-labelled so one config reads the same on any hardware.
struct NativeButtonItem {
const char* configName;
const char* label;
uint32_t nativeButton;
};
inline constexpr auto kNativeButtons = std::to_array<NativeButtonItem>({
{"disabled", "Unmapped", PAD_NATIVE_BUTTON_DISABLED},
{"left_trigger", "Left trigger (LT / L2)", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFT_TRIGGER, false)},
{"right_trigger", "Right trigger (RT / R2)", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, false)},
{"left_stick_left", "Left stick left", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTX, true)},
{"left_stick_right", "Left stick right", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTX, false)},
{"left_stick_up", "Left stick up", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTY, true)},
{"left_stick_down", "Left stick down", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTY, false)},
{"right_stick_left", "Right stick left", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTX, true)},
{"right_stick_right", "Right stick right", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTX, false)},
{"right_stick_up", "Right stick up", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTY, true)},
{"right_stick_down", "Right stick down", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTY, false)},
{"unmapped", "Default", PAD_NATIVE_BUTTON_INVALID},
{"south", "South (A / Cross)", SDL_GAMEPAD_BUTTON_SOUTH},
{"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST},
{"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST},
{"north", "North (Y / Triangle)", SDL_GAMEPAD_BUTTON_NORTH},
{"back", "Back / Select / Create", SDL_GAMEPAD_BUTTON_BACK},
{"guide", "Guide / Home / PS", SDL_GAMEPAD_BUTTON_GUIDE},
{"start", "Start / Options", SDL_GAMEPAD_BUTTON_START},
{"left_stick", "Left stick click (L3)", SDL_GAMEPAD_BUTTON_LEFT_STICK},
{"right_stick", "Right stick click (R3)", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
{"left_shoulder", "Left bumper (LB / L1)", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
{"right_shoulder", "Right bumper (RB / R1)", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
{"dpad_up", "D-pad Up", SDL_GAMEPAD_BUTTON_DPAD_UP},
{"dpad_down", "D-pad Down", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
{"dpad_left", "D-pad Left", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
{"dpad_right", "D-pad Right", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
{"misc1", "Misc 1 / Share / Mic", SDL_GAMEPAD_BUTTON_MISC1},
{"right_paddle1", "Right paddle 1", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
{"left_paddle1", "Left paddle 1", SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
{"right_paddle2", "Right paddle 2", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
{"left_paddle2", "Left paddle 2", SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
{"touchpad", "Touchpad click", SDL_GAMEPAD_BUTTON_TOUCHPAD},
{"misc2", "Misc 2", SDL_GAMEPAD_BUTTON_MISC2},
{"misc3", "Misc 3 / GC L click", SDL_GAMEPAD_BUTTON_MISC3},
{"misc4", "Misc 4 / GC R click", SDL_GAMEPAD_BUTTON_MISC4},
{"misc5", "Misc 5", SDL_GAMEPAD_BUTTON_MISC5},
{"misc6", "Misc 6", SDL_GAMEPAD_BUTTON_MISC6},
});
inline std::string TrimToken(std::string_view token) {
const size_t begin = token.find_first_not_of(" \t");
if (begin == std::string_view::npos) {
return {};
}
const size_t end = token.find_last_not_of(" \t");
return std::string(token.substr(begin, end - begin + 1));
}
inline const NativeButtonItem* FindNativeButton(std::string_view configName) {
const std::string name = TrimToken(configName.substr(0, configName.find('@')));
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
[&](const NativeButtonItem& item) { return name == item.configName; });
return it == kNativeButtons.end() ? nullptr : &*it;
}
// Falls back to the "unmapped" entry so callers always have a label to draw.
inline const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) {
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
[&](const NativeButtonItem& item) { return PADAxisButtonIdentity(nativeButton) == PADAxisButtonIdentity(item.nativeButton); });
return it == kNativeButtons.end() ? *FindNativeButton("unmapped") : *it;
}
inline const GameCubeButtonItem* FindGameCubeButton(std::string_view configKey) {
const std::string key = TrimToken(configKey);
const auto it = std::find_if(kGameCubeButtons.begin(), kGameCubeButtons.end(),
[&](const GameCubeButtonItem& item) { return key == item.configKey; });
return it == kGameCubeButtons.end() ? nullptr : &*it;
}
// "up" or "up,a" -> the OR of those GC button bits. Unknown names are skipped so
// a typo costs one button instead of the whole macro.
inline uint16_t GameCubeMaskFromKeys(std::string_view keys) {
uint16_t mask = 0;
size_t begin = 0;
while (begin <= keys.size()) {
const size_t comma = keys.find(',', begin);
const std::string_view token =
keys.substr(begin, comma == std::string_view::npos ? std::string_view::npos : comma - begin);
if (const GameCubeButtonItem* item = FindGameCubeButton(token)) {
mask |= static_cast<uint16_t>(item->padButton);
}
if (comma == std::string_view::npos) {
break;
}
begin = comma + 1;
}
return mask;
}
inline std::string GameCubeKeysFromMask(uint16_t mask) {
std::string keys;
for (const auto& item : kGameCubeButtons) {
if ((mask & static_cast<uint16_t>(item.padButton)) == 0) {
continue;
}
if (!keys.empty()) {
keys += ',';
}
keys += item.configKey;
}
return keys;
}
inline std::string GameCubeLabelsFromMask(uint16_t mask) {
std::string labels;
for (const auto& item : kGameCubeButtons) {
if ((mask & static_cast<uint16_t>(item.padButton)) == 0) {
continue;
}
if (!labels.empty()) {
labels += " + ";
}
labels += item.label;
}
return labels.empty() ? std::string("None") : labels;
}
} // namespace ControllerNames
+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
+5 -10
View File
@@ -6,6 +6,7 @@
#include <mutex> #include <mutex>
#include <thread> #include <thread>
#include <unordered_map> #include <unordered_map>
#include <vector>
#if defined(_WIN32) #if defined(_WIN32)
#ifndef NOMINMAX #ifndef NOMINMAX
@@ -22,7 +23,7 @@
// Forward declarations // Forward declarations
struct CpuContext; 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 // 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 // threads only switch at explicit yield points (OSSleepThread, OSYieldThread, ...), matching
// Wii cooperative semantics exactly. // Wii cooperative semantics exactly.
@@ -39,7 +40,7 @@ enum class ThreadState : uint32_t {
// Information about a guest fiber // Information about a guest fiber
struct GuestFiber { struct GuestFiber {
void* fiber = nullptr; // Windows fiber handle void* fiber = nullptr; // Host context handle
uint32_t entryPoint = 0; // Thread entry function uint32_t entryPoint = 0; // Thread entry function
uint32_t entryArg = 0; // Argument to entry function uint32_t entryArg = 0; // Argument to entry function
CpuContext cpuContext{}; // Saved CPU context for this fiber CpuContext cpuContext{}; // Saved CPU context for this fiber
@@ -102,10 +103,6 @@ private:
static void CALLBACK FiberProc(void* param); static void CALLBACK FiberProc(void* param);
#else #else
static void FiberProc(void* param); static void FiberProc(void* param);
// libco's co_create() entry points take no argument (unlike CreateFiber's FiberProc(void*)),
// so this trampoline reads the guest thread address staged by CreateGuestFiber() and forwards
// into the (platform-neutral-bodied) FiberProc above. See fiber_manager.cpp.
static void FiberProcTrampoline();
#endif #endif
// Switch from whichever fiber is currently active straight to the scheduler fiber, without // Switch from whichever fiber is currently active straight to the scheduler fiber, without
// the SwitchToThread bookkeeping (CPU context save/restore, s_currentGuestThread). Used for // the SwitchToThread bookkeeping (CPU context save/restore, s_currentGuestThread). Used for
@@ -117,9 +114,8 @@ private:
static std::mutex s_mutex; static std::mutex s_mutex;
static std::unordered_map<uint32_t, GuestFiber> s_fibers; static std::unordered_map<uint32_t, GuestFiber> s_fibers;
static std::vector<void*> s_fibersPendingDelete; static std::vector<void*> s_fibersPendingDelete;
// The scheduler's own "fiber": a Windows HFIBER, or (non-Windows) libco's cothread_t for // The scheduler's own host context. Its opaque handle is supplied by the
// whichever native call stack first called GuestFiberManager::Initialize() - both are // active HostContext backend, so one field serves every supported host.
// plain void* handles, so one field serves both platforms.
static void* s_schedulerFiber; static void* s_schedulerFiber;
static uint32_t s_currentGuestThread; static uint32_t s_currentGuestThread;
static bool s_initialized; static bool s_initialized;
@@ -135,4 +131,3 @@ private:
extern std::atomic<uint32_t> g_viRetracePendingCount; extern std::atomic<uint32_t> g_viRetracePendingCount;
} // namespace Fiber } // namespace Fiber
+44 -2
View File
@@ -12,10 +12,33 @@
namespace GuestFlat { namespace GuestFlat {
// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load // 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 // of a global.
// usual image/heap placement.
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull; 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; 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)) #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. // True once the reservation exists and translated code may use the flat path.
bool IsActive(); 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 // 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 // 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 // reservation, the section objects or a view cannot be created - a silent
+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
+67
View File
@@ -0,0 +1,67 @@
#pragma once
// Per-port expression bindings for the GameCube controls, plus import of a
// Dolphin GCPadNew.ini.
#include <array>
#include <cstdint>
#include <string>
#include <dolphin/pad.h>
namespace InputBindings {
// The controls an expression can drive, in Dolphin's own naming so an
// imported config maps across without translation.
struct ControlInfo {
const char* dolphinName;
const char* label;
uint16_t padButton; // 0 for the analog-only controls below
int analog; // 0 none, 1 trigger L, 2 trigger R
};
inline constexpr std::array<ControlInfo, 14> kControls = {{
{"Buttons/A", "A", PAD_BUTTON_A, 0},
{"Buttons/B", "B", PAD_BUTTON_B, 0},
{"Buttons/X", "X", PAD_BUTTON_X, 0},
{"Buttons/Y", "Y", PAD_BUTTON_Y, 0},
{"Buttons/Z", "Z", PAD_TRIGGER_Z, 0},
{"Buttons/Start", "Start", PAD_BUTTON_START, 0},
{"D-Pad/Up", "D-pad Up", PAD_BUTTON_UP, 0},
{"D-Pad/Down", "D-pad Down", PAD_BUTTON_DOWN, 0},
{"D-Pad/Left", "D-pad Left", PAD_BUTTON_LEFT, 0},
{"D-Pad/Right", "D-pad Right", PAD_BUTTON_RIGHT, 0},
{"Triggers/L", "L", PAD_TRIGGER_L, 1},
{"Triggers/R", "R", PAD_TRIGGER_R, 2},
{"Triggers/L-Analog", "L analog", 0, 1},
{"Triggers/R-Analog", "R analog", 0, 2},
}};
void Reload() noexcept;
// The pad library has PADBlockInput but no matching query, so the settings
// overlay reports its own state here.
void SetInputBlocked(bool blocked) noexcept;
bool InputBlocked() noexcept;
// Mix expression output into a freshly read status set. Call once per guest
// PADRead, after every other input source has been merged.
void Apply(PADStatus* statuses) noexcept;
std::string GetExpression(uint32_t port, size_t control) noexcept;
// Returns false and fills error if the text does not parse; the binding is
// left unchanged in that case.
bool SetExpression(uint32_t port, size_t control, const std::string& text, std::string& error) noexcept;
// True while the control's expression is above the press threshold.
bool IsActive(uint32_t port, size_t control) noexcept;
// The default Dolphin config location on Windows, then next to the executable.
std::string DefaultDolphinConfigPath() noexcept;
// Imports [GCPad<padIndex>] into the given port. Returns the number of controls
// imported, or -1 on failure with error filled.
int ImportDolphinConfig(const std::string& path, int padIndex, uint32_t port,
std::string& summary, std::string& error) noexcept;
} // namespace InputBindings
+53
View File
@@ -0,0 +1,53 @@
#pragma once
// Dolphin-compatible input expressions.
//
// Values are doubles in Dolphin's ControlState style; a control counts as
// pressed above kConditionThreshold. Timing matches Dolphin: wall-clock
// seconds on a steady clock, so an expression copied from GCPadNew.ini
// behaves the same here as it does there.
#include <filesystem>
#include <functional>
#include <memory>
#include <string>
#include <vector>
namespace InputExpr {
inline constexpr double kConditionThreshold = 0.5;
// Resolves a backtick-quoted input name to its current value.
using InputSource = std::function<double(const std::string&)>;
struct Node;
class Expression {
public:
Expression();
~Expression();
Expression(Expression&&) noexcept;
Expression& operator=(Expression&&) noexcept;
// Returns false and fills error on a syntax problem.
static bool Parse(const std::string& text, Expression& out, std::string& error);
bool Empty() const { return m_root == nullptr; }
double Evaluate(const InputSource& source) const;
// Input names the expression references, for diagnostics.
std::vector<std::string> ReferencedInputs() const;
private:
std::unique_ptr<Node> m_root;
};
// Parses a Dolphin GCPadNew.ini and returns the expression text for each
// control of the requested pad, keyed by Dolphin's own control names
// ("Buttons/A", "D-Pad/Up", "Triggers/L", ...). Returns false if the file
// cannot be read or the section is missing.
bool ReadDolphinConfig(const std::filesystem::path& path, int padIndex,
std::vector<std::pair<std::string, std::string>>& controls,
std::string& deviceName, std::string& error);
} // namespace InputExpr
+6
View File
@@ -4,7 +4,13 @@
#include <cstdint> #include <cstdint>
#define MKW_RESTRICT __restrict #define MKW_RESTRICT __restrict
#if defined(__x86_64__)
#include <immintrin.h> #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 inline constexpr bool MkwStateFreeAbiEnabled(uint32_t) noexcept
{ {
+1 -1
View File
@@ -62,7 +62,7 @@ public:
{ {
g_currentCpuContext = ctx; g_currentCpuContext = ctx;
savedMxcsr_ = _mm_getcsr(); savedMxcsr_ = MkwGetHostFpControl();
if (ctx != nullptr) if (ctx != nullptr)
MkwApplyHostNiMode(ctx->fpscr); MkwApplyHostNiMode(ctx->fpscr);
} }
+229 -31
View File
@@ -78,37 +78,71 @@ inline void PpcSetPairedFprInline(PPC_FPR& fpr, double packed)
fpr.d = 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 // 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. // local already lives in a vector register, so these casts compile to nothing.
inline __m128 PpcPsToM128Inline(double value) inline PpcPairVec PpcPsToM128Inline(double value)
{ {
#if defined(__x86_64__)
return _mm_castpd_ps(_mm_set_sd(value)); 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)); 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); const __m128 lanes = PpcPsToM128Inline(value);
return _mm_shuffle_ps(lanes, lanes, _MM_SHUFFLE(1, 1, 1, 1)); 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); const __m128 lanes = PpcPsToM128Inline(value);
return _mm_shuffle_ps(lanes, lanes, _MM_SHUFFLE(0, 0, 0, 0)); 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 signMask = _mm_castsi128_ps(_mm_set1_epi32(static_cast<int>(0x80000000u)));
const __m128 negated = _mm_xor_ps(value, signMask); const __m128 negated = _mm_xor_ps(value, signMask);
const __m128 ordered = _mm_cmpord_ps(value, value); const __m128 ordered = _mm_cmpord_ps(value, value);
return _mm_or_ps(_mm_and_ps(ordered, negated), _mm_andnot_ps(ordered, 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 // 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) inline float PpcGetPs0Inline(double value)
{ {
// ps0 lives in lane 1; PpcBroadcastPs0Inline already splats it. // ps0 lives in lane 1; PpcBroadcastPs0Inline already splats it.
#if defined(__x86_64__)
return _mm_cvtss_f32(PpcBroadcastPs0Inline(value)); return _mm_cvtss_f32(PpcBroadcastPs0Inline(value));
#elif defined(__aarch64__)
return vget_lane_f32(PpcBroadcastPs0Inline(value), 0);
#endif
} }
inline float PpcGetPs1Inline(double value) inline float PpcGetPs1Inline(double value)
{ {
// ps1 is already lane 0 of the packed representation. // ps1 is already lane 0 of the packed representation.
#if defined(__x86_64__)
return _mm_cvtss_f32(PpcPsToM128Inline(value)); return _mm_cvtss_f32(PpcPsToM128Inline(value));
#elif defined(__aarch64__)
return vget_lane_f32(PpcPsToM128Inline(value), 0);
#endif
} }
inline double PpcPackPairedInline(float ps0, float ps1) 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 // _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. // 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))); 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. // 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 // 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 // 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 // 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 // passes the value to the double->float conversion. DAZ/FZ (set exactly when NI is) makes
// a subnormal as zero, matching the mask's answer. // 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 v = _mm_set_sd(value);
const __m128d signMask = _mm_set_sd(-0.0); const __m128d signMask = _mm_set_sd(-0.0);
const __m128d magnitude = _mm_andnot_pd(signMask, v); const __m128d magnitude = _mm_andnot_pd(signMask, v);
const __m128d flush = _mm_cmplt_sd(magnitude, _mm_set_sd(g_mkwNiFlushThreshold)); const __m128d flush = _mm_cmplt_sd(magnitude, _mm_set_sd(g_mkwNiFlushThreshold));
const __m128d kept = _mm_andnot_pd(_mm_andnot_pd(signMask, flush), v); const __m128d kept = _mm_andnot_pd(_mm_andnot_pd(signMask, flush), v);
return static_cast<float>(_mm_cvtsd_f64(kept)); 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) 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); 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) inline double PPC_PsMulInline(double lhs, double rhs)
{ {
return PpcFlushPairedForNiInline( return PpcFlushPairedForNiInline(
PpcM128ToPsInline(_mm_mul_ps(PpcPsToM128Inline(lhs), PpcPsToM128Inline(rhs)))); PpcM128ToPsInline(PpcMulPairInline(PpcPsToM128Inline(lhs), PpcPsToM128Inline(rhs))));
} }
inline double PPC_PsMulNoNiInline(double lhs, double 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 // 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 // PpcNegateNonNanLanesInline. NI flushing is handled by MXCSR (see
// MkwApplyHostNiMode), so the NI and NoNi entry points are identical here. // 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) inline double PPC_PsMsubInline(double multiplicand, double multiplier, double subtractor)
{ {
return PpcM128ToPsInline(_mm_fmsub_ps( return PpcM128ToPsInline(PpcFmsubPairInline(
PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(subtractor))); 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) inline double PPC_PsMaddInline(double multiplicand, double multiplier, double addend)
{ {
return PpcM128ToPsInline(_mm_fmadd_ps( return PpcM128ToPsInline(PpcFmaddPairInline(
PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(addend))); 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) inline double PPC_PsMadds0Inline(double multiplicand, double multiplier, double addend)
{ {
return PpcM128ToPsInline(_mm_fmadd_ps( return PpcM128ToPsInline(PpcFmaddPairInline(
PpcPsToM128Inline(multiplicand), PpcBroadcastPs0Inline(multiplier), PpcPsToM128Inline(addend))); PpcPsToM128Inline(multiplicand), PpcBroadcastPs0Inline(multiplier), PpcPsToM128Inline(addend)));
} }
inline double PPC_PsMadds1Inline(double multiplicand, double multiplier, double 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))); PpcPsToM128Inline(multiplicand), PpcBroadcastPs1Inline(multiplier), PpcPsToM128Inline(addend)));
} }
inline double PPC_PsNmsubInline(double multiplicand, double multiplier, double subtractor) 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)))); 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) 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)))); PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(addend))));
} }
inline double PPC_PsMuls0Inline(double aValue, double cValue) inline double PPC_PsMuls0Inline(double aValue, double cValue)
{ {
return PpcFlushPairedForNiInline(PpcM128ToPsInline( return PpcFlushPairedForNiInline(PpcM128ToPsInline(
_mm_mul_ps(PpcPsToM128Inline(aValue), PpcBroadcastPs0Inline(cValue)))); PpcMulPairInline(PpcPsToM128Inline(aValue), PpcBroadcastPs0Inline(cValue))));
} }
inline double PPC_PsMuls1Inline(double aValue, double cValue) inline double PPC_PsMuls1Inline(double aValue, double cValue)
{ {
return PpcFlushPairedForNiInline(PpcM128ToPsInline( return PpcFlushPairedForNiInline(PpcM128ToPsInline(
_mm_mul_ps(PpcPsToM128Inline(aValue), PpcBroadcastPs1Inline(cValue)))); PpcMulPairInline(PpcPsToM128Inline(aValue), PpcBroadcastPs1Inline(cValue))));
} }
inline PPC_FPR PpcMakePairedResultInline(float ps0, float ps1); inline PPC_FPR PpcMakePairedResultInline(float ps0, float ps1);
@@ -571,50 +714,105 @@ inline double PPC_PsToScalarInline(double value)
return static_cast<double>(PpcGetPs0Inline(value)); return static_cast<double>(PpcGetPs0Inline(value));
} }
// ps_merge* are pure lane selections (result.ps0 from frA, result.ps1 from frB); with lane 0 // ps_merge* are pure lane selections (result.ps0 from frA, result.ps1 from frB). On x86 two
// == ps1 and lane 1 == ps0, two shuffles build the result bit-exact instead of round-tripping // shuffles build the result bit-exact without round-tripping through the pack helper; NEON has
// through the pack helper. // 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) 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( const __m128 gathered = _mm_shuffle_ps(
PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(1, 1, 1, 1)); PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(1, 1, 1, 1));
return PpcM128ToPsInline(_mm_shuffle_ps(gathered, gathered, _MM_SHUFFLE(0, 0, 2, 0))); 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) 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( const __m128 gathered = _mm_shuffle_ps(
PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(1, 1, 0, 0)); PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(1, 1, 0, 0));
return PpcM128ToPsInline(_mm_shuffle_ps(gathered, gathered, _MM_SHUFFLE(0, 0, 2, 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) 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( const __m128 gathered = _mm_shuffle_ps(
PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(0, 0, 1, 1)); PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(0, 0, 1, 1));
return PpcM128ToPsInline(_mm_shuffle_ps(gathered, gathered, _MM_SHUFFLE(0, 0, 2, 0))); 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) 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( return PpcM128ToPsInline(
_mm_unpacklo_ps(PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue))); _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) inline double PPC_PsAddInline(double aValue, double bValue)
{ {
return PpcFlushPairedForNiInline( return PpcFlushPairedForNiInline(
PpcM128ToPsInline(_mm_add_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)))); PpcM128ToPsInline(PpcAddPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
} }
inline double PPC_PsAddNoNiInline(double aValue, double bValue) inline double PPC_PsAddNoNiInline(double aValue, double bValue)
{ {
return PpcM128ToPsInline( return PpcM128ToPsInline(
_mm_add_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))); PpcAddPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)));
} }
inline double PPC_PsSelInline(double lhsValue, double controlValue, double rhsValue) 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) inline double PPC_PsSubInline(double aValue, double bValue)
{ {
return PpcFlushPairedForNiInline( return PpcFlushPairedForNiInline(
PpcM128ToPsInline(_mm_sub_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)))); PpcM128ToPsInline(PpcSubPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
} }
inline double PPC_PsSubNoNiInline(double aValue, double bValue) inline double PPC_PsSubNoNiInline(double aValue, double bValue)
{ {
return PpcM128ToPsInline( return PpcM128ToPsInline(
_mm_sub_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))); PpcSubPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)));
} }
inline double PPC_PsDivInline(double aValue, double bValue) inline double PPC_PsDivInline(double aValue, double bValue)
{ {
return PpcFlushPairedForNiInline( return PpcFlushPairedForNiInline(
PpcM128ToPsInline(_mm_div_ps(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)))); PpcM128ToPsInline(PpcDivPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))));
} }
inline double PPC_PsNegInline(double value) inline double PPC_PsNegInline(double value)
+59 -20
View File
@@ -1,18 +1,29 @@
#pragma once #pragma once
// FPSCR[NI] (non-IEEE flush-to-zero) modeled on the host FP environment, plus // FPSCR[NI] (non-IEEE flush-to-zero) modeled on the host FP environment, plus
// the thread-local mirror of that state the hot paths read instead of MXCSR. // the thread-local mirror of that state the hot paths read instead of MXCSR/FPCR.
#include "ppc_isa_config.h" #include "ppc_isa_config.h"
#include <cstdint> #include <cstdint>
// Software-flushing Gekko's single-precision denormals per op roughly doubled the THP IDCT // Software-flushing Gekko's single-precision denormals per op roughly doubled the THP IDCT
// kernel's cycle count, so instead the runtime mirrors guest FPSCR[NI] into host MXCSR FTZ+DAZ // kernel's cycle count, so instead the runtime mirrors guest FPSCR[NI] into the host FP control
// wherever FPSCR can change (PPC_Mtfs*, fiber context switches, CpuContextScope), making per-op // register's flush-to-zero bit(s) wherever FPSCR can change (PPC_Mtfs*, fiber context switches,
// flushes free. Accepted deviations (same trade Dolphin makes): FTZ also flushes double // CpuContextScope), making per-op flushes free. Accepted deviations (same trade Dolphin makes):
// denormals unlike real NI, and a pre-round-flush edge near FLT_MIN rounds via cvtsd2ss instead. // flush-to-zero also flushes double denormals unlike real NI, and a pre-round-flush edge near
inline constexpr uint32_t kMkwMxcsrFlushToZeroBits = (1u << 15) | (1u << 6); // FTZ | DAZ // FLT_MIN rounds via cvtsd2ss (or its AArch64 equivalent) instead.
#if defined(__x86_64__)
// MXCSR: FTZ (bit 15, flushes arithmetic *output* denormals) and DAZ (bit 6, treats denormal
// *inputs* as zero) are two separate bits on x86; both are set/cleared together here.
inline constexpr uint32_t kMkwFpControlFlushToZeroBits = (1u << 15) | (1u << 6); // FTZ | DAZ
#elif defined(__aarch64__)
// FPCR: a single FZ bit (bit 24) flushes both single- and double-precision denormals, both as
// inputs and outputs - actually a cleaner match to the "flush everything" trade above than x86's
// two-bit combo, not an extra deviation.
inline constexpr uint32_t kMkwFpControlFlushToZeroBits = (1u << 24); // FZ
#else
#error "ppc_isa_fpenv.h has no host FP control register mapping for this architecture"
#endif
inline thread_local bool g_mkwHostNiActive = false; inline thread_local bool g_mkwHostNiActive = false;
@@ -22,32 +33,60 @@ inline thread_local bool g_mkwHostNiActive = false;
inline constexpr double kMkwNiFlushThreshold = 0x1p-126; // 0x3810000000000000 inline constexpr double kMkwNiFlushThreshold = 0x1p-126; // 0x3810000000000000
inline thread_local double g_mkwNiFlushThreshold = 0.0; inline thread_local double g_mkwNiFlushThreshold = 0.0;
// Host FP control register access (MXCSR on x86_64, FPCR on AArch64), abstracted so
// MkwApplyHostNiMode/MkwRestoreHostMxcsr and CpuContextScope (ppc_isa_context.h) don't each need
// their own per-arch branch.
inline uint32_t MkwGetHostFpControl() noexcept
{
#if defined(__x86_64__)
return _mm_getcsr();
#elif defined(__aarch64__)
uint64_t fpcr = 0;
__asm__ __volatile__("mrs %0, fpcr" : "=r"(fpcr));
return static_cast<uint32_t>(fpcr);
#endif
}
inline void MkwSetHostFpControl(uint32_t value) noexcept
{
#if defined(__x86_64__)
_mm_setcsr(value);
#elif defined(__aarch64__)
// Read-modify-write the full 64-bit FPCR: only the low 32 bits are architecturally defined
// today, but a bare 32-bit write would zero the (reserved) upper half of the real register.
uint64_t fpcr = 0;
__asm__ __volatile__("mrs %0, fpcr" : "=r"(fpcr));
fpcr = (fpcr & ~static_cast<uint64_t>(0xFFFFFFFFu)) | value;
__asm__ __volatile__("msr fpcr, %0" :: "r"(fpcr));
#endif
}
inline void MkwApplyHostNiMode(uint32_t fpscr) noexcept inline void MkwApplyHostNiMode(uint32_t fpscr) noexcept
{ {
const uint32_t csr = _mm_getcsr(); const uint32_t csr = MkwGetHostFpControl();
const bool wantNi = (fpscr & 0x4u) != 0; const bool wantNi = (fpscr & 0x4u) != 0;
const uint32_t want = wantNi const uint32_t want = wantNi
? (csr | kMkwMxcsrFlushToZeroBits) ? (csr | kMkwFpControlFlushToZeroBits)
: (csr & ~kMkwMxcsrFlushToZeroBits); : (csr & ~kMkwFpControlFlushToZeroBits);
if (want != csr) if (want != csr)
_mm_setcsr(want); MkwSetHostFpControl(want);
// `want` has both bits set or both clear, so this is exactly // `want` has every flush-to-zero bit set or every one clear, so this is exactly
// `(_mm_getcsr() & kMkwMxcsrFlushToZeroBits) != 0` after the write - the // `(MkwGetHostFpControl() & kMkwFpControlFlushToZeroBits) != 0` after the write - the
// mirror cannot disagree with the register even if the incoming CSR held // mirror cannot disagree with the register even if the incoming value held only some of
// only one of the two bits. // those bits.
g_mkwHostNiActive = wantNi; g_mkwHostNiActive = wantNi;
g_mkwNiFlushThreshold = wantNi ? kMkwNiFlushThreshold : 0.0; g_mkwNiFlushThreshold = wantNi ? kMkwNiFlushThreshold : 0.0;
} }
/// <summary> /// <summary>
/// Restores a previously captured MXCSR value and re-derives the mirror from /// Restores a previously captured MXCSR/FPCR value and re-derives the mirror from it. Every raw
/// it. Every raw restore has to go through here; a bare _mm_setcsr would leave /// restore has to go through here; a bare MkwSetHostFpControl would leave the mirror describing
/// the mirror describing the FP environment that was just replaced. /// the FP environment that was just replaced.
/// </summary> /// </summary>
inline void MkwRestoreHostMxcsr(uint32_t csr) noexcept inline void MkwRestoreHostMxcsr(uint32_t csr) noexcept
{ {
_mm_setcsr(csr); MkwSetHostFpControl(csr);
const bool niActive = (csr & kMkwMxcsrFlushToZeroBits) != 0; const bool niActive = (csr & kMkwFpControlFlushToZeroBits) != 0;
g_mkwHostNiActive = niActive; g_mkwHostNiActive = niActive;
g_mkwNiFlushThreshold = niActive ? kMkwNiFlushThreshold : 0.0; g_mkwNiFlushThreshold = niActive ? kMkwNiFlushThreshold : 0.0;
} }
+56
View File
@@ -12,6 +12,7 @@
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <cstdlib> #include <cstdlib>
#include <cstring>
#include <limits> #include <limits>
#include <type_traits> #include <type_traits>
#include <utility> #include <utility>
@@ -38,16 +39,26 @@ inline uint32_t PpcStorePsqFloatBitsInline(uint32_t value)
inline uint64_t PpcLoadPairPsqFloatBitsPackedInline(uint64_t value) inline uint64_t PpcLoadPairPsqFloatBitsPackedInline(uint64_t value)
{ {
#if defined(__x86_64__)
const __m128i lanes = _mm_cvtsi64_si128(static_cast<long long>(value)); const __m128i lanes = _mm_cvtsi64_si128(static_cast<long long>(value));
const __m128i magnitude = _mm_and_si128(lanes, _mm_set1_epi32(0x7FFFFFFF)); const __m128i magnitude = _mm_and_si128(lanes, _mm_set1_epi32(0x7FFFFFFF));
const __m128i nanMask = _mm_cmpgt_epi32(magnitude, _mm_set1_epi32(0x7F800000)); const __m128i nanMask = _mm_cmpgt_epi32(magnitude, _mm_set1_epi32(0x7F800000));
const __m128i result = _mm_or_si128( const __m128i result = _mm_or_si128(
lanes, _mm_and_si128(nanMask, _mm_set1_epi32(0x00400000))); lanes, _mm_and_si128(nanMask, _mm_set1_epi32(0x00400000)));
return static_cast<uint64_t>(_mm_cvtsi128_si64(result)); return static_cast<uint64_t>(_mm_cvtsi128_si64(result));
#elif defined(__aarch64__)
// Equivalent to applying PpcLoadPsqFloatBitsInline to each 32-bit lane: the
// x86 body above only ever acts on these same two lanes (the upper 64 bits
// _mm_cvtsi64_si128 zero-fills never survive the final truncating extract).
const uint32_t lo = PpcLoadPsqFloatBitsInline(static_cast<uint32_t>(value));
const uint32_t hi = PpcLoadPsqFloatBitsInline(static_cast<uint32_t>(value >> 32));
return (static_cast<uint64_t>(hi) << 32) | lo;
#endif
} }
inline uint64_t PpcStorePairPsqFloatBitsPackedInline(uint64_t value) inline uint64_t PpcStorePairPsqFloatBitsPackedInline(uint64_t value)
{ {
#if defined(__x86_64__)
const __m128i lanes = _mm_cvtsi64_si128(static_cast<long long>(value)); const __m128i lanes = _mm_cvtsi64_si128(static_cast<long long>(value));
const __m128i magnitude = _mm_and_si128(lanes, _mm_set1_epi32(0x7FFFFFFF)); const __m128i magnitude = _mm_and_si128(lanes, _mm_set1_epi32(0x7FFFFFFF));
const __m128i subnormalMask = _mm_cmplt_epi32(magnitude, _mm_set1_epi32(0x00800000)); const __m128i subnormalMask = _mm_cmplt_epi32(magnitude, _mm_set1_epi32(0x00800000));
@@ -60,8 +71,16 @@ inline uint64_t PpcStorePairPsqFloatBitsPackedInline(uint64_t value)
_mm_and_si128(subnormalMask, signedZero), _mm_and_si128(subnormalMask, signedZero),
_mm_andnot_si128(subnormalMask, quieted)); _mm_andnot_si128(subnormalMask, quieted));
return static_cast<uint64_t>(_mm_cvtsi128_si64(result)); return static_cast<uint64_t>(_mm_cvtsi128_si64(result));
#elif defined(__aarch64__)
// Equivalent to applying PpcStorePsqFloatBitsInline to each 32-bit lane;
// same reasoning as the load-side port above.
const uint32_t lo = PpcStorePsqFloatBitsInline(static_cast<uint32_t>(value));
const uint32_t hi = PpcStorePsqFloatBitsInline(static_cast<uint32_t>(value >> 32));
return (static_cast<uint64_t>(hi) << 32) | lo;
#endif
} }
#if defined(__x86_64__)
inline __m128i PpcPsqSwapPairBytesInline(__m128i lanes) inline __m128i PpcPsqSwapPairBytesInline(__m128i lanes)
{ {
const __m128i order = _mm_setr_epi8( const __m128i order = _mm_setr_epi8(
@@ -95,21 +114,42 @@ inline __m128i PpcStorePairPsqFloatBitsLanesInline(__m128i lanes)
_mm_and_si128(subnormalMask, signedZero), _mm_and_si128(subnormalMask, signedZero),
_mm_andnot_si128(subnormalMask, quieted)); _mm_andnot_si128(subnormalMask, quieted));
} }
#endif // defined(__x86_64__)
// host -> packed FPR double, guard already proven by the caller. // host -> packed FPR double, guard already proven by the caller.
inline double PpcLoadPairPsqFloatFromHostInline(const uint8_t* host) inline double PpcLoadPairPsqFloatFromHostInline(const uint8_t* host)
{ {
#if defined(__x86_64__)
const __m128i raw = _mm_loadl_epi64(reinterpret_cast<const __m128i*>(host)); const __m128i raw = _mm_loadl_epi64(reinterpret_cast<const __m128i*>(host));
return PpcM128ToPsInline(_mm_castsi128_ps( return PpcM128ToPsInline(_mm_castsi128_ps(
PpcLoadPairPsqFloatBitsLanesInline(PpcPsqSwapPairBytesInline(raw)))); PpcLoadPairPsqFloatBitsLanesInline(PpcPsqSwapPairBytesInline(raw))));
#elif defined(__aarch64__)
// The x86 path's full 8-byte pshufb reversal plus a same-endian load is,
// taken together, exactly a 64-bit byteswap of a plain little-endian load:
// it turns the on-disk [ps0 big-endian][ps1 big-endian] byte layout into a
// native uint64 with low 32 bits = ps1, high 32 bits = ps0 (this file's
// documented packed-double lane convention).
uint64_t raw = 0;
std::memcpy(&raw, host, sizeof(raw));
const uint64_t swapped = __builtin_bswap64(raw);
return PpcBitCastToDoubleInline(PpcLoadPairPsqFloatBitsPackedInline(swapped));
#endif
} }
// packed FPR double -> host, guard already proven by the caller. // packed FPR double -> host, guard already proven by the caller.
inline void PpcStorePairPsqFloatToHostInline(uint8_t* host, double value) inline void PpcStorePairPsqFloatToHostInline(uint8_t* host, double value)
{ {
#if defined(__x86_64__)
const __m128i lanes = PpcStorePairPsqFloatBitsLanesInline( const __m128i lanes = PpcStorePairPsqFloatBitsLanesInline(
_mm_castps_si128(PpcPsToM128Inline(value))); _mm_castps_si128(PpcPsToM128Inline(value)));
_mm_storel_epi64(reinterpret_cast<__m128i*>(host), PpcPsqSwapPairBytesInline(lanes)); _mm_storel_epi64(reinterpret_cast<__m128i*>(host), PpcPsqSwapPairBytesInline(lanes));
#elif defined(__aarch64__)
// Inverse of the load path above: bswap64 is its own inverse, so applying
// it to the quieted packed value reproduces the on-disk big-endian bytes.
const uint64_t quieted = PpcStorePairPsqFloatBitsPackedInline(PpcBitCastToU64Inline(value));
const uint64_t swapped = __builtin_bswap64(quieted);
std::memcpy(host, &swapped, sizeof(swapped));
#endif
} }
template <typename SignedType> template <typename SignedType>
@@ -224,6 +264,8 @@ inline void PpcWritePairPsqInline(uint32_t addr, T first, T second)
// reading stale bytes, and unmapped pages commit on demand, same as MemoryInline::Flat* loads. // reading stale bytes, and unmapped pages commit on demand, same as MemoryInline::Flat* loads.
MKW_PPC_FORCE_INLINE const uint8_t* PpcTryGetPsqReadableHostInline(uint32_t addr) MKW_PPC_FORCE_INLINE const uint8_t* PpcTryGetPsqReadableHostInline(uint32_t addr)
{ {
if (GuestFlat::RequiresCheckedAccess()) [[unlikely]]
return nullptr;
return MKW_FLAT_GUEST_BASE + addr; return MKW_FLAT_GUEST_BASE + addr;
} }
@@ -234,6 +276,8 @@ MKW_PPC_FORCE_INLINE const uint8_t* PpcTryGetPsqReadableHostInline(uint32_t addr
// executable, and unmapped pages still trap. // executable, and unmapped pages still trap.
MKW_PPC_FORCE_INLINE uint8_t* PpcTryGetPsqWritableHostInline(uint32_t addr) MKW_PPC_FORCE_INLINE uint8_t* PpcTryGetPsqWritableHostInline(uint32_t addr)
{ {
if (GuestFlat::RequiresCheckedAccess()) [[unlikely]]
return nullptr;
if (addr > UINT32_MAX - 7u) [[unlikely]] if (addr > UINT32_MAX - 7u) [[unlikely]]
return nullptr; return nullptr;
if (MemoryInline::FlatWriteNeedsPolicy(addr) || if (MemoryInline::FlatWriteNeedsPolicy(addr) ||
@@ -329,6 +373,7 @@ inline uint8_t PpcQuantizePsqU8Scale61Inline(float value)
// matching PpcQuantizePsqU8Scale61Inline's !(scaled > 0) rule. // matching PpcQuantizePsqU8Scale61Inline's !(scaled > 0) rule.
inline uint16_t PpcQuantizePairPsqU8Scale61PackedInline(double value) inline uint16_t PpcQuantizePairPsqU8Scale61PackedInline(double value)
{ {
#if defined(__x86_64__)
const __m128 scaled = _mm_mul_ps(PpcPsToM128Inline(value), _mm_set1_ps(0.125f)); const __m128 scaled = _mm_mul_ps(PpcPsToM128Inline(value), _mm_set1_ps(0.125f));
const __m128 nonNegative = _mm_max_ps(scaled, _mm_setzero_ps()); const __m128 nonNegative = _mm_max_ps(scaled, _mm_setzero_ps());
const __m128 clamped = _mm_min_ps(nonNegative, _mm_set1_ps(255.0f)); const __m128 clamped = _mm_min_ps(nonNegative, _mm_set1_ps(255.0f));
@@ -338,6 +383,17 @@ inline uint16_t PpcQuantizePairPsqU8Scale61PackedInline(double value)
// Native lane 0 is ps1 and lane 1 is ps0. Packing to the low uint16_t // Native lane 0 is ps1 and lane 1 is ps0. Packing to the low uint16_t
// therefore produces the guest-order numeric value (ps0 << 8) | ps1. // therefore produces the guest-order numeric value (ps0 << 8) | ps1.
return static_cast<uint16_t>(_mm_cvtsi128_si32(lanes8)); return static_cast<uint16_t>(_mm_cvtsi128_si32(lanes8));
#elif defined(__aarch64__)
// Equivalent to two calls of the already-portable scalar quantizer above
// (its own !(scaled > 0) rule maps NaN to 0, matching what MAXPS-with-zero
// does on the x86 path per the comment there), packed the same way the
// fallback path just below already does for the non-SIMD case.
PPC_FPR fpr{};
fpr.d = value;
const uint8_t q0 = PpcQuantizePsqU8Scale61Inline(fpr.paired.ps0);
const uint8_t q1 = PpcQuantizePsqU8Scale61Inline(fpr.paired.ps1);
return static_cast<uint16_t>((static_cast<uint16_t>(q0) << 8) | static_cast<uint16_t>(q1));
#endif
} }
// Preserve the complete memory/MMIO/executable-write behavior off the leaf // Preserve the complete memory/MMIO/executable-write behavior off the leaf
+34 -5
View File
@@ -231,6 +231,13 @@ MKW_MEMORY_FORCE_INLINE uint8_t* ResolveRangeHost(uint32_t base, int32_t minOffs
(void)needsRead; (void)needsRead;
const uint32_t guestStart = base + static_cast<uint32_t>(minOffset); const uint32_t guestStart = base + static_cast<uint32_t>(minOffset);
if (length == 0 || length > kPageSize || guestStart > UINT32_MAX - (length - 1)) return nullptr; if (length == 0 || length > kPageSize || guestStart > UINT32_MAX - (length - 1)) return nullptr;
if (GuestFlat::RequiresCheckedAccess()) {
// A host page can cover multiple independently-special Wii pages.
// Returning null keeps resolved accesses on the checked Memory::*
// path, which materializes deferred reads and applies write policy.
(void)needsWrite;
return nullptr;
}
if (needsWrite && if (needsWrite &&
(FlatWriteNeedsPolicy(guestStart) || FlatWriteNeedsPolicy(guestStart + (length - 1)))) (FlatWriteNeedsPolicy(guestStart) || FlatWriteNeedsPolicy(guestStart + (length - 1))))
[[unlikely]] return nullptr; [[unlikely]] return nullptr;
@@ -522,6 +529,13 @@ MKW_MEMORY_FORCE_INLINE void WriteResolvedFloat64(uint8_t* r, uint32_t o, uint32
// around `*(T*)(base + addr)`, no page-table load or limit check (interception model documented // around `*(T*)(base + addr)`, no page-table load or limit check (interception model documented
// in guest_flat_memory.h). The one exception kept inline is the MMIO write policy, since the // in guest_flat_memory.h). The one exception kept inline is the MMIO write policy, since the
// written value can't be recovered from a fault record. // written value can't be recovered from a fault record.
//
// When a host VM page is larger than a 4 KiB Wii page, guest-view protections
// cannot distinguish adjacent special Wii pages. The general FlatRead*/
// FlatWrite* helpers then use the checked page-table path, which materializes
// deferred reads and applies executable-write/MMIO policy before touching RAM.
// FlatWriteRam* remains direct because the translator emits it only for
// addresses it has proven are ordinary RAM.
template <typename T> template <typename T>
MKW_MEMORY_FORCE_INLINE T FlatLoad(uint32_t address) { MKW_MEMORY_FORCE_INLINE T FlatLoad(uint32_t address) {
@@ -536,47 +550,62 @@ MKW_MEMORY_FORCE_INLINE void FlatStore(uint32_t address, T value) {
std::memcpy(MKW_FLAT_GUEST_BASE + address, &swapped, sizeof(T)); std::memcpy(MKW_FLAT_GUEST_BASE + address, &swapped, sizeof(T));
} }
MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) { return FlatLoad<uint8_t>(address); } MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) {
MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) { return FlatLoad<uint16_t>(address); } if (GuestFlat::RequiresCheckedAccess()) return Memory::Read8(address);
MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) { return FlatLoad<uint32_t>(address); } return FlatLoad<uint8_t>(address);
}
MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) {
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read16(address);
return FlatLoad<uint16_t>(address);
}
MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) {
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read32(address);
return FlatLoad<uint32_t>(address);
}
MKW_MEMORY_FORCE_INLINE float FlatReadFloat32(uint32_t address) { MKW_MEMORY_FORCE_INLINE float FlatReadFloat32(uint32_t address) {
const uint32_t bits = FlatLoad<uint32_t>(address); const uint32_t bits = FlatRead32(address);
float value = 0.0f; float value = 0.0f;
std::memcpy(&value, &bits, sizeof(value)); std::memcpy(&value, &bits, sizeof(value));
return value; return value;
} }
MKW_MEMORY_FORCE_INLINE double FlatReadFloat64(uint32_t address) { MKW_MEMORY_FORCE_INLINE double FlatReadFloat64(uint32_t address) {
const uint64_t bits = FlatLoad<uint64_t>(address); const uint64_t bits = GuestFlat::RequiresCheckedAccess()
? Memory::Read64(address) : FlatLoad<uint64_t>(address);
double value = 0.0; double value = 0.0;
std::memcpy(&value, &bits, sizeof(value)); std::memcpy(&value, &bits, sizeof(value));
return value; return value;
} }
MKW_MEMORY_FORCE_INLINE void FlatWrite8(uint32_t address, uint8_t value) { MKW_MEMORY_FORCE_INLINE void FlatWrite8(uint32_t address, uint8_t value) {
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write8(address, value); return; }
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write8Slow(address, value); return; } if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write8Slow(address, value); return; }
FlatStore<uint8_t>(address, value); FlatStore<uint8_t>(address, value);
} }
MKW_MEMORY_FORCE_INLINE void FlatWrite16(uint32_t address, uint16_t value) { MKW_MEMORY_FORCE_INLINE void FlatWrite16(uint32_t address, uint16_t value) {
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write16(address, value); return; }
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write16Slow(address, value); return; } if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write16Slow(address, value); return; }
FlatStore<uint16_t>(address, value); FlatStore<uint16_t>(address, value);
} }
MKW_MEMORY_FORCE_INLINE void FlatWrite32(uint32_t address, uint32_t value) { MKW_MEMORY_FORCE_INLINE void FlatWrite32(uint32_t address, uint32_t value) {
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write32(address, value); return; }
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write32Slow(address, value); return; } if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write32Slow(address, value); return; }
FlatStore<uint32_t>(address, value); FlatStore<uint32_t>(address, value);
} }
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat32(uint32_t address, double value) { MKW_MEMORY_FORCE_INLINE void FlatWriteFloat32(uint32_t address, double value) {
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat32(address, value); return; }
const uint32_t bits = ConvertPpcDoubleToSingleBits(value); const uint32_t bits = ConvertPpcDoubleToSingleBits(value);
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat32Slow(address, value); return; } if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat32Slow(address, value); return; }
FlatStore<uint32_t>(address, bits); FlatStore<uint32_t>(address, bits);
} }
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat64(uint32_t address, double value) { MKW_MEMORY_FORCE_INLINE void FlatWriteFloat64(uint32_t address, double value) {
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat64(address, value); return; }
uint64_t bits = 0; uint64_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits)); std::memcpy(&bits, &value, sizeof(bits));
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat64Slow(address, value); return; } if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat64Slow(address, value); return; }
+3 -2
View File
@@ -4,8 +4,9 @@
namespace MusicAttenuation { namespace MusicAttenuation {
// Enables the optional Windows media-session integration. The monitor is // Enables the optional external-media integration (Windows media sessions,
// started lazily the first time this is enabled. // or MPRIS over D-Bus on Linux). The monitor is started lazily the first time
// this is enabled.
void SetEnabled(bool enabled) noexcept; void SetEnabled(bool enabled) noexcept;
void SetMusicVolume(float volume) noexcept; void SetMusicVolume(float volume) noexcept;
void SetSoundEffectsVolume(float volume) noexcept; void SetSoundEffectsVolume(float volume) noexcept;
+14 -1
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "runtime_config.h" #include "runtime_config.h"
#include "nand_settings.h"
#include "runtime_log.h" #include "runtime_log.h"
#include "system_bridge.h" #include "system_bridge.h"
@@ -163,7 +164,7 @@ inline std::filesystem::path CreateManagedNandRoot() {
return root; return root;
} }
inline std::filesystem::path DiscoverNandRootPath() { inline std::filesystem::path ResolveNandRootPath() {
const std::string configPath = RuntimeConfigFile::NandRoot(); const std::string configPath = RuntimeConfigFile::NandRoot();
if (!configPath.empty()) { if (!configPath.empty()) {
const auto path = ResolveConfiguredPath(configPath); const auto path = ResolveConfiguredPath(configPath);
@@ -179,4 +180,16 @@ inline std::filesystem::path DiscoverNandRootPath() {
return CreateManagedNandRoot(); return CreateManagedNandRoot();
} }
inline std::filesystem::path DiscoverNandRootPath() {
static const auto root = [] {
const auto resolved = ResolveNandRootPath();
std::string error;
if (!RuntimeNandSettings::Ensure(resolved, error)) {
FailNandRoot(error.c_str(), RuntimeNandSettings::FilePath(resolved));
}
return resolved;
}();
return root;
}
} // namespace RuntimeNandPath } // namespace RuntimeNandPath
+59
View File
@@ -0,0 +1,59 @@
#pragma once
#include <filesystem>
#include <fstream>
#include <istream>
namespace RuntimeNandSave {
enum class Contents { Missing, Blank, Nonzero, Error };
enum class ReadAction { Proceed, Missing, Error, RecoveryNeeded };
// A failed read is not evidence that a save is blank. Check badbit before EOF:
// an I/O failure may set both, whereas a successful short final read sets EOF.
inline Contents InspectStream(std::istream& input) {
if (!input) return Contents::Error;
char block[4096];
for (;;) {
input.read(block, sizeof(block));
if (input.bad() || (input.fail() && !input.eof())) return Contents::Error;
for (std::streamsize i = 0; i < input.gcount(); ++i) {
if (block[i] != 0) return Contents::Nonzero;
}
if (input.eof()) return Contents::Blank;
}
}
inline Contents InspectFile(const std::filesystem::path& path) {
std::error_code ec;
const auto status = std::filesystem::symlink_status(path, ec);
if (ec && ec != std::errc::no_such_file_or_directory) return Contents::Error;
if (!std::filesystem::exists(status)) return Contents::Missing;
if (!std::filesystem::is_regular_file(path, ec) || ec) return Contents::Error;
std::ifstream input(path, std::ios::binary);
return InspectStream(input);
}
// Probe only read-only opens of the actual save and its exact write shadow.
// No probe writes, removes, or repairs data, and backups are not save aliases.
inline ReadAction CheckRead(const std::filesystem::path& path, int mode) {
const auto name = path.filename();
const bool isMain = name == "rksys.dat";
if (mode != 1 || (!isMain && name != "rksys.dat.nandsafe.tmp")) return ReadAction::Proceed;
const auto contents = InspectFile(path);
if (contents == Contents::Error) return ReadAction::Error;
if (contents == Contents::Nonzero) return ReadAction::Proceed;
if (isMain) {
auto shadow = path;
shadow += ".nandsafe.tmp";
const auto shadowContents = InspectFile(shadow);
if (shadowContents == Contents::Error) return ReadAction::Error;
// The next write normally discards an old shadow. Preserve a possible
// recovery source when there is no usable original, without promoting
// an uncommitted (and potentially incomplete) shadow to the real save.
if (shadowContents == Contents::Nonzero) return ReadAction::RecoveryNeeded;
}
return contents == Contents::Blank ? ReadAction::Missing : ReadAction::Proceed;
}
} // namespace RuntimeNandSave
+220
View File
@@ -0,0 +1,220 @@
#pragma once
#include <array>
#include <atomic>
#include <chrono>
#include <ctime>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <map>
#include <optional>
#include <string>
#include <utility>
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif
namespace RuntimeNandSettings {
using Settings = std::map<std::string, std::string>;
inline std::filesystem::path FilePath(const std::filesystem::path& root) {
return root / "title/00000001/00000002/data/setting.txt";
}
// Wii setting.txt is a 256-byte buffer encrypted with a rotating XOR key.
inline std::optional<Settings> Read(const std::filesystem::path& nandRoot) {
std::ifstream input(FilePath(nandRoot), std::ios::binary);
std::array<uint8_t, 256> bytes{};
if (!input.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) {
return std::nullopt;
}
uint32_t key = 0x73B5DBFAu;
std::string decoded;
for (const uint8_t byte : bytes) {
const char value = static_cast<char>(byte ^ static_cast<uint8_t>(key));
key = (key << 1) | (key >> 31);
if (value == '\0') {
break;
}
if (value != '\r') {
decoded += value;
}
}
Settings settings;
for (size_t start = 0; start < decoded.size();) {
const size_t end = decoded.find('\n', start);
const std::string line = decoded.substr(start, end - start);
const size_t equals = line.find('=');
if (equals != std::string::npos && equals != 0) {
settings.emplace(line.substr(0, equals), line.substr(equals + 1));
}
if (end == std::string::npos) {
break;
}
start = end + 1;
}
return settings;
}
inline bool HasIdentity(const Settings& settings) {
const auto serial = settings.find("SERNO");
if (serial == settings.end() || serial->second.empty() || serial->second.size() > 9 ||
serial->second.find_first_not_of("0123456789") != std::string::npos ||
serial->second.find_first_not_of('0') == std::string::npos) {
return false;
}
for (const auto& field : {std::pair{"CODE", 5u}, {"AREA", 3u}, {"GAME", 2u}}) {
const auto value = settings.find(field.first);
if (value == settings.end() || value->second.empty() ||
value->second.size() > field.second) {
return false;
}
}
return true;
}
// Dolphin's normal (non-deterministic) first-boot algorithm. It is independent
// of the ES device ID. Matching another NAND requires that NAND's saved serial.
inline std::string GenerateSerial(std::time_t now) {
if (now < 0) {
return {};
}
const auto digits = std::to_string(now % 1000000000);
return std::string(9 - digits.size(), '0') + digits;
}
// This recompilation targets the European disc. These are Dolphin's PAL boot
// defaults; an existing setting.txt always takes precedence, in every region.
inline std::optional<std::array<uint8_t, 256>> EncodeNew(const std::string& serial) {
const Settings identity{{"SERNO", serial}, {"CODE", "LEH"}, {"AREA", "EUR"}, {"GAME", "EU"}};
if (!HasIdentity(identity)) {
return std::nullopt;
}
std::array<uint8_t, 256> bytes{};
size_t position = 0;
uint32_t key = 0x73B5DBFAu;
const auto writeByte = [&](char value) {
bytes[position++] = static_cast<uint8_t>(value) ^ static_cast<uint8_t>(key);
key = (key << 1) | (key >> 31);
};
for (const std::string& line : {std::string("AREA=EUR\r\n"), std::string("MODEL=RVL-001(EUR)\r\n"),
std::string("DVD=0\r\n"), std::string("MPCH=0x7FFE\r\n"), std::string("CODE=LEH\r\n"),
"SERNO=" + serial + "\r\n", std::string("VIDEO=PAL\r\n"), std::string("GAME=EU\r\n")}) {
for (;;) {
if (position + line.size() > bytes.size()) {
return std::nullopt;
}
const auto start = position;
const auto savedKey = key;
bool hasNull = false;
for (const char value : line) {
writeByte(value);
hasNull |= bytes[position - 1] == 0;
}
if (!hasNull) {
break;
}
// Nintendo stops at an encoded NUL. Dolphin inserts an extra LF
// before this line and retries with the shifted encryption key.
position = start;
key = savedKey;
writeByte('\n');
}
}
return bytes; // The unused tail stays raw zero, as in Dolphin.
}
// Atomically claim our own scratch directory. A collision belongs to another
// launch (or a previous crashed launch); leave it untouched and try another name.
inline std::optional<std::filesystem::path> CreateScratchDirectory(
const std::filesystem::path& parent, const std::string& token, std::error_code& ec) {
for (unsigned attempt = 0; attempt < 128; ++attempt) {
const auto candidate = parent / (".setting-init-" + token + "-" + std::to_string(attempt));
ec.clear();
if (std::filesystem::create_directory(candidate, ec)) return candidate;
if (ec && ec != std::errc::file_exists) return std::nullopt;
}
ec = std::make_error_code(std::errc::file_exists);
return std::nullopt;
}
// Never replace an existing file, including an unreadable or damaged one.
// Publish a complete file atomically so simultaneous launches use one identity.
inline bool Ensure(const std::filesystem::path& root, std::string& error,
std::time_t now = std::time(nullptr)) {
const auto path = FilePath(root);
std::error_code ec;
const auto status = std::filesystem::symlink_status(path, ec);
if (ec && ec != std::errc::no_such_file_or_directory) {
error = "Cannot inspect NAND setting.txt: " + ec.message();
return false;
}
if (std::filesystem::exists(status)) {
const auto existing = Read(root);
if (existing && HasIdentity(*existing)) {
return true;
}
error = "Existing NAND setting.txt is unreadable or invalid; restore it from this console's backup";
return false;
}
const auto bytes = EncodeNew(GenerateSerial(now));
if (!bytes) {
error = "Cannot initialize NAND settings: invalid system clock";
return false;
}
ec.clear();
std::filesystem::create_directories(path.parent_path(), ec);
if (ec) {
error = "Cannot create NAND settings directory: " + ec.message();
return false;
}
static std::atomic<unsigned> sequence{0};
#ifdef _WIN32
const auto processId = GetCurrentProcessId();
#else
const auto processId = getpid();
#endif
const auto scratch = CreateScratchDirectory(path.parent_path(),
std::to_string(processId) + "-" + std::to_string(
std::chrono::steady_clock::now().time_since_epoch().count()) + "-" +
std::to_string(sequence++), ec);
if (!scratch) {
error = "Cannot create temporary NAND settings directory: " + ec.message();
return false;
}
const auto temporary = *scratch / "setting.txt";
bool written = false;
{
std::ofstream output(temporary, std::ios::binary);
output.write(reinterpret_cast<const char*>(bytes->data()), bytes->size());
output.close();
written = static_cast<bool>(output);
}
bool published = false;
if (written) {
#ifdef _WIN32
published = MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_WRITE_THROUGH) != 0;
#else
published = ::link(temporary.c_str(), path.c_str()) == 0;
#endif
}
std::filesystem::remove(temporary, ec);
std::filesystem::remove(*scratch, ec);
// A competing launcher may have published its settings first. Always read
// the winner from NAND rather than using our unpersisted candidate serial.
const auto persisted = Read(root);
if (persisted && HasIdentity(*persisted)) {
return true;
}
error = published ? "Cannot read newly initialized NAND setting.txt" :
"Cannot persist NAND setting.txt; check NAND directory permissions";
return false;
}
} // namespace RuntimeNandSettings
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <cstdint>
#include <filesystem>
#include <optional>
#include <string_view>
// Small host-services boundary for functionality that must not leak Win32
// assumptions into runtime or game code. Guest execution, virtual memory, and
// cooperative contexts remain outside this layer until dedicated macOS
// prototypes establish a safe abstraction.
namespace RuntimePlatform {
std::optional<std::filesystem::path> ExecutableDirectory() noexcept;
// Returns the platform's conventional per-user application-data directory.
// It does not create the directory, leaving that policy to the caller.
std::filesystem::path ApplicationDataDirectory(std::string_view applicationName);
// The root for per-run diagnostics. Keeping this here ensures log placement
// follows the same host convention as configuration and other user data.
std::filesystem::path LogDirectory(std::string_view applicationName);
uint64_t CurrentProcessId() noexcept;
} // namespace RuntimePlatform
+157
View File
@@ -8,8 +8,10 @@
#include <cstdint> #include <cstdint>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iomanip>
#include <iostream> #include <iostream>
#include <limits> #include <limits>
#include <map>
#include <optional> #include <optional>
#include <sstream> #include <sstream>
#include <string> #include <string>
@@ -17,6 +19,7 @@
#include <utility> #include <utility>
#include <vector> #include <vector>
#include <toml.hpp> #include <toml.hpp>
#include "platform/host_platform.h"
#ifdef _WIN32 #ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN #ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
@@ -52,7 +55,31 @@ struct RuntimeUserConfig {
std::optional<bool> audioMuted; std::optional<bool> audioMuted;
std::optional<bool> audioMixWorker; std::optional<bool> audioMixWorker;
std::optional<bool> attenuateMusicWhenMediaPlays; std::optional<bool> attenuateMusicWhenMediaPlays;
// Real Wii Remotes (with or without Nunchuk / Classic Controller) and Wii U Pro
// Controllers paired over Bluetooth, driven by SDL's HIDAPI Wii driver. The driver
// is opt-in on SDL's side, so this decides whether the runtime turns it on.
std::optional<bool> wiiRemotes;
// Keep re-enumerating Bluetooth HID devices while no Wii controller is connected
// (Dolphin's "continuous scanning"), so a remote that dropped or was switched on
// after launch shows up without restarting.
std::optional<bool> wiiContinuousScan;
// Accelerometer zero-point correction for the Bluetooth Wii Remote, in g and in
// SDL's sensor frame (x right, y out of the button face, z towards the user).
// SDL's Wii driver falls back to a nominal zero point when its read of the
// remote's calibration block times out (common over Bluetooth), so this is
// measured in the overlay with the remote at rest.
std::optional<double> wiiAccelOffsetX;
std::optional<double> wiiAccelOffsetY;
std::optional<double> wiiAccelOffsetZ;
// Debugging aid: append every KPAD sample of the Bluetooth remote (raw and
// corrected accelerometer, buttons) to wii_accel_trace.csv next to Config.toml.
std::optional<bool> wiiAccelTrace;
std::optional<bool> networkEnabled; std::optional<bool> networkEnabled;
std::optional<bool> discordPresenceEnabled;
// The application ID of the WiiCompiled Discord application. This is only
// used by the base product; Retro Rewind supplies its own ID through the
// standard Dolphin /dev/dolphin interface.
std::optional<std::string> discordClientId;
std::optional<std::string> nandRoot; std::optional<std::string> nandRoot;
std::optional<std::string> dvdRoot; std::optional<std::string> dvdRoot;
// The one canonical Retro Rewind installation, owned and updated by the frontend. Setup records // The one canonical Retro Rewind installation, owned and updated by the frontend. Setup records
@@ -63,6 +90,8 @@ struct RuntimeUserConfig {
// comma-separated SDL-style physical button names ("south", or // comma-separated SDL-style physical button names ("south", or
// "dpad_up,left_shoulder") as values; pressing either bound button counts. // "dpad_up,left_shoulder") as values; pressing either bound button counts.
std::array<std::optional<std::string>, 12> controllerButtons; std::array<std::optional<std::string>, 12> controllerButtons;
std::optional<bool> rumbleEnabled;
std::map<std::string, std::string> controllerExpressions;
}; };
namespace RuntimeConfigFile { namespace RuntimeConfigFile {
@@ -136,7 +165,14 @@ inline bool IsSupportedResolutionMultiplier(float value) {
// Must stay in step with the backend table in main.cpp, which is what actually // Must stay in step with the backend table in main.cpp, which is what actually
// maps these to AuroraBackend. // maps these to AuroraBackend.
inline bool IsSupportedGraphicsApi(std::string_view value) { inline bool IsSupportedGraphicsApi(std::string_view value) {
#if defined(__APPLE__)
static constexpr std::array<std::string_view, 2> values{"auto", "metal"};
// only vulkan for linux
#elif defined(__linux__)
static constexpr std::array<std::string_view, 2> values{"auto", "vulkan"};
#elif defined(_WIN32)
static constexpr std::array<std::string_view, 3> values{"auto", "d3d12", "vulkan"}; static constexpr std::array<std::string_view, 3> values{"auto", "d3d12", "vulkan"};
#endif
return std::find(values.begin(), values.end(), value) != values.end(); return std::find(values.begin(), values.end(), value) != values.end();
} }
@@ -167,6 +203,8 @@ inline std::optional<std::filesystem::path> ExecutableDirectory() {
} }
buffer.resize(buffer.size() * 2); buffer.resize(buffer.size() * 2);
} }
#elif defined(__APPLE__)
return RuntimePlatform::ExecutableDirectory();
#else #else
// /proc/self/exe is a Linux-specific magic symlink to the running executable; readlink() // /proc/self/exe is a Linux-specific magic symlink to the running executable; readlink()
// does not NUL-terminate and silently truncates if the buffer is too small, so this grows // does not NUL-terminate and silently truncates if the buffer is too small, so this grows
@@ -224,6 +262,8 @@ inline std::filesystem::path ApplicationDataDirectory() {
CoTaskMemFree(rawPath); CoTaskMemFree(rawPath);
return directory; return directory;
} }
#elif defined(__APPLE__)
return RuntimePlatform::ApplicationDataDirectory(kApplicationDirectoryName);
#else #else
// XDG Base Directory spec equivalent of FOLDERID_LocalAppData: $XDG_DATA_HOME if set and // XDG Base Directory spec equivalent of FOLDERID_LocalAppData: $XDG_DATA_HOME if set and
// non-empty, otherwise its default of $HOME/.local/share. // non-empty, otherwise its default of $HOME/.local/share.
@@ -286,6 +326,12 @@ inline void EnsureConfigFile() {
"mix_worker = true\n\n" "mix_worker = true\n\n"
"[network]\n" "[network]\n"
"enabled = true\n\n" "enabled = true\n\n"
"[discord]\n"
"# Rich Presence talks only to a locally-running Discord client.\n"
"# Retro Rewind supplies its official app ID automatically. Set this\n"
"# to WiiCompiled's Discord application ID for basic base-game presence.\n"
"enabled = true\n"
"# client_id = \"123456789012345678\"\n\n"
"[paths]\n" "[paths]\n"
"# dvd_root = \"D:\\\\MarioKartWii\\\\DATA\"\n" "# dvd_root = \"D:\\\\MarioKartWii\\\\DATA\"\n"
"# nand_root = \"D:\\\\WiiNand\"\n" "# nand_root = \"D:\\\\WiiNand\"\n"
@@ -352,6 +398,7 @@ inline void AppendOverlayRoots(RuntimeUserConfig& config, const std::string& roo
} }
} }
// Reads every supported setting out of a parsed Config.toml document.
inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) { inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
RuntimeUserConfig config; RuntimeUserConfig config;
@@ -363,6 +410,17 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
FindConfigValue<std::string>(document, "controller", buttonKeys[index]); FindConfigValue<std::string>(document, "controller", buttonKeys[index]);
} }
config.rumbleEnabled = FindConfigValue<bool>(document, "controller", "rumble");
if (const auto* section = document.contains("controller") ? &document.at("controller") : nullptr;
section != nullptr && section->is_table()) {
for (const auto& [key, value] : section->as_table()) {
if (key.rfind("expr_", 0) == 0 && value.is_string()) {
config.controllerExpressions[key] = value.as_string();
}
}
}
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen"); config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
config.windowPosX = FindConfigInt(document, "video", "window_x"); config.windowPosX = FindConfigInt(document, "video", "window_x");
config.windowPosY = FindConfigInt(document, "video", "window_y"); config.windowPosY = FindConfigInt(document, "video", "window_y");
@@ -417,7 +475,15 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.audioMixWorker = FindConfigValue<bool>(document, "audio", "mix_worker"); config.audioMixWorker = FindConfigValue<bool>(document, "audio", "mix_worker");
config.attenuateMusicWhenMediaPlays = config.attenuateMusicWhenMediaPlays =
FindConfigValue<bool>(document, "audio", "attenuate_music_when_media_plays"); FindConfigValue<bool>(document, "audio", "attenuate_music_when_media_plays");
config.wiiRemotes = FindConfigValue<bool>(document, "controller", "wii_remotes");
config.wiiContinuousScan = FindConfigValue<bool>(document, "controller", "wii_continuous_scan");
config.wiiAccelOffsetX = FindConfigValue<double>(document, "controller", "wii_accel_offset_x");
config.wiiAccelOffsetY = FindConfigValue<double>(document, "controller", "wii_accel_offset_y");
config.wiiAccelOffsetZ = FindConfigValue<double>(document, "controller", "wii_accel_offset_z");
config.wiiAccelTrace = FindConfigValue<bool>(document, "controller", "wii_accel_trace");
config.networkEnabled = FindConfigValue<bool>(document, "network", "enabled"); config.networkEnabled = FindConfigValue<bool>(document, "network", "enabled");
config.discordPresenceEnabled = FindConfigValue<bool>(document, "discord", "enabled");
config.discordClientId = FindConfigValue<std::string>(document, "discord", "client_id");
config.nandRoot = FindConfigValue<std::string>(document, "paths", "nand_root"); config.nandRoot = FindConfigValue<std::string>(document, "paths", "nand_root");
config.dvdRoot = FindConfigValue<std::string>(document, "paths", "dvd_root"); config.dvdRoot = FindConfigValue<std::string>(document, "paths", "dvd_root");
@@ -625,6 +691,25 @@ inline bool SetControllerButton(size_t index, std::string value) {
return WriteSetting("controller", kControllerButtonKeys[index], FormatString(value)); return WriteSetting("controller", kControllerButtonKeys[index], FormatString(value));
} }
inline std::string ControllerExpression(const std::string& key) {
const auto it = Get().controllerExpressions.find(key);
return it == Get().controllerExpressions.end() ? std::string() : it->second;
}
inline bool SetControllerExpression(const std::string& key, const std::string& value) {
Mutable().controllerExpressions[key] = value;
return WriteSetting("controller", key, FormatString(value));
}
inline bool RumbleEnabled(bool fallback = true) {
return Get().rumbleEnabled.value_or(fallback);
}
inline bool SetRumbleEnabled(bool value) {
Mutable().rumbleEnabled = value;
return WriteSetting("controller", "rumble", value ? "true" : "false");
}
inline bool SetAudioVolume(float value) { inline bool SetAudioVolume(float value) {
value = std::clamp(value, 0.0f, 1.0f); value = std::clamp(value, 0.0f, 1.0f);
Mutable().audioVolume = value; Mutable().audioVolume = value;
@@ -734,14 +819,75 @@ inline bool AudioMixWorkerEnabled(bool fallback = true) {
return Get().audioMixWorker.value_or(fallback); return Get().audioMixWorker.value_or(fallback);
} }
// Whether background music should duck automatically for other media playback.
inline bool AttenuateMusicWhenMediaPlays(bool fallback = false) { inline bool AttenuateMusicWhenMediaPlays(bool fallback = false) {
return Get().attenuateMusicWhenMediaPlays.value_or(fallback); return Get().attenuateMusicWhenMediaPlays.value_or(fallback);
} }
// Bluetooth Wii Remotes / Wii U Pro Controllers. Read once before SDL's joystick
// subsystem comes up, so a change only takes effect on the next launch.
inline bool WiiRemotesEnabled(bool fallback = true) {
return Get().wiiRemotes.value_or(fallback);
}
// Persists the Bluetooth Wii Remote driver switch.
inline bool SetWiiRemotesEnabled(bool value) {
Mutable().wiiRemotes = value;
return WriteSetting("controller", "wii_remotes", value ? "true" : "false");
}
// Whether to keep rescanning Bluetooth while no Wii controller is connected.
inline bool WiiContinuousScanEnabled(bool fallback = false) {
return Get().wiiContinuousScan.value_or(fallback);
}
// Persists the continuous scanning switch.
inline bool SetWiiContinuousScanEnabled(bool value) {
Mutable().wiiContinuousScan = value;
return WriteSetting("controller", "wii_continuous_scan", value ? "true" : "false");
}
// Wii Remote accelerometer zero-point correction (g, SDL sensor frame); all zero
// when the remote has not been calibrated.
inline std::array<double, 3> WiiAccelOffset() {
const RuntimeUserConfig& config = Get();
return {config.wiiAccelOffsetX.value_or(0.0), config.wiiAccelOffsetY.value_or(0.0),
config.wiiAccelOffsetZ.value_or(0.0)};
}
// Whether to write the per-frame accelerometer trace (off unless asked for).
inline bool WiiAccelTraceEnabled(bool fallback = false) {
return Get().wiiAccelTrace.value_or(fallback);
}
// True while a non-zero correction is stored ("Clear calibration" writes zeros).
inline bool HasWiiAccelOffset() {
const std::array<double, 3> offset = WiiAccelOffset();
return offset[0] != 0.0 || offset[1] != 0.0 || offset[2] != 0.0;
}
// Persists the accelerometer correction measured by the overlay's calibration.
inline bool SetWiiAccelOffset(const std::array<double, 3>& offset) {
Mutable().wiiAccelOffsetX = offset[0];
Mutable().wiiAccelOffsetY = offset[1];
Mutable().wiiAccelOffsetZ = offset[2];
bool ok = true;
const char* keys[3] = {"wii_accel_offset_x", "wii_accel_offset_y", "wii_accel_offset_z"};
for (size_t i = 0; i < 3; ++i) {
// Always a float literal, so a whole-number offset does not come back as a TOML integer.
std::ostringstream formatted;
formatted << std::fixed << std::setprecision(4) << offset[i];
ok = WriteSetting("controller", keys[i], formatted.str()) && ok;
}
return ok;
}
// Target frame rate for frame interpolation, or 0 to disable it.
inline uint32_t FrameInterpolationFps(uint32_t fallback = 0) { inline uint32_t FrameInterpolationFps(uint32_t fallback = 0) {
return Get().frameInterpolationFps.value_or(fallback); return Get().frameInterpolationFps.value_or(fallback);
} }
// Whether to skip draws whose graphics pipeline has not finished compiling yet.
inline bool SkipUnreadyPipelines(bool fallback = true) { inline bool SkipUnreadyPipelines(bool fallback = true) {
return Get().skipUnreadyPipelines.value_or(fallback); return Get().skipUnreadyPipelines.value_or(fallback);
} }
@@ -815,6 +961,14 @@ inline std::string RetroRewindRoot(std::string fallback = "") {
return Get().retroRewindRoot.value_or(std::move(fallback)); return Get().retroRewindRoot.value_or(std::move(fallback));
} }
inline bool DiscordPresenceEnabled(bool fallback = true) {
return Get().discordPresenceEnabled.value_or(fallback);
}
inline std::string DiscordClientId(std::string fallback = "1543984562369990706") {
return Get().discordClientId.value_or(std::move(fallback));
}
inline const std::vector<std::string>& OverlayRoots() { inline const std::vector<std::string>& OverlayRoots() {
return Get().overlayRoots; return Get().overlayRoots;
} }
@@ -871,6 +1025,9 @@ inline void LogLoadedConfig() {
if (config.networkEnabled) { if (config.networkEnabled) {
std::cout << " network_enabled=" << (*config.networkEnabled ? "true" : "false"); std::cout << " network_enabled=" << (*config.networkEnabled ? "true" : "false");
} }
if (config.discordPresenceEnabled) {
std::cout << " discord_enabled=" << (*config.discordPresenceEnabled ? "true" : "false");
}
if (config.nandRoot) { if (config.nandRoot) {
std::cout << " nand_root=" << *config.nandRoot; std::cout << " nand_root=" << *config.nandRoot;
} }
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <charconv>
#include <cstddef>
#include <cstdint>
#include <string_view>
#include <system_error>
namespace RuntimeScSerial {
// SCGetProductSN's output is a u32, not a character buffer. DWC loads
// that word and formats it with the product code to construct csnum.
template <typename RangeValidator, typename WordWriter>
uint32_t Write(std::string_view serial, uint32_t address,
RangeValidator&& contains, WordWriter&& write32) {
if (serial.empty() || serial.size() > 9 ||
serial.find_first_not_of("0123456789") != std::string_view::npos) return 0;
uint32_t number = 0;
const auto parsed = std::from_chars(serial.data(), serial.data() + serial.size(), number);
if (parsed.ec != std::errc{} || parsed.ptr != serial.data() + serial.size() ||
!address || !contains(address, sizeof(uint32_t))) return 0;
write32(address, number);
return 1;
}
} // namespace RuntimeScSerial
+1
View File
@@ -36,6 +36,7 @@ extern thread_local uint32_t g_sehLastAccessType;
void WriteFatalLog(std::string_view reason); void WriteFatalLog(std::string_view reason);
void SetRuntimeExitCode(int code); void SetRuntimeExitCode(int code);
void MarkFatalErrorReported();
// Centralized crash reporting (defined in main.cpp). Every fatal path funnels // Centralized crash reporting (defined in main.cpp). Every fatal path funnels
// through these so the per-run log folder always receives the same artifact // through these so the per-run log folder always receives the same artifact
+112
View File
@@ -0,0 +1,112 @@
#pragma once
#include <cstdint>
struct PADStatus;
// Real Wii Remotes paired over Bluetooth.
//
// SDL 3 ships a HIDAPI driver for them (Wii Remote alone, with Nunchuk, with a
// Classic Controller, and the Wii U Pro Controller) that exposes each as a
// regular SDL gamepad; it is off by default on SDL's side and ConfigureSdlHints
// turns it on. A remote, alone or with a Nunchuk or Classic Controller, is
// handed to the game as a real Wii Remote with that extension: the KPAD/WPAD
// HLE builds a KPADStatus (and the WPADCLStatus behind KPADGetUnifiedWpadStatus)
// from it every frame (see ReadKpadSample), so the game's own code does wheelies,
// tricks, Wii Wheel steering and the Classic Controller layout, and plugging an
// extension in or out mid-game switches control scheme like on the console. Only
// the Wii U Pro Controller, which has no Wii-era equivalent, goes through
// aurora's PAD layer as a GameCube pad.
namespace WiiRemoteInput {
enum class Kind : uint8_t {
NotWii,
Remote, // Wii Remote with no extension
RemoteWithNunchuk,
RemoteWithClassic,
WiiUPro,
};
// Must run before SDL's joystick subsystem is initialized (aurora does that
// inside aurora_initialize); SDL only consults the hint on its first device scan.
void ConfigureSdlHints(bool enabled);
// Classifies a gamepad by the name SDL's Wii driver reports for it.
Kind KindForName(const char* gamepadName);
// Classification of the SDL gamepad currently assigned to a game port.
Kind KindForPort(uint32_t port);
const char* KindLabel(Kind kind);
// One frame of a Wii Remote in the units KPAD uses.
struct KpadSample {
uint32_t hold = 0; // WPAD button bits (WPAD_BUTTON_*), Nunchuk C/Z included
float acc[3] = {}; // remote accelerometer in g, KPAD frame (rest: y = -1)
bool hasNunchuk = false;
float stick[2] = {}; // Nunchuk stick, -1..1, +y up
float nunchukAcc[3] = {};
bool hasClassic = false;
uint32_t clHold = 0; // WPAD_CL_BUTTON_* bits
float clLStick[2] = {}; // Classic sticks, -1..1, +y up
float clRStick[2] = {};
int16_t clLStickRaw[2] = {}; // as WPADCLStatus reports them: -512..511, centre 0, +y up
int16_t clRStickRaw[2] = {};
uint8_t clTriggerL = 0; // 0..255; SDL only exposes the digital click
uint8_t clTriggerR = 0;
};
// What the game should see on `chan`: the controller SDL has there right now,
// or, for a few seconds after a remote vanished, the kind it had. SDL's driver
// destroys and re-creates the joystick when an extension is plugged in or out,
// and the console never disconnects for that, so the gap is papered over with
// neutral input instead of a "communications interrupted" prompt.
Kind EffectiveKind(uint32_t chan);
// True when the game reads `chan` through KPAD: a Wii Remote alone, with a
// Nunchuk or with a Classic Controller (live or within the swap grace period).
bool IsRemoteChannel(uint32_t chan);
// Reads the current state of the remote on `chan`; false when IsRemoteChannel
// is false. During the swap grace period the sample is neutral.
bool ReadKpadSample(uint32_t chan, KpadSample& sample);
// Remote accelerometer for the overlay readout: the SDL sample in g after the
// zero-point correction (x right across the face, y out of the button face, z
// towards the user) and the KPAD vector built from it. False when the port has
// no remote or SDL has not delivered a sample yet.
bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]);
// Accelerometer zero-point calibration. SDL's Wii driver tries to read the
// remote's factory calibration block, but over Bluetooth that read often times
// out ("Using fallback accelerometer calibration" in console.log) and it falls
// back to a nominal 0x200 zero point; a real remote then carries a per-axis
// bias of up to ~0.2 g, which held sideways as a wheel is a permanent steering
// offset.
// StartAccelCalibration expects the remote lying still with the buttons up;
// Poll() then collects samples for about a second and stores the difference to
// the ideal (0, 1, 0) g in Config.toml.
void StartAccelCalibration(uint32_t chan);
// Forgets the stored correction.
void ClearAccelCalibration();
bool IsAccelCalibrating();
// 0..1 while a calibration is collecting samples.
float AccelCalibrationProgress();
// Outcome of the last calibration attempt for the overlay, or nullptr.
const char* AccelCalibrationMessage();
// Reports "no controller" on the GameCube side for every port served through
// KPAD, so the game never sees the same remote twice. Runs after aurora's PADRead.
void HideRemotesFromPad(PADStatus* statuses, uint32_t count);
// Dolphin-style continuous scanning. SDL's HIDAPI Wii driver drops a remote on
// a failed Bluetooth read or an extension change and never re-adds it on its
// own. Poll() runs once per PADRead and, while no Wii controller is present,
// periodically forces SDL to re-enumerate HIDAPI by toggling the driver hint.
void Poll();
// Forces one re-enumeration right now (settings overlay "Rescan now").
void RescanNow();
// True while Poll() is looking for a remote (no Wii controller connected).
bool IsScanning();
// True where looking means periodic rescans; elsewhere Poll() waits for hotplug.
bool PeriodicRescanEnabled();
// Rescans issued since a Wii controller was last seen.
uint32_t ScanCount();
} // namespace WiiRemoteInput
+466
View File
@@ -0,0 +1,466 @@
#include "discord_presence.h"
#include "runtime_log.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <optional>
#include <sstream>
#include <string_view>
#include <utility>
#include <vector>
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#else
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#endif
namespace DiscordPresence {
namespace {
constexpr uint32_t kHandshakeOpcode = 0;
constexpr uint32_t kFrameOpcode = 1;
constexpr size_t kMaxClientIdLength = 32;
constexpr auto kConnectionRetryCooldown = std::chrono::seconds(1);
bool IsClientId(std::string_view value) {
return !value.empty() && value.size() <= kMaxClientIdLength &&
std::all_of(value.begin(), value.end(), [](unsigned char ch) { return std::isdigit(ch) != 0; });
}
std::string EscapeJson(std::string_view value) {
std::string escaped;
escaped.reserve(value.size());
for (const unsigned char ch : value) {
switch (ch) {
case '\\': escaped += "\\\\"; break;
case '\"': escaped += "\\\""; break;
case '\b': escaped += "\\b"; break;
case '\f': escaped += "\\f"; break;
case '\n': escaped += "\\n"; break;
case '\r': escaped += "\\r"; break;
case '\t': escaped += "\\t"; break;
default:
if (ch < 0x20) {
static constexpr char kHex[] = "0123456789abcdef";
escaped += "\\u00";
escaped += kHex[ch >> 4];
escaped += kHex[ch & 0x0f];
} else {
escaped += static_cast<char>(ch);
}
}
}
return escaped;
}
void AppendJsonString(std::ostringstream& output, bool& hasValue, std::string_view key, std::string_view value) {
if (value.empty()) {
return;
}
if (hasValue) {
output << ',';
}
output << '\"' << key << "\":\"" << EscapeJson(value) << '\"';
hasValue = true;
}
std::string BuildActivityPayload(const Activity& activity) {
std::ostringstream json;
json << "{\"cmd\":\"SET_ACTIVITY\",\"nonce\":\"wiicompiled\",\"args\":{\"pid\":";
#if defined(_WIN32)
json << static_cast<unsigned long>(::GetCurrentProcessId());
#else
json << static_cast<long>(::getpid());
#endif
json << ",\"activity\":{";
bool hasActivityField = false;
AppendJsonString(json, hasActivityField, "details", activity.details);
AppendJsonString(json, hasActivityField, "state", activity.state);
const auto appendSection = [&](std::string_view name, const auto& append) {
if (hasActivityField) {
json << ',';
}
json << '\"' << name << "\":{";
append();
json << '}';
hasActivityField = true;
};
const bool hasAssets = !activity.largeImageKey.empty() || !activity.largeImageText.empty() ||
!activity.smallImageKey.empty() || !activity.smallImageText.empty();
if (hasAssets) {
appendSection("assets", [&] {
bool hasAsset = false;
AppendJsonString(json, hasAsset, "large_image", activity.largeImageKey);
AppendJsonString(json, hasAsset, "large_text", activity.largeImageText);
AppendJsonString(json, hasAsset, "small_image", activity.smallImageKey);
AppendJsonString(json, hasAsset, "small_text", activity.smallImageText);
});
}
if (activity.startTimestamp > 0 || activity.endTimestamp > 0) {
appendSection("timestamps", [&] {
if (activity.startTimestamp > 0) {
json << "\"start\":" << activity.startTimestamp;
}
if (activity.endTimestamp > 0) {
if (activity.startTimestamp > 0) {
json << ',';
}
json << "\"end\":" << activity.endTimestamp;
}
});
}
if (activity.partySize > 0 && activity.partyMax > 0) {
appendSection("party", [&] {
json << "\"size\":[" << activity.partySize << ',' << activity.partyMax << ']';
});
}
if (hasActivityField) {
json << ',';
}
json << "\"instance\":false}}}";
return json.str();
}
class Client {
public:
void Initialize(const std::string& clientId, const std::string& title) {
std::lock_guard<std::mutex> lock(mutex_);
basicClientId_ = IsClientId(clientId) ? clientId : std::string{};
basicActivity_ = {};
basicActivity_.details = title;
basicActivity_.startTimestamp = UnixSeconds();
customClient_ = false;
activity_ = basicActivity_;
ReconnectAndSendLocked();
}
void SetClient(const std::string& clientId) {
std::lock_guard<std::mutex> lock(mutex_);
if (!IsClientId(clientId)) {
RT_LOG(RT_TAG_RUNTIME) << "Ignoring invalid Discord client ID from /dev/dolphin" << std::endl;
return;
}
if (clientId_ == clientId && connected_) {
return;
}
customClient_ = true;
clientId_ = clientId;
CloseLocked();
ReconnectAndSendLocked();
}
void SetActivity(Activity activity) {
std::lock_guard<std::mutex> lock(mutex_);
if (!customClient_) {
return;
}
activity_ = std::move(activity);
SendActivityLocked();
}
void Reset() {
std::lock_guard<std::mutex> lock(mutex_);
customClient_ = false;
clientId_.clear();
activity_ = basicActivity_;
CloseLocked();
ReconnectAndSendLocked();
}
void Shutdown() {
std::lock_guard<std::mutex> lock(mutex_);
CloseLocked();
}
private:
static int64_t UnixSeconds() {
return std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
}
std::string ActiveClientIdLocked() const {
return customClient_ ? clientId_ : basicClientId_;
}
bool WriteFrameLocked(uint32_t opcode, std::string_view payload) {
std::array<uint8_t, 8> header{};
const uint32_t size = static_cast<uint32_t>(payload.size());
for (size_t i = 0; i < 4; ++i) {
header[i] = static_cast<uint8_t>(opcode >> (i * 8));
header[4 + i] = static_cast<uint8_t>(size >> (i * 8));
}
return WriteAllLocked(header.data(), header.size()) &&
WriteAllLocked(reinterpret_cast<const uint8_t*>(payload.data()), payload.size());
}
void ReconnectAndSendLocked() {
const std::string clientId = ActiveClientIdLocked();
if (clientId.empty() || !ConnectLocked()) {
return;
}
const std::string handshake = "{\"v\":1,\"client_id\":\"" + clientId + "\"}";
if (!WriteFrameLocked(kHandshakeOpcode, handshake)) {
RecordFailedConnectionLocked();
CloseLocked();
return;
}
if (!ReadReadyLocked()) {
RecordFailedConnectionLocked();
CloseLocked();
return;
}
SendActivityLocked();
}
void SendActivityLocked() {
if (!connected_) {
ReconnectAndSendLocked();
return;
}
if (ActiveClientIdLocked().empty()) {
return;
}
if (!WriteFrameLocked(kFrameOpcode, BuildActivityPayload(activity_))) {
RecordFailedConnectionLocked();
CloseLocked();
}
}
bool ConnectionRetryAllowedLocked() const {
return !lastFailedConnectionAttempt_ ||
std::chrono::steady_clock::now() - *lastFailedConnectionAttempt_ >= kConnectionRetryCooldown;
}
void RecordFailedConnectionLocked() {
lastFailedConnectionAttempt_ = std::chrono::steady_clock::now();
}
#if defined(_WIN32)
bool ConnectLocked() {
if (connected_) {
return true;
}
if (!ConnectionRetryAllowedLocked()) {
return false;
}
for (unsigned int index = 0; index < 10; ++index) {
const std::string name = "\\\\.\\pipe\\discord-ipc-" + std::to_string(index);
handle_ = ::CreateFileA(name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (handle_ != INVALID_HANDLE_VALUE) {
DWORD mode = PIPE_READMODE_BYTE;
::SetNamedPipeHandleState(handle_, &mode, nullptr, nullptr);
connected_ = true;
lastFailedConnectionAttempt_.reset();
return true;
}
}
RecordFailedConnectionLocked();
return false;
}
bool WriteAllLocked(const uint8_t* data, size_t size) {
while (size != 0) {
DWORD written = 0;
if (!::WriteFile(handle_, data, static_cast<DWORD>(size), &written, nullptr) || written == 0) {
return false;
}
data += written;
size -= written;
}
return true;
}
bool ReadAllLocked(uint8_t* data, size_t size) {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1);
while (size != 0) {
if (std::chrono::steady_clock::now() >= deadline) {
return false;
}
DWORD available = 0;
if (!::PeekNamedPipe(handle_, nullptr, 0, nullptr, &available, nullptr)) {
return false;
}
if (available == 0) {
if (std::chrono::steady_clock::now() >= deadline) {
return false;
}
::Sleep(10);
continue;
}
DWORD read = 0;
const DWORD requested = static_cast<DWORD>(std::min<size_t>(size, available));
if (!::ReadFile(handle_, data, requested, &read, nullptr) || read == 0) {
return false;
}
data += read;
size -= read;
}
return true;
}
void CloseLocked() {
if (handle_ != INVALID_HANDLE_VALUE) {
::CloseHandle(handle_);
handle_ = INVALID_HANDLE_VALUE;
}
connected_ = false;
}
HANDLE handle_ = INVALID_HANDLE_VALUE;
#else
bool ConnectLocked() {
if (connected_) {
return true;
}
if (!ConnectionRetryAllowedLocked()) {
return false;
}
std::array<std::string, 5> roots{};
size_t rootCount = 0;
if (const char* runtimeDir = std::getenv("XDG_RUNTIME_DIR"); runtimeDir && *runtimeDir) {
roots[rootCount++] = runtimeDir;
}
if (const char* tempDir = std::getenv("TMPDIR"); tempDir && *tempDir) {
roots[rootCount++] = tempDir;
}
if (const char* tempDir = std::getenv("TMP"); tempDir && *tempDir && rootCount < roots.size()) {
roots[rootCount++] = tempDir;
}
if (const char* tempDir = std::getenv("TEMP"); tempDir && *tempDir && rootCount < roots.size()) {
roots[rootCount++] = tempDir;
}
roots[rootCount++] = "/tmp";
for (size_t rootIndex = 0; rootIndex < rootCount; ++rootIndex) {
for (unsigned int index = 0; index < 10; ++index) {
const std::string path = roots[rootIndex] + "/discord-ipc-" + std::to_string(index);
if (path.size() >= sizeof(sockaddr_un::sun_path)) {
continue;
}
const int socketFd = ::socket(AF_UNIX, SOCK_STREAM, 0);
if (socketFd < 0) {
continue;
}
sockaddr_un address{};
address.sun_family = AF_UNIX;
std::memcpy(address.sun_path, path.c_str(), path.size() + 1);
if (::connect(socketFd, reinterpret_cast<const sockaddr*>(&address), sizeof(address)) == 0) {
timeval timeout{};
timeout.tv_sec = 1;
::setsockopt(socketFd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
fd_ = socketFd;
connected_ = true;
lastFailedConnectionAttempt_.reset();
return true;
}
::close(socketFd);
}
}
RecordFailedConnectionLocked();
return false;
}
bool WriteAllLocked(const uint8_t* data, size_t size) {
while (size != 0) {
const ssize_t written = ::send(fd_, data, size, MSG_NOSIGNAL);
if (written <= 0) {
return false;
}
data += written;
size -= static_cast<size_t>(written);
}
return true;
}
bool ReadAllLocked(uint8_t* data, size_t size) {
while (size != 0) {
const ssize_t read = ::recv(fd_, data, size, 0);
if (read <= 0) {
return false;
}
data += read;
size -= static_cast<size_t>(read);
}
return true;
}
void CloseLocked() {
if (fd_ >= 0) {
::close(fd_);
fd_ = -1;
}
connected_ = false;
}
int fd_ = -1;
#endif
bool ReadReadyLocked() {
std::array<uint8_t, 8> header{};
if (!ReadAllLocked(header.data(), header.size())) {
return false;
}
uint32_t opcode = 0;
uint32_t size = 0;
for (size_t i = 0; i < 4; ++i) {
opcode |= static_cast<uint32_t>(header[i]) << (i * 8);
size |= static_cast<uint32_t>(header[4 + i]) << (i * 8);
}
if (opcode != kFrameOpcode || size > 1024 * 1024) {
return false;
}
std::vector<uint8_t> payload(size);
if (!ReadAllLocked(payload.data(), payload.size())) {
return false;
}
const std::string_view json(reinterpret_cast<const char*>(payload.data()), payload.size());
return json.find("\"evt\":\"READY\"") != std::string_view::npos;
}
std::mutex mutex_;
bool connected_ = false;
bool customClient_ = false;
std::optional<std::chrono::steady_clock::time_point> lastFailedConnectionAttempt_;
std::string basicClientId_;
std::string clientId_;
Activity basicActivity_;
Activity activity_;
};
Client g_client;
} // namespace
void Initialize(const std::string& basicClientId, const std::string& basicTitle) {
g_client.Initialize(basicClientId, basicTitle);
}
void SetClient(const std::string& clientId) {
g_client.SetClient(clientId);
}
void SetActivity(Activity activity) {
g_client.SetActivity(std::move(activity));
}
void Reset() {
g_client.Reset();
}
void Shutdown() {
g_client.Shutdown();
}
} // namespace DiscordPresence
+29 -140
View File
@@ -2,6 +2,7 @@
#include "memory.h" #include "memory.h"
#include "abi_bridge.h" #include "abi_bridge.h"
#include "hle_stubs.h" #include "hle_stubs.h"
#include "host_context.h"
#include "runtime_log.h" #include "runtime_log.h"
// Defined in hle/os/os_sleep.cpp; the sleep-timer table is file-local there. // Defined in hle/os/os_sleep.cpp; the sleep-timer table is file-local there.
@@ -13,24 +14,8 @@
#include <iomanip> #include <iomanip>
#include <sstream> #include <sstream>
#if !defined(_WIN32)
#include "libco.h"
#endif
namespace Fiber { namespace Fiber {
#if !defined(_WIN32)
namespace {
// libco's co_create() entry points take no argument, unlike CreateFiber(size, FiberProc, param).
// CreateGuestFiber() stages the guest thread address here immediately before the first co_switch
// into a freshly created cothread; FiberProcTrampoline reads it exactly once, at the top of the
// fiber's very first activation. Safe because guest fibers are strictly cooperative on a single
// OS thread: nothing else can run (and so nothing else can overwrite this) between the staging
// write and the trampoline's read of it.
thread_local uint32_t s_pendingFiberArg = 0;
} // namespace
#endif
std::mutex GuestFiberManager::s_mutex; std::mutex GuestFiberManager::s_mutex;
std::unordered_map<uint32_t, GuestFiber> GuestFiberManager::s_fibers; std::unordered_map<uint32_t, GuestFiber> GuestFiberManager::s_fibers;
std::vector<void*> GuestFiberManager::s_fibersPendingDelete; std::vector<void*> GuestFiberManager::s_fibersPendingDelete;
@@ -45,21 +30,11 @@ void GuestFiberManager::PurgePendingFibers() {
std::lock_guard<std::mutex> lock(s_mutex); std::lock_guard<std::mutex> lock(s_mutex);
toDelete.swap(s_fibersPendingDelete); toDelete.swap(s_fibersPendingDelete);
} }
#if defined(_WIN32)
const void* current = GetCurrentFiber();
for (void* f : toDelete) { for (void* f : toDelete) {
if (f && f != current) { if (f && !HostContext::IsCurrent(f)) {
DeleteFiber(f); HostContext::Destroy(f);
} }
} }
#else
const void* current = co_active();
for (void* f : toDelete) {
if (f && f != current) {
co_delete(static_cast<cothread_t>(f));
}
}
#endif
} }
// Global VI retrace counter // Global VI retrace counter
@@ -212,27 +187,13 @@ void GuestFiberManager::Initialize() {
return; return;
} }
#if defined(_WIN32) if (!HostContext::InitializeScheduler(&s_schedulerFiber)) {
// Convert the main thread to a fiber (the scheduler fiber) RT_LOG(RT_TAG_OS) << "FATAL: Failed to initialize scheduler context!" << std::endl;
s_schedulerFiber = ConvertThreadToFiber(nullptr); ShowRuntimeFatalPopup("guest scheduler initialization failed",
if (!s_schedulerFiber) { "The host could not create the scheduler context required to run guest threads.");
// May already be a fiber std::abort();
s_schedulerFiber = GetCurrentFiber();
if (!s_schedulerFiber) {
RT_LOG(RT_TAG_OS) << "FATAL: Failed to initialize scheduler fiber!" << std::endl;
ShowRuntimeFatalPopup("guest scheduler initialization failed",
"Windows could not create the scheduler fiber required to run guest threads.");
std::abort();
}
} }
#else
// co_active() returns a handle for whichever native stack is currently running, creating one
// on first call if needed - the libco analogue of ConvertThreadToFiber(nullptr): it converts
// this call's own stack into a switchable target without altering control flow.
s_schedulerFiber = co_active();
#endif
s_currentGuestThread = 0; s_currentGuestThread = 0;
s_initialized = true; s_initialized = true;
} }
@@ -240,32 +201,18 @@ void GuestFiberManager::Initialize() {
void GuestFiberManager::Shutdown() { void GuestFiberManager::Shutdown() {
std::lock_guard<std::mutex> lock(s_mutex); std::lock_guard<std::mutex> lock(s_mutex);
#if defined(_WIN32)
for (auto& [addr, fiber] : s_fibers) { for (auto& [addr, fiber] : s_fibers) {
if (fiber.fiber && !fiber.isSchedulerFiber) { if (fiber.fiber && !fiber.isSchedulerFiber) {
DeleteFiber(fiber.fiber); HostContext::Destroy(fiber.fiber);
fiber.fiber = nullptr; fiber.fiber = nullptr;
} }
} }
s_fibers.clear(); s_fibers.clear();
// Convert scheduler fiber back to thread
if (s_schedulerFiber) { if (s_schedulerFiber) {
ConvertFiberToThread(); HostContext::ShutdownScheduler(s_schedulerFiber);
s_schedulerFiber = nullptr; s_schedulerFiber = nullptr;
} }
#else
for (auto& [addr, fiber] : s_fibers) {
if (fiber.fiber && !fiber.isSchedulerFiber) {
co_delete(static_cast<cothread_t>(fiber.fiber));
fiber.fiber = nullptr;
}
}
s_fibers.clear();
// Unlike ConvertFiberToThread, libco has no "undo" for co_active(): the scheduler's own
// stack was never separately allocated, so there is nothing to release here.
s_schedulerFiber = nullptr;
#endif
s_initialized = false; s_initialized = false;
} }
@@ -288,11 +235,7 @@ bool GuestFiberManager::CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entr
if (existingIt != s_fibers.end()) { if (existingIt != s_fibers.end()) {
// Delete the old fiber if it exists and is not the scheduler fiber // Delete the old fiber if it exists and is not the scheduler fiber
if (existingIt->second.fiber && !existingIt->second.isSchedulerFiber) { if (existingIt->second.fiber && !existingIt->second.isSchedulerFiber) {
#if defined(_WIN32) HostContext::Destroy(existingIt->second.fiber);
DeleteFiber(existingIt->second.fiber);
#else
co_delete(static_cast<cothread_t>(existingIt->second.fiber));
#endif
} }
s_fibers.erase(existingIt); s_fibers.erase(existingIt);
} }
@@ -312,31 +255,21 @@ bool GuestFiberManager::CreateGuestFiber(uint32_t guestThreadAddr, uint32_t entr
gf.cpuContext.pc = entryPoint; gf.cpuContext.pc = entryPoint;
gf.cpuContext.srr0 = entryPoint; gf.cpuContext.srr0 = entryPoint;
#if defined(_WIN32) // The host stack models only translated host calls; the guest stack starts
// Create Windows fiber with reasonable stack size // at stackBase in the CPU context above. 64 KiB is too small for deep
// Use host stack size (64KB should be plenty for translated code) // translated/HLE call chains (notably NW4R's sound worker), and on macOS
constexpr size_t kHostStackSize = 64 * 1024; // it can exhaust the guarded coroutine stack as unrelated host work (such
gf.fiber = CreateFiber(kHostStackSize, FiberProc, reinterpret_cast<void*>(static_cast<uintptr_t>(guestThreadAddr))); // as a window resize) adds a little more nesting. Keep enough headroom for
// those chains while the guest stack remains separately bounded.
constexpr size_t kHostStackSize = 1024 * 1024;
gf.fiber = HostContext::Create(kHostStackSize, FiberProc,
reinterpret_cast<void*>(static_cast<uintptr_t>(guestThreadAddr)));
if (!gf.fiber) { if (!gf.fiber) {
DWORD err = GetLastError(); RT_LOG(RT_TAG_OS) << "Failed to create host context for thread 0x"
RT_LOG(RT_TAG_OS) << "CreateFiber failed for thread 0x"
<< std::hex << guestThreadAddr
<< " error=" << std::dec << err << std::endl;
return false;
}
#else
// libco's co_create() entry point takes no argument; SwitchToThread() stages guestThreadAddr
// into s_pendingFiberArg immediately before the co_switch that first activates this handle.
constexpr unsigned int kHostStackSize = 64 * 1024;
gf.fiber = co_create(kHostStackSize, &FiberProcTrampoline);
if (!gf.fiber) {
RT_LOG(RT_TAG_OS) << "co_create failed for thread 0x"
<< std::hex << guestThreadAddr << std::dec << std::endl; << std::hex << guestThreadAddr << std::dec << std::endl;
return false; return false;
} }
#endif
s_fibers[guestThreadAddr] = gf; s_fibers[guestThreadAddr] = gf;
@@ -390,24 +323,11 @@ void GuestFiberManager::ExitGuestThread(uint32_t guestThreadAddr, ThreadState fi
} }
if (it->second.fiber && !it->second.isSchedulerFiber) { if (it->second.fiber && !it->second.isSchedulerFiber) {
#if defined(_WIN32) if (HostContext::IsCurrent(it->second.fiber)) {
const void* current = GetCurrentFiber();
if (it->second.fiber == current) {
s_fibersPendingDelete.push_back(it->second.fiber); s_fibersPendingDelete.push_back(it->second.fiber);
} else { } else {
DeleteFiber(it->second.fiber); HostContext::Destroy(it->second.fiber);
} }
#else
const void* current = co_active();
if (it->second.fiber == current) {
// Deleting the coroutine we're currently executing on would free the very stack
// this call is running on; defer it (PurgePendingFibers) until some other fiber is
// active, exactly like the Windows branch above.
s_fibersPendingDelete.push_back(it->second.fiber);
} else {
co_delete(static_cast<cothread_t>(it->second.fiber));
}
#endif
it->second.fiber = nullptr; it->second.fiber = nullptr;
} }
} }
@@ -474,12 +394,7 @@ void GuestFiberManager::SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu
// Check if we're already on the target fiber (e.g., switching to main thread // Check if we're already on the target fiber (e.g., switching to main thread
// when we're already on the scheduler fiber) // when we're already on the scheduler fiber)
#if defined(_WIN32) if (HostContext::IsCurrent(fiberHandle)) {
void* currentFiber = GetCurrentFiber();
#else
void* currentFiber = co_active();
#endif
if (currentFiber == fiberHandle) {
// Already executing on the target host fiber. This is common for the // Already executing on the target host fiber. This is common for the
// default guest thread, which also owns the scheduler fiber. Keep the // default guest thread, which also owns the scheduler fiber. Keep the
// live CPU context instead of restoring a possibly stale saved copy // live CPU context instead of restoring a possibly stale saved copy
@@ -496,15 +411,7 @@ void GuestFiberManager::SwitchToThread(uint32_t guestThreadAddr, CpuContext* cpu
} }
// Switch to the target fiber (the target fiber will load its own context) // Switch to the target fiber (the target fiber will load its own context)
#if defined(_WIN32) HostContext::Switch(fiberHandle);
SwitchToFiber(fiberHandle);
#else
// Staged for FiberProcTrampoline's first (and only) read; a no-op for a fiber that has
// already started, since resuming it re-enters mid-function rather than through the
// trampoline's entry point.
s_pendingFiberArg = guestThreadAddr;
co_switch(static_cast<cothread_t>(fiberHandle));
#endif
// When we return here, the fiber that issued SwitchToThread has resumed. // When we return here, the fiber that issued SwitchToThread has resumed.
// That does not automatically mean the previous guest thread became runnable // That does not automatically mean the previous guest thread became runnable
@@ -618,14 +525,6 @@ void GuestFiberManager::ProcessTimerEvents(CpuContext* cpu) {
} }
} }
void GuestFiberManager::SwitchToScheduler() {
#if defined(_WIN32)
SwitchToFiber(s_schedulerFiber);
#else
co_switch(static_cast<cothread_t>(s_schedulerFiber));
#endif
}
#if defined(_WIN32) #if defined(_WIN32)
void CALLBACK GuestFiberManager::FiberProc(void* param) void CALLBACK GuestFiberManager::FiberProc(void* param)
#else #else
@@ -634,6 +533,7 @@ void GuestFiberManager::FiberProc(void* param)
{ {
uint32_t guestThreadAddr = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(param)); uint32_t guestThreadAddr = static_cast<uint32_t>(reinterpret_cast<uintptr_t>(param));
// Get our fiber info // Get our fiber info
GuestFiber* fiber = nullptr; GuestFiber* fiber = nullptr;
uint32_t entryPoint = 0; uint32_t entryPoint = 0;
@@ -644,7 +544,7 @@ void GuestFiberManager::FiberProc(void* param)
auto it = s_fibers.find(guestThreadAddr); auto it = s_fibers.find(guestThreadAddr);
if (it == s_fibers.end()) { if (it == s_fibers.end()) {
RT_LOG(RT_TAG_OS) << "FiberProc: fiber not found!" << std::endl; RT_LOG(RT_TAG_OS) << "FiberProc: fiber not found!" << std::endl;
SwitchToScheduler(); HostContext::Switch(s_schedulerFiber);
return; return;
} }
fiber = &it->second; fiber = &it->second;
@@ -707,7 +607,7 @@ void GuestFiberManager::FiberProc(void* param)
<< ", fn=0x" << startFn << ") after retries; continuing anyway." << std::dec << std::endl; << ", fn=0x" << startFn << ") after retries; continuing anyway." << std::dec << std::endl;
break; break;
} }
SwitchToScheduler(); HostContext::Switch(s_schedulerFiber);
} }
// The deferral loop above yields to the scheduler and therefore can resume // The deferral loop above yields to the scheduler and therefore can resume
@@ -764,18 +664,7 @@ void GuestFiberManager::FiberProc(void* param)
} }
// Return to scheduler // Return to scheduler
SwitchToScheduler(); HostContext::Switch(s_schedulerFiber);
} }
#if !defined(_WIN32)
void GuestFiberManager::FiberProcTrampoline() {
const uint32_t guestThreadAddr = s_pendingFiberArg;
FiberProc(reinterpret_cast<void*>(static_cast<uintptr_t>(guestThreadAddr)));
// FiberProc always calls SwitchToScheduler() on every exit path and never falls off its own
// end; this is only a safety net in case that ever changes; falling off co_create's entry
// function is otherwise undefined behavior (libco's own crash() fallback aborts instead).
SwitchToScheduler();
}
#endif
} // namespace Fiber } // namespace Fiber
+27 -6
View File
@@ -36,6 +36,9 @@
#endif #endif
namespace GuestFlat { namespace GuestFlat {
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
bool g_requiresCheckedAccess = false;
#endif
namespace { namespace {
#if defined(_WIN32) #if defined(_WIN32)
@@ -48,6 +51,16 @@ constexpr DWORD kMemPreservePlaceholder = 0x00000002;
constexpr size_t kAllocationGranularity = 0x10000; // 64 KiB constexpr size_t kAllocationGranularity = 0x10000; // 64 KiB
constexpr size_t kHostPageSize = 0x1000; constexpr size_t kHostPageSize = 0x1000;
// Only hosts that can expose a page larger than 4 KiB need to discover their
// size at runtime; see RequiresCheckedAccess() in guest_flat_memory.h.
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
size_t HostPageSize()
{
const long size = sysconf(_SC_PAGESIZE);
return size > 0 ? static_cast<size_t>(size) : kGuestPageSize;
}
#endif
// Named, platform-neutral protection modes so every fault-interception call site below (the // Named, platform-neutral protection modes so every fault-interception call site below (the
// MMIO window, the executable-write guard, deferred-EFB-read protection, the on-demand // MMIO window, the executable-write guard, deferred-EFB-read protection, the on-demand
// unmapped-block commit) can stay identical text on both platforms; only ProtectRange() and // unmapped-block commit) can stay identical text on both platforms; only ProtectRange() and
@@ -273,8 +286,12 @@ void EnsureReservation() {
std::ostringstream oss; std::ostringstream oss;
oss << "Unable to reserve the 4 GiB flat guest address space at 0x" << std::hex oss << "Unable to reserve the 4 GiB flat guest address space at 0x" << std::hex
<< reinterpret_cast<uintptr_t>(requested) << std::dec << reinterpret_cast<uintptr_t>(requested) << std::dec
<< ". Something else in this process already occupies part of the 16 TiB region - " << ". Either something else in this process already occupies that address (an "
"an injected library, an overlay or a debugging tool is the usual cause."; "injected library, an overlay or a debugging tool is the usual cause), or this "
"kernel's virtual address space does not reach that high (common on some 32-bit-"
"userspace-compatible or older AArch64 configurations, e.g. a kernel built for "
"39-bit virtual addresses) - in the latter case mmap() silently substitutes an "
"address near the top of the space it does have instead of honoring the request.";
throw std::runtime_error(oss.str()); throw std::runtime_error(oss.str());
} }
#endif #endif
@@ -345,7 +362,7 @@ bool IsMmio(uint32_t address) { return MemoryInline::IsMmioAddress(address); }
bool IsGpuFifo(uint32_t address) { return MemoryInline::IsGpuFifoAddress(address); } bool IsGpuFifo(uint32_t address) { return MemoryInline::IsGpuFifoAddress(address); }
void ApplyExecutableProtectionLocked() { void ApplyExecutableProtectionLocked() {
if (g_base == nullptr) return; if (g_base == nullptr || RequiresCheckedAccess()) return;
auto& protectedPages = ExecutableProtectedPages(); auto& protectedPages = ExecutableProtectedPages();
for (const auto& range : ExecutableRanges()) { for (const auto& range : ExecutableRanges()) {
// Only pages fully inside the range are protected: edge pages often share a page with data // Only pages fully inside the range are protected: edge pages often share a page with data
@@ -493,6 +510,10 @@ bool IsActive() {
void Initialize(const std::vector<RegionRequest>& regions) { void Initialize(const std::vector<RegionRequest>& regions) {
std::lock_guard<std::mutex> lock(StateMutex()); std::lock_guard<std::mutex> lock(StateMutex());
#if !defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
g_requiresCheckedAccess = HostPageSize() > kGuestPageSize;
#endif
if (g_initialized) { if (g_initialized) {
if (!SameLayout(g_activeRegions, regions)) { if (!SameLayout(g_activeRegions, regions)) {
throw std::runtime_error( throw std::runtime_error(
@@ -611,7 +632,7 @@ uint8_t* HostPointer(uint32_t guestAddress) {
} }
void ProtectDeferredRange(uint32_t address, size_t length) { void ProtectDeferredRange(uint32_t address, size_t length) {
if (!g_initialized || length == 0) return; if (RequiresCheckedAccess() || !g_initialized || length == 0) return;
const uint64_t end = static_cast<uint64_t>(address) + length; const uint64_t end = static_cast<uint64_t>(address) + length;
if (end > kGuestSpaceSize) return; if (end > kGuestSpaceSize) return;
std::lock_guard<std::mutex> lock(StateMutex()); std::lock_guard<std::mutex> lock(StateMutex());
@@ -626,7 +647,7 @@ void ProtectDeferredRange(uint32_t address, size_t length) {
} }
void UnprotectDeferredRange(uint32_t address, size_t length) { void UnprotectDeferredRange(uint32_t address, size_t length) {
if (!g_initialized || length == 0) return; if (RequiresCheckedAccess() || !g_initialized || length == 0) return;
std::lock_guard<std::mutex> lock(StateMutex()); std::lock_guard<std::mutex> lock(StateMutex());
auto& ranges = DeferredRanges(); auto& ranges = DeferredRanges();
const uint64_t end = static_cast<uint64_t>(address) + length; const uint64_t end = static_cast<uint64_t>(address) + length;
@@ -641,7 +662,7 @@ void UnprotectDeferredRange(uint32_t address, size_t length) {
} }
void RegisterExecutableRange(uint32_t start, uint32_t end) { void RegisterExecutableRange(uint32_t start, uint32_t end) {
if (end <= start) return; if (RequiresCheckedAccess() || end <= start) return;
std::lock_guard<std::mutex> lock(StateMutex()); std::lock_guard<std::mutex> lock(StateMutex());
auto& ranges = ExecutableRanges(); auto& ranges = ExecutableRanges();
if (std::any_of(ranges.begin(), ranges.end(), [&](const GuardedRange& range) { if (std::any_of(ranges.begin(), ranges.end(), [&](const GuardedRange& range) {
+80
View File
@@ -0,0 +1,80 @@
#include "guest_flat_memory.h"
#include <mach/mach.h>
#include <mach/mach_vm.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <algorithm>
#include <cstdio>
#include <mutex>
#include <stdexcept>
#include <vector>
namespace GuestFlat {
bool g_requiresCheckedAccess = false;
namespace {
struct Mapping { uint32_t base; uint64_t size; uint8_t* host; };
std::mutex g_mutex;
std::vector<Mapping> g_mappings;
std::vector<RegionRequest> g_layout;
uint8_t* g_base = nullptr;
bool g_active = false;
uint64_t Offset(const RegionRequest& r) {
if (r.backing == Backing::Mem1) return r.base & 0x1fffffffu;
if (r.backing == Backing::Mem2) return (r.base & 0x1fffffffu) - 0x10000000u;
return 0;
}
bool Same(const std::vector<RegionRequest>& a, const std::vector<RegionRequest>& b) {
return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(),
[](const auto& x, const auto& y) { return x.base == y.base && x.size == y.size && x.backing == y.backing; });
}
int BackingFile(size_t size) {
char name[] = "/tmp/wiicompiled-guest-XXXXXX";
const int fd = mkstemp(name);
if (fd >= 0) { unlink(name); if (ftruncate(fd, static_cast<off_t>(size)) != 0) { close(fd); return -1; } }
return fd;
}
} // namespace
bool IsActive() { return g_active; }
void Initialize(const std::vector<RegionRequest>& regions) {
std::lock_guard lock(g_mutex);
g_requiresCheckedAccess = static_cast<size_t>(getpagesize()) > kGuestPageSize;
if (g_active) { if (!Same(g_layout, regions)) throw std::runtime_error("flat guest layout cannot be remapped"); return; }
mach_vm_address_t address = kFixedFlatGuestBase;
if (mach_vm_allocate(mach_task_self(), &address, kGuestSpaceSize, VM_FLAGS_FIXED) != KERN_SUCCESS || address != kFixedFlatGuestBase)
throw std::runtime_error("unable to reserve fixed 4 GiB macOS guest address space");
g_base = reinterpret_cast<uint8_t*>(address);
struct Store { Backing kind; uint32_t owned; uint64_t size; int fd; };
std::vector<Store> stores;
for (const auto& r : regions) {
if (!r.size) continue;
const uint32_t owned = r.backing == Backing::Owned ? r.base : 0;
auto it = std::find_if(stores.begin(), stores.end(), [&](const Store& s) { return s.kind == r.backing && s.owned == owned; });
const uint64_t need = Offset(r) + r.size;
if (it == stores.end()) stores.push_back({r.backing, owned, need, -1}); else it->size = std::max(it->size, need);
}
for (auto& s : stores) { s.fd = BackingFile(s.size); if (s.fd < 0) throw std::runtime_error("unable to create macOS guest backing store"); }
for (const auto& r : regions) {
if (!r.size) continue;
const uint32_t owned = r.backing == Backing::Owned ? r.base : 0;
const auto& s = *std::find_if(stores.begin(), stores.end(), [&](const Store& x) { return x.kind == r.backing && x.owned == owned; });
auto* host = static_cast<uint8_t*>(mmap(nullptr, r.size, PROT_READ | PROT_WRITE, MAP_SHARED, s.fd, Offset(r)));
auto* guest = mmap(g_base + r.base, r.size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, s.fd, Offset(r));
if (host == MAP_FAILED || guest != g_base + r.base) throw std::runtime_error("unable to map macOS guest alias");
g_mappings.push_back({r.base, r.size, host});
}
for (auto& s : stores) close(s.fd);
g_layout = regions; g_active = true;
}
uint8_t* HostPointer(uint32_t a) { for (const auto& m : g_mappings) if (a >= m.base && uint64_t(a - m.base) < m.size) return m.host + (a - m.base); return nullptr; }
void ProtectDeferredRange(uint32_t, size_t) {}
void UnprotectDeferredRange(uint32_t, size_t) {}
void RegisterExecutableRange(uint32_t, uint32_t) {}
FaultCounters Counters() { return {}; }
void LogFaultSummary() noexcept {}
bool HandleAccessViolation(void*, bool) noexcept { return false; }
} // namespace GuestFlat
+1
View File
@@ -394,3 +394,4 @@ extern std::mutex g_tlutObjMutex;
// that happens on another key: unordered_map keeps references valid across a // that happens on another key: unordered_map keeps references valid across a
// rehash, an open-addressed table would not. // rehash, an open-addressed table would not.
extern std::unordered_map<uint32_t, TexObjSlot> g_TexObjMeta; extern std::unordered_map<uint32_t, TexObjSlot> g_TexObjMeta;
extern std::map<uint32_t, TlutObjMeta> g_TlutObjMeta;
+9
View File
@@ -33,6 +33,15 @@ void WriteGuestFloat(uint32_t addr, float value, const char* label) {
void* GuestToHostPtr(uint32_t addr, size_t len) { void* GuestToHostPtr(uint32_t addr, size_t len) {
if (addr == 0) return nullptr; if (addr == 0) return nullptr;
#if defined(__APPLE__)
// The macOS flat guest map exposes separate host aliases for cached,
// uncached, and physical MEM1/MEM2 addresses. GX resources are identified
// by their host pointer, so all aliases of one guest allocation must use
// the same physical mapping before they reach Aurora. Kept macOS-only:
// changing which alias the other hosts hand out would re-key their existing
// GX resource identity.
addr = CanonicalizeGxMainRamAddress(addr);
#endif
try { return Memory::GetPointer(addr, len); } catch (const Memory::AccessViolation& e) { LogMemoryError(RT_TAG_GX, "GX guest pointer", e); return nullptr; } try { return Memory::GetPointer(addr, len); } catch (const Memory::AccessViolation& e) { LogMemoryError(RT_TAG_GX, "GX guest pointer", e); return nullptr; }
} }
+234 -8
View File
@@ -1,23 +1,249 @@
#include "hle_stubs.h" #include "hle_stubs.h"
#include "memory.h"
#include "wii_remote_input.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint> #include <cstdint>
// KPAD HLE fed by a real Bluetooth Wii Remote. The game calls KPADRead once per
// frame with room for 16 KPADStatus entries and only looks at entry 0; with a
// Classic Controller it also calls KPADGetUnifiedWpadStatus for the raw
// WPADCLStatus (buttons, sticks and triggers of the extension).
namespace {
constexpr uint32_t kKpadStatusSize = 0x84;
// KPADStatus field offsets (RVL SDK).
constexpr uint32_t kHold = 0x00, kTrig = 0x04, kRelease = 0x08, kAcc = 0x0C, kAccValue = 0x18,
kAccSpeed = 0x1C, kPos = 0x20, kAccVertical = 0x54, kDevType = 0x5C, kWpadErr = 0x5D,
kDpdValidFg = 0x5E, kDataFormat = 0x5F, kFsStick = 0x60, kFsAcc = 0x68, kFsAccValue = 0x74,
kFsAccSpeed = 0x78;
// KPADStatus.ex_status.cl (KPADEXStatus, Classic Controller view).
constexpr uint32_t kClHold = 0x60, kClTrig = 0x64, kClRelease = 0x68, kClLStick = 0x6C, kClRStick = 0x74,
kClLTrigger = 0x7C, kClRTrigger = 0x80;
// KPADUnifiedWpadStatus: WPADStatus / WPADFSStatus / WPADCLStatus union, then fmt.
constexpr uint32_t kUnifiedSize = 0x38;
constexpr uint32_t kUButton = 0x00, kUAccX = 0x02, kUAccY = 0x04, kUAccZ = 0x06, kUObj = 0x08, kUDev = 0x28,
kUErr = 0x29, kUFsStickX = 0x2A, kUFsStickY = 0x2B, kUFsAccX = 0x2C, kUFsAccY = 0x2E,
kUFsAccZ = 0x30, kUClButton = 0x2A, kUClLStickX = 0x2C, kUClLStickY = 0x2E, kUClRStickX = 0x30,
kUClRStickY = 0x32, kUClTriggerL = 0x34, kUClTriggerR = 0x35, kUFmt = 0x36;
// WPAD device types (WPAD_DEV_*) and the data formats KPAD runs each of them
// in (WPAD_FMT_*_ACC_DPD): the values KPADStatus.dev_type / data_format and
// KPADUnifiedWpadStatus.dev / fmt carry on the console.
constexpr uint8_t kDevCore = 0;
constexpr uint8_t kDevFreestyle = 1;
constexpr uint8_t kDevClassic = 2;
constexpr uint8_t kFmtCoreAccDpd = 2;
constexpr uint8_t kFmtFreestyleAccDpd = 5;
constexpr uint8_t kFmtClassicAccDpd = 8;
constexpr int8_t kWpadErrNone = 0;
constexpr int8_t kWpadErrNoController = -1;
// Raw accelerometer as WPADStatus carries it: 10 bits, 0x200 at 0 g, 100 per g.
constexpr float kRawAccZero = 512.0f;
constexpr float kRawAccPerG = 100.0f;
struct ChannelState {
uint32_t prevHold = 0;
uint32_t prevClHold = 0;
float prevAcc[3] = {0.0f, -1.0f, 0.0f};
float prevFsAcc[3] = {0.0f, -1.0f, 0.0f};
};
std::array<ChannelState, 4> g_channels{};
// Euclidean length of a 3-vector.
float Length(const float* v) {
return std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
}
// Euclidean distance between two 3-vectors.
float Distance(const float* a, const float* b) {
const float d[3] = {a[0] - b[0], a[1] - b[1], a[2] - b[2]};
return Length(d);
}
// Writes three big-endian floats to guest memory.
void WriteVec3(uint32_t addr, const float* v) {
Memory::WriteFloat32(addr, v[0]);
Memory::WriteFloat32(addr + 4, v[1]);
Memory::WriteFloat32(addr + 8, v[2]);
}
// Zeroes `count` consecutive floats in guest memory.
void WriteZeroFloats(uint32_t addr, uint32_t count) {
for (uint32_t i = 0; i < count; ++i) {
Memory::WriteFloat32(addr + i * 4, 0.0f);
}
}
// Fills one KPADStatus at `addr` and returns the number of valid entries (1),
// or writes an "unplugged" status and returns 0.
int32_t WriteStatus(uint32_t chan, uint32_t addr, const WiiRemoteInput::KpadSample* sample) {
ChannelState& state = g_channels[chan];
if (sample == nullptr) {
state = {};
Memory::Write32(addr + kHold, 0);
Memory::Write32(addr + kTrig, 0);
Memory::Write32(addr + kRelease, 0);
Memory::Write8(addr + kDevType, kDevCore);
Memory::Write8(addr + kWpadErr, static_cast<uint8_t>(kWpadErrNoController));
Memory::Write8(addr + kDpdValidFg, 0);
return 0;
}
const uint32_t hold = sample->hold;
Memory::Write32(addr + kHold, hold);
Memory::Write32(addr + kTrig, hold & ~state.prevHold);
Memory::Write32(addr + kRelease, state.prevHold & ~hold);
state.prevHold = hold;
WriteVec3(addr + kAcc, sample->acc);
Memory::WriteFloat32(addr + kAccValue, Length(sample->acc));
Memory::WriteFloat32(addr + kAccSpeed, Distance(sample->acc, state.prevAcc));
for (int i = 0; i < 3; ++i) state.prevAcc[i] = sample->acc[i];
// No IR pointer: pos .. acc_vertical zeroed and dpd_valid_fg clear, which
// the game treats as "pointing away from the screen".
WriteZeroFloats(addr + kPos, (kAccVertical + 8 - kPos) / 4);
Memory::Write8(addr + kDpdValidFg, 0);
const uint8_t devType = sample->hasClassic ? kDevClassic : sample->hasNunchuk ? kDevFreestyle : kDevCore;
const uint8_t dataFormat =
sample->hasClassic ? kFmtClassicAccDpd : sample->hasNunchuk ? kFmtFreestyleAccDpd : kFmtCoreAccDpd;
Memory::Write8(addr + kDevType, devType);
Memory::Write8(addr + kWpadErr, static_cast<uint8_t>(kWpadErrNone));
Memory::Write8(addr + kDataFormat, dataFormat);
if (sample->hasClassic) {
const uint32_t clHold = sample->clHold;
Memory::Write32(addr + kClHold, clHold);
Memory::Write32(addr + kClTrig, clHold & ~state.prevClHold);
Memory::Write32(addr + kClRelease, state.prevClHold & ~clHold);
state.prevClHold = clHold;
Memory::WriteFloat32(addr + kClLStick, sample->clLStick[0]);
Memory::WriteFloat32(addr + kClLStick + 4, sample->clLStick[1]);
Memory::WriteFloat32(addr + kClRStick, sample->clRStick[0]);
Memory::WriteFloat32(addr + kClRStick + 4, sample->clRStick[1]);
Memory::WriteFloat32(addr + kClLTrigger, sample->clTriggerL / 255.0f);
Memory::WriteFloat32(addr + kClRTrigger, sample->clTriggerR / 255.0f);
} else if (sample->hasNunchuk) {
state.prevClHold = 0;
Memory::WriteFloat32(addr + kFsStick, sample->stick[0]);
Memory::WriteFloat32(addr + kFsStick + 4, sample->stick[1]);
WriteVec3(addr + kFsAcc, sample->nunchukAcc);
Memory::WriteFloat32(addr + kFsAccValue, Length(sample->nunchukAcc));
Memory::WriteFloat32(addr + kFsAccSpeed, Distance(sample->nunchukAcc, state.prevFsAcc));
for (int i = 0; i < 3; ++i) state.prevFsAcc[i] = sample->nunchukAcc[i];
} else {
state.prevClHold = 0;
WriteZeroFloats(addr + kFsStick, (kKpadStatusSize - kFsStick) / 4);
}
return 1;
}
// One accelerometer axis of KPAD's g vector back to the 10-bit raw WPAD value.
uint16_t RawAcc(float g) {
const float raw = kRawAccZero + g * kRawAccPerG;
return static_cast<uint16_t>(std::clamp(raw, 0.0f, 1023.0f));
}
// Fills one KPADUnifiedWpadStatus at `addr` from the sample: the WPADStatus core
// (remote buttons, raw accelerometer, no IR objects), then the Nunchuk or Classic
// Controller tail, then the data format.
void WriteUnifiedStatus(uint32_t addr, const WiiRemoteInput::KpadSample* sample) {
for (uint32_t offset = 0; offset < kUnifiedSize; offset += 4) {
Memory::Write32(addr + offset, 0);
}
if (sample == nullptr) {
Memory::Write8(addr + kUDev, kDevCore);
Memory::Write8(addr + kUErr, static_cast<uint8_t>(kWpadErrNoController));
Memory::Write8(addr + kUFmt, kFmtCoreAccDpd);
return;
}
Memory::Write16(addr + kUButton, static_cast<uint16_t>(sample->hold & 0xFFFF));
// KPAD's acc is (-wiiX, -wiiZ, wiiY); WPADStatus keeps the remote's own axes.
Memory::Write16(addr + kUAccX, RawAcc(-sample->acc[0]));
Memory::Write16(addr + kUAccY, RawAcc(sample->acc[2]));
Memory::Write16(addr + kUAccZ, RawAcc(-sample->acc[1]));
// No IR: every DPDObject invalid (x/y at the sensor's out-of-range value).
for (uint32_t i = 0; i < 4; ++i) {
Memory::Write16(addr + kUObj + i * 8, 0x3FF);
Memory::Write16(addr + kUObj + i * 8 + 2, 0x3FF);
}
Memory::Write8(addr + kUErr, static_cast<uint8_t>(kWpadErrNone));
if (sample->hasClassic) {
Memory::Write8(addr + kUDev, kDevClassic);
Memory::Write16(addr + kUClButton, static_cast<uint16_t>(sample->clHold & 0xFFFF));
Memory::Write16(addr + kUClLStickX, static_cast<uint16_t>(sample->clLStickRaw[0]));
Memory::Write16(addr + kUClLStickY, static_cast<uint16_t>(sample->clLStickRaw[1]));
Memory::Write16(addr + kUClRStickX, static_cast<uint16_t>(sample->clRStickRaw[0]));
Memory::Write16(addr + kUClRStickY, static_cast<uint16_t>(sample->clRStickRaw[1]));
Memory::Write8(addr + kUClTriggerL, sample->clTriggerL);
Memory::Write8(addr + kUClTriggerR, sample->clTriggerR);
Memory::Write8(addr + kUFmt, kFmtClassicAccDpd);
} else if (sample->hasNunchuk) {
Memory::Write8(addr + kUDev, kDevFreestyle);
// WPADFSStatus: 8-bit stick (centre 128) and 10-bit Nunchuk accelerometer.
Memory::Write8(addr + kUFsStickX,
static_cast<uint8_t>(std::clamp(128.0f + sample->stick[0] * 100.0f, 0.0f, 255.0f)));
Memory::Write8(addr + kUFsStickY,
static_cast<uint8_t>(std::clamp(128.0f + sample->stick[1] * 100.0f, 0.0f, 255.0f)));
Memory::Write16(addr + kUFsAccX, RawAcc(-sample->nunchukAcc[0]));
Memory::Write16(addr + kUFsAccY, RawAcc(sample->nunchukAcc[2]));
Memory::Write16(addr + kUFsAccZ, RawAcc(-sample->nunchukAcc[1]));
Memory::Write8(addr + kUFmt, kFmtFreestyleAccDpd);
} else {
Memory::Write8(addr + kUDev, kDevCore);
Memory::Write8(addr + kUFmt, kFmtCoreAccDpd);
}
}
} // namespace
// KPADRead: fills KPADStatus[0] for `chan` from the Bluetooth remote, returns the entry count.
extern "C" int32_t KPAD__Read_HLE(uint32_t chan, uint32_t statusPtr, uint32_t count) extern "C" int32_t KPAD__Read_HLE(uint32_t chan, uint32_t statusPtr, uint32_t count)
{ {
(void)chan; if (chan >= g_channels.size() || statusPtr == 0 || count == 0) {
(void)statusPtr; return 0;
(void)count; }
return 0; WiiRemoteInput::KpadSample sample;
const bool have = WiiRemoteInput::ReadKpadSample(chan, sample);
try {
return WriteStatus(chan, statusPtr, have ? &sample : nullptr);
} catch (const Memory::AccessViolation&) {
return 0;
}
} }
PPC_NATIVE_OVERRIDE(80197380, KPAD__Read_HLE, int32_t, (uint32_t chan, uint32_t statusPtr, uint32_t count), PPC_NATIVE_OVERRIDE(80197380, KPAD__Read_HLE, int32_t, (uint32_t chan, uint32_t statusPtr, uint32_t count),
(chan, statusPtr, count)); (chan, statusPtr, count));
// KPADGetUnifiedWpadStatus: the raw WPAD status behind KPADStatus. The game
// reads the Classic Controller's buttons, sticks and triggers from here. The
// SDK fills `count` entries with the channel's recent samples (the game asks for
// as many as it asked KPADRead for and looks at entry 0); with one sample per
// frame here, every entry gets the current one.
extern "C" int32_t KPAD__GetUnifiedWpadStatus_HLE(uint32_t chan, uint32_t statusPtr, uint32_t count) extern "C" int32_t KPAD__GetUnifiedWpadStatus_HLE(uint32_t chan, uint32_t statusPtr, uint32_t count)
{ {
(void)chan; constexpr uint32_t kMaxEntries = 16; // KPAD_MAX_READ_BUFS
(void)statusPtr; if (chan >= g_channels.size() || statusPtr == 0 || count == 0) {
(void)count; return 0;
return 0; }
WiiRemoteInput::KpadSample sample;
const bool have = WiiRemoteInput::ReadKpadSample(chan, sample);
try {
const uint32_t entries = std::min(count, kMaxEntries);
for (uint32_t i = 0; i < entries; ++i) {
WriteUnifiedStatus(statusPtr + i * kUnifiedSize, have ? &sample : nullptr);
}
} catch (const Memory::AccessViolation&) {
return 0;
}
return have ? 1 : 0;
} }
PPC_NATIVE_OVERRIDE(8019812C, KPAD__GetUnifiedWpadStatus_HLE, int32_t, PPC_NATIVE_OVERRIDE(8019812C, KPAD__GetUnifiedWpadStatus_HLE, int32_t,
(uint32_t chan, uint32_t statusPtr, uint32_t count), (chan, statusPtr, count)); (uint32_t chan, uint32_t statusPtr, uint32_t count), (chan, statusPtr, count));
+76
View File
@@ -1,17 +1,75 @@
#include "hle_stubs.h" #include "hle_stubs.h"
#include "memory.h" #include "memory.h"
#include "hle/controller_status_contract.h" #include "hle/controller_status_contract.h"
#include "input_bindings.h"
#include "wii_remote_input.h"
#include <algorithm> #include <algorithm>
#include <atomic>
#include <cstdio> #include <cstdio>
#include <cstdint> #include <cstdint>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <SDL3/SDL_gamepad.h>
#include <dolphin/pad.h> #include <dolphin/pad.h>
namespace { namespace {
std::atomic<bool> g_rumbleEnabled{true};
bool NativeButtonHeld(SDL_Gamepad* gamepad, uint32_t nativeButton) {
if (gamepad == nullptr || nativeButton == PAD_NATIVE_BUTTON_INVALID ||
nativeButton >= SDL_GAMEPAD_BUTTON_COUNT) {
return false;
}
return SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(nativeButton));
}
// A digital button bound to L or R has no analog travel of its own. On real
// hardware the click only engages at full depression, so report a full pull.
void FillTriggersHeldByButtons(PADStatus* statuses) {
if (InputBindings::InputBlocked()) {
return;
}
for (uint32_t port = 0; port < PAD_CHANMAX; ++port) {
if (statuses[port].err != PAD_ERR_NONE) {
continue;
}
const s32 index = PADGetIndexForPort(port);
if (index < 0) {
continue;
}
SDL_Gamepad* gamepad = PADGetSDLGamepadForIndex(static_cast<u32>(index));
if (gamepad == nullptr) {
continue;
}
const auto scan = [&](PADButtonMapping* mappings, u32 count) {
if (mappings == nullptr) {
return;
}
for (u32 i = 0; i < count; ++i) {
const PADButtonMapping& mapping = mappings[i];
if (mapping.padButton != PAD_TRIGGER_L && mapping.padButton != PAD_TRIGGER_R) {
continue;
}
if (!NativeButtonHeld(gamepad, mapping.nativeButton)) {
continue;
}
if (mapping.padButton == PAD_TRIGGER_L) {
statuses[port].triggerLeft = 255;
} else {
statuses[port].triggerRight = 255;
}
}
};
u32 count = 0;
scan(PADGetButtonMappings(port, &count), count);
count = 0;
scan(PADGetAltButtonMappings(port, &count), count);
}
}
void WritePadStatus(uint32_t base, const PADStatus& status) { void WritePadStatus(uint32_t base, const PADStatus& status) {
const auto guestStatus = PadStatusContract::Encode({ const auto guestStatus = PadStatusContract::Encode({
status.button, status.button,
@@ -31,12 +89,18 @@ void WritePadStatus(uint32_t base, const PADStatus& status) {
} // namespace } // namespace
extern "C" void PAD_HLE_SetRumbleEnabled(bool enabled)
{
g_rumbleEnabled.store(enabled, std::memory_order_relaxed);
}
extern "C" uint32_t PAD__Init_HLE() extern "C" uint32_t PAD__Init_HLE()
{ {
return PADInit() ? 1u : 0u; return PADInit() ? 1u : 0u;
} }
PPC_NATIVE_OVERRIDE(801AF2F0, PAD__Init_HLE, uint32_t, (), ()); PPC_NATIVE_OVERRIDE(801AF2F0, PAD__Init_HLE, uint32_t, (), ());
// PADRead: gathers every GameCube pad source for the frame and writes the statuses to guest memory.
extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr) extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
{ {
if (statusPtr == 0) { if (statusPtr == 0) {
@@ -44,7 +108,16 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
} }
PADStatus statuses[PAD_CHANMAX]{}; PADStatus statuses[PAD_CHANMAX]{};
// Keep looking for a Bluetooth Wii Remote that dropped out (or was turned on late).
WiiRemoteInput::Poll();
uint32_t rumbleMask = PADRead(statuses); uint32_t rumbleMask = PADRead(statuses);
// Wii Remotes reach the game through KPAD, not as GameCube pads. This also
// applies while input is blocked (overlay open) so the port does not flip
// between "connected" and "no controller" every time the overlay toggles.
WiiRemoteInput::HideRemotesFromPad(statuses, PAD_CHANMAX);
FillTriggersHeldByButtons(statuses);
InputBindings::Apply(statuses);
try { try {
for (uint32_t i = 0; i < PAD_CHANMAX; ++i) { for (uint32_t i = 0; i < PAD_CHANMAX; ++i) {
@@ -73,6 +146,9 @@ PPC_NATIVE_OVERRIDE(801AF1E4, PAD__Recalibrate_HLE, uint32_t, (uint32_t mask), (
extern "C" void PAD__ControlMotor_HLE(int32_t chan, uint32_t command) extern "C" void PAD__ControlMotor_HLE(int32_t chan, uint32_t command)
{ {
if (command == PAD_MOTOR_RUMBLE && !g_rumbleEnabled.load(std::memory_order_relaxed)) {
command = PAD_MOTOR_STOP;
}
PADControlMotor(chan, command); PADControlMotor(chan, command);
} }
PPC_NATIVE_OVERRIDE_VOID(801AF908, PAD__ControlMotor_HLE, (int32_t chan, uint32_t command), (chan, command)); PPC_NATIVE_OVERRIDE_VOID(801AF908, PAD__ControlMotor_HLE, (int32_t chan, uint32_t command), (chan, command));
+24
View File
@@ -1,6 +1,7 @@
#include "hle_stubs.h" #include "hle_stubs.h"
#include "memory.h" #include "memory.h"
#include "hle/controller_status_contract.h" #include "hle/controller_status_contract.h"
#include "wii_remote_input.h"
#include <cstdint> #include <cstdint>
@@ -98,18 +99,41 @@ extern "C" int32_t WPADGetDataFormat_HLE(uint32_t chan)
} }
PPC_NATIVE_OVERRIDE(801C0B54, WPADGetDataFormat_HLE, int32_t, (uint32_t chan), (chan)); PPC_NATIVE_OVERRIDE(801C0B54, WPADGetDataFormat_HLE, int32_t, (uint32_t chan), (chan));
// WPADSetDataFormat: records the per-channel data format the game asked for.
extern "C" int32_t WPADSetDataFormat_HLE(uint32_t chan, int32_t format) extern "C" int32_t WPADSetDataFormat_HLE(uint32_t chan, int32_t format)
{ {
return g_state.contract.SetDataFormat(chan, format); return g_state.contract.SetDataFormat(chan, format);
} }
PPC_NATIVE_OVERRIDE(801C0B9C, WPADSetDataFormat_HLE, int32_t, (uint32_t chan, int32_t format), (chan, format)); PPC_NATIVE_OVERRIDE(801C0B9C, WPADSetDataFormat_HLE, int32_t, (uint32_t chan, int32_t format), (chan, format));
// WPADProbe: reports the extension type of a Bluetooth remote on `chan`, or no controller.
extern "C" int32_t WPADProbe_HLE(uint32_t chan, uint32_t typePtr) extern "C" int32_t WPADProbe_HLE(uint32_t chan, uint32_t typePtr)
{ {
if (chan >= WpadContract::kChannelCount) { if (chan >= WpadContract::kChannelCount) {
return WpadContract::kErrorBadChannel; return WpadContract::kErrorBadChannel;
} }
// Drive the rescan state machine here too: a reconnect probe can arrive
// before the next PADRead, and only Poll() brings a dropped remote back.
WiiRemoteInput::Poll();
// A real Bluetooth remote: WPAD_DEV_CORE (0) for a bare remote,
// WPAD_DEV_FREESTYLE (1) with a Nunchuk, WPAD_DEV_CLASSIC (2) with a Classic
// Controller. The game reads the type from here (not from
// KPADStatus.dev_type) to pick its control scheme, and re-reads it when it
// changes, which is what makes an extension swap mid-game work like on the
// console. EffectiveKind keeps the last type through SDL's re-creation of
// the joystick after a swap.
const WiiRemoteInput::Kind kind = WiiRemoteInput::EffectiveKind(chan);
if (WiiRemoteInput::IsRemoteChannel(chan)) {
if (typePtr != 0) {
uint32_t type = WpadContract::kExtensionCore;
if (kind == WiiRemoteInput::Kind::RemoteWithNunchuk) type = 1u;
if (kind == WiiRemoteInput::Kind::RemoteWithClassic) type = 2u;
Memory::Write32(typePtr, type);
}
return kStatusOk;
}
if (typePtr != 0) { if (typePtr != 0) {
Memory::Write32(typePtr, WpadContract::kExtensionCore); Memory::Write32(typePtr, WpadContract::kExtensionCore);
} }
+1 -1
View File
@@ -499,7 +499,7 @@ int32_t HandleIpTopIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const s
// Nonblocking sockets get -SO_EAGAIN immediately (Dolphin's retry predicate // Nonblocking sockets get -SO_EAGAIN immediately (Dolphin's retry predicate
// short-circuits on nonBlock/forceNonBlock, IOS/Network/Socket.cpp:715-718); // short-circuits on nonBlock/forceNonBlock, IOS/Network/Socket.cpp:715-718);
// waiting here anyway stalled the whole emulation thread on every empty read. // waiting here anyway stalled the whole emulation thread on every empty read.
constexpr int kStreamRecvWaitMs = 250; constexpr int kStreamRecvWaitMs = 1000;
const int streamWaitMs = (forceNonBlock || s->nonblocking) ? 0 : kStreamRecvWaitMs; const int streamWaitMs = (forceNonBlock || s->nonblocking) ? 0 : kStreamRecvWaitMs;
const bool waited = ret < 0 && !fromPtr && s->type == SOCK_STREAM && const bool waited = ret < 0 && !fromPtr && s->type == SOCK_STREAM &&
IsWouldBlockError(nativeErr) && WaitForReadable(s->native, streamWaitMs); IsWouldBlockError(nativeErr) && WaitForReadable(s->native, streamWaitMs);
+18 -6
View File
@@ -270,7 +270,13 @@ static int32_t EnsureSslCredentials(SslSession& ssl) {
SCHANNEL_CRED cred{}; SCHANNEL_CRED cred{};
cred.dwVersion = SCHANNEL_CRED_VERSION; cred.dwVersion = SCHANNEL_CRED_VERSION;
cred.dwFlags = SCH_USE_STRONG_CRYPTO | SCH_CRED_NO_DEFAULT_CREDS | SCH_CRED_MANUAL_CRED_VALIDATION; // Leave certificate validation to Schannel. SCH_CRED_MANUAL_CRED_VALIDATION
// suppresses that validation and requires an explicit CertGetCertificateChain
// implementation, which this HLE does not provide. The target hostname passed
// to InitializeSecurityContextA below is therefore checked together with the
// server certificate chain.
cred.dwFlags = SCH_USE_STRONG_CRYPTO | SCH_CRED_NO_DEFAULT_CREDS |
SCH_CRED_AUTO_CRED_VALIDATION;
TimeStamp expiry{}; TimeStamp expiry{};
const SECURITY_STATUS status = AcquireCredentialsHandleA( const SECURITY_STATUS status = AcquireCredentialsHandleA(
@@ -289,6 +295,13 @@ static int32_t SslHandshakeImpl(SslSession& ssl) {
return SSL_OK; return SSL_OK;
} }
// Schannel can authenticate a certificate chain without authenticating a
// server identity when no target name is supplied. Refuse that ambiguous
// mode rather than accepting a certificate for an unrelated endpoint.
if (ssl.hostname.empty()) {
return SSL_ERR_VCOMMONNAME;
}
const int32_t credRet = EnsureSslCredentials(ssl); const int32_t credRet = EnsureSslCredentials(ssl);
if (credRet != SSL_OK) { if (credRet != SSL_OK) {
return credRet; return credRet;
@@ -331,7 +344,7 @@ static int32_t SslHandshakeImpl(SslSession& ssl) {
const SECURITY_STATUS status = InitializeSecurityContextA( const SECURITY_STATUS status = InitializeSecurityContextA(
&ssl.cred, ssl.haveContext ? &ssl.context : nullptr, &ssl.cred, ssl.haveContext ? &ssl.context : nullptr,
ssl.hostname.empty() ? nullptr : const_cast<char*>(ssl.hostname.c_str()), flags, 0, SECURITY_NATIVE_DREP, const_cast<char*>(ssl.hostname.c_str()), flags, 0, SECURITY_NATIVE_DREP,
inDescPtr, 0, &ssl.context, &outDesc, &attrs, &expiry); inDescPtr, 0, &ssl.context, &outDesc, &attrs, &expiry);
if (status != SEC_E_INVALID_HANDLE) { if (status != SEC_E_INVALID_HANDLE) {
ssl.haveContext = true; ssl.haveContext = true;
@@ -567,10 +580,9 @@ int32_t HandleSslIoctlv(uint32_t cmd, const std::vector<IoVector>& in, const std
switch (cmd) { switch (cmd) {
case IOCTLV_NET_SSL_NEW: { case IOCTLV_NET_SSL_NEW: {
// out[0] carries the verify option. Its value is not consulted - every // out[0] carries the guest verify option. Host TLS verification is
// certificate check is delegated to Schannel with // always enforced by Schannel, but still read this value so a request
// SCH_CRED_MANUAL_CRED_VALIDATION - but it is still read so that a // pointing outside guest memory faults here as it always has.
// request pointing outside guest memory faults here as it always has.
if (!out.empty() && out[0].address && out[0].size >= 4) { if (!out.empty() && out[0].address && out[0].size >= 4) {
(void)Memory::Read32(out[0].address); (void)Memory::Read32(out[0].address);
} }
+29 -13
View File
@@ -78,22 +78,38 @@ bool ProcessSleepTimers(CpuContext* cpu)
{ {
using Clock = std::chrono::steady_clock; using Clock = std::chrono::steady_clock;
std::vector<SleepTimerEntry> dueTimers; // Pop and process ONE due timer at a time, straight from the shared table. Resuming a
// sleeper re-enters the scheduler (OSResumeThread -> SelectThread) and can switch fibers
// away from this call. Timers that had already been popped into a private list would then
// sit on the suspended fiber's stack with their threads parked and no entry in the table:
// exactly the "park-shaped with no pending wake timer" strand the reconciler below heals
// 100ms late, followed by a "sleep-timer stale" drop when this fiber finally resumes.
// Leaving unprocessed timers in the table keeps them visible to every other pump (idle
// loop, other threads' SelectThread) while this one is switched away.
bool processedAny = false;
constexpr size_t kMaxTimersPerCall = 64;
size_t processedCount = 0;
const auto now = Clock::now(); const auto now = Clock::now();
{ while (processedCount < kMaxTimersPerCall) {
std::lock_guard<std::mutex> lock(gSleepTimerMutex); SleepTimerEntry timer{0, {}};
auto it = gSleepTimers.begin(); bool found = false;
while (it != gSleepTimers.end()) { {
if (it->deadline > now) { std::lock_guard<std::mutex> lock(gSleepTimerMutex);
++it; for (auto it = gSleepTimers.begin(); it != gSleepTimers.end(); ++it) {
continue; if (it->deadline <= now) {
timer = *it;
gSleepTimers.erase(it);
found = true;
break;
}
} }
dueTimers.push_back(*it);
it = gSleepTimers.erase(it);
} }
} if (!found) {
break;
}
++processedCount;
processedAny = true;
for (const SleepTimerEntry& timer : dueTimers) {
const uint32_t threadPtr = timer.threadPtr; const uint32_t threadPtr = timer.threadPtr;
if (threadPtr == 0 || if (threadPtr == 0 ||
!Memory::Contains(threadPtr + kThreadSuspendOffset, sizeof(uint32_t))) { !Memory::Contains(threadPtr + kThreadSuspendOffset, sizeof(uint32_t))) {
@@ -219,7 +235,7 @@ bool ProcessSleepTimers(CpuContext* cpu)
} }
} }
return !dueTimers.empty(); return processedAny;
} }
} // namespace OsHleInternal } // namespace OsHleInternal
+33 -21
View File
@@ -1,6 +1,7 @@
#include "hle_stubs.h" #include "hle_stubs.h"
#include "console_identity.h" #include "console_identity.h"
#include "sc_serial_contract.h"
#include <cstdlib> #include <cstdlib>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
@@ -12,7 +13,25 @@
namespace { namespace {
constexpr uint32_t kPalProductRegion = 2; // Use the SDK's own value tables, including its unknown-region result.
uint32_t LookupProductRegion(uint32_t table, uint32_t stride, uint32_t count,
const std::string& value) {
for (uint32_t index = 0; index < count; ++index) {
const uint32_t entry = table + index * stride;
if (!Memory::Contains(entry, stride)) {
break;
}
const auto* bytes = static_cast<const uint8_t*>(Memory::GetPointer(entry, stride));
if (bytes[0] == 0xFF) {
break;
}
if (value.size() < stride - 1 &&
std::memcmp(bytes + 1, value.c_str(), value.size() + 1) == 0) {
return bytes[0];
}
}
return 0xFFFFFFFFu;
}
} // namespace } // namespace
@@ -50,16 +69,12 @@ extern "C" uint32_t SCGetEuRgb60Mode_HLE()
PPC_NATIVE_OVERRIDE(801B1CAC, SCGetEuRgb60Mode_HLE, uint32_t, (), ()); PPC_NATIVE_OVERRIDE(801B1CAC, SCGetEuRgb60Mode_HLE, uint32_t, (), ());
// The managed NAND intentionally starts without a console-owned setting.txt. // Expose the selected emulated NAND identity through the SDK SC APIs.
// DWC nevertheless requires the Wii product code and serial number so it can
// include csnum in NAS authentication. Expose one stable virtual-console
// identity without requiring or mutating a user's real NAND.
extern "C" uint32_t SCGetProductArea_HLE() extern "C" uint32_t SCGetProductArea_HLE()
{ {
// The PAL setting.txt AREA value is "EUR". The SDK's lookup table at return LookupProductRegion(0x8029CEB0u, 5, 13,
// 0x8029CEB0 maps JPN=0, USA=1, EUR=2. RuntimeConsoleIdentity::Current().area);
return kPalProductRegion;
} }
PPC_NATIVE_OVERRIDE(801B23A0, SCGetProductArea_HLE, uint32_t, (), ()); PPC_NATIVE_OVERRIDE(801B23A0, SCGetProductArea_HLE, uint32_t, (), ());
@@ -68,12 +83,13 @@ extern "C" uint32_t SCGetProductCode_HLE()
{ {
// Original PAL SC storage for the six-byte CODE value. // Original PAL SC storage for the six-byte CODE value.
constexpr uint32_t kProductCodeAddress = 0x803869E0u; constexpr uint32_t kProductCodeAddress = 0x803869E0u;
static constexpr char kProductCode[] = "LEH"; const std::string& productCode = RuntimeConsoleIdentity::Current().productCode;
if (!Memory::Contains(kProductCodeAddress, sizeof(kProductCode))) { const size_t size = productCode.size() + 1;
if (!Memory::Contains(kProductCodeAddress, size)) {
return 0; return 0;
} }
std::memcpy(Memory::GetPointer(kProductCodeAddress, sizeof(kProductCode)), std::memcpy(Memory::GetPointer(kProductCodeAddress, size),
kProductCode, sizeof(kProductCode)); productCode.c_str(), size);
return kProductCodeAddress; return kProductCodeAddress;
} }
@@ -82,21 +98,17 @@ PPC_NATIVE_OVERRIDE(801B2424, SCGetProductCode_HLE, uint32_t, (), ());
extern "C" uint32_t SCGetProductSN_HLE(uint32_t serialAddress) extern "C" uint32_t SCGetProductSN_HLE(uint32_t serialAddress)
{ {
const std::string& serial = RuntimeConsoleIdentity::Current().serial; const std::string& serial = RuntimeConsoleIdentity::Current().serial;
if (!serialAddress || !Memory::Contains(serialAddress, serial.size() + 1)) { return RuntimeScSerial::Write(serial, serialAddress,
return 0; [](uint32_t address, size_t size) { return Memory::Contains(address, size); },
} [](uint32_t address, uint32_t value) { Memory::Write32(address, value); });
std::memcpy(Memory::GetPointer(serialAddress, serial.size() + 1),
serial.c_str(), serial.size() + 1);
return 1;
} }
PPC_NATIVE_OVERRIDE(801B2460, SCGetProductSN_HLE, uint32_t, (uint32_t serialAddress), (serialAddress)); PPC_NATIVE_OVERRIDE(801B2460, SCGetProductSN_HLE, uint32_t, (uint32_t serialAddress), (serialAddress));
extern "C" uint32_t SCGetProductGameRegion_HLE() extern "C" uint32_t SCGetProductGameRegion_HLE()
{ {
// The PAL setting.txt GAME value is "EU". The SDK's own lookup table at return LookupProductRegion(0x8029CEF8u, 4, 4,
// 0x8029CEF8 maps JP=0, US=1, EU=2. RuntimeConsoleIdentity::Current().gameRegion);
return kPalProductRegion;
} }
PPC_NATIVE_OVERRIDE(801B24C8, SCGetProductGameRegion_HLE, uint32_t, (), ()); PPC_NATIVE_OVERRIDE(801B24C8, SCGetProductGameRegion_HLE, uint32_t, (), ());
+3
View File
@@ -93,6 +93,9 @@ extern "C" int32_t NANDOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint32_t
const std::filesystem::path hostPath = TranslateNandPath(path); const std::filesystem::path hostPath = TranslateNandPath(path);
if (const auto result = NandCheckSystemSaveRead("NANDOpen", hostPath, mode))
return *result;
// Existing-file write opens go through a shadow copy seeded from the original, so a // Existing-file write opens go through a shadow copy seeded from the original, so a
// crash between NANDWrite and NANDClose cannot leave a torn file (the game patches // crash between NANDWrite and NANDClose cannot leave a torn file (the game patches
// sub-ranges, e.g. ghost saves at a non-zero offset). New files still create in place. // sub-ranges, e.g. ghost saves at a non-zero offset). New files still create in place.
+2
View File
@@ -411,6 +411,8 @@ extern "C" int32_t NANDSafeOpen_HLE(uint32_t pathPtr, uint32_t fileInfoPtr, uint
if (mode == 1) { if (mode == 1) {
// Read-only safe open reads the original in place; the library builds no scratch // Read-only safe open reads the original in place; the library builds no scratch
// copy for this case. // copy for this case.
if (const auto result = NandCheckSystemSaveRead("NANDSafeOpen", hostPath, mode))
return *result;
FILE* file = NandFopen(hostPath, "rb"); FILE* file = NandFopen(hostPath, "rb");
if (!file && IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) { if (!file && IsFaceLibResourcePath(path) && SeedFaceLibResource(hostPath)) {
file = NandFopen(hostPath, "rb"); file = NandFopen(hostPath, "rb");
+20
View File
@@ -411,6 +411,26 @@ bool IsFaceLibResourcePath(const char* path) {
return std::strcmp(path, "/shared2/menu/FaceLib/RFL_Res.dat") == 0; return std::strcmp(path, "/shared2/menu/FaceLib/RFL_Res.dat") == 0;
} }
std::optional<int32_t> NandCheckSystemSaveRead(const char* who,
const std::filesystem::path& hostPath, int mode, bool ios) {
const auto action = RuntimeNandSave::CheckRead(hostPath, mode);
if (action == RuntimeNandSave::ReadAction::Proceed) return std::nullopt;
if (action == RuntimeNandSave::ReadAction::Missing) {
LogNandWarning(who, "treating empty or zero-filled system save '%s' as missing",
HostPathText(hostPath).c_str());
return ios ? ISFS_ENOENT : NAND_RESULT_NOEXISTS;
}
if (action == RuntimeNandSave::ReadAction::RecoveryNeeded) {
LogNandError(who, "system save '%s' is missing or blank but its .nandsafe.tmp contains data; "
"back up both files before attempting recovery",
HostPathText(hostPath).c_str());
} else {
LogNandError(who, "could not inspect system save '%s' or its write shadow; leaving data untouched",
HostPathText(hostPath).c_str());
}
return ios ? ISFS_EIO : NAND_RESULT_UNKNOWN;
}
// Create directories recursively // Create directories recursively
bool CreateDirectoryPath(const std::filesystem::path& path) { bool CreateDirectoryPath(const std::filesystem::path& path) {
if (path.empty()) { if (path.empty()) {
+7
View File
@@ -9,6 +9,7 @@
#include "hle/runtime_parse_helpers.h" #include "hle/runtime_parse_helpers.h"
#include "memory.h" #include "memory.h"
#include "nand_path.h" #include "nand_path.h"
#include "nand_save_probe.h"
#include "hle/net/network.h" #include "hle/net/network.h"
#include "recomp_mod_loader.h" #include "recomp_mod_loader.h"
#include "runtime_config.h" #include "runtime_config.h"
@@ -26,6 +27,7 @@
#include <deque> #include <deque>
#include <map> #include <map>
#include <mutex> #include <mutex>
#include <optional>
#include <vector> #include <vector>
#include <filesystem> #include <filesystem>
#include <string> #include <string>
@@ -56,6 +58,11 @@ constexpr uint32_t kNandTitleIdLo = 0x524D4350; // "RMCP" fallback
void LogNandError(const char* func, const char* fmt, ...); void LogNandError(const char* func, const char* fmt, ...);
void LogNandWarning(const char* func, const char* fmt, ...); void LogNandWarning(const char* func, const char* fmt, ...);
// An empty optional means continue opening normally; otherwise return the
// supplied NAND/IOS error without exposing a failed scan as a missing save.
std::optional<int32_t> NandCheckSystemSaveRead(const char* who,
const std::filesystem::path& hostPath, int mode, bool ios = false);
// ============================================================================ // ============================================================================
// File Descriptor Management // File Descriptor Management
// ============================================================================ // ============================================================================
+66 -2
View File
@@ -4,6 +4,7 @@
#include "nand_internal.h" #include "nand_internal.h"
#include "discord_presence.h"
#include "runtime_log.h" #include "runtime_log.h"
extern "C" void OSSleepThread_HLE_801aa9b8(CpuContext* ctx); extern "C" void OSSleepThread_HLE_801aa9b8(CpuContext* ctx);
@@ -201,9 +202,69 @@ static int32_t HandleDolphinIoctlv(uint32_t cmd, uint32_t numIn, uint32_t numOut
} }
case DOLPHIN_IOCTL_SET_SPEED_LIMIT: case DOLPHIN_IOCTL_SET_SPEED_LIMIT:
case DOLPHIN_IOCTL_DISCORD_SET_CLIENT: return ISFS_OK;
case DOLPHIN_IOCTL_DISCORD_SET_PRESENCE:
case DOLPHIN_IOCTL_DISCORD_SET_CLIENT: {
if (numIn != 1 || numOut != 0 || vectorPtr == 0) {
return ISFS_EINVAL;
}
const IosVector client = ReadIosVector(vectorPtr, 0);
if (!IsValidGuestRange(client.address, client.size)) {
return ISFS_EINVAL;
}
if (RuntimeConfigFile::DiscordPresenceEnabled()) {
DiscordPresence::SetClient(ReadGuestCString(client.address, client.size));
}
return ISFS_OK;
}
case DOLPHIN_IOCTL_DISCORD_SET_PRESENCE: {
if (numIn != 10 || numOut != 0 || vectorPtr == 0) {
return ISFS_EINVAL;
}
std::array<IosVector, 10> values{};
for (uint32_t index = 0; index < values.size(); ++index) {
values[index] = ReadIosVector(vectorPtr, index);
if (!IsValidGuestRange(values[index].address, values[index].size)) {
return ISFS_EINVAL;
}
}
if (RuntimeConfigFile::DiscordPresenceEnabled()) {
DiscordPresence::Activity activity;
activity.details = ReadGuestCString(values[0].address, values[0].size);
activity.state = ReadGuestCString(values[1].address, values[1].size);
activity.largeImageKey = ReadGuestCString(values[2].address, values[2].size);
activity.largeImageText = ReadGuestCString(values[3].address, values[3].size);
activity.smallImageKey = ReadGuestCString(values[4].address, values[4].size);
activity.smallImageText = ReadGuestCString(values[5].address, values[5].size);
if (values[6].size >= 8 && Memory::Contains(values[6].address, 8)) {
activity.startTimestamp = static_cast<int64_t>(
(static_cast<uint64_t>(Memory::Read32(values[6].address)) << 32) |
Memory::Read32(values[6].address + 4));
}
if (values[7].size >= 8 && Memory::Contains(values[7].address, 8)) {
activity.endTimestamp = static_cast<int64_t>(
(static_cast<uint64_t>(Memory::Read32(values[7].address)) << 32) |
Memory::Read32(values[7].address + 4));
}
if (values[8].size >= 4) {
activity.partySize = Memory::Read32(values[8].address);
}
if (values[9].size >= 4) {
activity.partyMax = Memory::Read32(values[9].address);
}
DiscordPresence::SetActivity(std::move(activity));
}
return ISFS_OK;
}
case DOLPHIN_IOCTL_DISCORD_RESET: case DOLPHIN_IOCTL_DISCORD_RESET:
if (numIn != 0 || numOut != 0) {
return ISFS_EINVAL;
}
if (RuntimeConfigFile::DiscordPresenceEnabled()) {
DiscordPresence::Reset();
}
return ISFS_OK; return ISFS_OK;
case DOLPHIN_IOCTL_GET_SYSTEM_TIME: { case DOLPHIN_IOCTL_GET_SYSTEM_TIME: {
@@ -331,6 +392,9 @@ extern "C" int32_t NAND_IOS_Open_HLE(uint32_t pathPtr, uint32_t mode) {
// It's a NAND file path // It's a NAND file path
const std::filesystem::path hostPath = TranslateNandPath(path); const std::filesystem::path hostPath = TranslateNandPath(path);
if (const auto result = NandCheckSystemSaveRead("IOS_Open", hostPath, mode, true))
return *result;
// Seed FaceLib resources before the existence check so every open mode can // Seed FaceLib resources before the existence check so every open mode can
// still find them on a fresh managed NAND. // still find them on a fresh managed NAND.
if (!PathExists(hostPath) && IsFaceLibResourcePath(path)) { if (!PathExists(hostPath) && IsFaceLibResourcePath(path)) {
+3 -5
View File
@@ -6,6 +6,7 @@
#include "aurora_events.h" #include "aurora_events.h"
#include "settings_overlay.h" #include "settings_overlay.h"
#include "fiber_manager.h" #include "fiber_manager.h"
#include "platform/host_platform.h"
#include "runtime_log.h" #include "runtime_log.h"
#include <dolphin/vi.h> #include <dolphin/vi.h>
@@ -20,18 +21,15 @@
#include <mutex> #include <mutex>
#include <thread> #include <thread>
#include <aurora/aurora.h>
#if defined(_WIN32) #if defined(_WIN32)
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN #ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#endif #endif
#include <windows.h> #include <windows.h>
#endif #endif
#include <aurora/aurora.h>
// Forward declaration for OSWakeupThread - used to wake threads on VI retrace queue // Forward declaration for OSWakeupThread - used to wake threads on VI retrace queue
extern "C" void OSWakeupThread_HLE_801aaaa4(CpuContext* ctx); extern "C" void OSWakeupThread_HLE_801aaaa4(CpuContext* ctx);
+270
View File
@@ -0,0 +1,270 @@
#include "host_context.h"
#if defined(_WIN32)
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#elif defined(__APPLE__) && defined(__aarch64__)
#include <sys/mman.h>
#include <unistd.h>
extern "C" void mkw_co_switch(void** targetSp, void** sourceSp);
extern "C" void* mkw_co_init(void* stackTop, void (*entry)(void*), void* argument);
#elif defined(__linux__)
#include <libco.h>
#include <cstdlib>
#include <unordered_map>
#else
#error "HostContext needs a supported cooperative-context backend"
#endif
namespace HostContext {
#if defined(_WIN32)
namespace {
thread_local bool g_convertedScheduler = false;
}
bool InitializeScheduler(Handle* scheduler)
{
void* context = ConvertThreadToFiber(nullptr);
g_convertedScheduler = context != nullptr;
if (!context) {
context = GetCurrentFiber();
}
*scheduler = context;
return context != nullptr;
}
void ShutdownScheduler(Handle scheduler)
{
if (scheduler && g_convertedScheduler) {
ConvertFiberToThread();
}
g_convertedScheduler = false;
}
Handle Create(std::size_t stackSize, Entry entry, void* argument)
{
return CreateFiber(stackSize, entry, argument);
}
void Destroy(Handle context)
{
if (context) {
DeleteFiber(context);
}
}
bool IsCurrent(Handle context)
{
return context != nullptr && GetCurrentFiber() == context;
}
void Switch(Handle target)
{
SwitchToFiber(target);
}
#elif defined(__APPLE__) && defined(__aarch64__)
namespace {
struct Context {
void* savedStackPointer = nullptr;
void* stack = nullptr;
std::size_t stackSize = 0;
};
// Guest scheduling is confined to the initialized main host thread. Keeping
// this as ordinary process state also avoids relying on Darwin TLS internals
// while executing on a manually managed stack.
Context* g_current = nullptr;
}
bool InitializeScheduler(Handle* scheduler)
{
auto* context = new Context();
g_current = context;
*scheduler = context;
return true;
}
void ShutdownScheduler(Handle scheduler)
{
auto* context = static_cast<Context*>(scheduler);
if (g_current == context) {
g_current = nullptr;
}
delete context;
}
Handle Create(std::size_t stackSize, Entry entry, void* argument)
{
auto* context = new Context();
const std::size_t guardSize = static_cast<std::size_t>(getpagesize());
const std::size_t totalSize = stackSize + guardSize;
context->stack = mmap(nullptr, totalSize, PROT_READ | PROT_WRITE,
MAP_ANON | MAP_PRIVATE, -1, 0);
if (context->stack == MAP_FAILED) {
delete context;
return nullptr;
}
// Fault on stack overflow instead of corrupting the preceding mapping.
if (mprotect(context->stack, guardSize, PROT_NONE) != 0) {
munmap(context->stack, totalSize);
delete context;
return nullptr;
}
context->stackSize = totalSize;
auto* stackTop = static_cast<char*>(context->stack) + totalSize;
context->savedStackPointer = mkw_co_init(stackTop, entry, argument);
return context;
}
void Destroy(Handle context)
{
auto* nativeContext = static_cast<Context*>(context);
if (!nativeContext) {
return;
}
if (nativeContext->stack) {
munmap(nativeContext->stack, nativeContext->stackSize);
}
delete nativeContext;
}
bool IsCurrent(Handle context)
{
return context != nullptr && context == g_current;
}
void Switch(Handle target)
{
auto* destination = static_cast<Context*>(target);
Context* source = g_current;
if (!destination || destination == source) {
return;
}
g_current = destination;
mkw_co_switch(&destination->savedStackPointer, &source->savedStackPointer);
g_current = source;
}
#elif defined(__linux__)
namespace {
struct Context {
cothread_t native = nullptr;
Entry entry = nullptr;
void* argument = nullptr;
bool ownsNative = false;
};
thread_local Context* g_current = nullptr;
thread_local std::unordered_map<cothread_t, Context*> g_contexts;
void ContextEntry()
{
const auto found = g_contexts.find(co_active());
if (found == g_contexts.end() || !found->second || !found->second->entry) {
std::abort();
}
Context* context = found->second;
g_current = context;
context->entry(context->argument);
// A guest fiber must return through FiberProc's scheduler handoff. There
// is no valid native caller to return to from libco's entry trampoline.
std::abort();
}
} // namespace
bool InitializeScheduler(Handle* scheduler)
{
auto* context = new Context();
context->native = co_active();
if (!context->native) {
delete context;
return false;
}
g_current = context;
g_contexts.emplace(context->native, context);
*scheduler = context;
return true;
}
void ShutdownScheduler(Handle scheduler)
{
auto* context = static_cast<Context*>(scheduler);
if (!context) {
return;
}
g_contexts.erase(context->native);
if (g_current == context) {
g_current = nullptr;
}
delete context;
}
Handle Create(std::size_t stackSize, Entry entry, void* argument)
{
auto* context = new Context();
context->entry = entry;
context->argument = argument;
context->native = co_create(static_cast<unsigned int>(stackSize), ContextEntry);
context->ownsNative = context->native != nullptr;
if (!context->native) {
delete context;
return nullptr;
}
g_contexts.emplace(context->native, context);
return context;
}
void Destroy(Handle context)
{
auto* nativeContext = static_cast<Context*>(context);
if (!nativeContext) {
return;
}
g_contexts.erase(nativeContext->native);
if (nativeContext->ownsNative) {
co_delete(nativeContext->native);
}
delete nativeContext;
}
bool IsCurrent(Handle context)
{
return context != nullptr && context == g_current;
}
void Switch(Handle target)
{
auto* destination = static_cast<Context*>(target);
Context* source = g_current;
if (!destination || destination == source) {
return;
}
g_current = destination;
co_switch(destination->native);
g_current = source;
}
#endif
} // namespace HostContext
+15
View File
@@ -4,6 +4,13 @@
// build/PCH) and runs from a priority-101 C initializer, ahead of every C++ dynamic initializer // build/PCH) and runs from a priority-101 C initializer, ahead of every C++ dynamic initializer
// and thus the first AVX2 code that could execute. Keep it free of anything that could pull in // and thus the first AVX2 code that could execute. Keep it free of anything that could pull in
// vectorized code: no iostreams, no std::string, no runtime-wide headers. // vectorized code: no iostreams, no std::string, no runtime-wide headers.
//
// x86-64-v3 is an x86-specific optional-feature baseline (AVX2/BMI2/FMA and friends are not
// guaranteed present on every x86_64 chip); nothing here applies on AArch64, where ASIMD/NEON is
// mandatory in the base architecture and PublicProducts.cmake never applies an -march=x86-64-v3
// equivalent flag to begin with. That branch below is a no-op stub, not a port of this check.
#if defined(__x86_64__)
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
@@ -214,3 +221,11 @@ extern "C" int MkwHostCpuBaselineInit() {
__attribute__((constructor(101))) static void MkwHostCpuBaselineCtor() { __attribute__((constructor(101))) static void MkwHostCpuBaselineCtor() {
MkwHostCpuBaselineInit(); MkwHostCpuBaselineInit();
} }
#else // !defined(__x86_64__)
extern "C" int MkwHostCpuBaselineInit() {
return 0;
}
#endif // defined(__x86_64__)
+307
View File
@@ -0,0 +1,307 @@
#include "input_bindings.h"
#include "controller_button_names.h"
#include "input_expr.h"
#include "runtime_config.h"
#include "runtime_log.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <filesystem>
#include <mutex>
#include <unordered_map>
#include <SDL3/SDL_gamepad.h>
namespace InputBindings {
namespace {
struct Binding {
std::string text;
InputExpr::Expression expr;
bool active = false;
};
std::mutex g_mutex;
std::array<std::array<Binding, kControls.size()>, PAD_CHANMAX> g_bindings;
bool g_anyBound = false;
bool g_inputBlocked = false;
// Dolphin input names, mapped onto SDL. XInput-style names are exact; DInput
// "Button <n>" indices follow the common PlayStation layout, which is what
// DInput reports for a DualShock/DualSense. Other pads may number differently.
const std::unordered_map<std::string, SDL_GamepadButton>& ButtonNames() {
static const std::unordered_map<std::string, SDL_GamepadButton> table = {
{"Button A", SDL_GAMEPAD_BUTTON_SOUTH}, {"Button B", SDL_GAMEPAD_BUTTON_EAST},
{"Button X", SDL_GAMEPAD_BUTTON_WEST}, {"Button Y", SDL_GAMEPAD_BUTTON_NORTH},
{"Shoulder L", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
{"Shoulder R", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
{"Thumb L", SDL_GAMEPAD_BUTTON_LEFT_STICK}, {"Thumb R", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
{"Start", SDL_GAMEPAD_BUTTON_START}, {"Back", SDL_GAMEPAD_BUTTON_BACK},
{"Guide", SDL_GAMEPAD_BUTTON_GUIDE},
{"Pad N", SDL_GAMEPAD_BUTTON_DPAD_UP}, {"Pad S", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
{"Pad W", SDL_GAMEPAD_BUTTON_DPAD_LEFT}, {"Pad E", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
{"Hat 0 N", SDL_GAMEPAD_BUTTON_DPAD_UP}, {"Hat 0 S", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
{"Hat 0 W", SDL_GAMEPAD_BUTTON_DPAD_LEFT}, {"Hat 0 E", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
{"Button 0", SDL_GAMEPAD_BUTTON_WEST}, {"Button 1", SDL_GAMEPAD_BUTTON_SOUTH},
{"Button 2", SDL_GAMEPAD_BUTTON_EAST}, {"Button 3", SDL_GAMEPAD_BUTTON_NORTH},
{"Button 4", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
{"Button 5", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
{"Button 8", SDL_GAMEPAD_BUTTON_BACK}, {"Button 9", SDL_GAMEPAD_BUTTON_START},
{"Button 10", SDL_GAMEPAD_BUTTON_LEFT_STICK},
{"Button 11", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
{"Button 12", SDL_GAMEPAD_BUTTON_GUIDE}, {"Button 13", SDL_GAMEPAD_BUTTON_TOUCHPAD},
};
return table;
}
// Signed axis names: SDL axis plus the direction that counts as positive.
struct AxisRef {
SDL_GamepadAxis axis;
int sign;
};
const std::unordered_map<std::string, AxisRef>& AxisNames() {
static const std::unordered_map<std::string, AxisRef> table = {
{"Axis X-", {SDL_GAMEPAD_AXIS_LEFTX, -1}}, {"Axis X+", {SDL_GAMEPAD_AXIS_LEFTX, 1}},
{"Axis Y-", {SDL_GAMEPAD_AXIS_LEFTY, -1}}, {"Axis Y+", {SDL_GAMEPAD_AXIS_LEFTY, 1}},
{"Axis Z-", {SDL_GAMEPAD_AXIS_RIGHTX, -1}}, {"Axis Z+", {SDL_GAMEPAD_AXIS_RIGHTX, 1}},
{"Axis Zr-", {SDL_GAMEPAD_AXIS_RIGHTY, -1}},{"Axis Zr+", {SDL_GAMEPAD_AXIS_RIGHTY, 1}},
{"Left X-", {SDL_GAMEPAD_AXIS_LEFTX, -1}}, {"Left X+", {SDL_GAMEPAD_AXIS_LEFTX, 1}},
{"Left Y-", {SDL_GAMEPAD_AXIS_LEFTY, 1}}, {"Left Y+", {SDL_GAMEPAD_AXIS_LEFTY, -1}},
{"Right X-", {SDL_GAMEPAD_AXIS_RIGHTX, -1}},{"Right X+", {SDL_GAMEPAD_AXIS_RIGHTX, 1}},
{"Right Y-", {SDL_GAMEPAD_AXIS_RIGHTY, 1}}, {"Right Y+", {SDL_GAMEPAD_AXIS_RIGHTY, -1}},
{"Full Axis Xr+", {SDL_GAMEPAD_AXIS_LEFT_TRIGGER, 1}},
{"Full Axis Yr+", {SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, 1}},
{"Trigger L", {SDL_GAMEPAD_AXIS_LEFT_TRIGGER, 1}},
{"Trigger R", {SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, 1}},
};
return table;
}
double ReadInput(SDL_Gamepad* gamepad, const std::string& name) {
if (gamepad == nullptr) {
return 0.0;
}
if (const auto it = ButtonNames().find(name); it != ButtonNames().end()) {
return SDL_GetGamepadButton(gamepad, it->second) ? 1.0 : 0.0;
}
if (const auto it = AxisNames().find(name); it != AxisNames().end()) {
const double raw = SDL_GetGamepadAxis(gamepad, it->second.axis) / 32767.0;
return std::clamp(raw * it->second.sign, 0.0, 1.0);
}
// Fall back to this project's own positional names, so a binding written
// here does not have to use Dolphin vocabulary.
if (const auto* native = ControllerNames::FindNativeButton(name)) {
if (PADIsAxisButton(native->nativeButton)) {
const auto axis = static_cast<SDL_GamepadAxis>(PADAxisButtonAxis(native->nativeButton));
const double sign = PADAxisButtonNegative(native->nativeButton) ? -1.0 : 1.0;
return std::clamp(SDL_GetGamepadAxis(gamepad, axis) / 32767.0 * sign, 0.0, 1.0);
}
if (native->nativeButton < SDL_GAMEPAD_BUTTON_COUNT) {
return SDL_GetGamepadButton(gamepad, static_cast<SDL_GamepadButton>(native->nativeButton)) ? 1.0
: 0.0;
}
}
return 0.0;
}
SDL_Gamepad* GamepadForPort(uint32_t port) {
const s32 index = PADGetIndexForPort(port);
return index < 0 ? nullptr : PADGetSDLGamepadForIndex(static_cast<u32>(index));
}
size_t ControlIndexForDolphinName(const std::string& name) {
for (size_t i = 0; i < kControls.size(); ++i) {
if (name == kControls[i].dolphinName) {
return i;
}
}
return kControls.size();
}
void RecomputeAnyBoundLocked() {
g_anyBound = false;
for (const auto& port : g_bindings) {
for (const auto& binding : port) {
if (!binding.expr.Empty()) {
g_anyBound = true;
return;
}
}
}
}
std::string ConfigKey(uint32_t port, size_t control) {
std::string key = "expr_" + std::to_string(port + 1) + "_";
for (const char* c = kControls[control].dolphinName; *c != '\0'; ++c) {
key += (*c == '/' || *c == '-') ? '_' : static_cast<char>(std::tolower(*c));
}
return key;
}
} // namespace
void SetInputBlocked(bool blocked) noexcept {
std::lock_guard<std::mutex> lock(g_mutex);
g_inputBlocked = blocked;
}
bool InputBlocked() noexcept {
std::lock_guard<std::mutex> lock(g_mutex);
return g_inputBlocked;
}
void Reload() noexcept {
std::lock_guard<std::mutex> lock(g_mutex);
for (uint32_t port = 0; port < PAD_CHANMAX; ++port) {
for (size_t control = 0; control < kControls.size(); ++control) {
Binding& binding = g_bindings[port][control];
binding = Binding{};
binding.text = RuntimeConfigFile::ControllerExpression(ConfigKey(port, control));
std::string error;
if (!binding.text.empty() &&
!InputExpr::Expression::Parse(binding.text, binding.expr, error)) {
RT_LOG(RT_TAG_CONFIG) << "expression for port " << (port + 1) << " "
<< kControls[control].dolphinName << ": " << error << std::endl;
}
}
}
RecomputeAnyBoundLocked();
}
void Apply(PADStatus* statuses) noexcept {
if (statuses == nullptr) {
return;
}
std::lock_guard<std::mutex> lock(g_mutex);
if (!g_anyBound) {
return;
}
const bool blocked = g_inputBlocked;
for (uint32_t port = 0; port < PAD_CHANMAX; ++port) {
if (statuses[port].err != PAD_ERR_NONE) {
continue;
}
SDL_Gamepad* gamepad = GamepadForPort(port);
const InputExpr::InputSource source = [gamepad](const std::string& name) {
return ReadInput(gamepad, name);
};
for (size_t control = 0; control < kControls.size(); ++control) {
Binding& binding = g_bindings[port][control];
if (binding.expr.Empty()) {
continue;
}
if (blocked) {
binding.active = false;
continue;
}
const double value = binding.expr.Evaluate(source);
binding.active = value > InputExpr::kConditionThreshold;
const ControlInfo& info = kControls[control];
if (info.padButton != 0 && binding.active) {
statuses[port].button |= info.padButton;
}
if (info.analog != 0) {
const double safe = std::isfinite(value) ? std::clamp(value, 0.0, 1.0) : 0.0;
const auto scaled = static_cast<uint8_t>(safe * 255.0);
uint8_t& target =
info.analog == 1 ? statuses[port].triggerLeft : statuses[port].triggerRight;
target = std::max(target, scaled);
}
}
}
}
std::string GetExpression(uint32_t port, size_t control) noexcept {
if (port >= PAD_CHANMAX || control >= kControls.size()) {
return {};
}
std::lock_guard<std::mutex> lock(g_mutex);
return g_bindings[port][control].text;
}
bool SetExpression(uint32_t port, size_t control, const std::string& text, std::string& error) noexcept {
if (port >= PAD_CHANMAX || control >= kControls.size()) {
error = "invalid control";
return false;
}
InputExpr::Expression parsed;
if (!InputExpr::Expression::Parse(text, parsed, error)) {
return false;
}
{
std::lock_guard<std::mutex> lock(g_mutex);
Binding& binding = g_bindings[port][control];
binding.text = text;
binding.expr = std::move(parsed);
binding.active = false;
RecomputeAnyBoundLocked();
}
RuntimeConfigFile::SetControllerExpression(ConfigKey(port, control), text);
return true;
}
bool IsActive(uint32_t port, size_t control) noexcept {
if (port >= PAD_CHANMAX || control >= kControls.size()) {
return false;
}
std::lock_guard<std::mutex> lock(g_mutex);
return g_bindings[port][control].active;
}
std::string DefaultDolphinConfigPath() noexcept {
std::error_code ec;
if (const char* appdata = std::getenv("APPDATA"); appdata != nullptr) {
const std::filesystem::path roaming =
std::filesystem::path(appdata) / "Dolphin Emulator" / "Config" / "GCPadNew.ini";
if (std::filesystem::exists(roaming, ec)) {
return RuntimeConfigFile::PathToUtf8(roaming);
}
}
const auto executableDirectory = RuntimeConfigFile::ExecutableDirectory();
return RuntimeConfigFile::PathToUtf8(executableDirectory ? *executableDirectory / "GCPadNew.ini"
: std::filesystem::path("GCPadNew.ini"));
}
int ImportDolphinConfig(const std::string& path, int padIndex, uint32_t port, std::string& summary,
std::string& error) noexcept {
std::vector<std::pair<std::string, std::string>> controls;
std::string device;
if (!InputExpr::ReadDolphinConfig(RuntimeConfigFile::PathFromUtf8(path), padIndex, controls, device,
error)) {
return -1;
}
int imported = 0;
std::vector<std::string> skipped;
for (const auto& [name, text] : controls) {
const size_t control = ControlIndexForDolphinName(name);
if (control == kControls.size()) {
if (name.rfind("Main Stick/", 0) == 0 || name.rfind("C-Stick/", 0) == 0) {
skipped.push_back(name);
}
continue;
}
std::string parseError;
if (!SetExpression(port, control, text, parseError)) {
skipped.push_back(name);
RT_LOG(RT_TAG_CONFIG) << "import " << name << ": " << parseError << std::endl;
continue;
}
++imported;
}
summary = "Imported " + std::to_string(imported) + " controls";
if (!device.empty()) {
summary += " from " + device;
}
if (!skipped.empty()) {
summary += "; skipped " + std::to_string(skipped.size()) +
" (stick axes and unsupported inputs keep their existing mapping)";
}
return imported;
}
} // namespace InputBindings
+631
View File
@@ -0,0 +1,631 @@
#include "input_expr.h"
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <fstream>
#include <unordered_map>
namespace InputExpr {
namespace {
using Clock = std::chrono::steady_clock;
using FSec = std::chrono::duration<double>;
enum class Kind {
Literal, Input, Not, Add, Sub, Mul, Div, And, Or, Xor,
Greater, Less, Equal,
FnIf, FnMin, FnMax, FnClamp, FnAbs, FnSqrt, FnPow, FnSin, FnCos, FnTan,
FnDeadzone, FnTimer, FnToggle, FnHold, FnTap, FnPulse, FnSmooth, FnNot,
};
struct FnInfo {
Kind kind;
int minArgs;
int maxArgs;
};
const std::unordered_map<std::string, FnInfo>& FunctionTable() {
static const std::unordered_map<std::string, FnInfo> table = {
{"not", {Kind::FnNot, 1, 1}}, {"if", {Kind::FnIf, 3, 3}},
{"min", {Kind::FnMin, 2, 2}}, {"max", {Kind::FnMax, 2, 2}},
{"clamp", {Kind::FnClamp, 3, 3}}, {"abs", {Kind::FnAbs, 1, 1}},
{"sqrt", {Kind::FnSqrt, 1, 1}}, {"pow", {Kind::FnPow, 2, 2}},
{"sin", {Kind::FnSin, 1, 1}}, {"cos", {Kind::FnCos, 1, 1}},
{"tan", {Kind::FnTan, 1, 1}}, {"deadzone", {Kind::FnDeadzone, 2, 2}},
{"timer", {Kind::FnTimer, 1, 1}}, {"toggle", {Kind::FnToggle, 1, 2}},
{"hold", {Kind::FnHold, 2, 2}}, {"tap", {Kind::FnTap, 2, 3}},
{"pulse", {Kind::FnPulse, 2, 2}}, {"smooth", {Kind::FnSmooth, 2, 3}},
};
return table;
}
} // namespace
struct Node {
Kind kind;
double literal = 0.0;
std::string input;
std::vector<std::unique_ptr<Node>> args;
// Per-instance state for the stateful functions. Mutable because Evaluate
// is logically a read of current input state.
mutable bool released = false;
mutable bool state = false;
mutable unsigned taps = 0;
mutable double value = 0.0;
mutable Clock::time_point mark = Clock::now();
mutable bool marked = false;
};
namespace {
// ---- tokenizer ----------------------------------------------------------
struct Token {
enum Type { End, Input, Number, Ident, Op, LParen, RParen, Comma } type = End;
std::string text;
};
class Lexer {
public:
explicit Lexer(const std::string& text) : m_text(text) {}
bool Next(Token& tok, std::string& error) {
while (m_pos < m_text.size() && std::isspace(static_cast<unsigned char>(m_text[m_pos]))) {
++m_pos;
}
if (m_pos >= m_text.size()) {
tok = Token{};
return true;
}
const char c = m_text[m_pos];
if (c == '`') {
const size_t close = m_text.find('`', m_pos + 1);
if (close == std::string::npos) {
error = "unterminated ` in expression";
return false;
}
tok.type = Token::Input;
tok.text = m_text.substr(m_pos + 1, close - m_pos - 1);
m_pos = close + 1;
return true;
}
if (std::isdigit(static_cast<unsigned char>(c)) || c == '.') {
size_t end = m_pos;
while (end < m_text.size() &&
(std::isdigit(static_cast<unsigned char>(m_text[end])) || m_text[end] == '.')) {
++end;
}
tok.type = Token::Number;
tok.text = m_text.substr(m_pos, end - m_pos);
m_pos = end;
return true;
}
if (std::isalpha(static_cast<unsigned char>(c)) || c == '_') {
size_t end = m_pos;
while (end < m_text.size() &&
(std::isalnum(static_cast<unsigned char>(m_text[end])) || m_text[end] == '_' ||
m_text[end] == ' ')) {
++end;
}
// Trailing spaces belong to the separator, not the identifier.
while (end > m_pos && m_text[end - 1] == ' ') {
--end;
}
tok.type = Token::Ident;
tok.text = m_text.substr(m_pos, end - m_pos);
m_pos = end;
return true;
}
if (c == '(') { tok.type = Token::LParen; ++m_pos; return true; }
if (c == ')') { tok.type = Token::RParen; ++m_pos; return true; }
if (c == ',') { tok.type = Token::Comma; ++m_pos; return true; }
if (std::string("!&|^+-*/><=").find(c) != std::string::npos) {
tok.type = Token::Op;
tok.text = std::string(1, c);
++m_pos;
return true;
}
error = std::string("unexpected character '") + c + "' in expression";
return false;
}
size_t Position() const { return m_pos; }
private:
const std::string& m_text;
size_t m_pos = 0;
};
// ---- parser -------------------------------------------------------------
using NodePtr = std::unique_ptr<Node>;
class Parser {
public:
explicit Parser(const std::string& text) : m_lexer(text) { Advance(); }
NodePtr ParseExpression(std::string& error) {
NodePtr node = ParseBinary(0, error);
if (!node) {
return nullptr;
}
if (m_failed) {
error = m_lexError;
return nullptr;
}
if (m_tok.type != Token::End) {
error = "unexpected trailing input in expression";
return nullptr;
}
return node;
}
private:
void Advance() {
if (!m_lexer.Next(m_tok, m_lexError)) {
m_tok = Token{};
m_failed = true;
}
}
static int Precedence(const std::string& op) {
if (op == "|") return 1;
if (op == "^") return 2;
if (op == "&") return 3;
if (op == ">" || op == "<" || op == "=") return 4;
if (op == "+" || op == "-") return 5;
if (op == "*" || op == "/") return 6;
return -1;
}
static Kind BinaryKind(const std::string& op) {
if (op == "|") return Kind::Or;
if (op == "^") return Kind::Xor;
if (op == "&") return Kind::And;
if (op == ">") return Kind::Greater;
if (op == "<") return Kind::Less;
if (op == "=") return Kind::Equal;
if (op == "+") return Kind::Add;
if (op == "-") return Kind::Sub;
if (op == "*") return Kind::Mul;
return Kind::Div;
}
NodePtr ParseBinary(int minPrec, std::string& error) {
NodePtr lhs = ParseUnary(error);
if (!lhs) {
return nullptr;
}
while (m_tok.type == Token::Op) {
const int prec = Precedence(m_tok.text);
if (prec < 0 || prec < minPrec) {
break;
}
const std::string op = m_tok.text;
Advance();
NodePtr rhs = ParseBinary(prec + 1, error);
if (!rhs) {
return nullptr;
}
auto node = std::make_unique<Node>();
node->kind = BinaryKind(op);
node->args.push_back(std::move(lhs));
node->args.push_back(std::move(rhs));
lhs = std::move(node);
}
return lhs;
}
NodePtr ParseUnary(std::string& error) {
if (m_failed) {
error = m_lexError;
return nullptr;
}
if (m_tok.type == Token::Op && (m_tok.text == "!" || m_tok.text == "-" || m_tok.text == "+")) {
const std::string op = m_tok.text;
Advance();
NodePtr inner = ParseUnary(error);
if (!inner) {
return nullptr;
}
if (op == "+") {
return inner;
}
auto node = std::make_unique<Node>();
if (op == "!") {
node->kind = Kind::Not;
node->args.push_back(std::move(inner));
} else {
node->kind = Kind::Sub;
auto zero = std::make_unique<Node>();
zero->kind = Kind::Literal;
node->args.push_back(std::move(zero));
node->args.push_back(std::move(inner));
}
return node;
}
return ParsePrimary(error);
}
NodePtr ParsePrimary(std::string& error) {
if (m_failed) {
error = m_lexError;
return nullptr;
}
switch (m_tok.type) {
case Token::Input: {
auto node = std::make_unique<Node>();
node->kind = Kind::Input;
node->input = m_tok.text;
Advance();
return node;
}
case Token::Number: {
auto node = std::make_unique<Node>();
node->kind = Kind::Literal;
node->literal = std::strtod(m_tok.text.c_str(), nullptr);
Advance();
return node;
}
case Token::LParen: {
Advance();
NodePtr inner = ParseBinary(0, error);
if (!inner) {
return nullptr;
}
if (m_tok.type != Token::RParen) {
error = "expected closing paren";
return nullptr;
}
Advance();
return inner;
}
case Token::Ident: {
const std::string name = m_tok.text;
Advance();
if (m_tok.type != Token::LParen) {
// A bare identifier is an input name, as Dolphin allows for
// simple cases such as "Start" or "LSHIFT".
auto node = std::make_unique<Node>();
node->kind = Kind::Input;
node->input = name;
return node;
}
const auto it = FunctionTable().find(name);
if (it == FunctionTable().end()) {
error = "unknown function '" + name + "'";
return nullptr;
}
Advance();
auto node = std::make_unique<Node>();
node->kind = it->second.kind;
if (m_tok.type != Token::RParen) {
while (true) {
NodePtr arg = ParseBinary(0, error);
if (!arg) {
return nullptr;
}
node->args.push_back(std::move(arg));
if (m_tok.type != Token::Comma) {
break;
}
Advance();
}
}
if (m_tok.type != Token::RParen) {
error = "expected closing paren after " + name + " arguments";
return nullptr;
}
Advance();
const int count = static_cast<int>(node->args.size());
if (count < it->second.minArgs || count > it->second.maxArgs) {
error = name + " takes " + std::to_string(it->second.minArgs) + " to " +
std::to_string(it->second.maxArgs) + " arguments";
return nullptr;
}
return node;
}
default:
error = "expected start of expression";
return nullptr;
}
}
Lexer m_lexer;
Token m_tok;
std::string m_lexError;
bool m_failed = false;
};
// ---- evaluator ----------------------------------------------------------
double Eval(const Node& node, const InputSource& source);
double Arg(const Node& node, size_t index, const InputSource& source) {
return Eval(*node.args[index], source);
}
double Eval(const Node& node, const InputSource& source) {
switch (node.kind) {
case Kind::Literal: return node.literal;
case Kind::Input: return source ? source(node.input) : 0.0;
case Kind::Not:
case Kind::FnNot: return 1.0 - Arg(node, 0, source);
case Kind::Add: return Arg(node, 0, source) + Arg(node, 1, source);
case Kind::Sub: return Arg(node, 0, source) - Arg(node, 1, source);
case Kind::Mul: return Arg(node, 0, source) * Arg(node, 1, source);
case Kind::Div: {
// Both sides are evaluated even when the divisor is zero: the left
// subtree may hold stateful functions that need their frame update.
const double lhs = Arg(node, 0, source);
const double rhs = Arg(node, 1, source);
return rhs == 0.0 ? 0.0 : lhs / rhs;
}
case Kind::And: return std::min(Arg(node, 0, source), Arg(node, 1, source));
case Kind::Or: return std::max(Arg(node, 0, source), Arg(node, 1, source));
case Kind::Xor: {
const double a = Arg(node, 0, source);
const double b = Arg(node, 1, source);
return std::max(std::min(a, 1.0 - b), std::min(b, 1.0 - a));
}
case Kind::Greater: return Arg(node, 0, source) > Arg(node, 1, source) ? 1.0 : 0.0;
case Kind::Less: return Arg(node, 0, source) < Arg(node, 1, source) ? 1.0 : 0.0;
case Kind::Equal: return Arg(node, 0, source) == Arg(node, 1, source) ? 1.0 : 0.0;
case Kind::FnIf:
return Arg(node, 0, source) > kConditionThreshold ? Arg(node, 1, source) : Arg(node, 2, source);
case Kind::FnMin: return std::min(Arg(node, 0, source), Arg(node, 1, source));
case Kind::FnMax: return std::max(Arg(node, 0, source), Arg(node, 1, source));
case Kind::FnClamp: {
const double v = Arg(node, 0, source);
double lo = Arg(node, 1, source);
double hi = Arg(node, 2, source);
if (lo > hi) {
std::swap(lo, hi);
}
return std::clamp(v, lo, hi);
}
case Kind::FnAbs: return std::abs(Arg(node, 0, source));
case Kind::FnSqrt: return std::sqrt(Arg(node, 0, source));
case Kind::FnPow: return std::pow(Arg(node, 0, source), Arg(node, 1, source));
case Kind::FnSin: return std::sin(Arg(node, 0, source));
case Kind::FnCos: return std::cos(Arg(node, 0, source));
case Kind::FnTan: return std::tan(Arg(node, 0, source));
case Kind::FnDeadzone: {
const double v = Arg(node, 0, source);
const double dz = std::clamp(Arg(node, 1, source), 0.0, 0.999);
return std::copysign(std::max(0.0, std::abs(v) - dz) / (1.0 - dz), v);
}
case Kind::FnTimer: {
const auto now = Clock::now();
if (!node.marked) {
node.mark = now;
node.marked = true;
}
const double period = Arg(node, 0, source);
double progress = std::chrono::duration_cast<FSec>(now - node.mark).count() / period;
if (!std::isfinite(progress) || progress < 0.0) {
progress = 0.0;
node.mark = now;
} else if (progress >= 1.0) {
const double resets = std::floor(progress);
node.mark += std::chrono::duration_cast<Clock::duration>(FSec(period * resets));
progress -= resets;
}
return progress;
}
case Kind::FnToggle: {
const double inner = Arg(node, 0, source);
if (inner < kConditionThreshold) {
node.released = true;
} else if (node.released) {
node.released = false;
node.state = !node.state;
}
if (node.args.size() == 2 && Arg(node, 1, source) > kConditionThreshold) {
node.state = false;
}
return node.state ? 1.0 : 0.0;
}
case Kind::FnHold: {
const auto now = Clock::now();
if (!node.marked) {
node.mark = now;
node.marked = true;
}
const double input = Arg(node, 0, source);
if (input < kConditionThreshold) {
node.state = false;
node.mark = now;
} else if (!node.state) {
if (std::chrono::duration_cast<FSec>(now - node.mark).count() >= Arg(node, 1, source)) {
node.state = true;
}
}
return node.state ? 1.0 : 0.0;
}
case Kind::FnTap: {
const auto now = Clock::now();
if (!node.marked) {
node.mark = now;
node.marked = true;
}
const double elapsed = std::chrono::duration_cast<FSec>(now - node.mark).count();
const double input = Arg(node, 0, source);
const bool timeUp = elapsed > Arg(node, 1, source);
// The count is user authored, so a negative or huge value must not
// reach the unsigned conversion.
double requested = node.args.size() == 3 ? Arg(node, 2, source) : 2.0;
if (!std::isfinite(requested)) {
requested = 2.0;
}
const auto desired = static_cast<unsigned>(std::clamp(requested + 0.5, 1.0, 64.0));
if (input < kConditionThreshold) {
node.released = true;
if (node.taps > 0 && timeUp) {
node.taps = 0;
}
return 0.0;
}
if (node.released) {
if (node.taps == 0) {
node.mark = now;
}
++node.taps;
node.released = false;
}
return desired == node.taps ? 1.0 : 0.0;
}
case Kind::FnPulse: {
const auto now = Clock::now();
const double input = Arg(node, 0, source);
if (input < kConditionThreshold) {
node.released = true;
} else if (node.released) {
node.released = false;
const double requested = Arg(node, 1, source);
const double safe = std::isfinite(requested) ? std::clamp(requested, 0.0, 3600.0) : 0.0;
const auto seconds = std::chrono::duration_cast<Clock::duration>(FSec(safe));
if (node.state) {
node.mark += seconds;
} else {
node.state = true;
node.mark = now + seconds;
}
}
if (node.state && now >= node.mark) {
node.state = false;
}
return node.state ? 1.0 : 0.0;
}
case Kind::FnSmooth: {
const auto now = Clock::now();
if (!node.marked) {
node.mark = now;
node.marked = true;
}
const double elapsed = std::chrono::duration_cast<FSec>(now - node.mark).count();
node.mark = now;
const double desired = Arg(node, 0, source);
const double up = Arg(node, 1, source);
const double down = node.args.size() == 3 ? Arg(node, 2, source) : up;
const double rate = (desired < node.value) ? down : up;
const double maxMove = elapsed / rate;
if (!std::isfinite(maxMove)) {
node.value = desired;
} else {
const double diff = desired - node.value;
node.value += std::copysign(std::min(maxMove, std::abs(diff)), diff);
}
return node.value;
}
}
return 0.0;
}
void Collect(const Node& node, std::vector<std::string>& out) {
if (node.kind == Kind::Input) {
if (std::find(out.begin(), out.end(), node.input) == out.end()) {
out.push_back(node.input);
}
}
for (const auto& arg : node.args) {
Collect(*arg, out);
}
}
std::string Trim(const std::string& text) {
const size_t begin = text.find_first_not_of(" \t\r\n");
if (begin == std::string::npos) {
return {};
}
return text.substr(begin, text.find_last_not_of(" \t\r\n") - begin + 1);
}
} // namespace
Expression::Expression() = default;
Expression::~Expression() = default;
Expression::Expression(Expression&&) noexcept = default;
Expression& Expression::operator=(Expression&&) noexcept = default;
bool Expression::Parse(const std::string& text, Expression& out, std::string& error) {
out.m_root.reset();
if (Trim(text).empty()) {
return true;
}
Parser parser(text);
NodePtr root = parser.ParseExpression(error);
if (!root) {
return false;
}
out.m_root = std::move(root);
return true;
}
double Expression::Evaluate(const InputSource& source) const {
if (m_root == nullptr) {
return 0.0;
}
const double value = Eval(*m_root, source);
return std::isfinite(value) ? value : 0.0;
}
std::vector<std::string> Expression::ReferencedInputs() const {
std::vector<std::string> out;
if (m_root) {
Collect(*m_root, out);
}
return out;
}
bool ReadDolphinConfig(const std::filesystem::path& path, int padIndex,
std::vector<std::pair<std::string, std::string>>& controls,
std::string& deviceName, std::string& error) {
std::ifstream file(path);
if (!file) {
error = "could not open " + path.string();
return false;
}
const std::string wanted = "[GCPad" + std::to_string(padIndex) + "]";
bool inSection = false;
bool found = false;
std::string line;
controls.clear();
deviceName.clear();
while (std::getline(file, line)) {
const std::string trimmed = Trim(line);
if (trimmed.empty() || trimmed[0] == '#' || trimmed[0] == ';') {
continue;
}
if (trimmed.front() == '[') {
inSection = trimmed == wanted;
found = found || inSection;
continue;
}
if (!inSection) {
continue;
}
const size_t eq = trimmed.find('=');
if (eq == std::string::npos) {
continue;
}
const std::string key = Trim(trimmed.substr(0, eq));
const std::string value = Trim(trimmed.substr(eq + 1));
if (key == "Device") {
deviceName = value;
} else if (!value.empty()) {
controls.emplace_back(key, value);
}
}
if (!found) {
error = wanted + " not found in " + path.string();
return false;
}
return true;
}
} // namespace InputExpr
+58
View File
@@ -23,6 +23,10 @@
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#if !defined(_WIN32)
#include <unistd.h>
#endif
#if defined(_WIN32) #if defined(_WIN32)
#ifndef NOMINMAX #ifndef NOMINMAX
#define NOMINMAX #define NOMINMAX
@@ -38,7 +42,12 @@
#include <dbghelp.h> #include <dbghelp.h>
#else #else
#include <signal.h> #include <signal.h>
#if defined(__x86_64__)
// Only the x86 POSIX fault path inspects ucontext_t to recover the page-fault
// write bit. macOS deprecates ucontext and requires _XOPEN_SOURCE just to
// include the header, while the arm64 handler does not use it at all.
#include <ucontext.h> #include <ucontext.h>
#endif
#include <unistd.h> #include <unistd.h>
#endif #endif
@@ -49,6 +58,8 @@
#include "system_bridge.h" #include "system_bridge.h"
#include "ppc_runtime.h" #include "ppc_runtime.h"
#include "aurora_events.h" #include "aurora_events.h"
#include "wii_remote_input.h"
#include "discord_presence.h"
#include "fiber_manager.h" #include "fiber_manager.h"
#include "hle_stubs.h" #include "hle_stubs.h"
#include "runtime_config.h" #include "runtime_config.h"
@@ -921,6 +932,7 @@ constexpr DWORD kCppExceptionCodeMsvc = 0xE06D7363;
// AddressSanitizer uses STATUS_FATAL_APP_EXIT when it detects an error and wants to report it. // AddressSanitizer uses STATUS_FATAL_APP_EXIT when it detects an error and wants to report it.
// We must let ASan's handler run so it can print file/line information. // We must let ASan's handler run so it can print file/line information.
constexpr DWORD kAsanFatalAppExit = 0x40000015; // STATUS_FATAL_APP_EXIT constexpr DWORD kAsanFatalAppExit = 0x40000015; // STATUS_FATAL_APP_EXIT
LONG ReportFatalSehAndExit(EXCEPTION_POINTERS* info);
void ReportStructuredException(EXCEPTION_POINTERS* info) { void ReportStructuredException(EXCEPTION_POINTERS* info) {
if (!info || !info->ExceptionRecord) { if (!info || !info->ExceptionRecord) {
@@ -1022,7 +1034,28 @@ LONG CALLBACK SehLogger(EXCEPTION_POINTERS* info) {
info->ExceptionRecord->ExceptionCode == kAsanFatalAppExit) { // ASan reporting - let it print first info->ExceptionRecord->ExceptionCode == kAsanFatalAppExit) { // ASan reporting - let it print first
return EXCEPTION_CONTINUE_SEARCH; return EXCEPTION_CONTINUE_SEARCH;
} }
// Software-raised exceptions (customer bit set) are used for internal control flow by
// system DLLs (e.g. msxml6 while mscms parses a display colour profile) and are caught
// by their own frame handlers. Only hardware faults are fatal at first chance; anything
// else that truly goes unhandled reaches UnhandledSehFilter.
if ((info->ExceptionRecord->ExceptionCode & 0x20000000u) != 0) {
return EXCEPTION_CONTINUE_SEARCH;
}
return ReportFatalSehAndExit(info);
}
LONG WINAPI UnhandledSehFilter(EXCEPTION_POINTERS* info) {
if (info == nullptr || info->ExceptionRecord == nullptr) {
return EXCEPTION_CONTINUE_SEARCH;
}
const DWORD code = info->ExceptionRecord->ExceptionCode;
if (code == kCppExceptionCodeGcc || code == kCppExceptionCodeMsvc || code == kAsanFatalAppExit) {
return EXCEPTION_CONTINUE_SEARCH;
}
return ReportFatalSehAndExit(info);
}
LONG ReportFatalSehAndExit(EXCEPTION_POINTERS* info) {
// Guard against re-entrancy: if we crash while reporting, don't recurse // Guard against re-entrancy: if we crash while reporting, don't recurse
static std::atomic_flag s_inCrashHandler = ATOMIC_FLAG_INIT; static std::atomic_flag s_inCrashHandler = ATOMIC_FLAG_INIT;
if (s_inCrashHandler.test_and_set()) { if (s_inCrashHandler.test_and_set()) {
@@ -1063,6 +1096,7 @@ LONG CALLBACK SehLogger(EXCEPTION_POINTERS* info) {
void InstallSehLogger() { void InstallSehLogger() {
if (!g_vectoredSehHandle) { if (!g_vectoredSehHandle) {
g_vectoredSehHandle = AddVectoredExceptionHandler(1, SehLogger); g_vectoredSehHandle = AddVectoredExceptionHandler(1, SehLogger);
SetUnhandledExceptionFilter(UnhandledSehFilter);
} }
} }
#else #else
@@ -1268,6 +1302,7 @@ static void TerminateHandler() {
std::_Exit(EXIT_FAILURE); std::_Exit(EXIT_FAILURE);
} }
// Runtime entry point: loads the configuration, brings up aurora and runs the game.
int RuntimeMain(int argc, char** argv) { int RuntimeMain(int argc, char** argv) {
// Must run before the transcript duplicates stdout/stderr: it decides what // Must run before the transcript duplicates stdout/stderr: it decides what
// those descriptors are mirrored to now that the products are GUI-subsystem. // those descriptors are mirrored to now that the products are GUI-subsystem.
@@ -1292,6 +1327,9 @@ int RuntimeMain(int argc, char** argv) {
throw std::invalid_argument("The game runtime does not accept command-line options; use Config.toml through the installed host."); throw std::invalid_argument("The game runtime does not accept command-line options; use Config.toml through the installed host.");
} }
RuntimeConfigFile::LogLoadedConfig(); RuntimeConfigFile::LogLoadedConfig();
if (RuntimeConfigFile::DiscordPresenceEnabled()) {
DiscordPresence::Initialize(RuntimeConfigFile::DiscordClientId(), "Mario Kart Wii");
}
SystemBridge::Initialize(); SystemBridge::Initialize();
TranslatedFunctionRegistry::Finalize(); TranslatedFunctionRegistry::Finalize();
@@ -1343,9 +1381,21 @@ int RuntimeMain(int argc, char** argv) {
const char* configName; const char* configName;
AuroraBackend backend; AuroraBackend backend;
}; };
#if defined(__APPLE__)
static constexpr std::array<GraphicsBackendEntry, 2> kGraphicsBackends{{
{"auto", BACKEND_AUTO}, {"metal", BACKEND_METAL},
}};
// only vulkan for linux
#elif defined(__linux__)
static constexpr std::array<GraphicsBackendEntry, 2> kGraphicsBackends{{
{"auto", BACKEND_AUTO}, {"vulkan", BACKEND_VULKAN},
}};
#elif defined(_WIN32)
static constexpr std::array<GraphicsBackendEntry, 3> kGraphicsBackends{{ static constexpr std::array<GraphicsBackendEntry, 3> kGraphicsBackends{{
{"auto", BACKEND_AUTO}, {"d3d12", BACKEND_D3D12}, {"vulkan", BACKEND_VULKAN}, {"auto", BACKEND_AUTO}, {"d3d12", BACKEND_D3D12}, {"vulkan", BACKEND_VULKAN},
}}; }};
#endif
const auto backendDisplayName = [](AuroraBackend value) -> const char* { const auto backendDisplayName = [](AuroraBackend value) -> const char* {
for (const auto& entry : kGraphicsBackends) { for (const auto& entry : kGraphicsBackends) {
if (entry.backend == value) { if (entry.backend == value) {
@@ -1364,6 +1414,11 @@ int RuntimeMain(int argc, char** argv) {
} }
const AuroraBackend requestedBackend = auroraConfig.desiredBackend; const AuroraBackend requestedBackend = auroraConfig.desiredBackend;
// SDL only reads its Wii driver hint when the joystick subsystem starts, which
// aurora_initialize does; a Bluetooth Wii Remote paired before launch must be
// visible on that first scan.
WiiRemoteInput::ConfigureSdlHints(RuntimeConfigFile::WiiRemotesEnabled(true));
const AuroraInfo auroraInfo = aurora_initialize(0, nullptr, &auroraConfig); const AuroraInfo auroraInfo = aurora_initialize(0, nullptr, &auroraConfig);
if (requestedBackend != BACKEND_AUTO && auroraInfo.backend != requestedBackend) { if (requestedBackend != BACKEND_AUTO && auroraInfo.backend != requestedBackend) {
RT_LOG(RT_TAG_RUNTIME) << "graphics_api=\"" << backend RT_LOG(RT_TAG_RUNTIME) << "graphics_api=\"" << backend
@@ -1418,6 +1473,7 @@ int RuntimeMain(int argc, char** argv) {
Fiber::GuestFiberManager::Shutdown(); Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true); WindowPlacementPersistence::Flush(true);
aurora_shutdown(); aurora_shutdown();
DiscordPresence::Shutdown();
SetRuntimeExitCodeImpl(0); SetRuntimeExitCodeImpl(0);
ShutdownProcessTranscript(); ShutdownProcessTranscript();
return 0; return 0;
@@ -1435,6 +1491,7 @@ int RuntimeMain(int argc, char** argv) {
Fiber::GuestFiberManager::Shutdown(); Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true); WindowPlacementPersistence::Flush(true);
aurora_shutdown(); aurora_shutdown();
DiscordPresence::Shutdown();
ShutdownProcessTranscript(); ShutdownProcessTranscript();
return 1; return 1;
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
@@ -1446,6 +1503,7 @@ int RuntimeMain(int argc, char** argv) {
Fiber::GuestFiberManager::Shutdown(); Fiber::GuestFiberManager::Shutdown();
WindowPlacementPersistence::Flush(true); WindowPlacementPersistence::Flush(true);
aurora_shutdown(); aurora_shutdown();
DiscordPresence::Shutdown();
ShutdownProcessTranscript(); ShutdownProcessTranscript();
return 1; return 1;
} }
+258
View File
@@ -16,6 +16,11 @@
#include <winrt/Windows.Foundation.h> #include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h> #include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Media.Control.h> #include <winrt/Windows.Media.Control.h>
#elif defined(__linux__)
#include <dlfcn.h>
#include <string>
#include <vector>
#endif #endif
namespace MusicAttenuation { namespace MusicAttenuation {
@@ -191,6 +196,256 @@ void MonitorWindowsMediaSessions() noexcept {
<< error.message().c_str() << std::endl; << error.message().c_str() << std::endl;
} }
} }
#elif defined(__linux__)
// For linux - watches MPRIS players on the D-Bus session bus and drives the
// same g_externalMediaPlaying / g_mediaControl* atomics.
//
// libdbus-1 is loaded with dlopen rather than linked so a machine with no
// session bus (headless, D-Bus absent, some containers) can still run the game,
// here, the monitor reports "unavailable" like the Windows does
// when the media-session API is missing.
extern "C" {
struct MprisDBusConnection;
struct MprisDBusMessage;
// Mirrors DBusError from dbus-errors.h (frozen public layout).
struct MprisDBusError {
const char* name;
const char* message;
unsigned int dummy1 : 1;
unsigned int dummy2 : 1;
unsigned int dummy3 : 1;
unsigned int dummy4 : 1;
unsigned int dummy5 : 1;
void* padding1;
};
// Mirrors DBusMessageIter from dbus-message.h
struct MprisDBusIter {
void* dummy1;
void* dummy2;
uint32_t dummy3;
int dummy4;
int dummy5;
int dummy6;
int dummy7;
int dummy8;
int dummy9;
int dummy10;
int dummy11;
int pad1;
int pad2;
void* pad3;
};
} // extern "C"
constexpr int kMprisBusSession = 0; // DBUS_BUS_SESSION
constexpr int kMprisTypeInvalid = 0; // DBUS_TYPE_INVALID
constexpr int kMprisTypeString = 's'; // DBUS_TYPE_STRING
constexpr int kMprisTypeArray = 'a'; // DBUS_TYPE_ARRAY
constexpr int kMprisTypeVariant = 'v';// DBUS_TYPE_VARIANT
// Short blocking timeout so a wedged player can never stall the poll thread for
// the full libdbus default (25s).
constexpr int kMprisSendTimeoutMs = 500;
constexpr char kMprisNamePrefix[] = "org.mpris.MediaPlayer2.";
struct MprisDBus {
void* handle = nullptr;
MprisDBusConnection* (*bus_get_private)(int, MprisDBusError*) = nullptr;
void (*connection_close)(MprisDBusConnection*) = nullptr;
void (*connection_unref)(MprisDBusConnection*) = nullptr;
void (*connection_set_exit_on_disconnect)(MprisDBusConnection*, unsigned int) = nullptr;
void (*error_init)(MprisDBusError*) = nullptr;
void (*error_free)(MprisDBusError*) = nullptr;
MprisDBusMessage* (*message_new_method_call)(const char*, const char*, const char*,
const char*) = nullptr;
unsigned int (*message_append_args)(MprisDBusMessage*, int, ...) = nullptr;
MprisDBusMessage* (*send_with_reply_and_block)(MprisDBusConnection*, MprisDBusMessage*, int,
MprisDBusError*) = nullptr;
void (*message_unref)(MprisDBusMessage*) = nullptr;
unsigned int (*message_iter_init)(MprisDBusMessage*, MprisDBusIter*) = nullptr;
unsigned int (*message_iter_next)(MprisDBusIter*) = nullptr;
void (*message_iter_recurse)(MprisDBusIter*, MprisDBusIter*) = nullptr;
int (*message_iter_get_arg_type)(MprisDBusIter*) = nullptr;
void (*message_iter_get_basic)(MprisDBusIter*, void*) = nullptr;
template <typename Fn>
bool Bind(Fn& fn, const char* symbol) noexcept {
fn = reinterpret_cast<Fn>(dlsym(handle, symbol));
return fn != nullptr;
}
bool Load() noexcept {
handle = dlopen("libdbus-1.so.3", RTLD_NOW | RTLD_LOCAL);
if (handle == nullptr) {
return false;
}
const bool ok =
Bind(bus_get_private, "dbus_bus_get_private") &&
Bind(connection_close, "dbus_connection_close") &&
Bind(connection_unref, "dbus_connection_unref") &&
Bind(connection_set_exit_on_disconnect, "dbus_connection_set_exit_on_disconnect") &&
Bind(error_init, "dbus_error_init") &&
Bind(error_free, "dbus_error_free") &&
Bind(message_new_method_call, "dbus_message_new_method_call") &&
Bind(message_append_args, "dbus_message_append_args") &&
Bind(send_with_reply_and_block, "dbus_connection_send_with_reply_and_block") &&
Bind(message_unref, "dbus_message_unref") &&
Bind(message_iter_init, "dbus_message_iter_init") &&
Bind(message_iter_next, "dbus_message_iter_next") &&
Bind(message_iter_recurse, "dbus_message_iter_recurse") &&
Bind(message_iter_get_arg_type, "dbus_message_iter_get_arg_type") &&
Bind(message_iter_get_basic, "dbus_message_iter_get_basic");
if (!ok) {
dlclose(handle);
handle = nullptr;
return false;
}
return true;
}
// org.freedesktop.DBus.ListNames
bool ListNames(MprisDBusConnection* conn, std::vector<std::string>& out) noexcept {
MprisDBusMessage* msg = message_new_method_call(
"org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames");
if (msg == nullptr) {
return false;
}
MprisDBusError err;
error_init(&err);
MprisDBusMessage* reply = send_with_reply_and_block(conn, msg, kMprisSendTimeoutMs, &err);
message_unref(msg);
if (reply == nullptr) {
error_free(&err);
return false;
}
error_free(&err);
MprisDBusIter iter;
MprisDBusIter sub;
if (message_iter_init(reply, &iter) &&
message_iter_get_arg_type(&iter) == kMprisTypeArray) {
message_iter_recurse(&iter, &sub);
while (message_iter_get_arg_type(&sub) == kMprisTypeString) {
const char* name = nullptr;
message_iter_get_basic(&sub, &name);
if (name != nullptr) {
out.emplace_back(name);
}
message_iter_next(&sub);
}
}
message_unref(reply);
return true;
}
// org.freedesktop.DBus.Properties.Get(Player, "PlaybackStatus") for one
// MPRIS player - the reply is a variant wrapping a string (eg: "Playing").
bool PlaybackStatus(MprisDBusConnection* conn, const char* busName, std::string& out) noexcept {
MprisDBusMessage* msg = message_new_method_call(
busName, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "Get");
if (msg == nullptr) {
return false;
}
const char* iface = "org.mpris.MediaPlayer2.Player";
const char* prop = "PlaybackStatus";
if (!message_append_args(msg, kMprisTypeString, &iface, kMprisTypeString, &prop,
kMprisTypeInvalid)) {
message_unref(msg);
return false;
}
MprisDBusError err;
error_init(&err);
MprisDBusMessage* reply = send_with_reply_and_block(conn, msg, kMprisSendTimeoutMs, &err);
message_unref(msg);
if (reply == nullptr) {
error_free(&err);
return false;
}
error_free(&err);
bool haveValue = false;
MprisDBusIter iter;
MprisDBusIter variant;
if (message_iter_init(reply, &iter) &&
message_iter_get_arg_type(&iter) == kMprisTypeVariant) {
message_iter_recurse(&iter, &variant);
if (message_iter_get_arg_type(&variant) == kMprisTypeString) {
const char* value = nullptr;
message_iter_get_basic(&variant, &value);
if (value != nullptr) {
out.assign(value);
haveValue = true;
}
}
}
message_unref(reply);
return haveValue;
}
};
void MonitorLinuxMprisSessions() noexcept {
using namespace std::chrono_literals;
MprisDBus dbus;
if (!dbus.Load()) {
g_mediaControlAvailable.store(false, std::memory_order_release);
g_externalMediaPlaying.store(false, std::memory_order_release);
g_mediaControlInitializationComplete.store(true, std::memory_order_release);
std::cerr << "[audio] MPRIS media monitoring unavailable: libdbus-1 could not be loaded"
<< std::endl;
return;
}
MprisDBusError err;
dbus.error_init(&err);
MprisDBusConnection* conn = dbus.bus_get_private(kMprisBusSession, &err);
if (conn == nullptr) {
g_mediaControlAvailable.store(false, std::memory_order_release);
g_externalMediaPlaying.store(false, std::memory_order_release);
g_mediaControlInitializationComplete.store(true, std::memory_order_release);
std::cerr << "[audio] MPRIS media monitoring unavailable: "
<< (err.message != nullptr ? err.message : "no D-Bus session bus") << std::endl;
dbus.error_free(&err);
return;
}
dbus.error_free(&err);
// A private bus connection defaults to terminating the process when the bus drops.
dbus.connection_set_exit_on_disconnect(conn, 0u);
for (bool firstPoll = true;; firstPoll = false) {
bool playing = false;
std::vector<std::string> names;
const bool queried = dbus.ListNames(conn, names);
if (queried) {
for (const auto& name : names) {
if (name.compare(0, sizeof(kMprisNamePrefix) - 1, kMprisNamePrefix) != 0) {
continue;
}
std::string status;
if (dbus.PlaybackStatus(conn, name.c_str(), status) && status == "Playing") {
playing = true;
break;
}
}
}
g_externalMediaPlaying.store(playing, std::memory_order_release);
g_mediaControlAvailable.store(queried, std::memory_order_release);
if (firstPoll) {
g_mediaControlInitializationComplete.store(true, std::memory_order_release);
if (!queried) {
std::cerr << "[audio] MPRIS media monitoring unavailable: session bus query failed"
<< std::endl;
}
}
std::this_thread::sleep_for(250ms);
}
}
#endif #endif
void StartMonitor() noexcept { void StartMonitor() noexcept {
@@ -198,6 +453,9 @@ void StartMonitor() noexcept {
// The process owns this monitor for its remaining lifetime. Keeping it // The process owns this monitor for its remaining lifetime. Keeping it
// detached avoids shutdown ordering between WinRT and static audio state. // detached avoids shutdown ordering between WinRT and static audio state.
std::thread(MonitorWindowsMediaSessions).detach(); std::thread(MonitorWindowsMediaSessions).detach();
#elif defined(__linux__)
// Detached so there is no shutdown ordering to manage against static audio state.
std::thread(MonitorLinuxMprisSessions).detach();
#else #else
g_mediaControlAvailable.store(false, std::memory_order_release); g_mediaControlAvailable.store(false, std::memory_order_release);
g_mediaControlInitializationComplete.store(true, std::memory_order_release); g_mediaControlInitializationComplete.store(true, std::memory_order_release);
+85
View File
@@ -0,0 +1,85 @@
#include "platform/host_platform.h"
#include <cstdlib>
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <shlobj.h>
#else
#include <unistd.h>
#endif
#if defined(__APPLE__)
#include <mach-o/dyld.h>
#include <pwd.h>
#endif
namespace RuntimePlatform {
std::optional<std::filesystem::path> ExecutableDirectory() noexcept {
#if defined(_WIN32)
std::wstring buffer(MAX_PATH, L'\0');
for (;;) {
const DWORD length = GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
if (length == 0) {
return std::nullopt;
}
if (length < buffer.size() - 1) {
buffer.resize(length);
return std::filesystem::path(buffer).parent_path();
}
buffer.resize(buffer.size() * 2);
}
#elif defined(__APPLE__)
uint32_t size = 0;
if (_NSGetExecutablePath(nullptr, &size) != -1 || size == 0) {
return std::nullopt;
}
std::string path(size, '\0');
if (_NSGetExecutablePath(path.data(), &size) != 0) {
return std::nullopt;
}
path.resize(std::char_traits<char>::length(path.c_str()));
std::error_code ec;
const auto resolved = std::filesystem::weakly_canonical(path, ec);
return (ec ? std::filesystem::path(path) : resolved).parent_path();
#else
return std::nullopt;
#endif
}
std::filesystem::path ApplicationDataDirectory(std::string_view applicationName) {
#if defined(_WIN32)
PWSTR rawPath = nullptr;
if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_CREATE, nullptr, &rawPath)) && rawPath) {
const std::filesystem::path directory = std::filesystem::path(rawPath) / applicationName;
CoTaskMemFree(rawPath);
return directory;
}
#elif defined(__APPLE__)
if (const char* home = std::getenv("HOME"); home && *home) {
return std::filesystem::path(home) / "Library" / "Application Support" / applicationName;
}
if (const passwd* user = getpwuid(getuid()); user && user->pw_dir && *user->pw_dir) {
return std::filesystem::path(user->pw_dir) / "Library" / "Application Support" / applicationName;
}
#endif
return std::filesystem::current_path() / applicationName;
}
std::filesystem::path LogDirectory(std::string_view applicationName) {
return ApplicationDataDirectory(applicationName) / "Logs";
}
uint64_t CurrentProcessId() noexcept {
#if defined(_WIN32)
return static_cast<uint64_t>(::GetCurrentProcessId());
#else
return static_cast<uint64_t>(::getpid());
#endif
}
} // namespace RuntimePlatform

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