Compare commits

..

100 Commits

Author SHA1 Message Date
patchzyy dd6f596b45 Update to 0.2.34 2026-10-08 20:40:33 +02:00
Michael G 6fa24737d3 feat(macos): back guest flat memory with anonymous vm_remap aliases (#252)
Replace /tmp unlinked temporary files and file-backed mmap with anonymous host pages and Mach mach_vm_remap shared aliases, eliminating disk I/O, APFS churn, and SSD write amplification on macOS.

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

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

* docs(macos): document Setup.pkg installation

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

* macos: pin Apple Silicon deployment target

* fix(macos): restore Retro Rewind local builds

* macos: package universal setup tools

* ci: build macOS input expression tests

* runtime: Do not force 14.0 minimum anymore

new minimum is 12.0

* macos: support older libc++ algorithms

* macos: allow undefined MTLLogStateDescriptor for older SDKs

* tests: deflake input expression timing window test

* build: Fix mac tests

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

* feat(macos): add MetalFX spatial upscaling

* Fix automatic music muting on macOS

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

* Address CodeRabbit review comments and integrate upstream TLS cmake

* Remove version requirement for running package CI

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

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

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

* cmake: synchronize CMAKE_SYSTEM_PROCESSOR with CMAKE_OSX_ARCHITECTURES on macOS

* fix rpaths

---------

Co-authored-by: DarthM <mgracer48@yahoo.com>
Co-authored-by: Michael G <10155689+DarthMDev@users.noreply.github.com>
Co-authored-by: Daan Vervacke <23398694+DaanVervacke@users.noreply.github.com>
Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
2026-10-03 11:07:15 +02:00
patchzyy 9d182f8316 Split PSQ fallbacks and add PSQ ISA tests (#278) 2026-10-02 10:34:51 +02:00
matellush 75886669bc feat: Facilitate reproducible builds on non-Windows systems (#270) 2026-09-30 19:06:18 +02:00
matellush a50bad2970 fix: Use DOWNLOAD_EXTRACT_TIMESTAMP consistently (#276)
* fix: Use `DOWNLOAD_EXTRACT_TIMESTAMP` consistently

* fix: Set all extracted dependency files' timestamps to extraction time
2026-09-30 19:05:47 +02:00
patchzyy 9b7b9913e4 Handle extra Mbed TLS archives (#275) 2026-09-30 09:14:48 +02:00
matellush a88b7b502b feat: Bump xxHash to 0.8.4 and use new CMake subdirectory (#271) 2026-09-29 21:58:57 +02:00
patchzyy d0d58072d1 Bundle mbed TLS in Linux native prebuilt (#272) 2026-09-29 21:50:19 +02:00
patchzyy 6b853ca371 Fix Wii certificates (#265)
confirmed as fixed by forgottonice (in dms)
2026-09-29 20:03:35 +02:00
patchzyy 77a8623416 Fix Mac THP memory path (#267) 2026-09-29 20:01:39 +02:00
patchzyy 10ab54adee Fix native build paths for special characters (#266) 2026-09-29 20:01:03 +02:00
patchzyy a05c89739d Always refresh Retro-WFC payload with fallback (#264) 2026-09-28 21:01:40 +02:00
patchzyy 82991ec337 Version centrilsation 2026-09-27 13:00:12 +02:00
patchzyy e164af9ff4 Shader wait screen 2026-09-27 12:39:10 +02:00
patchzyy e409d9f99b undo black kart fix
it causes stutters
2026-09-27 12:11:03 +02:00
Michael G 85f250155f Fix controller mapping platform, player index sync, and socket errors (#251)
* Fix macOS controller platform, sync player indices, and preserve socket send error

- Preserve captured socket send error across diagnostic logging in network_socket.cpp
- Use SDL_GetPlatform() instead of hardcoded Windows in controller mapping wizard
- Synchronize Aurora and SDL player indices on port assignment and clear in pad.cpp
- Auto-assign Player 1 on macOS when unconfigured in input.cpp

* fix(input): refine macOS port 0 assignment and persistence logic

- Persist port 0 preference for newly connected controller even if already assigned player index 0
- Verify controller has no preference on other ports before assigning to port 0
2026-09-24 22:01:49 +02:00
patchzyy a9c9c0de26 Add force 16:9 viewport mode (#256) 2026-09-24 22:00:57 +02:00
patchzyy c92e45c2e7 Add early Linux linker dependency probe (#255) 2026-09-24 18:53:56 +02:00
patchzyy 583e702547 add bltl (#254) 2026-09-24 18:06:32 +02:00
dorPXP b59e035b87 Add TLS support for non-Windows devices (#144)
* Implement real TLS for non-Windows via vendored mbed TLS

Windows gets TLS for the guest network HLE's SSL ioctlvs for free from
Schannel; every other platform fell into a stub that always returned
failure, meaning any HTTPS-based network feature (WFC login, fetching
the Retro-WFC payload) silently could not work at all on those
platforms regardless of server availability.

Vendors mbed TLS 3.6.7 LTS under runtime/third_party/mbedtls (same
convention as Crypto++/pugixml - a real source checkout, not a
submodule/FetchContent download) and a standard Mozilla CA bundle
(runtime/assets/certs/cacert.pem, via curl.se's redistribution) copied
next to the built product the same way dsp_coef.bin already is.

Verified against real HTTPS servers: a valid certificate completes the
handshake and an HTTP round-trip; a known-expired certificate is
correctly rejected with a real X509 verification failure, not silently
accepted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qmdewk7VfVVJTfCVd2WStu

* Fix TLS handshake hang and partial-write truncation on non-Windows

Add a POSIX socket timeout to match Windows' existing 15s one, plus a
deadline on the handshake retry loop itself, so a peer that accepts the
TCP connection but never sends TLS data can no longer hang the thread
forever. Also fix SslWrite to loop on partial mbedTLS writes instead of
returning the first partial count, and add mbedTLS to
THIRD-PARTY-NOTICES.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Fetch mbedTLS from a pinned, checksum-verified release instead of vendoring it

Replace the committed mbedTLS source tree with a CMake FetchContent download
of the official mbedtls-3.6.7 release tarball, verified against its signed
SHA-256, matching how aurora-main's own dependencies (SDL, zlib, etc.) are
pulled in. Ships the compiled dependency instead of ~280 tracked upstream
files. CA bundle packaging and THIRD-PARTY-NOTICES.md coverage are unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Limit the mbedTLS dependency to the platforms that use it

The FetchContent block ran on every platform, including Windows, whose builds
configure with FETCHCONTENT_FULLY_DISCONNECTED=ON against the offline
dependency set from Launcher/Prepare-Dependencies.ps1 - which has no
mkw_mbedtls_upstream entry, so a clean Windows configure failed. Windows
compiles the Schannel path (network_ssl.cpp is `#ifndef _WIN32` for mbed TLS)
and never links mbed TLS, so nothing needs preparing there: the fetch, the
linkage and the cacert.pem copy are now guarded to non-Windows, while the
mkw::mbedtls alias stays defined everywhere so the link lines in
PublicProducts.cmake remain platform-independent.

Also copy cacert.pem alongside the installed executable in the Linux and macOS
publication paths (Launcher/local-build.sh and Launcher/macos/publish-app.command),
which already copied the other runtime assets but left the TLS root bundle in
the build directory, so published builds could not verify any certificate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Harden mbed TLS socket I/O handling

* delete wii socket

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
2026-09-24 13:50:33 +02:00
patchzyy 6f14bde26a Kartpad upstream fixes (#244)
* Preserve interrupted registers and unwind alarm guards before rescheduling

Adapt the RFL interrupt-context and alarm reschedule fixes from KartPad ed8e4ca and 0c9bff0. Keep caller registers private and release the recursion guard before a woken fiber can pump callbacks.

* Keep local Wii identity services available when networking is disabled

Adapt KartPad a0f3fb5. Only IP and SSL devices require network access; KD request/time and NCD management remain available for offline save and license initialization.

* Share repeated LR continuation dispatch in translated functions

Adapt KartPad be91d8f/a3f90eb without its floating-point ABI changes. Preserve upstream continuation discovery and all resume labels. Validation: 640 translator tests passed.

* Reject inconsistent GPU cache sizes before allocation or copying

Adapt KartPad runtime 70951022. Validate raw lengths, compression tags and Zstd frame lengths on the size probe as well as the fetch. Tested against malformed SQLite rows and valid raw/compressed round trips.

* Wake compiler workers when pipeline work becomes runnable

Adapt KartPad runtime 956d811e. Wake all consumers of the shared condition variable after queue insertion or promotion; retain upstream desktop prewarm policy. A blocked-compiler probe verified progress by an idle worker.

* Reuse and release one Metal view per SDL window

Adapt KartPad 3606741. Surface recreation reuses the existing view and window property cleanup owns its lifetime. Reviewed against SDL3 cleanup semantics; Apple hardware validation remains outstanding.

* Avoid overreading packed three-byte vertex attributes

Adapt KartPad 0f6b274. Do not read a second storage word when all three requested bytes fit in the first. Preserve upstream depth and fog corrections.

* Keep interpolation history within each split-screen viewport

Adapt KartPad d6299b5. Scope exact, material and sibling-palette matching to the logical viewport so identical meshes from different cameras cannot share transforms.

* Report graphics startup failures and safely clean up partial ImGui initialization

Adapt KartPad runtime 70dc9380 and c4566e50 using the existing WiiCompiled exception/reporting path. A dummy-video-driver probe verified error return and repeated partial shutdown without aborting.

* Preserve GX draw boundaries and GPU staging and readback state

Adapt the validated renderer fixes from KartPad runtime 31add0c3, 7393dafe, b7f515de, cf46a9c7, fad42a7b, 7cd09b69, 9feea6b2 and Android 2505ae22 to current upstream. Preserve complete primitives and fresh vertex layouts, split staging batches before overflow, retain offscreen state, scope asynchronous callbacks and frame state, and complete texture-copy sources.

Add unit regressions and an optional ROM-free GPU pixel test. Validation: 250 GX tests and actual D3D12 pixel/readback, capacity, interpolation and frame-worker checks passed with Dawn validation enabled.
2026-09-23 19:27:51 +02:00
Daan Vervacke 83463764b8 [Linux] Handle NAND moves across mount points (#212)
* Fix NAND moves across mount points

* Harden cross-mount NAND move fallback

* Preserve directory copy on NAND move cleanup failure

* Clarify NAND move cleanup behavior

* Use exclusive scratch paths for NAND moves

* Copy NAND move directories into reserved destination

* Publish NAND move directories without replacement
2026-09-19 00:33:59 +02:00
Nicholas Bly 8008d885ad Fix controller input leaking when exit prompt is open (#233)
* Add exit prompt check to input blocking logic

* Improve settings hint layout and exit handling

Refactor settings input hint display and exit prompt logic.

* Simplify input blocking with ApplyInputBlockState

Refactored input blocking logic into ApplyInputBlockState function.
2026-09-19 00:22:48 +02:00
patchzyy 7e6604c415 Why did this change the readme?
I merged a PR and missed it changed the readme...
2026-09-15 22:53:20 +02:00
patchzyy 6fb593749e version 2026-09-14 20:36:18 +02:00
theofficialgman 8ec3a1b752 Switch to custom patched llvm 22.1.8 in Linux appimage builder (#214)
* linux prepare-portable-tools: derive llvm and cmake asset paths directly from archive names

* switch to patched LLVM 22.X release

Release assets are extracted and re-uploaded from official CI run on a WIP PR: https://github.com/llvm/llvm-project/actions/runs/34549173201?pr=222821
2026-09-14 18:27:49 +02:00
Nicholas Bly 8705e957c7 Fix autohide cursor + mute hotkey (#211)
* Fix autohide cursor + mute hotkey

* fixes

* Fix rebinding + spacing fixes
2026-09-14 17:00:08 +02:00
theofficialgman 8e0cc96898 Scope Kamek bl-patch LR-continuation detection to genuine skip-return… (#218)
* Scope Kamek bl-patch LR-continuation detection to genuine skip-return targets

Fix crash from Kamek skip-return hooks (Item Rain crash) (#182) added every
Kamek BranchLink patch target to lrContinuationCallTargets unconditionally,
with no filter analogous to the RetroWfcHookSetsLinkRegister check already
used for RetroWFC hooks. Since bl is the ordinary PowerPC call instruction,
this made the codegen treat effectively every patched call in the mod as a
potential skip-return hook, forcing conservative handling (full register
reload, disabled resident-call fast paths, local LR-continuation dispatch
tables) onto thousands of calls that just return normally.

For Retro Rewind this inflated total translated mod size by +42%
(1,414,327 -> 2,005,284 lines), concentrated in ~10 unrelated overlay
functions that happened to call a patched target, and was enough to make
one aggregate build shard pathologically slow to compile (hangs Linux CI).

Instead, only mark a bl target as LR-continuation-aware if a lightweight
discovery-only decode of its own body actually finds evidence of
skip-return behavior via DiscoverLrRelativeIndirectJumpOffsets. Falls back
to the conservative (old) behavior if a target can't be statically
analyzed, so no skip-return case is silently missed.

Verified against the real Retro Rewind mod: total mod size returns to
1,416,350 lines (+0.14% vs. pre-fix, down from +42%), all 6 genuinely new
continuation functions from the original fix are preserved, zero
functions lost, and all 609 existing translator tests still pass.

* Distinguish exhausted from truncated LR-relative offset search

CodeRabbit flagged that TargetExhibitsLrSkipReturn (added in ad2d4e7) treated
an empty DiscoverLrRelativeIndirectJumpOffsets result as a verified "this
target never skip-returns," but the analysis silently drops any path state
once more than MaxStatesPerInstruction (16) distinct states reach one
instruction - a bctr/return on a dropped state can never contribute its
offset, so an empty result could be an incomplete search rather than a real
negative. Treating every capped case as "skip-return possible" outright was
rejected as too broad a fallback given how conservative/expensive that path
already is.

Instead: raise MaxStatesPerInstruction 16 -> 512 (an arbitrary conservative
bound to begin with, not something correctness depended on) so genuinely
branchy functions have far more headroom to reach an exhaustive answer, and
give DiscoverLrRelativeIndirectJumpOffsets an optional onStateCapExceeded
callback that fires exactly when a state is dropped. TargetExhibitsLrSkipReturn
now only falls back to the conservative "treat as skip-return" answer when
the search both found nothing and the cap was actually hit during that run -
not whenever the cap merely exists - so a target is trusted as clean once the
search genuinely exhausts it.

Verified: all 609 translator tests pass, and a full translate-mod run against
the real Retro Rewind mod produces byte-for-byte identical output to the
prior fix (same 4,065 functions, 1,416,350 total lines) - confirming the
16-state cap was never actually the limiting factor in practice and this
change is a pure safety-net closure, not a behavior change for this mod.

* Add LR continuation regression tests

* Refine LR continuation hook analysis

---------

Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
2026-09-14 16:47:28 +02:00
jamie 6458ec6abe feat(linux): add --sysroot plumbing for bundled toolchains (#224)
* feat(linux): add --sysroot plumbing for bundled toolchains

* fix(linux): clear cached CMAKE_SYSROOT when --sysroot is omitted

* fix(linux): reject null/empty --sysroot in install command
2026-09-13 22:49:40 +02:00
theofficialgman 209405dfb7 switch to dawn-build fork building on Ubuntu 22.04 rather than 24.04 runners (#215)
fixes https://github.com/patchzyy/Wiicompiled/issues/159
previous requirement for ubuntu 24.04+ libstdc++ inherited from dawn prebuilds now dropped to ubuntu 22.04+ libstdc++ like the rest of the prebuilds

also add all architectures to the URL_HASH check since the dawn tag doesn't change but the binaries have
2026-09-13 14:15:30 +02:00
Nicholas Bly 4bdaff01fc Add Exit button + controller led fix (#221) 2026-09-13 14:07:54 +02:00
devangpratap b555ede2d3 Fix entry point and payload URL in macOS build guide (#222)
translate-recursive now starts at 0x800060A4 (__start) to match recomp.yml and system_bridge.h. The payload curl uses rwfc.net/api/wfc/payload like the rest of the guide, since nas.play.rwfc.net does not answer over https.

Co-authored-by: devangpratap <devangpratap@proton.me>
2026-09-13 10:35:44 +02:00
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
205 changed files with 19882 additions and 1281 deletions
+1
View File
@@ -3,3 +3,4 @@
# Patch files must stay LF: git apply matches context bytes against LF upstream sources
*.patch -text
translator/tests/Translator.Tests/TestAssets/**/*.bin binary
+55 -2
View File
@@ -14,18 +14,30 @@ concurrency:
cancel-in-progress: true
jobs:
recompilation:
name: Recompilation test
uses: ./.github/workflows/recomp-test.yml
translator:
name: Translator (build + test)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@v4
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '8.0.x'
- 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
run: dotnet restore translator/Translator.sln
@@ -34,3 +46,44 @@ jobs:
- name: Test
run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal
macos_substrate:
name: macOS arm64 (configure + substrate tests)
runs-on: macos-14
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Test macOS app dependency packaging
shell: bash
run: bash Launcher/macos/test-publish-app.command
- name: Configure native runtime
shell: bash
run: |
test "$(uname -m)" = arm64
cmake -S runtime -B build-macos -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DMKW_BUILD_PRODUCTS=OFF
grep -qx 'CMAKE_OSX_DEPLOYMENT_TARGET:STRING=12.0' \
build-macos/CMakeCache.txt
- name: Build macOS portability targets
shell: bash
run: |
cmake --build build-macos --target \
mkw_platform_paths_tests \
mkw_runtime_config_tests \
mkw_nand_save_tests \
mkw_nand_settings_tests \
mkw_sc_serial_tests \
mkw_input_expr_tests \
mkw_macos_native_compile \
mkw_macos_context_abi_tests \
mkw_macos_host_context_tests \
mkw_macos_guest_flat_memory_tests \
mkw_macos_external_audio_tests
- name: Test execution substrate
shell: bash
run: ctest --test-dir build-macos --output-on-failure
+280
View File
@@ -0,0 +1,280 @@
name: Package installers
# Builds the per-platform installer/setup tool (WiiCompiled-Setup.exe /
# WiiCompiled-Setup-x86_64.AppImage / WiiCompiled-Setup.pkg) via the platform packaging scripts -
# the same scripts a maintainer runs by hand today to
# produce a GitHub Release asset. This does NOT build the actual translated game executable:
# that step requires the end user's own Mario Kart Wii dump (Assets/main.dol, Assets/StaticR.rel),
# which is proprietary and not present in this repository or in CI.
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
macos-setup-package:
name: macOS (universal Setup.pkg)
runs-on: macos-14
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '8.0.x'
- name: Verify Apple Silicon runner
shell: bash
run: |
test "$(uname -m)" = arm64
xcode-select -p
- name: Download pinned Nod tools
shell: bash
run: |
mkdir -p Launcher/artifacts/macos
nodtool_version=v2.0.0-alpha.10
nodtool_arm64_asset=nodtool-macos-arm64
nodtool_arm64_sha256=e23ca466999b720c55e6d29c9683fce8cc74451ba64ead2e543d50129f24528a
nodtool_x86_64_asset=nodtool-macos-x86_64
nodtool_x86_64_sha256=f68f504dc2b72694b468ca78b6a24142c7aa5c8800f77564297f4143682e6575
curl -fsSL --retry 3 \
"https://github.com/encounter/nod/releases/download/${nodtool_version}/${nodtool_arm64_asset}" \
-o Launcher/artifacts/macos/nodtool-arm64
curl -fsSL --retry 3 \
"https://github.com/encounter/nod/releases/download/${nodtool_version}/${nodtool_x86_64_asset}" \
-o Launcher/artifacts/macos/nodtool-x86_64
printf '%s %s\n' "$nodtool_arm64_sha256" Launcher/artifacts/macos/nodtool-arm64 | shasum -a 256 -c -
printf '%s %s\n' "$nodtool_x86_64_sha256" Launcher/artifacts/macos/nodtool-x86_64 | shasum -a 256 -c -
chmod +x Launcher/artifacts/macos/nodtool-arm64 Launcher/artifacts/macos/nodtool-x86_64
- name: Publish self-contained Translator tools
shell: bash
run: |
dotnet publish translator/src/Translator.Cli/Translator.Cli.csproj \
-c Release -r osx-arm64 --self-contained true \
-p:PublishSingleFile=true \
-o Launcher/artifacts/macos/translator-arm64
dotnet publish translator/src/Translator.Cli/Translator.Cli.csproj \
-c Release -r osx-x64 --self-contained true \
-p:PublishSingleFile=true \
-o Launcher/artifacts/macos/translator-x86_64
- name: Download pinned universal Ninja
shell: bash
run: |
ninja_version=1.13.2
ninja_sha256=c99048673aa765960a99cf10c6ddb9f1fad506099ff0a0e137ad8960a88f321b
curl -fsSL --retry 3 "https://github.com/ninja-build/ninja/releases/download/v${ninja_version}/ninja-mac.zip" -o ninja-mac.zip
printf '%s %s\n' "$ninja_sha256" ninja-mac.zip | shasum -a 256 -c -
unzip -q ninja-mac.zip -d Launcher/artifacts/macos/ninja
chmod +x Launcher/artifacts/macos/ninja/ninja
- name: Download pinned portable CMake
shell: bash
run: |
cmake_version=4.4.3
archive="cmake-${cmake_version}-macos-universal.tar.gz"
base_url="https://github.com/Kitware/CMake/releases/download/v${cmake_version}"
expected_sha256=0c5d65251c14cc884bfa16bdbed3c263ce5bffe2e21c0d0d00962cb0610464fa
curl -fsSL --retry 3 "$base_url/$archive" -o "$archive"
printf '%s %s\n' "$expected_sha256" "$archive" | shasum -a 256 -c -
tar -xzf "$archive"
mv "cmake-${cmake_version}-macos-universal/CMake.app/Contents" Launcher/artifacts/macos/cmake
- name: Build Setup.pkg
env:
TAG_VERSION: ${{ github.ref_name }}
shell: bash
run: |
package_version=""
if [[ "${TAG_VERSION:-}" =~ ^v?[0-9] ]]; then
package_version="${TAG_VERSION#v}"
elif [[ -f "Launcher/Directory.Build.props" ]]; then
package_version=$(grep -m1 '<Version>' Launcher/Directory.Build.props | sed -E 's/.*<Version>([^<]+)<\/Version>.*/\1/')
fi
mkdir -p Launcher/dist
Launcher/macos/build-setup-pkg.command \
--nodtool-arm64 Launcher/artifacts/macos/nodtool-arm64 \
--nodtool-x86_64 Launcher/artifacts/macos/nodtool-x86_64 \
--translator-arm64 Launcher/artifacts/macos/translator-arm64/Translator.Cli \
--translator-x86_64 Launcher/artifacts/macos/translator-x86_64/Translator.Cli \
--cmake-root Launcher/artifacts/macos/cmake \
--ninja-arm64 Launcher/artifacts/macos/ninja/ninja \
--ninja-x86_64 Launcher/artifacts/macos/ninja/ninja \
--output Launcher/dist/WiiCompiled-Setup.pkg \
${package_version:+--version "$package_version"}
- name: Verify package layout and architecture-specific tools
shell: bash
run: |
pkgutil --check-signature Launcher/dist/WiiCompiled-Setup.pkg || true
if pkgutil --payload-files Launcher/dist/WiiCompiled-Setup.pkg | \
grep -E '/(Assets|generated|PulsarPacks|WiiCompiled.app|RetroRewind.app)(/|$)'; then
echo "::error::Setup.pkg contains a forbidden payload"
exit 1
fi
expanded="$RUNNER_TEMP/wiicompiled-setup-expanded"
pkgutil --expand-full Launcher/dist/WiiCompiled-Setup.pkg "$expanded"
resources="$expanded/Payload/Applications/WiiCompiled Setup.app/Contents/Resources"
for arch in arm64 x86_64; do
for tool in nodtool Translator.Cli ninja; do
lipo "$resources/tools/$arch/$tool" -verify_arch "$arch"
done
done
lipo "$resources/tools/cmake/bin/cmake" -verify_arch arm64 x86_64
bash "$resources/setup.command" --help
/usr/bin/arch -x86_64 /bin/bash "$resources/setup.command" --help
- uses: actions/upload-artifact@v7
with:
name: WiiCompiled-Setup-macos-universal
path: Launcher/dist/WiiCompiled-Setup.pkg
if-no-files-found: error
archive: false
# Publishes the packaged installers as a GitHub Release whenever a v* tag is pushed. Wheel Wizard
# discovers updates from these releases, so the contract it relies on is enforced here: a full
# (non-prerelease) release whose tag is v<semver>, carrying the expected platform assets,
# produced by setup hosts that report that same version.
release:
name: Publish GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
needs: [linux-appimage, windows-installer, macos-setup-package, 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 the 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/Directory.Build.props "<Version>$version</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 WiiCompiled-Setup.pkg; do
found="$(find artifacts -type f -name "$name" | head -n 1)"
[ -n "$found" ] && [ -s "$found" ] || { echo "::error::missing release asset $name"; exit 1; }
assets+=("$found")
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/
/build-*/
/native-build/
/native-build-macos/
/local-products/
/dist/
/out/
[Bb]in/
@@ -69,3 +72,5 @@ project.lock.json
*.log
output.txt
# Operating System
.DS_Store
+16 -7
View File
@@ -88,17 +88,23 @@ Assert-File $setupProject '.NET setup project'
Assert-File $translatorProject 'Translator CLI project'
if (-not (Test-Path -LiteralPath (Join-Path $portableTools 'llvm-mingw\bin\x86_64-w64-mingw32-clang++.exe'))) {
# A PowerShell script, not a native executable - it never touches $LASTEXITCODE, and its own
# $ErrorActionPreference = 'Stop' + throw already aborts this run on failure.
& (Join-Path $PSScriptRoot 'Prepare-PortableTools.ps1') -Destination $portableTools
if ($LASTEXITCODE -ne 0) { throw 'Portable tool preparation failed.' }
}
Assert-File (Join-Path $portableTools 'CMake\bin\cmake.exe') 'Portable CMake'
Assert-File (Join-Path $portableTools 'Ninja\ninja.exe') 'Portable Ninja'
Assert-Directory $dependencySources 'Pinned offline dependency sources'
# native_prebuilt carries the aurora/third-party archives the user no longer has
# to compile (launcher/Prepare-NativePrebuilt.ps1).
# Kept in step with InstalledLayout.DependencyNames by Test-PinnedFacts.ps1: the installed host
# refuses to call a toolkit complete unless every one of these directories is present.
$requiredDependencies = @('abseil-cpp','cppwinrt','dawn_prebuilt','fmt','freetype','imgui','libusb','native_prebuilt','png','SDL','sqlite3','tracy','xxhash','zlib','zstd')
$missingSources = @($requiredDependencies | Where-Object { $_ -ne 'native_prebuilt' } |
Where-Object { -not (Test-Path -LiteralPath (Join-Path $dependencySources $_) -PathType Container) })
if ($missingSources.Count -gt 0) {
& (Join-Path $PSScriptRoot 'Prepare-Dependencies.ps1') -Destination $dependencySources
}
Assert-Directory $dependencySources 'Pinned offline dependency sources'
# The precompiled archives are only interchangeable with what the user's machine
# compiles if both came from this toolchain and this flag set, so a stale package
@@ -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
# Linux) rather than a separate download/version-pin copy here: it downloads and caches the same way
# NodToolProvider.cs always does (Launcher/artifacts/nodtool.exe), replacing the old manual
# -DolphinToolPath handoff with an automated, pinned acquisition step.
# NodToolProvider.cs always does (Launcher/artifacts/nodtool.exe)
$nodToolCliProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup.Common.Cli'
$nodTool = (& dotnet run --project $nodToolCliProject -c Release -- --workspace $repoRoot | Select-Object -Last 1)
if ($LASTEXITCODE -ne 0) { throw "nodtool resolution failed with exit code $LASTEXITCODE." }
@@ -274,9 +279,12 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
if ([string]::IsNullOrWhiteSpace($identities.$required)) { throw "Payload identity output is missing $required." }
}
$productVersion = ((& $setupHost --version) -join '').Trim()
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($productVersion)) { throw 'The setup host did not report its version.' }
$manifest = [ordered]@{
SchemaVersion = 2
ProductVersion = '0.2.25'
ProductVersion = $productVersion
ExpectedGameId = $pins.GameId
ExpectedDolSha256 = $pins.DolSha256
ExpectedRelSha256 = $pins.RelSha256
@@ -292,11 +300,12 @@ $manifest = [ordered]@{
$manifest | ConvertTo-Json | Set-Content (Join-Path $payloadRoot 'payload-manifest.json') -Encoding UTF8
Write-Host '[4/6] Enforcing the copyright and generated-code boundary...'
# Both audits are PowerShell scripts that throw directly on failure (Test-PayloadBoundary.ps1
# never invokes a native command at all, so it never touches $LASTEXITCODE); there is no exit
# code to check here, and doing so risks reading a stale value from an unrelated earlier command.
& (Join-Path $PSScriptRoot 'Test-PayloadBoundary.ps1') -PayloadRoot $payloadRoot
if ($LASTEXITCODE -ne 0) { throw 'Payload boundary audit failed.' }
& (Join-Path $PSScriptRoot 'Test-NativeDependencies.ps1') -PayloadRoot $payloadRoot -SetupHost $setupHost `
-LlvmReadobjPath (Join-Path $portableTools 'llvm-mingw\bin\llvm-readobj.exe')
if ($LASTEXITCODE -ne 0) { throw 'Native dependency audit failed.' }
Write-Host '[5/6] Creating the canonical installer payload...'
$payloadZip = Join-Path $workRoot 'payload.zip'
+7
View File
@@ -0,0 +1,7 @@
<Project>
<PropertyGroup>
<!-- The one product version. Release tags must match it. -->
<Version>0.2.34</Version>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
</Project>
+10 -2
View File
@@ -103,7 +103,7 @@ function Write-MkwBuildStep([string]$StepId, [string]$Message) {
function Reset-LocalDirectory([string]$Path) {
$full = [IO.Path]::GetFullPath($Path)
$root = [IO.Path]::GetFullPath($Workspace).TrimEnd('\') + '\'
$installRoot = [IO.Path]::GetFullPath((Split-Path -Parent $Workspace)).TrimEnd('\') + '\'
$installRoot = [IO.Path]::GetFullPath((Split-Path -Parent $realWorkspace)).TrimEnd('\') + '\'
# The caller-supplied output destinations are legitimate reset targets by
# definition, wherever the caller placed them: a fresh install's operation
# scratch lives beside the installation directory rather than inside it.
@@ -150,8 +150,16 @@ if ($Profile -eq 'both' -and [string]::IsNullOrWhiteSpace($BaseOutputDirectory))
if ($Profile -ne 'both' -and -not [string]::IsNullOrWhiteSpace($BaseOutputDirectory)) {
throw '-BaseOutputDirectory is valid only with -Profile both.'
}
$realWorkspace = $Workspace.TrimEnd('\')
$Workspace = Get-MkwBuildSafePath $realWorkspace 'workspace' 'runtime\CMakeLists.txt'
# A selected package inside the install may also name the same Code.pul through the real path.
# Give it the workspace spelling before comparing it with the staged copy.
if (-not [string]::IsNullOrWhiteSpace($RetroRewindPackageDirectory) -and
$RetroRewindPackageDirectory.StartsWith($realWorkspace + '\', [StringComparison]::OrdinalIgnoreCase)) {
$RetroRewindPackageDirectory = $Workspace + $RetroRewindPackageDirectory.Substring($realWorkspace.Length)
}
$translator = Join-Path $Toolkit 'Translator\Translator.Cli.exe'
$toolchain = Get-MkwShellSafeToolchainRoot $Toolkit
$toolchain = Get-MkwBuildSafePath $Toolkit 'toolchain' 'CMake\bin\cmake.exe'
$cmake = Join-Path $toolchain 'CMake\bin\cmake.exe'
$ninja = Join-Path $toolchain 'Ninja\ninja.exe'
$toolchainBin = Join-Path $toolchain 'llvm-mingw\bin'
+43 -23
View File
@@ -40,41 +40,55 @@ function Get-MkwToolchainPath([string]$ToolchainRoot) {
) -join ';')
}
function Get-MkwShellSafeToolchainRoot([string]$ToolchainRoot) {
if ([string]::IsNullOrWhiteSpace($ToolchainRoot)) { throw 'A toolchain root is required.' }
$full = [IO.Path]::GetFullPath($ToolchainRoot)
function Get-MkwBuildSafePath([string]$Path, [string]$Kind, [string]$MarkerFile) {
<#
The Windows native build passes workspace paths through CMake, Ninja response files and
clang, which do not all interpret quotes the same way. Keep those paths plain even when the
user's install directory contains an apostrophe, ampersand or other punctuation.
#>
if ([string]::IsNullOrWhiteSpace($Path)) { throw "A $Kind path is required." }
$full = [IO.Path]::GetFullPath($Path)
# A drive root keeps its separator: "C:" is relative to the current directory on that drive.
if ($full -ne [IO.Path]::GetPathRoot($full)) { $full = $full.TrimEnd('\') }
if ($full -notmatch '[()&^%!]') { return $full }
if ($full -cmatch '^[A-Za-z0-9 ._\\:-]+$') { return $full }
Assert-File (Join-Path $full $MarkerFile) "$Kind marker"
$sha = [Security.Cryptography.SHA256]::Create()
try {
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($full.ToLowerInvariant()))
} finally { $sha.Dispose() }
$linkName = 'toolchain-' + ((($bytes[0..7]) | ForEach-Object { $_.ToString('x2') }) -join '')
$linkName = "$Kind-" + ((($bytes[0..7]) | ForEach-Object { $_.ToString('x2') }) -join '')
$failures = @()
foreach ($base in @($env:ProgramData, $env:PUBLIC)) {
if ([string]::IsNullOrWhiteSpace($base) -or $base -match '[()&^%! ]') { continue }
if ([string]::IsNullOrWhiteSpace($base) -or $base -cnotmatch '^[A-Za-z0-9._\\:-]+$') { continue }
$link = Join-Path (Join-Path $base 'WiiCompiled') $linkName
try {
[IO.Directory]::CreateDirectory((Split-Path -Parent $link)) | Out-Null
# The name already identifies the target, so an existing junction that still resolves is
# this one; only a broken leftover is replaced. Directory.Delete removes the reparse
# point itself, where Remove-Item -Recurse would delete the toolchain it points at.
if (-not (Test-Path -LiteralPath (Join-Path $link 'CMake\bin\cmake.exe') -PathType Leaf)) {
if (Test-Path -LiteralPath $link) { [IO.Directory]::Delete($link) }
$existing = Get-Item -LiteralPath $link -Force -ErrorAction SilentlyContinue
if ($null -ne $existing) {
# Never trust a directory just because it has the marker: it could point at a
# different installation. Nor may we remove a directory we did not create.
if ($existing.LinkType -ne 'Junction' -or
@($existing.Target).Count -ne 1 -or
-not [string]::Equals([IO.Path]::GetFullPath(@($existing.Target)[0]),
$full, [StringComparison]::OrdinalIgnoreCase)) {
throw "An existing path is not the expected junction: $link"
}
} else {
New-Item -ItemType Junction -Path $link -Target $full -ErrorAction Stop | Out-Null
}
Write-Host "MKWCBUILD: Building through $link, because $full contains characters cmd.exe cannot parse"
Assert-File (Join-Path $link $MarkerFile) "$Kind junction marker"
Write-Host "MKWCBUILD: Building the $Kind through $link, because $full contains characters the native build cannot quote reliably"
return $link
} catch {
$failures += "$link ($($_.Exception.Message))"
}
}
throw ("The toolchain path $full contains a character (one of ( ) & ^ % !) that the compiler " +
'cannot be invoked through, and no junction to it could be created: ' + ($failures -join '; ') +
'. Install to a path without those characters.')
throw ("The $Kind path $full cannot be passed safely to the native build, and no junction " +
'to it could be created: ' + ($failures -join '; ') + '. Install to a path of plain ' +
'letters, digits and spaces, or make a safe junction location available.')
}
function Get-MkwProjectPins([string]$ProjectFile) {
@@ -117,12 +131,13 @@ function Get-MkwProjectPins([string]$ProjectFile) {
}
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
is deliberate: it 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
doesn't publish $LASTEXITCODE, so this sets it manually for callers that check it.
Runs a build tool and turns a non-zero exit code into a described failure. By default,
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. Callers that
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
WiiCompiled.Setup/InstallProgress.cs); the human sentence stays on the same log line.
#>
@@ -132,9 +147,14 @@ function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Descri
if ($_.Contains('"')) { throw "A native build argument contains an unsupported quote: $_" }
'"' + $_ + '"'
})
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
-NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
if ($WaitForProcessTree) {
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
-NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
} else {
& $FilePath @Arguments
$exitCode = $LASTEXITCODE
}
$global:LASTEXITCODE = $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/theofficialgman/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=13be9cff8b9b179c42dcd16aeabb6effcc8f0dfdcc14463eda2a5caeda225142' })
},
[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.4.tar.gz'
Uris = @('https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.4.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.4.tar.gz' })
},
[pscustomobject]@{
Name = 'zlib'; File = 'zlib-1.3.2.tar.gz'
Uris = @('https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz' })
},
[pscustomobject]@{
Name = 'zstd'; File = 'zstd-1.5.7.tar.gz'
Uris = @('https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz')
Pins = @(@{ File = $auroraExtern; Text = 'https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz' })
}
)
# The C++/WinRT compiler (NuGet package = zip) that generates the projection headers.
$cppWinRtTool = [pscustomobject]@{
Name = 'cppwinrt-tool'; File = 'Microsoft.Windows.CppWinRT.3.0.260818.1.nupkg'
Uris = @('https://www.nuget.org/api/v2/package/Microsoft.Windows.CppWinRT/3.0.260818.1')
Pins = @()
}
if (-not $Destination) { $Destination = Join-Path $PSScriptRoot 'artifacts\dependencies' }
$Destination = [IO.Path]::GetFullPath($Destination)
$downloads = Join-Path $PSScriptRoot 'artifacts\downloads'
$tar = Join-Path $env:SystemRoot 'System32\tar.exe'
if (-not (Test-Path -LiteralPath $tar -PathType Leaf)) { throw "Windows archive tool is missing: $tar" }
[IO.Directory]::CreateDirectory($Destination) | Out-Null
[IO.Directory]::CreateDirectory($downloads) | Out-Null
function Assert-Pinned($Package) {
foreach ($pin in $Package.Pins) {
if (-not (Test-Path -LiteralPath $pin.File -PathType Leaf)) { throw "Pin source is missing: $($pin.File)" }
$content = [IO.File]::ReadAllText($pin.File)
if (-not $content.Contains($pin.Text)) {
throw "$($Package.Name) is pinned to '$($pin.Text)' here but $($pin.File) no longer declares it; update both."
}
}
}
function Get-Archive($Package) {
$archive = Join-Path $downloads $Package.File
if (Test-Path -LiteralPath $archive -PathType Leaf) { return $archive }
$temporary = $archive + '.partial'
$failures = @()
foreach ($uri in $Package.Uris) {
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
Write-Host "Downloading $($Package.Name) from $uri..."
try {
Invoke-WebRequest -UseBasicParsing -Uri $uri -OutFile $temporary
} catch {
$failures += "$uri ($($_.Exception.Message))"
continue
}
Move-Item -LiteralPath $temporary -Destination $archive -Force
return $archive
}
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
throw "$($Package.Name) could not be fetched: $($failures -join '; ')"
}
function Expand-Package([string]$Archive, [string]$Target) {
# Release archives wrap everything in one versioned directory (SDL3-3.4.4/, tracy-<sha>/) while
# dawn-build's package is flat; either way the tree lands directly under the target.
$extract = Join-Path (Split-Path -Parent $Target) ('.extract-' + (Split-Path -Leaf $Target))
if (Test-Path -LiteralPath $extract) { Remove-Item -LiteralPath $extract -Recurse -Force }
[IO.Directory]::CreateDirectory($extract) | Out-Null
# Symbolic links need a privilege Windows tar usually lacks; the only ones in these archives
# are zstd test aliases, and none are build inputs.
$listing = & $tar -tvf $Archive
if ($LASTEXITCODE -ne 0) { throw "Listing $Archive failed with exit code $LASTEXITCODE." }
$excludes = @($listing | ForEach-Object { if ($_ -match '^l.*\s(\S+)\s->\s') { '--exclude=' + $Matches[1] } })
& $tar -xf $Archive -C $extract @excludes
if ($LASTEXITCODE -ne 0) { throw "Extracting $Archive failed with exit code $LASTEXITCODE." }
$entries = @(Get-ChildItem -LiteralPath $extract -Force)
$source = $extract
if ($entries.Count -eq 1 -and $entries[0].PSIsContainer) { $source = $entries[0].FullName }
if (Test-Path -LiteralPath $Target) { Remove-Item -LiteralPath $Target -Recurse -Force }
Move-Item -LiteralPath $source -Destination $Target
if (Test-Path -LiteralPath $extract) { Remove-Item -LiteralPath $extract -Recurse -Force }
}
foreach ($package in $packages) {
Assert-Pinned $package
$target = Join-Path $Destination $package.Name
if (Test-Path -LiteralPath $target -PathType Container) { continue }
Expand-Package (Get-Archive $package) $target
Write-Host "Prepared $($package.Name)"
}
$cppWinRt = Join-Path $Destination 'cppwinrt'
if (-not (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType Leaf)) {
$toolRoot = Join-Path $PSScriptRoot 'artifacts\cppwinrt-tool'
$compiler = Join-Path $toolRoot 'bin\cppwinrt.exe'
if (-not (Test-Path -LiteralPath $compiler -PathType Leaf)) {
Expand-Package (Get-Archive $cppWinRtTool) $toolRoot
}
if (-not (Test-Path -LiteralPath $compiler -PathType Leaf)) { throw "cppwinrt.exe is missing: $compiler" }
if (Test-Path -LiteralPath $cppWinRt) { Remove-Item -LiteralPath $cppWinRt -Recurse -Force }
[IO.Directory]::CreateDirectory($cppWinRt) | Out-Null
Write-Host "Generating C++/WinRT headers (-input $CppWinRtInput)..."
& $compiler -input $CppWinRtInput -output $cppWinRt
if ($LASTEXITCODE -ne 0) { throw "cppwinrt.exe failed with exit code $LASTEXITCODE." }
if (-not (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType Leaf)) {
throw "cppwinrt.exe produced no winrt\base.h under $cppWinRt"
}
Copy-Item -LiteralPath (Join-Path $toolRoot 'LICENSE') -Destination (Join-Path $cppWinRt 'LICENSE.txt')
Write-Host 'Prepared cppwinrt'
}
Write-Host "Pinned dependency sources ready: $Destination"
-2
View File
@@ -61,8 +61,6 @@ Assert-File $windres 'Portable resource compiler'
Assert-File $clangBinary 'Portable clang driver binary'
Assert-Directory $dependencies 'Pinned offline dependency sources'
Assert-Directory $auroraSource 'aurora-main source tree'
Assert-File (Join-Path $repoRoot 'generated\build_shards\shards.cmake') `
'Translator shard manifest (run the developer build once so runtime/ can configure)'
if ($Parallel -le 0) { $Parallel = [Environment]::ProcessorCount }
+660
View File
@@ -0,0 +1,660 @@
#!/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), Crypto++ and mbed TLS 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=""
toolchain_dir=""
ninja_bin=""
cmake_bin=""
llvm_dir=""
sysroot=""
stage_dir="$workspace/build/native-prebuilt-stage"
keep_stage=0
reuse_stage=0
parallel=0
print_fingerprint_only=0
disconnected=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 provenance inputs 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 ;;
--toolchain-dir) toolchain_dir=$2; shift 2 ;;
--ninja-bin) ninja_bin=$2; shift 2 ;;
--cmake-bin) cmake_bin=$2; shift 2 ;;
--llvm-dir) llvm_dir=$2; shift 2 ;;
--sysroot) sysroot=$2; shift 2 ;;
--disconnected) disconnected=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"; }
# Environment check: Ensure the `SOURCE_DATE_EPOCH` value is not invalid
# before trying to build anything (fail-fast)
if [[ -n "${SOURCE_DATE_EPOCH:-}" ]] && ! python3 -c \
'import datetime, os; datetime.datetime.fromtimestamp(int(os.environ.get("SOURCE_DATE_EPOCH")), datetime.timezone.utc)'
then
fail "SOURCE_DATE_EPOCH must be a valid UNIX timestamp"
fi
[[ -n "$output_dir" ]] || output_dir="$script_dir/artifacts/native-prebuilt-$arch"
[[ "$stage_dir" = /* ]] || stage_dir="$workspace/$stage_dir"
toolchain_dir="${toolchain_dir:-$script_dir/artifacts/portable-tools/toolchain-$arch}"
cc="${llvm_dir:-$toolchain_dir}/bin/clang"
cxx="${llvm_dir:-$toolchain_dir}/bin/clang++"
cmake_bin="${cmake_bin:-$toolchain_dir/bin/cmake}"
ninja_bin="${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 "${llvm_dir:-$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
-DUSE_STATIC_MBEDTLS_LIBRARY=ON
-DUSE_SHARED_MBEDTLS_LIBRARY=OFF
# 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
)
if [[ "$disconnected" -eq 1 ]]; then
fixed_configure_flags+=(
-DFETCHCONTENT_FULLY_DISCONNECTED=ON
)
fi
if [[ -n "$sysroot" ]]; then
fixed_configure_flags+=(
-DCMAKE_SYSROOT="$sysroot"
)
fi
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.
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"
mbedtls_fingerprint=$(sha256_of "$runtime_source/cmake/MbedTLSPin.cmake")
compiler_sha256=$(sha256_of "$cc")
cxx_sha256=$(sha256_of "$cxx")
# Append the CXX SHA256 if it doesn't match the hash of CC
if [[ "${cxx_sha256}" != "${compiler_sha256}" ]]; then
compiler_sha256="${compiler_sha256}:${cxx_sha256}"
fi
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"
printf 'mbedtls_fingerprint=%s\n' "$mbedtls_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"
mbedtls_refs=()
mbedtls_txt="$export_dir/mbedtls.txt"
assert_file "$mbedtls_txt" "Mbed TLS export targets list"
while IFS='|' read -r name linkerfile; do
[[ -n "$name" ]] || continue
linkerfile=$(normalize "$linkerfile")
ref=${linker_file_to_reference["$linkerfile"]:-}
[[ -n "$ref" ]] || fail "Mbed TLS archive was not harvested: $name ($linkerfile)"
mbedtls_refs+=("$ref")
done < "$mbedtls_txt"
[[ ${#mbedtls_refs[@]} -ge 3 ]] || fail "Expected at least three Mbed TLS archives, got ${#mbedtls_refs[@]}"
mbedtls_source_dir=$(get_meta mbedtls_source_dir)
assert_dir "$mbedtls_source_dir/include/mbedtls" "Mbed TLS headers"
mkdir -p "$output_dir/include/mbedtls"
cp -a "$mbedtls_source_dir/include/." "$output_dir/include/mbedtls/"
# 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
for mbedtls_ref in "${mbedtls_refs[@]}"; do
[[ "$ref" == "$mbedtls_ref" ]] && continue 2
done
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_MBEDTLS_LIBRARIES "${mbedtls_refs[@]}"
echo 'set(MKW_NP_MBEDTLS_INCLUDE_DIR "@PKG@/include/mbedtls")'
printf 'set(MKW_NP_MBEDTLS_FINGERPRINT "%s")\n' "$mbedtls_fingerprint"
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" "$mbedtls_fingerprint" <<'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, mbedtls_fingerprint) = 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"])
source_date_epoch = os.environ.get("SOURCE_DATE_EPOCH") or None
utc = datetime.timezone.utc
if source_date_epoch is not None:
built_utc = datetime.datetime.fromtimestamp(
int(source_date_epoch),
utc
)
else:
built_utc = datetime.datetime.now(utc)
built_utc = built_utc.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
provenance = {
"SchemaVersion": 1,
"BuiltUtc": built_utc,
"CompilerSha256": compiler_sha256,
"CompilerVersion": compiler_version,
"FlagFingerprint": flag_fingerprint,
"DawnVersion": dawn_version,
"DawnRuntimeSha256": dawn_runtime_sha256,
"AuroraSourceFingerprint": aurora_fingerprint,
"ThirdPartySourceFingerprint": third_party_fingerprint,
"MbedTlsFingerprint": mbedtls_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
# 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.
[CmdletBinding()]
param([string]$RepositoryRoot)
@@ -58,6 +58,12 @@ $hostUri = Get-CapturedValue $retroWfcPayload 'CurrentRetroWfcPayloadUri\s*=\s*"
if ($hostUri -cne $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
# --- 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,6 @@
<Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
<Version>0.2.22</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Packaging-time helper: resolves (downloading if needed) the nodtool binary bundled by build-appimage.sh and Build-Installer.ps1</Description>
@@ -24,7 +24,7 @@ public static class RetroWfcPayload
private static readonly TimeSpan RetroWfcDownloadTimeout = TimeSpan.FromSeconds(30);
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 =
Path.Combine("binary", "payload.RMCPD00.bin");
@@ -53,9 +53,22 @@ public static class RetroWfcPayload
var root = Path.GetFullPath(stagedDirectory);
var payload = Path.Combine(root, RetroWfcOfflinePayloadFile);
if (!File.Exists(payload))
try
{
if ((File.GetAttributes(payload) & FileAttributes.Directory) != 0)
throw new InvalidDataException(
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
}
catch (FileNotFoundException)
{
throw new InvalidDataException(
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
}
catch (DirectoryNotFoundException)
{
throw new InvalidDataException(
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
}
ValidateRetroWfcPayloadFile(payload, signingKey);
return root;
}
@@ -5,7 +5,6 @@
<Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
<Version>0.2.22</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description>
@@ -12,7 +12,10 @@ internal static class BuildRunner
public static async Task RunAsync(
string workspace, string profile, string outputDir, string? baseOutputDir,
string? retroDir, string? retroWfcOfflineDir, bool skipRetroWfcPayload,
bool forceCleanBuild, string? translatorBin, IInstallReporter reporter, CancellationToken cancellationToken)
bool forceCleanBuild, string? translatorBin, string? ccBin, string? cxxBin, string? fuseLd,
string? cmakeBin, string? ninjaBin, string? nativePrebuiltDir, string? sysroot,
IInstallReporter reporter,
CancellationToken cancellationToken)
{
var script = Path.Combine(workspace, "Launcher", "local-build.sh");
if (!File.Exists(script)) throw new FileNotFoundException("local-build.sh is missing", script);
@@ -47,6 +50,38 @@ internal static class BuildRunner
{
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);
}
if (!string.IsNullOrEmpty(sysroot))
{
startInfo.ArgumentList.Add("--sysroot"); startInfo.ArgumentList.Add(sysroot);
}
using var process = new Process { StartInfo = startInfo };
var window = new BuildProgressWindow(reporter, InstallStages.Build, start: 6, end: 96);
+5 -1
View File
@@ -1,9 +1,13 @@
using System.Reflection;
namespace WiiCompiled.Setup.Linux;
internal static class ProductInfo
{
public const string Name = "WiiCompiled";
public const string Version = "0.2.22";
public static readonly string Version =
typeof(ProductInfo).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!
.InformationalVersion;
}
/// <summary>One installed product's record inside install-state.json.</summary>
+46 -8
View File
@@ -127,22 +127,52 @@ internal static class Program
string? retroWfcOfflineDir = null;
if (downloadPayload)
{
// Reused if a previous install already downloaded and it's still valid - matches
// Windows's own reuse-if-valid behavior instead of re-downloading on every install.
var cacheDir = Path.Combine(workspace, "generated", "retro-wfc-payload");
reporter.Progress(InstallStages.Validate, "Preparing the Retro-WFC payload", 1);
reporter.Progress(InstallStages.Validate,
"Downloading the current Retro-WFC payload", 1);
try
{
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(cacheDir);
}
catch (InvalidDataException)
{
// A valid signature authenticates a payload, but does not prove it is the latest
// signed revision. Always ask the fixed endpoint for the current snapshot; the
// downloader verifies it before atomically replacing the cache.
await RetroWfcPayload.DownloadRetroWfcPayloadAsync(
RetroWfcPayload.CurrentRetroWfcPayloadUri, cacheDir, token);
}
catch (Exception downloadFailure) when (!token.IsCancellationRequested &&
downloadFailure is HttpRequestException or TimeoutException
or IOException)
{
// Offline installs may continue with a previously authenticated snapshot. Do not
// use this path for a newly downloaded payload that failed signature validation:
// that must remain a hard failure instead of hiding possible endpoint tampering.
try
{
RetroWfcPayload.ValidateStagedRetroWfcPayloadDirectory(cacheDir);
}
catch (Exception cacheFailure) when (cacheFailure is IOException or
UnauthorizedAccessException or InvalidDataException)
{
throw new InvalidOperationException(
"The current Retro-WFC payload could not be downloaded and no valid cached " +
$"payload is available ({cacheFailure.Message.TrimEnd('.')}).", downloadFailure);
}
reporter.Diagnostic(
"The current Retro-WFC payload could not be downloaded; using the previously " +
$"verified cached payload instead ({downloadFailure.Message.TrimEnd('.')}).");
}
retroWfcOfflineDir = cacheDir;
}
var sysroot = flags.GetValueOrDefault("sysroot");
// --sysroot explicitly provided (even as bare flag at end of argv, which ParseArgs
// stores as null) must carry a path; omitting --sysroot entirely is fine (local-build.sh
// adds -UCMAKE_SYSROOT to clear any stale cached value from a prior configure).
if (flags.ContainsKey("sysroot") && string.IsNullOrWhiteSpace(sysroot))
{
throw new ArgumentException("--sysroot requires a non-empty directory path.");
}
await BuildRunner.RunAsync(
workspace, profile, installDir, baseInstallDir,
retroDir,
@@ -150,6 +180,13 @@ internal static class Program
skipPayload,
flags.ContainsKey("force-clean-build"),
flags.GetValueOrDefault("translator-bin"),
flags.GetValueOrDefault("cc"),
flags.GetValueOrDefault("cxx"),
flags.GetValueOrDefault("fuse-ld"),
flags.GetValueOrDefault("cmake"),
flags.GetValueOrDefault("ninja"),
flags.GetValueOrDefault("native-prebuilt-dir"),
sysroot,
reporter, token);
reporter.Progress(InstallStages.Shortcuts, "Creating shortcuts", 98);
@@ -317,7 +354,8 @@ internal static class Program
install [--game ISO_PATH] [--install-dir DIR] [--retro-dir DIR
{--download-retro-wfc-payload | --skip-retro-wfc-payload}]
[--force-clean-build] [--translator-bin PATH] [--disc-tool-bin PATH]
[--progress-json] [--workspace DIR]
[--cc PATH] [--cxx PATH] [--fuse-ld NAME_OR_PATH] [--cmake PATH] [--ninja PATH]
[--native-prebuilt-dir DIR] [--sysroot PATH] [--progress-json] [--workspace DIR]
uninstall
launch-base
launch-retro
@@ -6,7 +6,6 @@
<Nullable>enable</Nullable>
<AssemblyName>WiiCompiled.Setup.Linux</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Linux</RootNamespace>
<Version>0.2.22</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled on Linux</Description>
@@ -22,6 +22,9 @@ internal sealed class InstallerEngine
var existing = new Installation(installDirectory);
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();
using var scratch = Directory.Exists(installDirectory)
? InstallScratchSpace.CreateInsideInstall(installDirectory, _reporter)
@@ -139,7 +142,7 @@ internal sealed class InstallerEngine
updatedState.RetroRewindInstalled, candidateRuntimeAssetsFingerprint,
remainingCancellation);
Publish(staging, installDirectory, canonicalRetroRoot, updatedState,
releaseEntries, remainingCancellation);
releaseEntries, remainingCancellation, createShortcuts: firstInstall);
return;
}
@@ -161,7 +164,8 @@ internal sealed class InstallerEngine
await PublishToolkitAndReconcileProductsAsync(existing, staging, workspace, manifest,
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,
InstallOptions options, string? canonicalRetroRoot,
RetroRewindCompileInputs? retroCompileInputs,
bool publishGameAssets, CancellationToken cancellationToken)
bool publishGameAssets, bool createShortcuts, CancellationToken cancellationToken)
{
var installDirectory = existing.Root;
@@ -214,7 +218,8 @@ internal sealed class InstallerEngine
if (publishGameAssets) AddComponent(entries, staging, installDirectory, "GameAssets");
Publish(staging, installDirectory, canonicalRetroRoot, state,
entries, cancellationToken, progressPercent: 8, completionPercent: 10);
entries, cancellationToken, progressPercent: 8, completionPercent: 10,
createShortcuts: createShortcuts);
_reporter.Progress(InstallStages.BuildBase,
"Producing the installed products with the published toolkit...", 11);
@@ -277,7 +282,7 @@ internal sealed class InstallerEngine
private void Publish(string staging, string installDirectory,
string? canonicalRetroRoot, InstallState state,
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"),
Path.Combine(installDirectory, "licenses")));
@@ -322,7 +327,8 @@ internal sealed class InstallerEngine
{
ShellIntegration.RegisterUninstaller(installDirectory, state.RetroRewindInstalled);
}
ShellIntegration.CreateShortcuts(installDirectory);
if (createShortcuts)
ShellIntegration.CreateShortcuts(installDirectory);
}
catch (Exception ex)
{
@@ -110,8 +110,7 @@ internal sealed class ProductRepairService
InputValidation.CurrentRetroWfcPayloadUri, payloadScratch, cancellationToken);
}
catch (Exception ex) when (!cancellationToken.IsCancellationRequested &&
ex is HttpRequestException or IOException or InvalidDataException
or InvalidOperationException or OperationCanceledException)
ex is HttpRequestException or TimeoutException or IOException)
{
payloadSnapshot = RecoverInstalledRetroWfcPayload(toolkitFingerprint,
Path.Combine(scratchRoot, "retro-wfc-payload-recovered"), ex, cancellationToken);
@@ -1,5 +1,6 @@
namespace WiiCompiled.Setup.Windows;
using System.Reflection;
using System.Runtime.InteropServices;
internal static class Program
@@ -121,7 +122,9 @@ internal static class PlatformChecks
internal static class ProductInfo
{
public const string Name = "WiiCompiled";
public const string Version = "0.2.25";
public static readonly string Version =
typeof(ProductInfo).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!
.InformationalVersion;
/// <summary>
/// The setup executable is copied into the installation under this name. It is the launcher and
@@ -7,7 +7,6 @@
<AssemblyName>WiiCompiled.Setup</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.2.25</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled</Description>
Regular → Executable
+145 -14
View File
@@ -5,8 +5,12 @@
# self-contained binaries, and `nodtool` (a prebuilt MIT/Apache-2.0 CLI from encounter/nod, see
# NodToolProvider.cs) is downloaded and bundled too - AppRun passes --translator-bin and
# --disc-tool-bin so local-build.sh/DiscTool.cs skip their from-source/download fallbacks entirely.
# It still shells out to system clang/cmake/ninja - no C/C++ toolchain is bundled, matching
# Launcher/local-build.sh's own remaining prerequisites.
# A pruned native clang/lld/cmake/ninja toolchain (see prepare-portable-tools.sh) is bundled the
# same way - AppRun passes --cc/--cxx/--fuse-ld/--cmake/--ninja so local-build.sh never has to find
# a system compiler, CMake, or Ninja. It still shells out to system pkg-config and Vulkan headers,
# matching Launcher/local-build.sh's own remaining prerequisites. A precompiled aurora +
# third-party package (see Prepare-NativePrebuilt.sh) is bundled the same way too - AppRun passes
# --native-prebuilt-dir so local-build.sh never compiles aurora-main from source at all.
#
# An AppImage mounts read-only, but local-build.sh writes generated/, native-build/, Assets/, etc.
# into the workspace it's given. So AppRun (written below) copies the bundled workspace snapshot
@@ -15,12 +19,58 @@
# - generated/native-build/Assets/PulsarPacks live only in that writable cache and are never
# touched by the sync, so local-build.sh's own incremental caching survives across runs and across
# AppImage updates. translator/ isn't part of this snapshot at all: it's published as its own
# self-contained binary (usr/bin/translator-cli) below and never needs a writable copy.
# self-contained binary (usr/bin/translator-cli) below and never needs a writable copy. Neither
# native-prebuilt/ nor the toolchain are copied into the cache either - both are large
# (~90 MiB / ~500 MiB) and local-build.sh only ever reads from them - but AppRun does point
# $CACHE/toolchain and $CACHE/native-prebuilt symlinks at the current mount on every single launch
# (see AppRun's own comment): an AppImage's FUSE mount is at a fresh random /tmp/.mount_XXXXXX
# every run, and CMake bakes whatever compiler/tool path it's given directly into each
# build.ninja rule's command line, so referencing $HERE straight would change that command line -
# and Ninja reruns any rule whose command line changed - forcing a full rebuild on every single
# launch even though the compiler itself never actually changed. The symlink keeps the path
# string CMake/Ninja see identical across runs while what it resolves to tracks the current mount
# underneath.
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace=$(cd "$script_dir/.." && pwd)
# `uname -m` reports the *kernel's* architecture, which can differ from userspace - an aarch64
# kernel can run a 32-bit armhf userland (as shipped by 32-bit Raspberry Pi OS), same as an x86_64
# kernel can run an i686 one. What matters here is which userspace binaries (dotnet, appimagetool)
# will actually run, so this reads the ELF header of this script's own running bash interpreter -
# real userspace - rather than trusting the kernel's self-report. /proc/$$/exe (not /proc/self/exe:
# that would resolve inside the readlink subprocess below, to readlink itself, not to bash) is this
# shell's own PID. EI_CLASS (byte 4: 1=32-bit, 2=64-bit) and e_machine (bytes 18-19: 3=EM_386,
# 40=EM_ARM, 62=EM_X86_64, 183=EM_AARCH64) are read as plain little-endian bytes, which every
# real-world x86/ARM Linux userland uses; ELF's big-endian encoding is a non-issue here since no
# Linux distro ships a big-endian x86 or ARM userland.
elf_exe=$(readlink -f "/proc/$$/exe")
elf_class=$(od -An -t u1 -j 4 -N 1 "$elf_exe" | tr -d ' ')
elf_machine_lo=$(od -An -t u1 -j 18 -N 1 "$elf_exe" | tr -d ' ')
elf_machine_hi=$(od -An -t u1 -j 19 -N 1 "$elf_exe" | tr -d ' ')
elf_machine=$(( elf_machine_hi * 256 + elf_machine_lo ))
# Mirrors the host-architecture detection NodToolProvider.cs already does (RuntimeInformation.
# OSArchitecture) so this script's own dotnet RID and appimagetool selection agree with the
# nodtool binary that same code path resolves below. local-build.sh needs no such mapping itself:
# it just drives the native CMake configure, which already accepts x86_64 or aarch64 natively
# (see runtime/CMakeLists.txt's CMAKE_SYSTEM_PROCESSOR check).
case "$elf_class:$elf_machine" in
2:62)
dotnet_rid=linux-x64
appimagetool_arch=x86_64
;;
2:183)
dotnet_rid=linux-arm64
appimagetool_arch=aarch64
;;
*)
echo "build-appimage.sh: unsupported userspace architecture (ELF class $elf_class, machine $elf_machine) - WiiCompiled requires a 64-bit x86_64 or aarch64 userland" >&2
exit 1
;;
esac
output_dir="$workspace/Launcher/dist"
appimagetool_override=""
@@ -40,10 +90,10 @@ appdir="$workspace/Launcher/artifacts/appimage-build/AppDir"
rm -rf "$appdir"
mkdir -p "$appdir/usr/bin" "$appdir/workspace/Launcher"
echo "Publishing the installer (self-contained linux-x64)..."
echo "Publishing the installer (self-contained $dotnet_rid)..."
publish_tmp="$workspace/Launcher/artifacts/appimage-build/publish"
rm -rf "$publish_tmp"
dotnet publish "$workspace/Launcher/WiiCompiled.Setup.Linux" -c Release -r linux-x64 \
dotnet publish "$workspace/Launcher/WiiCompiled.Setup.Linux" -c Release -r "$dotnet_rid" \
--self-contained -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \
-o "$publish_tmp"
cp "$publish_tmp/WiiCompiled.Setup.Linux" "$appdir/usr/bin/wiicompiled-setup"
@@ -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
# PATH at all - local-build.sh is told about it via --translator-bin and skips its own
# dotnet-build-from-source step entirely (see local-build.sh's translator resolution branch).
echo "Publishing the translator (self-contained linux-x64)..."
echo "Publishing the translator (self-contained $dotnet_rid)..."
translator_publish_tmp="$workspace/Launcher/artifacts/appimage-build/publish-translator"
rm -rf "$translator_publish_tmp"
dotnet publish "$workspace/translator/src/Translator.Cli" -c Release -r linux-x64 \
dotnet publish "$workspace/translator/src/Translator.Cli" -c Release -r "$dotnet_rid" \
--self-contained -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true \
-o "$translator_publish_tmp"
cp "$translator_publish_tmp/Translator.Cli" "$appdir/usr/bin/translator-cli"
@@ -71,6 +121,48 @@ nodtool_path=$(dotnet run --project "$workspace/Launcher/WiiCompiled.Setup.Commo
cp "$nodtool_path" "$appdir/usr/bin/nodtool"
chmod +x "$appdir/usr/bin/nodtool"
echo "Preparing the portable clang/lld/cmake/ninja toolchain ($appimagetool_arch)..."
bash "$script_dir/prepare-portable-tools.sh" --arch "$appimagetool_arch"
mkdir -p "$appdir/usr/toolchain"
cp -a "$workspace/Launcher/artifacts/portable-tools/toolchain-$appimagetool_arch"/. "$appdir/usr/toolchain/"
# Precompiled aurora + third-party package (see Prepare-NativePrebuilt.sh) so a user's own
# local-build.sh never has to compile aurora itself (~43% of local build CPU time). Re-harvesting
# recompiles the aurora/Crypto++/mbed TLS 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, source trees or mbed TLS pin.
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",
"mbedtls_fingerprint": "MbedTlsFingerprint",
}
print(1 if all(provenance.get(v) == current.get(k) for k, v in fields.items()) else 0)
PY
)
fi
if [[ "$package_current" == "1" ]]; then
echo "Native prebuilt package is current; reusing $native_prebuilt_dir"
else
echo "Native prebuilt package is missing or stale; harvesting a fresh one (compiles aurora once, can take a while)..."
bash "$script_dir/Prepare-NativePrebuilt.sh" --arch "$appimagetool_arch"
fi
mkdir -p "$appdir/native-prebuilt"
cp -a "$native_prebuilt_dir/." "$appdir/native-prebuilt/"
echo "Staging the bundled workspace snapshot..."
for dir in runtime aurora-main projects; do
cp -r "$workspace/$dir" "$appdir/workspace/$dir"
@@ -84,8 +176,21 @@ find "$appdir/workspace/aurora-main/extern" -mindepth 1 -maxdepth 1 -type d -exe
rm -rf "$appdir/workspace/runtime/build"
cp "$workspace/Launcher/local-build.sh" "$appdir/workspace/Launcher/local-build.sh"
# AppRun re-syncs runtime/aurora-main/projects/local-build.sh into the writable cache only when
# this changes, so it must change whenever any of those bundled paths actually did - a bare commit
# hash gets this wrong for an uncommitted change (verified directly: rebuilding after editing
# local-build.sh with no commit produced the same hash as the stale cache, so AppRun kept serving
# the old script and failed on a flag that didn't exist yet). `git status --porcelain` catches both
# modified tracked files and new untracked ones; appending a fresh timestamp when it's non-empty
# guarantees this never matches a previous build's stamp, forcing a resync every time the tree is
# dirty. A clean tree (a real tagged release) keeps the stable commit-hash behavior, so identical
# reruns of the same release AppImage don't resync needlessly.
if git -C "$workspace" rev-parse HEAD >/dev/null 2>&1; then
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
date -u +%s > "$appdir/workspace/.bundle-version"
fi
@@ -96,6 +201,7 @@ cat > "$appdir/AppRun" <<'APPRUN'
set -euo pipefail
HERE="$(dirname "$(readlink -f "$0")")"
CACHE="${XDG_DATA_HOME:-$HOME/.local/share}/WiiCompiled/workspace"
mkdir -p "$CACHE"
if [ ! -f "$CACHE/.bundle-version" ] || \
[ "$(cat "$HERE/workspace/.bundle-version")" != "$(cat "$CACHE/.bundle-version")" ]; then
mkdir -p "$CACHE/Launcher"
@@ -106,9 +212,31 @@ if [ ! -f "$CACHE/.bundle-version" ] || \
cp "$HERE/workspace/Launcher/local-build.sh" "$CACHE/Launcher/local-build.sh"
cp "$HERE/workspace/.bundle-version" "$CACHE/.bundle-version"
fi
# toolchain/ and native-prebuilt/ are NOT copied into the cache (they're large - ~500 MiB /
# ~90 MiB - and local-build.sh only ever reads from them): $CACHE/toolchain and
# $CACHE/native-prebuilt are symlinks re-pointed at the current mount on every single launch
# (unconditionally, not gated on .bundle-version above, since the mount path itself - unlike the
# bundled content - changes every run regardless). CMake bakes a compiler/tool path directly into
# each build.ninja rule's command line and Ninja reruns any rule whose command line changed since
# the last build (verified directly) - an AppImage's FUSE mount is at a fresh random
# /tmp/.mount_XXXXXX every launch, so referencing $HERE straight would change that command line,
# and therefore force a full rebuild, on every single run even though the compiler itself never
# actually changed. A symlink keeps the *path string* CMake/Ninja see identical across runs while
# what it resolves to tracks the current mount underneath (verified directly: CMake records
# whatever path it's given as-is - including a symlink - without resolving it first).
[ -L "$CACHE/toolchain" ] || rm -rf "$CACHE/toolchain"
[ -L "$CACHE/native-prebuilt" ] || rm -rf "$CACHE/native-prebuilt"
ln -sfn "$HERE/usr/toolchain" "$CACHE/toolchain"
ln -sfn "$HERE/native-prebuilt" "$CACHE/native-prebuilt"
exec "$HERE/usr/bin/wiicompiled-setup" --workspace "$CACHE" \
--translator-bin "$HERE/usr/bin/translator-cli" \
--disc-tool-bin "$HERE/usr/bin/nodtool" "$@"
--disc-tool-bin "$HERE/usr/bin/nodtool" \
--cc "$CACHE/toolchain/bin/clang" \
--cxx "$CACHE/toolchain/bin/clang++" \
--fuse-ld lld \
--cmake "$CACHE/toolchain/bin/cmake" \
--ninja "$CACHE/toolchain/bin/ninja" \
--native-prebuilt-dir "$CACHE/native-prebuilt" "$@"
APPRUN
chmod +x "$appdir/AppRun"
@@ -155,11 +283,13 @@ PY
echo "Resolving appimagetool..."
appimagetool="$appimagetool_override"
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
echo "Downloading appimagetool..."
echo "Downloading appimagetool ($appimagetool_arch)..."
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"
chmod +x "$appimagetool"
fi
@@ -169,5 +299,6 @@ mkdir -p "$output_dir"
echo "Packaging..."
# appimagetool detects the target architecture from the first ELF executable it finds in the
# AppDir; AppRun here is a shell script, not ELF, so ARCH must be set explicitly.
ARCH=x86_64 "$appimagetool" "$appdir" "$output_dir/WiiCompiled-Setup-x86_64.AppImage"
echo "Built: $output_dir/WiiCompiled-Setup-x86_64.AppImage"
output_name="WiiCompiled-Setup-$appimagetool_arch.AppImage"
ARCH="$appimagetool_arch" "$appimagetool" "$appdir" "$output_dir/$output_name"
echo "Built: $output_dir/$output_name"
+144
View File
@@ -0,0 +1,144 @@
#!/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 current-architecture native build 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'
macos_arch=$(uname -m)
case "$macos_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $macos_arch" ;; esac
macos_deployment_target=12.0
workspace=$(cd "$workspace" && pwd)
[[ -n "$output_dir" ]] || fail '--output-dir is required'
case "$profile" in base|retro-rewind|both) ;; *) fail '--profile must be base, retro-rewind, or both' ;; esac
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-$macos_arch"
assert_file "$project" 'translation project'
if [[ -n "$game" ]]; then "$script_dir/macos/extract-disc.command" --game "$game" --assets-dir "$assets" --nodtool "$nodtool"; fi
assert_file "$assets/main.dol" 'extracted main.dol'; assert_file "$assets/StaticR.rel" 'extracted StaticR.rel'
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'
# Use Aurora's pinned SDL3 source on macOS. A system SDL3 can be older than
# Aurora's required API even when find_package() succeeds.
"$cmake_bin" -S "$workspace/runtime" -B "$native_build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM="$ninja_bin" -DCMAKE_OSX_ARCHITECTURES="$macos_arch" -DCMAKE_OSX_DEPLOYMENT_TARGET="$macos_deployment_target" -DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs" -DAURORA_SDL3_PROVIDER=vendor
targets=(); [[ "$profile" != retro-rewind ]] && targets+=(WiiCompiled); [[ "$profile" != base ]] && targets+=(RetroRewind)
step compile "Compiling ${targets[*]} locally"; "$cmake_bin" --build "$native_build" --target "${targets[@]}" --parallel "$global_jobs"
if [[ "$profile" != retro-rewind ]]; then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product WiiCompiled --output-dir "${base_output_dir:-$output_dir}" --architecture "$macos_arch" --minimum-system-version "$macos_deployment_target"; fi
if (( builds_retro )); then "$script_dir/macos/publish-app.command" --build-dir "$native_build" --product RetroRewind --output-dir "$output_dir" --architecture "$macos_arch" --minimum-system-version "$macos_deployment_target"; fi
printf 'MKWCBUILD:OUTPUT=%s\n' "$output_dir"
+111 -12
View File
@@ -2,11 +2,12 @@
# Linux build automation: translate -> emit build shards -> configure -> compile -> publish.
#
# This is the native-Linux counterpart to Launcher/LocalBuild.ps1. It is a from-scratch parallel
# implementation, not a port of NativeBuildFlags.ps1: that file's canonical flags and
# prebuilt-package fingerprinting exist only for the Windows/mingw toolchain (a precompiled
# aurora/third-party package, offline pinned dependencies) that this script does not build.
# Linux always builds aurora from source, letting its own CMake auto-detect Vulkan + vendor
# SDL3/Dawn via FetchContent - the same configuration already verified working by hand.
# implementation, not a port of NativeBuildFlags.ps1: that file's canonical flags exist only for
# the Windows/mingw toolchain's offline pinned dependencies, which this script does not use.
# Without --native-prebuilt-dir, aurora is built from source, letting its own CMake auto-detect
# Vulkan + vendor SDL3/Dawn via FetchContent - the same configuration already verified working by
# hand. With it, aurora is not compiled at all - see Launcher/Prepare-NativePrebuilt.sh, which
# harvests exactly that package (the Linux counterpart to Prepare-NativePrebuilt.ps1).
set -euo pipefail
# ---------------------------------------------------------------------------
@@ -62,6 +63,9 @@ ninja_override=""
dotnet_override=""
translator_dll_override=""
translator_bin_override=""
fuse_ld_override=""
native_prebuilt_dir=""
sysroot=""
usage() {
cat <<'EOF'
@@ -77,10 +81,15 @@ Usage: local-build.sh --output-dir DIR [options]
--force-clean-build Discard every translation/build cache first
--parallel N Pin translator threads, translated-shard job pool, and Ninja parallelism to N
--cc PATH / --cxx PATH C/C++ compiler (default: cc/c++ on PATH)
--fuse-ld NAME_OR_PATH Linker passed to clang as -fuse-ld=NAME_OR_PATH (default: clang's own default linker)
--cmake PATH / --ninja PATH Build tools (default: on PATH)
--dotnet PATH dotnet executable (default: on PATH)
--translator-dll PATH Pre-built Translator.Cli.dll (skips building the translator; still needs --dotnet to run it)
--translator-bin PATH Self-contained Translator.Cli executable (skips building AND needs no dotnet at all)
--native-prebuilt-dir DIR Precompiled aurora/third-party package (see Prepare-NativePrebuilt.sh);
skips compiling aurora-main from source entirely
--sysroot PATH Passed to CMake as -DCMAKE_SYSROOT: where the compiler resolves
standard headers/startup files
EOF
}
@@ -97,11 +106,14 @@ while [[ $# -gt 0 ]]; do
--parallel) parallel_override=$2; shift 2 ;;
--cc) cc_override=$2; shift 2 ;;
--cxx) cxx_override=$2; shift 2 ;;
--fuse-ld) fuse_ld_override=$2; shift 2 ;;
--cmake) cmake_override=$2; shift 2 ;;
--ninja) ninja_override=$2; shift 2 ;;
--dotnet) dotnet_override=$2; shift 2 ;;
--translator-dll) translator_dll_override=$2; shift 2 ;;
--translator-bin) translator_bin_override=$2; shift 2 ;;
--native-prebuilt-dir) native_prebuilt_dir=$2; shift 2 ;;
--sysroot) sysroot=$2; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown argument: $1" ;;
esac
@@ -157,6 +169,10 @@ require_command "$ninja_bin" ninja
require_command "$cc_bin" cc
require_command "$cxx_bin" cxx
if [[ -n "$native_prebuilt_dir" ]]; then
assert_file "$native_prebuilt_dir/native_prebuilt.cmake" "Native prebuilt package"
fi
project=$workspace/projects/mkwii/recomp.yml
assets=$workspace/Assets
generated=$workspace/generated
@@ -174,6 +190,30 @@ assert_file "$project" "Translation project"
assert_file "$assets/main.dol" "Extracted main.dol (see translator/README.md - owning the game is required)"
assert_file "$assets/StaticR.rel" "Extracted StaticR.rel (see translator/README.md - owning the game is required)"
# The AppImage bundles Clang, but Linux startup objects and the C/C++ link runtimes
# still come from the host. Check them before the expensive translation so a missing
# development package produces a useful error instead of CMake's generic exit 1.
link_probe_dir=$(mktemp -d)
link_probe_flags=()
[[ -z "$sysroot" ]] || link_probe_flags+=(--sysroot="$sysroot")
[[ -z "$fuse_ld_override" ]] || link_probe_flags+=(-fuse-ld="$fuse_ld_override")
printf 'int main(void) { return 0; }\n' > "$link_probe_dir/probe.c"
cat > "$link_probe_dir/probe.cpp" <<'EOF'
#include <vector>
int main() { std::vector<int> values{1}; return values.front() - 1; }
EOF
if ! "$cc_bin" "${link_probe_flags[@]}" "$link_probe_dir/probe.c" -o "$link_probe_dir/probe-c" > "$link_probe_dir/error" 2>&1; then
cat "$link_probe_dir/error" >&2
rm -rf "$link_probe_dir"
fail "The C compiler cannot link a test program. Linux needs C development files (glibc startup objects and a compiler runtime) in addition to bundled Clang. Install your distribution's development packages, or on SteamOS run WiiCompiled through the Wheel Wizard Flatpak."
fi
if ! "$cxx_bin" "${link_probe_flags[@]}" "$link_probe_dir/probe.cpp" -o "$link_probe_dir/probe-cxx" > "$link_probe_dir/error" 2>&1; then
cat "$link_probe_dir/error" >&2
rm -rf "$link_probe_dir"
fail "The C++ compiler cannot link a test program. Install your distribution's C++ development packages, or on SteamOS run WiiCompiled through the Wheel Wizard Flatpak."
fi
rm -rf "$link_probe_dir"
# Literal line matching against the manifest's fixed shape, not a YAML dependency - the same
# approach NativeBuildFlags.ps1's Get-MkwProjectPins uses on Windows, kept here only for the one
# field this script actually needs from the manifest.
@@ -336,26 +376,83 @@ translator "${shard_args[@]}"
# ---------------------------------------------------------------------------
keep_native_build=0
stale_reason=""
if [[ -f "$build/CMakeCache.txt" ]]; then
expected_home=$workspace/runtime
cache_home=$(grep '^CMAKE_HOME_DIRECTORY:INTERNAL=' "$build/CMakeCache.txt" | cut -d= -f2- || true)
if [[ -n "$cache_home" && "$(cd "$cache_home" 2>/dev/null && pwd)" == "$expected_home" ]]; then
keep_native_build=1
# CMake auto-detects and caches auxiliary tool paths (CMAKE_AR/RANLIB/LINKER/ASM_COMPILER
# and their per-language *_AR/*_RANLIB variants) only once, the first time a language's
# compiler is checked - unlike CMAKE_C_COMPILER/CMAKE_CXX_COMPILER, which get overwritten by
# the -D flags below on every configure, these are never refreshed on a plain reconfigure.
# An AppImage's own AppRun works around this at the source by keeping --cc/--cxx/--cmake/
# --ninja pointed at a stable symlink it re-targets at the current mount every launch
# (see build-appimage.sh), so the *path string* CMake caches never actually changes run to
# run - but this check stays as a general fallback for any tool path that goes stale some
# other way (a moved/removed system toolchain, a relocated portable-tools directory, etc):
# any :FILEPATH= cache entry whose recorded path no longer exists means this cache belongs
# to a toolchain location that's gone - not just a workspace/path mismatch.
while IFS= read -r tool_path; do
[[ -n "$tool_path" ]] || continue
# CMake's own sentinel for "this optional tool was legitimately never found" (e.g.
# clang-scan-deps, not required here) - not a path at all, and not a sign of anything
# stale. Without this exclusion, every configure wiped the cache unconditionally.
[[ "$tool_path" != *-NOTFOUND ]] || continue
if [[ ! -e "$tool_path" ]]; then
keep_native_build=0
stale_reason=" (cached tool path no longer exists: $tool_path)"
break
fi
done < <(grep -o ':FILEPATH=.*' "$build/CMakeCache.txt" | sed 's/^:FILEPATH=//')
fi
fi
if [[ -d "$build" && "$keep_native_build" -eq 0 ]]; then
echo "MKWCBUILD: The native build cache does not belong to this workspace path; 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"
elif [[ "$keep_native_build" -eq 1 ]]; then
echo "MKWCBUILD: Reusing the incremental native build directory"
fi
configure_args=(-S "$workspace/runtime" -B "$build" -G Ninja
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_C_COMPILER="$cc_bin" -DCMAKE_CXX_COMPILER="$cxx_bin"
-DCMAKE_MAKE_PROGRAM="$ninja_bin"
-DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs")
# CMAKE_C_COMPILER/CXX_COMPILER stay stable across AppImage runs on their own (they're exactly
# what's passed via -D above, and AppRun points --cc/--cxx at a symlink it re-targets at the
# current mount every launch - see build-appimage.sh). CMAKE_AR/RANLIB/LINKER/ASM_COMPILER do NOT
# inherit that stability just because $cc_bin does: verified directly that even with a stable
# --cc symlink, CMake's own auto-detection of these still resolved to the *real*, ephemeral mount
# path underneath (clang's own driver locates its sibling llvm-ar/ld.lld tools by resolving its own
# invoked path, symlinks included, rather than trusting the symlink CMake invoked it through) -
# each subsequent run's differing command line then made Ninja rebuild every object from scratch
# even though nothing had actually changed. Deriving these from $cc_bin (itself already stable)
# and re-passing them explicitly every configure keeps them pinned to the same stable value too.
if [[ "$cc_bin" == */* ]]; then
toolchain_bin=$(dirname "$cc_bin")
[[ -x "$toolchain_bin/llvm-ar" ]] && configure_args+=(-DCMAKE_AR="$toolchain_bin/llvm-ar" -DCMAKE_ASM_COMPILER_AR="$toolchain_bin/llvm-ar" -DCMAKE_C_COMPILER_AR="$toolchain_bin/llvm-ar" -DCMAKE_CXX_COMPILER_AR="$toolchain_bin/llvm-ar")
[[ -x "$toolchain_bin/llvm-ranlib" ]] && configure_args+=(-DCMAKE_RANLIB="$toolchain_bin/llvm-ranlib" -DCMAKE_ASM_COMPILER_RANLIB="$toolchain_bin/llvm-ranlib" -DCMAKE_C_COMPILER_RANLIB="$toolchain_bin/llvm-ranlib" -DCMAKE_CXX_COMPILER_RANLIB="$toolchain_bin/llvm-ranlib")
[[ -x "$toolchain_bin/ld.lld" ]] && configure_args+=(-DCMAKE_LINKER="$toolchain_bin/ld.lld")
configure_args+=(-DCMAKE_ASM_COMPILER="$cc_bin")
fi
if [[ -n "$fuse_ld_override" ]]; then
configure_args+=(-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=$fuse_ld_override")
fi
if [[ -n "$native_prebuilt_dir" ]]; then
configure_args+=(-DMKW_NATIVE_PREBUILT_DIR="$native_prebuilt_dir")
fi
if [[ -n "$sysroot" ]]; then
configure_args+=(-DCMAKE_SYSROOT="$sysroot")
else
# Explicitly clear any cached CMAKE_SYSROOT from a prior configure so an
# incremental build that transitions from one sysroot to none does not
# silently keep the stale cached path.
configure_args+=(-UCMAKE_SYSROOT)
fi
log_step configure-native "Configuring the native toolchain"
"$cmake_bin" -S "$workspace/runtime" -B "$build" -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER="$cc_bin" -DCMAKE_CXX_COMPILER="$cxx_bin" \
-DCMAKE_MAKE_PROGRAM="$ninja_bin" \
-DMKW_TRANSLATED_COMPILE_JOBS="$translated_jobs"
"$cmake_bin" "${configure_args[@]}"
case "$profile" in
base) targets=(WiiCompiled) ;;
@@ -384,7 +481,9 @@ publish_built_product() {
local exe=$build/$target
assert_file "$exe" "Locally compiled game executable"
cp -f "$exe" "$destination/$target"
for name in dsp_coef.bin initial_pipeline_cache.db; do
# cacert.pem is the TLS root bundle the mbed TLS path looks up beside the executable
# (runtime/src/hle/net/network_ssl.cpp); without it HTTPS fails at runtime.
for name in dsp_coef.bin initial_pipeline_cache.db cacert.pem; do
[[ -f "$build/$name" ]] && cp -f "$build/$name" "$destination/"
done
[[ -d "$build/wii_bootstrap" ]] && cp -rf "$build/wii_bootstrap" "$destination/"
+174
View File
@@ -0,0 +1,174 @@
#!/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-arm64 PATH --nodtool-x86_64 PATH --translator-arm64 PATH --translator-x86_64 PATH --cmake-root DIR --ninja-arm64 PATH --ninja-x86_64 PATH --output PKG [options]
Creates a game-code-free WiiCompiled Setup.pkg. The supplied tools must be
maintainer-verified, redistributable macOS artifacts for both arm64 and
x86_64. The setup package selects native tools for its host while the game is
compiled locally for that host architecture. CMake must be universal2. The
resulting pkg is unsigned unless
--installer-identity is supplied; releases should sign and notarize it with a
Developer ID Installer certificate.
--workspace DIR Repository root (default: script's grandparent)
--version VERSION Bundle/package version (default: 0.1.0)
--installer-identity NAME Developer ID Installer identity for productbuild
EOF
}
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace=$(cd "$script_dir/../.." && pwd); nodtool_arm64=""; nodtool_x86_64=""; translator_arm64=""; translator_x86_64=""; cmake_root=""; ninja_arm64=""; ninja_x86_64=""; output=""; version=0.1.0; identity=""
while (($#)); do
case "$1" in
--workspace) workspace=${2:-}; shift 2 ;;
--nodtool-arm64) nodtool_arm64=${2:-}; shift 2 ;;
--nodtool-x86_64) nodtool_x86_64=${2:-}; shift 2 ;;
--translator-arm64) translator_arm64=${2:-}; shift 2 ;;
--translator-x86_64) translator_x86_64=${2:-}; shift 2 ;;
--cmake-root) cmake_root=${2:-}; shift 2 ;;
--ninja-arm64) ninja_arm64=${2:-}; shift 2 ;;
--ninja-x86_64) ninja_x86_64=${2:-}; shift 2 ;;
--output) output=${2:-}; shift 2 ;;
--version) version=${2:-}; shift 2 ;;
--installer-identity) identity=${2:-}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown option: $1" ;;
esac
done
version=${version#v}
if [[ -z "$version" || "$version" == "0.1.0" ]]; then
local_csproj="$workspace/Launcher/Directory.Build.props"
if [[ -f "$local_csproj" ]]; then
detected=$(grep -m1 '<Version>' "$local_csproj" | sed -E 's/.*<Version>([^<]+)<\/Version>.*/\1/' || true)
if [[ -n "$detected" ]]; then
version="$detected"
fi
fi
fi
[[ "$version" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]] || fail '--version must contain one to three period-separated integers'
IFS=. read -r version_major version_minor version_patch <<< "$version"
short_version="$version_major.${version_minor:-0}.${version_patch:-0}"
for tool in pkgbuild productbuild ditto codesign lipo; do command -v "$tool" >/dev/null || fail "required macOS tool unavailable: $tool"; done
for tool_path in "$nodtool_arm64" "$nodtool_x86_64" "$translator_arm64" "$translator_x86_64" "$ninja_arm64" "$ninja_x86_64"; do [[ -x "$tool_path" ]] || fail 'each architecture-specific tool must name an executable'; done
[[ -x "$cmake_root/bin/cmake" ]] || fail '--cmake-root must contain bin/cmake'
require_arch() {
local artifact=$1 arch=$2 label=$3
lipo "$artifact" -verify_arch "$arch" >/dev/null 2>&1 || fail "$label must contain a $arch slice: $artifact"
}
require_arch "$nodtool_arm64" arm64 '--nodtool-arm64'; require_arch "$nodtool_x86_64" x86_64 '--nodtool-x86_64'
require_arch "$translator_arm64" arm64 '--translator-arm64'; require_arch "$translator_x86_64" x86_64 '--translator-x86_64'
require_arch "$ninja_arm64" arm64 '--ninja-arm64'; require_arch "$ninja_x86_64" x86_64 '--ninja-x86_64'
lipo "$cmake_root/bin/cmake" -verify_arch arm64 x86_64 >/dev/null 2>&1 || fail '--cmake-root/bin/cmake must be universal2'
# Slice checks above prevent accidental cross-architecture packaging. Exercise
# each supplied executable as well: an incorrectly bundled runtime can have a
# valid Mach-O header but still fail before the setup app can use it. Apple
# Silicon maintainers validate Intel tools through Rosetta when it is present.
host_arch=$(uname -m)
run_for_arch() {
local arch=$1 label=$2
shift 2
if [[ "$arch" == "$host_arch" ]]; then
"$@" >/dev/null || fail "$label did not run successfully"
elif [[ "$host_arch" == arm64 && "$arch" == x86_64 ]] && /usr/bin/arch -x86_64 /usr/bin/true >/dev/null 2>&1; then
/usr/bin/arch -x86_64 "$@" >/dev/null || fail "$label did not run successfully under Rosetta"
else
# Intel hosts cannot execute arm64 binaries. The slice remains checked
# above; CI or an Apple Silicon maintainer must execute that tool set.
printf 'build-setup-pkg.command: warning: unable to execute %s on %s; architecture slice was verified, but run it in %s CI before release\n' \
"$label" "$host_arch" "$arch" >&2
fi
}
for arch in arm64 x86_64; do
if [[ "$arch" == arm64 ]]; then
nodtool=$nodtool_arm64; translator=$translator_arm64; ninja=$ninja_arm64
else
nodtool=$nodtool_x86_64; translator=$translator_x86_64; ninja=$ninja_x86_64
fi
run_for_arch "$arch" "--nodtool-$arch" "$nodtool" --version
run_for_arch "$arch" "--translator-$arch" "$translator" --help
run_for_arch "$arch" "--ninja-$arch" "$ninja" --version
done
run_for_arch "$host_arch" '--cmake-root/bin/cmake' "$cmake_root/bin/cmake" --version
workspace=$(cd "$workspace" && pwd); output=$(cd "$(dirname "$output")" && pwd)/$(basename "$output")
stage=$(mktemp -d "${TMPDIR:-/tmp}/wiicompiled-pkg.XXXXXX")
trap 'rm -rf "$stage"' EXIT
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>12.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
# setup.command uses this marker to update source inputs in an existing user
# workspace without replacing extracted game assets or Retro Rewind files.
printf '%s\n' "$version" > "$resources/workspace/.bundle-version"
mkdir -p "$resources/tools/cmake" "$resources/tools/arm64" "$resources/tools/x86_64"
copy_clean "$nodtool_arm64" "$resources/tools/arm64/nodtool"; chmod +x "$resources/tools/arm64/nodtool"
copy_clean "$nodtool_x86_64" "$resources/tools/x86_64/nodtool"; chmod +x "$resources/tools/x86_64/nodtool"
copy_clean "$translator_arm64" "$resources/tools/arm64/Translator.Cli"; chmod +x "$resources/tools/arm64/Translator.Cli"
copy_clean "$translator_x86_64" "$resources/tools/x86_64/Translator.Cli"; chmod +x "$resources/tools/x86_64/Translator.Cli"
copy_clean "$cmake_root" "$resources/tools/cmake"
copy_clean "$ninja_arm64" "$resources/tools/arm64/ninja"; chmod +x "$resources/tools/arm64/ninja"
copy_clean "$ninja_x86_64" "$resources/tools/x86_64/ninja"; chmod +x "$resources/tools/x86_64/ninja"
copy_clean "$workspace/LICENSE" "$resources/LICENSE"
copy_clean "$workspace/THIRD-PARTY-NOTICES.md" "$resources/THIRD-PARTY-NOTICES.md"
codesign --force --deep --sign - "$app"
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'
@@ -0,0 +1,11 @@
# Cross-compile a thin x86_64 macOS build from an Apple Silicon Mac.
# Pass this file on the first configure with:
# -DCMAKE_TOOLCHAIN_FILE=/absolute/path/to/macos-x86_64-toolchain.cmake
#
# CMAKE_SYSTEM_PROCESSOR is deliberately declared here rather than inferred
# from CMAKE_OSX_ARCHITECTURES, so target-aware CMake dependencies select their
# x86_64 artifacts.
set(CMAKE_SYSTEM_NAME Darwin)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_OSX_ARCHITECTURES x86_64 CACHE STRING
"Target macOS architectures" FORCE)
+171
View File
@@ -0,0 +1,171 @@
#!/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 [options]
Copies a locally built product and its runtime assets into OUTPUT-DIR/<product>.app.
It bundles non-system dylibs, rewrites their install names, and ad-hoc signs the
result. This is suitable for local use; a release must replace ad-hoc signing
with the project's Developer ID signing and notarization process.
--architecture {arm64|x86_64} Required architecture of the compiled product (default: host)
--minimum-system-version VERSION App bundle minimum macOS version (default: 12.0)
EOF
}
build_dir=""; product=""; output_dir=""; architecture=$(uname -m); minimum_system_version=12.0
while (($#)); do
case "$1" in
--build-dir) build_dir=${2:-}; shift 2 ;;
--product) product=${2:-}; shift 2 ;;
--output-dir) output_dir=${2:-}; shift 2 ;;
--architecture) architecture=${2:-}; shift 2 ;;
--minimum-system-version) minimum_system_version=${2:-}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown option: $1" ;;
esac
done
[[ "$product" == WiiCompiled || "$product" == RetroRewind ]] || fail '--product must be WiiCompiled or RetroRewind'
[[ "$architecture" == arm64 || "$architecture" == x86_64 ]] || fail '--architecture must be arm64 or x86_64'
[[ "$minimum_system_version" =~ ^[0-9]+(\.[0-9]+){1,2}$ ]] || fail '--minimum-system-version must contain two or three period-separated integers'
for tool in codesign ditto install_name_tool lipo otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
[[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product"
lipo "$build_dir/$product" -verify_arch "$architecture" || fail "compiled product is not $architecture: $build_dir/$product"
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done
app="$output_dir/$product.app"
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>$minimum_system_version</string>
<key>NSHighResolutionCapable</key><true/>
</dict></plist>
EOF
ditto "$build_dir/$product" "$macos/$product"
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem wii_bootstrap; do
ditto "$build_dir/$asset" "$resources/$asset"
ln -s "../Resources/$asset" "$macos/$asset"
done
# Expand each image's rpaths before inheriting them, so @loader_path stays
# relative to the image that declared it rather than a descendant library.
expanded_rpaths() {
local target=$1 rpath
while IFS= read -r rpath; do
case "$rpath" in
@loader_path/*) rpath="$(dirname "$target")/${rpath#@loader_path/}" ;;
@loader_path) rpath="$(dirname "$target")" ;;
@executable_path/*) rpath="$build_dir/${rpath#@executable_path/}" ;;
@executable_path) rpath="$build_dir" ;;
esac
printf '%s\n' "$rpath"
done < <(otool -l "$target" | awk '
/LC_RPATH/ { rpath = 1; next }
rpath && /^[[:space:]]*path / {
sub(/^[[:space:]]*path[[:space:]]+/, "");
sub(/[[:space:]]+\(offset[[:space:]]+[0-9]+\)$/, "");
print;
rpath = 0;
}')
}
# Resolve a non-system dependency using the current image's rpaths followed
# by the inherited loader stack, matching dyld's dependency-chain search.
dependency_path() {
local current=$1 dependency=$2 search_rpaths=$3 rpath candidate
case "$dependency" in
/System/Library/*|/usr/lib/*)
return 1
;;
/*)
[[ -f "$dependency" ]] && { printf '%s\n' "$dependency"; return 0; }
;;
@loader_path/*)
candidate="$(dirname "$current")/${dependency#@loader_path/}"
[[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; }
;;
@executable_path/*)
candidate="$build_dir/${dependency#@executable_path/}"
[[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; }
;;
@rpath/*|*.dylib)
local subpath
if [[ "$dependency" == @rpath/* ]]; then
subpath="${dependency#@rpath/}"
else
subpath="$dependency"
fi
while IFS= read -r rpath; do
[[ -n "$rpath" ]] || continue
candidate="$rpath/$subpath"
[[ -f "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; }
done <<< "$search_rpaths"
;;
esac
return 1
}
# Build a closure of non-system dylibs. System libraries remain system
# references, while every resolved dependency is copied beside the executable.
queue=("$build_dir/$product")
queue_rpaths=("")
while ((${#queue[@]})); do
current=${queue[0]}
inherited_rpaths=${queue_rpaths[0]}
queue=("${queue[@]:1}")
queue_rpaths=("${queue_rpaths[@]:1}")
current_rpaths=$(expanded_rpaths "$current")
if [[ -n "$inherited_rpaths" ]]; then
current_rpaths="${current_rpaths}${current_rpaths:+$'\n'}$inherited_rpaths"
fi
self_id=$(otool -D "$current" 2>/dev/null | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]*$/\1/p' || true)
while IFS= read -r dependency; do
[[ -z "$dependency" ]] && continue
[[ -n "$self_id" && "$dependency" == "$self_id" ]] && continue
case "$dependency" in
/System/Library/*|/usr/lib/*) continue ;;
esac
dep_path=$(dependency_path "$current" "$dependency" "$current_rpaths") || fail "unresolved non-system dependency: '$dependency' needed by '$current'"
name=$(basename "$dep_path")
if [[ ! -f "$frameworks/$name" ]]; then
ditto "$dep_path" "$frameworks/$name"
install_name_tool -id "@rpath/$name" "$frameworks/$name"
queue+=("$dep_path")
queue_rpaths+=("$current_rpaths")
fi
done < <(otool -L "$current" | tail -n +2 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]+\(compatibility version .*/\1/p')
done
while IFS= read -r binary; do
while IFS= read -r old; do
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 | sed -nE 's/^[[:space:]]*(.*)[[:space:]]+\(compatibility version .*/\1/p')
done < <(find "$frameworks" -type f -print; printf '%s\n' "$macos/$product")
find "$frameworks" -type f -exec codesign --force --sign - {} +
codesign --force --deep --sign - "$app"
codesign --verify --deep --strict "$app"
printf 'MKWCBUILD:APP=%s\n' "$app"
+183
View File
@@ -0,0 +1,183 @@
#!/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"
cmake_bin="$resources/tools/cmake/bin/cmake"
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'
host_arch=$(uname -m)
case "$host_arch" in arm64|x86_64) ;; *) fail "unsupported macOS architecture: $host_arch" ;; esac
host_tools="$resources/tools/$host_arch"
nodtool="$host_tools/nodtool"
translator="$host_tools/Translator.Cli"
ninja_bin="$host_tools/ninja"
if [[ -z "$game" ]]; then
game=$(/usr/bin/osascript <<'APPLESCRIPT'
set selectedFile to choose file with prompt "Choose your clean Mario Kart Wii PAL (RMCP01) disc image"
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
for tool in "$nodtool" "$translator" "$cmake_bin" "$ninja_bin"; do
/usr/bin/lipo "$tool" -verify_arch "$host_arch" >/dev/null 2>&1 || \
fail "the packaged $(basename "$tool") does not support $host_arch"
done
mkdir -p "$support_root" "$products"
source_bundle_version="$workspace_source/.bundle-version"
workspace_bundle_version="$workspace/.bundle-version"
needs_workspace_refresh=0
if [[ ! -f "$workspace/projects/mkwii/recomp.yml" ]]; then
needs_workspace_refresh=1
elif [[ -f "$source_bundle_version" ]] && [[ ! -f "$workspace_bundle_version" || "$(<"$source_bundle_version")" != "$(<"$workspace_bundle_version")" ]]; then
needs_workspace_refresh=1
fi
if (( needs_workspace_refresh )); then
printf 'Preparing the local build workspace...\n'
if [[ ! -d "$workspace" ]]; then
/usr/bin/ditto "$workspace_source" "$workspace"
else
# Refresh only packaged source inputs. Assets and the staged Retro
# Rewind package belong to the user and stay in place.
for source in aurora-main projects runtime translator Launcher; do
rm -rf "$workspace/$source"
/usr/bin/ditto "$workspace_source/$source" "$workspace/$source"
done
/usr/bin/ditto "$source_bundle_version" "$workspace_bundle_version"
# A dependency provider can be cached in this directory, so make the
# refreshed sources configure from a clean native build tree.
rm -rf "$workspace/native-build-macos-arm64" "$workspace/native-build-macos-x86_64"
fi
fi
profile=base
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"
cached_payload_valid=0
if [[ -f "$retro_wfc_payload" ]]; then
if "$translator" validate-retro-wfc-payload --directory "$retro_wfc_dir"; then
cached_payload_valid=1
else
printf 'Discarding an invalid cached Retro-WFC payload...\n' >&2
rm -f "$retro_wfc_payload"
fi
fi
# A signed cache may still be an older vulnerable revision, so always attempt to replace it
# with the current signed snapshot. A transport failure may fall back to the verified cache;
# a downloaded snapshot with an invalid signature remains a hard failure.
printf 'Downloading the current 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
if /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'; then
"$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
elif (( cached_payload_valid )); then
printf 'Could not download the current Retro-WFC payload; using the previously verified cached payload.\n' >&2
rm -rf "$payload_stage"
trap - EXIT
else
fail 'could not download the current Retro-WFC payload and no valid cached payload is available'
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."
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# Exercise a real dyld dependency chain before and after app packaging.
set -euo pipefail
[[ $(uname -s) == Darwin ]] || { printf 'This test requires macOS.\n' >&2; exit 1; }
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
temp_root=$(cd "${TMPDIR:-/tmp}" && pwd)
test_root=$(mktemp -d "$temp_root/wiicompiled-publish-test.XXXXXX")
[[ "$test_root" == "$temp_root"/wiicompiled-publish-test.* ]] || exit 1
trap 'rm -rf "$test_root"' EXIT
build_dir="$test_root/build with spaces"
output_dir="$test_root/output with spaces"
mkdir -p "$build_dir/A/b" "$build_dir/A/c" "$build_dir/wii_bootstrap" "$output_dir"
cat > "$test_root/c.c" <<'EOF'
int value_c(void) { return 7; }
EOF
cat > "$test_root/b.c" <<'EOF'
extern int value_c(void);
int value_b(void) { return 2 * value_c(); }
EOF
cat > "$test_root/a.c" <<'EOF'
extern int value_b(void);
int value_a(void) { return 1 + value_b(); }
EOF
cat > "$test_root/main.c" <<'EOF'
#include <stdio.h>
extern int value_a(void);
int main(void) { printf("%d\n", value_a()); return 0; }
EOF
clang -dynamiclib "$test_root/c.c" -o "$build_dir/A/c/libC.dylib" \
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libC.dylib
clang -dynamiclib "$test_root/b.c" -o "$build_dir/A/b/libB.dylib" \
-L "$build_dir/A/c" -lC \
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libB.dylib
clang -dynamiclib "$test_root/a.c" -o "$build_dir/A/libA.dylib" \
-L "$build_dir/A/b" -lB \
-Wl,-headerpad_max_install_names -Wl,-install_name,@rpath/libA.dylib \
-Wl,-rpath,@loader_path/b -Wl,-rpath,@loader_path/c
clang "$test_root/main.c" -o "$build_dir/WiiCompiled" \
-L "$build_dir/A" -lA \
-Wl,-headerpad_max_install_names -Wl,-rpath,@executable_path/A
# B has no rpaths: its C dependency must inherit A's loader-relative path.
[[ $("$build_dir/WiiCompiled") == 15 ]]
for asset in dsp_coef.bin initial_pipeline_cache.db cacert.pem; do
: > "$build_dir/$asset"
done
bash "$script_dir/publish-app.command" --build-dir "$build_dir" \
--product WiiCompiled --output-dir "$output_dir" --architecture "$(uname -m)"
for name in libA.dylib libB.dylib libC.dylib; do
[[ -f "$output_dir/WiiCompiled.app/Contents/Frameworks/$name" ]]
done
# Remove access to the original paths and relocate the app before executing.
mv "$build_dir" "$test_root/hidden build"
mkdir "$test_root/relocated app"
mv "$output_dir/WiiCompiled.app" "$test_root/relocated app/WiiCompiled.app"
[[ $("$test_root/relocated app/WiiCompiled.app/Contents/MacOS/WiiCompiled") == 15 ]]
printf 'publish-app dependency-chain test passed\n'
+242
View File
@@ -0,0 +1,242 @@
#!/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=fccecb1906e7ddf5ec040aec5b646b650e2daaafa4423b41341c4717db5bdec0
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=d431eff9f064c86ee7c4c94af570a8f74fcccd1f74c6f0da3af32ce34a1e1b05
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 ---
# built from PR https://github.com/llvm/llvm-project/pull/222821 on official LLVM Github Actions Runner
# only switch to an official stable LLVM release again once:
# - this PR has merged https://github.com/llvm/llvm-project/pull/221365 and been backported to LLVM stable branch
# - this bug has been fixed with a workaround in the Wiicompiled translator https://github.com/patchzyy/Wiicompiled/issues/208 or in LLVM and been backported to LLVM stable branch
llvm_archive_name="LLVM-PR222821-5ae1c7c43a11b4cdc5ce4dd483c28357bab7dae2-Linux-$llvm_release_arch.tar.xz"
llvm_archive="$downloads/$llvm_archive_name"
download_verified "$llvm_archive" \
"https://github.com/theofficialgman/llvm-project/releases/download/llvmorg-22.1.8-patched/$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_archive_name%.tar.xz}"
[[ -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_archive_name%.tar.gz}"
[[ -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.25)
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))"
+64 -10
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
<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.
There's no emulator in the loop, no interpreter, no JIT, no PowerPC
@@ -39,33 +52,56 @@ Play at several times the console's resolution.
**Music ducking.**
Start playing something else, Spotify, a YouTube video, and
the game automatically mutes its own music until the other audio stops. Optional, if you'd
rather it didn't. All audio that shows in your display media controls on your windows pc fall under this.
rather it didn't. Windows uses system media controls and Linux uses MPRIS players.
On macOS 14.2 or later, this detects other apps with active audio output and excludes
the game's own audio. Apps that keep an output stream running silently can keep
game music muted even when nothing is audible.
**An in-game settings bar.**
Press **F10** while the game window has focus:
- Internal resolution
- FPS counter
- MetalFX spatial upscaling on supported macOS GPUs
- Controller assignment for all four ports
- Full per-controller button mapping
- 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
Everything you change is saved to `Config.toml` on the spot and restored next launch.
**Real controller support.**
Controllers are fed to the game as a GameCube controller.
The port does NOT pretend to be a Wii Remote or Classic Controller.
Mappings are positional (`south`, `east`, `west`, `north`) rather than Xbox-labelled, so the
same config makes sense on Xbox, PlayStation, Nintendo and generic SDL pads alike, and extra
inputs like paddles, touchpads and share buttons show up when the hardware reports them.
**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
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
- Windows 10 or 11, 64-bit
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
- About 20 GB of free disk space during installation (Final game size ~5 GB)
- macOS 12 (Monterey) or later on Apple Silicon (`arm64`) or Intel (`x86_64-v3`); pre-Haswell Intel CPUs are unsupported
- On macOS, a Metal-capable GPU and Apple Xcode Command Line Tools (Setup opens Apple's installer when they are missing)
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
GCZ, CISO, WBFS, WIA and RVZ are accepted.
@@ -86,6 +122,22 @@ image under Settings, turn on **WiiCompiled (beta)**, and hit install from the H
Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
### macOS
Download `WiiCompiled-Setup.pkg` from this repository's Releases page and open it. The universal
package selects the appropriate bundled tools for the host architecture, supporting both Apple Silicon (`arm64`)
and Intel (`x86_64`) Macs. It installs **WiiCompiled Setup** in Applications; open that app, choose
your clean PAL `RMCP01` disc image, and select either the base game or Retro Rewind. For Retro Rewind, choose the `RetroRewind6` folder
or its parent folder.
Setup verifies and extracts the image locally, then translates and compiles the native app on your
Mac. On a first run it may ask macOS to install Xcode Command Line Tools; complete Apple's installer,
then open Setup again. When the build completes, Setup asks for administrator approval once to install
`WiiCompiled.app` (and, if selected, `RetroRewind.app`) in `/Applications`.
Setup opens Terminal while it works, so the extraction and build progress—and any error that needs
reporting—remain visible.
> [!CAUTION]
> Only take builds from this repository's
> [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an
@@ -124,7 +176,9 @@ The default test suite needs no binaries and no host C++ compiler, so you can ha
translator without any game data around.
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
@@ -173,7 +227,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).
## Credits
- **inkwreck** - making the logo
- **[aurora](https://github.com/encounter/aurora)** - the GX rendering/windowing backend this
project's whole graphics layer sits on. MIT licensed.
- **[Dawn](https://dawn.googlesource.com/dawn)** - Google's WebGPU implementation, powering
+8 -9
View File
@@ -26,10 +26,6 @@ Aurora itself vendors:
(shagkur) and Dave Murphy (WinterMute). `aurora-main/lib/card/SRAM.hpp`.
Source: <https://github.com/devkitPro/libogc>
> [!NOTE]
> Upstream aurora ships `assets/screenshot.png`, a rendered frame from a different Nintendo
> title. It is intentionally omitted from this repository.
### Dolphin Emulator data files - GPL-2.0-or-later
Copyright (c) 2003+ Dolphin Emulator Project.
@@ -118,13 +114,16 @@ Source: <https://github.com/higan-emu/libco>. Full license text:
## Fetched at build time and redistributed in release builds
These are pinned in `aurora-main/extern/CMakeLists.txt`, `aurora-main/CMakeLists.txt` and
`aurora-main/cmake/AuroraDawnProvider.cmake`. They are not stored in this repository; the build
downloads them, and release installers carry the resulting binaries. Their license texts are
included in the installer's `licenses/` folder.
These are pinned in `aurora-main/extern/CMakeLists.txt`, `aurora-main/CMakeLists.txt`,
`aurora-main/cmake/AuroraDawnProvider.cmake`, and (for Mbed TLS) `runtime/CMakeLists.txt`. They are
not stored in this repository; the build downloads them - each fetch is pinned to an exact version
with a checked SHA-256 - and links or redistributes the resulting binaries. Their license texts are
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 |
| --- | --- | --- | --- |
| Mbed TLS | 3.6.7 | Apache-2.0 / GPL-2.0-or-later | <https://github.com/Mbed-TLS/mbedtls> |
| Dawn (WebGPU) | `v20260603.191052` prebuilt | BSD-3-Clause | <https://dawn.googlesource.com/dawn> |
| Tint (part of Dawn) | with Dawn | BSD-3-Clause | <https://dawn.googlesource.com/dawn> |
| DirectXShaderCompiler (`dxcompiler.dll`) | with Dawn | NCSA / University of Illinois Open Source | <https://github.com/microsoft/DirectXShaderCompiler> |
@@ -133,7 +132,7 @@ included in the installer's `licenses/` folder.
| Abseil | LTS 20240722.0 | Apache-2.0 | <https://github.com/abseil/abseil-cpp> |
| Dear ImGui | 1.91.9b-docking | MIT | <https://github.com/ocornut/imgui> |
| {fmt} | 11.1.4 | MIT | <https://github.com/fmtlib/fmt> |
| xxHash | 0.8.3 | BSD-2-Clause | <https://github.com/Cyan4973/xxHash> |
| xxHash | 0.8.4 | BSD-2-Clause | <https://github.com/Cyan4973/xxHash> |
| zlib | 1.3.2 | zlib | <https://github.com/madler/zlib> |
| libpng | 1.6.58 | PNG Reference Library License v2 | <https://github.com/pnggroup/libpng> |
| FreeType | 2.14.3 | **FreeType License (FTL)** - see below | <https://freetype.org/> |
+8
View File
@@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.25)
project(aurora LANGUAGES C CXX)
if (APPLE)
enable_language(OBJC)
enable_language(OBJCXX)
endif()
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 20)
@@ -83,3 +84,10 @@ if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND NOT CMAKE_CROSSCOMPIL
enable_testing()
add_subdirectory(tests)
endif ()
option(AURORA_BUILD_METALFX_PRESENTATION_TEST "Build the macOS MetalFX presentation test" OFF)
if (AURORA_BUILD_METALFX_PRESENTATION_TEST AND APPLE AND AURORA_ENABLE_GX AND DAWN_ENABLE_METAL AND AURORA_METALFX_FRAMEWORK)
add_executable(metalfx_presentation_test tests/metalfx_interop/presentation_test.cpp)
target_include_directories(metalfx_presentation_test PRIVATE lib)
target_link_libraries(metalfx_presentation_test PRIVATE aurora::core aurora::gx aurora::vi dawn::webgpu_dawn)
endif ()
+34 -9
View File
@@ -48,7 +48,7 @@ if (_aurora_dawn_provider STREQUAL "auto")
set(_has_package TRUE)
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|aarch64)$")
set(_has_package TRUE)
elseif (APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|x86_64)$")
elseif (APPLE AND (CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|x86_64)$" OR CMAKE_OSX_ARCHITECTURES MATCHES "^(arm64|x86_64)$"))
set(_has_package TRUE)
endif ()
@@ -101,7 +101,7 @@ if (_aurora_dawn_provider STREQUAL "vendor")
include(FetchContent)
FetchContent_Declare(dawn
URL "https://github.com/google/dawn/archive/refs/tags/${AURORA_DAWN_VERSION}.tar.gz"
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(dawn)
@@ -143,6 +143,10 @@ elseif (_aurora_dawn_provider STREQUAL "package")
if (NOT AURORA_DAWN_PACKAGE_URL)
string(TOLOWER "${CMAKE_SYSTEM_NAME}" _dawn_system)
string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _dawn_arch)
if (APPLE AND CMAKE_OSX_ARCHITECTURES)
list(GET CMAKE_OSX_ARCHITECTURES 0 _dawn_osx_arch)
string(TOLOWER "${_dawn_osx_arch}" _dawn_arch)
endif ()
if (_dawn_system STREQUAL "windows")
if (_dawn_arch STREQUAL "x86_64")
set(_dawn_arch "amd64")
@@ -151,15 +155,36 @@ elseif (_aurora_dawn_provider STREQUAL "package")
endif ()
endif ()
set(AURORA_DAWN_PACKAGE_URL
"https://github.com/encounter/dawn-build/releases/download/${AURORA_DAWN_VERSION}/dawn-${_dawn_system}-${_dawn_arch}.tar.gz")
"https://github.com/theofficialgman/dawn-build/releases/download/${AURORA_DAWN_VERSION}/dawn-${_dawn_system}-${_dawn_arch}.tar.gz")
# A release asset is mutable: the same tag has already served two different windows-amd64 archives,
# and a cached extraction is never re-verified. Pin the digest for the combinations we ship.
if (NOT AURORA_DAWN_PACKAGE_URL_HASH
AND AURORA_DAWN_VERSION STREQUAL "v20260603.191052"
AND _dawn_system STREQUAL "windows" AND _dawn_arch STREQUAL "amd64")
set(AURORA_DAWN_PACKAGE_URL_HASH
"SHA256=7785373d569b3b0237918ec9c523239f7d0667857c5ea8242e3cdfde95e6aeab")
if (NOT AURORA_DAWN_PACKAGE_URL_HASH AND AURORA_DAWN_VERSION STREQUAL "v20260603.191052")
if (_dawn_system STREQUAL "windows" AND _dawn_arch STREQUAL "amd64")
set(AURORA_DAWN_PACKAGE_URL_HASH
"SHA256=13be9cff8b9b179c42dcd16aeabb6effcc8f0dfdcc14463eda2a5caeda225142")
elseif (_dawn_system STREQUAL "windows" AND _dawn_arch STREQUAL "arm64")
set(AURORA_DAWN_PACKAGE_URL_HASH
"SHA256=bf2d921110f14a1d6553f673c5597988e66c02af5587e4a1fee167937d247734")
elseif (_dawn_system STREQUAL "linux" AND _dawn_arch STREQUAL "x86_64")
set(AURORA_DAWN_PACKAGE_URL_HASH
"SHA256=7adcf241bb2a24ec0c576609f2d67203e0e65db9c5a286ca2bbb6281fa644b35")
elseif (_dawn_system STREQUAL "linux" AND _dawn_arch STREQUAL "aarch64")
set(AURORA_DAWN_PACKAGE_URL_HASH
"SHA256=2415e253d46f91b2d72fc73bf6055fb31b98b67c773dc546e1991b1cf019732f")
elseif (_dawn_system STREQUAL "darwin" AND _dawn_arch STREQUAL "arm64")
set(AURORA_DAWN_PACKAGE_URL_HASH
"SHA256=0a8ea8eb0159fc0ba1083c52155d9376fb173cffe690b400464a6ad8881bb461")
elseif (_dawn_system STREQUAL "darwin" AND _dawn_arch STREQUAL "x86_64")
set(AURORA_DAWN_PACKAGE_URL_HASH
"SHA256=5fe2c7a2a8b4cb82acee4af16779a83ae333c7657b9dc1a5008f5fd1f5ad5f80")
elseif (_dawn_system STREQUAL "ios" AND _dawn_arch STREQUAL "arm64")
set(AURORA_DAWN_PACKAGE_URL_HASH
"SHA256=f97701d26fd1f25bbcc260b4c31736ede134c730c12556029e2470fde967f424")
elseif (_dawn_system STREQUAL "android" AND _dawn_arch STREQUAL "aarch64")
set(AURORA_DAWN_PACKAGE_URL_HASH
"SHA256=0e63e8cbf53551f703f582d1306f4257c0380353f66b53369d96952ce6d9f934")
endif ()
endif ()
endif ()
message(STATUS "aurora: Fetching prebuilt Dawn package from ${AURORA_DAWN_PACKAGE_URL}")
@@ -173,7 +198,7 @@ elseif (_aurora_dawn_provider STREQUAL "package")
FetchContent_Declare(dawn_prebuilt
URL "${AURORA_DAWN_PACKAGE_URL}"
${_dawn_prebuilt_hash_argument}
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
)
FetchContent_MakeAvailable(dawn_prebuilt)
+1 -1
View File
@@ -7,7 +7,7 @@
include(FetchContent)
FetchContent_Declare(libusb
URL "https://github.com/libusb/libusb/releases/download/v${AURORA_LIBUSB_VERSION}/libusb-${AURORA_LIBUSB_VERSION}.tar.bz2"
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
)
# Upstream ships no CMakeLists.txt, so this only populates the source tree.
FetchContent_MakeAvailable(libusb)
+1 -1
View File
@@ -120,7 +120,7 @@ elseif (_aurora_nod_provider STREQUAL "package")
include(FetchContent)
FetchContent_Declare(nod_prebuilt
URL "${AURORA_NOD_PACKAGE_URL}"
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(nod_prebuilt)
+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 ()
+16 -4
View File
@@ -41,7 +41,10 @@ if (_aurora_sdl3_provider STREQUAL "auto")
set(_aurora_sdl3_provider "package")
else ()
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
find_package(SDL3 QUIET)
# Aurora uses APIs from the SDL version pinned by AURORA_SDL3_VERSION.
# Do not silently select an older system package and fail later while
# compiling its headers.
find_package(SDL3 ${AURORA_SDL3_VERSION} QUIET)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
if (SDL3_FOUND)
set(_aurora_sdl3_provider "system")
@@ -58,7 +61,7 @@ if (_aurora_sdl3_provider STREQUAL "system")
message(STATUS "aurora: Using system SDL3 (provider=system)")
if (NOT SDL3_FOUND)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
find_package(SDL3 REQUIRED)
find_package(SDL3 ${AURORA_SDL3_VERSION} REQUIRED)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
endif ()
_aurora_sdl3_select_target()
@@ -92,7 +95,7 @@ elseif (_aurora_sdl3_provider STREQUAL "package")
include(FetchContent)
FetchContent_Declare(sdl3_prebuilt
URL "${AURORA_SDL3_PACKAGE_URL}"
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
)
FetchContent_MakeAvailable(sdl3_prebuilt)
@@ -135,9 +138,18 @@ elseif (_aurora_sdl3_provider STREQUAL "vendor")
endif ()
include(FetchContent)
# Source fixes for the vendored SDL (see AuroraSDL3Patches.cmake). A freshly
# downloaded tarball is patched by the PATCH_COMMAND; a tree handed in through
# FETCHCONTENT_SOURCE_DIR_SDL skips the download steps, so patch it here.
set(_aurora_sdl3_patches "${CMAKE_CURRENT_LIST_DIR}/AuroraSDL3Patches.cmake")
include("${_aurora_sdl3_patches}")
if (DEFINED FETCHCONTENT_SOURCE_DIR_SDL AND EXISTS "${FETCHCONTENT_SOURCE_DIR_SDL}")
aurora_sdl3_apply_patches("${FETCHCONTENT_SOURCE_DIR_SDL}")
endif ()
FetchContent_Declare(SDL
URL "https://github.com/libsdl-org/SDL/releases/download/release-${AURORA_SDL3_VERSION}/SDL3-${AURORA_SDL3_VERSION}.tar.gz"
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
PATCH_COMMAND "${CMAKE_COMMAND}" -DSDL_SOURCE_DIR=<SOURCE_DIR> -P "${_aurora_sdl3_patches}"
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(SDL)
+14 -1
View File
@@ -34,6 +34,17 @@ if (AURORA_ENABLE_GX)
target_compile_definitions(aurora_core PUBLIC AURORA_ENABLE_GX WEBGPU_DAWN)
target_sources(aurora_core PRIVATE lib/webgpu/gpu.cpp lib/webgpu/gpu_cache.cpp lib/dawn/BackendBinding.cpp)
target_link_libraries(aurora_core PRIVATE dawn::webgpu_dawn)
if (APPLE AND DAWN_ENABLE_METAL)
find_library(AURORA_METALFX_FRAMEWORK MetalFX)
endif ()
if (APPLE AND DAWN_ENABLE_METAL AND AURORA_METALFX_FRAMEWORK)
target_sources(aurora_core PRIVATE lib/webgpu/metalfx.mm)
set_source_files_properties(lib/webgpu/metalfx.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
target_link_options(aurora_core PUBLIC "LINKER:-weak_framework,MetalFX")
target_link_libraries(aurora_core PRIVATE "-framework IOSurface")
else ()
target_sources(aurora_core PRIVATE lib/webgpu/metalfx_stub.cpp)
endif ()
if (DAWN_ENABLE_VULKAN)
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_VULKAN)
endif ()
@@ -41,7 +52,9 @@ if (AURORA_ENABLE_GX)
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_METAL)
target_sources(aurora_core PRIVATE lib/dawn/MetalBinding.mm)
set_source_files_properties(lib/dawn/MetalBinding.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
target_link_options(aurora_core PUBLIC "LINKER:-weak_framework,Metal")
target_link_options(aurora_core PUBLIC
"LINKER:-weak_framework,Metal"
"LINKER:-U,_OBJC_CLASS_$_MTLLogStateDescriptor")
endif ()
if (DAWN_ENABLE_D3D11)
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_D3D11)
+63 -13
View File
@@ -16,10 +16,60 @@ function(aurora_find_package_global)
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ${_PREV_FIND_PACKAGE_TARGETS_GLOBAL})
endfunction()
# A macOS build for another architecture must not discover Homebrew packages
# built for its physical host. CMAKE_CROSSCOMPILING is not sufficient here:
# Apple Clang can target another architecture through CMAKE_OSX_ARCHITECTURES
# without CMake considering the configure a cross-build.
set(_AURORA_EXCLUDE_HOST_HOMEBREW FALSE)
if (APPLE AND CMAKE_OSX_ARCHITECTURES)
list(LENGTH CMAKE_OSX_ARCHITECTURES _AURORA_OSX_ARCH_COUNT)
if (_AURORA_OSX_ARCH_COUNT EQUAL 1)
list(GET CMAKE_OSX_ARCHITECTURES 0 _AURORA_TARGET_ARCH)
string(TOLOWER "${_AURORA_TARGET_ARCH}" _AURORA_TARGET_ARCH)
# hw.optional.arm64 identifies Apple Silicon even when CMake itself runs
# through Rosetta, where CMAKE_HOST_SYSTEM_PROCESSOR reports x86_64.
execute_process(
COMMAND /usr/sbin/sysctl -n hw.optional.arm64
RESULT_VARIABLE _AURORA_ARM64_PROBE_RESULT
OUTPUT_VARIABLE _AURORA_ARM64_PROBE
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE)
if (_AURORA_ARM64_PROBE_RESULT EQUAL 0 AND _AURORA_ARM64_PROBE STREQUAL "1")
set(_AURORA_HOST_ARCH arm64)
else ()
execute_process(
COMMAND /usr/bin/uname -m
OUTPUT_VARIABLE _AURORA_HOST_ARCH
OUTPUT_STRIP_TRAILING_WHITESPACE)
string(TOLOWER "${_AURORA_HOST_ARCH}" _AURORA_HOST_ARCH)
endif ()
if (_AURORA_TARGET_ARCH STREQUAL "x86_64" AND _AURORA_HOST_ARCH MATCHES "^(arm64|aarch64)$")
list(APPEND CMAKE_IGNORE_PREFIX_PATH "/opt/homebrew")
set(_AURORA_EXCLUDE_HOST_HOMEBREW TRUE)
elseif (_AURORA_TARGET_ARCH MATCHES "^(arm64|aarch64)$" AND _AURORA_HOST_ARCH MATCHES "^(x86_64|amd64)$")
list(APPEND CMAKE_IGNORE_PREFIX_PATH "/usr/local")
set(_AURORA_EXCLUDE_HOST_HOMEBREW TRUE)
endif ()
endif ()
endif ()
if (_AURORA_EXCLUDE_HOST_HOMEBREW)
list(REMOVE_DUPLICATES CMAKE_IGNORE_PREFIX_PATH)
message(STATUS "aurora: cross-architecture macOS build; ignoring host Homebrew prefixes")
endif ()
if (AURORA_ENABLE_GX)
include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/AuroraDawnProvider.cmake)
endif ()
# SDL's pkg-config probe can bypass CMake's prefix exclusion. macOS does not
# need libusb for the supported SDL input paths, so keep that host-only library
# out of a cross-architecture configure.
if (_AURORA_EXCLUDE_HOST_HOMEBREW)
set(SDL_HIDAPI_LIBUSB OFF CACHE BOOL "" FORCE)
set(SDL_HIDAPI_LIBUSB_SHARED OFF CACHE BOOL "" FORCE)
endif ()
# Abseil is needed for core libraries. It normally comes via Dawn's vendor build.
# Otherwise prefer a system package and only fetch it as a last resort.
if (NOT TARGET absl::flat_hash_map OR NOT TARGET absl::btree)
@@ -38,7 +88,7 @@ else ()
set(ABSL_PROPAGATE_CXX_STD ON)
FetchContent_Declare(abseil-cpp
URL https://github.com/abseil/abseil-cpp/archive/refs/tags/20240722.0.tar.gz
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(abseil-cpp)
@@ -52,10 +102,10 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/AuroraSDL3Provider.cmake)
if (NOT TARGET xxhash)
message(STATUS "aurora: Fetching xxhash")
FetchContent_Declare(xxhash
URL https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.3.tar.gz
URL_HASH SHA256=aae608dfe8213dfd05d909a57718ef82f30722c392344583d3f39050c7f29a80
SOURCE_SUBDIR cmake_unofficial
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
URL https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.4.tar.gz
URL_HASH SHA256=5738270935e7c3d38a79b3adf7c9692566ce7895a25f67de43ad52ab504acd32
SOURCE_SUBDIR build/cmake
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL
)
set(XXHASH_BUILD_XXHSUM OFF CACHE INTERNAL "Build the xxhsum binary")
@@ -78,7 +128,7 @@ if (NOT TARGET fmt)
FetchContent_Declare(fmt
URL https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz
URL_HASH SHA256=ac366b7b4c2e9f0dde63a59b3feb5ee59b67974b14ee5dc9ea8ad78aa2c1ee1e
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(fmt)
@@ -116,7 +166,7 @@ if (AURORA_ENABLE_GX)
ZLIB
URL https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz
URL_HASH SHA256=bb329a0a2cd0274d05519d61c667c062e06990d72e125ee2dfa8de64f0119d16
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(ZLIB)
@@ -152,7 +202,7 @@ if (AURORA_ENABLE_GX)
PNG
URL https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.58.tar.gz
URL_HASH SHA256=A9D4DF463D36A6E5F9C29BD6F4967312D17E996C1854F3511F833924EB1993CF
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(PNG)
@@ -176,7 +226,7 @@ if (AURORA_ENABLE_GX)
FetchContent_Declare(Freetype
URL https://files.twilitrealm.dev/freetype-2.14.3.tar.gz
URL_HASH SHA256=e61b31ab26358b946e767ed7eb7f4bb2e507da1cfefeb7a8861ace7fd5c899a1
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(Freetype)
@@ -190,7 +240,7 @@ if (AURORA_ENABLE_GX)
FetchContent_Declare(imgui
URL https://github.com/ocornut/imgui/archive/refs/tags/v1.91.9b-docking.tar.gz
URL_HASH SHA256=466fdef9b18de15f0bb6e288e3d00ffa3d82200ec458ce5e4f724a161d9528a5
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(imgui)
@@ -235,7 +285,7 @@ if (AURORA_ENABLE_GX)
FetchContent_Declare(sqlite3
URL https://sqlite.org/2026/sqlite-amalgamation-3510300.zip
URL_HASH SHA256=acb1e6f5d832484bf6d32b681e858c38add8b2acdfd42ac5df24b8afb46552b4
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(sqlite3)
@@ -278,7 +328,7 @@ if (AURORA_ENABLE_GX)
FetchContent_Declare(zstd
URL https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz
URL_HASH SHA256=eb33e51f49a15e023950cd7825ca74a4a2b43db8354825ac24fc1b7ee09e6fa3
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
SOURCE_SUBDIR build/cmake
EXCLUDE_FROM_ALL
)
@@ -299,7 +349,7 @@ if (NOT TARGET TracyClient)
tracy
URL https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz
URL_HASH SHA256=24d342b5127d7f659dc3cf94f24347b348cc736f25b17a691fd7f69541937658
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(tracy)
+23
View File
@@ -127,12 +127,20 @@ typedef struct {
const char* pipelineCachePath;
} AuroraConfig;
typedef enum {
AURORA_INITIALIZATION_SUCCESS = 0,
AURORA_INITIALIZATION_GRAPHICS_UNAVAILABLE = 1,
} AuroraInitializationStatus;
typedef struct {
AuroraBackend backend;
const char* userPath;
const char* cachePath;
SDL_Window* window;
AuroraWindowSize windowSize;
AuroraInitializationStatus initializationStatus;
// On failure, owned by SDL on the calling thread. Copy before another SDL call.
const char* initializationError;
} AuroraInfo;
AuroraInfo aurora_initialize(int argc, char* argv[], const AuroraConfig* config);
@@ -162,6 +170,21 @@ void aurora_set_background_input(bool value);
void aurora_set_display_mode(AuroraDisplayMode mode);
AuroraDisplayMode aurora_get_display_mode();
typedef enum {
AURORA_METALFX_DISABLED,
AURORA_METALFX_UNSUPPORTED,
AURORA_METALFX_NOT_UPSCALING,
AURORA_METALFX_ACTIVE,
AURORA_METALFX_ERROR,
} AuroraMetalFXStatus;
// Changes are consumed at the next sealed frame boundary. MetalFX only applies
// when both source dimensions are smaller than the aspect-fitted output.
void aurora_set_metalfx_spatial(bool enabled);
bool aurora_get_metalfx_spatial();
bool aurora_is_metalfx_spatial_supported();
AuroraMetalFXStatus aurora_get_metalfx_status();
AuroraBackend aurora_get_backend();
const AuroraBackend* aurora_get_available_backends(size_t* count);
@@ -88,6 +88,7 @@ typedef enum _AuroraViewportPolicy {
AURORA_VIEWPORT_FIT = 0, // Preserve logical aspect in the content framebuffer
AURORA_VIEWPORT_STRETCH = 1, // Match content framebuffer aspect to the native surface
AURORA_VIEWPORT_NATIVE = 2, // Use active framebuffer pixels directly
AURORA_VIEWPORT_16_9 = 3, // Fixed 16:9 content with bars on other surfaces
} AuroraViewportPolicy;
/**
+16
View File
@@ -171,6 +171,22 @@ typedef struct PADButtonMapping {
PADButton padButton;
} 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 {
PADSignedNativeAxis nativeAxis;
s32 nativeButton;
+199 -26
View File
@@ -7,6 +7,7 @@
#include "gx/shader_info.hpp"
#include "imgui.hpp"
#include "webgpu/gpu.hpp"
#include "webgpu/metalfx.hpp"
#include <webgpu/webgpu_cpp.h>
#endif
@@ -60,6 +61,9 @@ std::atomic<AuroraFrameWorkerWaitCallback> g_frameWorkerWaitCallback{nullptr};
// deadlines derived from it, so the presenter cannot drift. Zero means present when ready.
std::atomic<uint64_t> g_presentScheduleBaseNanos{0};
std::atomic<uint64_t> g_presentScheduleIntervalNanos{0};
std::atomic<bool> g_metalfxRequested{false};
std::atomic<bool> g_metalfxSupported{false};
std::atomic<AuroraMetalFXStatus> g_metalfxStatus{AURORA_METALFX_DISABLED};
namespace {
Module Log("aurora");
@@ -225,7 +229,7 @@ enum class ImGuiFramePolicy {
bool begin_frame_impl(bool pumpEvents, ImGuiFramePolicy imguiPolicy = ImGuiFramePolicy::Immediate,
bool* imguiNewFrameOwed = nullptr) noexcept;
bool begin_frame_render_state_impl(ImGuiFramePolicy imguiPolicy, bool* imguiNewFrameOwed) noexcept;
void end_frame_impl(bool pumpEvents, bool drainFifo) noexcept;
void end_frame_impl(bool pumpEvents, bool drainFifo);
// The two publication points of a frame-worker cycle, cleared together under `mutex`. Sealed:
// producer-shared renderer state is free again. Done: slots encoded, presented, ImGui restarted.
@@ -255,6 +259,14 @@ FrameWorkerState g_frameWorker;
bool frame_worker_requested() noexcept {
#ifdef AURORA_ENABLE_GX
static const bool enabled = [] {
#if defined(__APPLE__)
// ImGui's SDL backend may raise an SDL window from ImGui::NewFrame(). On
// macOS that reaches AppKit, whose window operations are main-thread-only;
// doing it on the frame worker terminates the process with EXC_BREAKPOINT.
// Keep all SDL/ImGui work on the calling thread until the worker no longer
// owns frame preparation on Apple platforms.
return false;
#endif
#if defined(_WIN32)
// RenderDoc's D3D12 layer is injected before Aurora starts and needs device and command
// ownership on one thread, so keep frame submission synchronous there.
@@ -681,15 +693,23 @@ AuroraInfo initialize(int argc, char* argv[], const AuroraConfig& config) noexce
const AuroraBackend requestedBackend = config.desiredBackend;
AuroraBackend selectedBackend = requestedBackend;
bool windowCreated = false;
std::string firstGraphicsError;
const auto rememberGraphicsError = [&] {
if (firstGraphicsError.empty() && SDL_GetError()[0] != '\0') {
firstGraphicsError = SDL_GetError();
}
};
if (selectedBackend != BACKEND_AUTO) {
Log.info("Requested graphics backend: {}", backend_name(selectedBackend));
if (window::create_window(selectedBackend)) {
if (webgpu::initialize(selectedBackend)) {
windowCreated = true;
} else {
rememberGraphicsError();
window::destroy_window();
}
} else {
rememberGraphicsError();
Log.error("Failed to create a window for backend {}: {}", backend_name(selectedBackend),
SDL_GetError());
}
@@ -706,18 +726,28 @@ AuroraInfo initialize(int argc, char* argv[], const AuroraConfig& config) noexce
for (const auto backendType : PreferredBackendOrder) {
selectedBackend = backendType;
if (!window::create_window(selectedBackend)) {
rememberGraphicsError();
continue;
}
if (webgpu::initialize(selectedBackend)) {
windowCreated = true;
break;
} else {
rememberGraphicsError();
window::destroy_window();
}
}
}
ASSERT(windowCreated, "Error creating window: {}", SDL_GetError());
if (!windowCreated) {
if (firstGraphicsError.empty()) firstGraphicsError = "No supported graphics backend is available";
SDL_SetError("%s", firstGraphicsError.c_str());
Log.error("Graphics initialization failed: {}", firstGraphicsError);
return {
.initializationStatus = AURORA_INITIALIZATION_GRAPHICS_UNAVAILABLE,
.initializationError = SDL_GetError(),
};
}
if (requestedBackend != BACKEND_AUTO && selectedBackend != requestedBackend) {
Log.error("Graphics backend fallback in effect: video.graphics_api requested {}, "
"running on {}",
@@ -739,6 +769,9 @@ AuroraInfo initialize(int argc, char* argv[], const AuroraConfig& config) noexce
#ifdef AURORA_ENABLE_GX
gfx::initialize();
g_metalfxSupported.store(webgpu::metalfx::supported(g_device, webgpu::g_backendType));
g_metalfxStatus.store(AURORA_METALFX_DISABLED);
imgui::create_context();
#endif
const auto size = window::get_window_size();
@@ -1161,12 +1194,114 @@ void stop_presenter() noexcept {
g_presenterStarted.store(false, std::memory_order_release);
}
struct MetalFXSlot {
webgpu::metalfx::Size size{};
std::unique_ptr<webgpu::metalfx::SpatialScaler> scaler;
wgpu::BindGroup bindGroup;
};
std::array<MetalFXSlot, gx::MaxInterpolatedFrames + 1> g_metalfxSlots;
size_t g_metalfxNextSlot = 0;
webgpu::metalfx::SpatialScaler* g_metalfxPendingOutput = nullptr;
bool g_metalfxFailed = false;
void metalfx_failed(const std::string& reason) {
Log.warn("MetalFX spatial upscaling disabled: {}; using normal presentation", reason);
g_metalfxFailed = true;
g_metalfxStatus.store(AURORA_METALFX_ERROR);
g_metalfxPendingOutput = nullptr;
g_metalfxSlots = {};
}
wgpu::BindGroup upscale_presentation(wgpu::CommandEncoder& encoder,
const webgpu::PresentSource& source,
const webgpu::Viewport& viewport, bool enabled) {
if (!enabled) {
g_metalfxSlots = {};
g_metalfxFailed = false;
g_metalfxStatus.store(AURORA_METALFX_DISABLED);
return {};
}
if (!g_metalfxSupported.load()) {
g_metalfxStatus.store(AURORA_METALFX_UNSUPPORTED);
return {};
}
if (g_metalfxFailed) return {};
const webgpu::metalfx::Size size{
source.size.width, source.size.height,
static_cast<uint32_t>(viewport.width), static_cast<uint32_t>(viewport.height),
webgpu::g_graphicsConfig.surfaceConfiguration.format,
};
// The existing copy path samples perceptual values from unorm game images.
// Do not introduce implicit sRGB decoding or downscaling into MetalFX.
if (!size.inputWidth || !size.inputHeight || size.inputWidth >= size.outputWidth ||
size.inputHeight >= size.outputHeight ||
(source.format != wgpu::TextureFormat::RGBA8Unorm && source.format != wgpu::TextureFormat::BGRA8Unorm)) {
g_metalfxStatus.store(AURORA_METALFX_NOT_UPSCALING);
return {};
}
auto& slot = g_metalfxSlots[g_metalfxNextSlot++ % g_metalfxSlots.size()];
if (!slot.scaler || !(slot.size == size)) {
slot = {};
std::string error;
slot.scaler = webgpu::metalfx::create(g_instance, g_device, size, error);
if (!slot.scaler) {
if (error.empty()) g_metalfxStatus.store(AURORA_METALFX_NOT_UPSCALING);
else metalfx_failed(error);
return {};
}
slot.size = size;
wgpu::SamplerDescriptor samplerDescriptor{};
samplerDescriptor.magFilter = wgpu::FilterMode::Linear;
samplerDescriptor.minFilter = wgpu::FilterMode::Linear;
slot.bindGroup = webgpu::create_copy_bind_group(slot.scaler->output_view(),
g_device.CreateSampler(&samplerDescriptor));
Log.info("MetalFX spatial slot: {}x{} -> {}x{}", size.inputWidth, size.inputHeight,
size.outputWidth, size.outputHeight);
}
if (!slot.scaler->begin_input()) {
metalfx_failed(slot.scaler->error());
return {};
}
const wgpu::RenderPassColorAttachment attachment{
.view = slot.scaler->input_view(),
.loadOp = wgpu::LoadOp::Clear,
.storeOp = wgpu::StoreOp::Store,
};
const wgpu::RenderPassDescriptor descriptor{
.label = "MetalFX input copy",
.colorAttachmentCount = 1,
.colorAttachments = &attachment,
};
auto pass = encoder.BeginRenderPass(&descriptor);
pass.SetPipeline(webgpu::g_CopyPipeline);
pass.SetBindGroup(0, source.bindGroup);
pass.SetViewport(0, 0, static_cast<float>(size.inputWidth), static_cast<float>(size.inputHeight), 0, 1);
pass.Draw(3);
pass.End();
// Submit the sealed scene and input copy before crossing to the native queue.
// The replacement encoder composites the upscaled image and ImGui normally.
auto buffer = encoder.Finish();
{
std::lock_guard submitLock(g_queueSubmitMutex);
g_queue.Submit(1, &buffer);
}
encoder = g_device.CreateCommandEncoder();
if (!slot.scaler->upscale()) {
metalfx_failed(slot.scaler->error());
return {};
}
g_metalfxPendingOutput = slot.scaler.get();
g_metalfxStatus.store(AURORA_METALFX_ACTIVE);
return slot.bindGroup;
}
// `presentSource` is latched in the seal prologue: by the time this encodes, the producer's next
// gfx::begin_frame() may already have cleared the display-copy override.
void encode_presentation_snapshot(const wgpu::CommandEncoder& encoder,
const webgpu::PresentSource& presentSource,
const PresentationImage& image,
bool includeImGui) {
wgpu::BindGroup encode_presentation_snapshot(wgpu::CommandEncoder& encoder,
const webgpu::PresentSource& presentSource,
const PresentationImage& image,
bool includeImGui, bool metalfxEnabled,
const wgpu::BindGroup* cachedMetalFXOutput = nullptr) {
ZoneScoped;
auto viewport = webgpu::calculate_present_viewport(
image.texture.size.width, image.texture.size.height, presentSource.size.width,
@@ -1177,6 +1312,13 @@ void encode_presentation_snapshot(const wgpu::CommandEncoder& encoder,
image.texture.size.width, image.texture.size.height, presentAspect);
}
wgpu::BindGroup presentBindGroup = presentSource.bindGroup;
wgpu::BindGroup newMetalFXOutput;
if (cachedMetalFXOutput && *cachedMetalFXOutput) {
presentBindGroup = *cachedMetalFXOutput;
} else if (auto upscaled = upscale_presentation(encoder, presentSource, viewport, metalfxEnabled)) {
presentBindGroup = std::move(upscaled);
newMetalFXOutput = presentBindGroup;
}
{
const std::array attachments{
wgpu::RenderPassColorAttachment{
@@ -1217,6 +1359,7 @@ void encode_presentation_snapshot(const wgpu::CommandEncoder& encoder,
imgui::render(pass);
pass.End();
}
return newMetalFXOutput;
}
#endif
@@ -1225,6 +1368,13 @@ void shutdown() noexcept {
#ifdef AURORA_ENABLE_GX
stop_presenter();
g_presentationImagePools = {};
g_metalfxSlots = {};
g_metalfxPendingOutput = nullptr;
g_metalfxNextSlot = 0;
g_metalfxFailed = false;
g_metalfxRequested.store(false);
g_metalfxSupported.store(false);
g_metalfxStatus.store(AURORA_METALFX_DISABLED);
imgui::shutdown();
gfx::shutdown();
webgpu::shutdown();
@@ -1337,6 +1487,7 @@ struct SealedFrameContext {
uint32_t logicalFrame = 0;
bool interpolationActive = false;
bool replayInterpolatedFrames = false;
bool metalfxEnabled = false;
};
// Phase 1: everything that touches producer-shared renderer state. Needs g_rendererGpuMutex and
@@ -1364,6 +1515,7 @@ void seal_frame_locked(gfx::SealedFrame& sealedFrame, SealedFrameContext& ctx) {
ctx.snapshotWidth = (std::max)(windowSize.native_fb_width, 1u);
ctx.snapshotHeight = (std::max)(windowSize.native_fb_height, 1u);
ctx.logicalFrame = gfx::current_frame();
ctx.metalfxEnabled = g_metalfxRequested.load();
// Latched before webgpu::clear_present_source_override() in the producer's
// next gfx::begin_frame().
ctx.presentSource = webgpu::current_present_source();
@@ -1411,10 +1563,14 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
const wgpu::CommandBufferDescriptor cmdBufDescriptor{
.label = "Presentation slot command buffer",
};
const auto submitEncodedSlot = [&](wgpu::CommandEncoder& target) {
const auto submitEncodedSlot = [&](wgpu::CommandEncoder& target, bool releaseMetalFXOutput = true) {
const auto buffer = target.Finish(&cmdBufDescriptor);
std::lock_guard submitLock(g_queueSubmitMutex);
g_queue.Submit(1, &buffer);
if (releaseMetalFXOutput && g_metalfxPendingOutput) {
if (!g_metalfxPendingOutput->end_output()) metalfx_failed(g_metalfxPendingOutput->error());
g_metalfxPendingOutput = nullptr;
}
};
if (ctx.replayInterpolatedFrames) {
@@ -1423,7 +1579,7 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
gfx::render(sealedFrame, encoder, static_cast<int32_t>(interpolatedFrame), false);
auto image =
acquire_presentation_image(interpolatedFrame, ctx.snapshotWidth, ctx.snapshotHeight);
encode_presentation_snapshot(encoder, ctx.presentSource, *image, true);
encode_presentation_snapshot(encoder, ctx.presentSource, *image, true, ctx.metalfxEnabled);
presentationJobs.push_back({
.image = std::move(image),
.logicalFrame = ctx.logicalFrame,
@@ -1441,12 +1597,18 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
// The copy targets now hold this frame's resolves, so queue their readbacks on the same encoder;
// completion is harvested in gfx::after_submit, never waited on here.
gfx::efb_ram::encode_async_downloads(encoder);
wgpu::BindGroup duplicatedMetalFXOutput;
if (!ctx.replayInterpolatedFrames) {
for (uint32_t interpolatedFrame = 0; interpolatedFrame < ctx.interpolatedFrameCount;
++interpolatedFrame) {
auto image =
acquire_presentation_image(interpolatedFrame, ctx.snapshotWidth, ctx.snapshotHeight);
encode_presentation_snapshot(encoder, ctx.presentSource, *image, true);
const auto newMetalFXOutput = encode_presentation_snapshot(
encoder, ctx.presentSource, *image, true, ctx.metalfxEnabled,
duplicatedMetalFXOutput ? &duplicatedMetalFXOutput : nullptr);
if (!duplicatedMetalFXOutput && newMetalFXOutput) {
duplicatedMetalFXOutput = newMetalFXOutput;
}
presentationJobs.push_back({
.image = std::move(image),
.logicalFrame = ctx.logicalFrame,
@@ -1454,13 +1616,14 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
.interpolated = true,
.duplicated = true,
});
submitEncodedSlot(encoder);
submitEncodedSlot(encoder, false);
encoder = g_device.CreateCommandEncoder(&encoderDescriptor);
}
}
auto finalImage =
acquire_presentation_image(ctx.interpolatedFrameCount, ctx.snapshotWidth, ctx.snapshotHeight);
encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true);
encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true, ctx.metalfxEnabled,
duplicatedMetalFXOutput ? &duplicatedMetalFXOutput : nullptr);
auto pendingFrameCapture = encode_frame_capture(encoder, ctx.presentSource);
presentationJobs.push_back({
.image = std::move(finalImage),
@@ -1516,6 +1679,15 @@ std::vector<PresentationJob> encode_sealed_frame(gfx::SealedFrame& sealedFrame,
// Phase 3: hand the encoded group to whoever owns presentation.
void publish_presentations(std::vector<PresentationJob>&& presentationJobs, bool interpolationActive) {
#if defined(__APPLE__)
(void)interpolationActive;
// Presenting reaches SDL/AppKit, whose window operations must stay on the
// main thread. Interpolation normally starts the presenter worker, so keep
// its jobs synchronous on Apple platforms.
for (const auto& job : presentationJobs) {
present_presentation_job(job);
}
#else
// Keep presentation on the presenter whenever the async frame worker runs, even with
// interpolation off, so every mode shares one surface/resize path. RenderDoc keeps the sync path.
if (frame_worker_requested() || interpolationActive ||
@@ -1526,6 +1698,7 @@ void publish_presentations(std::vector<PresentationJob>&& presentationJobs, bool
present_presentation_job(job);
}
}
#endif
}
void record_frame_telemetry() {
@@ -1643,7 +1816,7 @@ bool run_frame_worker_cycle(gfx::SealedFrame& sealedFrame) noexcept {
// Synchronous frame submission: seal, encode and present inline on the calling thread. Used when
// the frame worker is disabled (RenderDoc captures) and on the boot path.
void end_frame_impl(bool pumpEvents, bool drainFifo) noexcept {
void end_frame_impl(bool pumpEvents, bool drainFifo) {
ZoneScoped;
#ifdef AURORA_ENABLE_GX
webgpu::fail_if_device_lost();
@@ -1653,11 +1826,9 @@ void end_frame_impl(bool pumpEvents, bool drainFifo) noexcept {
gfx::SealedFrame sealedFrame;
SealedFrameContext ctx;
std::vector<PresentationJob> presentationJobs;
if (drainFifo) gx::fifo::drain();
{
std::lock_guard gpuLock(g_rendererGpuMutex);
if (drainFifo) {
gx::fifo::drain();
}
seal_frame_locked(sealedFrame, ctx);
presentationJobs = encode_sealed_frame(sealedFrame, ctx);
}
@@ -1734,7 +1905,7 @@ bool begin_frame() noexcept {
return prepared;
}
void end_frame() noexcept {
void end_frame() {
#ifdef AURORA_ENABLE_GX
webgpu::fail_if_device_lost();
#endif
@@ -1750,10 +1921,7 @@ void end_frame() noexcept {
// Seal all current GX work on the CPU while the renderer is known ready.
// Later FIFO writes belong exclusively to the next frame.
{
std::lock_guard gpuLock(g_rendererGpuMutex);
gx::fifo::drain();
}
gx::fifo::drain();
{
std::lock_guard lock(g_frameWorker.mutex);
g_frameWorker.framePrepared = false;
@@ -1779,6 +1947,10 @@ bool wait_for_frame_worker_for(std::chrono::microseconds timeout) noexcept {
return wait_for_frame_worker_private_for(FrameWorkerPhase::Done, timeout);
}
std::recursive_mutex& renderer_gpu_mutex() noexcept { return g_rendererGpuMutex; }
void submit_staging_commands(const wgpu::CommandBuffer& commands) {
std::lock_guard submitLock(g_queueSubmitMutex);
webgpu::g_queue.Submit(1, &commands);
}
} // namespace aurora
// C API bindings
@@ -1841,10 +2013,6 @@ bool aurora_flush_efb_copies_to_ram() {
if (!aurora::gfx::efb_ram::has_pending()) {
return true;
}
if (!aurora::gfx::efb_ram::prepare_downloads()) {
return false;
}
// This finalizes the frame still being recorded, on the producer thread, so join the whole cycle
// first: the encode phase owns the previous passes, EFB targets and image pool.
aurora::wait_for_frame_worker();
@@ -1852,6 +2020,7 @@ bool aurora_flush_efb_copies_to_ram() {
// suffix cannot safely be replayed against the same mutable EFB resources.
aurora::gx::mark_frame_interpolation_replay_unsafe();
aurora::gx::fifo::drain();
if (!aurora::gfx::efb_ram::prepare_downloads()) return false;
const wgpu::CommandEncoderDescriptor encoderDescriptor{
.label = "GX CPU-visible EFB copy encoder",
};
@@ -1877,8 +2046,7 @@ bool aurora_flush_efb_copies_to_ram() {
}
bool aurora_flush_efb_copy_to_ram(void* dest) {
#ifdef AURORA_ENABLE_GX
if (dest == nullptr || !aurora::gfx::efb_ram::has_pending(dest) ||
!aurora::gfx::efb_ram::prepare_downloads(dest)) {
if (dest == nullptr || !aurora::gfx::efb_ram::has_pending(dest)) {
return false;
}
@@ -1889,6 +2057,7 @@ bool aurora_flush_efb_copy_to_ram(void* dest) {
// image instead of replaying this split frame.
aurora::gx::mark_frame_interpolation_replay_unsafe();
aurora::gx::fifo::drain();
if (!aurora::gfx::efb_ram::prepare_downloads(dest)) return false;
const wgpu::CommandEncoderDescriptor encoderDescriptor{
.label = "GX demanded EFB copy encoder",
};
@@ -1930,3 +2099,7 @@ void aurora_set_background_input(bool value) {
}
void aurora_set_display_mode(AuroraDisplayMode mode) { aurora::window::set_display_mode(mode); }
AuroraDisplayMode aurora_get_display_mode() { return aurora::window::get_display_mode(); }
void aurora_set_metalfx_spatial(bool enabled) { aurora::g_metalfxRequested.store(enabled); }
bool aurora_get_metalfx_spatial() { return aurora::g_metalfxRequested.load(); }
bool aurora_is_metalfx_spatial_supported() { return aurora::g_metalfxSupported.load(); }
AuroraMetalFXStatus aurora_get_metalfx_status() { return aurora::g_metalfxStatus.load(); }
+34 -3
View File
@@ -2,12 +2,43 @@
#import <Foundation/Foundation.h>
#include <SDL3/SDL_metal.h>
#include <SDL3/SDL_properties.h>
#include <SDL3/SDL_video.h>
namespace aurora::webgpu::utils {
namespace {
constexpr const char* MetalViewProperty = "aurora.window.metal_view";
void SDLCALL DestroyMetalView(void*, void* value) {
SDL_Metal_DestroyView(value);
}
} // namespace
std::shared_ptr<wgpu::ChainedStruct> SetupWindowAndGetSurfaceDescriptorCocoa(SDL_Window* window) {
SDL_MetalView view = SDL_Metal_CreateView(window);
std::shared_ptr<wgpu::SurfaceSourceMetalLayer> desc = std::make_shared<wgpu::SurfaceSourceMetalLayer>();
const auto properties = SDL_GetWindowProperties(window);
if (!properties) {
return nullptr;
}
auto view = SDL_GetPointerProperty(properties, MetalViewProperty, nullptr);
if (!view) {
view = SDL_Metal_CreateView(window);
if (!view) {
return nullptr;
}
// Own one view per window, not per WebGPU surface. Surface recovery must
// preserve the UIKit root and its controls (and the Cocoa Metal subview).
// SDL cleans window properties before destroying its native window.
// The cleanup callback also runs if setting the property fails.
if (!SDL_SetPointerPropertyWithCleanup(properties, MetalViewProperty, view, DestroyMetalView, nullptr)) {
return nullptr;
}
}
auto desc = std::make_shared<wgpu::SurfaceSourceMetalLayer>();
desc->layer = SDL_Metal_GetLayer(view);
return std::move(desc);
if (!desc->layer) {
SDL_ClearProperty(properties, MetalViewProperty);
return nullptr;
}
return desc;
}
} // namespace aurora::webgpu::utils
+1
View File
@@ -46,6 +46,7 @@ void AuroraSetViewportPolicy(AuroraViewportPolicy policy) {
}
g_gxState.viewportPolicy = policy;
aurora::window::set_frame_buffer_aspect_fit(policy == AURORA_VIEWPORT_FIT);
aurora::window::set_force_aspect_16_9(policy == AURORA_VIEWPORT_16_9);
aurora::window::set_present_surface_fill(policy == AURORA_VIEWPORT_STRETCH);
if (changed) {
// Reapply the guest viewport and scissor after a resize.
+160 -37
View File
@@ -4,11 +4,12 @@
#include <dolphin/pad.h>
#include <dolphin/si.h>
#include <SDL3/SDL_mouse.h>
#include <SDL3/SDL_joystick.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <sys/stat.h>
#include <ranges>
namespace {
constexpr int32_t k_mappingsFileVersion = 3;
@@ -193,6 +194,32 @@ std::array<PADButtonMapping, PAD_BUTTON_COUNT> g_defaultButtonsJoyPair{{
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT},
}};
// Wii U Pro Controllers through SDL's HIDAPI Wii driver. No SDL_GamepadType
// singles them out, so they are picked by the name the driver gives them (see
// __PADSetDefaultMapping). Wii Remotes, with or without a Nunchuk or Classic
// Controller, are read by the game through KPAD instead and never get a
// GameCube mapping (the runtime hides those ports from PADRead).
// Nintendo's labelled layout: A on the right accelerates, B at the bottom
// brakes. SDL's Wii driver reports ZL/ZR as the LEFT_TRIGGER/RIGHT_TRIGGER
// axes, never as shoulder buttons, so they are left unbound here and picked up
// by aurora's default axis mapping (g_defaultAxes) the same way every
// analog-trigger pad's L/R is.
std::array<PADButtonMapping, PAD_BUTTON_COUNT> g_defaultButtonsWiiUPro{{
{SDL_GAMEPAD_BUTTON_EAST, PAD_BUTTON_A},
{SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_B},
{SDL_GAMEPAD_BUTTON_NORTH, PAD_BUTTON_X},
{SDL_GAMEPAD_BUTTON_WEST, PAD_BUTTON_Y},
{SDL_GAMEPAD_BUTTON_START, PAD_BUTTON_START},
{SDL_GAMEPAD_BUTTON_BACK, PAD_TRIGGER_Z},
{PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_L},
{PAD_NATIVE_BUTTON_INVALID, PAD_TRIGGER_R},
{SDL_GAMEPAD_BUTTON_DPAD_UP, PAD_BUTTON_UP},
{SDL_GAMEPAD_BUTTON_DPAD_DOWN, PAD_BUTTON_DOWN},
{SDL_GAMEPAD_BUTTON_DPAD_LEFT, PAD_BUTTON_LEFT},
{SDL_GAMEPAD_BUTTON_DPAD_RIGHT, PAD_BUTTON_RIGHT},
}};
std::array<PADKeyButtonBinding, PAD_BUTTON_COUNT> g_defaultKeys{{
{PAD_KEY_INVALID, PAD_BUTTON_A},
{PAD_KEY_INVALID, PAD_BUTTON_B},
@@ -292,6 +319,18 @@ std::array<bool, PAD_CHANMAX> g_suppressLeftTrigger{};
std::array<bool, PAD_CHANMAX> g_suppressRightTrigger{};
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) {
const int32_t buttonNum = -(scancode + 1);
if (buttonNum < 1 || buttonNum > 5) {
@@ -315,7 +354,7 @@ BOOL PADInit() {
}
g_initialized = true;
std::ranges::for_each(g_keyboardBindings, [](auto& state) {
std::for_each(g_keyboardBindings.begin(), g_keyboardBindings.end(), [](auto& state) {
state.m_buttonMapping = g_defaultKeys;
state.m_axisMapping = g_defaultKeyAxis;
});
@@ -360,19 +399,20 @@ const char* PADGetNameForControllerIndex(const u32 idx) {
}
void PADSetPortForIndex(const u32 idx, const u32 port) {
if (port >= PAD_MAX_CONTROLLERS) return;
const auto* ctrl = __PADGetControllerForIndex(idx);
if (ctrl == nullptr) {
return;
}
const int32_t oldPort = SDL_GetGamepadPlayerIndex(ctrl->m_controller);
const int32_t oldPort = aurora::input::player_index(ctrl->m_index);
if (const auto* dest = aurora::input::get_controller_for_player(port); dest != nullptr && dest != ctrl) {
SDL_SetGamepadPlayerIndex(dest->m_controller, -1);
aurora::input::set_player_index(dest->m_index, -1);
}
if (oldPort >= 0 && oldPort != port) {
aurora::input::persist_controller_for_player(oldPort, nullptr);
}
SDL_SetGamepadPlayerIndex(ctrl->m_controller, static_cast<Sint32>(port));
aurora::input::set_player_index(ctrl->m_index, static_cast<Sint32>(port));
aurora::input::persist_controller_for_player(port, ctrl);
}
@@ -398,7 +438,7 @@ void PADClearPort(const u32 port) {
if (ctrl == nullptr) {
return;
}
SDL_SetGamepadPlayerIndex(ctrl->m_controller, -1);
aurora::input::set_player_index(ctrl->m_index, -1);
}
// Secondary bindings live only in memory; the runtime re-applies them from its
@@ -409,8 +449,26 @@ static void reset_alt_button_mapping(aurora::input::GameController* controller)
}
}
// SDL's hidapi Wii driver names the pad "Nintendo Wii U Pro Controller"; the
// other names it produces are Wii Remotes, which the game reads through KPAD
// and which therefore never take a GameCube mapping.
static bool wii_default_mapping(const aurora::input::GameController* controller,
std::array<PADButtonMapping, PAD_BUTTON_COUNT>& out) {
const char* name = SDL_GetGamepadName(controller->m_controller);
if (name == nullptr || SDL_strstr(name, "Wii U Pro Controller") == nullptr) {
return false;
}
out = g_defaultButtonsWiiUPro;
return true;
}
// Picks the default button table for a controller by name (Wii pads) or SDL gamepad type.
void __PADSetDefaultMapping(aurora::input::GameController* controller) /* NOLINT(*-reserved-identifier) */
{
if (wii_default_mapping(controller, controller->m_buttonMapping)) {
reset_alt_button_mapping(controller);
return;
}
switch (SDL_GetGamepadType(controller->m_controller)) {
case SDL_GAMEPAD_TYPE_XBOX360:
controller->m_buttonMapping = g_defaultButtonsXBox360;
@@ -589,8 +647,8 @@ static void EnsureMappingLoaded(aurora::input::GameController* controller) {
static Sint16 _get_axis_value(const aurora::input::GameController* controller, // NOLINT(*-reserved-identifier)
PADAxis axis) {
const auto iter =
std::ranges::find_if(controller->m_axisMapping, [axis](const auto& pair) { return pair.padAxis == axis; });
const auto iter = std::find_if(controller->m_axisMapping.begin(), controller->m_axisMapping.end(),
[axis](const auto& pair) { return pair.padAxis == axis; });
if (iter == controller->m_axisMapping.end()) {
return 0;
}
@@ -679,10 +737,10 @@ u32 PADRead(PADStatus* status) {
}
status[i].err = PAD_ERR_NONE;
if (g_keyboardBindings[i].m_mappingsSet) {
std::ranges::for_each(
g_keyboardBindings[i].m_buttonMapping, [&kbState, &i, &status](const PADKeyButtonBinding& mapping) {
if (mapping.scancode > PAD_KEY_INVALID && kbState[mapping.scancode]) {
if (g_keyboardBindings[i].m_mappingsSet && SDL_GetKeyboardFocus() != nullptr) {
std::for_each(g_keyboardBindings[i].m_buttonMapping.begin(), g_keyboardBindings[i].m_buttonMapping.end(),
[&kbState, &numKeys, &i, &status](const PADKeyButtonBinding& mapping) {
if (mapping.scancode > PAD_KEY_INVALID && mapping.scancode < numKeys && kbState[mapping.scancode]) {
status[i].button |= mapping.padButton;
} else if (is_mouse_scancode(mapping.scancode) && is_mouse_button_pressed(mapping.scancode)) {
status[i].button |= mapping.padButton;
@@ -743,13 +801,54 @@ u32 PADRead(PADStatus* status) {
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);
// Wii U Pro Controller raw D-pad fallback. SDL's HIDAPI Wii driver posts
// the D-pad as joystick buttons 11-14 (the SDL_GAMEPAD_BUTTON_DPAD_*
// values) and never as a hat, but the mapping SDL generates for HIDAPI
// pads binds the D-pad to hat 0, so SDL_GetGamepadButton(DPAD_*) stays
// false. Keep this restricted to the Wii driver's pad so raw button
// indices don't interfere with other controller types.
const char* name = SDL_GetGamepadName(controller->m_controller);
const bool isWiiUPro = name != nullptr && SDL_strstr(name, "Wii U Pro Controller") != nullptr;
if (isWiiUPro) {
SDL_Joystick* joystick =
SDL_GetGamepadJoystick(controller->m_controller);
uint32_t raw = 0;
const int buttonCount = SDL_GetNumJoystickButtons(joystick);
for (int b = 0; b < buttonCount && b < 32; ++b) {
if (SDL_GetJoystickButton(joystick, b)) {
raw |= (1u << b);
}
}
// Up = button 11
if (raw & (1u << 11)) {
status[i].button |= PAD_BUTTON_UP;
}
// Down = button 12
if (raw & (1u << 12)) {
status[i].button |= PAD_BUTTON_DOWN;
}
// Left = button 13
if (raw & (1u << 13)) {
status[i].button |= PAD_BUTTON_LEFT;
}
// Right = button 14
if (raw & (1u << 14)) {
status[i].button |= PAD_BUTTON_RIGHT;
}
}
bool leftTriggerSet = false;
bool rightTriggerSet = false;
std::ranges::for_each(controller->m_buttonMapping, [&controller, &i, &status, &leftTriggerSet,
&rightTriggerSet](const auto& mapping) {
if (SDL_GetGamepadButton(controller->m_controller, static_cast<SDL_GamepadButton>(mapping.nativeButton))) {
std::for_each(controller->m_buttonMapping.begin(), controller->m_buttonMapping.end(),
[&controller, &i, &status, &leftTriggerSet, &rightTriggerSet](const auto& mapping) {
if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) {
status[i].button |= mapping.padButton;
}
@@ -761,12 +860,12 @@ u32 PADRead(PADStatus* status) {
}
});
std::ranges::for_each(controller->m_altButtonMapping, [&controller, &i, &status, &leftTriggerSet,
&rightTriggerSet](const auto& mapping) {
std::for_each(controller->m_altButtonMapping.begin(), controller->m_altButtonMapping.end(),
[&controller, &i, &status, &leftTriggerSet, &rightTriggerSet](const auto& mapping) {
if (mapping.nativeButton == PAD_NATIVE_BUTTON_INVALID) {
return;
}
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;
}
@@ -778,6 +877,7 @@ u32 PADRead(PADStatus* status) {
}
});
// TODO: Add serializable mappings for these (probably not necessary)?
static constexpr std::array<std::pair<SDL_GamepadButton, PADExtButton>, PAD_EXT_BUTTON_COUNT> kExtButtonMappings{{
{SDL_GAMEPAD_BUTTON_BACK, PAD_BUTTON_BACK},
@@ -859,6 +959,17 @@ u32 PADRead(PADStatus* status) {
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));
// 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 (!leftTriggerSet && tl > controller->m_deadZones.leftTriggerActivationZone) {
status[i].button |= PAD_TRIGGER_L;
@@ -903,12 +1014,13 @@ void PADControlMotor(const u32 chan, const u32 cmd) {
}
if (controller->m_isGameCube) {
if (cmd == PAD_MOTOR_STOP) {
aurora::input::controller_rumble(instance, 0, 1, 0);
if (cmd == PAD_MOTOR_STOP || cmd == PAD_MOTOR_STOP_HARD) {
// 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) {
aurora::input::controller_rumble(instance, 1, 1, 0);
} else if (cmd == PAD_MOTOR_STOP_HARD) {
aurora::input::controller_rumble(instance, 0, 0, 0);
}
} else {
if (cmd == PAD_MOTOR_STOP) {
@@ -1078,8 +1190,8 @@ void PADSetButtonMapping(const u32 port, const PADButtonMapping mapping) {
return;
}
const auto iter = std::ranges::find_if(controller->m_buttonMapping,
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
const auto iter = std::find_if(controller->m_buttonMapping.begin(), controller->m_buttonMapping.end(),
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
if (iter == controller->m_buttonMapping.end()) {
return;
}
@@ -1112,8 +1224,8 @@ void PADSetAltButtonMapping(const u32 port, const PADButtonMapping mapping) {
return;
}
const auto iter = std::ranges::find_if(controller->m_altButtonMapping,
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
const auto iter = std::find_if(controller->m_altButtonMapping.begin(), controller->m_altButtonMapping.end(),
[mapping](const auto& pair) { return mapping.padButton == pair.padButton; });
if (iter == controller->m_altButtonMapping.end()) {
return;
}
@@ -1139,8 +1251,8 @@ void PADSetAxisMapping(const u32 port, const PADAxisMapping mapping) {
return;
}
const auto iter = std::ranges::find_if(controller->m_axisMapping,
[mapping](const auto& pair) { return mapping.padAxis == pair.padAxis; });
const auto iter = std::find_if(controller->m_axisMapping.begin(), controller->m_axisMapping.end(),
[mapping](const auto& pair) { return mapping.padAxis == pair.padAxis; });
if (iter == controller->m_axisMapping.end()) {
return;
}
@@ -1191,6 +1303,11 @@ BOOL PADSetKeyButtonBindings(const u32 port, PADKeyButtonBinding bindings[PAD_BU
}
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) {
return nullptr;
}
@@ -1309,9 +1426,10 @@ static void load_keyboard_bindings() {
if (mappingsSet) {
const bool anyBound =
std::ranges::any_of(buttonMapping,
[](const PADKeyButtonBinding& b) { return b.scancode != PAD_KEY_INVALID; }) ||
std::ranges::any_of(axisMapping, [](const PADKeyAxisBinding& b) { return b.scancode != PAD_KEY_INVALID; });
std::any_of(buttonMapping.begin(), buttonMapping.end(),
[](const PADKeyButtonBinding& b) { return b.scancode != PAD_KEY_INVALID; }) ||
std::any_of(axisMapping.begin(), axisMapping.end(),
[](const PADKeyAxisBinding& b) { return b.scancode != PAD_KEY_INVALID; });
if (!anyBound) {
mappingsSet = false;
}
@@ -1351,7 +1469,10 @@ void __PADWriteDeadZones(SDL_IOStream* file, // NOLINT(*-reserved-identifier)
void PADSerializeMappings() {
const std::filesystem::path basePath = fs_path_from_string(aurora::g_config.userPath);
for (auto& controller : aurora::input::g_GameControllers | std::views::values) {
// Avoid std::views::values here: older Apple libc++ releases implement the
// C++20 ranges algorithms we use but not this adaptor.
for (auto& entry : aurora::input::g_GameControllers) {
auto& controller = entry.second;
EnsureMappingLoaded(&controller);
const auto filePath =
basePath / fmt::format("{}_{:04X}_{:04X}.controller", aurora::input::controller_name(controller.m_index),
@@ -1453,8 +1574,8 @@ static constexpr std::array<std::pair<PADButton, std::string_view>, PAD_AXIS_COU
const char* PADGetButtonName(const PADButton button) {
if (const auto iter =
std::ranges::find_if(skButtonNames, [&button](const auto& pair) { return button == pair.first; });
if (const auto iter = std::find_if(skButtonNames.begin(), skButtonNames.end(),
[&button](const auto& pair) { return button == pair.first; });
iter != skButtonNames.end()) {
return iter->second.data();
}
@@ -1467,7 +1588,8 @@ const char* PADGetNativeButtonName(u32 button) {
}
const char* PADGetAxisName(const PADAxis axis) {
if (const auto it = std::ranges::find_if(skAxisNames, [&axis](const auto& pair) { return axis == pair.first; });
if (const auto it = std::find_if(skAxisNames.begin(), skAxisNames.end(),
[&axis](const auto& pair) { return axis == pair.first; });
it != skAxisNames.end()) {
return it->second.data();
}
@@ -1476,7 +1598,8 @@ const char* PADGetAxisName(const PADAxis axis) {
}
const char* PADGetAxisDirectionLabel(const PADAxis axis) {
if (const auto it = std::ranges::find_if(skAxisDirLabels, [&axis](const auto& pair) { return axis == pair.first; });
if (const auto it = std::find_if(skAxisDirLabels.begin(), skAxisDirLabels.end(),
[&axis](const auto& pair) { return axis == pair.first; });
it != skAxisDirLabels.end()) {
return it->second.data();
}
+27 -12
View File
@@ -7,11 +7,13 @@
#include <algorithm>
#include <atomic>
#include <optional>
#include <mutex>
namespace aurora::vi {
std::optional<GXRenderModeObj> g_renderMode;
namespace {
std::atomic<float> g_presentAspectCorrection{1.f};
std::mutex g_renderModeMutex;
float calculate_present_aspect_correction(const GXRenderModeObj& rm) noexcept {
if (rm.viWidth == 0 || rm.viHeight == 0) {
@@ -29,9 +31,8 @@ float calculate_present_aspect_correction(const GXRenderModeObj& rm) noexcept {
const float verticalFill = static_cast<float>(rm.viHeight) / nominalActiveHeight;
return horizontalFill / verticalFill;
}
} // namespace
Vec2<uint32_t> render_mode_size() noexcept {
Vec2<uint32_t> render_mode_size_locked() noexcept {
if (!g_renderMode) {
return {640, 528};
}
@@ -40,18 +41,31 @@ Vec2<uint32_t> render_mode_size() noexcept {
return {std::max<uint32_t>(g_renderMode->fbWidth, 640), std::max<uint32_t>(g_renderMode->efbHeight, 528)};
}
} // namespace
Vec2<uint32_t> render_mode_size() noexcept {
std::lock_guard lock(g_renderModeMutex);
return render_mode_size_locked();
}
void configure(const GXRenderModeObj* rm) noexcept {
const auto oldSize = render_mode_size();
if (rm == nullptr) {
g_renderMode.reset();
} else {
g_renderMode = *rm;
g_presentAspectCorrection.store(calculate_present_aspect_correction(*rm), std::memory_order_release);
bool sizeChanged = false;
{
std::lock_guard lock(g_renderModeMutex);
const auto oldSize = render_mode_size_locked();
if (rm == nullptr) {
g_renderMode.reset();
} else {
g_renderMode = *rm;
g_presentAspectCorrection.store(calculate_present_aspect_correction(*rm), std::memory_order_release);
}
if (rm == nullptr) {
g_presentAspectCorrection.store(1.f, std::memory_order_release);
}
sizeChanged = render_mode_size_locked() != oldSize;
}
if (rm == nullptr) {
g_presentAspectCorrection.store(1.f, std::memory_order_release);
}
if (render_mode_size() != oldSize) {
// Never hold the mode lock across a resize request or a renderer callback.
if (sizeChanged) {
window::request_frame_buffer_resize();
}
}
@@ -61,6 +75,7 @@ Vec2<uint32_t> configured_fb_size() noexcept {
}
Vec2<uint32_t> visible_fb_size() noexcept {
std::lock_guard lock(g_renderModeMutex);
if (!g_renderMode) {
return {640, 528};
}
+191 -53
View File
@@ -1,4 +1,5 @@
#include "common.hpp"
#include "staging_map.hpp"
#include "../gx/shader_info.hpp"
#include "clear.hpp"
@@ -22,7 +23,6 @@
#include <memory>
#include <mutex>
#include <optional>
#include <ranges>
#include <absl/container/flat_hash_map.h>
#include <magic_enum.hpp>
@@ -36,10 +36,13 @@ using webgpu::g_device;
using webgpu::g_instance;
using webgpu::g_queue;
struct DebugFrameData {
#ifdef AURORA_GFX_DEBUG_GROUPS
std::vector<std::string> g_debugGroupStack;
std::vector<std::string> g_debugMarkers;
std::vector<std::string> groups;
std::vector<std::string> markers;
#endif
};
DebugFrameData g_debugFrame;
constexpr uint64_t StagingBufferSize = UniformBufferSize + VertexBufferSize + IndexBufferSize + StorageBufferSize +
(UseTextureBuffer ? TextureUploadSize : 0);
@@ -128,12 +131,7 @@ wgpu::Buffer g_storageBuffer;
constexpr size_t FrameSlotCount = 3;
static std::array<wgpu::Buffer, FrameSlotCount> g_stagingBuffers;
static size_t currentStagingBuffer = 0;
enum class BufferMapState {
Unmapped,
Mapping,
Mapped,
};
static std::atomic s_mappingState{BufferMapState::Unmapped};
static StagingMapState s_mappingState;
static wgpu::Limits g_cachedLimits;
// Advanced once per logical frame in the seal prologue, under the renderer GPU mutex and with the
// producer blocked, so every later reader sees a value that no longer moves.
@@ -168,7 +166,12 @@ struct RenderPass {
Range resolveUniformRange;
std::array<u32, 3> resolveCopyFilterCoefficients{0, 64, 0};
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;
bool clearColor = true;
bool clearDepth = true;
@@ -229,6 +232,8 @@ static void recycle_render_passes(std::vector<RenderPass>& passes) noexcept {
}
struct SealedFrameData {
depth_peek::FrameMapping depthMapping;
DebugFrameData debug;
std::vector<RenderPass> passes;
};
@@ -250,6 +255,51 @@ static std::atomic_bool g_inOffscreen{false};
static std::optional<RenderPass> g_suspendedEfbPass;
static Viewport g_suspendedEfbViewport;
static ClipRect g_suspendedEfbScissor;
// Prefix referenced by a suspended EFB pass. Preserve its offsets across an
// offscreen split, without rendering it before the bake it may sample finishes.
static StagingSizes g_suspendedEfbBytes{};
static constexpr StagingSizes PhysicalStagingCapacity{
VertexBufferSize, UniformBufferSize, IndexBufferSize, StorageBufferSize};
static StagingSizes g_stagingCapacity = PhysicalStagingCapacity;
static uint64_t g_stagingEpoch = 0;
static uint64_t g_stagingSplitCount = 0;
static StagingSizes g_stagingHighWater{};
StagingSizes staging_usage() noexcept {
return {g_verts.size(), g_uniforms.size(), g_indices.size(), g_storage.size()};
}
StagingSizes staging_high_water() noexcept { return g_stagingHighWater; }
uint64_t staging_epoch() noexcept { return g_stagingEpoch; }
uint64_t staging_split_count() noexcept { return g_stagingSplitCount; }
uint64_t staging_uniform_bytes(uint64_t bytes) {
return staging_padded(bytes, g_cachedLimits.minUniformBufferOffsetAlignment);
}
uint64_t staging_storage_bytes(uint64_t bytes) {
return staging_padded(bytes, g_cachedLimits.minStorageBufferOffsetAlignment);
}
void set_staging_capacity_limits_for_testing(const StagingSizes& limits) {
for (unsigned i = 0; i < limits.size(); ++i) {
if (limits[i] > PhysicalStagingCapacity[i])
throw StagingCapacityError("Test staging capacity exceeds physical buffer");
}
g_stagingCapacity = limits;
g_stagingHighWater = {};
}
bool staging_has_space(const StagingSizes& demand) {
// Async readback preparation runs in the worker's noexcept seal prologue.
// Reserve all 32 slots plus the uniform binding's 3840-byte trailing window.
const StagingSizes tail{0, gx::MaxUniformSize + efb_ram::MaxAsyncReadbackSlots * staging_uniform_bytes(48), 0, 0};
const StagingSizes retained = g_suspendedEfbPass ? g_suspendedEfbBytes : StagingSizes{};
if (!staging_fits(retained, demand, tail, g_stagingCapacity))
throw StagingCapacityError("GPU operation exceeds staging capacity including retained EFB data");
return staging_fits(staging_usage(), demand, tail, g_stagingCapacity);
}
void ensure_staging_space(const StagingSizes& demand) {
if (staging_has_space(demand)) return;
split_staging_batch();
if (!staging_has_space(demand))
throw StagingCapacityError("GPU operation still exceeds staging capacity after submission");
}
static void discard_suspended_efb_pass() noexcept {
if (g_suspendedEfbPass) {
@@ -274,7 +324,8 @@ static size_t g_recordingSnapshotSlot = 0;
static TextureHandle new_resolve_source_snapshot(wgpu::Extent3D size, wgpu::TextureFormat format) noexcept {
const wgpu::TextureDescriptor textureDescriptor{
.label = "GX Copy Source Snapshot",
.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst,
.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc |
wgpu::TextureUsage::CopyDst,
.dimension = wgpu::TextureDimension::e2D,
.size = size,
.format = format,
@@ -420,7 +471,7 @@ static inline void push_command(CommandType type, const Command::Data& data) {
g_renderPasses[g_currentRenderPass].commands.push_back({
.type = type,
#ifdef AURORA_GFX_DEBUG_GROUPS
.debugGroupStack = g_debugGroupStack,
.debugGroupStack = g_debugFrame.groups,
#endif
.data = data,
});
@@ -480,6 +531,7 @@ void set_scissor(const ClipRect& cmd) noexcept {
template <>
void push_draw_command(clear::DrawData data) {
if (data.uniformRange.size == 0) {
ensure_staging_space({0, staging_uniform_bytes(16), 0, 0});
const std::array clearUniform{
std::clamp(data.depth, 0.f, 1.f),
0.f,
@@ -506,6 +558,7 @@ void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool cl
Log.warn("Dropping resolve pass without an active render pass");
return;
}
ensure_staging_space({0, 2 * staging_uniform_bytes(48), 0, 0});
auto& prevPass = g_renderPasses[g_currentRenderPass];
const auto targetWidth = static_cast<int32_t>(prevPass.targetSize.width);
const auto targetHeight = static_cast<int32_t>(prevPass.targetSize.height);
@@ -538,7 +591,7 @@ void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool cl
sourceRect = {srcLeft, srcTop, std::max(srcRight - srcLeft, 1.0f), std::max(srcBottom - srcTop, 1.0f)};
}
prevPass.resolveTarget = std::move(texture);
prevPass.requireReadyPipelines = persistentCopy;
prevPass.requireReadyPipelines |= persistentCopy;
prevPass.resolveRect = rect;
prevPass.resolveSourceRect = sourceRect;
prevPass.resolveFormat = resolveFormat;
@@ -734,6 +787,7 @@ void begin_offscreen(uint32_t width, uint32_t height) {
if (!g_inOffscreen) {
auto& currentPass = g_renderPasses[g_currentRenderPass];
if (!currentPass.resolveTarget) {
g_suspendedEfbBytes = staging_usage();
g_suspendedEfbPass = std::move(currentPass);
g_renderPasses.pop_back();
--g_currentRenderPass;
@@ -757,7 +811,9 @@ void begin_offscreen(uint32_t width, uint32_t height) {
.targetSize = {width, height, 1},
.msaaSamples = 1,
.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,
.clearDepth = true,
};
@@ -844,7 +900,7 @@ void initialize() {
label.c_str());
}
currentStagingBuffer = 0;
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
s_mappingState.reset();
map_staging_buffer();
{
@@ -950,6 +1006,8 @@ void shutdown() {
g_uniformBuffer = {};
g_indexBuffer = {};
g_storageBuffer = {};
// Invalidate outstanding callbacks before releasing their buffers.
s_mappingState.reset();
g_stagingBuffers.fill({});
for (auto& pool : g_resolveSourceSnapshotPools) {
pool.entry.reset();
@@ -968,37 +1026,36 @@ void shutdown() {
g_inOffscreen = false;
g_frameIndex = UINT32_MAX;
currentStagingBuffer = 0;
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
}
void map_staging_buffer() {
auto expected = BufferMapState::Unmapped;
if (!s_mappingState.compare_exchange_strong(expected, BufferMapState::Mapping, std::memory_order_acq_rel,
std::memory_order_acquire)) {
const auto generation = s_mappingState.request();
if (generation == 0) {
return;
}
g_stagingBuffers[currentStagingBuffer].MapAsync(
wgpu::MapMode::Write, 0, StagingBufferSize, wgpu::CallbackMode::AllowSpontaneous,
[](wgpu::MapAsyncStatus status, wgpu::StringView message) {
[generation](wgpu::MapAsyncStatus status, wgpu::StringView message) {
const auto result = status == wgpu::MapAsyncStatus::Success
? BufferMapState::Mapped : BufferMapState::Unmapped;
if (!s_mappingState.complete(generation, result)) return;
if (status == wgpu::MapAsyncStatus::CallbackCancelled || status == wgpu::MapAsyncStatus::Aborted) {
Log.warn("Buffer mapping {}: {}", magic_enum::enum_name(status), message);
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
return;
}
ASSERT(status == wgpu::MapAsyncStatus::Success, "Buffer mapping failed: {} {}", magic_enum::enum_name(status),
message);
s_mappingState.store(BufferMapState::Mapped, std::memory_order_release);
});
}
static bool begin_frame_impl(bool clearEfb) {
static bool begin_frame_impl(bool clearEfb, bool capacityResume = false) {
ZoneScoped;
{
ZoneScopedN("Wait for buffer map");
map_staging_buffer();
while (true) {
const auto mappingState = s_mappingState.load(std::memory_order_acquire);
const auto mappingState = s_mappingState.state();
if (mappingState == BufferMapState::Mapped) {
break;
}
@@ -1014,8 +1071,11 @@ static bool begin_frame_impl(bool clearEfb) {
return false;
}
g_instance.ProcessEvents();
webgpu::fail_if_device_lost();
s_mappingState.wait_for_progress();
}
}
++g_stagingEpoch;
g_recordingSnapshotSlot = currentStagingBuffer;
size_t bufferOffset = 0;
const auto& stagingBuf = g_stagingBuffers[currentStagingBuffer];
@@ -1040,7 +1100,7 @@ static bool begin_frame_impl(bool clearEfb) {
gx::begin_frame_interpolation();
}
discard_suspended_efb_pass();
webgpu::clear_present_source_override();
if (!capacityResume) webgpu::clear_present_source_override();
push_render_pass(RenderPass{});
set_efb_targets(g_renderPasses[0]);
@@ -1079,12 +1139,12 @@ void abort_frame() noexcept {
g_textureUploads.clear();
g_textureUpload.release();
}
if (s_mappingState.load(std::memory_order_acquire) == BufferMapState::Mapped) {
if (s_mappingState.state() == BufferMapState::Mapped) {
// Pending interpolation tasks hold raw pointers into the mapped staging
// range; they must be dropped before the buffer is unmapped and rotated.
gx::drop_pending_frame_interpolation_uniforms();
g_stagingBuffers[currentStagingBuffer].Unmap();
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
s_mappingState.reset();
currentStagingBuffer = (currentStagingBuffer + 1) % g_stagingBuffers.size();
map_staging_buffer();
}
@@ -1101,7 +1161,7 @@ void abort_frame() noexcept {
static void end_batch_impl(const wgpu::CommandEncoder& cmd, bool advanceFrame) {
ZoneScoped;
ASSERT(!g_inOffscreen, "end_frame called while offscreen rendering is active");
ASSERT(!advanceFrame || !g_inOffscreen, "end_frame called while offscreen rendering is active");
if (advanceFrame) {
gx::finalize_frame_interpolation();
} else {
@@ -1110,6 +1170,8 @@ static void end_batch_impl(const wgpu::CommandEncoder& cmd, bool advanceFrame) {
gx::drop_pending_frame_interpolation_uniforms();
}
g_uniforms.append_zeroes(gx::MaxUniformSize); // Pad the end of the buffer
const auto used = staging_usage();
for (unsigned i = 0; i < used.size(); ++i) g_stagingHighWater[i] = std::max(g_stagingHighWater[i], used[i]);
uint64_t bufferOffset = 0;
const auto writeBuffer = [&](ByteBuffer& buf, wgpu::Buffer& out, uint64_t size, std::string_view label) {
const auto writeSize = buf.size(); // Only need to copy this many bytes
@@ -1121,7 +1183,7 @@ static void end_batch_impl(const wgpu::CommandEncoder& cmd, bool advanceFrame) {
return writeSize;
};
g_stagingBuffers[currentStagingBuffer].Unmap();
s_mappingState.store(BufferMapState::Unmapped, std::memory_order_release);
s_mappingState.reset();
g_stats.drawCallCount = g_drawCallCount;
g_stats.mergedDrawCallCount = g_mergedDrawCallCount;
g_stats.lastVertSize = writeBuffer(g_verts, g_vertexBuffer, VertexBufferSize, "Vertex");
@@ -1164,6 +1226,63 @@ void end_frame(const wgpu::CommandEncoder& cmd) { end_batch_impl(cmd, true); }
void end_batch(const wgpu::CommandEncoder& cmd) { end_batch_impl(cmd, false); }
void split_staging_batch() {
// Never called under the decoder's renderer lock: the worker needs that lock
// to reach DONE. FIFO admission yields its unconsumed command first.
aurora::wait_for_frame_worker();
std::lock_guard gpuLock(aurora::renderer_gpu_mutex());
if (!has_current_render_pass())
throw StagingCapacityError("Cannot split staging outside an active render pass");
gx::mark_frame_interpolation_replay_unsafe();
const bool offscreen = g_inOffscreen;
const auto viewport = g_cachedViewport;
const auto scissor = g_cachedScissor;
const auto renderViewport = gx::g_gxState.renderViewport;
const auto renderScissor = gx::g_gxState.renderScissor;
const auto& active = g_renderPasses[g_currentRenderPass];
RenderPass continuation{
.colorView = active.colorView, .resolveView = active.resolveView,
.depthView = active.depthView, .copySourceTexture = active.copySourceTexture,
.copySourceView = active.copySourceView, .copySourceDepthView = active.copySourceDepthView,
.targetSize = active.targetSize, .msaaSamples = active.msaaSamples,
.clearColor = false, .clearDepth = false,
.requireReadyPipelines = active.requireReadyPipelines || offscreen,
};
auto suspended = std::move(g_suspendedEfbPass);
g_suspendedEfbPass.reset();
std::array<std::vector<uint8_t>, 4> retained;
std::array<ByteBuffer*, 4> buffers{&g_verts, &g_uniforms, &g_indices, &g_storage};
if (suspended) {
for (unsigned i = 0; i < buffers.size(); ++i) {
if (g_suspendedEfbBytes[i])
retained[i].assign(buffers[i]->data(), buffers[i]->data() + g_suspendedEfbBytes[i]);
}
}
auto encoder = g_device.CreateCommandEncoder();
end_batch(encoder);
render(encoder);
aurora::submit_staging_commands(encoder.Finish());
after_submit();
if (!begin_frame_impl(false, true))
throw StagingCapacityError("Staging remap failed after capacity submission");
recycle_render_passes(g_renderPasses);
push_render_pass(std::move(continuation));
g_currentRenderPass = 0;
g_suspendedEfbPass = std::move(suspended);
for (unsigned i = 0; i < buffers.size(); ++i) {
if (!retained[i].empty()) buffers[i]->append(retained[i].data(), retained[i].size());
}
g_inOffscreen = offscreen;
g_cachedViewport = viewport;
g_cachedScissor = scissor;
gx::g_gxState.renderViewport = renderViewport;
gx::g_gxState.renderScissor = renderScissor;
gx::g_gxState.stateDirty = true;
push_command(CommandType::SetViewport, Command::Data{.setViewport = viewport});
push_command(CommandType::SetScissor, Command::Data{.setScissor = scissor});
++g_stagingSplitCount;
}
uint32_t current_frame() noexcept { return g_frameIndex; }
// The only place that erases from g_cachedBindGroups, whose handles the frame being encoded still
@@ -1196,10 +1315,10 @@ static const char* render_pass_label(u32 index) noexcept {
}
static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vector<RenderPass>& passes, u32 idx,
int32_t interpolatedFrame);
int32_t interpolatedFrame, DebugFrameData& debugFrame);
static void render_impl(std::vector<RenderPass>& renderPasses, wgpu::CommandEncoder& cmd, int32_t interpolatedFrame,
bool finalize) {
bool finalize, DebugFrameData& debugFrame, const depth_peek::FrameMapping& depthMapping) {
ZoneScoped;
// Palette conversions, MSAA resolves and EFB copies depend on sealed frame state, not on the
// interpolation weight, so encode them on the native render and let replay slots sample them.
@@ -1249,11 +1368,11 @@ static void render_impl(std::vector<RenderPass>& renderPasses, wgpu::CommandEnco
};
auto pass = cmd.BeginRenderPass(&renderPassDescriptor);
render_pass_impl(pass, renderPasses, i, interpolatedFrame);
render_pass_impl(pass, renderPasses, i, interpolatedFrame, debugFrame);
pass.End();
if (finalize && i == renderPasses.size() - 1) {
depth_peek::encode_frame_snapshot(cmd, passInfo.copySourceDepthView, passInfo.targetSize, passInfo.msaaSamples);
depth_peek::encode_frame_snapshot(cmd, passInfo.copySourceDepthView, passInfo.targetSize, passInfo.msaaSamples, depthMapping);
}
if (passInfo.resolveTarget) {
@@ -1327,20 +1446,21 @@ static void render_impl(std::vector<RenderPass>& renderPasses, wgpu::CommandEnco
}
#if defined(AURORA_GFX_DEBUG_GROUPS)
if (finalize && !g_debugGroupStack.empty()) {
for (auto& it : std::ranges::reverse_view(g_debugGroupStack)) {
Log.warn("Debug group was not popped at end of frame: {}", it);
if (finalize && !debugFrame.groups.empty()) {
for (auto it = debugFrame.groups.rbegin(); it != debugFrame.groups.rend(); ++it) {
Log.warn("Debug group was not popped at end of frame: {}", *it);
}
g_debugGroupStack.clear();
debugFrame.groups.clear();
}
if (finalize && g_debugMarkers.size() > 0) {
g_debugMarkers.clear();
if (finalize && debugFrame.markers.size() > 0) {
debugFrame.markers.clear();
}
#endif
}
void seal_frame(SealedFrame& out) noexcept {
out.data().depthMapping = depth_peek::capture_frame_mapping();
ZoneScoped;
// The encode that could still have been holding these has completed: the
// producer joins the worker's DONE phase before it seals another frame.
@@ -1350,15 +1470,24 @@ void seal_frame(SealedFrame& out) noexcept {
// capacity included, back to the producer.
recycle_render_passes(passes);
passes.swap(g_renderPasses);
#ifdef AURORA_GFX_DEBUG_GROUPS
// Marker indices and unmatched-group warnings belong to these detached passes.
// The next producer frame must not modify strings still read by this encoder.
auto& debug = out.data().debug;
debug.groups.clear();
debug.markers.clear();
debug.groups.swap(g_debugFrame.groups);
debug.markers.swap(g_debugFrame.markers);
#endif
g_currentRenderPass = UINT32_MAX;
}
void render(SealedFrame& frame, wgpu::CommandEncoder& cmd, int32_t interpolatedFrame, bool finalize) {
render_impl(frame.data().passes, cmd, interpolatedFrame, finalize);
render_impl(frame.data().passes, cmd, interpolatedFrame, finalize, frame.data().debug, frame.data().depthMapping);
}
void render(wgpu::CommandEncoder& cmd, int32_t interpolatedFrame, bool finalize) {
render_impl(g_renderPasses, cmd, interpolatedFrame, finalize);
render_impl(g_renderPasses, cmd, interpolatedFrame, finalize, g_debugFrame, depth_peek::capture_frame_mapping());
if (finalize) {
g_currentRenderPass = UINT32_MAX;
expire_bind_group_cache();
@@ -1376,7 +1505,7 @@ void after_submit() noexcept {
}
static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vector<RenderPass>& renderPasses, u32 idx,
int32_t interpolatedFrame) {
int32_t interpolatedFrame, DebugFrameData& debugFrame) {
// Per-invocation, not per-process: two encoders can be recording at once.
gx::DrawEncodeState encodeState{};
encodeState.boundTextureBindGroup = gx::g_emptyTextureBindGroup.Get();
@@ -1410,10 +1539,19 @@ static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vec
switch (cmd.type) {
case CommandType::SetViewport: {
const auto& vp = cmd.data.setViewport;
// WebGPU requires 0 <= minDepth <= maxDepth <= 1, and the guest's (near, far) order is already
// reproduced in clip space. Passing the raw swapped pair diverged per backend in release builds.
const float minDepth = std::clamp(std::min(vp.znear, vp.zfar), 0.0f, 1.0f);
const float maxDepth = std::clamp(std::max(vp.znear, vp.zfar), 0.0f, 1.0f);
// WebGPU requires 0 <= minDepth <= maxDepth <= 1. vp.znear/vp.zfar are in GX's own distance
// terms (0 = near); under UseReversedZ the host depth-buffer storage direction is flipped
// (near = 1, far = 0), so this range has to be remapped through 1-x the same way the
// 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);
} break;
case CommandType::SetScissor: {
@@ -1447,7 +1585,7 @@ static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vec
} break;
case CommandType::DebugMarker: {
#if defined(AURORA_GFX_DEBUG_GROUPS)
pass.InsertDebugMarker(wgpu::StringView(g_debugMarkers[cmd.data.debugMarkerIndex]));
pass.InsertDebugMarker(wgpu::StringView(debugFrame.markers[cmd.data.debugMarkerIndex]));
#endif
} break;
}
@@ -1600,8 +1738,8 @@ uint32_t align_uniform(uint32_t value) { return AURORA_ALIGN(value, g_cachedLimi
void insert_debug_marker(std::string label) {
#if defined(AURORA_GFX_DEBUG_GROUPS)
auto idx = g_debugMarkers.size();
g_debugMarkers.emplace_back(std::move(label));
auto idx = g_debugFrame.markers.size();
g_debugFrame.markers.emplace_back(std::move(label));
push_command(CommandType::DebugMarker, {.debugMarkerIndex = idx});
#endif
}
@@ -1610,22 +1748,22 @@ void insert_debug_marker(std::string label) {
void aurora::gfx::push_debug_group(std::string label) {
#if defined(AURORA_GFX_DEBUG_GROUPS)
g_debugGroupStack.push_back(std::move(label));
g_debugFrame.groups.push_back(std::move(label));
#endif
}
void aurora_push_debug_group(const char* label) {
#ifdef AURORA_GFX_DEBUG_GROUPS
aurora::gfx::g_debugGroupStack.emplace_back(label);
aurora::gfx::g_debugFrame.groups.emplace_back(label);
#endif
}
void aurora_pop_debug_group() {
#ifdef AURORA_GFX_DEBUG_GROUPS
if (aurora::gfx::g_debugGroupStack.empty()) {
if (aurora::gfx::g_debugFrame.groups.empty()) {
aurora::gfx::Log.error("Debug group stack underflowed!");
return;
}
aurora::gfx::g_debugGroupStack.pop_back();
aurora::gfx::g_debugFrame.groups.pop_back();
#endif
}
+15
View File
@@ -1,4 +1,5 @@
#pragma once
#include "staging_capacity.hpp"
#include "../internal.hpp"
#include "../webgpu/gpu.hpp"
@@ -394,6 +395,20 @@ wgpu::BindGroup& find_bind_group(BindGroupRef id);
wgpu::Sampler& sampler_ref(const wgpu::SamplerDescriptor& descriptor);
uint32_t align_uniform(uint32_t value);
uint64_t staging_uniform_bytes(uint64_t bytes);
uint64_t staging_storage_bytes(uint64_t bytes);
// Admission does not allocate. A false result requires a producer-side split.
// Oversized operations fail before mutating the current draw/pass.
bool staging_has_space(const StagingSizes& demand);
void ensure_staging_space(const StagingSizes& demand);
void split_staging_batch();
uint64_t staging_epoch() noexcept;
StagingSizes staging_usage() noexcept;
StagingSizes staging_high_water() noexcept;
uint64_t staging_split_count() noexcept;
// Internal integration-test seam: never increases the physical allocation.
void set_staging_capacity_limits_for_testing(const StagingSizes& limits);
Vec2<uint32_t> get_render_target_size() noexcept;
// Same value as get_render_target_size() outside a render pass, but never
+14 -8
View File
@@ -92,7 +92,7 @@ struct Params {
constexpr std::string_view ReversedZBody = R"(
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;
@@ -196,7 +196,8 @@ wgpu::BindGroupLayout create_bind_group_layout(const char* label) {
return g_device.CreateBindGroupLayout(&descriptor);
}
Params make_params(wgpu::Extent3D sourceSize, Vec2<uint32_t> dstSize) noexcept {
Params make_params(wgpu::Extent3D sourceSize, const FrameMapping& mapping) noexcept {
const auto dstSize = mapping.logicalSize;
Params params{
.dstWidth = dstSize.x,
.dstHeight = dstSize.y,
@@ -204,16 +205,17 @@ Params make_params(wgpu::Extent3D sourceSize, Vec2<uint32_t> dstSize) noexcept {
.srcHeight = sourceSize.height,
};
if (gx::g_gxState.viewportPolicy == AURORA_VIEWPORT_NATIVE) {
if (mapping.viewportPolicy == AURORA_VIEWPORT_NATIVE) {
return params;
}
const auto logicalSize = vi::configured_fb_size();
const auto logicalSize = mapping.logicalSize;
if (logicalSize.x == 0 || logicalSize.y == 0 || sourceSize.width == 0 || sourceSize.height == 0) {
return params;
}
const bool stretch = gx::g_gxState.viewportPolicy == AURORA_VIEWPORT_STRETCH;
const bool stretch = mapping.viewportPolicy == AURORA_VIEWPORT_STRETCH ||
mapping.viewportPolicy == AURORA_VIEWPORT_16_9;
const float scaleX = static_cast<float>(sourceSize.width) / static_cast<float>(logicalSize.x);
const float scaleY = static_cast<float>(sourceSize.height) / static_cast<float>(logicalSize.y);
const float scale = std::min(scaleX, scaleY);
@@ -336,8 +338,12 @@ void poll() noexcept {
}
}
FrameMapping capture_frame_mapping() noexcept {
return {vi::configured_fb_size(), gx::g_gxState.viewportPolicy};
}
void encode_frame_snapshot(const wgpu::CommandEncoder& cmd, const wgpu::TextureView& depthView,
wgpu::Extent3D sourceSize, uint32_t msaaSamples) noexcept {
wgpu::Extent3D sourceSize, uint32_t msaaSamples, const FrameMapping& mapping) noexcept {
ZoneScoped;
const auto now = Clock::now();
{
@@ -349,7 +355,7 @@ void encode_frame_snapshot(const wgpu::CommandEncoder& cmd, const wgpu::TextureV
g_nextSnapshotTime = now + SnapshotInterval;
}
const auto dstSize = vi::configured_fb_size();
const auto dstSize = mapping.logicalSize;
if (!depthView || dstSize.x == 0 || dstSize.y == 0 || sourceSize.width == 0 || sourceSize.height == 0) {
return;
}
@@ -357,7 +363,7 @@ void encode_frame_snapshot(const wgpu::CommandEncoder& cmd, const wgpu::TextureV
Log.fatal("Depth Peek from multisampled EFB targets is not supported");
}
const Params params = make_params(sourceSize, dstSize);
const Params params = make_params(sourceSize, mapping);
wgpu::Buffer storageBuffer;
wgpu::Buffer readbackBuffer;
wgpu::Buffer paramsBuffer;
+9 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#include "common.hpp"
#include <dolphin/gx/GXAurora.h>
#include <vector>
@@ -13,8 +14,15 @@ void request_snapshot() noexcept;
bool read_latest(uint16_t x, uint16_t y, uint32_t& z) noexcept;
void poll() noexcept;
// Captured before SEALED; the producer may configure the next frame during encode.
struct FrameMapping {
Vec2<uint32_t> logicalSize{};
AuroraViewportPolicy viewportPolicy = AURORA_VIEWPORT_FIT;
};
FrameMapping capture_frame_mapping() noexcept;
void encode_frame_snapshot(const wgpu::CommandEncoder& cmd, const wgpu::TextureView& depthView,
wgpu::Extent3D sourceSize, uint32_t msaaSamples) noexcept;
wgpu::Extent3D sourceSize, uint32_t msaaSamples, const FrameMapping& mapping) noexcept;
void after_submit() noexcept;
namespace testing {
+63 -21
View File
@@ -8,7 +8,9 @@
#include <algorithm>
#include <array>
#include <cstring>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
@@ -27,7 +29,7 @@ using webgpu::g_instance;
constexpr size_t kAsyncReadbackMaxBytes = 256;
// Each destination keeps its readback buffer forever. Only a handful are expected, and the cap
// stops an unexpected pattern of one-shot destinations from leaking GPU buffers.
constexpr size_t kMaxAsyncSlots = 32;
constexpr size_t kMaxAsyncSlots = MaxAsyncReadbackSlots;
struct PendingCopy {
void* dest = nullptr;
@@ -37,6 +39,7 @@ struct PendingCopy {
TextureHandle texture;
TextureHandle nativeTexture;
Range nativeBlitUniform;
uint64_t nativeUniformEpoch = 0;
};
struct Download {
@@ -81,6 +84,7 @@ std::vector<PendingCopy> g_asyncSealed;
std::mutex g_asyncMutex;
std::unordered_map<void*, AsyncSlot> g_asyncSlots;
uint32_t g_asyncMapsInFlight = 0;
uint64_t g_asyncGeneration = 1;
uint32_t align_to(uint32_t value, uint32_t alignment) noexcept { return (value + alignment - 1) & ~(alignment - 1); }
@@ -90,10 +94,10 @@ void ensure_native_texture(PendingCopy& pending, TextureHandle* cache = nullptr)
if (pending.texture->size.width == pending.width && pending.texture->size.height == pending.height) {
return;
}
if (pending.nativeTexture && pending.nativeUniformEpoch == staging_epoch()) return;
if (pending.nativeTexture) {
return;
}
if (cache != nullptr && *cache && (*cache)->size.width == pending.width &&
// Keep the texture; its old staging range belongs to a submitted batch.
} else if (cache != nullptr && *cache && (*cache)->size.width == pending.width &&
(*cache)->size.height == pending.height) {
pending.nativeTexture = *cache;
} else {
@@ -102,10 +106,12 @@ void ensure_native_texture(PendingCopy& pending, TextureHandle* cache = nullptr)
*cache = pending.nativeTexture;
}
}
// The shared blit shader clamps Y to flags.z/w; preserve the full source.
const std::array nativeBlitUniform{
0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 64.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 64.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,
};
pending.nativeBlitUniform = push_uniform(nativeBlitUniform);
pending.nativeUniformEpoch = staging_epoch();
}
void encode_native_blit(const wgpu::CommandEncoder& encoder, const PendingCopy& pending) noexcept {
@@ -125,16 +131,17 @@ HostPixelOrder texture_pixel_order(const TextureHandle& texture) noexcept {
return texture->format == wgpu::TextureFormat::BGRA8Unorm ? HostPixelOrder::BGRA : HostPixelOrder::RGBA;
}
void complete_async_slot(void* dest, wgpu::MapAsyncStatus status, wgpu::StringView message) noexcept {
void complete_async_slot(void* dest, uint64_t generation, wgpu::MapAsyncStatus status,
wgpu::StringView message) noexcept {
std::lock_guard lock{g_asyncMutex};
if (g_asyncMapsInFlight > 0) {
--g_asyncMapsInFlight;
}
if (generation != g_asyncGeneration) return;
const auto it = g_asyncSlots.find(dest);
if (it == g_asyncSlots.end()) {
return;
}
auto& slot = it->second;
if (slot.state != AsyncState::MapPending) return;
if (g_asyncMapsInFlight > 0) --g_asyncMapsInFlight;
if (status == wgpu::MapAsyncStatus::Success) {
const auto* pixels = static_cast<const uint8_t*>(slot.buffer.GetConstMappedRange(0, slot.bufferSize));
if (pixels != nullptr) {
@@ -227,7 +234,14 @@ bool has_pending(void* dest) noexcept {
[dest](const Download& download) { return download.copy.dest == dest; });
}
bool prepare_downloads(void* dest) noexcept {
bool prepare_downloads(void* dest) {
uint64_t copies = 0;
for (const auto& pending : g_pending) {
if (dest != nullptr && pending.dest != dest) continue;
if (pending.texture->size.width != pending.width || pending.texture->size.height != pending.height) ++copies;
}
// Reserve all copies, even already-prepared ones: a split retires their ranges.
ensure_staging_space({0, copies * staging_uniform_bytes(48), 0, 0});
bool found = false;
for (auto& pending : g_pending) {
if (dest != nullptr && pending.dest != dest) continue;
@@ -290,15 +304,35 @@ void encode_downloads(const wgpu::CommandEncoder& encoder, void* dest) noexcept
bool complete_downloads() noexcept {
bool success = true;
for (auto& download : g_downloads) {
wgpu::MapAsyncStatus mapStatus = wgpu::MapAsyncStatus::CallbackCancelled;
wgpu::StringView mapMessage{};
// WaitAny may time out before Dawn delivers cancellation. The callback must
// own its result rather than retaining references to this stack frame.
struct MapResult {
std::mutex mutex;
wgpu::MapAsyncStatus status = wgpu::MapAsyncStatus::CallbackCancelled;
std::string message;
};
const auto result = std::make_shared<MapResult>();
const auto future =
download.buffer.MapAsync(wgpu::MapMode::Read, 0, download.bufferSize, wgpu::CallbackMode::WaitAnyOnly,
[&mapStatus, &mapMessage](wgpu::MapAsyncStatus status, wgpu::StringView message) {
mapStatus = status;
mapMessage = message;
[result](wgpu::MapAsyncStatus status, wgpu::StringView message) {
std::lock_guard lock{result->mutex};
result->status = status;
if (message.data != nullptr) {
size_t length = 0;
while (length < 512 && length < message.length && message.data[length] != '\0') {
++length;
}
result->message.assign(message.data, length);
}
});
const auto waitStatus = g_instance.WaitAny(future, 5000000000);
wgpu::MapAsyncStatus mapStatus;
std::string mapMessage;
{
std::lock_guard lock{result->mutex};
mapStatus = result->status;
mapMessage = result->message;
}
if (waitStatus != wgpu::WaitStatus::Success || mapStatus != wgpu::MapAsyncStatus::Success) {
Log.error("EFB RAM readback failed wait={} map={} message={}", magic_enum::enum_name(waitStatus),
magic_enum::enum_name(mapStatus), mapMessage);
@@ -412,6 +446,7 @@ void after_submit() noexcept {
void* dest;
wgpu::Buffer buffer;
uint64_t bufferSize;
uint64_t generation;
};
std::vector<PendingMap> pendingMaps;
{
@@ -422,14 +457,15 @@ void after_submit() noexcept {
}
slot.state = AsyncState::MapPending;
++g_asyncMapsInFlight;
pendingMaps.push_back({dest, slot.buffer, slot.bufferSize});
pendingMaps.push_back({dest, slot.buffer, slot.bufferSize, g_asyncGeneration});
}
}
for (const auto& pending : pendingMaps) {
pending.buffer.MapAsync(wgpu::MapMode::Read, 0, pending.bufferSize, wgpu::CallbackMode::AllowSpontaneous,
[dest = pending.dest](wgpu::MapAsyncStatus status, wgpu::StringView message) {
complete_async_slot(dest, status, message);
[dest = pending.dest, generation = pending.generation](wgpu::MapAsyncStatus status,
wgpu::StringView message) {
complete_async_slot(dest, generation, status, message);
});
}
@@ -443,9 +479,15 @@ void abort_async() noexcept { g_asyncSealed.clear(); }
void shutdown() noexcept {
cancel();
g_asyncSealed.clear();
std::lock_guard lock{g_asyncMutex};
g_asyncSlots.clear();
g_asyncMapsInFlight = 0;
// Retire callbacks before releasing buffers, and release outside their mutex:
// destruction may itself deliver an AllowSpontaneous cancellation callback.
decltype(g_asyncSlots) retiredSlots;
{
std::lock_guard lock{g_asyncMutex};
++g_asyncGeneration;
retiredSlots.swap(g_asyncSlots);
g_asyncMapsInFlight = 0;
}
}
} // namespace aurora::gfx::efb_ram
+3 -1
View File
@@ -7,9 +7,11 @@
namespace aurora::gfx::efb_ram {
inline constexpr size_t MaxAsyncReadbackSlots = 32;
void schedule(void* dest, uint32_t width, uint32_t height, GXTexFmt format, TextureHandle texture) noexcept;
bool has_pending(void* dest = nullptr) noexcept;
bool prepare_downloads(void* dest = nullptr) noexcept;
bool prepare_downloads(void* dest = nullptr);
void encode_downloads(const wgpu::CommandEncoder& encoder, void* dest = nullptr) noexcept;
bool complete_downloads() noexcept;
void cancel() noexcept;
+3 -1
View File
@@ -396,6 +396,7 @@ static PendingPipeline* touch_pending_pipeline(PipelineRef hash, bool prioritize
g_priorityPipelines.emplace_back(std::move(*backgroundIt));
g_backgroundPipelines.erase(backgroundIt);
g_pipelineCv.notify_all();
return &g_priorityPipelines.back();
}
@@ -530,7 +531,8 @@ static PipelineRef find_pipeline_impl(ShaderType type, const PipelineConfig& con
}
if (notifyWorker) {
g_pipelineCv.notify_one();
// Compiler workers and renderer waiters share this condition variable.
g_pipelineCv.notify_all();
}
if (notifyWaiters) {
g_pipelineCv.notify_all();
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <array>
#include <cstdint>
#include <limits>
#include <stdexcept>
namespace aurora::gfx {
// Byte counts after each allocation's own trailing alignment, in V/U/I/S order.
using StagingSizes = std::array<uint64_t, 4>;
class StagingCapacityError : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
struct StagingBatchFull {};
inline uint64_t staging_padded(uint64_t bytes, uint64_t alignment) {
if (!bytes) return alignment;
const auto remainder = alignment ? bytes % alignment : 0;
const auto padding = remainder ? alignment - remainder : 0;
if (bytes > UINT64_MAX - padding) throw StagingCapacityError("Staging allocation size overflow");
return bytes + padding;
}
inline bool staging_fits(const StagingSizes& used, const StagingSizes& demand,
const StagingSizes& tail, const StagingSizes& capacity) noexcept {
for (unsigned i = 0; i < used.size(); ++i) {
const auto limit = capacity[i] < UINT32_MAX ? capacity[i] : UINT32_MAX;
// The final GPU copy rounds to four bytes. Subtractions avoid wraparound.
const auto alignedLimit = limit & ~uint64_t(3);
if (tail[i] > alignedLimit || used[i] > alignedLimit - tail[i] ||
demand[i] > alignedLimit - tail[i] - used[i]) return false;
}
return true;
}
} // namespace aurora::gfx
+61
View File
@@ -0,0 +1,61 @@
#pragma once
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <mutex>
namespace aurora::gfx {
enum class BufferMapState { Unmapped, Mapping, Mapped };
// The renderer owns request/reset; Dawn may complete a request on another thread.
// An old callback must never publish readiness for a different staging slot.
class StagingMapState {
mutable std::mutex mutex_;
std::condition_variable changed_;
uint64_t generation_ = 0;
BufferMapState state_ = BufferMapState::Unmapped;
public:
uint64_t request() {
std::lock_guard lock(mutex_);
if (state_ != BufferMapState::Unmapped) return 0;
state_ = BufferMapState::Mapping;
return ++generation_;
}
bool complete(uint64_t generation, BufferMapState state) {
{
std::lock_guard lock(mutex_);
if (generation != generation_ || state_ != BufferMapState::Mapping) return false;
state_ = state;
}
changed_.notify_all();
return true;
}
void reset() {
{
std::lock_guard lock(mutex_);
++generation_;
state_ = BufferMapState::Unmapped;
}
changed_.notify_all();
}
BufferMapState state() const {
std::lock_guard lock(mutex_);
return state_;
}
void wait_for_progress() {
std::unique_lock lock(mutex_);
// ProcessEvents is still serviced between waits for implementations that
// need it. A spontaneous completion wakes immediately, without polling.
changed_.wait_for(lock, std::chrono::milliseconds(1),
[&] { return state_ != BufferMapState::Mapping; });
}
};
} // namespace aurora::gfx
+16 -7
View File
@@ -137,7 +137,7 @@ fn gx_z24_at_coord(unclamped_coord: vec2i) -> u32 {
let tex_size = vec2i(textureDimensions(src));
let coord = clamp(unclamped_coord, vec2i(0), tex_size - vec2i(1));
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
: R"(
@@ -373,7 +373,7 @@ static wgpu::BindGroupLayout g_depthBindGroupLayout;
static wgpu::Sampler g_nearestSampler;
static wgpu::Sampler g_linearSampler;
static absl::flat_hash_map<GXTexFmt, wgpu::RenderPipeline> g_pipelines;
static wgpu::RenderPipeline g_blitPipeline;
static absl::flat_hash_map<wgpu::TextureFormat, wgpu::RenderPipeline> g_blitPipelines;
static wgpu::RenderPipeline create_pipeline(const ConvPipeline& conv, const std::string_view shaderPreamble,
const wgpu::BindGroupLayout& bindGroupLayout) {
@@ -487,9 +487,12 @@ void initialize() {
};
g_depthBindGroupLayout = g_device.CreateBindGroupLayout(&depthBindGroupLayoutDescriptor);
g_blitPipeline = create_pipeline(
{GX_TF_RGBA8, FragPassthrough, webgpu::g_graphicsConfig.surfaceConfiguration.format, "TexCopyConv Blit"},
ShaderPreamble, g_bindGroupLayout);
// Native RAM readback uses RGBA even when the EFB/surface uses BGRA.
// Build both variants here; frame workers only read the completed map.
for (const auto format : {wgpu::TextureFormat::RGBA8Unorm, wgpu::TextureFormat::BGRA8Unorm}) {
g_blitPipelines[format] = create_pipeline(
{GX_TF_RGBA8, FragPassthrough, format, "TexCopyConv Blit"}, ShaderPreamble, g_bindGroupLayout);
}
for (const auto& conv : ConvPipelines) {
g_pipelines[conv.fmt] = create_pipeline(conv, ShaderPreamble, g_bindGroupLayout);
if (conv.outputFormat != to_wgpu(conv.fmt)) {
@@ -520,7 +523,7 @@ void initialize() {
void shutdown() {
g_pipelines.clear();
g_blitPipeline = {};
g_blitPipelines.clear();
g_bindGroupLayout = {};
g_depthBindGroupLayout = {};
g_nearestSampler = {};
@@ -602,6 +605,12 @@ void run(const wgpu::CommandEncoder& cmd, const ConvRequest& req) {
execute(cmd, req, it->second);
}
void blit(const wgpu::CommandEncoder& cmd, const ConvRequest& req) { execute(cmd, req, g_blitPipeline); }
void blit(const wgpu::CommandEncoder& cmd, const ConvRequest& req) {
const auto it = g_blitPipelines.find(req.dst->format);
if (it == g_blitPipelines.end()) {
Log.fatal("Unsupported blit destination format {}", static_cast<int>(req.dst->format));
}
execute(cmd, req, it->second);
}
} // namespace aurora::gfx::tex_copy_conv
+87 -21
View File
@@ -30,10 +30,11 @@ using IndexBuffer = std::vector<u16>;
static u32 prepare_idx_template(IndexBuffer& buf, GXPrimitive prim, u16 vtxCount) {
size_t writePos = 0;
if (prim == GX_QUADS) {
// Retain the existing incomplete-quad behavior: every started group emits a complete six-index quad.
buf.resize(((static_cast<u32>(vtxCount) + 3u) / 4u) * 6u);
// GX renders a three-vertex remainder as a triangle. One/two are ignored.
const u32 completeVertices = static_cast<u32>(vtxCount) & ~3u;
buf.resize((completeVertices / 4u) * 6u + (vtxCount % 4u == 3u ? 3u : 0u));
for (u16 v = 0; v < vtxCount; v += 4) {
for (u32 v = 0; v < completeVertices; v += 4) {
const u16 idx0 = v;
const u16 idx1 = static_cast<u16>(v + 1);
const u16 idx2 = static_cast<u16>(v + 2);
@@ -45,15 +46,21 @@ static u32 prepare_idx_template(IndexBuffer& buf, GXPrimitive prim, u16 vtxCount
buf[writePos++] = idx3;
buf[writePos++] = idx0;
}
if (vtxCount % 4u == 3u) {
buf[writePos++] = static_cast<u16>(completeVertices);
buf[writePos++] = static_cast<u16>(completeVertices + 1u);
buf[writePos++] = static_cast<u16>(completeVertices + 2u);
}
} else if (prim == GX_TRIANGLES) {
buf.resize(vtxCount);
for (u16 v = 0; v < vtxCount; ++v) {
const u32 completeVertices = (static_cast<u32>(vtxCount) / 3u) * 3u;
buf.resize(completeVertices);
for (u32 v = 0; v < completeVertices; ++v) {
buf[writePos++] = v;
}
} else if (prim == GX_TRIANGLEFAN) {
const u32 indexCount = vtxCount <= 3 ? vtxCount : 3u + (static_cast<u32>(vtxCount) - 3u) * 3u;
const u32 indexCount = vtxCount < 3 ? 0u : (static_cast<u32>(vtxCount) - 2u) * 3u;
buf.resize(indexCount);
for (u16 v = 0; v < vtxCount; ++v) {
for (u32 v = 0; indexCount != 0 && v < vtxCount; ++v) {
if (v < 3) {
buf[writePos++] = v;
continue;
@@ -63,9 +70,9 @@ static u32 prepare_idx_template(IndexBuffer& buf, GXPrimitive prim, u16 vtxCount
buf[writePos++] = v;
}
} else if (prim == GX_TRIANGLESTRIP) {
const u32 indexCount = vtxCount <= 3 ? vtxCount : 3u + (static_cast<u32>(vtxCount) - 3u) * 3u;
const u32 indexCount = vtxCount < 3 ? 0u : (static_cast<u32>(vtxCount) - 2u) * 3u;
buf.resize(indexCount);
for (u16 v = 0; v < vtxCount; ++v) {
for (u32 v = 0; indexCount != 0 && v < vtxCount; ++v) {
if (v < 3) {
buf[writePos++] = v;
continue;
@@ -88,6 +95,13 @@ static u32 prepare_idx_template(IndexBuffer& buf, GXPrimitive prim, u16 vtxCount
return static_cast<u32>(writePos);
}
// Empty/incomplete draws consume FIFO bytes but cannot produce a primitive.
static bool has_complete_primitive(GXPrimitive prim, u16 count) {
if (prim == GX_POINTS) return count >= 1;
if (prim == GX_LINES || prim == GX_LINESTRIP) return count >= 2;
return count >= 3;
}
// GX FIFO opcodes - use CP_ prefix to avoid clashing with GXCommandList.h macros
static constexpr u8 CP_CMD_NOP = GX_NOP;
static constexpr u8 CP_CMD_LOAD_CP_REG = GX_LOAD_CP_REG;
@@ -466,13 +480,14 @@ static void handle_xf(const u8* data, u32& pos, u32 size, bool bigEndian);
static bool handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndian);
static bool handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian);
void process(const u8* data, u32 size, bool bigEndian) {
uint32_t process(const u8* data, u32 size, bool bigEndian) {
ZoneScoped;
// Everything decoded here mutates renderer state (GX state, the recorded command lists and the mapped staging buffers), so take the renderer GPU mutex once for the whole drain rather than once per draw command.
std::lock_guard gpuLock(aurora::renderer_gpu_mutex());
u32 pos = 0;
while (pos < size) {
const u32 commandStart = pos;
u8 cmd = data[pos++];
u8 opcode = cmd & CP_OPCODE_MASK;
// Log.warn("Processing opcode {:02x} at pos {} (size {})", opcode, pos - 1, size);
@@ -551,12 +566,16 @@ void process(const u8* data, u32 size, bool bigEndian) {
for (int i = GX_VA_POS; i <= GX_VA_TEX7; ++i) {
g_gxState.arrays[i].cachedRange = {};
}
// A merged draw retains its previous array uploads. Force a new draw so
// handle_draw_unmerged observes the invalidation and uploads fresh data.
// Pipeline configuration itself did not change.
g_gxState.stateDirty = true;
break;
}
case GX_LOAD_AURORA: {
if (!handle_aurora(data, pos, size, bigEndian)) {
return;
return size;
}
break;
}
@@ -564,8 +583,10 @@ void process(const u8* data, u32 size, bool bigEndian) {
default:
// Draw commands occupy the full 0x80-0xBF range.
if (is_draw_cmd(cmd)) {
if (!handle_draw(cmd, data, pos, size, bigEndian)) {
return;
try {
if (!handle_draw(cmd, data, pos, size, bigEndian)) return size;
} catch (const gfx::StagingBatchFull&) {
return commandStart;
}
} else {
static u32 unknownLogCount = 0;
@@ -588,6 +609,7 @@ void process(const u8* data, u32 size, bool bigEndian) {
break;
}
}
return size;
}
// Helper to extract bit fields from a 32-bit register
@@ -1848,6 +1870,10 @@ static u32 calculate_last_vtx_size(GXVtxFmt fmt) {
g_gxState.lastVtxFmt = fmt;
g_gxState.lastVtxSize = vtxSize;
// The format is selected by the draw opcode, without a register write.
// Even equal-stride formats may decode bytes differently, so do not merge
// into a draw using the previous format's shader and uniform layout.
g_gxState.stateDirty = true;
return vtxSize;
}
@@ -2080,6 +2106,22 @@ static const CachedPipelineState& resolve_pipeline_state(GXPrimitive prim, GXVtx
return state;
}
static bool admit_draw(GXPrimitive prim, GXVtxFmt fmt, u16 count, uint32_t vertexBytes, bool merged = false) {
const auto& indexTemplate = cached_index_template(prim, count);
gfx::StagingSizes demand{vertexBytes, 0, indexTemplate.indices.size() * sizeof(u16), 0};
if (merged) return gfx::staging_has_space(demand);
const auto& info = resolve_pipeline_state(prim, fmt).shaderInfo;
demand[1] = gfx::staging_uniform_bytes(info.uniformSize);
if (frame_interpolation_identity_needed() && frame_interpolation_replay_safe())
demand[1] *= 1 + MaxInterpolatedFrames;
for (int i = GX_VA_POS; i <= GX_VA_TEX7; ++i) {
if ((g_gxState.vtxDesc[i] == GX_INDEX8 || g_gxState.vtxDesc[i] == GX_INDEX16) &&
g_gxState.arrays[i].cachedRange.size == 0)
demand[3] += gfx::staging_storage_bytes(g_gxState.arrays[i].size);
}
return gfx::staging_has_space(demand);
}
bool submit_raw_draw(GXPrimitive prim, GXVtxFmt fmt, const uint8_t* vertices, uint16_t vtxCount,
uint32_t vertexBytes) {
ZoneScoped;
@@ -2112,8 +2154,17 @@ bool submit_raw_draw(GXPrimitive prim, GXVtxFmt fmt, const uint8_t* vertices, ui
return false;
}
if (!has_complete_primitive(prim, vtxCount)) return true;
// This entry point bypasses process(), so it owns the renderer lock itself.
std::lock_guard gpuLock(aurora::renderer_gpu_mutex());
std::unique_lock gpuLock(aurora::renderer_gpu_mutex());
if (!admit_draw(prim, fmt, vtxCount, vertexBytes)) {
gpuLock.unlock();
gfx::split_staging_batch();
gpuLock.lock();
if (!admit_draw(prim, fmt, vtxCount, vertexBytes))
throw gfx::StagingCapacityError("Raw draw does not fit after capacity submission");
}
const gfx::Range vertRange = gfx::push_verts(vertices, vertexBytes);
const bool interpolationIdentityActive = frame_interpolation_identity_needed();
const PnMtxUsage matrixUsage = interpolationIdentityActive
@@ -2151,17 +2202,32 @@ static bool handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndi
}
// Push raw vertex data to buffer
const uint8_t* vertices = data + pos;
gfx::Range vertRange = gfx::push_verts(vertices, totalVtxBytes);
pos += totalVtxBytes;
if (!has_complete_primitive(prim, vtxCount)) {
pos += totalVtxBytes;
return true;
}
DrawData* mergeTarget = nullptr;
// Decide admission before allocating anything. The merged path needs only
// vertices and indices; it must not resolve pipelines or upload arrays.
// Try to merge with previous draw call
if (!g_gxState.stateDirty) LIKELY {
auto* lastDraw = gfx::get_last_draw_command<DrawData>();
// Only if the previous draw call was a single instance draw (no lines/points handling)
// Expanded lines/points have different vertex interpretation even with one instance.
// Triangle-list output has no restart index; index 65535 is usable.
// Overflow would address earlier vertices instead of the appended geometry.
if (lastDraw != nullptr && prim != GX_LINES && prim != GX_LINESTRIP && prim != GX_POINTS &&
lastDraw->instanceCount == 1) LIKELY {
!lastDraw->expandedPrimitive && lastDraw->instanceCount == 1 &&
uint64_t(lastDraw->vtxCount) +
vtxCount <= 65536u) LIKELY {
mergeTarget = lastDraw;
}
}
if (!admit_draw(prim, fmt, vtxCount, totalVtxBytes, mergeTarget != nullptr)) throw gfx::StagingBatchFull{};
const uint8_t* vertices = data + pos;
gfx::Range vertRange = gfx::push_verts(vertices, totalVtxBytes);
pos += totalVtxBytes;
if (auto* lastDraw = mergeTarget) {
const auto& indexTemplate = cached_index_template(prim, vtxCount);
const auto indices = offset_index_template(indexTemplate, lastDraw->vtxCount);
const u32 numIndices = indexTemplate.indexCount;
@@ -2182,7 +2248,6 @@ static bool handle_draw(u8 cmd, const u8* data, u32& pos, u32 size, bool bigEndi
extend_interpolation_draw(pn_mtx_mask(vertices, vtxCount, vtxSize));
}
return true;
}
}
const bool interpolationIdentityActive = frame_interpolation_identity_needed();
@@ -2278,6 +2343,7 @@ static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount,
.vtxCount = vtxCount,
.indexCount = numIndices,
.instanceCount = instanceCount,
.expandedPrimitive = prim == GX_LINES || prim == GX_LINESTRIP || prim == GX_POINTS,
.bindGroups = bindGroups,
.dstAlpha = pipelineState.dstAlpha,
});
+1 -1
View File
@@ -9,7 +9,7 @@ namespace aurora::gx::fifo {
void reset_cp_register_cache();
// Process a buffer of GX FIFO commands
void process(const uint8_t* data, uint32_t size, bool bigEndian);
uint32_t process(const uint8_t* data, uint32_t size, bool bigEndian);
// Submit already-packed direct vertex bytes against the current GX state.
bool submit_raw_draw(GXPrimitive prim, GXVtxFmt fmt, const uint8_t* vertices, uint16_t vtxCount,
+13 -1
View File
@@ -1,5 +1,6 @@
#include "fifo.hpp"
#include "command_processor.hpp"
#include "../gfx/common.hpp"
#include "../internal.hpp"
#include <chrono>
@@ -81,7 +82,18 @@ void drain() {
if (detail::sBufferSize == 0) {
return;
}
process(detail::sBufferData, detail::sBufferSize, true);
uint32_t consumed = 0;
bool retried = false;
while (consumed < detail::sBufferSize) {
const auto count = process(detail::sBufferData + consumed, detail::sBufferSize - consumed, true);
if (count == 0 && retried)
throw gfx::StagingCapacityError("FIFO draw does not fit after capacity submission");
consumed += count;
if (consumed == detail::sBufferSize) break;
// process returned with its renderer lock released. No recursive drain.
gfx::split_staging_batch();
retried = true;
}
detail::sBufferSize = 0;
}
+15 -2
View File
@@ -143,6 +143,7 @@ private:
};
struct FrameTransformSnapshot {
Mat4x4<float> projection{};
HashType viewportIdentity = 0;
Mat3x4<float> position{};
Mat3x4<float> normal{};
uint16_t usedMatrixMask = 1;
@@ -1185,7 +1186,8 @@ void finalize_frame_interpolation() noexcept {
if ((transform.usedMatrixMask & (1u << slot)) == 0) {
continue;
}
paletteSlotKeys.push_back({transform.indexedMatrices->slotHash[slot], palette, slot});
paletteSlotKeys.push_back({combine_identity(transform.indexedMatrices->slotHash[slot],
transform.viewportIdentity), palette, slot});
}
}
std::sort(paletteSlotKeys.begin(), paletteSlotKeys.end(),
@@ -1446,10 +1448,21 @@ void extend_interpolation_draw(uint16_t usedPnMtxMask) noexcept {
}
std::array<gfx::Range, MaxInterpolatedFrames> record_interpolation_draw(
const FrameInterpolationDrawIdentity& identity, const Mat4x4<float>& projection,
const FrameInterpolationDrawIdentity& drawIdentity, const Mat4x4<float>& projection,
uint16_t usedPnMtxMask, const InterpolatedUniformLayout& uniformLayout) noexcept {
// Split-screen cameras can draw identical meshes in unrelated view spaces.
// Scope exact, material-only and sibling-palette history to the guest viewport.
// Logical coordinates keep render-scale changes out of the camera identity.
const auto& viewport = g_gxState.logicalViewport;
const std::array viewportValues{viewport.left, viewport.top, viewport.width,
viewport.height, viewport.znear, viewport.zfar};
const HashType viewportIdentity = xxh3_hash_s(viewportValues.data(), sizeof(viewportValues));
auto identity = drawIdentity;
identity.combined = combine_identity(identity.combined, viewportIdentity);
identity.pipeline = combine_identity(identity.pipeline, viewportIdentity);
FrameTransformSnapshot snapshot{
.projection = projection,
.viewportIdentity = viewportIdentity,
.usedMatrixMask = usedPnMtxMask,
};
if (uniformLayout.indexedMatrices) {
+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) {
switch (func) {
DEFAULT_FATAL("invalid depth fn {}", underlying(func));
case GX_NEVER:
return wgpu::CompareFunction::Never;
case GX_LESS:
return wgpu::CompareFunction::Less;
return UseReversedZ ? wgpu::CompareFunction::Greater : wgpu::CompareFunction::Less;
case GX_EQUAL:
return wgpu::CompareFunction::Equal;
case GX_LEQUAL:
return wgpu::CompareFunction::LessEqual;
return UseReversedZ ? wgpu::CompareFunction::GreaterEqual : wgpu::CompareFunction::LessEqual;
case GX_GREATER:
return wgpu::CompareFunction::Greater;
return UseReversedZ ? wgpu::CompareFunction::Less : wgpu::CompareFunction::Greater;
case GX_NEQUAL:
return wgpu::CompareFunction::NotEqual;
case GX_GEQUAL:
return wgpu::CompareFunction::GreaterEqual;
return UseReversedZ ? wgpu::CompareFunction::LessEqual : wgpu::CompareFunction::GreaterEqual;
case GX_ALWAYS:
return wgpu::CompareFunction::Always;
}
+10 -1
View File
@@ -436,6 +436,8 @@ struct GXState {
u32 pipelineStateGeneration = next_gx_state_epoch();
std::array<u32, 0x100> bpRegCache = [] {
std::array<u32, 0x100> regs{};
// Force the first GEN_MODE decode without changing its masked reset value.
regs[0x00] = 0xFF000000;
regs[0xFE] = 0x00FFFFFF;
return regs;
}();
@@ -485,7 +487,14 @@ const gfx::TextureBind& get_texture(GXTexMapID id) noexcept;
void resolve_sampled_textures(const ShaderInfo& info) noexcept;
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; }
+1
View File
@@ -13,6 +13,7 @@ struct DrawData {
uint32_t vtxCount;
uint32_t indexCount;
uint32_t instanceCount;
bool expandedPrimitive;
GXBindGroups bindGroups;
uint32_t dstAlpha;
};
+39 -10
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 out.pos = vec4f(clip_base.xy + offset_ndc * clip_base.w, clip_base.zw);";
}
if constexpr (UseReversedZ) {
vtxXfrAttrsPre += "\n out.pos.z = -out.pos.z;";
} else {
vtxXfrAttrsPre += "\n out.pos.z += out.pos.w;";
}
// The near/far depth correction used to be applied here per-vertex (out.pos.z = -out.pos.z for
// reversed, or += out.pos.w for forward), redundantly on top of the same correction already
// folded into ubuf.proj by effective_projection() (shader_info.cpp) - applying it twice canceled
// 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.
vtxXfrAttrsPre +=
"\n let gx_pixel_center_correction = "
@@ -1465,7 +1467,14 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
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 =
fmt::format("u32(round(clamp({}, 0.0, 1.0) * 16777216.0))", fogDepthExpr);
if (usesZTextureDepth) {
@@ -1498,7 +1507,7 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
fragmentFn += fmt::format(
"\n let oldZ = u32(round(clamp({0}, 0.0, 1.0) * 16777216.0));"
"\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;";
fogZCoordExpr = "ztexCoord";
@@ -1639,7 +1648,13 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept {
" @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";
fragmentReturn =
" var out: FragmentOutput;\n"
@@ -1693,8 +1708,22 @@ fn load_u16(p: ptr<storage, array<u32>>, byte_off: u32, le: bool) -> u32 {{
return bswap16(raw, le);
}}
fn load_u24_raw(p: ptr<storage, array<u32>>, byte_off: u32) -> u32 {{
let word_idx = byte_off >> 2u;
let sub = byte_off & 3u;
let word = p[word_idx];
// Three bytes at offsets zero or one fit entirely in this word. Do not
// access the next word: this attribute may end at the binding boundary.
if (sub <= 1u) {{
return (word >> (sub * 8u)) & 0x00FFFFFFu;
}}
let next = p[word_idx + 1u];
let shift = sub * 8u;
return ((word >> shift) | (next << (32u - shift))) & 0x00FFFFFFu;
}}
fn load_u24(p: ptr<storage, array<u32>>, byte_off: u32, le: bool) -> u32 {{
let raw = load_u32_raw(p, byte_off) & 0x00FFFFFFu;
let raw = load_u24_raw(p, byte_off);
if (le) {{
return raw;
}}
@@ -1734,7 +1763,7 @@ fn raw_fetch_u8_2(p: ptr<storage, array<u32>>, byte_off: u32) -> vec2u {{
}}
fn raw_fetch_u8_3(p: ptr<storage, array<u32>>, byte_off: u32) -> vec3u {{
let raw = load_u32_raw(p, byte_off);
let raw = load_u24_raw(p, byte_off);
return vec3u(
extractBits(raw, 0u, 8u),
extractBits(raw, 8u, 8u),
+12 -4
View File
@@ -548,14 +548,22 @@ constexpr size_t kStagedUniformBytes =
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).
//
// 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 {
const auto& vp = g_gxState.renderViewport;
const bool flip = (vp.znear <= vp.zfar) == UseReversedZ;
Mat4x4<float> proj = g_gxState.proj;
if (flip) {
for (size_t i = 0; i < 4; ++i) {
proj.m2.m[i] = -(proj.m2.m[i] + proj.m3.m[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]);
}
return proj;
}
+18 -6
View File
@@ -75,18 +75,30 @@ void initialize() noexcept {
void shutdown() noexcept {
ZoneScoped;
if (g_useSdlRenderer) {
ImGui_ImplSDLRenderer3_Shutdown();
} else {
ImGui_ImplWGPU_Shutdown();
// Startup can fail before either backend initializes. A context alone does
// not mean its renderer/platform backend owns resources to release.
if (ImGui::GetCurrentContext() != nullptr) {
ImGuiIO& io = ImGui::GetIO();
if (io.BackendRendererUserData != nullptr) {
if (g_useSdlRenderer) {
ImGui_ImplSDLRenderer3_Shutdown();
} else {
ImGui_ImplWGPU_Shutdown();
}
}
if (io.BackendPlatformUserData != nullptr) {
ImGui_ImplSDL3_Shutdown();
}
ImGui::DestroyContext();
}
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext();
for (const auto& texture : g_sdlTextures) {
SDL_DestroyTexture(texture);
}
g_sdlTextures.clear();
g_wgpuTextures.clear();
g_useSdlRenderer = false;
g_scale = 0.f;
g_frameDataBuilt = false;
}
void process_event(const SDL_Event& event) noexcept {
+32
View File
@@ -401,6 +401,8 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
return -1;
}
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 ||
(SDL_GetGamepadType(ctrl) == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO && controller.m_pid == 0x2073)) {
controller.m_deadZones.emulateTriggers = false;
@@ -412,6 +414,29 @@ SDL_JoystickID add_controller(SDL_JoystickID which) noexcept {
g_GameControllers[instance] = controller;
ensure_player_index(g_GameControllers[instance]);
apply_port_preferences();
#if defined(SDL_PLATFORM_MACOS)
// First-use convenience only: never override a saved assignment or None.
if (g_portPreferences[0].state == PortPreferenceState::Unset) {
bool hasOtherPortPreference = false;
for (size_t port = 1; port < g_portPreferences.size(); ++port) {
if (g_portPreferences[port].state == PortPreferenceState::Controller &&
identity_match(g_portPreferences[port].identity, controller_identity(g_GameControllers[instance])) !=
IdentityMatch::None) {
hasOtherPortPreference = true;
break;
}
}
if (!hasOtherPortPreference) {
const auto* p0 = get_controller_for_player(0);
if (p0 == nullptr) {
assign_player_index(g_GameControllers[instance], 0);
persist_controller_for_player(0, &g_GameControllers[instance]);
} else if (p0 == &g_GameControllers[instance]) {
persist_controller_for_player(0, &g_GameControllers[instance]);
}
}
}
#endif
return instance;
}
@@ -481,6 +506,13 @@ bool controller_has_rumble(Uint32 instance) noexcept {
void controller_rumble(uint32_t instance, uint16_t low_freq_intensity, uint16_t high_freq_intensity,
uint16_t duration_ms) noexcept {
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);
}
}
+1
View File
@@ -17,6 +17,7 @@ extern Module Log;
struct GameController {
SDL_Gamepad* m_controller = nullptr;
bool m_isGameCube = false;
bool m_gameCubeUseOrdinaryStop = false;
Sint32 m_index = -1;
Sint32 m_playerIndex = -1;
bool m_hasRumble = false;
+3
View File
@@ -122,7 +122,10 @@ auto underlying(T value) -> std::underlying_type_t<T> {
#define UNIMPLEMENTED() FATAL("UNIMPLEMENTED: {}", __FUNCTION__)
namespace wgpu { class CommandBuffer; }
namespace aurora {
void submit_staging_commands(const wgpu::CommandBuffer& commands);
extern AuroraConfig g_config;
extern uint32_t g_sdlCustomEventsStart;
extern char g_gameName[4];
+1 -1
View File
@@ -235,7 +235,7 @@ std::string GetOSVersion() {
constexpr auto name = "iOS";
#elif TARGET_OS_TV
constexpr auto name = "tvOS";
#elif
#else
constexpr auto name = Unknown;
#endif
+16
View File
@@ -570,12 +570,16 @@ bool initialize(AuroraBackend auroraBackend) {
g_adapter = std::move(adapter);
} else {
Log.warn("Adapter request failed: {}", message);
const std::string_view reason{message};
SDL_SetError("Graphics adapter unavailable: %.*s",
static_cast<int>(std::min<size_t>(reason.size(), 512)), reason.data());
}
});
const auto status = g_instance.WaitAny(future, 5000000000);
if (status != wgpu::WaitStatus::Success) {
Log.error("Failed to create {} adapter: {}", magic_enum::enum_name(backend),
magic_enum::enum_name(status));
SDL_SetError("Graphics adapter request did not complete within its startup deadline");
return false;
}
if (!g_adapter) {
@@ -638,6 +642,14 @@ bool initialize(AuroraBackend auroraBackend) {
requiredLimits.maxDynamicStorageBuffersPerPipelineLayout, requiredLimits.maxStorageBuffersPerShaderStage,
requiredLimits.minUniformBufferOffsetAlignment, requiredLimits.minStorageBufferOffsetAlignment);
std::vector<wgpu::FeatureName> requiredFeatures;
// Optional native sharing for MetalFX. Devices without either feature keep
// the normal renderer; the upscaler checks the enabled pair at runtime.
if (backend == wgpu::BackendType::Metal &&
g_adapter.HasFeature(wgpu::FeatureName::SharedTextureMemoryIOSurface) &&
g_adapter.HasFeature(wgpu::FeatureName::SharedFenceMTLSharedEvent)) {
requiredFeatures.push_back(wgpu::FeatureName::SharedTextureMemoryIOSurface);
requiredFeatures.push_back(wgpu::FeatureName::SharedFenceMTLSharedEvent);
}
bool implicitDeviceSynchronizationSupported = false;
wgpu::SupportedFeatures supportedFeatures;
g_adapter.GetFeatures(&supportedFeatures);
@@ -738,11 +750,15 @@ bool initialize(AuroraBackend auroraBackend) {
g_device = std::move(device);
} else {
Log.warn("Device request failed: {}", message);
const std::string_view reason{message};
SDL_SetError("Graphics device unavailable: %.*s",
static_cast<int>(std::min<size_t>(reason.size(), 512)), reason.data());
}
});
const auto status = g_instance.WaitAny(future, 5000000000);
if (status != wgpu::WaitStatus::Success) {
Log.error("Failed to create device: {}", magic_enum::enum_name(status));
SDL_SetError("Graphics device request did not complete within its startup deadline");
return false;
}
if (!g_device) {
+28 -2
View File
@@ -2,6 +2,7 @@
#include <cstring>
#include <ctime>
#include <mutex>
#include <limits>
#include <string>
#include <filesystem>
#include <vector>
@@ -286,8 +287,33 @@ size_t load_from_cache(void const* key, size_t keySize, void* value, size_t valu
if (ret == SQLITE_ROW) {
// Hit
const auto foundPtr = sqlite3_column_blob(load_stmt, 0);
foundSize = sqlite3_column_int64(load_stmt, 1);
const bool compressed = sqlite3_column_int(load_stmt, 2) != 0;
const auto declaredSize = sqlite3_column_int64(load_stmt, 1);
const auto storedSize = sqlite3_column_bytes(load_stmt, 0);
const auto compression = sqlite3_column_int(load_stmt, 2);
const bool compressed = compression == 1;
// Dawn asks for the size before allocating its destination. Validate here,
// not only during the copy: corrupt metadata must become a cache miss.
bool valid = declaredSize > 0 &&
static_cast<uint64_t>(declaredSize) <= std::numeric_limits<size_t>::max() &&
foundPtr != nullptr && storedSize > 0 && (compression == 0 || compression == 1);
if (valid && compressed) {
#if defined(AURORA_CACHE_USE_ZSTD)
// Our writer uses ZSTD_compress, which records the original content size.
const auto frameSize = ZSTD_getFrameContentSize(foundPtr, static_cast<size_t>(storedSize));
valid = frameSize != ZSTD_CONTENTSIZE_ERROR && frameSize != ZSTD_CONTENTSIZE_UNKNOWN &&
frameSize == static_cast<uint64_t>(declaredSize);
#else
valid = false;
#endif
} else if (valid) {
valid = declaredSize == storedSize;
}
if (!valid) {
Log.error("Ignoring cache entry with inconsistent size or compression metadata");
check(sqlite3_reset(load_stmt));
return 0;
}
foundSize = static_cast<size_t>(declaredSize);
if (value == nullptr) {
g_hits.fetch_add(1, std::memory_order_relaxed);
} else {
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <webgpu/webgpu_cpp.h>
#include <memory>
#include <string>
namespace aurora::webgpu::metalfx {
struct Size {
uint32_t inputWidth;
uint32_t inputHeight;
uint32_t outputWidth;
uint32_t outputHeight;
wgpu::TextureFormat format;
bool operator==(const Size&) const = default;
};
// All methods except supported() belong to the serialized frame encoder.
// GPU ownership is explicit: begin_input -> submit input -> upscale ->
// submit output consumption -> end_output. Neither texture may be used by
// Dawn outside its access interval. Destruction retires in-flight resources.
class SpatialScaler {
public:
virtual ~SpatialScaler() = default;
virtual const wgpu::TextureView& input_view() const = 0;
virtual const wgpu::TextureView& output_view() const = 0;
virtual const wgpu::Texture& output_texture() const = 0;
virtual bool begin_input() = 0;
virtual bool upscale() = 0;
virtual bool end_output() = 0;
virtual const std::string& error() const = 0;
};
bool supported(const wgpu::Device& device, wgpu::BackendType backend);
// A null result with no error means the bounded retirement pool is busy;
// skip upscaling for this frame and retry at a later frame boundary.
std::unique_ptr<SpatialScaler> create(const wgpu::Instance& instance,
const wgpu::Device& device, const Size& size,
std::string& error);
} // namespace aurora::webgpu::metalfx
+326
View File
@@ -0,0 +1,326 @@
#include "metalfx.hpp"
#import <Foundation/Foundation.h>
#import <IOSurface/IOSurface.h>
#import <Metal/Metal.h>
#import <MetalFX/MetalFX.h>
#include <dawn/native/MetalBackend.h>
#include <atomic>
#include <string_view>
namespace aurora::webgpu::metalfx {
namespace {
constexpr uint64_t kScheduleTimeoutNs = 1'000'000'000;
// Four current slots plus at most four retiring slots during resize. A busy
// GPU must not allow resize events to allocate unbounded full-resolution images.
constexpr unsigned kMaxLiveResources = 8;
std::atomic<unsigned> g_liveResources{0};
struct SharedImage {
IOSurfaceRef surface = nullptr;
id<MTLTexture> metal;
wgpu::SharedTextureMemory memory;
wgpu::Texture texture;
wgpu::TextureView view;
~SharedImage() { if (surface) CFRelease(surface); }
bool create(const wgpu::Device& device, id<MTLDevice> native, uint32_t width,
uint32_t height, wgpu::TextureFormat format, MTLTextureUsage nativeUsage,
wgpu::TextureUsage usage) {
const bool bgra = format == wgpu::TextureFormat::BGRA8Unorm;
const size_t rowBytes = IOSurfaceAlignProperty(kIOSurfaceBytesPerRow, size_t(width) * 4);
NSDictionary* properties = @{
(id)kIOSurfaceWidth: @(width), (id)kIOSurfaceHeight: @(height),
(id)kIOSurfaceBytesPerElement: @4, (id)kIOSurfaceBytesPerRow: @(rowBytes),
(id)kIOSurfaceAllocSize: @(rowBytes * height),
(id)kIOSurfacePixelFormat: @(bgra ? 0x42475241u : 0x52474241u)
};
surface = IOSurfaceCreate((__bridge CFDictionaryRef)properties);
if (!surface) return false;
auto descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:
bgra ? MTLPixelFormatBGRA8Unorm : MTLPixelFormatRGBA8Unorm
width:width height:height mipmapped:NO];
descriptor.storageMode = MTLStorageModeShared;
descriptor.usage = nativeUsage;
metal = [native newTextureWithDescriptor:descriptor iosurface:surface plane:0];
if (!metal) return false;
wgpu::SharedTextureMemoryIOSurfaceDescriptor io{};
io.ioSurface = surface;
io.allowStorageBinding = false;
wgpu::SharedTextureMemoryDescriptor importDescriptor{};
importDescriptor.nextInChain = &io;
memory = device.ImportSharedTextureMemory(&importDescriptor);
wgpu::SharedTextureMemoryProperties actual{};
if (!memory || memory.GetProperties(&actual) != wgpu::Status::Success ||
actual.format != format || actual.size.width != width || actual.size.height != height ||
(actual.usage & usage) != usage) return false;
wgpu::TextureDescriptor textureDescriptor{};
textureDescriptor.label = "MetalFX shared texture";
textureDescriptor.size = {width, height, 1};
textureDescriptor.format = format;
textureDescriptor.usage = usage;
texture = memory.CreateTexture(&textureDescriptor);
if (!texture) return false;
view = texture.CreateView();
return view != nullptr;
}
};
struct API_AVAILABLE(macos(13.0)) Resources {
SharedImage input, output;
id<MTLFXSpatialScaler> scaler;
id<MTLTexture> privateOutput;
id<MTLCommandQueue> nativeQueue;
id<MTLSharedEvent> event;
wgpu::SharedFence fence;
std::atomic<bool> failed{false};
Resources() { ++g_liveResources; }
~Resources() { --g_liveResources; }
};
class API_AVAILABLE(macos(13.0)) MetalSpatialScaler final : public SpatialScaler {
wgpu::Instance m_instance;
wgpu::Queue m_queue;
std::shared_ptr<Resources> m_resources;
wgpu::SharedTextureMemoryEndAccessState m_outputReleased{};
wgpu::Future m_outputScheduled{};
uint64_t m_value = 0;
std::string m_error;
bool fail(const char* reason) {
m_error = reason;
m_resources->failed = true;
return false;
}
bool wait_scheduled(wgpu::Future future) {
return m_instance.WaitAny(future, kScheduleTimeoutNs) == wgpu::WaitStatus::Success ||
fail("Timed out scheduling MetalFX GPU work");
}
bool end_access(SharedImage& image, wgpu::SharedTextureMemoryEndAccessState& state,
wgpu::Future& scheduled) {
wgpu::SharedTextureMemoryMetalEndAccessState metal{};
state.nextInChain = &metal;
const auto status = image.memory.EndAccess(image.texture, &state);
state.nextInChain = nullptr;
scheduled = metal.commandsScheduledFuture;
return status == wgpu::Status::Success || fail("MetalFX Dawn EndAccess failed");
}
bool begin_access(SharedImage& image, bool initialized, uint64_t value) {
wgpu::SharedTextureMemoryBeginAccessDescriptor access{};
access.initialized = initialized;
if (value) {
access.fenceCount = 1;
access.fences = &m_resources->fence;
access.signaledValueCount = 1;
access.signaledValues = &value;
}
return image.memory.BeginAccess(image.texture, &access) == wgpu::Status::Success ||
fail("MetalFX Dawn BeginAccess failed");
}
bool encode_waits(id<MTLCommandBuffer> commands,
const wgpu::SharedTextureMemoryEndAccessState& state) {
for (size_t i = 0; i < state.fenceCount; ++i) {
wgpu::SharedFenceMTLSharedEventExportInfo metal{};
wgpu::SharedFenceExportInfo info{};
info.nextInChain = &metal;
state.fences[i].ExportInfo(&info);
if (info.type != wgpu::SharedFenceType::MTLSharedEvent || !metal.sharedEvent)
return fail("Dawn did not export a MetalFX shared-event dependency");
[commands encodeWaitForEvent:(__bridge id<MTLSharedEvent>)metal.sharedEvent
value:state.signaledValues[i]];
}
return true;
}
void retain_until_dawn_done() {
// A resize/toggle can destroy this wrapper immediately. The last submitted
// Dawn consumer keeps the IOSurfaces/scaler alive independently of the cache.
m_queue.OnSubmittedWorkDone(wgpu::CallbackMode::AllowSpontaneous,
[resources = m_resources](wgpu::QueueWorkDoneStatus status, wgpu::StringView) {
if (status != wgpu::QueueWorkDoneStatus::Success) resources->failed = true;
});
}
public:
MetalSpatialScaler(const wgpu::Instance& instance, const wgpu::Device& device,
std::shared_ptr<Resources> resources)
: m_instance(instance), m_queue(device.GetQueue()), m_resources(std::move(resources)) {}
const wgpu::TextureView& input_view() const override { return m_resources->input.view; }
const wgpu::TextureView& output_view() const override { return m_resources->output.view; }
const wgpu::Texture& output_texture() const override { return m_resources->output.texture; }
const std::string& error() const override { return m_error; }
bool begin_input() override {
if (m_resources->failed) return fail("Previous MetalFX GPU work failed");
return begin_access(m_resources->input, m_value != 0, m_value);
}
bool upscale() override {
@autoreleasepool {
// Input has already been submitted. Retain it even if an export or native
// allocation fails and the caller immediately falls back to normal copy.
retain_until_dawn_done();
wgpu::SharedTextureMemoryEndAccessState inputReleased{};
wgpu::Future inputScheduled{};
if (!end_access(m_resources->input, inputReleased, inputScheduled) ||
!wait_scheduled(inputScheduled)) return false;
if (m_value && !wait_scheduled(m_outputScheduled)) return false;
id<MTLCommandBuffer> commands = [m_resources->nativeQueue commandBuffer];
if (!commands) return fail("Could not allocate a MetalFX command buffer");
commands.label = @"MetalFX spatial upscale and return to Dawn";
if (!encode_waits(commands, inputReleased) || !encode_waits(commands, m_outputReleased))
return false;
[m_resources->scaler encodeToCommandBuffer:commands];
id<MTLBlitCommandEncoder> blit = [commands blitCommandEncoder];
if (!blit) return fail("Could not allocate the MetalFX output blit");
[blit copyFromTexture:m_resources->privateOutput sourceSlice:0 sourceLevel:0
sourceOrigin:MTLOriginMake(0, 0, 0)
sourceSize:MTLSizeMake(m_resources->privateOutput.width, m_resources->privateOutput.height, 1)
toTexture:m_resources->output.metal destinationSlice:0 destinationLevel:0
destinationOrigin:MTLOriginMake(0, 0, 0)];
[blit endEncoding];
++m_value;
[commands encodeSignalEvent:m_resources->event value:m_value];
const auto resources = m_resources;
[commands addCompletedHandler:^(id<MTLCommandBuffer> completed) {
if (completed.status == MTLCommandBufferStatusError) resources->failed = true;
}];
[commands commit];
// Scheduling is required to order independent Metal queues. Completion
// remains asynchronous; resource reuse is guarded by shared GPU events.
[commands waitUntilScheduled];
if (commands.status == MTLCommandBufferStatusError)
return fail("MetalFX command buffer failed");
return begin_access(m_resources->output, true, m_value);
}
}
bool end_output() override {
retain_until_dawn_done();
m_outputReleased = {};
return end_access(m_resources->output, m_outputReleased, m_outputScheduled);
}
};
// Allocation failures must be caught here rather than reaching Aurora's fatal
// uncaptured-error callback. Scope callbacks own their strings even on timeout.
bool pop_scope(const wgpu::Instance& instance, const wgpu::Device& device, std::string& error) {
auto message = std::make_shared<std::string>();
auto future = device.PopErrorScope(wgpu::CallbackMode::WaitAnyOnly,
[message](wgpu::PopErrorScopeStatus status, wgpu::ErrorType type, wgpu::StringView text) {
if (status != wgpu::PopErrorScopeStatus::Success || type != wgpu::ErrorType::NoError) {
const std::string_view detail{text};
*message = detail.empty() ? "MetalFX texture allocation failed" : std::string(detail);
}
});
if (instance.WaitAny(future, kScheduleTimeoutNs) != wgpu::WaitStatus::Success) {
error = "Timed out checking MetalFX texture allocation";
return false;
}
if (!message->empty()) { error = *message; return false; }
return true;
}
} // namespace
bool supported(const wgpu::Device& device, wgpu::BackendType backend) {
if (@available(macOS 13.0, *)) {
if (!device || backend != wgpu::BackendType::Metal ||
!device.HasFeature(wgpu::FeatureName::SharedTextureMemoryIOSurface) ||
!device.HasFeature(wgpu::FeatureName::SharedFenceMTLSharedEvent)) return false;
auto native = dawn::native::metal::GetMTLDevice(device.Get());
return native && [MTLFXSpatialScalerDescriptor supportsDevice:native];
}
return false;
}
std::unique_ptr<SpatialScaler> create(const wgpu::Instance& instance,
const wgpu::Device& device, const Size& size,
std::string& error) {
error.clear();
if (@available(macOS 13.0, *)) {
@autoreleasepool {
if (!supported(device, wgpu::BackendType::Metal)) {
error = "MetalFX spatial scaling is unsupported";
return {};
}
wgpu::Limits limits{};
device.GetLimits(&limits);
if (!size.inputWidth || !size.inputHeight || size.inputWidth >= size.outputWidth ||
size.inputHeight >= size.outputHeight || size.outputWidth > limits.maxTextureDimension2D ||
size.outputHeight > limits.maxTextureDimension2D ||
(size.format != wgpu::TextureFormat::RGBA8Unorm && size.format != wgpu::TextureFormat::BGRA8Unorm)) {
error = "MetalFX requires smaller input dimensions and an RGBA8/BGRA8 unorm target";
return {};
}
if (g_liveResources.load() >= kMaxLiveResources) {
return {};
}
auto native = dawn::native::metal::GetMTLDevice(device.Get());
auto resources = std::make_shared<Resources>();
auto descriptor = [MTLFXSpatialScalerDescriptor new];
descriptor.inputWidth = size.inputWidth;
descriptor.inputHeight = size.inputHeight;
descriptor.outputWidth = size.outputWidth;
descriptor.outputHeight = size.outputHeight;
descriptor.colorTextureFormat = size.format == wgpu::TextureFormat::BGRA8Unorm
? MTLPixelFormatBGRA8Unorm : MTLPixelFormatRGBA8Unorm;
descriptor.outputTextureFormat = descriptor.colorTextureFormat;
descriptor.colorProcessingMode = MTLFXSpatialScalerColorProcessingModePerceptual;
resources->scaler = [descriptor newSpatialScalerWithDevice:native];
resources->nativeQueue = [native newCommandQueue];
resources->event = [native newSharedEvent];
if (!resources->scaler || !resources->nativeQueue || !resources->event) {
error = "Could not create MetalFX spatial resources";
return {};
}
auto outputDescriptor = [MTLTextureDescriptor
texture2DDescriptorWithPixelFormat:descriptor.outputTextureFormat
width:size.outputWidth height:size.outputHeight mipmapped:NO];
outputDescriptor.storageMode = MTLStorageModePrivate;
outputDescriptor.usage = resources->scaler.outputTextureUsage;
resources->privateOutput = [native newTextureWithDescriptor:outputDescriptor];
if (!resources->privateOutput) { error = "Could not allocate MetalFX private output"; return {}; }
device.PushErrorScope(wgpu::ErrorFilter::Validation);
device.PushErrorScope(wgpu::ErrorFilter::OutOfMemory);
device.PushErrorScope(wgpu::ErrorFilter::Internal);
bool allocated = resources->input.create(device, native, size.inputWidth, size.inputHeight,
size.format, resources->scaler.colorTextureUsage, wgpu::TextureUsage::RenderAttachment);
allocated = allocated && resources->output.create(device, native, size.outputWidth, size.outputHeight,
size.format, MTLTextureUsageShaderRead, wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc);
if (allocated) {
wgpu::SharedFenceMTLSharedEventDescriptor event{};
event.sharedEvent = (__bridge void*)resources->event;
wgpu::SharedFenceDescriptor fence{};
fence.nextInChain = &event;
resources->fence = device.ImportSharedFence(&fence);
allocated = resources->fence != nullptr;
}
for (int i = 0; i < 3; ++i) {
if (!pop_scope(instance, device, error)) allocated = false;
}
if (!allocated) {
if (error.empty()) error = "Could not import MetalFX IOSurface textures into Dawn";
return {};
}
resources->scaler.colorTexture = resources->input.metal;
resources->scaler.outputTexture = resources->privateOutput;
resources->scaler.inputContentWidth = size.inputWidth;
resources->scaler.inputContentHeight = size.inputHeight;
return std::make_unique<MetalSpatialScaler>(instance, device, std::move(resources));
}
}
error = "MetalFX requires macOS 13 or newer";
return {};
}
} // namespace aurora::webgpu::metalfx
+11
View File
@@ -0,0 +1,11 @@
#include "metalfx.hpp"
namespace aurora::webgpu::metalfx {
bool supported(const wgpu::Device&, wgpu::BackendType) { return false; }
std::unique_ptr<SpatialScaler> create(const wgpu::Instance&, const wgpu::Device&,
const Size&, std::string& error) {
error = "MetalFX is not available in this build";
return {};
}
} // namespace aurora::webgpu::metalfx
+27 -1
View File
@@ -46,6 +46,7 @@ SDL_Window* g_window;
SDL_Renderer* g_renderer;
float g_frameBufferScale = 0.f;
bool g_frameBufferAspectFit = true;
std::atomic_bool g_forceAspect169{false};
bool g_presentSurfaceFill = false;
int g_presentAspectWidth = 0;
int g_presentAspectHeight = 0;
@@ -525,7 +526,20 @@ AuroraWindowSize get_window_size() {
int fb_w = native_fb_w;
int fb_h = native_fb_h;
const auto [baseW, baseH] = vi::configured_fb_size();
if (g_frameBufferAspectFit && baseW > 0 && baseH > 0) {
if (g_forceAspect169.load(std::memory_order_acquire) && native_fb_w > 0 && native_fb_h > 0) {
if (g_frameBufferScale > 0.f && baseW > 0 && baseH > 0) {
const auto [scaledW, scaledH] =
scale_frame_buffer_to_aspect(static_cast<int>(baseW), static_cast<int>(baseH),
g_frameBufferScale, 16.f / 9.f);
fb_w = scaledW;
fb_h = scaledH;
} else {
fb_w = std::min(native_fb_w,
std::max(1, static_cast<int>(std::lround(native_fb_h * (16.f / 9.f)))));
fb_h = std::min(native_fb_h,
std::max(1, static_cast<int>(std::lround(native_fb_w * (9.f / 16.f)))));
}
} else if (g_frameBufferAspectFit && baseW > 0 && baseH > 0) {
float renderScale = g_frameBufferScale > 0.f ? g_frameBufferScale : 1.f;
if (g_frameBufferScale <= 0.f) {
renderScale = std::min(static_cast<float>(native_fb_w) / static_cast<float>(baseW),
@@ -759,6 +773,14 @@ void set_frame_buffer_aspect_fit(bool fit) {
request_frame_buffer_resize();
}
void set_force_aspect_16_9(bool force) {
if (g_forceAspect169.load(std::memory_order_relaxed) == force) {
return;
}
g_forceAspect169.store(force, std::memory_order_release);
request_frame_buffer_resize();
}
void set_present_surface_fill(bool fill) {
g_presentSurfaceFill = fill;
}
@@ -781,6 +803,10 @@ void unlock_present_aspect_ratio() {
}
bool get_present_aspect_ratio(float& aspect) noexcept {
if (g_forceAspect169.load(std::memory_order_acquire)) {
aspect = 16.f / 9.f;
return true;
}
if (g_presentSurfaceFill && g_window != nullptr) {
// Queried once per presentation snapshot; use the cached native client size
// instead of re-entering SDL for a value the window procedure already knows.
+1
View File
@@ -56,6 +56,7 @@ void sync_frame_buffer_size() noexcept;
void request_frame_buffer_resize();
void set_frame_buffer_scale(float scale);
void set_frame_buffer_aspect_fit(bool fit);
void set_force_aspect_16_9(bool force);
void set_present_surface_fill(bool fill);
void lock_present_aspect_ratio(int width, int height);
void unlock_present_aspect_ratio();
+14 -1
View File
@@ -5,7 +5,7 @@ if (NOT TARGET gtest)
FetchContent_Declare(googletest
URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.tar.gz
URL_HASH SHA256=65fab701d9829d38cb77c14acdc431d2108bfdbf8979e40eb8ae567edf10b27c
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(googletest)
@@ -18,6 +18,7 @@ if (AURORA_ENABLE_GX)
gx_fifo_test.cpp
gx_test_stubs.cpp
texture_bind_group_cache_key_test.cpp
renderer_regression_test.cpp
../lib/gfx/efb_ram_encoder.cpp
# GX API implementations (encoders)
../lib/dolphin/gx/GXBump.cpp
@@ -66,6 +67,18 @@ if (AURORA_ENABLE_GX)
)
gtest_discover_tests(gx_fifo_tests)
option(AURORA_BUILD_GPU_TESTS "Build renderer pixel tests requiring a graphics device" OFF)
if (AURORA_BUILD_GPU_TESTS)
add_executable(gx_readback_tests gpu_readback_test.cpp)
target_compile_features(gx_readback_tests PRIVATE cxx_std_20)
target_include_directories(gx_readback_tests PRIVATE ../lib)
target_link_libraries(gx_readback_tests PRIVATE
aurora::core aurora::gx aurora::pad aurora::vi aurora::mtx aurora::si
dawn::dawncpp_headers absl::flat_hash_map absl::btree TracyClient)
add_test(NAME gx_readback_tests COMMAND gx_readback_tests "${CMAKE_CURRENT_BINARY_DIR}/readback-cache")
set_tests_properties(gx_readback_tests PROPERTIES TIMEOUT 90)
endif ()
endif () # AURORA_ENABLE_GX
# DVD API tests
+363
View File
@@ -0,0 +1,363 @@
// ROM-free integration probe. Links the actual maintained Aurora renderer.
#include "gfx/common.hpp"
#include "gfx/clear.hpp"
#include "gfx/efb_ram_copy.hpp"
#include "gfx/pipeline_cache.hpp"
#include "gfx/texture.hpp"
#include "gx/gx.hpp"
#include "gx/fifo.hpp"
#include "gx/command_processor.hpp"
#include "gx/frame_interpolation.hpp"
#include <dolphin/gx.h>
#include <aurora/aurora.h>
#include <array>
#include <bit>
#include <atomic>
#include <chrono>
#include <cstdio>
#include <filesystem>
#include <stdexcept>
#include <thread>
#include <vector>
namespace {
using namespace aurora;
std::atomic<unsigned> errors{};
std::atomic<unsigned> guestWrites{};
// Keep destinations alive through shutdown, including any failing wait.
std::array<uint8_t, 16 * 16 * 4 + 32> guarded;
std::array<uint8_t, 16 * 16 * 4 + 32> guardedBake;
void require(bool value, const char* message) {
if (!value) throw std::runtime_error(message);
}
void submit(bool final, bool download = false, bool async = false) {
auto encoder = webgpu::g_device.CreateCommandEncoder();
if (final) gfx::end_frame(encoder); else gfx::end_batch(encoder);
gfx::render(encoder);
if (download) gfx::efb_ram::encode_downloads(encoder);
if (async) gfx::efb_ram::encode_async_downloads(encoder);
auto commands = encoder.Finish();
webgpu::g_queue.Submit(1, &commands);
if (download) require(gfx::efb_ram::complete_downloads(), "EFB readback failed");
gfx::after_submit();
if (!final) require(gfx::resume_frame(), "Batch resume failed");
}
constexpr std::array<std::array<uint8_t, 4>, 4> colors{{
{255, 0, 0, 255}, {0, 255, 0, 255}, {0, 0, 255, 255}, {255, 255, 0, 255}}};
using Pixels = std::vector<uint8_t>;
Pixels expected(unsigned extent) {
Pixels bytes(extent * extent * 4);
// GX RGBA8: 4x4 tiles, sixteen A/R pairs followed by sixteen G/B pairs.
for (unsigned y = 0; y < extent; ++y) for (unsigned x = 0; x < extent; ++x) {
const auto color = colors[y * 4 / extent];
const auto tile = ((y / 4) * (extent / 4) + x / 4) * 64;
const auto pair = ((y % 4) * 4 + x % 4) * 2;
bytes[tile + pair] = color[3]; bytes[tile + pair + 1] = color[0];
bytes[tile + 32 + pair] = color[1]; bytes[tile + 33 + pair] = color[2];
}
return bytes;
}
Pixels run(unsigned splitEvery, bool async = false, bool offscreen = false, unsigned geometry = 0, bool capacityStress = false, bool interpolate = false, bool frameWorker = false, unsigned copyCase = 0) {
require(!async || !offscreen, "Combined probe mode is not supported");
guardedBake.fill(0xa5);
gx::g_gxState.clearColor = {0.f, 0.f, 0.f, 1.f};
require(frameWorker ? aurora_begin_frame() : gfx::begin_frame(), "Frame begin failed");
std::array<std::array<float, 3>, 4> positions{};
if (geometry) {
alignas(32) static std::array<uint8_t, 32768> fifo;
GXInit(fifo.data(), fifo.size());
gx::g_gxState.viewportPolicy = AURORA_VIEWPORT_NATIVE;
GXSetViewport(0.f, 0.f, 64.f, 64.f, 0.f, 1.f);
GXSetScissor(0, 0, 64, 64);
GXSetCullMode(GX_CULL_NONE);
GXSetZMode(false, GX_ALWAYS, false);
GXSetBlendMode(GX_BM_NONE, GX_BL_ONE, GX_BL_ZERO, GX_LO_COPY);
GXSetColorUpdate(true); GXSetAlphaUpdate(true);
GXSetNumTexGens(0); GXSetNumChans(1); GXSetNumTevStages(copyCase ? copyCase : 1);
for (unsigned stage = 1; stage < copyCase; ++stage) {
GXSetTevOrder(static_cast<GXTevStageID>(stage), GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR0A0);
GXSetTevOp(static_cast<GXTevStageID>(stage), GX_PASSCLR);
}
GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR0A0);
GXSetTevOp(GX_TEVSTAGE0, GX_PASSCLR);
GXSetChanCtrl(GX_COLOR0A0, false, GX_SRC_REG, GX_SRC_VTX, GX_LIGHT_NULL, GX_DF_NONE, GX_AF_NONE);
const float projection[]{interpolate ? 0.f : 1.f, 1.f, 0.f, 1.f, 0.f, 0.f, -0.5f};
GXSetProjectionv(projection);
GXClearVtxDesc();
GXSetVtxDesc(GX_VA_POS, geometry == 2 ? GX_INDEX8 : GX_DIRECT);
GXSetVtxDesc(GX_VA_CLR0, GX_DIRECT);
if (geometry == 2) GXSetArray(GX_VA_POS, positions.data(), sizeof(positions), sizeof(positions[0]), true);
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0);
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_CLR0, GX_CLR_RGBA, GX_RGBA8, 0);
gx::fifo::drain();
}
const auto pipeline = gfx::pipeline_ref(gfx::clear::PipelineConfig{});
for (unsigned band = 0; band < 4; ++band) {
const auto c = colors[band];
if (geometry) {
const float top = 1.f - band * 0.5f;
const float bottom = top - 0.5f;
const float z = interpolate ? -1.f : 0.f;
positions = {{{-1.f, top, z}, {1.f, top, z}, {1.f, bottom, z}, {-1.f, bottom, z}}};
// Keep the same array address/format and change only its bytes between draws.
if (geometry == 2) GXInvalidateVtxCache();
if (geometry == 3) {
std::array<uint8_t, 64> raw{};
for (unsigned index = 0; index < positions.size(); ++index) {
for (unsigned axis = 0; axis < 3; ++axis) {
const auto bits = std::bit_cast<uint32_t>(positions[index][axis]);
for (unsigned byte = 0; byte < 4; ++byte)
raw[index * 16 + axis * 4 + byte] = bits >> (24 - byte * 8);
}
std::copy(c.begin(), c.end(), raw.begin() + index * 16 + 12);
}
require(gx::fifo::submit_raw_draw(GX_QUADS, GX_VTXFMT0, raw.data(), 4, raw.size()),
"Raw bridge rejected valid quad");
} else {
GXBegin(GX_QUADS, GX_VTXFMT0, 4);
for (unsigned index = 0; index < positions.size(); ++index) {
if (geometry == 2) GXPosition1x8(index);
else GXPosition3f32(positions[index][0], positions[index][1], positions[index][2]);
GXColor4u8(c[0], c[1], c[2], 255);
}
GXEnd();
}
} else {
gfx::push_draw_command(gfx::clear::DrawData{
.pipeline = pipeline,
.color = {c[0] / 255., c[1] / 255., c[2] / 255., 1.},
.depth = 0.5f,
.useScissor = true,
.scissor = {0, static_cast<int32_t>(band * 16), 64, 16}});
}
if (offscreen && band == 0) {
// Suspend a partially recorded EFB, bake an independently observable copy,
// then resume it before a possible capacity-boundary submission.
gfx::begin_offscreen(64, 64);
gfx::push_draw_command(gfx::clear::DrawData{
.pipeline = pipeline, .color = {1., 0., 1., 1.}, .depth = 0.25f});
if (capacityStress) for (unsigned draw = 0; draw < 24; ++draw) {
gfx::push_draw_command(gfx::clear::DrawData{
.pipeline = pipeline, .color = {1., 0., 1., 1.}, .depth = 0.25f,
.useScissor = true, .scissor = {0, 0, 4, 4}});
}
auto baked = gfx::new_render_texture(64, 64, GX_TF_RGBA8, "Aurora probe offscreen bake");
gfx::resolve_pass(baked, {0, 0, 64, 64}, false, false, false,
{0.f, 0.f, 0.f, 1.f}, 1.f, GX_TF_RGBA8, nullptr, false,
nullptr, false, 1.f, false, false, true);
gfx::efb_ram::schedule(guardedBake.data() + 16, 16, 16, GX_TF_RGBA8, baked);
gfx::end_offscreen();
if (capacityStress) {
require(gfx::efb_ram::prepare_downloads(), "Early bake readback preparation failed");
for (unsigned draw = 0; draw < 24; ++draw) {
gfx::push_draw_command(gfx::clear::DrawData{
.pipeline = pipeline, .color = {1., 0., 0., 1.}, .depth = 0.5f,
.useScissor = true, .scissor = {0, 0, 4, 4}});
}
}
}
if (splitEvery && band < 3 && (band + 1) % splitEvery == 0) submit(false);
}
auto texture = gfx::new_render_texture(64, 64, GX_TF_RGBA8, "Aurora probe persistent copy");
static std::array<uint8_t, 64 * 64 * 4> copyDestination;
if (copyCase) {
gx::fifo::drain();
GXSetTexCopySrc(0, 0, 64, 64);
GXSetTexCopyDst(64, 64, GX_TF_RGBA8, GX_FALSE);
GXCopyTex(copyDestination.data(), GX_FALSE);
texture = gx::g_gxState.copyTextures.at(copyDestination.data()).handle;
} else {
// A partial clear forces the real snapshot and clear-uniform paths after the copy.
gfx::resolve_pass(texture, {0, 0, 64, 64}, true, true, true, {0.f, 0.f, 0.f, 1.f},
1.f, GX_TF_RGBA8, nullptr, false, nullptr, false, 1.f, false, false, true);
}
guarded.fill(0xa5);
const unsigned extent = async ? 4 : 16;
const unsigned bytes = extent * extent * 4;
gfx::efb_ram::schedule(guarded.data() + 16, extent, extent, GX_TF_RGBA8, texture);
const auto before = guestWrites.load(std::memory_order_acquire);
if (async) gfx::efb_ram::seal_async_downloads();
else require(gfx::efb_ram::prepare_downloads(), "Readback preparation failed");
if (frameWorker) {
require(!async && aurora_flush_efb_copies_to_ram(), "Worker-mode EFB readback failed");
aurora_end_frame();
} else submit(true, !async, async);
if (async) {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
while (guestWrites.load(std::memory_order_acquire) == before) {
webgpu::g_instance.ProcessEvents();
require(std::chrono::steady_clock::now() < deadline, "Async readback did not complete");
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
require(std::all_of(guarded.begin(), guarded.begin() + 16, [](auto b) { return b == 0xa5; }) &&
std::all_of(guarded.begin() + 16 + bytes, guarded.end(), [](auto b) { return b == 0xa5; }),
"Readback wrote outside its destination");
if (offscreen) {
require(std::all_of(guardedBake.begin(), guardedBake.begin() + 16, [](auto b) { return b == 0xa5; }) &&
std::all_of(guardedBake.end() - 16, guardedBake.end(), [](auto b) { return b == 0xa5; }),
"Offscreen readback wrote outside its destination");
for (unsigned tile = 0; tile < 16; ++tile) for (unsigned pair = 0; pair < 16; ++pair) {
const auto offset = 16 + tile * 64 + pair * 2;
require(guardedBake[offset] == 255 && guardedBake[offset + 1] == 255 &&
guardedBake[offset + 32] == 0 && guardedBake[offset + 33] == 255,
"Offscreen bake did not preserve expected magenta pixels");
}
}
Pixels pixels(guarded.begin() + 16, guarded.begin() + 16 + bytes);
return pixels;
}
} // namespace
int main(int argc, char** argv) {
if (argc != 2) return 2;
std::filesystem::create_directories(argv[1]);
AuroraConfig config{};
config.appName = "Aurora readback regression tests";
config.userPath = argv[1];
config.cachePath = argv[1];
config.resourcesPath = argv[1];
config.desiredBackend = BACKEND_AUTO;
config.windowWidth = 64;
config.windowHeight = 64;
config.msaa = 1;
config.maxTextureAnisotropy = 1;
config.logLevel = LOG_INFO;
config.logCallback = [](AuroraLogLevel level, const char* module, const char* message, unsigned size) {
if (level >= LOG_ERROR) ++errors;
std::fprintf(stderr, "[%s] %.*s\n", module, static_cast<int>(size), message);
};
const auto initialized = aurora_initialize(1, argv, &config);
if (initialized.initializationStatus != AURORA_INITIALIZATION_SUCCESS) return 3;
aurora_set_skip_unready_pipelines(true);
aurora_set_guest_write_hooks(nullptr, [](const void*, size_t) {
guestWrites.fetch_add(1, std::memory_order_release);
});
try {
const auto prewarmQueued = gfx::queued_pipeline_count();
const auto prewarmDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(60);
while (gfx::queued_pipeline_count() != 0) {
require(std::chrono::steady_clock::now() < prewarmDeadline, "Seeded pipeline prewarm did not finish");
webgpu::g_instance.ProcessEvents();
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
std::printf("Actual Aurora completed seeded startup queue (%u observed pending)\n", prewarmQueued);
const auto control = run(0);
for (unsigned band = 0; band < 4; ++band) {
const auto tile = band * 4 * 64;
std::fprintf(stderr, "band=%u ARGB=%u,%u,%u,%u\n", band, control[tile],
control[tile + 1], control[tile + 32], control[tile + 33]);
}
require(control == expected(16), "Unsplit pixels differ from independently expected GX data");
for (unsigned iteration = 0; iteration < 9; ++iteration) {
const auto splitEvery = iteration % 3 + 1;
require(run(splitEvery) == control, "Split pixels differ from unsplit control");
std::printf("Actual Aurora split=%u iteration=%u matched native tiled readback\n", splitEvery, iteration);
}
for (unsigned iteration = 0; iteration < 9; ++iteration) {
require(run(iteration % 3 + 1, true) == expected(4), "Async pixels differ from expected GX data");
std::printf("Actual Aurora async iteration=%u matched native tiled readback\n", iteration);
}
for (unsigned splitEvery = 0; splitEvery < 4; ++splitEvery) {
require(run(splitEvery, false, true) == expected(16), "Offscreen interlude changed the suspended EFB");
std::printf("Actual Aurora offscreen split=%u preserved bake and suspended EFB\n", splitEvery);
}
for (unsigned splitEvery = 0; splitEvery < 4; ++splitEvery) {
require(run(splitEvery, false, false, true) == expected(16), "GX FIFO quad pixels differ from expected output");
std::printf("Actual Aurora GX FIFO split=%u preserved direct vertices, indices and uniforms\n", splitEvery);
}
for (unsigned splitEvery = 0; splitEvery < 4; ++splitEvery) {
require(run(splitEvery, false, false, 2) == expected(16), "Invalidated GX array pixels differ from expected output");
std::printf("Actual Aurora GX invalidation split=%u refreshed the same array address\n", splitEvery);
}
const gfx::StagingSizes physical{gfx::VertexBufferSize, gfx::UniformBufferSize,
gfx::IndexBufferSize, gfx::StorageBufferSize};
const auto uniformTail = gx::MaxUniformSize + 32 * gfx::staging_uniform_bytes(48);
for (unsigned buffer = 0; buffer < 4; ++buffer) {
auto limits = physical;
limits[buffer] = buffer == 0 ? 128 : buffer == 1 ? uniformTail + 512 :
buffer == 2 ? 24 : 2 * gfx::staging_storage_bytes(48);
gfx::set_staging_capacity_limits_for_testing(limits);
const auto before = gfx::staging_split_count();
require(run(0, false, false, buffer == 1 ? 0 : buffer == 3 ? 2 : 1) == expected(16),
"Automatic capacity split changed pixels");
require(gfx::staging_split_count() > before, "Forced capacity did not split");
const auto highWater = gfx::staging_high_water();
for (unsigned i = 0; i < limits.size(); ++i)
require(highWater[i] <= limits[i], "Actual staging usage exceeded admission budget");
std::printf("Actual staging high-water V/U/I/S=%llu/%llu/%llu/%llu bytes\n",
static_cast<unsigned long long>(highWater[0]), static_cast<unsigned long long>(highWater[1]),
static_cast<unsigned long long>(highWater[2]), static_cast<unsigned long long>(highWater[3]));
std::printf("Actual Aurora automatic capacity buffer=%u splits=%llu matched pixels\n", buffer,
static_cast<unsigned long long>(gfx::staging_split_count() - before));
}
auto limits = physical;
limits[0] = 128;
gfx::set_staging_capacity_limits_for_testing(limits);
require(run(0, false, false, 3) == expected(16), "Raw bridge capacity split changed pixels");
std::puts("Actual Aurora raw bridge capacity split preserved direct quad pixels");
limits = physical;
limits[1] = uniformTail + 768;
gfx::set_staging_capacity_limits_for_testing(limits);
const auto beforeBake = gfx::staging_split_count();
require(run(0, false, true, 0, true) == expected(16),
"Automatic offscreen split changed bake or suspended EFB");
require(gfx::staging_split_count() - beforeBake >= 9, "Offscreen test did not reuse all staging slots");
std::printf("Actual Aurora automatic offscreen/readback splits=%llu preserved all pixels\n",
static_cast<unsigned long long>(gfx::staging_split_count() - beforeBake));
gfx::set_staging_capacity_limits_for_testing(physical);
aurora_set_frame_interpolation_fps(120);
for (unsigned frame = 0; frame < 3; ++frame)
require(run(0, false, false, 2, false, true) == expected(16), "Perspective warmup changed pixels");
AuroraFrameInterpolationDiagnostics interpolation{};
gx::get_frame_interpolation_diagnostics(interpolation);
require(interpolation.matchable > 0 && interpolation.activeSamples > 0,
"Interpolation probe did not establish matching perspective draws");
limits = physical;
limits[3] = 2 * gfx::staging_storage_bytes(48);
gfx::set_staging_capacity_limits_for_testing(limits);
require(run(0, false, false, 2, false, true) == expected(16), "Interpolated split changed native pixels");
gx::get_frame_interpolation_diagnostics(interpolation);
require(!interpolation.replaySafe, "Split frame incorrectly retained interpolation replay");
aurora_set_frame_interpolation_fps(0);
std::puts("Actual Aurora matched perspective interpolation survived capacity split and disabled replay");
limits = physical;
limits[0] = 32; // One quad needs 64 bytes: typed rejection before any draw allocation.
gfx::set_staging_capacity_limits_for_testing(limits);
bool oversized = false;
try { run(0, false, false, 1); }
catch (const gfx::StagingCapacityError&) { oversized = true; }
require(oversized, "Oversized primitive was not rejected");
require(gfx::staging_usage() == gfx::StagingSizes{}, "Oversized primitive partially allocated");
gx::fifo::clear_buffer();
gfx::abort_frame();
gfx::set_staging_capacity_limits_for_testing(physical);
std::puts("Actual Aurora oversized primitive rejected before staging mutation");
require(run(0, false, false, 1) == expected(16), "Renderer failed after rejected primitive cleanup");
limits = physical;
limits[3] = 2 * gfx::staging_storage_bytes(48);
gfx::set_staging_capacity_limits_for_testing(limits);
aurora_set_frame_interpolation_fps(120);
for (unsigned frame = 0; frame < 16; ++frame)
require(run(0, false, false, 2, false, true, true) == expected(16),
"Frame-worker capacity split changed pixels");
// Grant preparation of the next frame before joining DONE, exactly as the
// real producer does; leave no worker waiting for a future begin_frame.
require(aurora_begin_frame(), "Final worker frame preparation failed");
aurora::wait_for_frame_worker();
gfx::abort_frame();
aurora_set_frame_interpolation_fps(0);
gfx::set_staging_capacity_limits_for_testing(physical);
std::puts("Actual Aurora frame worker completed 16 capacity-split perspective frames");
require(errors == 0, "Renderer reported an error");
} catch (const std::exception& error) {
std::fprintf(stderr, "FAIL: %s\n", error.what());
aurora_shutdown();
return 4;
}
aurora_shutdown();
if (errors != 0) return 4;
std::puts("Actual Aurora clear/resolve/snapshot/downsample/readback batches passed");
}
+86 -5
View File
@@ -462,8 +462,18 @@ TEST(FrameInterpolationContract, IndexedPaletteHistoryKeepsAbsoluteVertexSlots)
std::array<uint8_t, uniformSize> changedSource{};
aurora::gx::begin_frame_interpolation();
const auto changedRanges = recordFrame(changedTopology, 91.0f, 9.0f, changedSource);
EXPECT_EQ(changedRanges[0].size, 0u);
// Staging may reserve a copy for sibling matching; the correctness contract
// is that an unmatched topology receives the current pose unchanged.
const auto expectedCurrent = changedSource;
aurora::gx::finalize_frame_interpolation();
EXPECT_EQ(changedSource, expectedCurrent);
if (changedRanges[0].size != 0) {
// No replacement range also correctly selects the original current uniform.
ASSERT_EQ(changedRanges[0].size, uniformSize);
const auto& duplicated = aurora::gfx::testing::uniform_allocation(changedRanges[0].offset);
ASSERT_EQ(duplicated.size(), expectedCurrent.size());
EXPECT_EQ(std::memcmp(duplicated.data(), expectedCurrent.data(), expectedCurrent.size()), 0);
}
aurora::gx::set_frame_interpolation_fps(0);
aurora::gx::begin_frame_interpolation();
@@ -646,12 +656,34 @@ TEST(TevRegisterLivenessContract, PacksOneUniformWhenBothHalvesNeedInitialValue)
auto config = baseline;
config.tevStages[0].colorPass.a = GX_CC_C0;
config.tevStages[0].alphaPass.a = GX_CA_A0;
config.tevStages[0].colorPass.b = GX_CC_KONST;
config.tevStages[0].kcSel = GX_TEV_KCSEL_K0;
const auto baselineInfo = aurora::gx::build_shader_info(baseline);
const auto info = aurora::gx::build_shader_info(config);
EXPECT_TRUE(info.loadsTevRegRgb.test(GX_TEVREG0));
EXPECT_TRUE(info.loadsTevRegAlpha.test(GX_TEVREG0));
EXPECT_EQ(info.uniformSize, baselineInfo.uniformSize + sizeof(aurora::Vec4<float>));
// The final allocation is alignment-rounded, so adding one register need
// not increase it. Verify actual packing with a distinct following K color.
const auto savedReg = g_gxState.colorRegs[GX_TEVREG0];
const auto savedKColor = g_gxState.kcolors[GX_KCOLOR0];
g_gxState.colorRegs[GX_TEVREG0] = {11.f, 22.f, 33.f, 44.f};
g_gxState.kcolors[GX_KCOLOR0] = {55.f, 66.f, 77.f, 88.f};
EXPECT_TRUE(info.sampledKColors.test(GX_KCOLOR0));
aurora::gfx::testing::reset_uniform_allocations();
aurora::gx::build_uniform(info, 0, {}, {}, false);
const auto expectedReg = g_gxState.colorRegs[GX_TEVREG0];
const auto expectedKColor = g_gxState.kcolors[GX_KCOLOR0];
g_gxState.colorRegs[GX_TEVREG0] = savedReg;
g_gxState.kcolors[GX_KCOLOR0] = savedKColor;
const auto& bytes = aurora::gfx::testing::uniform_allocation(0);
const auto* reg = reinterpret_cast<const uint8_t*>(&expectedReg);
const auto found = std::search(bytes.begin(), bytes.end(), reg, reg + sizeof(aurora::Vec4<float>));
ASSERT_NE(found, bytes.end());
const size_t offset = static_cast<size_t>(found - bytes.begin());
ASSERT_LE(offset + 2 * sizeof(aurora::Vec4<float>), bytes.size());
EXPECT_EQ(std::memcmp(bytes.data() + offset + sizeof(aurora::Vec4<float>),
&expectedKColor, sizeof(aurora::Vec4<float>)), 0);
aurora::gfx::testing::reset_uniform_allocations();
}
// BP registers (direct FIFO writes, no dirty state flush needed)
@@ -708,6 +740,52 @@ TEST_F(GXFifoTest, BlendMode_Logic) {
EXPECT_EQ(g_gxState.blendOp, GX_LO_XOR);
}
TEST_F(GXFifoTest, GenMode_FirstZeroWriteDecodesAndRepeatDeduplicates) {
reset_gx_state();
const auto before = g_gxState.pipelineStateGeneration;
decode_fifo(bp_cmd(0, 0));
EXPECT_EQ(g_gxState.numTevStages, 1u);
EXPECT_EQ(g_gxState.cullMode, GX_CULL_NONE);
EXPECT_EQ(g_gxState.numChans, 0u);
EXPECT_EQ(g_gxState.numTexGens, 0u);
EXPECT_EQ(g_gxState.numIndStages, 0u);
EXPECT_EQ(g_gxState.bpRegCache[0], 0u);
EXPECT_NE(g_gxState.pipelineStateGeneration, before);
const auto decoded = g_gxState.pipelineStateGeneration;
decode_fifo(bp_cmd(0, 0));
EXPECT_EQ(g_gxState.pipelineStateGeneration, decoded);
}
TEST_F(GXFifoTest, GenMode_FirstMaskedWritePreservesZeroResetBits) {
for (const u32 mask : {0u, 1u << 10}) {
reset_gx_state();
const auto before = g_gxState.pipelineStateGeneration;
decode_fifo(bp_cmd(0xFE, mask));
decode_fifo(bp_cmd(0, 0xFFFFFF));
EXPECT_EQ(g_gxState.bpRegCache[0], mask);
EXPECT_EQ(g_gxState.bpRegCache[0xFE], 0xFFFFFFu);
EXPECT_EQ(g_gxState.numTevStages, mask ? 2u : 1u);
EXPECT_EQ(g_gxState.cullMode, GX_CULL_NONE);
EXPECT_NE(g_gxState.pipelineStateGeneration, before);
decode_fifo(bp_cmd(0, 0));
EXPECT_EQ(g_gxState.numTevStages, 1u);
EXPECT_EQ(g_gxState.bpRegCache[0], 0u);
}
}
TEST_F(GXFifoTest, GenMode_ColdSingleStageApiSetupDecodes) {
reset_gx_state();
GXSetNumTevStages(1);
GXSetNumTexGens(0);
GXSetNumChans(0);
GXSetCullMode(GX_CULL_NONE);
const auto bytes = flush_and_capture();
decode_fifo(bytes);
EXPECT_EQ(g_gxState.numTevStages, 1u);
EXPECT_EQ(g_gxState.cullMode, GX_CULL_NONE);
}
TEST_F(GXFifoTest, BpMask_AppliesOnlyToNextWrite) {
std::vector<u8> bytes;
auto mask = bp_cmd(0xFE, 1u << 19);
@@ -2179,6 +2257,7 @@ TEST_F(GXFifoTest, DrawTopologyTemplatesPreserveExactGxIndexOrder) {
const auto decodeAndReadIndices = [&](GXPrimitive primitive, u16 count) {
std::vector<u8> fifo;
append_test_draw(fifo, primitive, count);
aurora::gfx::testing::reset_vertex_push_record();
decode_fifo(fifo);
return aurora::gfx::testing::last_pushed_indices();
};
@@ -2193,7 +2272,7 @@ TEST_F(GXFifoTest, DrawTopologyTemplatesPreserveExactGxIndexOrder) {
(std::vector<u16>{0, 1, 2, 0, 2, 3, 0, 3, 4}));
g_gxState.stateDirty = true;
EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLEFAN, 2),
(std::vector<u16>{0, 1}));
(std::vector<u16>{}));
g_gxState.stateDirty = true;
EXPECT_EQ(decodeAndReadIndices(GX_TRIANGLESTRIP, 6),
(std::vector<u16>{0, 1, 2, 2, 1, 3, 2, 3, 4, 4, 3, 5}));
@@ -4166,7 +4245,9 @@ TEST_F(GXFifoTest, CopyTexClearTruePassesScratchRectAndUpdateMasksToResolve) {
EXPECT_NEAR(resolve.clearColorValue.y(), 128.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(resolve.clearColorValue.z(), 192.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(resolve.clearColorValue.w(), 32.f / 255.f, 1.f / 255.f);
EXPECT_NEAR(resolve.clearDepthValue, 0x123456 / 16777216.f, 1.f / 16777216.f);
const float gxDepth = 0x123456 / 16777216.f;
EXPECT_NEAR(resolve.clearDepthValue, aurora::gx::UseReversedZ ? 1.f - gxDepth : gxDepth,
1.f / 16777216.f);
EXPECT_EQ(resolve.resolveFormat, GX_TF_RGBA8);
EXPECT_FALSE(resolve.halfScale);
EXPECT_FALSE(resolve.forceOpaqueAlpha);
+4
View File
@@ -299,6 +299,10 @@ std::pair<ByteBuffer, Range> copy_uniform(Range source) {
return map_uniform(source.size);
}
uint32_t align_uniform(uint32_t value) { return (value + 255u) & ~255u; }
uint64_t staging_uniform_bytes(uint64_t value) { return staging_padded(value, 256); }
uint64_t staging_storage_bytes(uint64_t value) { return staging_padded(value, 256); }
bool staging_has_space(const StagingSizes&) { return true; }
void split_staging_batch() { throw StagingCapacityError("Unexpected split in FIFO unit test"); }
Vec2<uint32_t> get_render_target_size() noexcept { return s_renderTargetSize; }
Vec2<uint32_t> get_frame_buffer_size() noexcept { return s_renderTargetSize; }
@@ -0,0 +1,28 @@
cmake_minimum_required(VERSION 3.25)
project(metalfx_interop LANGUAGES CXX)
if(NOT APPLE)
message(FATAL_ERROR "The MetalFX interoperability probe requires macOS")
endif()
enable_language(OBJCXX)
set(CMAKE_OBJCXX_STANDARD 20)
set(CMAKE_OBJCXX_STANDARD_REQUIRED ON)
set(CMAKE_OSX_DEPLOYMENT_TARGET 13.0)
# Use the same Dawn package as Aurora; no separate download or renderer build.
find_package(Threads REQUIRED)
find_package(Dawn CONFIG REQUIRED)
add_executable(metalfx_interop main.mm ../../lib/webgpu/metalfx.mm)
target_include_directories(metalfx_interop PRIVATE ../../lib)
target_compile_options(metalfx_interop PRIVATE -fobjc-arc -Wall -Wextra)
target_link_libraries(metalfx_interop PRIVATE dawn::webgpu_dawn
"-framework MetalFX" "-framework Metal" "-framework IOSurface" "-framework Foundation")
enable_testing()
add_test(NAME metalfx_interop COMMAND metalfx_interop)
set_tests_properties(metalfx_interop PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
add_executable(metalfx_stub_test stub_test.cpp ../../lib/webgpu/metalfx_stub.cpp)
target_include_directories(metalfx_stub_test PRIVATE ../../lib)
target_compile_features(metalfx_stub_test PRIVATE cxx_std_20)
target_link_libraries(metalfx_stub_test PRIVATE dawn::webgpu_dawn)
add_test(NAME metalfx_stub COMMAND metalfx_stub_test)
+124
View File
@@ -0,0 +1,124 @@
# MetalFX spatial upscaling: renderer integration and tests
Aurora can now upscale the completed game image with MetalFX before aspect-fit
presentation and ImGui composition. It is opt-in and requires macOS 13+, a
supported Metal device, and Dawn IOSurface/shared-event support. Other backends
and builds without the MetalFX SDK use a stub and the existing presentation path.
The MetalFX framework is weak-linked; the game's deployment target is unchanged.
## Trying the renderer integration
Use F10 → Graphics → MetalFX spatial upscaling. Then use the existing
Resolution control to render below the output viewport's size.
Both source dimensions must be smaller than the output dimensions. Equal-size
rendering, supersampling, and unsupported source formats bypass MetalFX. In
particular, Auto (window size) generally offers no upscaling opportunity.
The F10 toggle is saved in `Config.toml` as
`video.metalfx_spatial_upscaling`. It uses these thread-safe Aurora entry points:
- `aurora_set_metalfx_spatial(bool)` requests a change at the next sealed frame.
- `aurora_get_metalfx_spatial()` returns the requested setting.
- `aurora_is_metalfx_spatial_supported()` reports device/build support.
- `aurora_get_metalfx_status()` distinguishes Disabled, Unsupported,
Not Upscaling, Active, and Error. A busy resize-retirement pool temporarily
bypasses upscaling and retries on a later frame. Other upscaler errors log a
reason and use normal presentation until a disabled frame resets the error.
The game's HUD is part of the source image and is upscaled. ImGui/F10/FPS overlays
are composed afterward at output resolution. Existing source-frame captures
still capture the original source image. The interpolation snapshot call sites
all use the same upscaling hook; game-specific interpolation remains untested.
## GPU path and ownership
1. Request Dawn's `SharedTextureMemoryIOSurface` and `SharedFenceMTLSharedEvent`
features when the Metal adapter supports both. Use that Dawn device's native
`MTLDevice`, not a separately selected default device.
2. Cache MaxInterpolatedFrames + 1 upscaling slots with IOSurface-backed input and output textures,
a spatial scaler, a private MetalFX output, and shared-event dependencies.
Check texture formats, dimensions, usages, and device size limits on creation.
3. Begin Dawn input access, copy the completed game image at its source size,
and submit the scene plus copy. End input access and wait for Dawn's
`commandsScheduledFuture` before submitting dependent native Metal work.
4. On the native queue, wait for input rendering and any prior Dawn consumption
of the shared output. Encode MetalFX into its required **private** output
texture, then GPU-blit the result into the output IOSurface and signal an event.
5. After native scheduling, begin Dawn output access with that event/value.
Composite the upscaled image into the existing content viewport, retaining
letterboxing, then draw ImGui. Submit and end output access. Reuse observes
both Dawn-to-Metal and Metal-to-Dawn event dependencies.
6. GPU completion callbacks retain resources after a cache entry is replaced,
disabled, or shut down. At most eight resource sets may exist (four current
plus four retiring); rapid resizing cannot allocate an unbounded queue.
CPU scheduling waits remain, but there are no CPU image transfers or per-frame
GPU-completion waits in the upscaling path. There is one source-size GPU copy
and one full-output GPU blit. Their cost must be measured before promising a
performance gain. The input copy follows the existing perceptual/unorm sampling
path. sRGB texture formats bypass MetalFX to avoid implicit color conversion.
## Standalone GPU regression test
The test builds the actual `lib/webgpu/metalfx.mm` implementation. Point
`Dawn_DIR` at the package used by an existing Aurora build:
```sh
cmake -S aurora-main/tests/metalfx_interop -B build-metalfx-interop \
-DDawn_DIR="/absolute/path/to/dawn_prebuilt-src/lib/cmake/Dawn" \
-DCMAKE_BUILD_TYPE=Release
cmake --build build-metalfx-interop
MTL_DEBUG_LAYER=1 MTL_SHADER_VALIDATION=1 \
ctest --test-dir build-metalfx-interop --output-on-failure -V
```
The GPU test returns 77 (CTest **Skipped**) when no Metal adapter, required
sharing features, or spatial scaler is available. A skip is not evidence of
interoperability. Sandboxed processes may need GPU access. CTest imposes a
60-second timeout. The separate stub test needs no GPU.
Tested on Apple M3, macOS 26.5.1, using Aurora's existing Dawn package
(`v20260603.191052`). Metal API and GPU validation were enabled:
| Formats | Input | Output | Frames per format |
| --- | --- | --- | --- |
| RGBA8Unorm, BGRA8Unorm | 64 × 48 | 128 × 96 | 24 |
| RGBA8Unorm, BGRA8Unorm | 320 × 180 | 480 × 270 | 24 |
| RGBA8Unorm, BGRA8Unorm | 960 × 540 | 1920 × 1080 | 24 |
All 144 frames and 2,304 interior pixel samples passed. Red changes per frame;
green and blue distinguish left/right and top/bottom. All channels are checked
within five 8-bit levels, catching stale images, orientation/channel mistakes,
and missing output. Tests cover 1.5× and 2× scaling, padded readback rows, slot
reuse, dropping wrappers before readback completion, invalid dimensions/sRGB
formats, the eight-set allocation bound, and the unavailable-backend stub.
Readback is only the test oracle and is absent from the game upscaling path.
These samples do not measure reconstruction quality at edges or race performance.
## Windowed presentation test
This optional target exercises Aurora's actual frame submission and presentation
with a synthetic source and an ImGui overlay. It requires no Wii game data and
creates an automatically closing test window. Add the option to an existing
from-source runtime build (the normal dependency/provider options still apply):
```sh
cmake -S runtime -B build-macos -DCMAKE_BUILD_TYPE=Release \
-DAURORA_BUILD_METALFX_PRESENTATION_TEST=ON
cmake --build build-macos --target metalfx_presentation_test
MTL_DEBUG_LAYER=1 MTL_SHADER_VALIDATION=1 \
./build-macos/aurora-build/metalfx_presentation_test
```
On the same M3, all 84 frames passed with Metal API/GPU validation: disabled,
enabled, window resize, 4:3/16:9 aspect changes, native-size bypass, disable, and
re-enable. Assertions check renderer status and errors; this is not a pixel-level
verification of the window image. The core build and macOS 12 deployment-target
availability compilation also passed. Full Mario Kart gameplay, race performance,
visual quality, Intel Macs, other Apple GPUs, older macOS runtime versions, and
non-macOS full builds remain untested.
References: [Apple MetalFX](https://developer.apple.com/documentation/metalfx),
[spatial scaler requirements](https://developer.apple.com/documentation/metalfx/mtlfxspatialscaler),
and the installed Dawn `MetalBackend.h` / `webgpu_cpp.h` APIs.
+269
View File
@@ -0,0 +1,269 @@
#import <Foundation/Foundation.h>
#import <IOSurface/IOSurface.h>
#import <Metal/Metal.h>
#include "webgpu/metalfx.hpp"
#include <dawn/native/MetalBackend.h>
#include <webgpu/webgpu_cpp.h>
#include <array>
#include <atomic>
#include <cmath>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string_view>
#include <vector>
namespace {
constexpr uint64_t kTimeoutNs = 10'000'000'000;
constexpr unsigned kFrames = 24;
std::atomic<unsigned> g_errors{0};
void require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
void wait(const wgpu::Instance& instance, wgpu::Future future) {
require(instance.WaitAny(future, kTimeoutNs) == wgpu::WaitStatus::Success,
"Dawn operation timed out or failed");
}
void runCase(const wgpu::Instance& instance, const wgpu::Device& device,
bool bgra, uint32_t width, uint32_t height,
uint32_t outWidth, uint32_t outHeight) {
const auto format = bgra ? wgpu::TextureFormat::BGRA8Unorm : wgpu::TextureFormat::RGBA8Unorm;
using namespace aurora::webgpu::metalfx;
std::array<std::unique_ptr<SpatialScaler>, 3> slots;
for (auto& slot : slots) {
std::string error;
slot = create(instance, device, {width, height, outWidth, outHeight, format}, error);
if (!slot) {
throw std::runtime_error(error.empty()
? "MetalFX resource pool was still busy retiring earlier slots"
: error);
}
}
// Asymmetric quadrants expose channel swaps, vertical flips, and stale frames.
wgpu::ShaderSourceWGSL source{};
source.code = R"(
@group(0) @binding(0) var<uniform> params: vec4f;
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
let p = array(vec2f(-1, -1), vec2f(3, -1), vec2f(-1, 3));
return vec4f(p[i], 0, 1);
}
@fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
return vec4f(params.x, select(0.2, 0.8, p.x >= params.y / 2),
select(0.3, 0.7, p.y >= params.z / 2), 1);
}
)";
wgpu::ShaderModuleDescriptor shaderDescriptor{};
shaderDescriptor.nextInChain = &source;
auto shader = device.CreateShaderModule(&shaderDescriptor);
wgpu::ColorTargetState target{};
target.format = format;
wgpu::FragmentState fragment{};
fragment.module = shader;
fragment.entryPoint = "fs";
fragment.targetCount = 1;
fragment.targets = &target;
wgpu::RenderPipelineDescriptor pipelineDescriptor{};
pipelineDescriptor.vertex.module = shader;
pipelineDescriptor.vertex.entryPoint = "vs";
pipelineDescriptor.fragment = &fragment;
auto pipeline = device.CreateRenderPipeline(&pipelineDescriptor);
auto dawnQueue = device.GetQueue();
const uint32_t bytesPerRow = (outWidth * 4 + 255) & ~255u;
const uint64_t readbackSize = uint64_t(bytesPerRow) * outHeight;
std::vector<wgpu::Buffer> readbacks;
for (unsigned frame = 0; frame < kFrames; ++frame) {
auto& slot = slots[frame % slots.size()];
require(slot->begin_input(), "Production MetalFX begin_input failed");
const std::array<float, 4> params{0.2f + float(frame % 5) * 0.1f,
float(width), float(height), 0};
wgpu::BufferDescriptor uniformDescriptor{};
uniformDescriptor.size = sizeof(params);
uniformDescriptor.usage = wgpu::BufferUsage::Uniform | wgpu::BufferUsage::CopyDst;
auto uniform = device.CreateBuffer(&uniformDescriptor);
dawnQueue.WriteBuffer(uniform, 0, params.data(), sizeof(params));
wgpu::BindGroupEntry entry{};
entry.binding = 0;
entry.buffer = uniform;
entry.size = sizeof(params);
wgpu::BindGroupDescriptor bindDescriptor{};
bindDescriptor.layout = pipeline.GetBindGroupLayout(0);
bindDescriptor.entryCount = 1;
bindDescriptor.entries = &entry;
auto bindGroup = device.CreateBindGroup(&bindDescriptor);
auto encoder = device.CreateCommandEncoder();
wgpu::RenderPassColorAttachment attachment{};
attachment.view = slot->input_view();
attachment.loadOp = wgpu::LoadOp::Clear;
attachment.storeOp = wgpu::StoreOp::Store;
wgpu::RenderPassDescriptor passDescriptor{};
passDescriptor.colorAttachmentCount = 1;
passDescriptor.colorAttachments = &attachment;
auto pass = encoder.BeginRenderPass(&passDescriptor);
pass.SetPipeline(pipeline);
pass.SetBindGroup(0, bindGroup);
pass.Draw(3);
pass.End();
auto render = encoder.Finish();
dawnQueue.Submit(1, &render);
if (!slot->upscale()) throw std::runtime_error(slot->error());
wgpu::BufferDescriptor readbackDescriptor{};
readbackDescriptor.size = readbackSize;
readbackDescriptor.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead;
auto readback = device.CreateBuffer(&readbackDescriptor);
encoder = device.CreateCommandEncoder();
wgpu::TexelCopyTextureInfo copySource{};
copySource.texture = slot->output_texture();
wgpu::TexelCopyBufferInfo destination{};
destination.buffer = readback;
destination.layout.bytesPerRow = bytesPerRow;
destination.layout.rowsPerImage = outHeight;
const wgpu::Extent3D extent{outWidth, outHeight, 1};
encoder.CopyTextureToBuffer(&copySource, &destination, &extent);
auto copy = encoder.Finish();
dawnQueue.Submit(1, &copy);
if (!slot->end_output()) throw std::runtime_error(slot->error());
readbacks.push_back(std::move(readback));
}
// Model toggle/resize immediately after submission, while either queue may
// still be consuming these textures. Production completion callbacks must
// keep the resources alive after the cache drops its wrappers.
slots = {};
// Readback is only the test oracle. No CPU image transfer or GPU completion
// wait occurs between Dawn rendering, MetalFX, and Dawn consumption above.
for (unsigned frame = 0; frame < kFrames; ++frame) {
bool mapped = false;
auto& readback = readbacks[frame];
wait(instance, readback.MapAsync(wgpu::MapMode::Read, 0, readbackSize,
wgpu::CallbackMode::WaitAnyOnly, [&mapped](wgpu::MapAsyncStatus status, wgpu::StringView) {
mapped = status == wgpu::MapAsyncStatus::Success;
}));
require(mapped, "Output readback mapping failed");
const auto* bytes = static_cast<const uint8_t*>(readback.GetConstMappedRange());
require(bytes != nullptr, "Output readback pointer is null");
for (unsigned y = 0; y < 4; ++y) {
for (unsigned x = 0; x < 4; ++x) {
const unsigned px = (2 * x + 1) * outWidth / 8;
const unsigned py = (2 * y + 1) * outHeight / 8;
const auto* pixel = bytes + py * bytesPerRow + px * 4;
const std::array<float, 4> expected{
0.2f + float(frame % 5) * 0.1f, x >= 2 ? 0.8f : 0.2f,
y >= 2 ? 0.7f : 0.3f, 1};
for (unsigned c = 0; c < 4; ++c) {
const unsigned channel = bgra && c != 1 && c != 3 ? 2 - c : c;
if (std::abs(int(pixel[channel]) - int(std::lround(expected[c] * 255))) > 5) {
std::cerr << "Pixel mismatch: frame=" << frame << " x=" << px << " y=" << py
<< " channel=" << c << " actual=" << int(pixel[channel])
<< " expected=" << std::lround(expected[c] * 255) << '\n';
throw std::runtime_error("MetalFX output failed image validation");
}
}
}
}
readback.Unmap();
}
require(g_errors.load() == 0, "Dawn reported validation errors or device loss");
std::cout << "PASS " << (bgra ? "BGRA8" : "RGBA8") << ' ' << width << 'x' << height
<< " -> " << outWidth << 'x' << outHeight << ": " << kFrames
<< " frames, 3 reused slots, 16 pixel samples/frame\n";
}
int run() {
const wgpu::InstanceFeatureName timedWait = wgpu::InstanceFeatureName::TimedWaitAny;
wgpu::InstanceDescriptor instanceDescriptor{};
instanceDescriptor.requiredFeatureCount = 1;
instanceDescriptor.requiredFeatures = &timedWait;
auto instance = wgpu::CreateInstance(&instanceDescriptor);
require(instance != nullptr, "Dawn instance creation failed");
wgpu::Adapter adapter;
wgpu::RequestAdapterOptions options{};
options.backendType = wgpu::BackendType::Metal;
wait(instance, instance.RequestAdapter(&options, wgpu::CallbackMode::WaitAnyOnly,
[&adapter](wgpu::RequestAdapterStatus status, wgpu::Adapter result, wgpu::StringView message) {
if (status == wgpu::RequestAdapterStatus::Success) adapter = std::move(result);
else std::cerr << "Adapter: " << std::string_view(message) << '\n';
}));
if (!adapter) { std::cout << "SKIP: no Dawn Metal adapter\n"; return 77; }
const std::array features{wgpu::FeatureName::SharedTextureMemoryIOSurface,
wgpu::FeatureName::SharedFenceMTLSharedEvent};
for (auto feature : features) {
if (!adapter.HasFeature(feature)) {
std::cout << "SKIP: Dawn adapter lacks IOSurface/shared-event interoperability\n";
return 77;
}
}
wgpu::DeviceDescriptor descriptor{};
descriptor.requiredFeatureCount = features.size();
descriptor.requiredFeatures = features.data();
descriptor.SetUncapturedErrorCallback(
[](const wgpu::Device&, wgpu::ErrorType, wgpu::StringView message) {
++g_errors;
std::cerr << "Dawn error: " << std::string_view(message) << '\n';
});
descriptor.SetDeviceLostCallback(wgpu::CallbackMode::AllowSpontaneous,
[](const wgpu::Device&, wgpu::DeviceLostReason reason, wgpu::StringView message) {
if (reason != wgpu::DeviceLostReason::Destroyed) {
++g_errors;
std::cerr << "Device lost: " << std::string_view(message) << '\n';
}
});
wgpu::Device device;
wait(instance, adapter.RequestDevice(&descriptor, wgpu::CallbackMode::WaitAnyOnly,
[&device](wgpu::RequestDeviceStatus status, wgpu::Device result, wgpu::StringView message) {
if (status == wgpu::RequestDeviceStatus::Success) device = std::move(result);
else std::cerr << "Device: " << std::string_view(message) << '\n';
}));
require(device != nullptr, "Dawn device creation failed");
id<MTLDevice> native = dawn::native::metal::GetMTLDevice(device.Get());
require(native != nil, "Dawn native Metal device is unavailable");
std::cout << "GPU: " << native.name.UTF8String << '\n';
if (!aurora::webgpu::metalfx::supported(device, wgpu::BackendType::Metal)) {
std::cout << "SKIP: GPU does not support MetalFX spatial scaling\n";
return 77;
}
require(!aurora::webgpu::metalfx::supported(device, wgpu::BackendType::Vulkan),
"MetalFX must reject non-Metal backends");
std::string error;
require(!aurora::webgpu::metalfx::create(instance, device,
{128, 96, 128, 96, wgpu::TextureFormat::RGBA8Unorm}, error) && !error.empty(),
"MetalFX must reject equal-size input/output");
require(!aurora::webgpu::metalfx::create(instance, device,
{128, 96, 256, 192, wgpu::TextureFormat::RGBA8UnormSrgb}, error),
"MetalFX must reject implicit sRGB conversion");
{
using namespace aurora::webgpu::metalfx;
std::array<std::unique_ptr<SpatialScaler>, 8> resources;
for (auto& scaler : resources) {
scaler = create(instance, device, {64, 48, 128, 96, wgpu::TextureFormat::RGBA8Unorm}, error);
require(scaler != nullptr, "Could not fill the MetalFX resource pool");
}
require(!create(instance, device, {64, 48, 128, 96, wgpu::TextureFormat::RGBA8Unorm}, error)
&& error.empty(), "A full retirement pool must defer allocation without a fatal error");
}
for (bool bgra : {false, true}) {
runCase(instance, device, bgra, 64, 48, 128, 96);
runCase(instance, device, bgra, 320, 180, 480, 270);
runCase(instance, device, bgra, 960, 540, 1920, 1080);
}
return 0;
}
} // namespace
int main() {
@autoreleasepool {
try { return run(); }
catch (const std::exception& error) {
std::cerr << "FAIL: " << error.what() << '\n';
return 1;
}
}
}
@@ -0,0 +1,124 @@
// Explicitly opted-in windowed test of Aurora's real frame/presentation path.
// No Wii game data is needed; the source override supplies a synthetic image.
#include <aurora/aurora.h>
#include <imgui.h>
#include <SDL3/SDL_timer.h>
#include "webgpu/gpu.hpp"
#include "window.hpp"
#include <atomic>
#include <chrono>
#include <cstdio>
#include <filesystem>
#include <stdexcept>
#include <string_view>
#include <system_error>
#include <vector>
namespace {
std::atomic<unsigned> g_errors{0};
void log_message(AuroraLogLevel level, const char* module, const char* message, unsigned len) {
if (level >= LOG_ERROR) ++g_errors;
if (level >= LOG_WARNING || std::string_view(message, len).find("MetalFX") != std::string_view::npos)
std::fprintf(stderr, "[%s] %.*s\n", module, static_cast<int>(len), message);
if (level == LOG_FATAL) std::abort();
}
void require(bool value, const char* message) {
if (!value) throw std::runtime_error(message);
}
void draw_frames(uint32_t width, uint32_t height, AuroraMetalFXStatus expected) {
using namespace aurora::webgpu;
auto source = create_render_texture(width, height, false);
auto bindGroup = create_copy_bind_group(source);
std::vector<uint32_t> pixels(size_t(width) * height);
for (uint32_t y = 0; y < height; ++y) {
for (uint32_t x = 0; x < width; ++x) {
pixels[size_t(y) * width + x] = 0xff000000u | ((x / 16 % 2) ? 0x00bb55u : 0xbb5500u);
}
}
wgpu::TexelCopyTextureInfo target{};
target.texture = source.texture;
wgpu::TexelCopyBufferLayout layout{};
layout.bytesPerRow = width * 4;
layout.rowsPerImage = height;
g_queue.WriteTexture(&target, pixels.data(), pixels.size() * sizeof(uint32_t), &layout, &source.size);
unsigned rendered = 0;
unsigned matchingStatus = 0;
for (unsigned attempt = 0; attempt < 300 && rendered < 12; ++attempt) {
aurora_update();
if (!aurora_begin_frame()) { SDL_Delay(5); continue; }
set_present_source_override(bindGroup, source.texture, source.size, source.format);
ImGui::SetNextWindowPos(ImVec2(12, 12), ImGuiCond_Always);
ImGui::Begin("MetalFX presentation test", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
ImGui::TextUnformatted("Output-resolution overlay after game upscaling");
ImGui::Text("Source: %u x %u", width, height);
ImGui::End();
aurora_end_frame();
aurora_wait_for_frame_worker();
const auto status = aurora_get_metalfx_status();
require(status != AURORA_METALFX_ERROR, "MetalFX reported a presentation error");
if (status == expected) ++matchingStatus;
++rendered;
}
require(rendered == 12 && matchingStatus >= 9, "Presentation did not reach the expected MetalFX state");
require(g_errors.load() == 0, "Aurora reported an error");
std::printf("PASS presentation source=%ux%u status=%d frames=%u\n", width, height, expected, rendered);
}
} // namespace
int main(int argc, char** argv) {
const auto cache = std::filesystem::temp_directory_path() /
("aurora-metalfx-presentation-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
std::filesystem::create_directories(cache);
const auto path = cache.string();
AuroraConfig config{};
config.appName = "MetalFX presentation test";
config.userPath = path.c_str();
config.cachePath = path.c_str();
config.resourcesPath = path.c_str();
config.desiredBackend = BACKEND_METAL;
config.windowWidth = 640;
config.windowHeight = 480;
config.msaa = 1;
config.maxTextureAnisotropy = 1;
config.logCallback = log_message;
config.logLevel = LOG_INFO;
aurora_initialize(argc, argv, &config);
int result = 0;
try {
if (!aurora_is_metalfx_spatial_supported()) {
std::puts("SKIP: MetalFX spatial scaling is unavailable");
result = 77;
} else {
aurora::window::lock_present_aspect_ratio(4, 3);
aurora_set_metalfx_spatial(false);
require(!aurora_get_metalfx_spatial(), "Disable request was not retained");
draw_frames(320, 240, AURORA_METALFX_DISABLED);
aurora_set_metalfx_spatial(true);
require(aurora_get_metalfx_spatial(), "Enable request was not retained");
draw_frames(320, 240, AURORA_METALFX_ACTIVE);
aurora::window::set_window_size(800, 500);
draw_frames(320, 240, AURORA_METALFX_ACTIVE);
aurora::window::lock_present_aspect_ratio(16, 9);
draw_frames(320, 240, AURORA_METALFX_ACTIVE);
const auto output = aurora::window::get_window_size();
draw_frames(output.native_fb_width, output.native_fb_height, AURORA_METALFX_NOT_UPSCALING);
aurora_set_metalfx_spatial(false);
draw_frames(320, 240, AURORA_METALFX_DISABLED);
aurora_set_metalfx_spatial(true);
draw_frames(320, 240, AURORA_METALFX_ACTIVE);
}
} catch (const std::exception& error) {
std::fprintf(stderr, "FAIL: %s\n", error.what());
result = 1;
}
aurora_shutdown();
std::error_code cleanupError;
std::filesystem::remove_all(cache, cleanupError);
return result;
}
@@ -0,0 +1,9 @@
#include "webgpu/metalfx.hpp"
int main() {
using namespace aurora::webgpu::metalfx;
if (supported({}, wgpu::BackendType::Vulkan) || supported({}, wgpu::BackendType::Metal)) return 1;
std::string error;
if (create({}, {}, {640, 480, 1280, 960, wgpu::TextureFormat::RGBA8Unorm}, error)) return 1;
return error.empty() ? 1 : 0;
}
@@ -0,0 +1,174 @@
#include "gx_test_common.hpp"
#include "gfx/staging_map.hpp"
#include "gx/pipeline.hpp"
#include <thread>
using aurora::gx::g_gxState;
namespace {
std::vector<u8> draw(GXPrimitive primitive, u16 count, GXVtxFmt format = GX_VTXFMT0) {
std::vector<u8> bytes{static_cast<u8>(primitive | format), static_cast<u8>(count >> 8),
static_cast<u8>(count)};
bytes.resize(3 + count);
return bytes;
}
}
TEST_F(GXFifoTest, MaximumQuadCountTerminatesWithoutOutOfRangeIndices) {
g_gxState.lastVtxFmt = GX_VTXFMT0;
g_gxState.lastVtxSize = 1;
for (const u16 count : {65532, 65533, 65534, 65535}) {
g_gxState.stateDirty = true;
decode_fifo(draw(GX_QUADS, count));
const auto& indices = aurora::gfx::testing::last_pushed_indices();
ASSERT_EQ(indices.size(), (count / 4) * 6 + (count % 4 == 3 ? 3 : 0));
for (const auto index : indices) ASSERT_LT(index, count);
}
}
TEST_F(GXFifoTest, IncompletePrimitivesNeverJoinAcrossDraws) {
g_gxState.lastVtxFmt = GX_VTXFMT0;
g_gxState.lastVtxSize = 1;
aurora::gfx::testing::use_draw_command_tracking(true);
decode_fifo(draw(GX_TRIANGLES, 4));
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{0, 1, 2}));
decode_fifo(draw(GX_TRIANGLES, 5));
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{4, 5, 6}));
const auto before = aurora::gfx::testing::last_pushed_indices();
decode_fifo(draw(GX_TRIANGLEFAN, 2));
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), before);
}
TEST_F(GXFifoTest, MergeStopsBeforeSixteenBitIndexOverflow) {
g_gxState.lastVtxFmt = GX_VTXFMT0;
g_gxState.lastVtxSize = 1;
aurora::gfx::testing::use_draw_command_tracking(true);
decode_fifo(draw(GX_TRIANGLES, 65535));
decode_fifo(draw(GX_TRIANGLES, 3));
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 0u);
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{0, 1, 2}));
}
TEST_F(GXFifoTest, VertexCacheInvalidationBreaksDrawMerging) {
g_gxState.lastVtxFmt = GX_VTXFMT0;
g_gxState.lastVtxSize = 1;
aurora::gfx::testing::use_draw_command_tracking(true);
decode_fifo(draw(GX_TRIANGLES, 3));
decode_fifo({GX_CMD_INVL_VC});
EXPECT_TRUE(g_gxState.stateDirty);
decode_fifo(draw(GX_TRIANGLES, 3));
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 0u);
}
TEST_F(GXFifoTest, EqualStrideVertexFormatChangeBreaksDrawMerging) {
aurora::gfx::testing::use_real_vertex_format_helpers(true);
g_gxState.vtxDesc[GX_VA_POS] = GX_DIRECT;
for (const auto format : {GX_VTXFMT0, GX_VTXFMT1}) {
g_gxState.vtxFmts[format].attrs[GX_VA_POS].cnt = GX_POS_XY;
g_gxState.vtxFmts[format].attrs[GX_VA_POS].type = GX_U8;
}
g_gxState.vtxFmts[GX_VTXFMT1].attrs[GX_VA_POS].frac = 1;
aurora::gfx::testing::use_draw_command_tracking(true);
for (const auto format : {GX_VTXFMT0, GX_VTXFMT1}) {
auto bytes = draw(GX_TRIANGLES, 3, format);
bytes.resize(9);
decode_fifo(bytes);
}
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 0u);
}
TEST_F(GXFifoTest, SingleExpandedPrimitiveCannotMergeWithTriangles) {
g_gxState.lastVtxFmt = GX_VTXFMT0;
g_gxState.lastVtxSize = 1;
aurora::gfx::testing::use_draw_command_tracking(true);
decode_fifo(draw(GX_POINTS, 1));
decode_fifo(draw(GX_TRIANGLES, 3));
EXPECT_EQ(aurora::gfx::g_mergedDrawCallCount, 0u);
EXPECT_EQ(aurora::gfx::testing::last_pushed_indices(), (std::vector<u16>{0, 1, 2}));
}
TEST(StagingMapping, RetiredCallbacksCannotPublishAnotherBuffersReadiness) {
using namespace aurora::gfx;
StagingMapState state;
const auto old = state.request();
EXPECT_EQ(state.request(), 0u);
state.reset();
const auto current = state.request();
EXPECT_FALSE(state.complete(old, BufferMapState::Mapped));
EXPECT_FALSE(state.complete(old, BufferMapState::Unmapped));
EXPECT_EQ(state.state(), BufferMapState::Mapping);
EXPECT_TRUE(state.complete(current, BufferMapState::Mapped));
EXPECT_FALSE(state.complete(current, BufferMapState::Unmapped));
EXPECT_EQ(state.state(), BufferMapState::Mapped);
}
TEST(StagingMapping, AsyncCompletionWakesWaiters) {
using namespace aurora::gfx;
StagingMapState state;
const auto generation = state.request();
std::thread callback([&] {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
state.complete(generation, BufferMapState::Mapped);
});
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
while (state.state() == BufferMapState::Mapping && std::chrono::steady_clock::now() < deadline)
state.wait_for_progress();
callback.join();
EXPECT_EQ(state.state(), BufferMapState::Mapped);
}
TEST(StagingCapacity, ReservesPaddingAndRejectsOverflow) {
using namespace aurora::gfx;
EXPECT_EQ(staging_padded(257, 256), 512u);
EXPECT_THROW(staging_padded(UINT64_MAX, 256), StagingCapacityError);
const StagingSizes used{0, 256, 0, 0}, demand{0, 256, 0, 0}, tail{0, 3840, 0, 0};
EXPECT_TRUE(staging_fits(used, demand, tail, {4, 4352, 4, 4}));
EXPECT_FALSE(staging_fits(used, demand, tail, {4, 4351, 4, 4}));
EXPECT_FALSE(staging_fits({UINT64_MAX, 0, 0, 0}, {1, 0, 0, 0}, {},
{UINT64_MAX, UINT64_MAX, UINT64_MAX, UINT64_MAX}));
}
TEST(FrameInterpolationContract, IdenticalMeshesInDifferentViewportsDoNotShareHistory) {
using namespace aurora;
const auto savedViewport = gx::g_gxState.logicalViewport;
constexpr size_t positionOffset = sizeof(Mat4x4<float>);
constexpr size_t normalOffset = positionOffset + gx::MaxPnMtx * sizeof(Mat3x4<float>);
constexpr size_t uniformSize = normalOffset + gx::MaxPnMtx * sizeof(Mat3x4<float>);
const gx::FrameInterpolationDrawIdentity identity{0x1234, 0x5678, 0x9abc, 0xdef0};
const Mat4x4<float> projection{};
const auto record = [&](float x, std::array<uint8_t, uniformSize>& source) {
gx::g_gxState.pnMtx[0].pos = {{1.f, 0.f, 0.f, x}, {0.f, 1.f, 0.f, 0.f}, {0.f, 0.f, 1.f, 0.f}};
gx::g_gxState.pnMtx[0].nrm = {{1.f, 0.f, 0.f, 0.f}, {0.f, 1.f, 0.f, 0.f}, {0.f, 0.f, 1.f, 0.f}};
std::memcpy(source.data() + positionOffset, &gx::g_gxState.pnMtx[0].pos, sizeof(Mat3x4<float>));
std::memcpy(source.data() + normalOffset, &gx::g_gxState.pnMtx[0].nrm, sizeof(Mat3x4<float>));
return gx::record_interpolation_draw(identity, projection, 1, {
.sourceUniformData = source.data(), .uniformSize = source.size(), .projectionOffset = 0,
.positionOffset = positionOffset, .normalOffset = normalOffset, .currentMatrix = 0,
.indexedMatrices = true});
};
gx::set_frame_interpolation_fps(0);
gx::begin_frame_interpolation();
gx::set_frame_interpolation_fps(120);
gx::g_gxState.logicalViewport = {0.f, 0.f, 640.f, 240.f, 0.f, 1.f};
std::array<uint8_t, uniformSize> previous{};
gx::begin_frame_interpolation();
record(0.f, previous);
gx::finalize_frame_interpolation();
gfx::testing::reset_uniform_allocations();
gx::g_gxState.logicalViewport.top = 240.f;
std::array<uint8_t, uniformSize> current{};
gx::begin_frame_interpolation();
const auto ranges = record(20.f, current);
const auto expected = current;
gx::finalize_frame_interpolation();
EXPECT_EQ(current, expected);
if (ranges[0].size) {
const auto& duplicate = gfx::testing::uniform_allocation(ranges[0].offset);
ASSERT_EQ(duplicate.size(), expected.size());
EXPECT_EQ(std::memcmp(duplicate.data(), expected.data(), expected.size()), 0);
}
gx::g_gxState.logicalViewport = savedViewport;
gx::set_frame_interpolation_fps(0);
gx::begin_frame_interpolation();
}
+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 0x800060A4 \
--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://rwfc.net/api/wfc/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
output: build/mods/retro_rewind_full_cpp
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
riivolution:
xml: xml/RetroRewind6.xml
+317 -46
View File
@@ -1,18 +1,54 @@
cmake_minimum_required(VERSION 3.16)
cmake_minimum_required(VERSION 3.25)
# Dawn's pinned macOS artifacts target 12.0. Set the same floor before project()
# initializes the Apple toolchain so direct developer CMake invocations cannot
# accidentally inherit the running SDK's deployment version. This cache entry
# is harmless on non-Apple platforms and remains overridable by a caller.
if(NOT CMAKE_OSX_DEPLOYMENT_TARGET)
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING
"Minimum macOS version supported by WiiCompiled" FORCE)
endif()
project(mkw_recompiled)
if((NOT (WIN32 AND MINGW)) AND (NOT CMAKE_SYSTEM_NAME STREQUAL "Linux"))
message(FATAL_ERROR "WiiCompiled requires Windows (LLVM-MinGW) or native Linux")
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
message(FATAL_ERROR "WiiCompiled requires a 64-bit Clang toolchain")
endif()
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR
NOT CMAKE_SIZEOF_VOID_P EQUAL 8 OR
NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
message(FATAL_ERROR "WiiCompiled requires 64-bit Clang targeting x86_64")
if(APPLE AND CMAKE_OSX_ARCHITECTURES)
list(LENGTH CMAKE_OSX_ARCHITECTURES MKW_OSX_ARCHITECTURE_COUNT)
if(MKW_OSX_ARCHITECTURE_COUNT GREATER 1)
message(FATAL_ERROR
"WiiCompiled supports one macOS architecture per build directory; "
"configure separate arm64 and x86_64 build directories")
endif()
list(GET CMAKE_OSX_ARCHITECTURES 0 _mkw_osx_arch)
set(CMAKE_SYSTEM_PROCESSOR "${_mkw_osx_arch}" CACHE STRING "Target processor architecture" FORCE)
set(CMAKE_SYSTEM_PROCESSOR "${_mkw_osx_arch}")
endif()
if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_PLATFORM_WINDOWS TRUE)
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|arm64|ARM64)$")
set(MKW_PLATFORM_MACOS TRUE)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_PLATFORM_MACOS_X86_64 TRUE)
else()
set(MKW_PLATFORM_MACOS_ARM64 TRUE)
endif()
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64|aarch64|arm64|ARM64)$")
set(MKW_PLATFORM_LINUX TRUE)
else()
message(FATAL_ERROR
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS x86_64/arm64")
endif()
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
message(FATAL_ERROR "WiiCompiled only supports Release builds")
endif()
option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON)
option(MKW_BUILD_PSQ_TESTS "Build focused PSQ ISA tests" OFF)
# Preprocessor definitions that belong to this project's own code (the runtime,
# the translated shards and the product glue) and to nothing else. They are
# applied directory-scoped, immediately after the aurora add_subdirectory() call,
@@ -49,17 +85,15 @@ target_include_directories(mkw_pugixml PUBLIC third_party/pugixml)
target_compile_features(mkw_pugixml PUBLIC cxx_std_17)
set_target_properties(mkw_pugixml PROPERTIES UNITY_BUILD OFF)
# Non-Windows guest-fiber scheduling (runtime/src/fiber_manager.cpp) needs a symmetric
# POSIX x86-64 guest-fiber scheduling (runtime/src/host_context.cpp) needs a symmetric
# stackful-coroutine primitive to stand in for Win32 Fibers. libco's co_switch() transfers
# directly to any other created coroutine, matching SwitchToFiber's semantics exactly (unlike
# asymmetric resume/yield coroutine libraries, which would need every call site restructured).
# Vendored from upstream (higan-emu/libco @ e18e09d, 2019-10-16, ISC license; valgrind.h is
# separately BSD-style licensed, see third_party/libco/LICENSE) - all of libco's non-Windows
# CPU-architecture backends are kept, even though libco.c's own preprocessor dispatch
# (__amd64__/__i386__/__arm__/__aarch64__/etc.) only ever selects amd64.c for this project's
# x86_64-only target (see the platform/arch check above). Windows keeps using native Fibers
# untouched, so this target is never built there.
if(NOT WIN32)
# separately BSD-style licensed, see third_party/libco/LICENSE). Windows keeps native Fibers;
# Apple Silicon uses the project's x18-safe AArch64 assembly backend, while Intel macOS uses
# libco's existing System V AMD64 backend.
if(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
add_library(mkw_libco STATIC third_party/libco/libco.c)
add_library(mkw::libco ALIAS mkw_libco)
target_include_directories(mkw_libco PUBLIC third_party/libco)
@@ -100,6 +134,44 @@ if(NOT MKW_NATIVE_PREBUILT_DIR)
set_target_properties(mkw_cryptopp PROPERTIES UNITY_BUILD OFF)
endif()
# TLS for non-Windows guest network HLE (runtime/src/hle/net/network_ssl.cpp) - the Windows path
# uses Schannel (a Windows-only OS API), which has no equivalent on Linux/Android, so this project
# needs its own TLS library there. mbed TLS was chosen over OpenSSL specifically because it cross-
# compiles cleanly for Android with nothing beyond a plain C toolchain (no perl/asm build-script
# dependency the way OpenSSL's build has), matching how this project already prefers toolchain-
# simple libraries (see Crypto++ above, similarly stripped of ASM/SIMD for portability).
# The from-source build fetches a pinned upstream tarball with a checked SHA-256.
# The Linux native prebuilt package instead ships its compiled archives and headers.
# Bump both values in cmake/MbedTLSPin.cmake when updating; the hash comes from
# upstream's signed `mbedtls-<version>-sha256sum.txt` release asset.
#
# The alias exists on every platform so the link lines in cmake/PublicProducts.cmake stay
# platform-independent, but it is only populated where network_ssl.cpp actually compiles the mbed
# TLS path (`#ifndef _WIN32`). Windows keeps Schannel and must not fetch anything: its builds run
# with FETCHCONTENT_FULLY_DISCONNECTED=ON against the offline dependency set prepared by
# Launcher/Prepare-Dependencies.ps1, so an unconditional fetch would fail a clean configure there
# and would also add a dependency Windows never links.
add_library(mkw_mbedtls INTERFACE)
add_library(mkw::mbedtls ALIAS mkw_mbedtls)
if(NOT MKW_PLATFORM_WINDOWS)
include("${CMAKE_CURRENT_LIST_DIR}/cmake/MbedTLSPin.cmake")
if(NOT MKW_NATIVE_PREBUILT_DIR)
include(FetchContent)
FetchContent_Declare(mkw_mbedtls_upstream
URL "https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-${MKW_MBEDTLS_VERSION}/mbedtls-${MKW_MBEDTLS_VERSION}.tar.bz2"
URL_HASH SHA256=${MKW_MBEDTLS_SHA256}
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
)
# Subproject mode already disables testing and codegen; it still enables programs.
set(ENABLE_PROGRAMS OFF CACHE BOOL "" FORCE)
set(ENABLE_TESTING OFF CACHE BOOL "" FORCE)
set(MBEDTLS_FATAL_WARNINGS OFF CACHE BOOL "" FORCE)
set(DISABLE_PACKAGE_CONFIG_AND_INSTALL ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(mkw_mbedtls_upstream)
target_link_libraries(mkw_mbedtls INTERFACE MbedTLS::mbedtls MbedTLS::mbedx509 MbedTLS::mbedcrypto)
endif()
endif()
set(MKW_TRANSLATED_COMPILE_JOBS 0 CACHE STRING
"Cap on concurrently compiling translated shard TUs via a Ninja job pool (0 = uncapped). \
Scheduling only - never affects output bytes, so it is deliberately outside the canonical flag fingerprint.")
@@ -117,6 +189,18 @@ set(MKW_AURORA_DIR "${CMAKE_CURRENT_LIST_DIR}/../aurora-main")
# Fast-math may erase them and change guest-visible integer conversions.
set(MKW_TRANSLATED_PPC_FP_OPTIONS -fno-fast-math -ffp-contract=off)
set(MKW_PPC_SEMANTIC_RUNTIME_SOURCES
"${CMAKE_CURRENT_LIST_DIR}/src/ppc_helpers.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/fpu_helpers.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/ppc_quantized.cpp")
set_source_files_properties(${MKW_PPC_SEMANTIC_RUNTIME_SOURCES} PROPERTIES
SKIP_UNITY_BUILD_INCLUSION ON
SKIP_PRECOMPILE_HEADERS ON
COMPILE_OPTIONS "${MKW_TRANSLATED_PPC_FP_OPTIONS}")
# Match the optimization policy of the translated callers as well as their FP policy.
set_property(SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/ppc_quantized.cpp" APPEND PROPERTY
COMPILE_OPTIONS -O2 -fno-slp-vectorize)
# ----------------------------------------------------------------------
# Third-party: aurora-main (provides SDL3 + GPU backends)
# ----------------------------------------------------------------------
@@ -130,31 +214,35 @@ else()
message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing")
endif()
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_VULKAN ON CACHE BOOL "" FORCE)
set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE)
set(TINT_BUILD_HLSL_WRITER ON CACHE BOOL "" FORCE)
set(DAWN_USE_WINDOWS_UI OFF CACHE BOOL "" FORCE)
else()
# Non-Windows (Linux): mirrors aurora-main's own
# _aurora_dawn_set_platform_backends() choice for this platform - Vulkan only, no
# D3D/HLSL. Kept in sync here because this project's own CMake FORCEs these cache
# variables before aurora-main's add_subdirectory() runs, which pre-empts aurora's
# auto-detection (CACHE ... INTERNAL "" without FORCE never overrides an existing value).
elseif(MKW_PLATFORM_MACOS)
set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE)
set(DAWN_ENABLE_VULKAN OFF CACHE BOOL "" FORCE)
set(DAWN_ENABLE_METAL ON CACHE BOOL "" FORCE)
set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE)
else()
set(DAWN_ENABLE_D3D12 OFF CACHE BOOL "" FORCE)
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
set(DAWN_ENABLE_METAL OFF CACHE BOOL "" FORCE)
set(TINT_BUILD_HLSL_WRITER OFF CACHE BOOL "" FORCE)
endif()
set(DAWN_ENABLE_VULKAN ON CACHE BOOL "" FORCE)
set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE)
set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE)
# Provide a tiny stub for DXProgrammableCapture when the SDK/PIX headers are
# missing (common on MinGW). Dawn only includes the header; no symbols are
# referenced when PIX isn't present.
set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs")
if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h")
file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR})
file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h"
"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX is absent.\n")
if(MKW_PLATFORM_WINDOWS)
set(MKW_DX_STUB_DIR "${CMAKE_BINARY_DIR}/aurora_dx_stubs")
if(NOT EXISTS "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h")
file(MAKE_DIRECTORY ${MKW_DX_STUB_DIR})
file(WRITE "${MKW_DX_STUB_DIR}/DXProgrammableCapture.h"
"#pragma once\n// Stubbed PIX capture header for Dawn; no functionality when PIX isn't present.\n")
endif()
endif()
# Deliberately NOT injected project-wide. Only a from-source Dawn build ever
# includes DXProgrammableCapture.h, and this tree consumes Dawn as a prebuilt
@@ -183,7 +271,9 @@ else()
if(TARGET ${t})
set_target_properties(${t} PROPERTIES UNITY_BUILD OFF)
target_compile_options(${t} PRIVATE -O3 -ffast-math -w -pipe)
target_include_directories(${t} PRIVATE ${MKW_DX_STUB_DIR})
if(MKW_PLATFORM_WINDOWS)
target_include_directories(${t} PRIVATE ${MKW_DX_STUB_DIR})
endif()
# Aurora's own sources include Windows headers and call std::min/max;
# they relied on the old project-wide NOMINMAX that no longer leaks
# into this subtree, so the define is applied per target here.
@@ -228,6 +318,20 @@ endif()
# a registration file is silently never compiled and never errors. The stale-glob
# failure mode is worth far more than the milliseconds.
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
if(MKW_PLATFORM_MACOS)
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory.cpp")
if(MKW_PLATFORM_MACOS_ARM64)
# HostContext's Apple Silicon backend is implemented in a small assembly
# companion. It must be part of the product runtime as well as the
# standalone context test; otherwise the final executable is missing
# mkw_co_init/mkw_co_switch at link time.
enable_language(ASM)
list(APPEND SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
endif()
else()
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
endif()
set(MKW_PLATFORM_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/platform/host_platform.cpp")
set(MKW_BASE_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/base_product.cpp")
set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro_rewind_product.cpp")
# The host ISA guard is the one translation unit that must not receive the
@@ -235,25 +339,192 @@ set(MKW_RETRO_REWIND_PRODUCT_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/product/retro
# mkw_runtime_common. See cmake/PublicProducts.cmake and the file's own header.
set(MKW_CPU_BASELINE_SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/host_cpu_baseline.cpp")
list(REMOVE_ITEM SOURCES ${MKW_BASE_PRODUCT_SOURCE} ${MKW_RETRO_REWIND_PRODUCT_SOURCE}
${MKW_CPU_BASELINE_SOURCE})
${MKW_CPU_BASELINE_SOURCE} ${MKW_PLATFORM_SOURCE})
# This deliberately small library contains host services that are safe to
# validate before guest memory and fiber work makes a full runtime build viable.
add_library(mkw_platform STATIC "${MKW_PLATFORM_SOURCE}")
target_include_directories(mkw_platform PUBLIC "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_platform PUBLIC cxx_std_17)
set_target_properties(mkw_platform PROPERTIES UNITY_BUILD OFF)
# Keep these independent from Aurora's BUILD_TESTING option: they validate the
# project's host-platform contracts, not Aurora's third-party test suite.
enable_testing()
if(MKW_BUILD_PSQ_TESTS)
add_executable(mkw_psq_helpers_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/psq_helpers_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/ppc_quantized.cpp")
target_include_directories(mkw_psq_helpers_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/tests/psq_memory"
"${CMAKE_CURRENT_LIST_DIR}/include/isa"
"${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_psq_helpers_tests PRIVATE cxx_std_17)
target_compile_options(mkw_psq_helpers_tests PRIVATE
-O2 ${MKW_TRANSLATED_PPC_FP_OPTIONS} -fno-slp-vectorize)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
target_compile_options(mkw_psq_helpers_tests PRIVATE -march=x86-64-v3)
endif()
set_target_properties(mkw_psq_helpers_tests PROPERTIES UNITY_BUILD OFF)
add_test(NAME mkw_psq_helpers_tests COMMAND mkw_psq_helpers_tests)
add_test(NAME mkw_psq_reserved_tests COMMAND "${CMAKE_COMMAND}"
"-DPSQ_TEST_EXECUTABLE=$<TARGET_FILE:mkw_psq_helpers_tests>"
-P "${CMAKE_CURRENT_LIST_DIR}/tests/psq_reserved_tests.cmake")
endif()
add_executable(mkw_platform_paths_tests "${CMAKE_CURRENT_LIST_DIR}/tests/platform_paths_tests.cpp")
target_link_libraries(mkw_platform_paths_tests PRIVATE mkw_platform)
target_compile_features(mkw_platform_paths_tests PRIVATE cxx_std_17)
add_test(NAME mkw_platform_paths_tests COMMAND mkw_platform_paths_tests)
add_executable(mkw_runtime_config_tests "${CMAKE_CURRENT_LIST_DIR}/tests/runtime_config_tests.cpp")
target_include_directories(mkw_runtime_config_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include"
"${CMAKE_CURRENT_LIST_DIR}/third_party/toml11")
target_compile_features(mkw_runtime_config_tests PRIVATE cxx_std_20)
add_test(NAME mkw_runtime_config_tests COMMAND mkw_runtime_config_tests)
add_executable(mkw_nand_save_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_save_tests.cpp")
target_include_directories(mkw_nand_save_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_nand_save_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_save_tests COMMAND mkw_nand_save_tests)
add_executable(mkw_nand_settings_tests "${CMAKE_CURRENT_LIST_DIR}/tests/nand_settings_tests.cpp")
find_package(Threads REQUIRED)
target_link_libraries(mkw_nand_settings_tests PRIVATE Threads::Threads)
target_include_directories(mkw_nand_settings_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_nand_settings_tests PRIVATE cxx_std_17)
add_test(NAME mkw_nand_settings_tests COMMAND mkw_nand_settings_tests)
add_executable(mkw_sc_serial_tests "${CMAKE_CURRENT_LIST_DIR}/tests/sc_serial_tests.cpp")
target_include_directories(mkw_sc_serial_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_sc_serial_tests PRIVATE cxx_std_17)
add_test(NAME mkw_sc_serial_tests COMMAND mkw_sc_serial_tests)
# The input expression engine is self-contained, so it can be exercised without
# linking the runtime or SDL.
add_executable(mkw_input_expr_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/test_expr.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/input_expr.cpp")
target_include_directories(mkw_input_expr_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
# HostContext deliberately keeps the platform-specific context primitive out
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
# refactors cannot silently remove its headers, implementation, or link edge.
if(MKW_PLATFORM_LINUX)
add_executable(mkw_linux_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp")
target_include_directories(mkw_linux_host_context_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include"
"${CMAKE_CURRENT_LIST_DIR}/third_party/libco")
target_compile_features(mkw_linux_host_context_tests PRIVATE cxx_std_17)
target_link_libraries(mkw_linux_host_context_tests PRIVATE mkw::libco)
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
endif()
if(MKW_PLATFORM_MACOS)
# Exercise the public host-memory contracts separately from translated products.
if(MKW_PLATFORM_MACOS_ARM64)
# Apple Silicon's context ABI is implemented by the local assembly backend.
enable_language(ASM)
add_executable(mkw_macos_context_abi_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
target_compile_features(mkw_macos_context_abi_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_context_abi_tests COMMAND mkw_macos_context_abi_tests)
add_executable(mkw_macos_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S")
target_include_directories(mkw_macos_host_context_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
else()
# Intel macOS follows the same System V AMD64 libco path as Linux.
add_executable(mkw_macos_host_context_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/host_context_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/host_context.cpp")
target_include_directories(mkw_macos_host_context_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include"
"${CMAKE_CURRENT_LIST_DIR}/third_party/libco")
target_link_libraries(mkw_macos_host_context_tests PRIVATE mkw::libco)
endif()
target_compile_features(mkw_macos_host_context_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_host_context_tests COMMAND mkw_macos_host_context_tests)
add_executable(mkw_macos_guest_flat_memory_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_guest_flat_memory_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
target_include_directories(mkw_macos_guest_flat_memory_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_macos_guest_flat_memory_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_guest_flat_memory_tests COMMAND mkw_macos_guest_flat_memory_tests)
add_executable(mkw_macos_external_audio_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_external_audio_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/external_audio_macos.cpp")
target_include_directories(mkw_macos_external_audio_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_macos_external_audio_tests PRIVATE cxx_std_17)
add_test(NAME mkw_macos_external_audio_tests COMMAND mkw_macos_external_audio_tests)
endif()
# The translator emits the complete, content-addressed source graph. Consuming
# this one manifest keeps configure independent of the 28k generated function
# files and of optional Retro Rewind artifacts such as code.map.
set(MKW_TRANSLATED_SHARD_MANIFEST
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
CACHE FILEPATH "Translator-owned aggregate shard manifest")
if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
message(FATAL_ERROR
"Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. "
"Run Translator.Cli emit-build-shards first; see translator/README.md.")
endif()
include("${MKW_TRANSLATED_SHARD_MANIFEST}")
set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS})
message(STATUS
"Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; "
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; "
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions")
if(MKW_BUILD_PRODUCTS)
set(MKW_TRANSLATED_SHARD_MANIFEST
"${CMAKE_CURRENT_LIST_DIR}/../generated/build_shards/shards.cmake"
CACHE FILEPATH "Translator-owned aggregate shard manifest")
# The prebuilt export only needs the aurora/third-party closure configured above, so a
# packaging machine without a translation stops here instead of failing.
if(MKW_NATIVE_PREBUILT_EXPORT_DIR AND NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
message(STATUS "No translator shard manifest; configuring the native prebuilt export only")
return()
endif()
if(NOT EXISTS "${MKW_TRANSLATED_SHARD_MANIFEST}")
message(FATAL_ERROR
"Missing translator-owned shard manifest: ${MKW_TRANSLATED_SHARD_MANIFEST}. "
"Run Translator.Cli emit-build-shards first; see translator/README.md.")
endif()
include("${MKW_TRANSLATED_SHARD_MANIFEST}")
set(MKW_HAVE_RETRO_REWIND ${MKW_HAVE_RETRO_REWIND_SHARDS})
message(STATUS
"Translator graph: ${MKW_SHARED_BASE_FUNCTION_COUNT}/${MKW_BASE_FUNCTION_COUNT} base functions shared; "
"${MKW_PROFILE_SENSITIVE_CALLER_COUNT} profile-sensitive callers; "
"${MKW_RETRO_REWIND_FUNCTION_COUNT} Retro Rewind functions")
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake")
set(MKW_RUNTIME_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
include("${CMAKE_CURRENT_LIST_DIR}/cmake/PublicProducts.cmake")
else()
if(MKW_PLATFORM_MACOS)
# Compile-only audit of native runtime sources. It deliberately avoids
# translated products until their host dependencies are portable.
# These sources depend on generated/RuntimeConfig.h, which is emitted for
# a particular game by the translator and is intentionally unavailable
# in this platform-only configuration.
set(MKW_MACOS_NATIVE_AUDIT_SOURCES ${SOURCES})
list(REMOVE_ITEM MKW_MACOS_NATIVE_AUDIT_SOURCES
"${CMAKE_CURRENT_LIST_DIR}/src/abi_bridge.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/hle/os/os_alarm.cpp")
add_library(mkw_macos_native_compile OBJECT ${MKW_MACOS_NATIVE_AUDIT_SOURCES})
target_include_directories(mkw_macos_native_compile PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include" "${CMAKE_CURRENT_LIST_DIR}/src"
"${CMAKE_CURRENT_LIST_DIR}/.." "${CMAKE_CURRENT_LIST_DIR}/../aurora-main/include")
target_compile_features(mkw_macos_native_compile PRIVATE cxx_std_20)
target_compile_definitions(mkw_macos_native_compile PRIVATE SDL_MAIN_HANDLED TARGET_PC)
target_link_libraries(mkw_macos_native_compile PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
if(MKW_PLATFORM_MACOS_X86_64)
target_link_libraries(mkw_macos_native_compile PRIVATE mkw::libco)
# Keep this compile-only audit on the same Haswell-era x86-64-v3
# baseline as the translated product. PPC paired FMA helpers use
# FMA intrinsics and intentionally cannot compile for plain x86-64.
target_compile_options(mkw_macos_native_compile PRIVATE -march=x86-64-v3)
endif()
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()
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
# Keep the release and verified archive hash together. The Linux native prebuilt
# fingerprint includes this file, so changing either value forces a new harvest.
set(MKW_MBEDTLS_VERSION "3.6.7")
set(MKW_MBEDTLS_SHA256 "a7e8bcbec0e6f761b4af24f25677626b35f762f68eef79c08677a363212d11f6")
+32
View File
@@ -42,6 +42,28 @@ endfunction()
mkw_np_resolve(_mkw_np_includes ${MKW_NP_INCLUDE_DIRECTORIES})
mkw_np_resolve(_mkw_np_links ${MKW_NP_LINK_LIBRARIES})
mkw_np_resolve(_mkw_np_dawn_config ${MKW_NP_DAWN_CONFIG_DIR})
if(NOT MKW_PLATFORM_WINDOWS)
if(NOT MKW_NP_MBEDTLS_LIBRARIES OR NOT MKW_NP_MBEDTLS_INCLUDE_DIR)
message(FATAL_ERROR
"The Linux native prebuilt package has no Mbed TLS archives; regenerate it with Prepare-NativePrebuilt.sh")
endif()
file(SHA256 "${CMAKE_CURRENT_LIST_DIR}/MbedTLSPin.cmake" _mkw_np_current_mbedtls_fingerprint)
if(NOT MKW_NP_MBEDTLS_FINGERPRINT STREQUAL _mkw_np_current_mbedtls_fingerprint)
message(FATAL_ERROR "The native prebuilt Mbed TLS version differs from this workspace; regenerate the package")
endif()
mkw_np_resolve(_mkw_np_mbedtls_links ${MKW_NP_MBEDTLS_LIBRARIES})
mkw_np_resolve(_mkw_np_mbedtls_include ${MKW_NP_MBEDTLS_INCLUDE_DIR})
foreach(_archive IN LISTS _mkw_np_mbedtls_links)
if(NOT EXISTS "${_archive}")
message(FATAL_ERROR "The native prebuilt package is missing Mbed TLS archive: ${_archive}")
endif()
endforeach()
if(NOT IS_DIRECTORY "${_mkw_np_mbedtls_include}")
message(FATAL_ERROR "The native prebuilt package is missing Mbed TLS headers: ${_mkw_np_mbedtls_include}")
endif()
target_include_directories(mkw_mbedtls SYSTEM INTERFACE "${_mkw_np_mbedtls_include}")
target_link_libraries(mkw_mbedtls INTERFACE ${_mkw_np_mbedtls_links})
endif()
foreach(_dir IN LISTS _mkw_np_includes)
if(NOT IS_DIRECTORY "${_dir}")
@@ -51,6 +73,16 @@ foreach(_dir IN LISTS _mkw_np_includes)
endif()
endforeach()
# Dawn's own packaged config (DawnTargets.cmake) links dawn::webgpu_dawn against
# Threads::Threads directly. A from-source aurora build resolves that as a side effect of
# add_subdirectory(aurora-main) pulling in Dawn's own CMakeLists.txt; this mode never runs that
# subdirectory at all, so nothing else would ever define it (verified directly: configuring without
# this fails with "The link interface of target dawn::webgpu_dawn contains: Threads::Threads but
# the target was not found"). find_package(Threads) is one of CMake's most basic finder modules and
# is a no-op on Windows (its threading support is already part of the CRT), so this is safe on
# every platform this package format targets, not just the one that first hit the failure.
find_package(Threads REQUIRED)
# Dawn is a prebuilt package on both sides; resolve the same install tree the
# package was built against so its imported target (and therefore
# webgpu_dawn.dll / dxcompiler.dll / dxil.dll) is available exactly as a

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