43 Commits

Author SHA1 Message Date
patchzyy 987aef0b6a Update settings_overlay.cpp 2026-09-24 22:02:20 +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
150 changed files with 10598 additions and 2350 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
+12 -43
View File
@@ -14,6 +14,10 @@ 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
@@ -26,6 +30,14 @@ jobs:
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,46 +46,3 @@ jobs:
- name: Test
run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal
ios-crosscompile:
name: iOS arm64 (cross-compile, no Xcode)
runs-on: ubuntu-24.04
env:
IOS_SDK_DIR: ${{ github.workspace }}/iOS-SDKs/iPhoneOS26.5.sdk
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
# The runner images carry clang up to 18; the iOS toolchain file is tested
# against upstream 19, so take it from apt.llvm.org rather than the archive.
- name: Install clang 19, lld and ninja
run: |
wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh
chmod +x /tmp/llvm.sh
sudo /tmp/llvm.sh 19
sudo apt-get install -y lld-19 ninja-build
# The only Apple piece in the build. Sparse-cloned so the checkout is one
# SDK (~50 MB) rather than every SDK the repository carries.
- name: Fetch the iPhoneOS SDK
run: |
git clone --depth 1 --filter=blob:none --sparse \
https://github.com/xybp888/iOS-SDKs.git "${{ github.workspace }}/iOS-SDKs"
git -C "${{ github.workspace }}/iOS-SDKs" sparse-checkout set --no-cone iPhoneOS26.5.sdk
# Compile-only: linking a product needs the translated shards, which come
# from the user's own Mario Kart Wii dump and are not in this repository.
# This proves the runtime's own sources still build for iOS arm64.
- name: Configure
run: |
cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}/runtime/cmake/ios-linux-toolchain.cmake" \
-DIOS_SDK="$IOS_SDK_DIR" \
-DMKW_IOS_LLVM_BIN=/usr/lib/llvm-19/bin \
-DMKW_BUILD_PRODUCTS=OFF \
-DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE \
-DAURORA_DAWN_PROVIDER=package
- name: Compile the runtime sources for iOS
run: cmake --build build-ios --target mkw_macos_native_compile
+5 -1
View File
@@ -20,6 +20,10 @@ concurrency:
cancel-in-progress: true
jobs:
recompilation:
name: Recompilation test
uses: ./.github/workflows/recomp-test.yml
linux-appimage:
name: Linux (AppImage, ${{ matrix.arch }})
strategy:
@@ -93,7 +97,7 @@ jobs:
release:
name: Publish GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
needs: [linux-appimage, windows-installer]
needs: [linux-appimage, windows-installer, recompilation]
runs-on: ubuntu-latest
permissions:
contents: write
+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
+4
View File
@@ -26,6 +26,8 @@ Code.pul
/build/
/build-*/
/native-build/
/native-build-macos/
/local-products/
/dist/
/out/
[Bb]in/
@@ -70,3 +72,5 @@ project.lock.json
*.log
output.txt
# Operating System
.DS_Store
+1 -1
View File
@@ -281,7 +281,7 @@ foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToo
$manifest = [ordered]@{
SchemaVersion = 2
ProductVersion = '0.2.27'
ProductVersion = '0.2.32'
ExpectedGameId = $pins.GameId
ExpectedDolSha256 = $pins.DolSha256
ExpectedRelSha256 = $pins.RelSha256
+14 -8
View File
@@ -117,12 +117,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 +133,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." }
}
+2 -2
View File
@@ -39,9 +39,9 @@ $packages = @(
},
[pscustomobject]@{
Name = 'dawn_prebuilt'; File = 'dawn-v20260603.191052-windows-amd64.tar.gz'
Uris = @('https://github.com/encounter/dawn-build/releases/download/v20260603.191052/dawn-windows-amd64.tar.gz')
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=7785373d569b3b0237918ec9c523239f7d0667857c5ea8242e3cdfde95e6aeab' })
@{ File = $auroraDawn; Text = 'SHA256=13be9cff8b9b179c42dcd16aeabb6effcc8f0dfdcc14463eda2a5caeda225142' })
},
[pscustomobject]@{
Name = 'fmt'; File = 'fmt-11.1.4.tar.gz'
+1 -1
View File
@@ -88,7 +88,7 @@ assert_file "$ninja_bin" "Portable Ninja"
assert_file "$cc" "Portable C compiler"
assert_file "$cxx" "Portable C++ compiler"
assert_dir "$aurora_source" "aurora-main source tree"
clang_binary=$(normalize "$toolchain_dir/bin/clang-23")
clang_binary=$(normalize "$toolchain_dir/bin/clang-22")
assert_file "$clang_binary" "Portable clang driver binary"
(( parallel > 0 )) || parallel=$(nproc)
+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,7 @@
<Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common.Cli</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common.Cli</AssemblyName>
<Version>0.2.22</Version>
<Version>0.2.32</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");
@@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<RootNamespace>WiiCompiled.Setup.Common</RootNamespace>
<AssemblyName>WiiCompiled.Setup.Common</AssemblyName>
<Version>0.2.22</Version>
<Version>0.2.32</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Shared nodtool/Retro-WFC-payload logic used by both the Windows and Linux installers</Description>
@@ -13,7 +13,8 @@ internal static class BuildRunner
string workspace, string profile, string outputDir, string? baseOutputDir,
string? retroDir, string? retroWfcOfflineDir, bool skipRetroWfcPayload,
bool forceCleanBuild, string? translatorBin, string? ccBin, string? cxxBin, string? fuseLd,
string? cmakeBin, string? ninjaBin, string? nativePrebuiltDir, IInstallReporter reporter,
string? cmakeBin, string? ninjaBin, string? nativePrebuiltDir, string? sysroot,
IInstallReporter reporter,
CancellationToken cancellationToken)
{
var script = Path.Combine(workspace, "Launcher", "local-build.sh");
@@ -77,6 +78,10 @@ internal static class BuildRunner
{
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);
+1 -1
View File
@@ -3,7 +3,7 @@ namespace WiiCompiled.Setup.Linux;
internal static class ProductInfo
{
public const string Name = "WiiCompiled";
public const string Version = "0.2.27";
public const string Version = "0.2.32";
}
/// <summary>One installed product's record inside install-state.json.</summary>
+11 -1
View File
@@ -143,6 +143,15 @@ internal static class Program
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,
@@ -156,6 +165,7 @@ internal static class Program
flags.GetValueOrDefault("cmake"),
flags.GetValueOrDefault("ninja"),
flags.GetValueOrDefault("native-prebuilt-dir"),
sysroot,
reporter, token);
reporter.Progress(InstallStages.Shortcuts, "Creating shortcuts", 98);
@@ -324,7 +334,7 @@ internal static class Program
{--download-retro-wfc-payload | --skip-retro-wfc-payload}]
[--force-clean-build] [--translator-bin PATH] [--disc-tool-bin PATH]
[--cc PATH] [--cxx PATH] [--fuse-ld NAME_OR_PATH] [--cmake PATH] [--ninja PATH]
[--native-prebuilt-dir DIR] [--progress-json] [--workspace DIR]
[--native-prebuilt-dir DIR] [--sysroot PATH] [--progress-json] [--workspace DIR]
uninstall
launch-base
launch-retro
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<AssemblyName>WiiCompiled.Setup.Linux</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Linux</RootNamespace>
<Version>0.2.22</Version>
<Version>0.2.32</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled on Linux</Description>
@@ -121,7 +121,7 @@ internal static class PlatformChecks
internal static class ProductInfo
{
public const string Name = "WiiCompiled";
public const string Version = "0.2.27";
public const string Version = "0.2.32";
/// <summary>
/// The setup executable is copied into the installation under this name. It is the launcher and
@@ -7,7 +7,7 @@
<AssemblyName>WiiCompiled.Setup</AssemblyName>
<RootNamespace>WiiCompiled.Setup.Windows</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.2.27</Version>
<Version>0.2.32</Version>
<Authors>patchzy</Authors>
<Product>WiiCompiled</Product>
<Description>Command-line installer and launcher for WiiCompiled</Description>
+39 -1
View File
@@ -65,6 +65,7 @@ translator_dll_override=""
translator_bin_override=""
fuse_ld_override=""
native_prebuilt_dir=""
sysroot=""
usage() {
cat <<'EOF'
@@ -87,6 +88,8 @@ Usage: local-build.sh --output-dir DIR [options]
--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
}
@@ -110,6 +113,7 @@ while [[ $# -gt 0 ]]; do
--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
@@ -186,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.
@@ -414,6 +442,14 @@ 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" "${configure_args[@]}"
@@ -445,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/"
+2 -2
View File
@@ -27,7 +27,7 @@ done
[[ "$product" == WiiCompiled || "$product" == RetroRewind ]] || fail '--product must be WiiCompiled or RetroRewind'
for tool in codesign ditto install_name_tool otool; do command -v "$tool" >/dev/null || fail "required macOS tool is unavailable: $tool"; done
[[ -x "$build_dir/$product" ]] || fail "missing compiled product: $build_dir/$product"
for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do [[ -e "$build_dir/$asset" ]] || fail "missing runtime asset: $build_dir/$asset"; done
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"
@@ -52,7 +52,7 @@ cat > "$app/Contents/Info.plist" <<EOF
</dict></plist>
EOF
ditto "$build_dir/$product" "$macos/$product"
for asset in dsp_coef.bin initial_pipeline_cache.db wii_bootstrap; do
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
+1 -1
View File
@@ -90,7 +90,7 @@ if [[ -n "$retro_dir" ]]; then
trap 'rm -rf "$payload_stage"' EXIT
/usr/bin/curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \
--retry 1 --output "$temporary_payload" \
'http://nas.play.rwfc.net/payload?g=RMCPD00' || fail 'could not download the Retro-WFC payload needed for online play'
'https://rwfc.net/api/wfc/payload?g=RMCPD00' || fail 'could not download the Retro-WFC payload needed for online play'
"$translator" validate-retro-wfc-payload --directory "$payload_stage" || \
fail 'downloaded Retro-WFC payload failed signature validation'
mkdir -p "$retro_wfc_dir/binary"
+29 -24
View File
@@ -25,7 +25,7 @@ set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
workspace=$(cd "$script_dir/.." && pwd)
llvm_version=23.1.0
llvm_version=22.1.8
cmake_version=4.3.3
ninja_version=1.13.2
destination="$script_dir/artifacts/portable-tools"
@@ -52,13 +52,13 @@ done
case "$arch" in
x86_64) llvm_release_arch=X64; target_triple=x86_64-unknown-linux-gnu
llvm_release_sha256=18da30f77f475688a18f7704d23f9f155ae007ed9922dbed6850a9419d9fec8c
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=cfb31bfc713ef453248bf5bd026312f838ad6c52c25623e987cb6a340f3050d4
llvm_release_sha256=d431eff9f064c86ee7c4c94af570a8f74fcccd1f74c6f0da3af32ce34a1e1b05
cmake_release_arch=aarch64
cmake_sha256=9ea38356dbd3e32e51029a3e09a0f2f8e117ef4fbcaad7a21ffb36409bbd5cb4
ninja_asset=ninja-linux-aarch64.zip
@@ -101,11 +101,14 @@ rm -rf "$work"
mkdir -p "$work/bin" "$work/lib/$target_triple" "$work/include/$target_triple/c++/v1"
# --- clang/lld/llvm-ar, pruned from the official LLVM release ---
llvm_archive_name="LLVM-$llvm_version-Linux-$llvm_release_arch.tar.xz"
# 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/llvm/llvm-project/releases/download/llvmorg-$llvm_version/$llvm_archive_name" \
"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"
@@ -113,16 +116,16 @@ rm -rf "$extract_root"
mkdir -p "$extract_root"
echo "prepare-portable-tools.sh: extracting $llvm_archive_name (this is the full ~1.9 GiB release; only a fraction is kept)..."
tar -xf "$llvm_archive" -C "$extract_root"
src="$extract_root/LLVM-$llvm_version-Linux-$llvm_release_arch"
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-23" "$work/bin/"
strip "$work/bin/clang-23"
ln -s clang-23 "$work/bin/clang"
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 -
@@ -165,7 +168,8 @@ rm -rf "$cmake_extract_root"
mkdir -p "$cmake_extract_root"
echo "prepare-portable-tools.sh: extracting $cmake_archive_name..."
tar -xzf "$cmake_archive" -C "$cmake_extract_root"
cmake_src="$cmake_extract_root/cmake-$cmake_version-linux-$cmake_release_arch"
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"
@@ -201,10 +205,10 @@ Ninja $ninja_version
Apache License 2.0
EOF
echo "prepare-portable-tools.sh: smoke-testing the toolchain..."
smoke_dir=$(mktemp -d)
trap 'rm -rf "$smoke_dir"' EXIT
cat > "$smoke_dir/t.cpp" <<'EOF'
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() {
@@ -214,24 +218,25 @@ int main() {
return sum == 6 ? 0 : 1;
}
EOF
"$work/bin/clang++" -std=c++20 -fuse-ld=lld "$smoke_dir/t.cpp" -o "$smoke_dir/t"
"$smoke_dir/t"
"$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 > "$smoke_dir/CMakeLists.txt" <<'EOF'
cat > "$test_dir/CMakeLists.txt" <<'EOF'
cmake_minimum_required(VERSION 3.16)
project(smoke CXX)
add_executable(smoke t.cpp)
project(test CXX)
add_executable(test t.cpp)
EOF
"$work/bin/cmake" -S "$smoke_dir" -B "$smoke_dir/build" -G Ninja \
"$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 "$smoke_dir/build" >/dev/null
"$smoke_dir/build/smoke"
"$work/bin/cmake" --build "$test_dir/build" >/dev/null
"$test_dir/build/test"
rm -rf "$smoke_dir"
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))"
+28 -108
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
@@ -46,35 +59,26 @@ Press **F10** while the game window has focus:
- Internal resolution
- FPS counter
- 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.
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) and the game reads it as an actual Wii Remote through KPAD: Wii Remote icons and
prompts, Wii Wheel tilt steering, wheelies and tricks all come from the game's own motion code.
Nunchuk and Classic Controller are real Wii extensions too: the game gets the Nunchuk's stick,
C/Z and accelerometer, and the Classic Controller through `KPADGetUnifiedWpadStatus` with its own
layout and icons, so its buttons do what the game says they do and no mapping is involved. Plug an
extension in or pull it out mid-game and the game switches control scheme like on the console
(the runtime patches SDL's Wii driver, which otherwise loses the remote for good on an extension
change). Only the Wii U Pro Controller, which has no Wii-era equivalent, is fed to the game as a
GameCube pad with Nintendo's layout. If a remote drops out or was switched on after launch, the
runtime keeps rescanning Bluetooth until it comes back (F10 > Controller settings > Wii Remotes). SDL's read of
the remote's factory accelerometer calibration often times out over Bluetooth (`console.log`
then says "Using fallback accelerometer calibration") and it falls back to a nominal zero point,
so the same menu has a one-button calibration (remote flat, buttons up) that removes the small
tilt offset some remotes show.
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
@@ -115,92 +119,6 @@ Wheel Wizard downloads the setup tool from this repo and walks you through insta
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
### iOS
Requires macOS with Xcode. Build the `WiiCompiled` target for iOS arm64; the build writes
`WiiCompiled-unsigned.ipa` next to the app bundle.
```sh
cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_SYSTEM_NAME=iOS -DCMAKE_SYSTEM_PROCESSOR=arm64 \
-DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_SYSROOT=iphoneos \
-DCMAKE_OSX_DEPLOYMENT_TARGET=17.0 \
-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH \
-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY \
-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY \
-DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE \
-DAURORA_DAWN_PROVIDER=package
cmake --build build-ios --target WiiCompiled
```
#### iOS from Linux
The same .ipa builds on a Linux host with upstream clang and lld; no Theos, xtool or Xcode. Tested
on Debian 13 with `clang-19 lld-19 llvm-19 cmake ninja-build git`. The only Apple piece is the
iPhoneOS SDK, and [xybp888/iOS-SDKs](https://github.com/xybp888/iOS-SDKs) carries current ones:
```sh
git clone --depth 1 --filter=blob:none --sparse https://github.com/xybp888/iOS-SDKs.git /opt/iOS-SDKs
git -C /opt/iOS-SDKs sparse-checkout set --no-cone iPhoneOS26.5.sdk
```
Pass that directory as `IOS_SDK` (or copy `iPhoneOS.sdk` out of a Mac's Xcode to `/opt/iPhoneOS.sdk`,
the default). The translated shard manifest under `generated/` records absolute paths from the
machine that ran the translator, so either translate on the Linux host or symlink that path to your
checkout.
```sh
cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE=cmake/ios-linux-toolchain.cmake -DIOS_SDK=/opt/iOS-SDKs/iPhoneOS26.5.sdk \
-DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE -DAURORA_DAWN_PROVIDER=package
cmake --build build-ios --target WiiCompiled
```
#### iOS from Windows
The same toolchain file works on Windows with [llvm-mingw](https://github.com/mstorsjo/llvm-mingw),
which is what the Windows build already uses. Three things differ from a Linux host:
- llvm-mingw does not ship `ld64.lld.exe` or `llvm-install-name-tool.exe`; LLVM tools dispatch on
their file name, so copy `ld.lld.exe` to `ld64.lld.exe` and `llvm-objcopy.exe` to
`llvm-install-name-tool.exe` in its `bin` directory.
- Git for Windows checks the SDK repo's symlinks out as small text files unless `core.symlinks`
is on (Developer Mode). Either enable that before cloning, or replace each placeholder with a
copy of its target; `libSystem.tbd` is one of them and the link fails without it.
- Pass the toolchain file as an absolute path, and `MKW_IOS_LLVM_BIN` as the llvm-mingw `bin`
directory with forward slashes.
```powershell
cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release `
-DCMAKE_TOOLCHAIN_FILE=C:/src/Wiicompiled/runtime/cmake/ios-linux-toolchain.cmake `
-DIOS_SDK=C:/iOS-SDKs/iPhoneOS26.5.sdk -DMKW_IOS_LLVM_BIN=C:/llvm-mingw/bin `
-DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE -DAURORA_DAWN_PROVIDER=package
cmake --build build-ios --target WiiCompiled
```
Install it with AltStore or SideStore, which sign with your own Apple ID. The app needs the
`com.apple.developer.kernel.increased-memory-limit` entitlement or it exits at startup;
[GetMoreRam](https://github.com/hugeBlack/GetMoreRam) grants it with a free Apple ID.
GetMoreRam grants `com.apple.developer.kernel.extended-virtual-addressing` at the same time, which
matters on devices with less than 4 GB of RAM: the runtime reserves a 4 GiB guest address space at
startup and that is right on the limit iOS allows without it. It makes no difference on the 8 GB
devices this was developed on.
`runtime/cmake/ios/WiiCompiled.entitlements` is a template for signing by hand, with placeholders to
replace; the sideloaders build their own and ignore it. It carries both entitlements, so the
provisioning profile you sign with needs both capabilities enabled.
Copy `Config.toml` and an extracted `DATA` directory into the app's Documents folder, using Files on
the device or the Finder with it connected. Keep `dvd_root` relative:
```toml
[paths]
dvd_root = "DATA"
```
Touch controls appear when no gamepad is attached; tap the FPS readout for settings.
> [!CAUTION]
> Only take builds from this repository's
> [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an
@@ -239,7 +157,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
@@ -288,7 +208,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
+5 -3
View File
@@ -114,14 +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
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> |
+27 -6
View File
@@ -151,15 +151,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}")
+8
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);
+16 -1
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;
@@ -229,7 +245,6 @@ s32 PADGetNativeButtonPressed(u32 port);
PADSignedNativeAxis PADGetNativeAxisPulled(u32 port);
void PADRestoreDefaultMapping(u32 port);
void PADBlockInput(bool block);
bool PADIsInputBlocked(void);
/**
* Set the default controller mapping used.
+31 -17
View File
@@ -225,7 +225,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.
@@ -689,15 +689,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());
}
@@ -714,18 +722,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 {}",
@@ -1661,7 +1679,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();
@@ -1671,11 +1689,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);
}
@@ -1752,7 +1768,7 @@ bool begin_frame() noexcept {
return prepared;
}
void end_frame() noexcept {
void end_frame() {
#ifdef AURORA_ENABLE_GX
webgpu::fail_if_device_lost();
#endif
@@ -1768,10 +1784,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;
@@ -1797,6 +1810,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
@@ -1859,10 +1876,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();
@@ -1870,6 +1883,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",
};
@@ -1895,8 +1909,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;
}
@@ -1907,6 +1920,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",
};
+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
+5 -3
View File
@@ -520,9 +520,11 @@ void GXCopyTex(void* dest, GXBool clear) {
clearState.clearAlpha = clear && alphaUpdate;
}
const auto copyFilter = combined_copy_filter_coefficients(g_gxState.copyFilterVFilter);
// Skip only recurring color copies so one-shot copies are never lost.
const bool producedConsecutively = handle.revision != 0 && currentFrame - handle.lastProducedFrame <= 1;
const bool persistentCopy = !aurora::gx::is_depth_format(texCopyFmt) && !producedConsecutively;
// Every GXCopyTex is observable texture data. Reusing a destination in this
// or the previous frame does not guarantee another redraw: menu thumbnail
// scratch targets can be reused and then retained. Depth copies have the
// same requirement. Only display presentation may skip unfinished draws.
const bool persistentCopy = true;
aurora::gfx::resolve_pass(handle.handle, rect, clearState.clearColor, clearState.clearAlpha, clearState.clearDepth,
clearState.clearColorValue, aurora::gx::clear_depth_value(), resolveFmt,
&sourceRect.sampleRect, g_gxState.texCopyHalfScale, &copyFilter, forceOpaqueAlpha,
+39 -12
View File
@@ -319,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) {
@@ -724,10 +736,10 @@ u32 PADRead(PADStatus* status) {
}
status[i].err = PAD_ERR_NONE;
if (g_keyboardBindings[i].m_mappingsSet) {
if (g_keyboardBindings[i].m_mappingsSet && SDL_GetKeyboardFocus() != nullptr) {
std::ranges::for_each(
g_keyboardBindings[i].m_buttonMapping, [&kbState, &i, &status](const PADKeyButtonBinding& mapping) {
if (mapping.scancode > PAD_KEY_INVALID && kbState[mapping.scancode]) {
g_keyboardBindings[i].m_buttonMapping, [&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;
@@ -788,7 +800,7 @@ 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
@@ -835,7 +847,7 @@ u32 PADRead(PADStatus* status) {
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))) {
if (is_native_binding_pressed(controller->m_controller, mapping.nativeButton)) {
status[i].button |= mapping.padButton;
}
@@ -852,7 +864,7 @@ u32 PADRead(PADStatus* status) {
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;
}
@@ -946,6 +958,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;
@@ -990,12 +1013,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) {
@@ -1278,6 +1302,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;
}
@@ -1627,8 +1656,6 @@ void PADBlockInput(const bool block) {
}
}
bool PADIsInputBlocked() { return g_blockPAD.load(std::memory_order_acquire); }
SDL_Gamepad* PADGetSDLGamepadForIndex(const u32 index) {
const auto* ctrl = __PADGetControllerForIndex(index);
+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};
}
+197 -53
View File
@@ -1,4 +1,5 @@
#include "common.hpp"
#include "staging_map.hpp"
#include "../gx/shader_info.hpp"
#include "clear.hpp"
@@ -36,10 +37,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 +132,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 +167,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 +233,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 +256,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 +325,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 +472,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 +532,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 +559,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 +592,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 +788,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 +812,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 +901,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 +1007,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 +1027,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 +1072,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 +1101,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 +1140,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 +1162,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 +1171,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 +1184,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 +1227,68 @@ 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]);
}
}
// GXCopyTex can capture the ordinary EFB as well as an explicit offscreen
// target. Its command may arrive in the next batch, after this prefix has
// already been submitted. Preserve every prefix; target kind cannot tell
// us whether the guest will later retain these pixels in a texture.
for (auto& pass : g_renderPasses) pass.requireReadyPipelines = true;
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 +1321,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 +1374,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 +1452,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)) {
if (finalize && !debugFrame.groups.empty()) {
for (auto& it : std::ranges::reverse_view(debugFrame.groups)) {
Log.warn("Debug group was not popped at end of frame: {}", it);
}
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 +1476,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 +1511,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 +1545,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 +1591,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;
}
@@ -1470,8 +1614,8 @@ bool bind_pipeline(PipelineRef ref, const wgpu::RenderPassEncoder& pass, Pipelin
if (!skip_unready_pipelines()) {
pipelineReady = wait_pipeline(ref, pipeline);
} else if (requireReady) {
// The pass resolves into a persistent texture (a one-shot bake such as MKW's minimap), so a
// skipped draw would never be re-issued. These run behind loads, not mid-race.
// Texture copies and capacity prefixes must retain complete draw results.
// A future display frame cannot repair a texture that already captured them.
pipelineReady = wait_pipeline_for_persistent_pass(ref, pipeline);
} else {
pipelineReady = try_pipeline(ref, pipeline);
@@ -1600,8 +1744,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 +1754,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
+13 -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,16 @@ 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;
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 +337,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 +354,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 +362,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 {
+9
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;
@@ -481,6 +483,13 @@ bool controller_has_rumble(Uint32 instance) noexcept {
void controller_rumble(uint32_t instance, uint16_t low_freq_intensity, uint16_t high_freq_intensity,
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];
+8
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) {
@@ -738,11 +742,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 {
+2 -18
View File
@@ -11,9 +11,6 @@
#include <aurora/event.h>
#include <aurora/gfx.h>
#include <aurora/render_size_limits.hpp>
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
#include <SDL3/SDL_error.h>
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_keyboard.h>
@@ -411,21 +408,8 @@ bool create_window(AuroraBackend backend) {
height = 480;
}
Sint32 posX = g_config.hasWindowPosition ? g_config.windowPosX : SDL_WINDOWPOS_CENTERED;
Sint32 posY = g_config.hasWindowPosition ? g_config.windowPosY : SDL_WINDOWPOS_CENTERED;
#if defined(__APPLE__) && TARGET_OS_IPHONE
// Without this the window falls back to the 1280x960 default on any device
// with no saved size.
if (const SDL_DisplayID primary = SDL_GetPrimaryDisplay(); primary != 0) {
SDL_Rect bounds{};
if (SDL_GetDisplayBounds(primary, &bounds) && bounds.w > 0 && bounds.h > 0) {
width = bounds.w;
height = bounds.h;
}
posX = SDL_WINDOWPOS_CENTERED_DISPLAY(primary);
posY = SDL_WINDOWPOS_CENTERED_DISPLAY(primary);
}
#endif
const Sint32 posX = g_config.hasWindowPosition ? g_config.windowPosX : SDL_WINDOWPOS_CENTERED;
const Sint32 posY = g_config.hasWindowPosition ? g_config.windowPosY : SDL_WINDOWPOS_CENTERED;
const auto props = SDL_CreateProperties();
TRY(SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, g_config.appName), "Failed to set {}: {}",
+13
View File
@@ -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");
}
+90 -9
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);
@@ -4186,7 +4267,7 @@ TEST_F(GXFifoTest, CopyTexColorFormatMarksResolvePersistent) {
EXPECT_TRUE(records.front().persistentCopy);
}
TEST_F(GXFifoTest, RecurringColorCopyKeepsLaterResolveSkippable) {
TEST_F(GXFifoTest, RecurringColorCopyPreservesEveryResolve) {
std::array<u8, 152 * 114 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24;
@@ -4200,7 +4281,7 @@ TEST_F(GXFifoTest, RecurringColorCopyKeepsLaterResolveSkippable) {
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 2u);
EXPECT_TRUE(records[0].persistentCopy);
EXPECT_FALSE(records[1].persistentCopy);
EXPECT_TRUE(records[1].persistentCopy);
}
TEST_F(GXFifoTest, ColorCopyAfterFrameGapRegainsPersistentProtection) {
@@ -4220,7 +4301,7 @@ TEST_F(GXFifoTest, ColorCopyAfterFrameGapRegainsPersistentProtection) {
EXPECT_TRUE(records[1].persistentCopy);
}
TEST_F(GXFifoTest, CopyTexDepthFormatKeepsResolveSkippable) {
TEST_F(GXFifoTest, CopyTexDepthFormatPreservesResolve) {
std::array<u8, 4 * 4 * 4> image{};
gxState().pixelFmt = GX_PF_RGBA6_Z24;
@@ -4230,7 +4311,7 @@ TEST_F(GXFifoTest, CopyTexDepthFormatKeepsResolveSkippable) {
const auto& records = aurora::gfx::testing::resolve_pass_records();
ASSERT_EQ(records.size(), 1u);
EXPECT_FALSE(records.front().persistentCopy);
EXPECT_TRUE(records.front().persistentCopy);
}
TEST_F(GXFifoTest, CopyDispResolveIsNotPersistent) {
+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,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
+67 -61
View File
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.16)
cmake_minimum_required(VERSION 3.16)
project(mkw_recompiled)
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|AppleClang)$" OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
@@ -7,13 +7,6 @@ endif()
if(WIN32 AND MINGW AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_PLATFORM_WINDOWS TRUE)
elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS")
# Checked before macOS: APPLE is true for iOS too, so the macOS branch would
# otherwise claim an iOS build and select a backend that cannot compile
# there (<mach/mach_vm.h> is absent from the iOS SDK) or run there (/tmp is
# outside the sandbox).
set(MKW_PLATFORM_IOS TRUE)
set(MKW_PLATFORM_MACOS TRUE) # shares the Apple arm64 substrate
elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|ARM64)$")
# The first native macOS target is Apple Silicon. Intel and universal
# binaries remain future compatibility work; do not silently claim them.
@@ -22,16 +15,13 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(
set(MKW_PLATFORM_LINUX TRUE)
else()
message(FATAL_ERROR
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux "
"x86_64/aarch64, or Apple Clang on macOS arm64 or iOS arm64")
"WiiCompiled supports 64-bit LLVM-MinGW Clang on Windows, native Linux x86_64/aarch64, or Apple Clang on macOS arm64")
endif()
if(NOT CMAKE_BUILD_TYPE STREQUAL "Release")
message(FATAL_ERROR "WiiCompiled only supports Release builds")
endif()
option(MKW_BUILD_PRODUCTS "Build translated WiiCompiled product targets" ON)
set(MKW_IOS_BUNDLE_ID_PREFIX "it.tobyfox" CACHE STRING
"Reverse-DNS prefix for the iOS bundle identifier")
# 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
@@ -40,11 +30,6 @@ set(MKW_IOS_BUNDLE_ID_PREFIX "it.tobyfox" CACHE STRING
# libraries and the products in cmake/PublicProducts.cmake) inherits them while
# aurora-main and its third-party tree stay unaffected.
set(MKW_PROJECT_COMPILE_DEFINITIONS NOMINMAX)
if(MKW_PLATFORM_IOS)
# One project-wide answer to "is this iOS", so sources do not each pull in
# TargetConditionals.h and spell out the __APPLE__ && TARGET_OS_IPHONE dance.
list(APPEND MKW_PROJECT_COMPILE_DEFINITIONS MKW_PLATFORM_IOS=1)
endif()
set(CMAKE_CXX_STANDARD 17)
@@ -122,6 +107,43 @@ 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).
# Fetched at build time from a pinned upstream release tarball with a checked SHA-256, the same way
# aurora-main's own dependencies (SDL, zlib, etc.) are pulled in - not committed as a vendored
# source tree, so the repository ships the compiled dependency rather than ~280 tracked upstream
# files. Bump MKW_MBEDTLS_VERSION/MKW_MBEDTLS_SHA256 together when updating; the hash comes from
# upstream's own 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(FetchContent)
set(MKW_MBEDTLS_VERSION "3.6.7")
set(MKW_MBEDTLS_SHA256 "a7e8bcbec0e6f761b4af24f25677626b35f762f68eef79c08677a363212d11f6")
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})
# Subproject mode already defaults ENABLE_TESTING off and skips codegen (GEN_FILES), but
# ENABLE_PROGRAMS defaults on and installation/package-config isn't wanted for a linked-in copy.
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()
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.")
@@ -151,16 +173,6 @@ else()
if(NOT EXISTS ${MKW_AURORA_DIR}/CMakeLists.txt)
message(FATAL_ERROR "Requested aurora-main but ${MKW_AURORA_DIR} is missing")
endif()
# aurora picks shared third-party libraries whenever BUILD_SHARED_LIBS is
# merely undefined, and its extern/ tree calls unset(BUILD_SHARED_LIBS CACHE)
# while working around xxhash, which deletes any -D the user passed. Defining
# it here, before aurora is added, is what actually sticks across
# reconfigures. Apple builds ship a self-contained bundle, so a dylib
# resolved from the build tree or from Homebrew cannot be allowed to leak
# into the link.
if(APPLE)
set(BUILD_SHARED_LIBS OFF)
endif()
set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE)
if(MKW_PLATFORM_WINDOWS)
set(DAWN_ENABLE_D3D12 ON CACHE BOOL "" FORCE)
@@ -274,18 +286,8 @@ if(MKW_PLATFORM_MACOS)
# 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")
set_source_files_properties("${CMAKE_CURRENT_LIST_DIR}/src/platform/macos/co_switch.S"
PROPERTIES LANGUAGE ASM SKIP_UNITY_BUILD_INCLUSION ON SKIP_PRECOMPILE_HEADERS ON)
# iOS cannot use the macOS backend: <mach/mach_vm.h> is absent from that SDK
# and /tmp is outside the sandbox.
if(MKW_PLATFORM_IOS)
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
else()
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_ios.cpp")
endif()
else()
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_macos.cpp")
list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_LIST_DIR}/src/guest_flat_memory_ios.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")
@@ -312,25 +314,31 @@ 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_touch_auto_accelerate_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/touch_auto_accelerate_tests.cpp"
"${CMAKE_CURRENT_LIST_DIR}/src/hle/input/touch_pad.cpp")
target_include_directories(mkw_touch_auto_accelerate_tests PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/include"
"${CMAKE_CURRENT_LIST_DIR}/src"
"${CMAKE_CURRENT_LIST_DIR}/third_party/toml11/single_include"
"${CMAKE_CURRENT_LIST_DIR}/third_party/toml11"
"${MKW_AURORA_DIR}/include"
"${CMAKE_BINARY_DIR}/_deps/imgui-src"
"${CMAKE_BINARY_DIR}/_deps/sdl-src/include")
if(TARGET mkw_native_prebuilt)
target_include_directories(mkw_touch_auto_accelerate_tests PRIVATE
$<TARGET_PROPERTY:mkw_native_prebuilt,INTERFACE_INCLUDE_DIRECTORIES>)
endif()
target_compile_features(mkw_touch_auto_accelerate_tests PRIVATE cxx_std_20)
target_compile_definitions(mkw_touch_auto_accelerate_tests PRIVATE SDL_MAIN_HANDLED TARGET_PC)
target_link_libraries(mkw_touch_auto_accelerate_tests PRIVATE mkw_platform)
add_test(NAME mkw_touch_auto_accelerate_tests COMMAND mkw_touch_auto_accelerate_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
@@ -347,11 +355,9 @@ if(MKW_PLATFORM_LINUX)
add_test(NAME mkw_linux_host_context_tests COMMAND mkw_linux_host_context_tests)
endif()
if(MKW_PLATFORM_MACOS AND NOT MKW_PLATFORM_IOS)
if(MKW_PLATFORM_MACOS)
# Exercise the Apple Silicon context ABI and the public host-memory
# contracts separately from translated products. Host-only: the flat memory
# test pulls in guest_flat_memory_macos.cpp, whose <mach/mach_vm.h> does not
# exist in the iOS SDK, and none of these can run on a device anyway.
# contracts separately from translated products.
enable_language(ASM)
add_executable(mkw_macos_context_abi_tests
"${CMAKE_CURRENT_LIST_DIR}/tests/macos_context_abi_tests.cpp"
@@ -421,7 +427,7 @@ else()
target_compile_definitions(mkw_macos_native_compile PRIVATE SDL_MAIN_HANDLED TARGET_PC)
target_link_libraries(mkw_macos_native_compile PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx
PNG::PNG mkw::pugixml mkw::toml11 mkw::cryptopp)
mkw::pugixml mkw::toml11 mkw::cryptopp)
set_target_properties(mkw_macos_native_compile PROPERTIES UNITY_BUILD OFF)
endif()
add_custom_target(mkw_platform_paths_check DEPENDS mkw_platform)
File diff suppressed because it is too large Load Diff
-8
View File
@@ -1,8 +0,0 @@
GameCube Button Icons and Controls - Zacksly
Licensed under CC BY 3.0 - https://zacksly.itch.io
The PNG files in this directory are taken unmodified from the "Buttons Outline /
White" set of that pack, at 256px. They are drawn tinted and partly transparent
at runtime, which is a rendering choice and not an edit to the artwork.
Full licence: http://creativecommons.org/licenses/by/3.0/
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

+17 -69
View File
@@ -77,18 +77,11 @@ add_library(mkw_runtime_common OBJECT ${SOURCES})
mkw_configure_object_target(mkw_runtime_common)
target_compile_features(mkw_runtime_common PRIVATE cxx_std_20)
target_compile_definitions(mkw_runtime_common PRIVATE
SDL_MAIN_HANDLED
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION)
if(NOT MKW_PLATFORM_IOS)
# iOS is the one target where SDL must own main(): its entry point is what
# drives UIApplicationMain. Defining this makes <SDL3/SDL_main.h> a no-op,
# so the app links a bare main() that UIKit never calls.
target_compile_definitions(mkw_runtime_common PRIVATE SDL_MAIN_HANDLED)
endif()
target_link_libraries(mkw_runtime_common PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
# The touch overlay decodes its button artwork at startup.
target_link_libraries(mkw_runtime_common PRIVATE PNG::PNG)
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp)
target_link_libraries(mkw_runtime_common PRIVATE mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
if(MKW_PLATFORM_WINDOWS)
target_link_libraries(mkw_runtime_common PRIVATE shell32 windowsapp)
elseif(MKW_PLATFORM_LINUX)
@@ -199,17 +192,14 @@ function(mkw_configure_product target)
"${MKW_RUNTIME_SOURCE_DIR}/.."
"${MKW_RUNTIME_SOURCE_DIR}/../aurora-main/include")
target_compile_definitions(${target} PRIVATE
_DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION TARGET_PC)
if(NOT MKW_PLATFORM_IOS)
target_compile_definitions(${target} PRIVATE SDL_MAIN_HANDLED)
endif()
SDL_MAIN_HANDLED _DISABLE_STRING_ANNOTATION _DISABLE_VECTOR_ANNOTATION TARGET_PC)
target_compile_features(${target} PRIVATE cxx_std_20)
mkw_apply_common_compile_options(${target})
# The dispatch-table and registration shards compile inside the product target itself and
# include the same fat translated headers; bound them by the same pool.
mkw_bound_translated_compiles(${target})
target_link_libraries(${target} PRIVATE
mkw_platform mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp)
mkw_platform mkw_base_shared mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
target_link_libraries(${target} PRIVATE
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
@@ -231,26 +221,6 @@ function(mkw_configure_product target)
$<TARGET_FILE:sqlite3> $<TARGET_FILE_DIR:${target}>)
endif()
if(MKW_PLATFORM_IOS AND NOT CMAKE_HOST_APPLE)
set(shim "${MKW_RUNTIME_SOURCE_DIR}/src/platform/ios/compiler_rt_shim.c")
target_sources(${target} PRIVATE "${shim}")
set_source_files_properties("${shim}" PROPERTIES SKIP_PRECOMPILE_HEADERS ON SKIP_UNITY_BUILD_INCLUSION ON)
endif()
if(MKW_PLATFORM_IOS)
# CMake bundles iOS targets with its own default Info.plist, whose
# CFBundleIdentifier is empty and which carries none of the iOS keys, so
# iOS refuses to install the result. Supply a real one.
set_target_properties(${target} PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_LIST_DIR}/ios/Info.plist.in"
MACOSX_BUNDLE_EXECUTABLE_NAME "${target}"
MACOSX_BUNDLE_BUNDLE_NAME "${target}"
MACOSX_BUNDLE_GUI_IDENTIFIER "${MKW_IOS_BUNDLE_ID_PREFIX}.wiicompiled"
MACOSX_BUNDLE_BUNDLE_VERSION "1"
MACOSX_BUNDLE_SHORT_VERSION_STRING "1.0")
endif()
if(MKW_PLATFORM_WINDOWS)
target_link_libraries(${target} PRIVATE
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
@@ -293,19 +263,6 @@ function(mkw_configure_product target)
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory
"${MKW_WII_BOOTSTRAP_SOURCE_DIR}" "$<TARGET_FILE_DIR:${target}>/wii_bootstrap")
# Touch control artwork. Only the touch build reads these, but they are copied
# everywhere the other assets are so the bundle layout stays uniform.
set(MKW_TOUCH_ASSET_DIR "${MKW_RUNTIME_SOURCE_DIR}/assets/touch")
if(NOT EXISTS "${MKW_TOUCH_ASSET_DIR}/a.png")
message(FATAL_ERROR "Missing touch control artwork: ${MKW_TOUCH_ASSET_DIR}")
endif()
# Cleared first: copy_directory merges, so a removed icon would otherwise
# linger in the bundle and get shipped.
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E rm -rf "$<TARGET_FILE_DIR:${target}>/touch"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${MKW_TOUCH_ASSET_DIR}" "$<TARGET_FILE_DIR:${target}>/touch")
set(MKW_DSP_COEFFICIENT_ROM "${MKW_RUNTIME_SOURCE_DIR}/assets/dsp/dsp_coef.bin")
if(NOT EXISTS "${MKW_DSP_COEFFICIENT_ROM}")
message(FATAL_ERROR "Missing Wii DSP coefficient ROM: ${MKW_DSP_COEFFICIENT_ROM}")
@@ -330,24 +287,20 @@ function(mkw_configure_product target)
"${MKW_INITIAL_PIPELINE_CACHE}"
"$<TARGET_FILE_DIR:${target}>/initial_pipeline_cache.db")
if(MKW_PLATFORM_IOS)
# An .ipa is a zip with the bundle under Payload/. Left unsigned: the
# sideloaders people actually install with (AltStore, SideStore) sign on
# the device with the user's own Apple ID, and a signature applied here
# would only be replaced. WiiCompiled.entitlements beside this file names
# the one entitlement the runtime needs, for whatever does the signing.
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E rm -rf "$<TARGET_FILE_DIR:${target}>/../ipa"
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:${target}>/../ipa/Payload"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"$<TARGET_BUNDLE_DIR:${target}>"
"$<TARGET_FILE_DIR:${target}>/../ipa/Payload/${target}.app"
COMMAND ${CMAKE_COMMAND} -E chdir "$<TARGET_FILE_DIR:${target}>/../ipa"
${CMAKE_COMMAND} -E tar cf "${CMAKE_BINARY_DIR}/${target}-unsigned.ipa" --format=zip Payload
COMMAND ${CMAKE_COMMAND} -E rm -rf "$<TARGET_FILE_DIR:${target}>/../ipa"
COMMENT "Packaging ${target}-unsigned.ipa")
# Non-Windows TLS (runtime/src/hle/net/network_ssl.cpp's mbed TLS path) needs a trusted root
# CA bundle to verify server certificates against - Windows gets this for free from the OS via
# Schannel, mbed TLS does not ship one itself. Not SHA256-pinned like the DSP ROM above: unlike
# a fixed hardware ROM, this bundle is expected to be refreshed periodically as CAs rotate.
# Windows gets its trust store from Schannel, so only the platforms that actually build the
# mbed TLS path need the bundle beside the executable.
if(NOT MKW_PLATFORM_WINDOWS)
set(MKW_CA_CERTIFICATE_BUNDLE "${MKW_RUNTIME_SOURCE_DIR}/assets/certs/cacert.pem")
if(NOT EXISTS "${MKW_CA_CERTIFICATE_BUNDLE}")
message(FATAL_ERROR "Missing TLS root CA bundle: ${MKW_CA_CERTIFICATE_BUNDLE}")
endif()
add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${MKW_CA_CERTIFICATE_BUNDLE}" "$<TARGET_FILE_DIR:${target}>/cacert.pem")
endif()
endfunction()
add_executable(WiiCompiled "${MKW_BASE_PRODUCT_SOURCE}" ${MKW_BASE_REGISTRATION_SOURCES})
@@ -381,11 +334,6 @@ endif()
# tuning rather than leaving target-specific performance on the table.
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$")
set(MKW_BASELINE_ARCH_FLAG -march=x86-64-v3)
elseif(MKW_PLATFORM_IOS)
# iOS 17 runs on the A12 and later, and the build host is never the device;
# -mcpu=native there would tune for whatever Mac (or Linux box) did the
# compile, and upstream clang rejects it when cross-compiling.
set(MKW_BASELINE_ARCH_FLAG -mcpu=apple-a12)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
set(MKW_BASELINE_ARCH_FLAG -mcpu=native)
else()
-71
View File
@@ -1,71 +0,0 @@
# Cross-compile the iOS arm64 products from a Linux host.
#
# Needs upstream clang and lld (Debian 13: clang-19 lld-19 llvm-19) and an
# iPhoneOS SDK, either copied out of Xcode or sparse-cloned from
# github.com/xybp888/iOS-SDKs. Nothing from Theos or xtool. See README.md,
# "iOS from Linux".
#
# cmake -S runtime -B build-ios -G Ninja -DCMAKE_BUILD_TYPE=Release \
# -DCMAKE_TOOLCHAIN_FILE=cmake/ios-linux-toolchain.cmake \
# -DCMAKE_DISABLE_FIND_PACKAGE_absl=TRUE -DAURORA_DAWN_PROVIDER=package
set(IOS_SDK "/opt/iPhoneOS.sdk" CACHE PATH "iPhoneOS SDK copied from Xcode")
set(MKW_IOS_LLVM_BIN "/usr/lib/llvm-19/bin" CACHE PATH "Directory holding clang, ld64.lld and llvm-ar")
set(MKW_IOS_CLANG_RT "" CACHE FILEPATH
"Optional: Xcode's libclang_rt.ios.a. Left empty, src/platform/ios/compiler_rt_shim.c covers what the runtime needs")
list(APPEND CMAKE_TRY_COMPILE_PLATFORM_VARIABLES IOS_SDK MKW_IOS_LLVM_BIN MKW_IOS_CLANG_RT)
set(CMAKE_SYSTEM_NAME iOS)
set(CMAKE_SYSTEM_PROCESSOR arm64)
set(CMAKE_OSX_ARCHITECTURES arm64 CACHE STRING "")
set(CMAKE_OSX_DEPLOYMENT_TARGET 17.0 CACHE STRING "")
set(CMAKE_OSX_SYSROOT "${IOS_SDK}" CACHE PATH "")
set(CMAKE_SYSROOT "${IOS_SDK}")
set(x "${CMAKE_HOST_EXECUTABLE_SUFFIX}")
set(CMAKE_C_COMPILER "${MKW_IOS_LLVM_BIN}/clang${x}")
set(CMAKE_CXX_COMPILER "${MKW_IOS_LLVM_BIN}/clang++${x}")
set(CMAKE_OBJC_COMPILER "${MKW_IOS_LLVM_BIN}/clang${x}")
set(CMAKE_OBJCXX_COMPILER "${MKW_IOS_LLVM_BIN}/clang++${x}")
set(CMAKE_ASM_COMPILER "${MKW_IOS_LLVM_BIN}/clang${x}")
set(CMAKE_AR "${MKW_IOS_LLVM_BIN}/llvm-ar${x}" CACHE FILEPATH "")
set(CMAKE_RANLIB "${MKW_IOS_LLVM_BIN}/llvm-ranlib${x}" CACHE FILEPATH "")
set(CMAKE_STRIP "${MKW_IOS_LLVM_BIN}/llvm-strip${x}" CACHE FILEPATH "")
# Not every LLVM distribution ships these (llvm-mingw does not); nothing in
# this build needs them, but CMake's Darwin support likes to know where they are.
foreach(tool INSTALL_NAME_TOOL:install-name-tool LIPO:lipo OTOOL:otool)
string(REPLACE ":" ";" tool "${tool}")
list(GET tool 0 var)
list(GET tool 1 exe)
if(EXISTS "${MKW_IOS_LLVM_BIN}/llvm-${exe}${x}")
set(CMAKE_${var} "${MKW_IOS_LLVM_BIN}/llvm-${exe}${x}" CACHE FILEPATH "")
endif()
endforeach()
set(CMAKE_LINKER "${MKW_IOS_LLVM_BIN}/ld64.lld${x}" CACHE FILEPATH "")
foreach(lang C CXX OBJC OBJCXX ASM)
set(CMAKE_${lang}_COMPILER_TARGET arm64-apple-ios17.0)
endforeach()
# Apple's clang searches the SDK's SubFrameworks implicitly and UIKit's own
# headers import from there.
foreach(lang C CXX OBJC OBJCXX)
set(CMAKE_${lang}_FLAGS_INIT "-iframework ${IOS_SDK}/System/Library/SubFrameworks")
endforeach()
# The SDK carries its own libc++ headers and Apple's clang uses those. Upstream
# clang prefers a libc++ shipped beside itself when there is one (llvm-mingw
# does), which pairs the wrong C++ library with Darwin's C headers.
foreach(lang CXX OBJCXX)
string(APPEND CMAKE_${lang}_FLAGS_INIT " -nostdinc++ -isystem ${IOS_SDK}/usr/include/c++/v1")
endforeach()
set(CMAKE_EXE_LINKER_FLAGS_INIT "-fuse-ld=lld ${MKW_IOS_CLANG_RT}")
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-fuse-ld=lld ${MKW_IOS_CLANG_RT}")
set(CMAKE_MODULE_LINKER_FLAGS_INIT "-fuse-ld=lld ${MKW_IOS_CLANG_RT}")
string(STRIP "${CMAKE_EXE_LINKER_FLAGS_INIT}" CMAKE_EXE_LINKER_FLAGS_INIT)
set(CMAKE_FIND_ROOT_PATH "${IOS_SDK}")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
-26
View File
@@ -1,26 +0,0 @@
<?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>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key><string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
<key>CFBundleName</key><string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundleDisplayName</key><string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundleVersion</key><string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
<key>CFBundleShortVersionString</key><string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>LSRequiresIPhoneOS</key><true/>
<key>MinimumOSVersion</key><string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
<key>UIDeviceFamily</key><array><integer>1</integer><integer>2</integer></array>
<key>UILaunchScreen</key><dict/>
<key>UIRequiresFullScreen</key><true/>
<key>CADisableMinimumFrameDuration</key><true/>
<key>UIFileSharingEnabled</key><true/>
<key>LSSupportsOpeningDocumentsInPlace</key><true/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
@@ -1,11 +0,0 @@
<?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>application-identifier</key><string>CHANGEME.com.example.wiicompiled</string>
<key>com.apple.developer.team-identifier</key><string>CHANGEME</string>
<key>get-task-allow</key><true/>
<key>com.apple.developer.kernel.increased-memory-limit</key><true/>
<key>com.apple.developer.kernel.extended-virtual-addressing</key><true/>
</dict>
</plist>
+1
View File
@@ -58,6 +58,7 @@ inline void Flush(bool force = false) {
// still active. A window close is an intentional successful exit, so end the
// process directly and do not run the crash/atexit paths.
[[noreturn]] inline void ExitForAuroraWindowClose() noexcept {
settings_overlay::ReleaseControllers();
WindowPlacementPersistence::Flush(true);
#if defined(_WIN32)
::ExitProcess(0);
+20 -89
View File
@@ -1,38 +1,28 @@
#pragma once
#include "runtime_config.h"
#include "nand_path.h"
#include "nand_settings.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <optional>
#include <random>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
namespace RuntimeConsoleIdentity {
struct Identity {
std::string serial;
std::string productCode;
std::string area;
std::string gameRegion;
std::array<uint8_t, 6> mac;
};
inline bool IsValidSerial(const std::string& serial) {
return serial.size() == 9 &&
serial != "000000000" &&
std::all_of(serial.begin(), serial.end(),
[](unsigned char value) { return std::isdigit(value) != 0; });
}
inline Identity FromSerial(std::string serial) {
// Keep Nintendo's Wii OUI. The suffix is derived from the persisted serial
// Keep Nintendo's Wii OUI. The suffix is derived from the NAND serial
// so every API exposes one coherent, stable virtual-console identity.
uint32_t hash = 2166136261u;
for (const unsigned char value : serial) {
@@ -46,6 +36,7 @@ inline Identity FromSerial(std::string serial) {
return {
std::move(serial),
{}, {}, {},
{
0x00,
0x09,
@@ -57,83 +48,23 @@ inline Identity FromSerial(std::string serial) {
};
}
inline std::optional<std::string> ReadSerial(const std::filesystem::path& path) {
std::ifstream input(path);
std::string line;
if (!input || !std::getline(input, line)) {
return std::nullopt;
inline Identity LoadFromNand() {
const auto root = RuntimeNandPath::DiscoverNandRootPath();
const auto settings = RuntimeNandSettings::Read(root);
if (!settings || !RuntimeNandSettings::HasIdentity(*settings)) {
RuntimeNandPath::FailNandRoot(
"NAND setting.txt is missing or has invalid console identity fields (SERNO, CODE, AREA, GAME)",
root / "title/00000001/00000002/data/setting.txt");
}
constexpr std::string_view prefix = "serial=";
if (line.rfind(prefix, 0) != 0) {
return std::nullopt;
}
std::string serial = line.substr(prefix.size());
if (!IsValidSerial(serial)) {
return std::nullopt;
}
return serial;
}
inline bool WriteSerial(const std::filesystem::path& path, const std::string& serial) {
std::error_code ec;
std::filesystem::create_directories(path.parent_path(), ec);
if (ec) {
return false;
}
std::filesystem::path temporary = path;
temporary += ".tmp";
{
std::ofstream output(temporary, std::ios::trunc);
if (!output) {
return false;
}
output << "serial=" << serial << '\n';
output.close();
if (!output) {
return false;
}
}
std::filesystem::rename(temporary, path, ec);
if (!ec) {
return true;
}
std::filesystem::remove(temporary, ec);
return false;
}
inline std::string GenerateSerial() {
std::random_device entropy;
std::seed_seq seed{
entropy(),
entropy(),
entropy(),
entropy(),
};
std::mt19937 generator(seed);
std::uniform_int_distribution<uint32_t> distribution(100000000u, 999999999u);
return std::to_string(distribution(generator));
}
inline Identity LoadOrCreate(const std::filesystem::path& path) {
if (const auto serial = ReadSerial(path)) {
return FromSerial(*serial);
}
const std::string generated = GenerateSerial();
if (WriteSerial(path, generated)) {
return FromSerial(generated);
}
// Remain operational in a read-only environment. This fallback matches
// Dolphin's deterministic serial while keeping the same valid identity shape.
return FromSerial("123456789");
Identity identity = FromSerial(settings->at("SERNO"));
identity.productCode = settings->at("CODE");
identity.area = settings->at("AREA");
identity.gameRegion = settings->at("GAME");
return identity;
}
inline const Identity& Current() {
static const Identity identity =
LoadOrCreate(RuntimeConfigFile::ApplicationDataDirectory() / "ConsoleIdentity.txt");
static const Identity identity = LoadFromNand();
return identity;
}
+170
View File
@@ -0,0 +1,170 @@
#pragma once
// The single vocabulary shared by everything that has to turn a Config.toml
// controller name into a real button: the F10 settings bar, the macro engine,
// and the startup mapping pass. Keeping one table here means a name that the
// settings bar offers is always a name the config parser accepts, and vice
// versa; the two used to drift because each side carried its own copy.
#include <algorithm>
#include <array>
#include <cstdint>
#include <string>
#include <string_view>
#include <SDL3/SDL_gamepad.h>
#include <dolphin/pad.h>
namespace ControllerNames {
// A GameCube button as the game sees it, with the Config.toml key that selects
// it. Order matches RuntimeConfigFile::kControllerButtonKeys.
struct GameCubeButtonItem {
const char* configKey;
const char* label;
PADButton padButton;
};
inline constexpr std::array<GameCubeButtonItem, PAD_BUTTON_COUNT> kGameCubeButtons = {{
{"a", "A", PAD_BUTTON_A},
{"b", "B", PAD_BUTTON_B},
{"x", "X", PAD_BUTTON_X},
{"y", "Y", PAD_BUTTON_Y},
{"start", "Start", PAD_BUTTON_START},
{"z", "Z", PAD_TRIGGER_Z},
{"l", "L", PAD_TRIGGER_L},
{"r", "R", PAD_TRIGGER_R},
{"up", "D-pad Up", PAD_BUTTON_UP},
{"down", "D-pad Down", PAD_BUTTON_DOWN},
{"left", "D-pad Left", PAD_BUTTON_LEFT},
{"right", "D-pad Right", PAD_BUTTON_RIGHT},
}};
// A physical button on the host pad. Names are positional (south/east/...)
// rather than Xbox-labelled so one config reads the same on any hardware.
struct NativeButtonItem {
const char* configName;
const char* label;
uint32_t nativeButton;
};
inline constexpr auto kNativeButtons = std::to_array<NativeButtonItem>({
{"disabled", "Unmapped", PAD_NATIVE_BUTTON_DISABLED},
{"left_trigger", "Left trigger (LT / L2)", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFT_TRIGGER, false)},
{"right_trigger", "Right trigger (RT / R2)", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, false)},
{"left_stick_left", "Left stick left", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTX, true)},
{"left_stick_right", "Left stick right", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTX, false)},
{"left_stick_up", "Left stick up", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTY, true)},
{"left_stick_down", "Left stick down", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_LEFTY, false)},
{"right_stick_left", "Right stick left", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTX, true)},
{"right_stick_right", "Right stick right", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTX, false)},
{"right_stick_up", "Right stick up", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTY, true)},
{"right_stick_down", "Right stick down", PADEncodeAxisButton(SDL_GAMEPAD_AXIS_RIGHTY, false)},
{"unmapped", "Default", PAD_NATIVE_BUTTON_INVALID},
{"south", "South (A / Cross)", SDL_GAMEPAD_BUTTON_SOUTH},
{"east", "East (B / Circle)", SDL_GAMEPAD_BUTTON_EAST},
{"west", "West (X / Square)", SDL_GAMEPAD_BUTTON_WEST},
{"north", "North (Y / Triangle)", SDL_GAMEPAD_BUTTON_NORTH},
{"back", "Back / Select / Create", SDL_GAMEPAD_BUTTON_BACK},
{"guide", "Guide / Home / PS", SDL_GAMEPAD_BUTTON_GUIDE},
{"start", "Start / Options", SDL_GAMEPAD_BUTTON_START},
{"left_stick", "Left stick click (L3)", SDL_GAMEPAD_BUTTON_LEFT_STICK},
{"right_stick", "Right stick click (R3)", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
{"left_shoulder", "Left bumper (LB / L1)", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
{"right_shoulder", "Right bumper (RB / R1)", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
{"dpad_up", "D-pad Up", SDL_GAMEPAD_BUTTON_DPAD_UP},
{"dpad_down", "D-pad Down", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
{"dpad_left", "D-pad Left", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
{"dpad_right", "D-pad Right", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
{"misc1", "Misc 1 / Share / Mic", SDL_GAMEPAD_BUTTON_MISC1},
{"right_paddle1", "Right paddle 1", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
{"left_paddle1", "Left paddle 1", SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
{"right_paddle2", "Right paddle 2", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
{"left_paddle2", "Left paddle 2", SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
{"touchpad", "Touchpad click", SDL_GAMEPAD_BUTTON_TOUCHPAD},
{"misc2", "Misc 2", SDL_GAMEPAD_BUTTON_MISC2},
{"misc3", "Misc 3 / GC L click", SDL_GAMEPAD_BUTTON_MISC3},
{"misc4", "Misc 4 / GC R click", SDL_GAMEPAD_BUTTON_MISC4},
{"misc5", "Misc 5", SDL_GAMEPAD_BUTTON_MISC5},
{"misc6", "Misc 6", SDL_GAMEPAD_BUTTON_MISC6},
});
inline std::string TrimToken(std::string_view token) {
const size_t begin = token.find_first_not_of(" \t");
if (begin == std::string_view::npos) {
return {};
}
const size_t end = token.find_last_not_of(" \t");
return std::string(token.substr(begin, end - begin + 1));
}
inline const NativeButtonItem* FindNativeButton(std::string_view configName) {
const std::string name = TrimToken(configName.substr(0, configName.find('@')));
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
[&](const NativeButtonItem& item) { return name == item.configName; });
return it == kNativeButtons.end() ? nullptr : &*it;
}
// Falls back to the "unmapped" entry so callers always have a label to draw.
inline const NativeButtonItem& NativeButtonForValue(uint32_t nativeButton) {
const auto it = std::find_if(kNativeButtons.begin(), kNativeButtons.end(),
[&](const NativeButtonItem& item) { return PADAxisButtonIdentity(nativeButton) == PADAxisButtonIdentity(item.nativeButton); });
return it == kNativeButtons.end() ? *FindNativeButton("unmapped") : *it;
}
inline const GameCubeButtonItem* FindGameCubeButton(std::string_view configKey) {
const std::string key = TrimToken(configKey);
const auto it = std::find_if(kGameCubeButtons.begin(), kGameCubeButtons.end(),
[&](const GameCubeButtonItem& item) { return key == item.configKey; });
return it == kGameCubeButtons.end() ? nullptr : &*it;
}
// "up" or "up,a" -> the OR of those GC button bits. Unknown names are skipped so
// a typo costs one button instead of the whole macro.
inline uint16_t GameCubeMaskFromKeys(std::string_view keys) {
uint16_t mask = 0;
size_t begin = 0;
while (begin <= keys.size()) {
const size_t comma = keys.find(',', begin);
const std::string_view token =
keys.substr(begin, comma == std::string_view::npos ? std::string_view::npos : comma - begin);
if (const GameCubeButtonItem* item = FindGameCubeButton(token)) {
mask |= static_cast<uint16_t>(item->padButton);
}
if (comma == std::string_view::npos) {
break;
}
begin = comma + 1;
}
return mask;
}
inline std::string GameCubeKeysFromMask(uint16_t mask) {
std::string keys;
for (const auto& item : kGameCubeButtons) {
if ((mask & static_cast<uint16_t>(item.padButton)) == 0) {
continue;
}
if (!keys.empty()) {
keys += ',';
}
keys += item.configKey;
}
return keys;
}
inline std::string GameCubeLabelsFromMask(uint16_t mask) {
std::string labels;
for (const auto& item : kGameCubeButtons) {
if ((mask & static_cast<uint16_t>(item.padButton)) == 0) {
continue;
}
if (!labels.empty()) {
labels += " + ";
}
labels += item.label;
}
return labels.empty() ? std::string("None") : labels;
}
} // namespace ControllerNames
-14
View File
@@ -15,17 +15,6 @@ namespace GuestFlat {
// of a global.
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
inline constexpr size_t kGuestPageSize = 0x1000;
#ifdef MKW_PLATFORM_IOS
// No fixed base works on every device: probing an iPhone 17 Pro and an iPad
// Pro M5 gave disjoint sets of free 4 GiB windows (448 GiB only, versus 12-48
// GiB), and the extended-virtual-addressing entitlement changes neither. So the
// base is whatever the kernel hands out at Initialize().
//
// Note that memory_access.h routes every flat access to the checked path on
// Apple, so this has no reader yet. It is groundwork, not a hot path.
extern uint8_t* g_flatGuestBase;
#define MKW_FLAT_GUEST_BASE (GuestFlat::g_flatGuestBase)
#else
#if defined(__x86_64__)
// 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap
// placement.
@@ -51,10 +40,7 @@ inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0010'0000'0000ull;
#error "guest_flat_memory.h has no fixed flat guest base chosen for this architecture"
#endif
// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load
// of a global.
#define MKW_FLAT_GUEST_BASE (reinterpret_cast<uint8_t*>(GuestFlat::kFixedFlatGuestBase))
#endif
enum class Backing {
Owned,
+67
View File
@@ -0,0 +1,67 @@
#pragma once
// Per-port expression bindings for the GameCube controls, plus import of a
// Dolphin GCPadNew.ini.
#include <array>
#include <cstdint>
#include <string>
#include <dolphin/pad.h>
namespace InputBindings {
// The controls an expression can drive, in Dolphin's own naming so an
// imported config maps across without translation.
struct ControlInfo {
const char* dolphinName;
const char* label;
uint16_t padButton; // 0 for the analog-only controls below
int analog; // 0 none, 1 trigger L, 2 trigger R
};
inline constexpr std::array<ControlInfo, 14> kControls = {{
{"Buttons/A", "A", PAD_BUTTON_A, 0},
{"Buttons/B", "B", PAD_BUTTON_B, 0},
{"Buttons/X", "X", PAD_BUTTON_X, 0},
{"Buttons/Y", "Y", PAD_BUTTON_Y, 0},
{"Buttons/Z", "Z", PAD_TRIGGER_Z, 0},
{"Buttons/Start", "Start", PAD_BUTTON_START, 0},
{"D-Pad/Up", "D-pad Up", PAD_BUTTON_UP, 0},
{"D-Pad/Down", "D-pad Down", PAD_BUTTON_DOWN, 0},
{"D-Pad/Left", "D-pad Left", PAD_BUTTON_LEFT, 0},
{"D-Pad/Right", "D-pad Right", PAD_BUTTON_RIGHT, 0},
{"Triggers/L", "L", PAD_TRIGGER_L, 1},
{"Triggers/R", "R", PAD_TRIGGER_R, 2},
{"Triggers/L-Analog", "L analog", 0, 1},
{"Triggers/R-Analog", "R analog", 0, 2},
}};
void Reload() noexcept;
// The pad library has PADBlockInput but no matching query, so the settings
// overlay reports its own state here.
void SetInputBlocked(bool blocked) noexcept;
bool InputBlocked() noexcept;
// Mix expression output into a freshly read status set. Call once per guest
// PADRead, after every other input source has been merged.
void Apply(PADStatus* statuses) noexcept;
std::string GetExpression(uint32_t port, size_t control) noexcept;
// Returns false and fills error if the text does not parse; the binding is
// left unchanged in that case.
bool SetExpression(uint32_t port, size_t control, const std::string& text, std::string& error) noexcept;
// True while the control's expression is above the press threshold.
bool IsActive(uint32_t port, size_t control) noexcept;
// The default Dolphin config location on Windows, then next to the executable.
std::string DefaultDolphinConfigPath() noexcept;
// Imports [GCPad<padIndex>] into the given port. Returns the number of controls
// imported, or -1 on failure with error filled.
int ImportDolphinConfig(const std::string& path, int padIndex, uint32_t port,
std::string& summary, std::string& error) noexcept;
} // namespace InputBindings
+53
View File
@@ -0,0 +1,53 @@
#pragma once
// Dolphin-compatible input expressions.
//
// Values are doubles in Dolphin's ControlState style; a control counts as
// pressed above kConditionThreshold. Timing matches Dolphin: wall-clock
// seconds on a steady clock, so an expression copied from GCPadNew.ini
// behaves the same here as it does there.
#include <filesystem>
#include <functional>
#include <memory>
#include <string>
#include <vector>
namespace InputExpr {
inline constexpr double kConditionThreshold = 0.5;
// Resolves a backtick-quoted input name to its current value.
using InputSource = std::function<double(const std::string&)>;
struct Node;
class Expression {
public:
Expression();
~Expression();
Expression(Expression&&) noexcept;
Expression& operator=(Expression&&) noexcept;
// Returns false and fills error on a syntax problem.
static bool Parse(const std::string& text, Expression& out, std::string& error);
bool Empty() const { return m_root == nullptr; }
double Evaluate(const InputSource& source) const;
// Input names the expression references, for diagnostics.
std::vector<std::string> ReferencedInputs() const;
private:
std::unique_ptr<Node> m_root;
};
// Parses a Dolphin GCPadNew.ini and returns the expression text for each
// control of the requested pad, keyed by Dolphin's own control names
// ("Buttons/A", "D-Pad/Up", "Triggers/L", ...). Returns false if the file
// cannot be read or the section is missing.
bool ReadDolphinConfig(const std::filesystem::path& path, int padIndex,
std::vector<std::pair<std::string, std::string>>& controls,
std::string& deviceName, std::string& error);
} // namespace InputExpr
+14 -1
View File
@@ -1,6 +1,7 @@
#pragma once
#include "runtime_config.h"
#include "nand_settings.h"
#include "runtime_log.h"
#include "system_bridge.h"
@@ -163,7 +164,7 @@ inline std::filesystem::path CreateManagedNandRoot() {
return root;
}
inline std::filesystem::path DiscoverNandRootPath() {
inline std::filesystem::path ResolveNandRootPath() {
const std::string configPath = RuntimeConfigFile::NandRoot();
if (!configPath.empty()) {
const auto path = ResolveConfiguredPath(configPath);
@@ -179,4 +180,16 @@ inline std::filesystem::path DiscoverNandRootPath() {
return CreateManagedNandRoot();
}
inline std::filesystem::path DiscoverNandRootPath() {
static const auto root = [] {
const auto resolved = ResolveNandRootPath();
std::string error;
if (!RuntimeNandSettings::Ensure(resolved, error)) {
FailNandRoot(error.c_str(), RuntimeNandSettings::FilePath(resolved));
}
return resolved;
}();
return root;
}
} // namespace RuntimeNandPath
+59
View File
@@ -0,0 +1,59 @@
#pragma once
#include <filesystem>
#include <fstream>
#include <istream>
namespace RuntimeNandSave {
enum class Contents { Missing, Blank, Nonzero, Error };
enum class ReadAction { Proceed, Missing, Error, RecoveryNeeded };
// A failed read is not evidence that a save is blank. Check badbit before EOF:
// an I/O failure may set both, whereas a successful short final read sets EOF.
inline Contents InspectStream(std::istream& input) {
if (!input) return Contents::Error;
char block[4096];
for (;;) {
input.read(block, sizeof(block));
if (input.bad() || (input.fail() && !input.eof())) return Contents::Error;
for (std::streamsize i = 0; i < input.gcount(); ++i) {
if (block[i] != 0) return Contents::Nonzero;
}
if (input.eof()) return Contents::Blank;
}
}
inline Contents InspectFile(const std::filesystem::path& path) {
std::error_code ec;
const auto status = std::filesystem::symlink_status(path, ec);
if (ec && ec != std::errc::no_such_file_or_directory) return Contents::Error;
if (!std::filesystem::exists(status)) return Contents::Missing;
if (!std::filesystem::is_regular_file(path, ec) || ec) return Contents::Error;
std::ifstream input(path, std::ios::binary);
return InspectStream(input);
}
// Probe only read-only opens of the actual save and its exact write shadow.
// No probe writes, removes, or repairs data, and backups are not save aliases.
inline ReadAction CheckRead(const std::filesystem::path& path, int mode) {
const auto name = path.filename();
const bool isMain = name == "rksys.dat";
if (mode != 1 || (!isMain && name != "rksys.dat.nandsafe.tmp")) return ReadAction::Proceed;
const auto contents = InspectFile(path);
if (contents == Contents::Error) return ReadAction::Error;
if (contents == Contents::Nonzero) return ReadAction::Proceed;
if (isMain) {
auto shadow = path;
shadow += ".nandsafe.tmp";
const auto shadowContents = InspectFile(shadow);
if (shadowContents == Contents::Error) return ReadAction::Error;
// The next write normally discards an old shadow. Preserve a possible
// recovery source when there is no usable original, without promoting
// an uncommitted (and potentially incomplete) shadow to the real save.
if (shadowContents == Contents::Nonzero) return ReadAction::RecoveryNeeded;
}
return contents == Contents::Blank ? ReadAction::Missing : ReadAction::Proceed;
}
} // namespace RuntimeNandSave
+220
View File
@@ -0,0 +1,220 @@
#pragma once
#include <array>
#include <atomic>
#include <chrono>
#include <ctime>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <map>
#include <optional>
#include <string>
#include <utility>
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif
namespace RuntimeNandSettings {
using Settings = std::map<std::string, std::string>;
inline std::filesystem::path FilePath(const std::filesystem::path& root) {
return root / "title/00000001/00000002/data/setting.txt";
}
// Wii setting.txt is a 256-byte buffer encrypted with a rotating XOR key.
inline std::optional<Settings> Read(const std::filesystem::path& nandRoot) {
std::ifstream input(FilePath(nandRoot), std::ios::binary);
std::array<uint8_t, 256> bytes{};
if (!input.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) {
return std::nullopt;
}
uint32_t key = 0x73B5DBFAu;
std::string decoded;
for (const uint8_t byte : bytes) {
const char value = static_cast<char>(byte ^ static_cast<uint8_t>(key));
key = (key << 1) | (key >> 31);
if (value == '\0') {
break;
}
if (value != '\r') {
decoded += value;
}
}
Settings settings;
for (size_t start = 0; start < decoded.size();) {
const size_t end = decoded.find('\n', start);
const std::string line = decoded.substr(start, end - start);
const size_t equals = line.find('=');
if (equals != std::string::npos && equals != 0) {
settings.emplace(line.substr(0, equals), line.substr(equals + 1));
}
if (end == std::string::npos) {
break;
}
start = end + 1;
}
return settings;
}
inline bool HasIdentity(const Settings& settings) {
const auto serial = settings.find("SERNO");
if (serial == settings.end() || serial->second.empty() || serial->second.size() > 9 ||
serial->second.find_first_not_of("0123456789") != std::string::npos ||
serial->second.find_first_not_of('0') == std::string::npos) {
return false;
}
for (const auto& field : {std::pair{"CODE", 5u}, {"AREA", 3u}, {"GAME", 2u}}) {
const auto value = settings.find(field.first);
if (value == settings.end() || value->second.empty() ||
value->second.size() > field.second) {
return false;
}
}
return true;
}
// Dolphin's normal (non-deterministic) first-boot algorithm. It is independent
// of the ES device ID. Matching another NAND requires that NAND's saved serial.
inline std::string GenerateSerial(std::time_t now) {
if (now < 0) {
return {};
}
const auto digits = std::to_string(now % 1000000000);
return std::string(9 - digits.size(), '0') + digits;
}
// This recompilation targets the European disc. These are Dolphin's PAL boot
// defaults; an existing setting.txt always takes precedence, in every region.
inline std::optional<std::array<uint8_t, 256>> EncodeNew(const std::string& serial) {
const Settings identity{{"SERNO", serial}, {"CODE", "LEH"}, {"AREA", "EUR"}, {"GAME", "EU"}};
if (!HasIdentity(identity)) {
return std::nullopt;
}
std::array<uint8_t, 256> bytes{};
size_t position = 0;
uint32_t key = 0x73B5DBFAu;
const auto writeByte = [&](char value) {
bytes[position++] = static_cast<uint8_t>(value) ^ static_cast<uint8_t>(key);
key = (key << 1) | (key >> 31);
};
for (const std::string& line : {std::string("AREA=EUR\r\n"), std::string("MODEL=RVL-001(EUR)\r\n"),
std::string("DVD=0\r\n"), std::string("MPCH=0x7FFE\r\n"), std::string("CODE=LEH\r\n"),
"SERNO=" + serial + "\r\n", std::string("VIDEO=PAL\r\n"), std::string("GAME=EU\r\n")}) {
for (;;) {
if (position + line.size() > bytes.size()) {
return std::nullopt;
}
const auto start = position;
const auto savedKey = key;
bool hasNull = false;
for (const char value : line) {
writeByte(value);
hasNull |= bytes[position - 1] == 0;
}
if (!hasNull) {
break;
}
// Nintendo stops at an encoded NUL. Dolphin inserts an extra LF
// before this line and retries with the shifted encryption key.
position = start;
key = savedKey;
writeByte('\n');
}
}
return bytes; // The unused tail stays raw zero, as in Dolphin.
}
// Atomically claim our own scratch directory. A collision belongs to another
// launch (or a previous crashed launch); leave it untouched and try another name.
inline std::optional<std::filesystem::path> CreateScratchDirectory(
const std::filesystem::path& parent, const std::string& token, std::error_code& ec) {
for (unsigned attempt = 0; attempt < 128; ++attempt) {
const auto candidate = parent / (".setting-init-" + token + "-" + std::to_string(attempt));
ec.clear();
if (std::filesystem::create_directory(candidate, ec)) return candidate;
if (ec && ec != std::errc::file_exists) return std::nullopt;
}
ec = std::make_error_code(std::errc::file_exists);
return std::nullopt;
}
// Never replace an existing file, including an unreadable or damaged one.
// Publish a complete file atomically so simultaneous launches use one identity.
inline bool Ensure(const std::filesystem::path& root, std::string& error,
std::time_t now = std::time(nullptr)) {
const auto path = FilePath(root);
std::error_code ec;
const auto status = std::filesystem::symlink_status(path, ec);
if (ec && ec != std::errc::no_such_file_or_directory) {
error = "Cannot inspect NAND setting.txt: " + ec.message();
return false;
}
if (std::filesystem::exists(status)) {
const auto existing = Read(root);
if (existing && HasIdentity(*existing)) {
return true;
}
error = "Existing NAND setting.txt is unreadable or invalid; restore it from this console's backup";
return false;
}
const auto bytes = EncodeNew(GenerateSerial(now));
if (!bytes) {
error = "Cannot initialize NAND settings: invalid system clock";
return false;
}
ec.clear();
std::filesystem::create_directories(path.parent_path(), ec);
if (ec) {
error = "Cannot create NAND settings directory: " + ec.message();
return false;
}
static std::atomic<unsigned> sequence{0};
#ifdef _WIN32
const auto processId = GetCurrentProcessId();
#else
const auto processId = getpid();
#endif
const auto scratch = CreateScratchDirectory(path.parent_path(),
std::to_string(processId) + "-" + std::to_string(
std::chrono::steady_clock::now().time_since_epoch().count()) + "-" +
std::to_string(sequence++), ec);
if (!scratch) {
error = "Cannot create temporary NAND settings directory: " + ec.message();
return false;
}
const auto temporary = *scratch / "setting.txt";
bool written = false;
{
std::ofstream output(temporary, std::ios::binary);
output.write(reinterpret_cast<const char*>(bytes->data()), bytes->size());
output.close();
written = static_cast<bool>(output);
}
bool published = false;
if (written) {
#ifdef _WIN32
published = MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_WRITE_THROUGH) != 0;
#else
published = ::link(temporary.c_str(), path.c_str()) == 0;
#endif
}
std::filesystem::remove(temporary, ec);
std::filesystem::remove(*scratch, ec);
// A competing launcher may have published its settings first. Always read
// the winner from NAND rather than using our unpersisted candidate serial.
const auto persisted = Read(root);
if (persisted && HasIdentity(*persisted)) {
return true;
}
error = published ? "Cannot read newly initialized NAND setting.txt" :
"Cannot persist NAND setting.txt; check NAND directory permissions";
return false;
}
} // namespace RuntimeNandSettings
+46 -32
View File
@@ -11,6 +11,7 @@
#include <iomanip>
#include <iostream>
#include <limits>
#include <map>
#include <optional>
#include <sstream>
#include <string>
@@ -89,8 +90,9 @@ struct RuntimeUserConfig {
// comma-separated SDL-style physical button names ("south", or
// "dpad_up,left_shoulder") as values; pressing either bound button counts.
std::array<std::optional<std::string>, 12> controllerButtons;
// On-screen mobile touch controls: auto-accelerate locks A after a 1-second hold.
std::optional<bool> touchAutoAccelerate;
std::optional<bool> rumbleEnabled;
std::optional<int32_t> muteHotkey;
std::map<std::string, std::string> controllerExpressions;
};
namespace RuntimeConfigFile {
@@ -276,19 +278,7 @@ inline std::filesystem::path ApplicationDataDirectory() {
return std::filesystem::current_path() / kApplicationDirectoryName;
}
inline std::filesystem::path& ConfigPathOverride() {
static std::filesystem::path path;
return path;
}
inline void SetConfigPathOverride(std::filesystem::path path) {
ConfigPathOverride() = std::move(path);
}
inline std::filesystem::path ResolveConfigPath() {
if (!ConfigPathOverride().empty()) {
return ConfigPathOverride();
}
return ApplicationDataDirectory() / kConfigFileName;
}
@@ -335,9 +325,6 @@ inline void EnsureConfigFile() {
"# guest can observe it. Set to false to mix inline on the guest\n"
"# thread exactly as the runtime did before.\n"
"mix_worker = true\n\n"
"[controller]\n"
"# On-screen touch controls auto-accelerate latch (hold A for 1 second to lock)\n"
"touch_auto_accelerate = true\n\n"
"[network]\n"
"enabled = true\n\n"
"[discord]\n"
@@ -424,6 +411,20 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
FindConfigValue<std::string>(document, "controller", buttonKeys[index]);
}
config.rumbleEnabled = FindConfigValue<bool>(document, "controller", "rumble");
if (auto value = FindConfigInt(document, "audio", "mute_key")) {
config.muteHotkey = *value;
}
if (const auto* section = document.contains("controller") ? &document.at("controller") : nullptr;
section != nullptr && section->is_table()) {
for (const auto& [key, value] : section->as_table()) {
if (key.rfind("expr_", 0) == 0 && value.is_string()) {
config.controllerExpressions[key] = value.as_string();
}
}
}
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
config.windowPosX = FindConfigInt(document, "video", "window_x");
config.windowPosY = FindConfigInt(document, "video", "window_y");
@@ -484,8 +485,6 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.wiiAccelOffsetY = FindConfigValue<double>(document, "controller", "wii_accel_offset_y");
config.wiiAccelOffsetZ = FindConfigValue<double>(document, "controller", "wii_accel_offset_z");
config.wiiAccelTrace = FindConfigValue<bool>(document, "controller", "wii_accel_trace");
config.touchAutoAccelerate =
FindConfigValue<bool>(document, "controller", "touch_auto_accelerate");
config.networkEnabled = FindConfigValue<bool>(document, "network", "enabled");
config.discordPresenceEnabled = FindConfigValue<bool>(document, "discord", "enabled");
config.discordClientId = FindConfigValue<std::string>(document, "discord", "client_id");
@@ -696,6 +695,34 @@ inline bool SetControllerButton(size_t index, std::string value) {
return WriteSetting("controller", kControllerButtonKeys[index], FormatString(value));
}
inline std::string ControllerExpression(const std::string& key) {
const auto it = Get().controllerExpressions.find(key);
return it == Get().controllerExpressions.end() ? std::string() : it->second;
}
inline bool SetControllerExpression(const std::string& key, const std::string& value) {
Mutable().controllerExpressions[key] = value;
return WriteSetting("controller", key, FormatString(value));
}
inline bool RumbleEnabled(bool fallback = true) {
return Get().rumbleEnabled.value_or(fallback);
}
inline bool SetRumbleEnabled(bool value) {
Mutable().rumbleEnabled = value;
return WriteSetting("controller", "rumble", value ? "true" : "false");
}
inline int32_t MuteHotkey(int32_t fallback) {
return Get().muteHotkey.value_or(fallback);
}
inline bool SetMuteHotkey(int32_t value) {
Mutable().muteHotkey = value;
return WriteSetting("audio", "mute_key", std::to_string(value));
}
inline bool SetAudioVolume(float value) {
value = std::clamp(value, 0.0f, 1.0f);
Mutable().audioVolume = value;
@@ -868,16 +895,6 @@ inline bool SetWiiAccelOffset(const std::array<double, 3>& offset) {
return ok;
}
// On-screen mobile touch controls: auto-accelerate locks A after a 1-second hold.
inline bool TouchAutoAccelerate(bool fallback = true) {
return Get().touchAutoAccelerate.value_or(fallback);
}
inline bool SetTouchAutoAccelerate(bool value) {
Mutable().touchAutoAccelerate = value;
return WriteSetting("controller", "touch_auto_accelerate", value ? "true" : "false");
}
// Target frame rate for frame interpolation, or 0 to disable it.
inline uint32_t FrameInterpolationFps(uint32_t fallback = 0) {
return Get().frameInterpolationFps.value_or(fallback);
@@ -1030,9 +1047,6 @@ inline void LogLoadedConfig() {
if (config.retroRewindRoot) {
std::cout << " retro_rewind_root=" << *config.retroRewindRoot;
}
if (config.touchAutoAccelerate) {
std::cout << " touch_auto_accelerate=" << (*config.touchAutoAccelerate ? "true" : "false");
}
}
std::cout << std::endl;
return true;

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