mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-11 09:25:05 -04:00
init
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
|
||||
# Patch files must stay LF: git apply matches context bytes against LF upstream sources
|
||||
*.patch -text
|
||||
@@ -0,0 +1,97 @@
|
||||
name: Crash / freeze report
|
||||
description: The game crashed, froze, or closed on its own.
|
||||
title: "[Crash] "
|
||||
labels: ["crash"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for the report! Crashes are almost impossible to fix without logs, so please fill this in fully.
|
||||
|
||||
**Where to find your logs** (paste the path into the File Explorer address bar):
|
||||
- Playing through **WheelWizard** (most people): `%APPDATA%\CT-MKWII\Recomp\UserData\Logs`
|
||||
- Installed with the standalone setup exe: `%LOCALAPPDATA%\WiiCompiled\Logs`
|
||||
|
||||
Every run gets its own folder. Open the **newest** one and attach (drag & drop) `console.log` and any `crash_*.txt` file from it.
|
||||
- type: checkboxes
|
||||
id: preflight
|
||||
attributes:
|
||||
label: Before you continue
|
||||
options:
|
||||
- label: I'm on the latest release of WiiCompiled.
|
||||
required: true
|
||||
- label: I searched the existing issues and nobody has reported this crash yet.
|
||||
required: true
|
||||
- label: This is about the game runtime, not the WheelWizard app itself (WheelWizard problems go to their tracker - see the link on the "New issue" page).
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: WiiCompiled version
|
||||
description: It's the very first line of `console.log` (the log you're attaching below) - it starts with `[runtime] WiiCompiled v…`.
|
||||
placeholder: "e.g. v0.2.12"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: when
|
||||
attributes:
|
||||
label: When does it crash?
|
||||
options:
|
||||
- During startup / before the menus
|
||||
- In the menus
|
||||
- When loading a race
|
||||
- During a race (offline)
|
||||
- During a race (online)
|
||||
- After finishing a race / results screen
|
||||
- Randomly / no clear pattern
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: repro
|
||||
attributes:
|
||||
label: How often does it happen?
|
||||
options:
|
||||
- Every time
|
||||
- Most of the time
|
||||
- Sometimes
|
||||
- Happened only once so far
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: doing
|
||||
attributes:
|
||||
label: What were you doing when it crashed?
|
||||
description: Game mode, track, character/vehicle, how many players, anything you pressed right before. The more specific, the better.
|
||||
placeholder: "e.g. Retro Rewind online room, playing SNES Mario Circuit 3, crashed right as I crossed the finish line…"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: If you can make it crash on purpose, list the steps.
|
||||
placeholder: |
|
||||
1. Start the game
|
||||
2. …
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: Drag & drop `console.log` and any `crash_*.txt` from your newest run folder here, or paste the tail end of them below.
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: gpu
|
||||
attributes:
|
||||
label: GPU + driver version
|
||||
placeholder: "e.g. RTX 3060, driver 560.94"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: Windows version
|
||||
placeholder: "e.g. Windows 11 24H2"
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,91 @@
|
||||
name: In-game bug report
|
||||
description: Something looks, sounds, or behaves wrong (graphics, audio, input, online) - but the game doesn't crash.
|
||||
title: "[Bug] "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Where to find your logs** (paste the path into the File Explorer address bar):
|
||||
- Playing through **WheelWizard** (most people): `%APPDATA%\CT-MKWII\Recomp\UserData\Logs`
|
||||
- Installed with the standalone setup exe: `%LOCALAPPDATA%\WiiCompiled\Logs`
|
||||
|
||||
Every run gets its own folder. Open the **newest** one and attach `console.log`.
|
||||
|
||||
For visual bugs, a screenshot or short clip helps enormously.
|
||||
- type: checkboxes
|
||||
id: preflight
|
||||
attributes:
|
||||
label: Before you continue
|
||||
options:
|
||||
- label: I'm on the latest release of WiiCompiled.
|
||||
required: true
|
||||
- label: I searched the existing issues and nobody has reported this yet.
|
||||
required: true
|
||||
- label: The problem does NOT also happen on Dolphin or a real Wii (if it does, it's a game/mod issue, not a WiiCompiled issue).
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: WiiCompiled version
|
||||
description: It's the very first line of `console.log` (see the log locations above) - it starts with `[runtime] WiiCompiled v…`.
|
||||
placeholder: "e.g. v0.2.12"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: category
|
||||
attributes:
|
||||
label: What kind of bug is it?
|
||||
options:
|
||||
- Graphics / visual glitch
|
||||
- Audio
|
||||
- Input / controllers
|
||||
- Online / matchmaking
|
||||
- Gameplay behaves differently than on Wii/Dolphin
|
||||
- Save data / settings
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what
|
||||
attributes:
|
||||
label: What happens?
|
||||
description: Describe the bug and what you expected instead.
|
||||
placeholder: "e.g. The flag textures near the start line flicker on Coconut Mall…"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: doing
|
||||
attributes:
|
||||
label: What were you doing? / How to reproduce
|
||||
description: Game mode, track, and exact steps if you can reproduce it.
|
||||
placeholder: |
|
||||
1. Start a VS race on …
|
||||
2. …
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: media
|
||||
attributes:
|
||||
label: Screenshot / video
|
||||
description: Drag & drop images or clips here (very helpful for visual bugs).
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: Attach `console.log` from your newest run folder, or paste the relevant part below.
|
||||
render: text
|
||||
- type: input
|
||||
id: gpu
|
||||
attributes:
|
||||
label: GPU + driver version
|
||||
placeholder: "e.g. RTX 3060, driver 560.94"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: Windows version
|
||||
placeholder: "e.g. Windows 11 24H2"
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,75 @@
|
||||
name: Install / update problem
|
||||
description: Installing or updating WiiCompiled through WheelWizard fails, or the game won't launch at all after installing.
|
||||
title: "[Install] "
|
||||
labels: ["setup"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Where to find the setup log** (same place whether you installed through WheelWizard or the setup exe):
|
||||
- Press `Win+R`, type `%TEMP%` and press Enter, then find `WiiCompiled-setup.log` and attach it below.
|
||||
|
||||
If the install finished but the game won't start, also grab `console.log` from the newest folder in:
|
||||
- WheelWizard: `%APPDATA%\CT-MKWII\Recomp\UserData\Logs`
|
||||
- Standalone setup exe: `%LOCALAPPDATA%\WiiCompiled\Logs`
|
||||
- type: checkboxes
|
||||
id: preflight
|
||||
attributes:
|
||||
label: Before you continue
|
||||
options:
|
||||
- label: I'm on the latest version (latest WheelWizard, or the latest `MarioKartWiiCompiled-Setup.exe` from the Releases page).
|
||||
required: true
|
||||
- label: I searched the existing issues and nobody has reported this yet.
|
||||
required: true
|
||||
- label: I have a legally obtained copy of the game and it works in Dolphin.
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: stage
|
||||
attributes:
|
||||
label: Where does it go wrong?
|
||||
options:
|
||||
- Starting the install from WheelWizard
|
||||
- Selecting the game file (ISO/WBFS/RVZ rejected)
|
||||
- During compilation / translation
|
||||
- Install finishes but the game won't launch
|
||||
- Updating an existing install
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: installtype
|
||||
attributes:
|
||||
label: How did you install?
|
||||
options:
|
||||
- Through WheelWizard
|
||||
- Downloaded the setup exe myself
|
||||
- Not sure
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what
|
||||
attributes:
|
||||
label: What happens?
|
||||
description: The exact error message (copy the full text or screenshot it) and what you were doing when it appeared.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Setup log
|
||||
description: Drag & drop `%TEMP%\WiiCompiled-setup.log` here, or paste the tail end of it below.
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: Windows version
|
||||
placeholder: "e.g. Windows 11 24H2"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: av
|
||||
attributes:
|
||||
label: Antivirus (if any besides Windows Defender)
|
||||
placeholder: "e.g. Avast, none"
|
||||
@@ -0,0 +1,79 @@
|
||||
name: Performance problem
|
||||
description: Low FPS, stutters, or the game runs worse than expected.
|
||||
title: "[Perf] "
|
||||
labels: ["performance"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Where to find your logs** (paste the path into the File Explorer address bar):
|
||||
- Playing through **WheelWizard** (most people): `%APPDATA%\CT-MKWII\Recomp\UserData\Logs`
|
||||
- Installed with the standalone setup exe: `%LOCALAPPDATA%\WiiCompiled\Logs`
|
||||
|
||||
Every run gets its own folder. Open the **newest** one and attach `console.log`.
|
||||
- type: checkboxes
|
||||
id: preflight
|
||||
attributes:
|
||||
label: Before you continue
|
||||
options:
|
||||
- label: I'm on the latest release of WiiCompiled.
|
||||
required: true
|
||||
- label: I searched the existing issues and nobody has reported this yet.
|
||||
required: true
|
||||
- label: I checked that nothing else was hogging the GPU/CPU (recording software, browser video, etc.).
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: WiiCompiled version
|
||||
description: It's the very first line of `console.log` (see the log locations above) - it starts with `[runtime] WiiCompiled v…`.
|
||||
placeholder: "e.g. v0.2.12"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: pattern
|
||||
attributes:
|
||||
label: What does the problem look like?
|
||||
options:
|
||||
- Constant low FPS everywhere
|
||||
- Stutters / hitches (mostly smooth, brief freezes)
|
||||
- Only certain tracks or situations are slow
|
||||
- Got worse after an update
|
||||
- Slow only while recording/streaming the game window
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: doing
|
||||
attributes:
|
||||
label: When does it happen?
|
||||
description: Which tracks/modes, offline or online, how many players, and roughly what FPS you see.
|
||||
placeholder: "e.g. Drops to ~40 FPS on Rainbow Road with 12 players online, fine offline…"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: Attach `console.log` from your newest run folder.
|
||||
render: text
|
||||
- type: input
|
||||
id: gpu
|
||||
attributes:
|
||||
label: GPU + driver version
|
||||
placeholder: "e.g. RTX 3060, driver 560.94"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: cpu
|
||||
attributes:
|
||||
label: CPU
|
||||
placeholder: "e.g. Ryzen 5 5600"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: Windows version
|
||||
placeholder: "e.g. Windows 11 24H2"
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Feature request
|
||||
description: Suggest an improvement or new feature.
|
||||
title: "[Feature] "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: preflight
|
||||
attributes:
|
||||
label: Before you continue
|
||||
options:
|
||||
- label: I searched the existing issues and nobody has suggested this yet.
|
||||
required: true
|
||||
- label: This is about WiiCompiled itself, not WheelWizard or Retro Rewind content.
|
||||
required: true
|
||||
- type: textarea
|
||||
id: idea
|
||||
attributes:
|
||||
label: What would you like to see?
|
||||
description: Describe the feature and the problem it solves for you.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Workarounds you've tried
|
||||
description: Anything you currently do instead, if applicable.
|
||||
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: WheelWizard problem (launcher UI, mod downloads, Mii/save management)
|
||||
url: https://github.com/TeamWheelWizard/WheelWizard/issues
|
||||
about: If the problem is in the WheelWizard app itself rather than the game runtime, report it there instead.
|
||||
- name: Retro Rewind / track questions
|
||||
url: https://wiki.tockdom.com/wiki/Retro_Rewind
|
||||
about: Questions about Retro Rewind content (tracks, unlocks, online events) that also happen on Dolphin or console belong with the Retro Rewind project, not here.
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
translator:
|
||||
name: Translator (build + test)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore translator/Translator.sln
|
||||
|
||||
- name: Build
|
||||
run: dotnet build translator/Translator.sln -c Release --no-restore
|
||||
|
||||
- name: Test
|
||||
run: dotnet test translator/Translator.sln -c Release --no-build --verbosity normal
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/generated/
|
||||
/Assets/
|
||||
/assets/
|
||||
/PulsarPacks/
|
||||
*.dol
|
||||
*.rel
|
||||
*.szs
|
||||
*.arc
|
||||
*.brres
|
||||
*.thp
|
||||
*.bmg
|
||||
*.brsar
|
||||
*.breff
|
||||
*.u8
|
||||
*.carc
|
||||
*.iso
|
||||
*.rvz
|
||||
*.wbfs
|
||||
*.nkit
|
||||
*.ciso
|
||||
*.gcm
|
||||
*.wad
|
||||
Code.pul
|
||||
|
||||
# Build output
|
||||
/build/
|
||||
/build-*/
|
||||
/dist/
|
||||
/out/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
*.o
|
||||
*.obj
|
||||
*.a
|
||||
*.lib
|
||||
*.pdb
|
||||
*.ilk
|
||||
*.map
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
cmake-build-*/
|
||||
|
||||
|
||||
# .NET
|
||||
/translator/TestResults/
|
||||
**/TestResults/
|
||||
*.user
|
||||
*.suo
|
||||
*.userprefs
|
||||
project.lock.json
|
||||
.packages/
|
||||
|
||||
# Launcher artifacts
|
||||
/Launcher/artifacts/
|
||||
/Launcher/dist/
|
||||
/Launcher/WiiCompiled-Setup.exe
|
||||
*.exe
|
||||
*.msi
|
||||
|
||||
|
||||
# Local tooling and editors
|
||||
/.vs/
|
||||
/.vscode/
|
||||
/.idea/
|
||||
/tools/
|
||||
/TEMP/
|
||||
/tmp/
|
||||
/test_output/
|
||||
*.log
|
||||
output.txt
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Contributing to WiiCompiled
|
||||
|
||||
Thanks for wanting to help! A few ground rules
|
||||
|
||||
## The short version
|
||||
|
||||
- Code is judged on quality, not where it came from
|
||||
- You must be able understand and be able to explain every line you submit.
|
||||
- PR descriptions and responses must be written by you, **not** generated.
|
||||
- Accuracy is the bar for anything touching game behavior.
|
||||
|
||||
## Code quality
|
||||
|
||||
We don't care how your code came into existence. What we care about is whether it meets the
|
||||
project's patterns and standards, and the only way we measure that is by
|
||||
reading it.
|
||||
|
||||
Low-quality code won't be merged, regardless of origin. AI slop and human slop
|
||||
get the same treatment.
|
||||
|
||||
## If you use AI tools
|
||||
|
||||
That's ok, but rules apply:
|
||||
|
||||
1. **You must be able to explain your changes.** If a reviewer asks why a line
|
||||
exists or what a function does and you can't answer, the PR will be closed.
|
||||
"The AI wrote it" is not an explanation.
|
||||
2. **Write your own PR description.** The description exists so reviewers know
|
||||
what you changed and why, in your words. Generated descriptions tend to
|
||||
describe everything and explain nothing, and they will get your PR closed.
|
||||
|
||||
## Pull requests
|
||||
|
||||
- Keep PRs focused. try and keep it at 1 change per PR.
|
||||
Small PRs get reviewed fast.
|
||||
- Explain **what** and **why**. Reference the issue if there is one.
|
||||
- For anything affecting game behavior: identical behavior to real hardware is
|
||||
the goal. Be prepared to show your change doesn't diverge from the original
|
||||
game (hardware comparison, logs, whatever fits).
|
||||
- Review feedback. It's about the code, not about you ;).
|
||||
|
||||
## Bug reports
|
||||
|
||||
See the FAQ in the [README](README.md)
|
||||
|
||||
## A note on related projects
|
||||
|
||||
WiiCompiled, Wheel Wizard, and other projects in this ecosystem are developed
|
||||
independently and each has its **own** contribution rules and all have their own
|
||||
rules around AI usage. What applies here does not automatically apply there,
|
||||
and vice versa. Check each project's own CONTRIBUTING file.
|
||||
|
||||
## Legal
|
||||
|
||||
- Never!!! include Nintendo code, assets, or game data in a PR, an issue, or
|
||||
anywhere else in this project. No exceptions.
|
||||
- By contributing, you agree your contributions are licensed under
|
||||
[GPL v3.0](LICENSE), like the rest of the project.
|
||||
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -0,0 +1,4 @@
|
||||
/artifacts/
|
||||
/dist/
|
||||
**/bin/
|
||||
**/obj/
|
||||
@@ -0,0 +1,302 @@
|
||||
[CmdletBinding(PositionalBinding = $false)]
|
||||
param(
|
||||
[string]$OutputDirectory = 'Launcher/dist',
|
||||
[string]$DolphinToolPath,
|
||||
[string]$PortableToolsDirectory = 'Launcher/artifacts/portable-tools',
|
||||
[string]$DependencySourceDirectory = 'Launcher/artifacts/dependencies',
|
||||
[string]$VcRuntimeDirectory,
|
||||
[string]$ToolkitReleaseTag = $env:GITHUB_REF_NAME
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 3.0
|
||||
|
||||
# The canonical configure flags, and the Assert-File/Assert-Directory/Get-MkwFileSha256
|
||||
# helpers shared with LocalBuild.ps1 and Prepare-NativePrebuilt.ps1.
|
||||
. (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1')
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($DolphinToolPath)) {
|
||||
throw 'Build-Installer.ps1 requires -DolphinToolPath pointing to DolphinTool.exe.'
|
||||
}
|
||||
|
||||
$repoRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
||||
$outputRoot = [IO.Path]::GetFullPath((Join-Path $repoRoot $OutputDirectory))
|
||||
$portableTools = [IO.Path]::GetFullPath((Join-Path $repoRoot $PortableToolsDirectory))
|
||||
$dependencySources = [IO.Path]::GetFullPath((Join-Path $repoRoot $DependencySourceDirectory))
|
||||
$workRoot = Join-Path $PSScriptRoot 'artifacts\installer-build'
|
||||
$publish = Join-Path $workRoot 'publish'
|
||||
$payloadRoot = Join-Path $workRoot 'payload'
|
||||
$setupProject = Join-Path $PSScriptRoot 'WiiCompiled.Setup\WiiCompiled.Setup.csproj'
|
||||
$translatorProject = Join-Path $repoRoot 'translator\src\Translator.Cli\Translator.Cli.csproj'
|
||||
$projectFile = Join-Path $repoRoot 'projects\mkwii\recomp.yml'
|
||||
|
||||
# Everything Mario-Kart-specific in the manifest below is read from the project file rather than
|
||||
# restated here, and Test-PinnedFacts.ps1 checks the copies that cannot read it (the C++ entry
|
||||
# address, the C# endpoint constant, the lists this script and the installed host both carry).
|
||||
$pins = Get-MkwProjectPins $projectFile
|
||||
# The audit reports by throwing, which aborts this run; nothing native has been invoked yet, so
|
||||
# there is no exit code to inspect.
|
||||
& (Join-Path $PSScriptRoot 'Test-PinnedFacts.ps1') -RepositoryRoot $repoRoot
|
||||
|
||||
function Reset-Directory([string]$Path) {
|
||||
$full = [IO.Path]::GetFullPath($Path)
|
||||
$allowed = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot 'artifacts')).TrimEnd('\') + '\'
|
||||
if (-not $full.StartsWith($allowed, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Refusing to reset a directory outside Launcher/artifacts: $full"
|
||||
}
|
||||
if (Test-Path -LiteralPath $full) { Remove-Item -LiteralPath $full -Recurse -Force }
|
||||
[IO.Directory]::CreateDirectory($full) | Out-Null
|
||||
}
|
||||
function Reset-OutputDirectory([string]$Path) {
|
||||
$full = [IO.Path]::GetFullPath($Path)
|
||||
$allowed = [IO.Path]::GetFullPath($PSScriptRoot).TrimEnd('\') + '\'
|
||||
if (-not $full.StartsWith($allowed, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Refusing to reset an output directory outside Launcher: $full"
|
||||
}
|
||||
if (Test-Path -LiteralPath $full) { Remove-Item -LiteralPath $full -Recurse -Force }
|
||||
[IO.Directory]::CreateDirectory($full) | Out-Null
|
||||
}
|
||||
function Resolve-VcRuntimeDirectory([string]$RequestedPath) {
|
||||
if ($RequestedPath) {
|
||||
$resolved = [IO.Path]::GetFullPath($RequestedPath)
|
||||
Assert-File (Join-Path $resolved 'vcruntime140.dll') 'Visual C++ runtime'
|
||||
Assert-File (Join-Path $resolved 'msvcp140.dll') 'Visual C++ standard library runtime'
|
||||
return $resolved
|
||||
}
|
||||
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
|
||||
Assert-File $vswhere 'Visual Studio locator (or pass -VcRuntimeDirectory)'
|
||||
$installation = (& $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath | Select-Object -First 1)
|
||||
if (-not $installation) { throw 'Visual C++ redistributable files were not found; pass -VcRuntimeDirectory.' }
|
||||
$redistRoot = Join-Path $installation 'VC\Redist\MSVC'
|
||||
$candidate = Get-ChildItem -LiteralPath $redistRoot -Directory -Recurse | Where-Object {
|
||||
$_.Parent.Name -eq 'x64' -and $_.Name -match '^Microsoft\.VC\d+\.CRT$' -and
|
||||
(Test-Path (Join-Path $_.FullName 'vcruntime140.dll')) -and (Test-Path (Join-Path $_.FullName 'msvcp140.dll'))
|
||||
} | Sort-Object FullName -Descending | Select-Object -First 1
|
||||
if (-not $candidate) { throw "No redistributable x64 Visual C++ runtime was found below $redistRoot." }
|
||||
return $candidate.FullName
|
||||
}
|
||||
function Copy-Directory([string]$Source, [string]$Destination) {
|
||||
Assert-Directory $Source "Source directory"
|
||||
[IO.Directory]::CreateDirectory($Destination) | Out-Null
|
||||
Copy-Item -Path (Join-Path $Source '*') -Destination $Destination -Recurse -Force
|
||||
}
|
||||
function Compress-Zip([string]$Source, [string]$Destination, [string[]]$Entries) {
|
||||
$tar = Join-Path $env:SystemRoot 'System32\tar.exe'
|
||||
Assert-File $tar 'Windows archive tool'
|
||||
if (Test-Path -LiteralPath $Destination) { Remove-Item -LiteralPath $Destination -Force }
|
||||
& $tar -a -c -f $Destination -C $Source @Entries
|
||||
if ($LASTEXITCODE -ne 0) { throw "ZIP creation failed with exit code $LASTEXITCODE." }
|
||||
Assert-File $Destination 'ZIP output'
|
||||
}
|
||||
|
||||
Assert-File $setupProject '.NET setup project'
|
||||
Assert-File $translatorProject 'Translator CLI project'
|
||||
Assert-File $DolphinToolPath 'DolphinTool'
|
||||
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $portableTools 'llvm-mingw\bin\x86_64-w64-mingw32-clang++.exe'))) {
|
||||
& (Join-Path $PSScriptRoot 'Prepare-PortableTools.ps1') -Destination $portableTools
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Portable tool preparation failed.' }
|
||||
}
|
||||
Assert-File (Join-Path $portableTools 'CMake\bin\cmake.exe') 'Portable CMake'
|
||||
Assert-File (Join-Path $portableTools 'Ninja\ninja.exe') 'Portable Ninja'
|
||||
Assert-Directory $dependencySources 'Pinned offline dependency sources'
|
||||
# native_prebuilt carries the aurora/third-party archives the user no longer has
|
||||
# to compile (launcher/Prepare-NativePrebuilt.ps1).
|
||||
# Kept in step with InstalledLayout.DependencyNames by Test-PinnedFacts.ps1: the installed host
|
||||
# refuses to call a toolkit complete unless every one of these directories is present.
|
||||
$requiredDependencies = @('abseil-cpp','cppwinrt','dawn_prebuilt','fmt','freetype','imgui','native_prebuilt','png','SDL','sqlite3','tracy','xxhash','zlib','zstd')
|
||||
|
||||
# The precompiled archives are only interchangeable with what the user's machine
|
||||
# compiles if both came from this toolchain and this flag set, so a stale package
|
||||
# is rebuilt here rather than shipped. Without it every installation would fall
|
||||
# back to compiling aurora from source, which is what this package exists to
|
||||
# avoid - and a mismatched one would be an ABI hazard.
|
||||
$portableClangHash = Get-MkwFileSha256 (Join-Path $portableTools 'llvm-mingw\bin\clang-22.exe')
|
||||
function Test-NativePrebuiltCurrent([string]$PackageDirectory) {
|
||||
$provenancePath = Join-Path $PackageDirectory 'provenance.json'
|
||||
if (-not (Test-Path -LiteralPath $provenancePath -PathType Leaf)) { return $false }
|
||||
$provenance = Get-Content -LiteralPath $provenancePath -Raw | ConvertFrom-Json
|
||||
# Refreshing the pinned Dawn without re-harvesting would ship archives compiled
|
||||
# against headers that no longer describe the shipped DLL. A package predating
|
||||
# this field cannot prove it matches, so it is treated as stale.
|
||||
$recordedDawn = $provenance.PSObject.Properties['DawnRuntimeSha256']
|
||||
# Same reasoning for aurora itself: this release ships aurora-main sources
|
||||
# whose headers the user's runtime compiles against, so archives harvested
|
||||
# before an aurora change would be linked into call sites that no longer
|
||||
# match them.
|
||||
$recordedAurora = $provenance.PSObject.Properties['AuroraSourceFingerprint']
|
||||
# And for the vendored Crypto++ whose archive the package now carries: this
|
||||
# release ships runtime/third_party sources that first-party TUs compile
|
||||
# against, so archives harvested before a vendored change are stale.
|
||||
$recordedThirdParty = $provenance.PSObject.Properties['ThirdPartySourceFingerprint']
|
||||
return ($provenance.CompilerSha256 -eq $portableClangHash) -and
|
||||
($provenance.FlagFingerprint -eq (Get-MkwNativeFlagFingerprint)) -and
|
||||
($null -ne $recordedDawn) -and
|
||||
($recordedDawn.Value -eq (Get-MkwDawnRuntimeSha256 $dependencySources)) -and
|
||||
($null -ne $recordedAurora) -and
|
||||
($recordedAurora.Value -eq (Get-MkwAuroraSourceFingerprint (Join-Path $repoRoot 'aurora-main'))) -and
|
||||
($null -ne $recordedThirdParty) -and
|
||||
($recordedThirdParty.Value -eq (Get-MkwThirdPartySourceFingerprint (Join-Path $repoRoot 'runtime\third_party')))
|
||||
}
|
||||
$nativePrebuiltPackage = Join-Path $dependencySources 'native_prebuilt'
|
||||
if (-not (Test-NativePrebuiltCurrent $nativePrebuiltPackage)) {
|
||||
Write-Host '[0/6] Building the precompiled aurora and third-party package...'
|
||||
& (Join-Path $PSScriptRoot 'Prepare-NativePrebuilt.ps1') `
|
||||
-PortableToolsDirectory $portableTools -DependencySourceDirectory $dependencySources
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Precompiled aurora package preparation failed.' }
|
||||
if (-not (Test-NativePrebuiltCurrent $nativePrebuiltPackage)) {
|
||||
throw 'The precompiled aurora package still does not match this toolchain and flag set.'
|
||||
}
|
||||
}
|
||||
foreach ($name in $requiredDependencies) { Assert-Directory (Join-Path $dependencySources $name) "Pinned dependency $name" }
|
||||
$vcRuntime = Resolve-VcRuntimeDirectory $VcRuntimeDirectory
|
||||
|
||||
Reset-Directory $workRoot
|
||||
Reset-OutputDirectory $outputRoot
|
||||
foreach ($path in @($publish,$payloadRoot)) { [IO.Directory]::CreateDirectory($path) | Out-Null }
|
||||
|
||||
Write-Host '[1/6] Publishing self-contained CLI host and translator...'
|
||||
& dotnet publish $setupProject -c Release -r win-x64 --self-contained true `
|
||||
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true `
|
||||
-p:DebugType=None -o (Join-Path $publish 'setup')
|
||||
if ($LASTEXITCODE -ne 0) { throw "Setup publish failed with exit code $LASTEXITCODE." }
|
||||
& dotnet publish $translatorProject -c Release -r win-x64 --self-contained true `
|
||||
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true `
|
||||
-p:DebugType=None -o (Join-Path $publish 'translator')
|
||||
if ($LASTEXITCODE -ne 0) { throw "Translator publish failed with exit code $LASTEXITCODE." }
|
||||
$setupHost = Join-Path $publish 'setup\WiiCompiled.Setup.exe'
|
||||
$translator = Join-Path $publish 'translator\Translator.Cli.exe'
|
||||
Assert-File $setupHost 'Published setup host'
|
||||
Assert-File $translator 'Self-contained translator'
|
||||
|
||||
Write-Host '[2/6] Staging the explicit, game-code-free payload allowlist...'
|
||||
# The staged layout mirrors the installed layout exactly (Toolkit, BuildWorkspace): payload
|
||||
# identities hash relative paths, so the names here are part of the fingerprint contract.
|
||||
$toolkit = Join-Path $payloadRoot 'Toolkit'
|
||||
$workspace = Join-Path $payloadRoot 'BuildWorkspace'
|
||||
[IO.Directory]::CreateDirectory($toolkit) | Out-Null
|
||||
Copy-Directory (Join-Path $portableTools 'llvm-mingw') (Join-Path $toolkit 'llvm-mingw')
|
||||
Copy-Directory (Join-Path $portableTools 'CMake') (Join-Path $toolkit 'CMake')
|
||||
Copy-Directory (Join-Path $portableTools 'Ninja') (Join-Path $toolkit 'Ninja')
|
||||
# Debuggers, Python bindings, and the CMake GUI are not part of the local build graph.
|
||||
# Leaving them out reduces the attack/dependency surface without removing compiler support files.
|
||||
foreach ($unused in @(
|
||||
(Join-Path $toolkit 'llvm-mingw\python'),
|
||||
(Join-Path $toolkit 'CMake\bin\cmake-gui.exe')
|
||||
)) {
|
||||
if (Test-Path -LiteralPath $unused) { Remove-Item -LiteralPath $unused -Recurse -Force }
|
||||
}
|
||||
Get-ChildItem -LiteralPath (Join-Path $toolkit 'llvm-mingw\bin') -File |
|
||||
Where-Object { $_.Name -like 'lldb*' -or $_.Name -like 'liblldb*' } |
|
||||
Remove-Item -Force
|
||||
[IO.Directory]::CreateDirectory((Join-Path $toolkit 'Translator')) | Out-Null
|
||||
Copy-Item -LiteralPath $translator -Destination (Join-Path $toolkit 'Translator\Translator.Cli.exe')
|
||||
Copy-Item -LiteralPath $DolphinToolPath -Destination (Join-Path $toolkit 'DolphinTool.exe')
|
||||
[IO.Directory]::CreateDirectory((Join-Path $toolkit 'Redist')) | Out-Null
|
||||
Copy-Item -Path (Join-Path $vcRuntime '*.dll') -Destination (Join-Path $toolkit 'Redist')
|
||||
Copy-Item -Path (Join-Path $vcRuntime '*.dll') -Destination (Join-Path $toolkit 'CMake\bin')
|
||||
Copy-Item -Path (Join-Path $vcRuntime '*.dll') -Destination (Join-Path $toolkit 'Ninja')
|
||||
Copy-Item -Path (Join-Path $vcRuntime '*.dll') -Destination $toolkit
|
||||
|
||||
Copy-Directory (Join-Path $repoRoot 'runtime') (Join-Path $workspace 'runtime')
|
||||
Copy-Directory (Join-Path $repoRoot 'aurora-main') (Join-Path $workspace 'aurora-main')
|
||||
# Source trees can contain ignored developer build directories. They are never release inputs.
|
||||
$runtimeDeveloperBuild = Join-Path $workspace 'runtime\build'
|
||||
if (Test-Path -LiteralPath $runtimeDeveloperBuild) {
|
||||
Remove-Item -LiteralPath $runtimeDeveloperBuild -Recurse -Force
|
||||
}
|
||||
$auroraExtern = Join-Path $workspace 'aurora-main\extern'
|
||||
Get-ChildItem -LiteralPath $auroraExtern -Directory -Force | Remove-Item -Recurse -Force
|
||||
Copy-Directory (Join-Path $repoRoot 'projects\mkwii') (Join-Path $workspace 'projects\mkwii')
|
||||
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'LocalBuild.ps1') -Destination (Join-Path $workspace 'LocalBuild.ps1')
|
||||
# LocalBuild.ps1 dot-sources the canonical configure flags from this sibling.
|
||||
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1') -Destination (Join-Path $workspace 'NativeBuildFlags.ps1')
|
||||
[IO.Directory]::CreateDirectory((Join-Path $workspace 'Dependencies')) | Out-Null
|
||||
foreach ($name in $requiredDependencies) { Copy-Directory (Join-Path $dependencySources $name) (Join-Path $workspace "Dependencies\$name") }
|
||||
|
||||
[IO.Directory]::CreateDirectory((Join-Path $payloadRoot 'host')) | Out-Null
|
||||
Copy-Item $setupHost (Join-Path $payloadRoot 'host\WiiCompiled-Setup.exe')
|
||||
|
||||
Write-Host '[3/6] Writing manifests and third-party license inventory...'
|
||||
[IO.Directory]::CreateDirectory((Join-Path $payloadRoot 'licenses')) | Out-Null
|
||||
Copy-Item (Join-Path $portableTools 'README-LICENSES.txt') (Join-Path $payloadRoot 'licenses\Portable-build-tools.txt')
|
||||
Copy-Item (Join-Path $repoRoot 'aurora-main\LICENSE') (Join-Path $payloadRoot 'licenses\Aurora-LICENSE.txt')
|
||||
Copy-Item (Join-Path $dependencySources 'cppwinrt\LICENSE.txt') (Join-Path $payloadRoot 'licenses\CppWinRT-LICENSE.txt')
|
||||
# The precompiled aurora/third-party archives are built from the very sources
|
||||
# already shipped under build-workspace\Dependencies and aurora-main, so they add
|
||||
# no third-party component and therefore no new license obligation.
|
||||
$dolphinLicense = Join-Path (Split-Path -Parent $DolphinToolPath) 'COPYING'
|
||||
if (Test-Path $dolphinLicense) { Copy-Item $dolphinLicense (Join-Path $payloadRoot 'licenses\Dolphin-COPYING.txt') }
|
||||
@"
|
||||
DolphinTool source offer
|
||||
|
||||
Project and complete corresponding source: https://github.com/dolphin-emu/dolphin
|
||||
Dolphin is licensed under GPLv2+ with additional per-file SPDX licenses.
|
||||
"@ | Set-Content (Join-Path $payloadRoot 'licenses\Dolphin-SOURCE.txt') -Encoding UTF8
|
||||
@"
|
||||
Microsoft Visual C++ Runtime
|
||||
|
||||
Redistributable x64 runtime DLLs are included app-locally for DolphinTool and third-party renderer DLLs.
|
||||
Microsoft license terms: https://visualstudio.microsoft.com/license-terms/
|
||||
"@ | Set-Content (Join-Path $payloadRoot 'licenses\Microsoft-VC-Runtime.txt') -Encoding UTF8
|
||||
# Compute the payload's content identities once, here, with the same code every installed host
|
||||
# uses. Shipping them in the manifest is what keeps user machines from re-hashing the toolkit
|
||||
# during installation; only --repair-products re-derives content identity locally.
|
||||
Write-Host 'Computing the payload content identities (hashes the staged toolkit once)...'
|
||||
$identityJson = & $setupHost --emit-payload-identities --payload-root $payloadRoot
|
||||
if ($LASTEXITCODE -ne 0) { throw "Payload identity computation failed with exit code $LASTEXITCODE." }
|
||||
$identities = ($identityJson -join '') | ConvertFrom-Json
|
||||
foreach ($required in @('ToolkitFingerprint','TranslationFingerprint','NativeToolchainFingerprint',
|
||||
'ToolkitPackageFingerprint','RuntimeAssetsFingerprint')) {
|
||||
if ([string]::IsNullOrWhiteSpace($identities.$required)) { throw "Payload identity output is missing $required." }
|
||||
}
|
||||
|
||||
$manifest = [ordered]@{
|
||||
SchemaVersion = 2
|
||||
ProductVersion = '0.2.13'
|
||||
ExpectedGameId = $pins.GameId
|
||||
ExpectedDolSha256 = $pins.DolSha256
|
||||
ExpectedRelSha256 = $pins.RelSha256
|
||||
ToolkitReleaseTag = if ($ToolkitReleaseTag) { $ToolkitReleaseTag } else { '' }
|
||||
BuildModel = 'local-translation-and-compilation'
|
||||
RetroWfcPayloadUri = $pins.RetroWfcPayloadUri
|
||||
ToolkitFingerprint = $identities.ToolkitFingerprint
|
||||
ToolkitPackageFingerprint = $identities.ToolkitPackageFingerprint
|
||||
RuntimeAssetsFingerprint = $identities.RuntimeAssetsFingerprint
|
||||
TranslationFingerprint = $identities.TranslationFingerprint
|
||||
NativeToolchainFingerprint = $identities.NativeToolchainFingerprint
|
||||
}
|
||||
$manifest | ConvertTo-Json | Set-Content (Join-Path $payloadRoot 'payload-manifest.json') -Encoding UTF8
|
||||
|
||||
Write-Host '[4/6] Enforcing the copyright and generated-code boundary...'
|
||||
& (Join-Path $PSScriptRoot 'Test-PayloadBoundary.ps1') -PayloadRoot $payloadRoot
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Payload boundary audit failed.' }
|
||||
& (Join-Path $PSScriptRoot 'Test-NativeDependencies.ps1') -PayloadRoot $payloadRoot -SetupHost $setupHost `
|
||||
-LlvmReadobjPath (Join-Path $portableTools 'llvm-mingw\bin\llvm-readobj.exe')
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Native dependency audit failed.' }
|
||||
|
||||
Write-Host '[5/6] Creating the canonical installer payload...'
|
||||
$payloadZip = Join-Path $workRoot 'payload.zip'
|
||||
Compress-Zip $payloadRoot $payloadZip @(
|
||||
'Toolkit','BuildWorkspace','host','licenses','payload-manifest.json')
|
||||
|
||||
Write-Host '[6/6] Producing the single-file setup executable...'
|
||||
$outputSetup = Join-Path $outputRoot 'WiiCompiled-Setup.exe'
|
||||
Copy-Item $setupHost $outputSetup -Force
|
||||
$outputStream = [IO.File]::Open($outputSetup, [IO.FileMode]::Append, [IO.FileAccess]::Write, [IO.FileShare]::None)
|
||||
try {
|
||||
$offset = $outputStream.Position
|
||||
$input = [IO.File]::OpenRead($payloadZip)
|
||||
try { $input.CopyTo($outputStream) } finally { $input.Dispose() }
|
||||
$length = $outputStream.Position - $offset
|
||||
$writer = [IO.BinaryWriter]::new($outputStream, [Text.Encoding]::ASCII, $true)
|
||||
try {
|
||||
$writer.Write([Text.Encoding]::ASCII.GetBytes('MKWCPAY1'))
|
||||
$writer.Write([Int64]$offset)
|
||||
$writer.Write([Int64]$length)
|
||||
} finally { $writer.Dispose() }
|
||||
} finally { $outputStream.Dispose() }
|
||||
|
||||
$setupItem = Get-Item -LiteralPath $outputSetup
|
||||
Write-Host ("{0}: {1} MiB; SHA-256 {2}" -f $setupItem.Name,
|
||||
[math]::Round($setupItem.Length / 1MB, 1), (Get-MkwFileSha256 $outputSetup))
|
||||
@@ -0,0 +1,460 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)] [string]$Workspace,
|
||||
[Parameter(Mandatory)] [string]$Toolkit,
|
||||
# 'both' translates once (retro-aware) and compiles the two products from one build
|
||||
# graph, which shares every profile-neutral shard object between them. Building the
|
||||
# legs separately recompiles all of mkw_base_shared in the second leg, because the
|
||||
# retro-aware shard emission changes the content identity of every base_common shard.
|
||||
[Parameter(Mandatory)] [ValidateSet('base', 'retro-rewind', 'both')] [string]$Profile,
|
||||
[Parameter(Mandatory)] [string]$OutputDirectory,
|
||||
[string]$BaseOutputDirectory,
|
||||
[string]$RetroRewindPackageDirectory,
|
||||
[string]$RetroWfcOfflineDirectory,
|
||||
[ValidateSet('offline', 'downloaded')] [string]$RetroWfcPayloadOrigin = 'offline',
|
||||
[switch]$SkipRetroWfcPayload,
|
||||
# The caller's recomputed cache-reuse identities (see ToolkitFingerprint.ComputeComponents).
|
||||
# They are compared against the provenance recorded beside the caches themselves; a missing or
|
||||
# mismatched identity degrades that cache to a clean rebuild, never the other way around.
|
||||
[string]$TranslationFingerprint = '',
|
||||
[string]$NativeToolchainFingerprint = '',
|
||||
# Discards every cache first. Used when a product was reported broken/blocked, so a possibly
|
||||
# corrupted translation or build directory can never contribute to the repaired product.
|
||||
[switch]$ForceCleanBuild,
|
||||
[int]$Parallel = 0
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 3.0
|
||||
|
||||
# Canonical configure flags (plus Assert-File / Invoke-Checked / Get-MkwFileSha256),
|
||||
# shared with launcher/Prepare-NativePrebuilt.ps1 so the precompiled aurora/third-party
|
||||
# archives and the objects compiled here can never be produced with different settings.
|
||||
# See NativeBuildFlags.ps1; Build-Installer.ps1 stages it beside this script.
|
||||
. (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1')
|
||||
|
||||
function Resolve-NativePrebuiltDirectory([string]$DependenciesDirectory, [string]$ToolchainBin, [string]$AuroraDirectory, [string]$ThirdPartyDirectory) {
|
||||
<#
|
||||
The shipped precompiled aurora/third-party package, verified against its recorded provenance
|
||||
(exact compiler, exact flags) before use. Anything unexpected falls back to building from source.
|
||||
#>
|
||||
$package = Join-Path $DependenciesDirectory 'native_prebuilt'
|
||||
$provenancePath = Join-Path $package 'provenance.json'
|
||||
if (-not (Test-Path -LiteralPath $provenancePath -PathType Leaf)) {
|
||||
Write-Host 'MKWCBUILD: No precompiled aurora package was shipped; building it from source'
|
||||
return ''
|
||||
}
|
||||
try {
|
||||
$provenance = Get-Content -LiteralPath $provenancePath -Raw | ConvertFrom-Json
|
||||
$compiler = Join-Path $ToolchainBin 'clang-22.exe'
|
||||
$compilerHash = Get-MkwFileSha256 $compiler
|
||||
if ($provenance.CompilerSha256 -ne $compilerHash) {
|
||||
Write-Host 'MKWCBUILD: The precompiled aurora package was built by a different compiler; building it from source'
|
||||
return ''
|
||||
}
|
||||
if ($provenance.FlagFingerprint -ne (Get-MkwNativeFlagFingerprint)) {
|
||||
Write-Host 'MKWCBUILD: The precompiled aurora package was built with different flags; building it from source'
|
||||
return ''
|
||||
}
|
||||
# The archives embed Dawn's headers and link its import library, so a
|
||||
# dawn_prebuilt tree that is not the one they were harvested against is
|
||||
# an ABI mismatch that only shows up as a crash inside the GPU backend.
|
||||
$recordedDawn = $provenance.PSObject.Properties['DawnRuntimeSha256']
|
||||
if ($null -eq $recordedDawn -or
|
||||
$recordedDawn.Value -ne (Get-MkwDawnRuntimeSha256 $DependenciesDirectory)) {
|
||||
Write-Host 'MKWCBUILD: The precompiled aurora package was built against a different Dawn; building it from source'
|
||||
return ''
|
||||
}
|
||||
# The archives are aurora; the runtime compiled here calls its headers.
|
||||
# If this workspace's aurora sources are not the ones that were harvested
|
||||
# (a newer toolkit, or a locally modified tree), linking the package would
|
||||
# mix two versions of aurora, so build it from source instead.
|
||||
$recordedAurora = $provenance.PSObject.Properties['AuroraSourceFingerprint']
|
||||
if ($null -eq $recordedAurora -or
|
||||
$recordedAurora.Value -ne (Get-MkwAuroraSourceFingerprint $AuroraDirectory)) {
|
||||
Write-Host 'MKWCBUILD: The precompiled aurora package was built from different aurora sources; building it from source'
|
||||
return ''
|
||||
}
|
||||
# Same reasoning for the vendored Crypto++ the package also carries:
|
||||
# first-party TUs compile against this workspace's third_party headers,
|
||||
# so a package harvested from an older tree must not be linked here.
|
||||
$recordedThirdParty = $provenance.PSObject.Properties['ThirdPartySourceFingerprint']
|
||||
if ($null -eq $recordedThirdParty -or
|
||||
$recordedThirdParty.Value -ne (Get-MkwThirdPartySourceFingerprint $ThirdPartyDirectory)) {
|
||||
Write-Host 'MKWCBUILD: The precompiled aurora package was built from different vendored third-party sources; building it from source'
|
||||
return ''
|
||||
}
|
||||
} catch {
|
||||
Write-Host "MKWCBUILD: The precompiled aurora package could not be validated ($($_.Exception.Message)); building it from source"
|
||||
return ''
|
||||
}
|
||||
Write-Host 'MKWCBUILD: Using the precompiled aurora and third-party libraries'
|
||||
return $package
|
||||
}
|
||||
|
||||
function Write-MkwBuildStep([string]$StepId, [string]$Message) {
|
||||
<#
|
||||
Announces a build step. The identifier is the contract (BuildStepIds in
|
||||
WiiCompiled.Setup/InstallProgress.cs); the trailing sentence is only for the log.
|
||||
#>
|
||||
Write-Host "MKWCBUILD:STEP:${StepId} $Message"
|
||||
}
|
||||
|
||||
function Reset-LocalDirectory([string]$Path) {
|
||||
$full = [IO.Path]::GetFullPath($Path)
|
||||
$root = [IO.Path]::GetFullPath($Workspace).TrimEnd('\') + '\'
|
||||
$installRoot = [IO.Path]::GetFullPath((Split-Path -Parent $Workspace)).TrimEnd('\') + '\'
|
||||
# The caller-supplied output destinations are legitimate reset targets by
|
||||
# definition, wherever the caller placed them: a fresh install's operation
|
||||
# scratch lives beside the installation directory rather than inside it.
|
||||
$allowedDestinations = @($OutputDirectory, $BaseOutputDirectory) |
|
||||
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
|
||||
ForEach-Object { [IO.Path]::GetFullPath($_).TrimEnd('\') }
|
||||
$isAllowedDestination = $allowedDestinations |
|
||||
Where-Object { $full.TrimEnd('\').Equals($_, [StringComparison]::OrdinalIgnoreCase) }
|
||||
if (-not $isAllowedDestination -and
|
||||
-not $full.StartsWith($root, [StringComparison]::OrdinalIgnoreCase) -and
|
||||
-not $full.StartsWith($installRoot, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Refusing to reset a directory outside the local build workspace: $full"
|
||||
}
|
||||
if (Test-Path -LiteralPath $full) { Remove-Item -LiteralPath $full -Recurse -Force }
|
||||
[IO.Directory]::CreateDirectory($full) | Out-Null
|
||||
}
|
||||
|
||||
$Workspace = [IO.Path]::GetFullPath($Workspace)
|
||||
$Toolkit = [IO.Path]::GetFullPath($Toolkit)
|
||||
$OutputDirectory = [IO.Path]::GetFullPath($OutputDirectory)
|
||||
if (-not [string]::IsNullOrWhiteSpace($BaseOutputDirectory)) {
|
||||
$BaseOutputDirectory = [IO.Path]::GetFullPath($BaseOutputDirectory)
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($RetroRewindPackageDirectory)) {
|
||||
$RetroRewindPackageDirectory = [IO.Path]::GetFullPath($RetroRewindPackageDirectory)
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($RetroWfcOfflineDirectory)) {
|
||||
$RetroWfcOfflineDirectory = [IO.Path]::GetFullPath($RetroWfcOfflineDirectory)
|
||||
}
|
||||
$hasOfflineRetroWfc = -not [string]::IsNullOrWhiteSpace($RetroWfcOfflineDirectory)
|
||||
$buildsRetro = $Profile -in @('retro-rewind', 'both')
|
||||
if (-not $buildsRetro -and ($hasOfflineRetroWfc -or $SkipRetroWfcPayload)) {
|
||||
throw 'Retro-WFC payload options are valid only for a Retro Rewind build.'
|
||||
}
|
||||
if ($buildsRetro -and ($hasOfflineRetroWfc -eq [bool]$SkipRetroWfcPayload)) {
|
||||
throw 'Choose exactly one Retro-WFC mode: -RetroWfcOfflineDirectory or -SkipRetroWfcPayload.'
|
||||
}
|
||||
if (-not $buildsRetro -and -not [string]::IsNullOrWhiteSpace($RetroRewindPackageDirectory)) {
|
||||
throw '-RetroRewindPackageDirectory is valid only for a Retro Rewind build.'
|
||||
}
|
||||
if ($Profile -eq 'both' -and [string]::IsNullOrWhiteSpace($BaseOutputDirectory)) {
|
||||
throw "-BaseOutputDirectory is required with -Profile both; -OutputDirectory receives the Retro Rewind product."
|
||||
}
|
||||
if ($Profile -ne 'both' -and -not [string]::IsNullOrWhiteSpace($BaseOutputDirectory)) {
|
||||
throw '-BaseOutputDirectory is valid only with -Profile both.'
|
||||
}
|
||||
$translator = Join-Path $Toolkit 'Translator\Translator.Cli.exe'
|
||||
$cmake = Join-Path $Toolkit 'CMake\bin\cmake.exe'
|
||||
$ninja = Join-Path $Toolkit 'Ninja\ninja.exe'
|
||||
$toolchainBin = Join-Path $Toolkit 'llvm-mingw\bin'
|
||||
$cc = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang.exe'
|
||||
$cxx = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang++.exe'
|
||||
$windres = Join-Path $toolchainBin 'x86_64-w64-mingw32-windres.exe'
|
||||
$project = Join-Path $Workspace 'projects\mkwii\recomp.yml'
|
||||
$assets = Join-Path $Workspace 'Assets'
|
||||
$generated = Join-Path $Workspace 'generated'
|
||||
$functions = Join-Path $generated 'functions'
|
||||
$baseMetadata = Join-Path $generated 'base_translation_output.json'
|
||||
$baseManifestDir = Join-Path $Workspace 'build\base'
|
||||
$baseManifest = Join-Path $baseManifestDir 'mkwii_base_manifest.json'
|
||||
$shards = Join-Path $generated 'build_shards'
|
||||
$build = Join-Path $Workspace 'native-build'
|
||||
$retroRoot = if ([string]::IsNullOrWhiteSpace($RetroRewindPackageDirectory)) {
|
||||
Join-Path $Workspace 'PulsarPacks\completed\RetroRewind\RetroRewind6'
|
||||
} else {
|
||||
$RetroRewindPackageDirectory
|
||||
}
|
||||
|
||||
foreach ($required in @(
|
||||
@($translator, 'Bundled translator'), @($cmake, 'Bundled CMake'), @($ninja, 'Bundled Ninja'),
|
||||
@($cc, 'Bundled C compiler'), @($cxx, 'Bundled C++ compiler'), @($windres, 'Bundled resource compiler'), @($project, 'Translation project'),
|
||||
@((Join-Path $assets 'main.dol'), 'Extracted main.dol'), @((Join-Path $assets 'StaticR.rel'), 'Extracted StaticR.rel')
|
||||
)) { Assert-File $required[0] $required[1] }
|
||||
|
||||
# Everything Mario-Kart-specific this script needs is read from the project file rather than
|
||||
# restated here: the translation entry point below, and (through the retro-rewind profile the
|
||||
# translator loads itself) the module guest/link bases the mod translation uses.
|
||||
$pins = Get-MkwProjectPins $project
|
||||
|
||||
# Three independent knobs: $translatorThreads (the translator's own worker threads; peak memory is
|
||||
# one heap that barely moves with thread count, so clamping it only slows translation), $translatedJobs
|
||||
# (the real RAM guard, capping concurrent clang compiles of memory-hungry translated TUs via the Ninja
|
||||
# MKW_TRANSLATED_COMPILE_JOBS pool), and $globalJobs (Ninja's overall parallelism for everything else).
|
||||
# An explicit -Parallel pins all three.
|
||||
$memoryGiB = [math]::Max(1, [math]::Floor((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB))
|
||||
if ($Parallel -gt 0) {
|
||||
$translatorThreads = $Parallel
|
||||
$translatedJobs = $Parallel
|
||||
$globalJobs = $Parallel
|
||||
} else {
|
||||
$translatorThreads = [math]::Max(1, [math]::Min([Environment]::ProcessorCount, 16))
|
||||
$translatedJobs = [math]::Max(1, [math]::Min([Environment]::ProcessorCount,
|
||||
[math]::Floor($memoryGiB / 2)))
|
||||
$globalJobs = [math]::Max($translatedJobs, [Environment]::ProcessorCount)
|
||||
}
|
||||
|
||||
$oldPath = $env:PATH
|
||||
$oldDotnet = $env:DOTNET_ROOT
|
||||
try {
|
||||
$env:PATH = Get-MkwToolchainPath $Toolkit
|
||||
Remove-Item Env:DOTNET_ROOT -ErrorAction SilentlyContinue
|
||||
Push-Location $Workspace
|
||||
try {
|
||||
# This script is the single owner of every cache-reuse decision: it is what would misbuild.
|
||||
# The caller only supplies the identities to compare recorded provenance against.
|
||||
$translationProvenance = Join-Path $generated 'translation-provenance.json'
|
||||
$toolchainProvenance = Join-Path $build 'toolchain-provenance.json'
|
||||
|
||||
function Get-RecordedFingerprint([string]$Path, [string]$Property) {
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return '' }
|
||||
try {
|
||||
$record = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||
$value = $record.PSObject.Properties[$Property]
|
||||
if ($null -eq $value -or [string]::IsNullOrWhiteSpace($value.Value)) { return '' }
|
||||
return [string]$value.Value
|
||||
} catch { return '' }
|
||||
}
|
||||
|
||||
if ($ForceCleanBuild) {
|
||||
Write-Host 'MKWCBUILD: A clean build was requested; discarding every translation and build cache'
|
||||
Reset-LocalDirectory $generated
|
||||
if (Test-Path -LiteralPath $baseManifestDir) { Remove-Item -LiteralPath $baseManifestDir -Recurse -Force }
|
||||
if (Test-Path -LiteralPath $build) { Remove-Item -LiteralPath $build -Recurse -Force }
|
||||
}
|
||||
|
||||
# Reused only when its exact inputs match, its artifacts survived, and (for a modded build) it
|
||||
# already knows this Code.pul. Otherwise it retranslates incrementally: content-addressed
|
||||
# outputs plus --prune-stale mean Ninja only recompiles shards whose bytes actually moved.
|
||||
$reuseBase = $false
|
||||
if (-not $ForceCleanBuild -and -not [string]::IsNullOrWhiteSpace($TranslationFingerprint) -and
|
||||
(Get-RecordedFingerprint $translationProvenance 'TranslationFingerprint') -eq $TranslationFingerprint) {
|
||||
$artifacts = @($baseMetadata, $baseManifest, (Join-Path $generated 'base_translation_sources.bin'),
|
||||
(Join-Path $generated 'base_translation_mod_awareness.json'))
|
||||
$reuseBase = -not ($artifacts | Where-Object { -not (Test-Path -LiteralPath $_ -PathType Leaf) })
|
||||
}
|
||||
if ($buildsRetro) {
|
||||
# The translator discovers mods through the project file's workspace-relative profile
|
||||
# paths, and both the base and the mod leg block leaf inlining at every address those
|
||||
# profiles patch. A staged install workspace ships without any mod payload, so the
|
||||
# selected Retro Rewind Code.pul must sit at the profile's mod_root before either leg
|
||||
# runs - and it must be this build's pul, not the one a previous build left behind.
|
||||
$awarenessBinaries = Join-Path $Workspace 'PulsarPacks\completed\RetroRewind\RetroRewind6\Binaries'
|
||||
$sourcePul = Join-Path $retroRoot 'Binaries\Code.pul'
|
||||
Assert-File $sourcePul 'Retro Rewind Code.pul'
|
||||
[IO.Directory]::CreateDirectory($awarenessBinaries) | Out-Null
|
||||
$stagedPul = Join-Path $awarenessBinaries 'Code.pul'
|
||||
if (-not $stagedPul.Equals($sourcePul, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
Copy-Item -LiteralPath $sourcePul -Destination $stagedPul -Force
|
||||
}
|
||||
}
|
||||
|
||||
if ($reuseBase -and $buildsRetro) {
|
||||
# The base translation bakes leaf-inlining and residency decisions around the mod patch
|
||||
# sets it knew about when it ran, and it records that knowledge as a modPatchAwareness
|
||||
# stamp. A base tree that never saw this Code.pul would silently bake vanilla code into
|
||||
# the modded product, so it may only be reused once that has been ruled out.
|
||||
$retroCodePul = Join-Path $retroRoot 'Binaries\Code.pul'
|
||||
Assert-File $retroCodePul 'Retro Rewind Code.pul'
|
||||
$pulSha = Get-MkwFileSha256 $retroCodePul
|
||||
# The metadata file is tens of megabytes of machine-written JSON; a raw substring probe
|
||||
# for the uniquely named stamp field avoids a multi-minute ConvertFrom-Json parse.
|
||||
$rawMetadata = [IO.File]::ReadAllText($baseMetadata)
|
||||
if (-not $rawMetadata.Contains('"codePulSha256":"' + $pulSha + '"')) {
|
||||
# A different Code.pul is not automatically a different base translation: the base
|
||||
# reads a pul for one thing, the addresses it patches, so a pul that patches the
|
||||
# same translated functions produces the same base tree. Only the translator can
|
||||
# decide that - it takes parsing the Kamek patch set against the recorded translated
|
||||
# function ranges - and it fails closed, so anything but exit 0 retranslates.
|
||||
& $translator @(
|
||||
'check-base-mod-awareness', '--project', $project, '--profile', 'retro-rewind',
|
||||
'--translation-output-metadata', $baseMetadata, '--code-pul', $retroCodePul
|
||||
) | Write-Host
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-MkwBuildStep 'retranslate-base' 'The base translation is stale; retranslating the base game for the new Code.pul'
|
||||
$reuseBase = $false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($reuseBase) {
|
||||
Write-MkwBuildStep 'reuse-base-translation' 'Reusing the completed base translation'
|
||||
} else {
|
||||
# A failed translation must never leave provenance claiming the old outputs are current.
|
||||
if (Test-Path -LiteralPath $translationProvenance) { Remove-Item -LiteralPath $translationProvenance -Force }
|
||||
[IO.Directory]::CreateDirectory($generated) | Out-Null
|
||||
[IO.Directory]::CreateDirectory($baseManifestDir) | Out-Null
|
||||
|
||||
# No --clean-outdir: shard names are content-addressed and unchanged files keep their
|
||||
# bytes and mtimes, which is exactly what lets Ninja skip them. --prune-stale (active
|
||||
# only without --clean-outdir) removes everything the new translation no longer emits.
|
||||
Invoke-Checked $translator @(
|
||||
'translate-recursive', $pins.EntryPoint, '--project', $project,
|
||||
'--outdir', $functions, '--output-metadata', $baseMetadata,
|
||||
'--production-source-bundle', (Join-Path $generated 'base_translation_sources.bin'),
|
||||
'--no-function-files', '--prune-stale', '--threads', $translatorThreads
|
||||
) 'Translating the user-owned base game' -StepId 'translate-base'
|
||||
|
||||
Invoke-Checked $translator @(
|
||||
'emit-base-manifest', '--project', $project, '--out', $baseManifestDir,
|
||||
'--functions-dir', $functions, '--translation-output-metadata', $baseMetadata, '--region', 'P'
|
||||
) 'Creating the local base translation manifest' -StepId 'emit-base-manifest'
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($TranslationFingerprint)) {
|
||||
[ordered]@{ SchemaVersion = 1; TranslationFingerprint = $TranslationFingerprint } |
|
||||
ConvertTo-Json | Set-Content -LiteralPath $translationProvenance -Encoding UTF8
|
||||
}
|
||||
}
|
||||
|
||||
if ($buildsRetro) {
|
||||
$codePul = Join-Path $retroRoot 'Binaries\Code.pul'
|
||||
Assert-File $codePul 'Retro Rewind Code.pul'
|
||||
$retroOut = Join-Path $Workspace 'build\mods\retro_rewind_full_cpp'
|
||||
$translateModArguments = @(
|
||||
'translate-mod', '--project', $project, '--profile', 'retro-rewind',
|
||||
'--base-manifest', $baseManifest, '--base-translation-output-metadata', $baseMetadata,
|
||||
'--code-pul', $codePul, '--mod-root', $retroRoot, '--mod-name', 'Retro Rewind',
|
||||
'--region', 'P', '--out', $retroOut, '--prefer-cached-inputs', '--emit-cpp',
|
||||
'--threads', $translatorThreads
|
||||
)
|
||||
if ($SkipRetroWfcPayload) {
|
||||
$translateModArguments += '--skip-retro-wfc'
|
||||
} elseif (-not [string]::IsNullOrWhiteSpace($RetroWfcOfflineDirectory)) {
|
||||
$offlineRoot = [IO.Path]::GetFullPath($RetroWfcOfflineDirectory)
|
||||
$offlinePayload = Join-Path $offlineRoot 'binary\payload.RMCPD00.bin'
|
||||
Assert-File $offlinePayload 'Offline Retro-WFC shared payload'
|
||||
$translateModArguments += @('--retro-wfc-payload', $offlinePayload)
|
||||
} else {
|
||||
throw 'Retro Rewind requires either -RetroWfcOfflineDirectory or -SkipRetroWfcPayload.'
|
||||
}
|
||||
Invoke-Checked $translator $translateModArguments 'Translating the selected Retro Rewind Code.pul' `
|
||||
-StepId 'translate-mod'
|
||||
}
|
||||
|
||||
Invoke-Checked $translator @('generate-data-init', '--project', $project) `
|
||||
'Generating local game data initialization' -StepId 'generate-data-init'
|
||||
|
||||
$shardArgs = @(
|
||||
'emit-build-shards', '--project', $project, '--base-metadata', $baseMetadata,
|
||||
'--base-functions-dir', $functions,
|
||||
'--native-source-dir', (Join-Path $Workspace 'runtime\src'), '--out', $shards
|
||||
)
|
||||
if ($buildsRetro) {
|
||||
$retroOut = Join-Path $Workspace 'build\mods\retro_rewind_full_cpp'
|
||||
$shardArgs += @('--resolved-profile', (Join-Path $retroOut 'resolved_dispatch_profile.json'),
|
||||
'--retro-cpp-dir', (Join-Path $retroOut 'cpp'))
|
||||
}
|
||||
Invoke-Checked $translator $shardArgs 'Preparing local native build shards' -StepId 'emit-build-shards'
|
||||
|
||||
# The build directory is kept whenever the exact toolchain that filled it is the one
|
||||
# installed now and its CMake cache still belongs to this workspace path. Object files are
|
||||
# only ever unsafe to reuse when the compiler/flags changed - source changes are what
|
||||
# CMake/Ninja exist to track, so a kept directory turns a small runtime or dependency
|
||||
# change into a recompile of just the affected objects instead of every shard.
|
||||
if (Test-Path -LiteralPath $build) {
|
||||
$keepNativeBuild = -not [string]::IsNullOrWhiteSpace($NativeToolchainFingerprint) -and
|
||||
(Get-RecordedFingerprint $toolchainProvenance 'NativeToolchainFingerprint') -eq $NativeToolchainFingerprint -and
|
||||
(Test-Path -LiteralPath (Join-Path $build 'CMakeCache.txt') -PathType Leaf)
|
||||
if ($keepNativeBuild) {
|
||||
# A moved workspace invalidates every absolute path in the cache; CMake would hard
|
||||
# error rather than reconfigure. Move healing already deletes the directory for
|
||||
# portable installs, so this only fires for hand-moved trees.
|
||||
$expectedHome = (Join-Path $Workspace 'runtime')
|
||||
$cacheHome = Select-String -LiteralPath (Join-Path $build 'CMakeCache.txt') `
|
||||
-Pattern '^CMAKE_HOME_DIRECTORY:INTERNAL=(.*)$' | Select-Object -First 1
|
||||
if ($null -eq $cacheHome -or
|
||||
-not [string]::Equals([IO.Path]::GetFullPath($cacheHome.Matches[0].Groups[1].Value),
|
||||
[IO.Path]::GetFullPath($expectedHome), [StringComparison]::OrdinalIgnoreCase)) {
|
||||
$keepNativeBuild = $false
|
||||
}
|
||||
}
|
||||
if ($keepNativeBuild) {
|
||||
Write-Host 'MKWCBUILD: Reusing the incremental native build directory'
|
||||
} else {
|
||||
Write-Host 'MKWCBUILD: The native build cache does not belong to this toolchain; rebuilding from scratch'
|
||||
Remove-Item -LiteralPath $build -Recurse -Force
|
||||
}
|
||||
}
|
||||
$dependencies = Join-Path $Workspace 'Dependencies'
|
||||
$nativePrebuilt = Resolve-NativePrebuiltDirectory $dependencies $toolchainBin `
|
||||
(Join-Path $Workspace 'aurora-main') (Join-Path $Workspace 'runtime\third_party')
|
||||
$configure = Get-MkwNativeConfigureArguments `
|
||||
-SourceDirectory (Join-Path $Workspace 'runtime') -BuildDirectory $build `
|
||||
-Ninja $ninja -CCompiler $cc -CxxCompiler $cxx -ResourceCompiler $windres `
|
||||
-DependenciesDirectory $dependencies -NativePrebuiltDirectory $nativePrebuilt `
|
||||
-AdditionalArguments @("-DMKW_TRANSLATED_COMPILE_JOBS=$translatedJobs")
|
||||
Invoke-Checked $cmake $configure 'Configuring the bundled native toolchain' -StepId 'configure-native'
|
||||
if (-not [string]::IsNullOrWhiteSpace($NativeToolchainFingerprint)) {
|
||||
[ordered]@{ SchemaVersion = 1; NativeToolchainFingerprint = $NativeToolchainFingerprint } |
|
||||
ConvertTo-Json | Set-Content -LiteralPath $toolchainProvenance -Encoding UTF8
|
||||
}
|
||||
$targets = switch ($Profile) {
|
||||
'base' { @('WiiCompiled') }
|
||||
'retro-rewind' { @('RetroRewind') }
|
||||
'both' { @('WiiCompiled', 'RetroRewind') }
|
||||
}
|
||||
$buildArguments = @('--build', $build)
|
||||
foreach ($target in $targets) { $buildArguments += @('--target', $target) }
|
||||
$buildArguments += @('--parallel', $globalJobs)
|
||||
Invoke-Checked $cmake $buildArguments "Compiling $($targets -join ' and ') locally" -StepId 'compile'
|
||||
|
||||
# Hashed once here; every published product records the same digests.
|
||||
$dolSha = Get-MkwFileSha256 (Join-Path $assets 'main.dol')
|
||||
$relSha = Get-MkwFileSha256 (Join-Path $assets 'StaticR.rel')
|
||||
$compilerSha = Get-MkwFileSha256 (Join-Path $toolchainBin 'clang-22.exe')
|
||||
|
||||
function Publish-BuiltProduct([string]$Target, [string]$Destination, [string]$ProvenanceProfile) {
|
||||
Reset-LocalDirectory $Destination
|
||||
$exe = Join-Path $build "$Target.exe"
|
||||
Assert-File $exe 'Locally compiled game executable'
|
||||
Copy-Item -LiteralPath $exe -Destination (Join-Path $Destination "$Target.exe")
|
||||
foreach ($name in @('dxcompiler.dll','dxil.dll','libc++.dll','libpng16.dll','libunwind.dll','libz.dll','SDL3.dll','sqlite3.dll','webgpu_dawn.dll','z.dll','noshaders.zip','dsp_coef.bin','initial_pipeline_cache.db')) {
|
||||
$file = Join-Path $build $name
|
||||
if (Test-Path -LiteralPath $file -PathType Leaf) { Copy-Item -LiteralPath $file -Destination $Destination }
|
||||
}
|
||||
$redist = Join-Path $Toolkit 'Redist'
|
||||
if (Test-Path -LiteralPath $redist -PathType Container) {
|
||||
Copy-Item -Path (Join-Path $redist '*.dll') -Destination $Destination -Force
|
||||
}
|
||||
foreach ($name in @('config','wii_bootstrap')) {
|
||||
$directory = Join-Path $build $name
|
||||
if (Test-Path -LiteralPath $directory -PathType Container) {
|
||||
Copy-Item -LiteralPath $directory -Destination (Join-Path $Destination $name) -Recurse
|
||||
}
|
||||
}
|
||||
$isRetro = $ProvenanceProfile -eq 'retro-rewind'
|
||||
$provenance = [ordered]@{
|
||||
SchemaVersion = 1
|
||||
Profile = $ProvenanceProfile
|
||||
BuiltUtc = [DateTime]::UtcNow.ToString('O')
|
||||
DolSha256 = $dolSha
|
||||
RelSha256 = $relSha
|
||||
CodePulSha256 = if ($isRetro) { Get-MkwFileSha256 (Join-Path $retroRoot 'Binaries\Code.pul') } else { $null }
|
||||
RetroWfcPayloadMode = if (-not $isRetro) { $null } elseif ($SkipRetroWfcPayload) { 'skipped' } else { $RetroWfcPayloadOrigin }
|
||||
RetroWfcPayloadSha256 = if ($isRetro -and -not $SkipRetroWfcPayload) { Get-MkwFileSha256 (Join-Path $RetroWfcOfflineDirectory 'binary\payload.RMCPD00.bin') } else { $null }
|
||||
RetroWfcPayloadLength = if ($isRetro -and -not $SkipRetroWfcPayload) { (Get-Item -LiteralPath (Join-Path $RetroWfcOfflineDirectory 'binary\payload.RMCPD00.bin')).Length } else { $null }
|
||||
Compiler = "llvm-mingw clang-22 sha256:$compilerSha"
|
||||
}
|
||||
$provenance | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $Destination 'local-build.json') -Encoding UTF8
|
||||
}
|
||||
|
||||
if ($Profile -eq 'both') {
|
||||
Publish-BuiltProduct 'WiiCompiled' $BaseOutputDirectory 'base'
|
||||
Publish-BuiltProduct 'RetroRewind' $OutputDirectory 'retro-rewind'
|
||||
} elseif ($Profile -eq 'retro-rewind') {
|
||||
Publish-BuiltProduct 'RetroRewind' $OutputDirectory 'retro-rewind'
|
||||
} else {
|
||||
Publish-BuiltProduct 'WiiCompiled' $OutputDirectory 'base'
|
||||
}
|
||||
Write-Host "MKWCBUILD:OUTPUT=$OutputDirectory"
|
||||
} finally { Pop-Location }
|
||||
} finally {
|
||||
$env:PATH = $oldPath
|
||||
if ($null -ne $oldDotnet) { $env:DOTNET_ROOT = $oldDotnet }
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
# Canonical native (CMake) configure flags, plus the shared helpers (file assertions, checked
|
||||
# process spawn, file hashing) both LocalBuild.ps1 and Prepare-NativePrebuilt.ps1 dot-source.
|
||||
# They MUST stay byte-identical: the prebuilt package ships static archives linked into objects
|
||||
# the user's machine compiles, so any flag divergence is an ABI/ODR hazard, not just a rebuild.
|
||||
# The flag fingerprint below is recorded in the package provenance and re-checked before use.
|
||||
|
||||
Set-StrictMode -Version 3.0
|
||||
|
||||
function Assert-File([string]$Path, [string]$Description) {
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "$Description is missing: $Path" }
|
||||
}
|
||||
|
||||
function Assert-Directory([string]$Path, [string]$Description) {
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Container)) { throw "$Description is missing: $Path" }
|
||||
}
|
||||
|
||||
function Get-MkwFileSha256([string]$Path) {
|
||||
<#
|
||||
Lower-case SHA-256 of one file. -LiteralPath is required: Get-FileHash treats a positional
|
||||
path as a wildcard, so an install directory containing [, ] or * would hash the wrong file.
|
||||
#>
|
||||
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Get-MkwToolchainPath([string]$ToolchainRoot) {
|
||||
<#
|
||||
The scrubbed PATH a bundled-toolchain build runs with. The bundled toolchain must be the only
|
||||
one visible, or an ambient Visual Studio/MSYS install on PATH could pull in its own headers,
|
||||
libraries, or linker. Shared by LocalBuild.ps1 and Prepare-NativePrebuilt.ps1 since the
|
||||
installed Toolkit and the maintainer portable-tools tree share the same subdirectory layout.
|
||||
#>
|
||||
if ([string]::IsNullOrWhiteSpace($ToolchainRoot)) { throw 'A toolchain root is required.' }
|
||||
$root = [IO.Path]::GetFullPath($ToolchainRoot)
|
||||
return (@(
|
||||
(Join-Path $root 'llvm-mingw\bin'),
|
||||
(Join-Path $root 'CMake\bin'),
|
||||
(Join-Path $root 'Ninja'),
|
||||
(Join-Path $env:SystemRoot 'System32'),
|
||||
$env:SystemRoot
|
||||
) -join ';')
|
||||
}
|
||||
|
||||
function Get-MkwProjectPins([string]$ProjectFile) {
|
||||
<#
|
||||
The Mario Kart Wii facts pinned by projects/mkwii/recomp.yml (game identity, clean input
|
||||
digests, translation entry point, Retro-WFC endpoint), read from one place instead of being
|
||||
restated; Test-PinnedFacts.ps1 checks the copies that can't read YAML (C++ header, C#
|
||||
constants) against these values. Parsing is literal, not a YAML dependency, since the file is
|
||||
machine-written with a fixed shape.
|
||||
#>
|
||||
Assert-File $ProjectFile 'Translation project file'
|
||||
$pins = [ordered]@{
|
||||
GameId = ''; DolSha256 = ''; RelSha256 = ''; EntryPoint = ''; RetroWfcPayloadUri = ''
|
||||
}
|
||||
$section = ''
|
||||
$inputKey = ''
|
||||
foreach ($raw in [IO.File]::ReadAllLines($ProjectFile)) {
|
||||
$line = ($raw -replace '#.*$', '')
|
||||
if ([string]::IsNullOrWhiteSpace($line)) { continue }
|
||||
if ($line -match '^([A-Za-z0-9_]+):') { $section = $Matches[1]; $inputKey = ''; continue }
|
||||
if ($section -eq 'inputs' -and $line -match '^\s{2}([A-Za-z0-9_]+):\s*$') { $inputKey = $Matches[1]; continue }
|
||||
if ($section -eq 'project' -and $line -match '^\s*game_id:\s*(\S+)\s*$') {
|
||||
$pins.GameId = $Matches[1]
|
||||
} elseif ($section -eq 'inputs' -and $line -match '^\s*sha256:\s*([0-9a-fA-F]{64})\s*$') {
|
||||
if ($inputKey -eq 'dol') { $pins.DolSha256 = $Matches[1].ToLowerInvariant() }
|
||||
elseif ($inputKey -eq 'rel') { $pins.RelSha256 = $Matches[1].ToLowerInvariant() }
|
||||
} elseif ($section -eq 'translation' -and $pins.EntryPoint -eq '' -and
|
||||
$line -match '^\s*-\s*(0[xX][0-9a-fA-F]+)\s*$') {
|
||||
$pins.EntryPoint = $Matches[1].ToLowerInvariant()
|
||||
} elseif ($line -match '^\s*retro_wfc_payload:\s*(\S+)\s*$') {
|
||||
$pins.RetroWfcPayloadUri = $Matches[1]
|
||||
}
|
||||
}
|
||||
foreach ($name in @($pins.Keys)) {
|
||||
if ([string]::IsNullOrWhiteSpace($pins[$name])) {
|
||||
throw "$ProjectFile does not pin $name; the project file is not the shape this build expects."
|
||||
}
|
||||
}
|
||||
return $pins
|
||||
}
|
||||
|
||||
function Invoke-Checked([string]$FilePath, [string[]]$Arguments, [string]$Description,
|
||||
[string]$LogPrefix = 'MKWCBUILD', [string]$StepId = '') {
|
||||
<#
|
||||
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.
|
||||
-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.
|
||||
#>
|
||||
if ($StepId) { Write-Host "${LogPrefix}:STEP:${StepId} $Description" }
|
||||
else { Write-Host "${LogPrefix}: $Description" }
|
||||
$quotedArguments = @($Arguments | ForEach-Object {
|
||||
if ($_.Contains('"')) { throw "A native build argument contains an unsupported quote: $_" }
|
||||
'"' + $_ + '"'
|
||||
})
|
||||
$process = Start-Process -FilePath $FilePath -ArgumentList $quotedArguments `
|
||||
-NoNewWindow -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
$global:LASTEXITCODE = $exitCode
|
||||
if ($exitCode -ne 0) { throw "$Description failed with exit code $exitCode." }
|
||||
}
|
||||
|
||||
function Get-MkwNativeFixedConfigureFlags {
|
||||
<#
|
||||
The path-independent half of the configure command line, identical on every machine, which is
|
||||
what makes one precompiled aurora/third-party package valid for all users. Path-bearing
|
||||
arguments are added separately by Get-MkwNativeConfigureArguments and excluded from the fingerprint.
|
||||
#>
|
||||
return @(
|
||||
'-DCMAKE_BUILD_TYPE=Release',
|
||||
'-DCMAKE_SYSTEM_PROCESSOR=x86_64',
|
||||
'-DAURORA_DAWN_PROVIDER=package', '-DAURORA_SDL3_PROVIDER=vendor',
|
||||
'-DCMAKE_POLICY_DEFAULT_CMP0168=NEW', '-DFETCHCONTENT_FULLY_DISCONNECTED=ON',
|
||||
'-DAWK:FILEPATH='
|
||||
)
|
||||
}
|
||||
|
||||
function Get-MkwNativeFlagFingerprint {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
SHA-256 over the canonical flag list, used to reject a stale package.
|
||||
#>
|
||||
$canonical = (Get-MkwNativeFixedConfigureFlags) -join "`n"
|
||||
$sha = [Security.Cryptography.SHA256]::Create()
|
||||
try {
|
||||
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonical))
|
||||
} finally { $sha.Dispose() }
|
||||
return (($bytes | ForEach-Object { $_.ToString('x2') }) -join '')
|
||||
}
|
||||
|
||||
function Get-MkwDawnRuntimeSha256([string]$DependenciesDirectory) {
|
||||
<#
|
||||
SHA-256 of the pinned Dawn runtime the prebuilt aurora must match. The precompiled aurora
|
||||
archives are compiled and linked against the pinned dawn_prebuilt tree's headers/import
|
||||
library, but the upstream release asset is mutable (the same version tag has already served
|
||||
two different windows-amd64 archives), so the version string alone can't prove agreement.
|
||||
A mismatch surfaces as an access violation at surface creation, not a link error, so the
|
||||
digest is recorded at harvest and re-checked before use. Returns '' when the tree is absent.
|
||||
#>
|
||||
if ([string]::IsNullOrWhiteSpace($DependenciesDirectory)) { return '' }
|
||||
$runtime = Join-Path $DependenciesDirectory 'dawn_prebuilt\bin\webgpu_dawn.dll'
|
||||
if (-not (Test-Path -LiteralPath $runtime -PathType Leaf)) { return '' }
|
||||
return Get-MkwFileSha256 $runtime
|
||||
}
|
||||
|
||||
function Get-MkwThirdPartySourceFingerprint([string]$ThirdPartyDirectory) {
|
||||
<#
|
||||
SHA-256 over runtime/third_party, the vendored sources behind the prebuilt package. The
|
||||
precompiled Crypto++ archive is linked while first-party TUs compile against the same headers,
|
||||
so a workspace whose vendored tree moved on must not consume archives harvested from an older
|
||||
one (same reasoning as the aurora fingerprint below). Unlike that scan this hashes hidden files
|
||||
too, since Build-Installer stages this tree with Copy-Item -Force. Returns '' when absent.
|
||||
#>
|
||||
if ([string]::IsNullOrWhiteSpace($ThirdPartyDirectory)) { return '' }
|
||||
if (-not (Test-Path -LiteralPath $ThirdPartyDirectory -PathType Container)) { return '' }
|
||||
$root = [IO.Path]::GetFullPath($ThirdPartyDirectory).TrimEnd('\')
|
||||
$files = Get-ChildItem -LiteralPath $root -Recurse -File |
|
||||
Sort-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') }
|
||||
$sha = [Security.Cryptography.SHA256]::Create()
|
||||
try {
|
||||
$builder = [Text.StringBuilder]::new()
|
||||
foreach ($file in $files) {
|
||||
$relative = $file.FullName.Substring($root.Length + 1).Replace('\', '/')
|
||||
$content = Get-MkwFileSha256 $file.FullName
|
||||
[void]$builder.Append($relative).Append(' ').Append($content).Append("`n")
|
||||
}
|
||||
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($builder.ToString()))
|
||||
} finally { $sha.Dispose() }
|
||||
return (($bytes | ForEach-Object { $_.ToString('x2') }) -join '')
|
||||
}
|
||||
|
||||
function Get-MkwAuroraSourceFingerprint([string]$AuroraDirectory) {
|
||||
<#
|
||||
SHA-256 over the aurora sources the precompiled archives were harvested from. The compiler,
|
||||
flag, and Dawn digests only prove the package agrees with its target environment; none notice
|
||||
aurora itself moving on, and since the runtime calls aurora headers directly, a package
|
||||
harvested before an aurora change links old code against new call sites (undefined symbol at
|
||||
best, silently stale renderer at worst). extern/ is excluded because the payload ships those
|
||||
trees separately. Returns '' when the tree is absent.
|
||||
#>
|
||||
if ([string]::IsNullOrWhiteSpace($AuroraDirectory)) { return '' }
|
||||
if (-not (Test-Path -LiteralPath $AuroraDirectory -PathType Container)) { return '' }
|
||||
$root = [IO.Path]::GetFullPath($AuroraDirectory).TrimEnd('\')
|
||||
$excludedPrefixes = @(
|
||||
(Join-Path $root 'extern') + '\',
|
||||
(Join-Path $root 'build') + '\'
|
||||
)
|
||||
# Hidden files are skipped deliberately: Build-Installer.ps1 stages this tree
|
||||
# with Copy-Item -Recurse (no -Force), so they never reach the payload and
|
||||
# hashing them would make the two sides disagree by construction.
|
||||
$files = Get-ChildItem -LiteralPath $root -Recurse -File |
|
||||
Where-Object {
|
||||
$full = $_.FullName
|
||||
-not ($excludedPrefixes | Where-Object { $full.StartsWith($_, [StringComparison]::OrdinalIgnoreCase) })
|
||||
} |
|
||||
Sort-Object { $_.FullName.Substring($root.Length + 1).Replace('\', '/') }
|
||||
$sha = [Security.Cryptography.SHA256]::Create()
|
||||
try {
|
||||
$builder = [Text.StringBuilder]::new()
|
||||
foreach ($file in $files) {
|
||||
$relative = $file.FullName.Substring($root.Length + 1).Replace('\', '/')
|
||||
$content = Get-MkwFileSha256 $file.FullName
|
||||
[void]$builder.Append($relative).Append(' ').Append($content).Append("`n")
|
||||
}
|
||||
$bytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($builder.ToString()))
|
||||
} finally { $sha.Dispose() }
|
||||
return (($bytes | ForEach-Object { $_.ToString('x2') }) -join '')
|
||||
}
|
||||
|
||||
function ConvertTo-MkwCMakePath([string]$Path) {
|
||||
return [IO.Path]::GetFullPath($Path).Replace('\', '/')
|
||||
}
|
||||
|
||||
function Get-MkwNativeConfigureArguments {
|
||||
<# The complete `cmake` configure argument list for a local runtime build. #>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)] [string]$SourceDirectory,
|
||||
[Parameter(Mandatory)] [string]$BuildDirectory,
|
||||
[Parameter(Mandatory)] [string]$Ninja,
|
||||
[Parameter(Mandatory)] [string]$CCompiler,
|
||||
[Parameter(Mandatory)] [string]$CxxCompiler,
|
||||
[Parameter(Mandatory)] [string]$ResourceCompiler,
|
||||
[string]$DependenciesDirectory,
|
||||
[string]$NativePrebuiltDirectory,
|
||||
[string[]]$AdditionalArguments = @()
|
||||
)
|
||||
|
||||
$arguments = @(
|
||||
'-S', (ConvertTo-MkwCMakePath $SourceDirectory),
|
||||
'-B', (ConvertTo-MkwCMakePath $BuildDirectory),
|
||||
'-G', 'Ninja',
|
||||
"-DCMAKE_MAKE_PROGRAM=$(ConvertTo-MkwCMakePath $Ninja)",
|
||||
"-DCMAKE_C_COMPILER=$(ConvertTo-MkwCMakePath $CCompiler)",
|
||||
"-DCMAKE_CXX_COMPILER=$(ConvertTo-MkwCMakePath $CxxCompiler)",
|
||||
"-DCMAKE_RC_COMPILER=$(ConvertTo-MkwCMakePath $ResourceCompiler)"
|
||||
)
|
||||
$arguments += Get-MkwNativeFixedConfigureFlags
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($DependenciesDirectory) -and
|
||||
(Test-Path -LiteralPath $DependenciesDirectory -PathType Container)) {
|
||||
$cppWinRt = Join-Path $DependenciesDirectory 'cppwinrt'
|
||||
if (Test-Path -LiteralPath (Join-Path $cppWinRt 'winrt\base.h') -PathType Leaf) {
|
||||
$arguments += "-DMKW_CPPWINRT_INCLUDE_DIR=$(ConvertTo-MkwCMakePath $cppWinRt)"
|
||||
}
|
||||
foreach ($directory in Get-ChildItem -LiteralPath $DependenciesDirectory -Directory) {
|
||||
# native_prebuilt holds compiled output, not a FetchContent source tree.
|
||||
if ($directory.Name -eq 'native_prebuilt') { continue }
|
||||
# FetchContent uppercases the declared name but preserves punctuation
|
||||
# (for example abseil-cpp -> FETCHCONTENT_SOURCE_DIR_ABSEIL-CPP).
|
||||
$name = $directory.Name.ToUpperInvariant()
|
||||
$arguments += "-DFETCHCONTENT_SOURCE_DIR_$name=$(ConvertTo-MkwCMakePath $directory.FullName)"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($NativePrebuiltDirectory)) {
|
||||
$arguments += "-DMKW_NATIVE_PREBUILT_DIR=$(ConvertTo-MkwCMakePath $NativePrebuiltDirectory)"
|
||||
}
|
||||
|
||||
$arguments += $AdditionalArguments
|
||||
return $arguments
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
# Builds the redistributable precompiled aurora + third-party package: aurora (~43% of local build
|
||||
# CPU time) and vendored Crypto++ are identical for every user, so this configures runtime/ like
|
||||
# LocalBuild.ps1 (both dot-source NativeBuildFlags.ps1), builds just that closure, and harvests the
|
||||
# archives plus a generated CMake description into launcher/artifacts/dependencies/native_prebuilt.
|
||||
[CmdletBinding(PositionalBinding = $false)]
|
||||
param(
|
||||
[string]$PortableToolsDirectory = 'launcher/artifacts/portable-tools',
|
||||
[string]$DependencySourceDirectory = 'launcher/artifacts/dependencies',
|
||||
[string]$OutputDirectory,
|
||||
[string]$StageDirectory = 'build/native-prebuilt-stage',
|
||||
[switch]$KeepStage,
|
||||
# Maintainer iteration aid: keep an already configured staging build so a
|
||||
# re-harvest does not recompile aurora from scratch.
|
||||
[switch]$ReuseStage,
|
||||
[int]$Parallel = 0
|
||||
)
|
||||
|
||||
$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))
|
||||
}
|
||||
function Normalize([string]$Path) { return ([IO.Path]::GetFullPath($Path)).Replace('\', '/') }
|
||||
function StartsWithPath([string]$Candidate, [string]$Root) {
|
||||
$rootSlash = $Root.TrimEnd('/') + '/'
|
||||
return $Candidate.StartsWith($rootSlash, [StringComparison]::OrdinalIgnoreCase)
|
||||
}
|
||||
function RelativeTo([string]$Candidate, [string]$Root) {
|
||||
$rootSlash = $Root.TrimEnd('/') + '/'
|
||||
return $Candidate.Substring($rootSlash.Length)
|
||||
}
|
||||
|
||||
$portableTools = Full $PortableToolsDirectory
|
||||
$dependencies = Full $DependencySourceDirectory
|
||||
$stage = Full $StageDirectory
|
||||
if ([string]::IsNullOrWhiteSpace($OutputDirectory)) {
|
||||
$OutputDirectory = Join-Path $dependencies 'native_prebuilt'
|
||||
}
|
||||
$package = Full $OutputDirectory
|
||||
|
||||
$toolchainBin = Join-Path $portableTools 'llvm-mingw\bin'
|
||||
$cmake = Join-Path $portableTools 'CMake\bin\cmake.exe'
|
||||
$ninja = Join-Path $portableTools 'Ninja\ninja.exe'
|
||||
$cc = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang.exe'
|
||||
$cxx = Join-Path $toolchainBin 'x86_64-w64-mingw32-clang++.exe'
|
||||
$windres = Join-Path $toolchainBin 'x86_64-w64-mingw32-windres.exe'
|
||||
$clangBinary = Join-Path $toolchainBin 'clang-22.exe'
|
||||
$runtimeSource = Join-Path $repoRoot 'runtime'
|
||||
$auroraSource = Join-Path $repoRoot 'aurora-main'
|
||||
|
||||
Assert-File $cmake 'Portable CMake'
|
||||
Assert-File $ninja 'Portable Ninja'
|
||||
Assert-File $cc 'Portable C compiler'
|
||||
Assert-File $cxx 'Portable C++ compiler'
|
||||
Assert-File $windres 'Portable resource compiler'
|
||||
Assert-File $clangBinary 'Portable clang driver binary'
|
||||
Assert-Directory $dependencies 'Pinned offline dependency sources'
|
||||
Assert-Directory $auroraSource 'aurora-main source tree'
|
||||
Assert-File (Join-Path $repoRoot 'generated\build_shards\shards.cmake') `
|
||||
'Translator shard manifest (run the developer build once so runtime/ can configure)'
|
||||
|
||||
if ($Parallel -le 0) { $Parallel = [Environment]::ProcessorCount }
|
||||
|
||||
# The package must never contain a stale mixture of two builds.
|
||||
if (Test-Path -LiteralPath $package) { Remove-Item -LiteralPath $package -Recurse -Force }
|
||||
[IO.Directory]::CreateDirectory($package) | Out-Null
|
||||
[IO.Directory]::CreateDirectory((Join-Path $package 'lib')) | Out-Null
|
||||
[IO.Directory]::CreateDirectory((Join-Path $package 'bin')) | Out-Null
|
||||
[IO.Directory]::CreateDirectory((Join-Path $package 'include')) | Out-Null
|
||||
|
||||
# The staging build has to be configured without the package present, otherwise
|
||||
# the runtime would consume the very package this script is producing.
|
||||
if (-not $ReuseStage -and (Test-Path -LiteralPath $stage)) { Remove-Item -LiteralPath $stage -Recurse -Force }
|
||||
$exportDirectory = Join-Path $stage 'export'
|
||||
[IO.Directory]::CreateDirectory($exportDirectory) | Out-Null
|
||||
|
||||
$oldPath = $env:PATH
|
||||
try {
|
||||
$env:PATH = Get-MkwToolchainPath $portableTools
|
||||
|
||||
$configure = Get-MkwNativeConfigureArguments -SourceDirectory $runtimeSource -BuildDirectory $stage `
|
||||
-Ninja $ninja -CCompiler $cc -CxxCompiler $cxx -ResourceCompiler $windres `
|
||||
-DependenciesDirectory $dependencies `
|
||||
-AdditionalArguments @("-DMKW_NATIVE_PREBUILT_EXPORT_DIR=$(ConvertTo-MkwCMakePath $exportDirectory)")
|
||||
Invoke-Checked $cmake $configure 'Configuring the aurora/third-party staging build' -LogPrefix 'MKWNP'
|
||||
|
||||
# mkw_np_probe links exactly what mkw_runtime_common and the public products
|
||||
# link, so building it compiles the whole redistributable closure and nothing
|
||||
# else - and a successful link proves the harvested archives are complete.
|
||||
Invoke-Checked $cmake @('--build', $stage, '--target', 'mkw_np_probe', '--parallel', "$Parallel") `
|
||||
'Building the aurora/third-party closure' -LogPrefix 'MKWNP'
|
||||
} finally {
|
||||
$env:PATH = $oldPath
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Read the description CMake wrote, plus the resolved command lines Ninja holds.
|
||||
# ---------------------------------------------------------------------------
|
||||
$metaPath = Join-Path $exportDirectory 'meta.txt'
|
||||
Assert-File $metaPath 'Native prebuilt export metadata'
|
||||
$meta = @{}
|
||||
foreach ($line in Get-Content -LiteralPath $metaPath) {
|
||||
if ($line -match '^([a-z0-9_]+)=(.*)$') { $meta[$Matches[1]] = $Matches[2] }
|
||||
}
|
||||
|
||||
$buildNinja = Join-Path $stage 'build.ninja'
|
||||
Assert-File $buildNinja 'Generated build.ninja'
|
||||
$ninjaLines = [IO.File]::ReadAllLines($buildNinja)
|
||||
|
||||
function Get-NinjaStatementVariables([string[]]$Lines, [string]$RulePrefix) {
|
||||
$variables = $null
|
||||
for ($i = 0; $i -lt $Lines.Length; $i++) {
|
||||
$line = $Lines[$i]
|
||||
if (-not $line.StartsWith('build ')) { continue }
|
||||
$colon = $line.IndexOf(': ')
|
||||
if ($colon -lt 0) { continue }
|
||||
$rule = $line.Substring($colon + 2)
|
||||
$space = $rule.IndexOf(' ')
|
||||
if ($space -ge 0) { $rule = $rule.Substring(0, $space) }
|
||||
if ($rule -ne $RulePrefix) { continue }
|
||||
$variables = [ordered]@{}
|
||||
for ($j = $i + 1; $j -lt $Lines.Length; $j++) {
|
||||
$entry = $Lines[$j]
|
||||
if ($entry -notmatch '^ ([A-Z_]+) = ?(.*)$') { break }
|
||||
$variables[$Matches[1]] = $Matches[2]
|
||||
}
|
||||
break
|
||||
}
|
||||
if ($null -eq $variables) { throw "No Ninja statement used rule $RulePrefix." }
|
||||
return $variables
|
||||
}
|
||||
function Get-NinjaValue($Variables, [string]$Name) {
|
||||
if ($Variables.Contains($Name)) { return [string]$Variables[$Name] }
|
||||
return ''
|
||||
}
|
||||
function Split-NinjaList([string]$Value) {
|
||||
if ([string]::IsNullOrWhiteSpace($Value)) { return , [string[]]@() }
|
||||
return , [string[]]@($Value -split '\s+' | Where-Object { $_ })
|
||||
}
|
||||
function Subtract-Ordered([string[]]$All, [string[]]$Remove) {
|
||||
if ($null -eq $All) { return [string[]]@() }
|
||||
$result = [Collections.Generic.List[string]]::new()
|
||||
$pending = [Collections.Generic.List[string]]::new()
|
||||
if ($null -ne $Remove) { foreach ($item in $Remove) { $pending.Add($item) } }
|
||||
foreach ($item in $All) {
|
||||
$index = $pending.IndexOf($item)
|
||||
if ($index -ge 0) { $pending.RemoveAt($index); continue }
|
||||
$result.Add($item)
|
||||
}
|
||||
return $result.ToArray()
|
||||
}
|
||||
|
||||
$probeCompile = Get-NinjaStatementVariables $ninjaLines 'CXX_COMPILER__mkw_np_probe_unscanned_Release'
|
||||
$controlCompile = Get-NinjaStatementVariables $ninjaLines 'CXX_COMPILER__mkw_np_probe_control_unscanned_Release'
|
||||
$probeLink = Get-NinjaStatementVariables $ninjaLines 'CXX_EXECUTABLE_LINKER__mkw_np_probe_Release'
|
||||
$controlLink = Get-NinjaStatementVariables $ninjaLines 'CXX_EXECUTABLE_LINKER__mkw_np_probe_control_Release'
|
||||
|
||||
$includeArguments = Subtract-Ordered (Split-NinjaList (Get-NinjaValue $probeCompile 'INCLUDES')) `
|
||||
(Split-NinjaList (Get-NinjaValue $controlCompile 'INCLUDES'))
|
||||
$defineArguments = Subtract-Ordered (Split-NinjaList (Get-NinjaValue $probeCompile 'DEFINES')) `
|
||||
(Split-NinjaList (Get-NinjaValue $controlCompile 'DEFINES'))
|
||||
$compileOptions = Subtract-Ordered (Split-NinjaList (Get-NinjaValue $probeCompile 'FLAGS')) `
|
||||
(Split-NinjaList (Get-NinjaValue $controlCompile 'FLAGS'))
|
||||
$linkItems = Subtract-Ordered (Split-NinjaList (Get-NinjaValue $probeLink 'LINK_LIBRARIES')) `
|
||||
(Split-NinjaList (Get-NinjaValue $controlLink 'LINK_LIBRARIES'))
|
||||
if ($includeArguments.Count -eq 0) { throw 'The aurora compile interface is empty; the probe did not link aurora.' }
|
||||
if ($linkItems.Count -eq 0) { throw 'The aurora link interface is empty; the probe did not link aurora.' }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Harvest the built libraries.
|
||||
# ---------------------------------------------------------------------------
|
||||
$binaryRoot = Normalize $meta['binary_dir']
|
||||
$auroraRoot = Normalize $meta['aurora_dir']
|
||||
$runtimeRoot = Normalize $meta['runtime_dir']
|
||||
$dependencyRoot = Normalize $dependencies
|
||||
|
||||
$targets = @{}
|
||||
foreach ($line in Get-Content -LiteralPath (Join-Path $exportDirectory 'targets.txt')) {
|
||||
if ([string]::IsNullOrWhiteSpace($line)) { continue }
|
||||
$parts = $line -split '\|'
|
||||
if ($parts.Count -ne 4) { throw "Malformed exported target line: $line" }
|
||||
$targets[$parts[0]] = [pscustomobject]@{
|
||||
Name = $parts[0]; Type = $parts[1]
|
||||
File = Normalize $parts[2]; LinkerFile = Normalize $parts[3]
|
||||
}
|
||||
}
|
||||
|
||||
$dawnLinkerFile = ''
|
||||
$dawnRuntimeFile = ''
|
||||
foreach ($line in Get-Content -LiteralPath (Join-Path $exportDirectory 'dawn.txt')) {
|
||||
if ([string]::IsNullOrWhiteSpace($line)) { continue }
|
||||
$parts = $line -split '\|'
|
||||
$dawnRuntimeFile = Normalize $parts[1]
|
||||
$dawnLinkerFile = Normalize $parts[2]
|
||||
}
|
||||
|
||||
# linker-file path -> package reference, built only from libraries that were
|
||||
# actually produced by this build.
|
||||
$linkerFileToReference = @{}
|
||||
$sharedImports = [Collections.Generic.List[object]]::new()
|
||||
$copiedFiles = [Collections.Generic.List[string]]::new()
|
||||
function Copy-Harvested([string]$SourcePath, [string]$SubDirectory) {
|
||||
$name = [IO.Path]::GetFileName($SourcePath)
|
||||
$destination = Join-Path (Join-Path $package $SubDirectory) $name
|
||||
if (Test-Path -LiteralPath $destination) {
|
||||
throw "Two harvested files collide on the name ${name}: $SourcePath"
|
||||
}
|
||||
Copy-Item -LiteralPath $SourcePath -Destination $destination
|
||||
$copiedFiles.Add("$SubDirectory/$name")
|
||||
return "$SubDirectory/$name"
|
||||
}
|
||||
|
||||
foreach ($name in ($targets.Keys | Sort-Object)) {
|
||||
$target = $targets[$name]
|
||||
if (-not (Test-Path -LiteralPath $target.LinkerFile -PathType Leaf)) {
|
||||
# Not part of the closure mkw_np_probe pulled in; deliberately not shipped.
|
||||
continue
|
||||
}
|
||||
$relativeLinker = Copy-Harvested $target.LinkerFile 'lib'
|
||||
if ($target.Type -eq 'SHARED_LIBRARY') {
|
||||
if (-not (Test-Path -LiteralPath $target.File -PathType Leaf)) {
|
||||
throw "Shared library $name has no runtime file: $($target.File)"
|
||||
}
|
||||
$relativeRuntime = Copy-Harvested $target.File 'bin'
|
||||
$sharedImports.Add([pscustomobject]@{
|
||||
Target = "mkw_np::$name"; Runtime = $relativeRuntime; Implib = $relativeLinker })
|
||||
$linkerFileToReference[$target.LinkerFile] = "mkw_np::$name"
|
||||
} else {
|
||||
$linkerFileToReference[$target.LinkerFile] = "`@PKG`@/$relativeLinker"
|
||||
}
|
||||
}
|
||||
if ($sharedImports.Count -eq 0) { throw 'No shared libraries were harvested; the SDL/zlib/libpng DLLs would be missing.' }
|
||||
if ($dawnLinkerFile) { $linkerFileToReference[$dawnLinkerFile] = 'dawn::webgpu_dawn' }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rewrite the compile/link interface into workspace-relative tokens.
|
||||
# ---------------------------------------------------------------------------
|
||||
$generatedIncludeIndex = 0
|
||||
function ConvertTo-Token([string]$AbsolutePath, [string]$Kind) {
|
||||
$path = Normalize $AbsolutePath
|
||||
if (StartsWithPath $path $auroraRoot) { return "`@AURORA`@/$(RelativeTo $path $auroraRoot)" }
|
||||
if (StartsWithPath $path $runtimeRoot) { return "`@RUNTIME`@/$(RelativeTo $path $runtimeRoot)" }
|
||||
if (StartsWithPath $path $dependencyRoot) { return "`@DEPS`@/$(RelativeTo $path $dependencyRoot)" }
|
||||
if (StartsWithPath $path $binaryRoot) {
|
||||
if ($Kind -ne 'include') { throw "Unhandled build-tree artifact: $path" }
|
||||
# A generated header directory (SDL_revision.h and friends). The source
|
||||
# trees ship, but these do not exist until something configures them, so
|
||||
# they travel inside the package.
|
||||
$script:generatedIncludeIndex++
|
||||
$name = 'generated{0:d2}' -f $script:generatedIncludeIndex
|
||||
$destination = Join-Path (Join-Path $package 'include') $name
|
||||
[IO.Directory]::CreateDirectory($destination) | Out-Null
|
||||
Copy-Item -Path (Join-Path $path '*') -Destination $destination -Recurse -Force
|
||||
foreach ($file in Get-ChildItem -LiteralPath $destination -Recurse -File) {
|
||||
$copiedFiles.Add(('include/{0}/{1}' -f $name,
|
||||
$file.FullName.Substring($destination.Length + 1).Replace('\', '/')))
|
||||
}
|
||||
return "`@PKG`@/include/$name"
|
||||
}
|
||||
throw "Path escapes every shippable root ($Kind): $path"
|
||||
}
|
||||
|
||||
# Ninja splits "-isystem <path>" into two list entries (the vendored Crypto++
|
||||
# headers are declared SYSTEM, so they arrive that way), while plain aurora
|
||||
# includes stay glued to -I. Only the directory itself travels in the package;
|
||||
# the consumer re-emits it through INTERFACE_INCLUDE_DIRECTORIES and CMake
|
||||
# chooses the flag form per usage context.
|
||||
$packageIncludeDirectories = @()
|
||||
for ($i = 0; $i -lt $includeArguments.Count; $i++) {
|
||||
$entry = $includeArguments[$i]
|
||||
if ($entry.StartsWith('-I')) {
|
||||
$packageIncludeDirectories += (ConvertTo-Token $entry.Substring(2) 'include')
|
||||
} elseif ($entry -eq '-isystem' -and ($i + 1) -lt $includeArguments.Count) {
|
||||
$i++
|
||||
$packageIncludeDirectories += (ConvertTo-Token $includeArguments[$i] 'include')
|
||||
} elseif ($entry.StartsWith('-isystem')) {
|
||||
$packageIncludeDirectories += (ConvertTo-Token $entry.Substring('-isystem'.Length) 'include')
|
||||
} else {
|
||||
throw "Unexpected include argument: $entry"
|
||||
}
|
||||
}
|
||||
|
||||
$packageDefinitions = @($defineArguments | ForEach-Object {
|
||||
if (-not $_.StartsWith('-D')) { throw "Unexpected define argument: $_" }
|
||||
# Ninja carries the Windows command-line escaping; CMake re-applies it.
|
||||
$_.Substring(2).Replace('\"', '"')
|
||||
})
|
||||
|
||||
$packageLinkItems = @($linkItems | ForEach-Object {
|
||||
$item = $_
|
||||
if ($item.StartsWith('-')) { return $item }
|
||||
$absolute = if ([IO.Path]::IsPathRooted($item)) { Normalize $item } else { Normalize (Join-Path $stage $item) }
|
||||
if ($linkerFileToReference.ContainsKey($absolute)) { return $linkerFileToReference[$absolute] }
|
||||
if (StartsWithPath $absolute $dependencyRoot) { return "`@DEPS`@/$(RelativeTo $absolute $dependencyRoot)" }
|
||||
throw "Link item is neither a system library nor a harvested archive: $item"
|
||||
})
|
||||
|
||||
# fmt's `-include cstdlib` (and anything like it) is recorded from a C++ compile
|
||||
# line, so re-scope it to C++. The runtime also assembles generated .S blobs
|
||||
# through the same targets, and a forced C++ header include would break them.
|
||||
$packageCompileOptions = @($compileOptions | ForEach-Object { "`$<`$<COMPILE_LANGUAGE:CXX>:$_>" })
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Emit the consumer-facing CMake description.
|
||||
# ---------------------------------------------------------------------------
|
||||
function Format-CMakeBlock([string]$Name, [string[]]$Items) {
|
||||
if ($null -eq $Items -or $Items.Count -eq 0) { return "set($Name `"`")`n" }
|
||||
$body = ($Items | ForEach-Object { ' "' + $_.Replace('\', '/').Replace('"', '\"') + '"' }) -join "`n"
|
||||
return "set($Name`n$body)`n"
|
||||
}
|
||||
|
||||
$generated = [Collections.Generic.List[string]]::new()
|
||||
$generated.Add('# Generated by launcher/Prepare-NativePrebuilt.ps1 - do not edit.')
|
||||
$generated.Add('#')
|
||||
$generated.Add('# Describes the precompiled aurora + third-party archives that replace a')
|
||||
$generated.Add('# from-source aurora-main build. Every path is a token resolved against the')
|
||||
$generated.Add('# consuming workspace, because the source trees still ship next to this')
|
||||
$generated.Add('# package and the runtime compiles against their headers.')
|
||||
$generated.Add('')
|
||||
$generated.Add('if(NOT MKW_NP_PACKAGE_DIR OR NOT MKW_NP_AURORA_DIR OR NOT MKW_NP_RUNTIME_DIR OR NOT MKW_NP_DEPS_DIR)')
|
||||
$generated.Add(' message(FATAL_ERROR "native_prebuilt.cmake must be included by runtime/cmake/NativePrebuilt.cmake")')
|
||||
$generated.Add('endif()')
|
||||
$generated.Add('')
|
||||
$generated.Add((Format-CMakeBlock 'MKW_NP_INCLUDE_DIRECTORIES' $packageIncludeDirectories).TrimEnd())
|
||||
$generated.Add((Format-CMakeBlock 'MKW_NP_COMPILE_DEFINITIONS' $packageDefinitions).TrimEnd())
|
||||
$generated.Add((Format-CMakeBlock 'MKW_NP_COMPILE_OPTIONS' $packageCompileOptions).TrimEnd())
|
||||
$generated.Add((Format-CMakeBlock 'MKW_NP_LINK_LIBRARIES' $packageLinkItems).TrimEnd())
|
||||
$generated.Add((Format-CMakeBlock 'MKW_NP_AURORA_TARGETS' @('aurora::gx','aurora::pad','aurora::si','aurora::vi','aurora::mtx')).TrimEnd())
|
||||
$generated.Add('')
|
||||
$generated.Add("set(MKW_NP_DAWN_CONFIG_DIR `"$(ConvertTo-Token $meta['dawn_config_dir'] 'dawn')`")")
|
||||
$generated.Add("set(MKW_NP_DAWN_VERSION `"$($meta['aurora_dawn_version'])`")")
|
||||
$generated.Add('')
|
||||
$generated.Add('# Shared third-party libraries keep imported SHARED targets so that')
|
||||
$generated.Add('# $<TARGET_RUNTIME_DLLS> still copies them next to the game executable.')
|
||||
foreach ($import in $sharedImports) {
|
||||
$generated.Add("add_library($($import.Target) SHARED IMPORTED GLOBAL)")
|
||||
$generated.Add("set_target_properties($($import.Target) PROPERTIES")
|
||||
$generated.Add(" IMPORTED_LOCATION `"`${MKW_NP_PACKAGE_DIR}/$($import.Runtime)`"")
|
||||
$generated.Add(" IMPORTED_IMPLIB `"`${MKW_NP_PACKAGE_DIR}/$($import.Implib)`")")
|
||||
}
|
||||
$generatedText = ($generated -join "`r`n") + "`r`n"
|
||||
$generatedPath = Join-Path $package 'native_prebuilt.cmake'
|
||||
[IO.File]::WriteAllText($generatedPath, $generatedText, (New-Object Text.UTF8Encoding($false)))
|
||||
|
||||
# --- Provenance ---
|
||||
# The pinned Dawn tree is what this harvest compiled against; without its digest the
|
||||
# consumer-side ABI check would silently pass on a mismatched Dawn.
|
||||
$dawnRuntimeHash = Get-MkwDawnRuntimeSha256 $dependencies
|
||||
if (-not $dawnRuntimeHash) {
|
||||
throw "The pinned Dawn runtime is missing: $(Join-Path $dependencies 'dawn_prebuilt\bin\webgpu_dawn.dll')"
|
||||
}
|
||||
|
||||
$auroraSourceFingerprint = Get-MkwAuroraSourceFingerprint $auroraSource
|
||||
if (-not $auroraSourceFingerprint) {
|
||||
throw "The aurora source tree could not be fingerprinted: $auroraSource"
|
||||
}
|
||||
|
||||
# The harvested Crypto++ archive is consumed against this tree's headers, so it
|
||||
# is recorded for the same reason as the aurora fingerprint above.
|
||||
$thirdPartyDirectory = Join-Path $runtimeSource 'third_party'
|
||||
$thirdPartySourceFingerprint = Get-MkwThirdPartySourceFingerprint $thirdPartyDirectory
|
||||
if (-not $thirdPartySourceFingerprint) {
|
||||
throw "The vendored third-party tree could not be fingerprinted: $thirdPartyDirectory"
|
||||
}
|
||||
|
||||
$contents = @(Get-ChildItem -LiteralPath $package -Recurse -File | ForEach-Object {
|
||||
[pscustomobject][ordered]@{
|
||||
Path = $_.FullName.Substring($package.Length + 1).Replace('\', '/')
|
||||
Bytes = $_.Length
|
||||
Sha256 = Get-MkwFileSha256 $_.FullName
|
||||
}
|
||||
} | Sort-Object Path)
|
||||
|
||||
$provenance = [ordered]@{
|
||||
SchemaVersion = 1
|
||||
BuiltUtc = [DateTime]::UtcNow.ToString('O')
|
||||
# A precompiled archive is only interchangeable with locally compiled objects
|
||||
# when the compiler and the flag set are bit-for-bit the ones the user's
|
||||
# machine will use. LocalBuild.ps1 re-checks both before consuming this.
|
||||
CompilerSha256 = Get-MkwFileSha256 $clangBinary
|
||||
CompilerVersion = $meta['cxx_compiler_version']
|
||||
FlagFingerprint = Get-MkwNativeFlagFingerprint
|
||||
DawnVersion = $meta['aurora_dawn_version']
|
||||
# The version tag is not a content identity: the upstream prebuilt asset is
|
||||
# mutable, so record the digest of the Dawn runtime these archives were
|
||||
# actually compiled and linked against. Consumers re-check it.
|
||||
DawnRuntimeSha256 = $dawnRuntimeHash
|
||||
# libaurora_*.a is compiled from this tree, while the runtime that calls into
|
||||
# it is compiled on the user's machine. Record what was harvested so both the
|
||||
# release build and the local build can refuse a package aurora has outgrown.
|
||||
AuroraSourceFingerprint = $auroraSourceFingerprint
|
||||
ThirdPartySourceFingerprint = $thirdPartySourceFingerprint
|
||||
Sdl3Target = $meta['aurora_sdl3_target']
|
||||
HarvestedLibraryCount = @($linkerFileToReference.Keys).Count
|
||||
Contents = $contents
|
||||
}
|
||||
$provenance | ConvertTo-Json -Depth 6 |
|
||||
Set-Content -LiteralPath (Join-Path $package 'provenance.json') -Encoding UTF8
|
||||
|
||||
if (-not $KeepStage) { Remove-Item -LiteralPath $stage -Recurse -Force }
|
||||
|
||||
$totalBytes = ($contents | Measure-Object -Property Bytes -Sum).Sum
|
||||
Write-Host ''
|
||||
Write-Host "Native prebuilt package: $package"
|
||||
Write-Host (" files: {0}; size: {1:n1} MiB" -f $contents.Count, ($totalBytes / 1MB))
|
||||
Write-Host (" archives: {0}; shared imports: {1}" -f (@(Get-ChildItem (Join-Path $package 'lib') -File).Count), $sharedImports.Count)
|
||||
Write-Host (" flag fingerprint: {0}" -f $provenance.FlagFingerprint)
|
||||
@@ -0,0 +1,74 @@
|
||||
[CmdletBinding()]
|
||||
param([string]$Destination)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 3.0
|
||||
|
||||
$packages = @(
|
||||
[pscustomobject]@{
|
||||
Name = 'llvm-mingw'; File = 'llvm-mingw-20260616-ucrt-x86_64.zip'
|
||||
Uri = 'https://github.com/mstorsjo/llvm-mingw/releases/download/20260616/llvm-mingw-20260616-ucrt-x86_64.zip'
|
||||
Sha256 = 'b9b68a4d276e16fa25802aaba458e4638f64b3884c290aaccdc2d87083b6ca35'
|
||||
Root = 'llvm-mingw-20260616-ucrt-x86_64'
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'CMake'; File = 'cmake-4.3.3-windows-x86_64.zip'
|
||||
Uri = 'https://github.com/Kitware/CMake/releases/download/v4.3.3/cmake-4.3.3-windows-x86_64.zip'
|
||||
Sha256 = '935ade9e5e8723583c07f44c5592cea2a1c8f65c56ca7e07b34c025c880e0bd6'
|
||||
Root = 'cmake-4.3.3-windows-x86_64'
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Name = 'Ninja'; File = 'ninja-win-1.13.2.zip'
|
||||
Uri = 'https://github.com/ninja-build/ninja/releases/download/v1.13.2/ninja-win.zip'
|
||||
Sha256 = '07fc8261b42b20e71d1720b39068c2e14ffcee6396b76fb7a795fb460b78dc65'
|
||||
Root = $null
|
||||
}
|
||||
)
|
||||
|
||||
if (-not $Destination) { $Destination = Join-Path $PSScriptRoot 'artifacts\portable-tools' }
|
||||
$Destination = [IO.Path]::GetFullPath($Destination)
|
||||
$downloads = Join-Path $PSScriptRoot 'artifacts\downloads'
|
||||
[IO.Directory]::CreateDirectory($Destination) | Out-Null
|
||||
[IO.Directory]::CreateDirectory($downloads) | Out-Null
|
||||
|
||||
foreach ($package in $packages) {
|
||||
$archive = Join-Path $downloads $package.File
|
||||
if (-not (Test-Path -LiteralPath $archive -PathType Leaf) -or
|
||||
(Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant() -ne $package.Sha256) {
|
||||
Write-Host "Downloading $($package.Name)..."
|
||||
$temporary = $archive + '.partial'
|
||||
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
|
||||
Invoke-WebRequest -UseBasicParsing -Uri $package.Uri -OutFile $temporary
|
||||
$actual = (Get-FileHash -LiteralPath $temporary -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if ($actual -ne $package.Sha256) { throw "$($package.Name) hash mismatch: $actual" }
|
||||
Move-Item -LiteralPath $temporary -Destination $archive -Force
|
||||
}
|
||||
|
||||
$target = Join-Path $Destination $package.Name
|
||||
if (Test-Path -LiteralPath $target -PathType Container) { continue }
|
||||
$extract = Join-Path $Destination ('.extract-' + $package.Name)
|
||||
if (Test-Path -LiteralPath $extract) { Remove-Item -LiteralPath $extract -Recurse -Force }
|
||||
Expand-Archive -LiteralPath $archive -DestinationPath $extract
|
||||
$source = if ($package.Root) { Join-Path $extract $package.Root } else { $extract }
|
||||
if (-not (Test-Path -LiteralPath $source -PathType Container)) { throw "$($package.Name) archive layout changed." }
|
||||
Move-Item -LiteralPath $source -Destination $target
|
||||
if (Test-Path -LiteralPath $extract) { Remove-Item -LiteralPath $extract -Recurse -Force }
|
||||
}
|
||||
|
||||
$license = @"
|
||||
Portable build tools bundled by WiiCompiled
|
||||
|
||||
LLVM-MinGW 20260616 (LLVM/Clang/LLD, mingw-w64, libc++)
|
||||
https://github.com/mstorsjo/llvm-mingw
|
||||
Licenses are included in the llvm-mingw directory.
|
||||
|
||||
CMake 4.3.3
|
||||
https://cmake.org/
|
||||
License is included in the CMake directory.
|
||||
|
||||
Ninja 1.13.2
|
||||
https://github.com/ninja-build/ninja
|
||||
Apache License 2.0; see the installer license inventory.
|
||||
"@
|
||||
$license | Set-Content -LiteralPath (Join-Path $Destination 'README-LICENSES.txt') -Encoding UTF8
|
||||
Write-Host "Portable tools ready: $Destination"
|
||||
@@ -0,0 +1,30 @@
|
||||
[CmdletBinding(PositionalBinding = $false)]
|
||||
param(
|
||||
[string]$DolphinToolPath,
|
||||
[string]$PortableToolsDirectory = 'Launcher/artifacts/portable-tools',
|
||||
[string]$DependencySourceDirectory = 'Launcher/artifacts/dependencies',
|
||||
[string]$VcRuntimeDirectory,
|
||||
[string]$ToolkitReleaseTag = $env:GITHUB_REF_NAME
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 3.0
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($DolphinToolPath)) {
|
||||
throw 'Prepare-Release.ps1 requires -DolphinToolPath pointing to DolphinTool.exe.'
|
||||
}
|
||||
|
||||
$arguments = @{
|
||||
DolphinToolPath = $DolphinToolPath
|
||||
PortableToolsDirectory = $PortableToolsDirectory
|
||||
DependencySourceDirectory = $DependencySourceDirectory
|
||||
ToolkitReleaseTag = $ToolkitReleaseTag
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($VcRuntimeDirectory)) {
|
||||
$arguments.VcRuntimeDirectory = $VcRuntimeDirectory
|
||||
}
|
||||
|
||||
& (Join-Path $PSScriptRoot 'Build-Installer.ps1') @arguments
|
||||
if ($LASTEXITCODE -ne 0) { throw "Build-Installer.ps1 failed with exit code $LASTEXITCODE." }
|
||||
|
||||
Write-Host 'The release artifact is ready. It contains no translated game executable, game image, Code.pul, or Retro-WFC payload.'
|
||||
@@ -0,0 +1,69 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PayloadRoot,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SetupHost,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$LlvmReadobjPath
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 3.0
|
||||
|
||||
if (-not (Test-Path -LiteralPath $LlvmReadobjPath -PathType Leaf)) {
|
||||
throw "Bundled llvm-readobj.exe is missing: $LlvmReadobjPath"
|
||||
}
|
||||
$llvmReadobj = [System.IO.Path]::GetFullPath($LlvmReadobjPath)
|
||||
|
||||
$systemDlls = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||
@(
|
||||
'advapi32.dll', 'authz.dll', 'bcrypt.dll', 'combase.dll', 'comdlg32.dll', 'crypt32.dll',
|
||||
'd3d11.dll', 'd3d12.dll', 'dbghelp.dll', 'dcomp.dll', 'dwrite.dll', 'dwmapi.dll',
|
||||
'dxgi.dll', 'gdi32.dll', 'imm32.dll',
|
||||
'iphlpapi.dll', 'kernel32.dll', 'mf.dll', 'mfplat.dll', 'mfreadwrite.dll', 'mfuuid.dll',
|
||||
'ntdll.dll', 'ole32.dll', 'oleaut32.dll', 'opengl32.dll', 'powrprof.dll', 'rpcrt4.dll',
|
||||
'secur32.dll', 'setupapi.dll', 'shell32.dll', 'shlwapi.dll', 'user32.dll', 'userenv.dll',
|
||||
'version.dll', 'winhttp.dll', 'winmm.dll', 'wtsapi32.dll', 'ws2_32.dll', 'msvcrt.dll',
|
||||
'netapi32.dll', 'normaliz.dll', 'psapi.dll', 'ucrtbase.dll', 'uxtheme.dll'
|
||||
) | ForEach-Object { [void]$systemDlls.Add($_) }
|
||||
|
||||
$redistributableDlls = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||
$redistDirectory = Join-Path ([System.IO.Path]::GetFullPath($PayloadRoot)) 'Toolkit\Redist'
|
||||
if (Test-Path -LiteralPath $redistDirectory -PathType Container) {
|
||||
Get-ChildItem -LiteralPath $redistDirectory -Filter '*.dll' -File |
|
||||
ForEach-Object { [void]$redistributableDlls.Add($_.Name) }
|
||||
}
|
||||
|
||||
$binaries = @(
|
||||
Get-ChildItem -LiteralPath ([System.IO.Path]::GetFullPath($PayloadRoot)) -File -Recurse |
|
||||
Where-Object { $_.Extension -in @('.exe', '.dll') }
|
||||
)
|
||||
$binaries += Get-Item -LiteralPath ([System.IO.Path]::GetFullPath($SetupHost))
|
||||
$missing = [Collections.Generic.List[string]]::new()
|
||||
$checkedImports = 0
|
||||
|
||||
foreach ($binary in $binaries) {
|
||||
$output = & $llvmReadobj --coff-imports $binary.FullName 2>&1
|
||||
if ($LASTEXITCODE -ne 0) { throw "llvm-readobj failed for $($binary.FullName).`n$output" }
|
||||
$dependencies = @($output | ForEach-Object {
|
||||
if ($_ -match '^\s*Name:\s+([A-Za-z0-9._-]+\.dll)\s*$') { $Matches[1] }
|
||||
} | Sort-Object -Unique)
|
||||
foreach ($dependency in $dependencies) {
|
||||
$checkedImports++
|
||||
if ($dependency.StartsWith('api-ms-', [StringComparison]::OrdinalIgnoreCase) -or
|
||||
$dependency.StartsWith('ext-ms-', [StringComparison]::OrdinalIgnoreCase) -or
|
||||
$systemDlls.Contains($dependency) -or
|
||||
$redistributableDlls.Contains($dependency) -or
|
||||
(Test-Path -LiteralPath (Join-Path $binary.DirectoryName $dependency) -PathType Leaf)) {
|
||||
continue
|
||||
}
|
||||
$missing.Add("$($binary.FullName) -> $dependency")
|
||||
}
|
||||
}
|
||||
|
||||
if ($missing.Count -gt 0) {
|
||||
throw "Native dependency audit found imports that are neither Windows system DLLs nor app-local:`n$($missing -join "`n")"
|
||||
}
|
||||
|
||||
Write-Host "Native dependency audit passed: $($binaries.Count) binaries, $checkedImports imports."
|
||||
@@ -0,0 +1,43 @@
|
||||
[CmdletBinding()]
|
||||
param([Parameter(Mandatory)] [string]$PayloadRoot)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$PayloadRoot = [IO.Path]::GetFullPath($PayloadRoot)
|
||||
if (-not (Test-Path -LiteralPath $PayloadRoot -PathType Container)) { throw "Payload does not exist: $PayloadRoot" }
|
||||
|
||||
$forbiddenNames = @(
|
||||
'WiiCompiled.exe', 'RetroRewind.exe', 'mkw_recompiled.exe',
|
||||
'main.dol', 'StaticR.rel', 'Code.pul', 'base_translation_sources.bin',
|
||||
'data_sections_init.cpp', 'data_sections_init_blobs.S', 'mkwii_base_manifest.json',
|
||||
'generated_func_aliases.cpp', 'translated_function_decls.h'
|
||||
)
|
||||
$forbiddenExtensions = @('.iso', '.gcm', '.gcz', '.ciso', '.wbfs', '.wia', '.rvz', '.dol', '.rel', '.pul')
|
||||
$forbiddenSegments = @(
|
||||
'\BuildWorkspace\generated\', '\BuildWorkspace\runtime\build\', '\GameAssets\', '\BuildWorkspace\Assets\',
|
||||
'\BuildWorkspace\PulsarPacks\', '\BuildWorkspace\build\base\', '\BuildWorkspace\build\mods\'
|
||||
)
|
||||
$violations = [Collections.Generic.List[string]]::new()
|
||||
foreach ($file in Get-ChildItem -LiteralPath $PayloadRoot -Recurse -File) {
|
||||
$rootPrefix = $PayloadRoot.TrimEnd('\') + '\'
|
||||
if (-not $file.FullName.StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Payload enumeration escaped its root: $($file.FullName)"
|
||||
}
|
||||
$relative = '\' + $file.FullName.Substring($rootPrefix.Length).Replace('/', '\')
|
||||
$underCompiler = $relative.StartsWith('\Toolkit\llvm-mingw\', [StringComparison]::OrdinalIgnoreCase)
|
||||
$underPinnedDependency = $relative.StartsWith('\BuildWorkspace\Dependencies\', [StringComparison]::OrdinalIgnoreCase)
|
||||
if ($forbiddenNames -contains $file.Name -or $forbiddenExtensions -contains $file.Extension.ToLowerInvariant()) {
|
||||
$violations.Add($relative)
|
||||
continue
|
||||
}
|
||||
if ($forbiddenSegments | Where-Object { $relative.IndexOf($_, [StringComparison]::OrdinalIgnoreCase) -ge 0 }) {
|
||||
$violations.Add($relative)
|
||||
continue
|
||||
}
|
||||
if (-not $underCompiler -and -not $underPinnedDependency -and $file.Extension -in @('.obj','.o','.lib','.a')) {
|
||||
$violations.Add("$relative (precompiled object/library outside the licensed compiler payload)")
|
||||
}
|
||||
}
|
||||
if ($violations.Count -gt 0) {
|
||||
throw "Release payload contains forbidden game-derived or precompiled artifacts:`n$($violations -join "`n")"
|
||||
}
|
||||
Write-Host "Payload boundary audit passed: no translated game executable, game input, generated shard, or generated data was packaged."
|
||||
@@ -0,0 +1,133 @@
|
||||
# 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
|
||||
# both sides of the C#/PowerShell boundary), so those are checked here instead.
|
||||
[CmdletBinding()]
|
||||
param([string]$RepositoryRoot)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 3.0
|
||||
|
||||
. (Join-Path $PSScriptRoot 'NativeBuildFlags.ps1')
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) {
|
||||
$RepositoryRoot = Join-Path $PSScriptRoot '..'
|
||||
}
|
||||
$repoRoot = [IO.Path]::GetFullPath($RepositoryRoot)
|
||||
$launcher = Join-Path $repoRoot 'Launcher'
|
||||
$setup = Join-Path $launcher 'WiiCompiled.Setup'
|
||||
|
||||
$failures = [Collections.Generic.List[string]]::new()
|
||||
function Add-Failure([string]$Message) { $failures.Add($Message) }
|
||||
|
||||
function Read-SourceFile([string]$Path, [string]$Description) {
|
||||
Assert-File $Path $Description
|
||||
return [IO.File]::ReadAllText($Path)
|
||||
}
|
||||
|
||||
function Get-CapturedValue([string]$Text, [string]$Pattern, [string]$Description) {
|
||||
$match = [regex]::Match($Text, $Pattern)
|
||||
if (-not $match.Success) { throw "$Description could not be located; update Test-PinnedFacts.ps1." }
|
||||
return $match.Groups[1].Value
|
||||
}
|
||||
|
||||
function Get-QuotedList([string]$Text, [string]$BlockPattern, [string]$ItemPattern, [string]$Description) {
|
||||
$block = Get-CapturedValue $Text $BlockPattern $Description
|
||||
$items = [regex]::Matches($block, $ItemPattern) | ForEach-Object { $_.Groups[1].Value }
|
||||
if (@($items).Count -eq 0) { throw "$Description is empty; update Test-PinnedFacts.ps1." }
|
||||
return [string[]]@($items)
|
||||
}
|
||||
|
||||
function Compare-Set([string[]]$Expected, [string[]]$Actual, [string]$ExpectedName, [string]$ActualName) {
|
||||
$missing = @($Expected | Where-Object { $Actual -notcontains $_ })
|
||||
$extra = @($Actual | Where-Object { $Expected -notcontains $_ })
|
||||
if ($missing.Count -gt 0) { Add-Failure "$ActualName is missing: $($missing -join ', ') (present in $ExpectedName)." }
|
||||
if ($extra.Count -gt 0) { Add-Failure "$ActualName has entries $ExpectedName does not: $($extra -join ', ')." }
|
||||
}
|
||||
|
||||
$pins = Get-MkwProjectPins (Join-Path $repoRoot 'projects\mkwii\recomp.yml')
|
||||
|
||||
# --- The Retro-WFC endpoint: recomp.yml owns it; the installer host pins the same string so a
|
||||
# --- redirected or rewritten endpoint cannot be fetched from.
|
||||
$inputValidation = Read-SourceFile (Join-Path $setup 'InputValidation.cs') 'InputValidation.cs'
|
||||
$hostUri = Get-CapturedValue $inputValidation 'CurrentRetroWfcPayloadUri\s*=\s*"([^"]+)"' `
|
||||
'The host Retro-WFC endpoint constant'
|
||||
if ($hostUri -cne $pins.RetroWfcPayloadUri) {
|
||||
Add-Failure "InputValidation.CurrentRetroWfcPayloadUri is '$hostUri' 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.
|
||||
$models = Read-SourceFile (Join-Path $setup 'Models.cs') 'Models.cs'
|
||||
$hostGameId = Get-CapturedValue $models 'ExpectedGameId\s*\{\s*get;\s*set;\s*\}\s*=\s*"([^"]+)"' `
|
||||
'The host expected game id'
|
||||
if ($hostGameId -cne $pins.GameId) {
|
||||
Add-Failure "PayloadManifest.ExpectedGameId defaults to '$hostGameId' but recomp.yml pins '$($pins.GameId)'."
|
||||
}
|
||||
|
||||
# --- The translation entry point: the translator is told it by LocalBuild.ps1 (which reads
|
||||
# --- recomp.yml), while the runtime enters the translated image at its own constant.
|
||||
$systemBridge = Read-SourceFile (Join-Path $repoRoot 'runtime\include\system_bridge.h') 'system_bridge.h'
|
||||
$runtimeEntry = (Get-CapturedValue $systemBridge 'kDefaultEntryAddress\s*=\s*(0[xX][0-9a-fA-F]+)' `
|
||||
'The runtime default entry address').ToLowerInvariant()
|
||||
if ($runtimeEntry -ne $pins.EntryPoint) {
|
||||
Add-Failure "system_bridge.h enters at $runtimeEntry but recomp.yml translates from $($pins.EntryPoint)."
|
||||
}
|
||||
|
||||
# --- The pinned dependency set: Build-Installer.ps1 stages it, the installed host requires it.
|
||||
$installedLayout = Read-SourceFile (Join-Path $setup 'InstalledLayout.cs') 'InstalledLayout.cs'
|
||||
$buildInstaller = Read-SourceFile (Join-Path $launcher 'Build-Installer.ps1') 'Build-Installer.ps1'
|
||||
Compare-Set (Get-QuotedList $installedLayout '(?s)DependencyNames\s*=\s*\[(.*?)\]' '"([^"]+)"' `
|
||||
'InstalledLayout.DependencyNames') `
|
||||
(Get-QuotedList $buildInstaller '(?s)\$requiredDependencies\s*=\s*@\((.*?)\)' "'([^']+)'" `
|
||||
'Build-Installer.ps1 $requiredDependencies') `
|
||||
'InstalledLayout.DependencyNames' 'Build-Installer.ps1 $requiredDependencies'
|
||||
|
||||
# --- The runtime assets copied beside a product: the host hashes exactly these names, so the build
|
||||
# --- script must publish every one of them or every product reports its support files as stale.
|
||||
$localBuild = Read-SourceFile (Join-Path $launcher 'LocalBuild.ps1') 'LocalBuild.ps1'
|
||||
$productAssets = @(Get-CapturedValue $installedLayout 'ProductBootstrapDirectoryName\s*=\s*"([^"]+)"' `
|
||||
'The product bootstrap directory name') +
|
||||
@(Get-QuotedList $installedLayout '(?s)ProductFileName\)\[\]\s*Files\s*=\s*\[(.*?)\n\s*\];' `
|
||||
'\]\s*,\s*"([^"]+)"\s*\)' 'ProductRuntimeAssets.Files')
|
||||
foreach ($asset in $productAssets) {
|
||||
if ($localBuild.IndexOf("'$asset'", [StringComparison]::Ordinal) -lt 0) {
|
||||
Add-Failure "LocalBuild.ps1 never publishes the product runtime asset '$asset'."
|
||||
}
|
||||
}
|
||||
|
||||
# --- The command-line contract between the host and the build script.
|
||||
$localBuildService = Read-SourceFile (Join-Path $setup 'LocalBuildService.cs') 'LocalBuildService.cs'
|
||||
$parameterBlock = Get-CapturedValue $localBuild '(?sm)^param\((.*?)\r?\n\)' 'The LocalBuild.ps1 param block'
|
||||
$scriptParameters = [regex]::Matches($parameterBlock, '\$(\w+)') | ForEach-Object { $_.Groups[1].Value }
|
||||
# powershell.exe's own switches travel in the same argument list and are not script parameters.
|
||||
$hostSwitches = @('NoLogo', 'NoProfile', 'NonInteractive', 'ExecutionPolicy', 'File')
|
||||
$passedSwitches = [regex]::Matches($localBuildService, '"-([A-Za-z]\w*)"') |
|
||||
ForEach-Object { $_.Groups[1].Value } | Where-Object { $hostSwitches -notcontains $_ } |
|
||||
Sort-Object -Unique
|
||||
foreach ($switch in $passedSwitches) {
|
||||
if ($scriptParameters -notcontains $switch) {
|
||||
Add-Failure "LocalBuildService.cs passes -$switch, which LocalBuild.ps1 does not declare."
|
||||
}
|
||||
}
|
||||
foreach ($mandatory in ([regex]::Matches($parameterBlock,
|
||||
'\[Parameter\(Mandatory\)\][^$]*\$(\w+)') | ForEach-Object { $_.Groups[1].Value })) {
|
||||
if ($passedSwitches -notcontains $mandatory) {
|
||||
Add-Failure "LocalBuild.ps1 requires -$mandatory, which LocalBuildService.cs never passes."
|
||||
}
|
||||
}
|
||||
|
||||
# --- The build steps the installer's progress bar acts on.
|
||||
$installProgress = Read-SourceFile (Join-Path $setup 'InstallProgress.cs') 'InstallProgress.cs'
|
||||
$knownSteps = Get-QuotedList $installProgress '(?s)class BuildStepIds\s*\{(.*?)\n\}' `
|
||||
'const string \w+ = "([^"]+)";' 'BuildStepIds'
|
||||
$emittedSteps = @([regex]::Matches($localBuild, "(?:-StepId|Write-MkwBuildStep)\s+'([^']+)'") |
|
||||
ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique)
|
||||
Compare-Set $knownSteps $emittedSteps 'BuildStepIds' 'the steps LocalBuild.ps1 emits'
|
||||
|
||||
if ($failures.Count -gt 0) {
|
||||
throw "Pinned facts disagree between their copies:`n $($failures -join "`n ")"
|
||||
}
|
||||
Write-Host ("Pinned-fact audit passed: game {0}, entry {1}, endpoint {2}; dependency, runtime-asset, " -f
|
||||
$pins.GameId, $pins.EntryPoint, $pins.RetroWfcPayloadUri) -NoNewline
|
||||
Write-Host 'build-step, and build-script parameter contracts all agree.'
|
||||
@@ -0,0 +1,75 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
/// <summary>
|
||||
/// Bridges a frontend-owned, named Windows event into the cancellation token used by setup.
|
||||
/// </summary>
|
||||
internal sealed class CancellationSignal : IDisposable
|
||||
{
|
||||
public const string EnvironmentVariable = "MKWCOMPILED_CANCEL_EVENT";
|
||||
|
||||
private readonly EventWaitHandle? _event;
|
||||
private readonly CancellationTokenSource? _source;
|
||||
private readonly RegisteredWaitHandle? _registration;
|
||||
|
||||
private CancellationSignal(EventWaitHandle? @event, CancellationTokenSource? source,
|
||||
RegisteredWaitHandle? registration)
|
||||
{
|
||||
_event = @event;
|
||||
_source = source;
|
||||
_registration = registration;
|
||||
}
|
||||
|
||||
public CancellationToken Token => _source?.Token ?? CancellationToken.None;
|
||||
|
||||
public static CancellationSignal ObserveEnvironment()
|
||||
{
|
||||
var eventName = Environment.GetEnvironmentVariable(EnvironmentVariable);
|
||||
if (eventName is null)
|
||||
return new CancellationSignal(null, null, null);
|
||||
if (string.IsNullOrWhiteSpace(eventName))
|
||||
throw new InvalidOperationException(
|
||||
$"The cancellation event named by {EnvironmentVariable} is empty.");
|
||||
|
||||
EventWaitHandle signal;
|
||||
try
|
||||
{
|
||||
signal = EventWaitHandle.OpenExisting(eventName.Trim());
|
||||
}
|
||||
catch (Exception ex) when (ex is WaitHandleCannotBeOpenedException or UnauthorizedAccessException
|
||||
or IOException or ArgumentException)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The cancellation event named by {EnvironmentVariable} could not be opened.", ex);
|
||||
}
|
||||
|
||||
var source = new CancellationTokenSource();
|
||||
RegisteredWaitHandle registration;
|
||||
try
|
||||
{
|
||||
registration = ThreadPool.RegisterWaitForSingleObject(signal, static (state, _) =>
|
||||
{
|
||||
try { ((CancellationTokenSource)state!).Cancel(); }
|
||||
catch (ObjectDisposedException) { }
|
||||
}, source, Timeout.Infinite, executeOnlyOnce: true);
|
||||
|
||||
// Registering and checking explicitly closes the race where the event was already set
|
||||
// before RegisterWaitForSingleObject installed its wait.
|
||||
if (signal.WaitOne(0)) source.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
source.Dispose();
|
||||
signal.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
return new CancellationSignal(signal, source, registration);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_registration?.Unregister(null);
|
||||
_event?.Dispose();
|
||||
_source?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
internal enum AppMode
|
||||
{
|
||||
Help,
|
||||
SilentInstall,
|
||||
VerifyInputs,
|
||||
Uninstall,
|
||||
SilentUninstall,
|
||||
UninstallWorker,
|
||||
SelfTest,
|
||||
LaunchBase,
|
||||
LaunchRetro,
|
||||
CheckProducts,
|
||||
RepairProducts,
|
||||
Version,
|
||||
EmitPayloadIdentities
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What each mode accepts. Every cross-flag rule below is expressed here once instead of as a
|
||||
/// per-mode exception list, so adding a mode cannot silently inherit another mode's inputs.
|
||||
/// </summary>
|
||||
internal sealed record ModeRules
|
||||
{
|
||||
public string Flag { get; init; } = "this command";
|
||||
public bool AcceptsProgressJson { get; init; }
|
||||
public bool AcceptsGame { get; init; }
|
||||
public bool RequiresGame { get; init; }
|
||||
public bool AcceptsRetroDirectory { get; init; }
|
||||
public bool RequiresRetroDirectory { get; init; }
|
||||
public bool AcceptsPayloadMode { get; init; }
|
||||
public bool RequiresInstallDirectory { get; init; }
|
||||
public bool RequiresPayloadRoot { get; init; }
|
||||
public bool AcceptsPortable { get; init; }
|
||||
}
|
||||
|
||||
internal sealed class CommandLine
|
||||
{
|
||||
public AppMode Mode { get; private set; } = AppMode.Help;
|
||||
public string? GamePath { get; private set; }
|
||||
public string? RetroDirectoryPath { get; private set; }
|
||||
public RetroWfcPayloadMode RetroWfcPayloadMode { get; private set; }
|
||||
public string? InstallDirectory { get; private set; }
|
||||
public bool Quiet { get; private set; }
|
||||
public bool ProgressJson { get; private set; }
|
||||
public string? PayloadRootPath { get; private set; }
|
||||
public bool Portable { get; private set; }
|
||||
|
||||
public static bool WantsProgressJson(string[] args) =>
|
||||
args.Any(argument => argument.Equals("--progress-json", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
public static CommandLine Parse(string[] args)
|
||||
{
|
||||
var result = new CommandLine();
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
switch (args[i].ToLowerInvariant())
|
||||
{
|
||||
case "--version": result.Mode = AppMode.Version; break;
|
||||
case "--silent": result.Mode = AppMode.SilentInstall; break;
|
||||
case "--verify-inputs": result.Mode = AppMode.VerifyInputs; break;
|
||||
case "--uninstall": result.Mode = AppMode.Uninstall; break;
|
||||
case "--silent-uninstall": result.Mode = AppMode.SilentUninstall; result.Quiet = true; break;
|
||||
case "--uninstall-worker": result.Mode = AppMode.UninstallWorker; break;
|
||||
case "--self-test": result.Mode = AppMode.SelfTest; break;
|
||||
case "--launch-base": result.Mode = AppMode.LaunchBase; break;
|
||||
case "--launch-retro": result.Mode = AppMode.LaunchRetro; break;
|
||||
case "--check-products": result.Mode = AppMode.CheckProducts; break;
|
||||
case "--repair-products": result.Mode = AppMode.RepairProducts; break;
|
||||
case "--portable": result.Portable = true; break;
|
||||
case "--emit-payload-identities": result.Mode = AppMode.EmitPayloadIdentities; break;
|
||||
case "--payload-root": result.PayloadRootPath = RequireValue(args, ref i); break;
|
||||
case "--quiet": result.Quiet = true; break;
|
||||
case "--progress-json": result.ProgressJson = true; break;
|
||||
case "--game": result.GamePath = RequireValue(args, ref i); break;
|
||||
case "--retro-dir": result.RetroDirectoryPath = RequireValue(args, ref i); break;
|
||||
case "--download-retro-wfc-payload":
|
||||
if (result.RetroWfcPayloadMode != RetroWfcPayloadMode.NotApplicable)
|
||||
throw new ArgumentException("Choose only one Retro-WFC payload mode.");
|
||||
result.RetroWfcPayloadMode = RetroWfcPayloadMode.Online;
|
||||
break;
|
||||
case "--skip-retro-wfc-payload":
|
||||
if (result.RetroWfcPayloadMode != RetroWfcPayloadMode.NotApplicable)
|
||||
throw new ArgumentException("Choose only one Retro-WFC payload mode.");
|
||||
result.RetroWfcPayloadMode = RetroWfcPayloadMode.Skipped;
|
||||
break;
|
||||
case "--install-dir": result.InstallDirectory = RequireValue(args, ref i); break;
|
||||
default:
|
||||
throw new ArgumentException($"Unknown CLI option: {args[i]}");
|
||||
}
|
||||
}
|
||||
|
||||
result.Validate();
|
||||
return result;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<AppMode, ModeRules> Rules = new()
|
||||
{
|
||||
[AppMode.SilentInstall] = new ModeRules
|
||||
{
|
||||
Flag = "--silent",
|
||||
AcceptsProgressJson = true, AcceptsGame = true, RequiresGame = true,
|
||||
AcceptsRetroDirectory = true, AcceptsPayloadMode = true,
|
||||
AcceptsPortable = true
|
||||
},
|
||||
[AppMode.VerifyInputs] = new ModeRules
|
||||
{
|
||||
Flag = "--verify-inputs",
|
||||
AcceptsProgressJson = true, AcceptsGame = true, RequiresGame = true,
|
||||
AcceptsRetroDirectory = true
|
||||
},
|
||||
[AppMode.CheckProducts] = new ModeRules
|
||||
{
|
||||
Flag = "--check-products", AcceptsProgressJson = true, AcceptsRetroDirectory = true
|
||||
},
|
||||
[AppMode.RepairProducts] = new ModeRules
|
||||
{
|
||||
Flag = "--repair-products",
|
||||
AcceptsProgressJson = true, AcceptsRetroDirectory = true, RequiresRetroDirectory = true,
|
||||
AcceptsPayloadMode = true, RequiresInstallDirectory = true
|
||||
},
|
||||
[AppMode.LaunchBase] = new ModeRules { Flag = "--launch-base" },
|
||||
[AppMode.LaunchRetro] = new ModeRules { Flag = "--launch-retro" },
|
||||
[AppMode.Uninstall] = new ModeRules { Flag = "--uninstall", RequiresInstallDirectory = true },
|
||||
[AppMode.SilentUninstall] = new ModeRules
|
||||
{
|
||||
Flag = "--silent-uninstall", RequiresInstallDirectory = true
|
||||
},
|
||||
[AppMode.UninstallWorker] = new ModeRules
|
||||
{
|
||||
Flag = "--uninstall-worker", RequiresInstallDirectory = true
|
||||
},
|
||||
[AppMode.EmitPayloadIdentities] = new ModeRules
|
||||
{
|
||||
Flag = "--emit-payload-identities", RequiresPayloadRoot = true
|
||||
}
|
||||
};
|
||||
|
||||
private void Validate()
|
||||
{
|
||||
var rules = Rules.GetValueOrDefault(Mode) ?? new ModeRules();
|
||||
|
||||
void Reject(bool present, string option)
|
||||
{
|
||||
if (present) throw new ArgumentException($"{option} is not valid with {rules.Flag}.");
|
||||
}
|
||||
|
||||
Reject(GamePath is not null && !rules.AcceptsGame, "--game");
|
||||
Reject(RetroDirectoryPath is not null && !rules.AcceptsRetroDirectory, "--retro-dir");
|
||||
Reject(ProgressJson && !rules.AcceptsProgressJson, "--progress-json");
|
||||
Reject(PayloadRootPath is not null && !rules.RequiresPayloadRoot, "--payload-root");
|
||||
Reject(Portable && !rules.AcceptsPortable, "--portable");
|
||||
Reject(RetroWfcPayloadMode != RetroWfcPayloadMode.NotApplicable && !rules.AcceptsPayloadMode,
|
||||
"A Retro-WFC payload option");
|
||||
|
||||
if (rules.RequiresGame && string.IsNullOrWhiteSpace(GamePath))
|
||||
throw new ArgumentException("--game is required.");
|
||||
if (rules.RequiresRetroDirectory && string.IsNullOrWhiteSpace(RetroDirectoryPath))
|
||||
throw new ArgumentException(
|
||||
$"--retro-dir is required with {rules.Flag}. Supply Wheel Wizard's canonical Retro Rewind folder.");
|
||||
if (rules.RequiresPayloadRoot && string.IsNullOrWhiteSpace(PayloadRootPath))
|
||||
throw new ArgumentException($"--payload-root is required with {rules.Flag}.");
|
||||
|
||||
// A portable installation has no default location so the caller must state where that root is.
|
||||
if (Portable && string.IsNullOrWhiteSpace(InstallDirectory))
|
||||
throw new ArgumentException(
|
||||
"--install-dir is required with --portable. Its parent folder becomes the portable root.");
|
||||
if (Mode == AppMode.SilentInstall && string.IsNullOrWhiteSpace(InstallDirectory))
|
||||
InstallDirectory = ProductInfo.DefaultInstallDirectory;
|
||||
if (rules.RequiresInstallDirectory && string.IsNullOrWhiteSpace(InstallDirectory))
|
||||
throw new ArgumentException($"--install-dir is required with {rules.Flag}.");
|
||||
|
||||
// The Retro Rewind source and a payload decision are a pair. a base-only operation
|
||||
// has no payload to decide.
|
||||
if (rules.AcceptsPayloadMode && RetroDirectoryPath is not null &&
|
||||
RetroWfcPayloadMode == RetroWfcPayloadMode.NotApplicable)
|
||||
throw new ArgumentException(
|
||||
"Retro Rewind requires --download-retro-wfc-payload or --skip-retro-wfc-payload.");
|
||||
if (RetroDirectoryPath is null && RetroWfcPayloadMode != RetroWfcPayloadMode.NotApplicable)
|
||||
throw new ArgumentException(
|
||||
"--retro-dir is required when selecting a Retro-WFC payload option.");
|
||||
}
|
||||
|
||||
private static string RequireValue(string[] args, ref int index)
|
||||
{
|
||||
if (++index >= args.Length || string.IsNullOrWhiteSpace(args[index]))
|
||||
throw new ArgumentException($"A value is required after {args[index - 1]}.");
|
||||
return args[index];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
internal sealed record RetroRewindCompileInputs(
|
||||
string RetroRewindRoot,
|
||||
string CodePulSha256,
|
||||
string CompileInputsSha256);
|
||||
|
||||
internal static class CompileInputsFingerprint
|
||||
{
|
||||
private const string FormatVersion = "mkwc-retro-compile-inputs-v3";
|
||||
|
||||
/// <summary>The exact directory name a build snapshot uses, because it is the mod root name
|
||||
/// the translator turns into the product's relative DVD overlay root.</summary>
|
||||
public const string PackageDirectoryName = "RetroRewind6";
|
||||
|
||||
public static RetroRewindCompileInputs Compute(string selectedDirectory,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var root = RetroRewindSource.ResolveRetroRewind6(selectedDirectory);
|
||||
return new RetroRewindCompileInputs(root, ComputeCodePulSha256(root, cancellationToken),
|
||||
ComputeCompileInputsSha256(root, cancellationToken));
|
||||
}
|
||||
|
||||
public static string ComputeCodePulSha256(string retroRewindRoot,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var codePul = CodePulPath(Path.GetFullPath(retroRewindRoot));
|
||||
if (!File.Exists(codePul)) throw MissingCodePul(retroRewindRoot);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return InputValidation.Sha256File(codePul);
|
||||
}
|
||||
|
||||
|
||||
/// Hashes only the translator inputs without relying on timestamps.
|
||||
public static string ComputeCompileInputsSha256(string retroRewindRoot,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var root = Path.GetFullPath(retroRewindRoot);
|
||||
if (!Directory.Exists(root))
|
||||
throw new DirectoryNotFoundException($"The Retro Rewind folder is missing: {root}");
|
||||
var codePul = CodePulPath(root);
|
||||
if (!File.Exists(codePul)) throw MissingCodePul(root);
|
||||
|
||||
using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
|
||||
AppendField(hash, FormatVersion);
|
||||
AppendFile(hash, root, codePul, required: true, cancellationToken);
|
||||
|
||||
AppendTopology(hash, "topology:mod-root", root);
|
||||
AppendTopology(hash, "topology:files", Path.Combine(root, "files"));
|
||||
|
||||
return Convert.ToHexString(hash.GetHashAndReset()).ToLowerInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies just the compile inputs of a Wheel Wizard-owned Retro Rewind folder into
|
||||
/// operation-owned scratch and proves the copy is a faithful snapshot. This is the Code.pul
|
||||
/// </summary>
|
||||
public static RetroRewindCompileInputs Snapshot(string selectedDirectory, string scratchDirectory,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var source = Compute(selectedDirectory, cancellationToken);
|
||||
var destination = Path.Combine(Path.GetFullPath(scratchDirectory), PackageDirectoryName);
|
||||
if (Directory.Exists(destination) || File.Exists(destination))
|
||||
throw new IOException($"The compile-input snapshot destination already exists: {destination}");
|
||||
|
||||
CopyCompileInputs(source.RetroRewindRoot, destination, cancellationToken);
|
||||
var snapshot = new RetroRewindCompileInputs(destination,
|
||||
ComputeCodePulSha256(destination, cancellationToken),
|
||||
ComputeCompileInputsSha256(destination, cancellationToken));
|
||||
// A snapshot that hashes exactly like its source is that source; anything else means the
|
||||
// folder was being written while it was read, which is the frontend's lease to hold.
|
||||
if (!snapshot.CodePulSha256.Equals(source.CodePulSha256, StringComparison.OrdinalIgnoreCase) ||
|
||||
!snapshot.CompileInputsSha256.Equals(source.CompileInputsSha256,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new IOException(
|
||||
"The Retro Rewind compile inputs changed while they were being copied. " +
|
||||
"Wait for its update to finish and retry.");
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static void CopyCompileInputs(string root, string destination,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Directory.CreateDirectory(destination);
|
||||
CopyInput(root, destination, CodePulPath(root), required: true, cancellationToken);
|
||||
ReproduceTopology(root, destination, "files", cancellationToken);
|
||||
}
|
||||
|
||||
private static void CopyInput(string root, string destination, string path, bool required,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
if (required) throw MissingCompileInput(path);
|
||||
return;
|
||||
}
|
||||
RejectLink(path);
|
||||
var output = Path.Combine(destination, RelativePath(root, path)
|
||||
.Replace('/', Path.DirectorySeparatorChar));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(output)!);
|
||||
File.Copy(path, output, overwrite: true);
|
||||
}
|
||||
|
||||
|
||||
private static void ReproduceTopology(string root, string destination, string relative,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var source = Path.Combine(root, relative);
|
||||
if (!File.Exists(source) && !Directory.Exists(source)) return;
|
||||
RejectLink(source);
|
||||
var output = Path.Combine(destination, relative);
|
||||
if (Directory.Exists(source)) Directory.CreateDirectory(output);
|
||||
else File.Copy(source, output, overwrite: true);
|
||||
}
|
||||
|
||||
private static void AppendFile(IncrementalHash hash, string root, string path, bool required,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var relative = RelativePath(root, path);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
if (required) throw MissingCompileInput(path);
|
||||
AppendField(hash, "file:" + relative);
|
||||
AppendField(hash, "absent");
|
||||
return;
|
||||
}
|
||||
|
||||
RejectLink(path);
|
||||
AppendField(hash, "file:" + relative);
|
||||
AppendField(hash, InputValidation.Sha256File(path));
|
||||
}
|
||||
|
||||
private static void AppendTopology(IncrementalHash hash, string name, string path)
|
||||
{
|
||||
AppendField(hash, name);
|
||||
if (!File.Exists(path) && !Directory.Exists(path))
|
||||
{
|
||||
AppendField(hash, "absent");
|
||||
return;
|
||||
}
|
||||
|
||||
var attributes = File.GetAttributes(path);
|
||||
var kind = Directory.Exists(path) ? "directory" : "file";
|
||||
if ((attributes & FileAttributes.ReparsePoint) != 0) kind += "+link";
|
||||
AppendField(hash, kind);
|
||||
}
|
||||
|
||||
private static void AppendField(IncrementalHash hash, string value)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(value);
|
||||
Span<byte> length = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(length, bytes.Length);
|
||||
hash.AppendData(length);
|
||||
hash.AppendData(bytes);
|
||||
}
|
||||
|
||||
private static string RelativePath(string root, string path) =>
|
||||
Path.GetRelativePath(root, path).Replace('\\', '/');
|
||||
|
||||
private static string CodePulPath(string root) => Path.Combine(root, "Binaries", "Code.pul");
|
||||
|
||||
private static void RejectLink(string path)
|
||||
{
|
||||
if ((File.GetAttributes(path) & FileAttributes.ReparsePoint) != 0)
|
||||
throw new InvalidDataException(
|
||||
$"The Retro Rewind compile inputs contain a link instead of a regular entry: {path}");
|
||||
}
|
||||
|
||||
private static InvalidDataException MissingCompileInput(string path) =>
|
||||
new($"The Retro Rewind compile input is missing: {path}");
|
||||
|
||||
private static InvalidDataException MissingCodePul(string retroRewindRoot) =>
|
||||
new($"The Retro Rewind folder is missing Binaries\\Code.pul: {retroRewindRoot}");
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
internal static class ConsoleCommands
|
||||
{
|
||||
public static int Help()
|
||||
{
|
||||
Console.Out.WriteLine($"{ProductInfo.Name} command-line setup {ProductInfo.Version}");
|
||||
Console.Out.WriteLine("Wheel Wizard is the graphical interface for installing and launching WiiCompiled.");
|
||||
Console.Out.WriteLine();
|
||||
Console.Out.WriteLine("Commands:");
|
||||
Console.Out.WriteLine(" --silent --game <image> --install-dir <dir> [--retro-dir <folder>] [--portable]");
|
||||
Console.Out.WriteLine(" --verify-inputs --game <image> [--retro-dir <folder>]");
|
||||
Console.Out.WriteLine(" --check-products [--install-dir <dir>] [--retro-dir <folder>] [--progress-json]");
|
||||
Console.Out.WriteLine(" --repair-products --install-dir <dir> --retro-dir <folder> " +
|
||||
"(--download-retro-wfc-payload | --skip-retro-wfc-payload) [--progress-json]");
|
||||
Console.Out.WriteLine(" --launch-retro | --launch-base");
|
||||
Console.Out.WriteLine(" --uninstall --install-dir <dir>");
|
||||
Console.Out.WriteLine(" --version");
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int Version()
|
||||
{
|
||||
Console.Out.WriteLine(ProductInfo.Version);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
public static int EmitPayloadIdentities(CommandLine command)
|
||||
{
|
||||
var root = Path.GetFullPath(command.PayloadRootPath!);
|
||||
if (!Directory.Exists(InstalledLayout.Toolkit(root)) ||
|
||||
!Directory.Exists(InstalledLayout.Workspace(root)))
|
||||
{
|
||||
throw new InvalidDataException($"{root} is not a staged payload root: expected " +
|
||||
$"{InstalledLayout.ToolkitDirectoryName} and {InstalledLayout.WorkspaceDirectoryName} " +
|
||||
"directories.");
|
||||
}
|
||||
|
||||
var components = ToolkitFingerprint.ComputeComponents(root);
|
||||
var identities = new PayloadIdentities(
|
||||
components.Compile,
|
||||
components.Translation,
|
||||
components.NativeToolchain,
|
||||
ToolkitFingerprint.ComputePackage(root),
|
||||
ToolkitFingerprint.ComputeRuntimeAssets(root));
|
||||
Console.Out.WriteLine(JsonSerializer.Serialize(identities));
|
||||
return 0;
|
||||
}
|
||||
|
||||
private sealed record PayloadIdentities(string ToolkitFingerprint, string TranslationFingerprint,
|
||||
string NativeToolchainFingerprint, string ToolkitPackageFingerprint,
|
||||
string RuntimeAssetsFingerprint);
|
||||
|
||||
public static int VerifyInputs(CommandLine command)
|
||||
{
|
||||
var reporter = command.ProgressJson ? new NdjsonInstallReporter() : null;
|
||||
var temp = Path.Combine(Path.GetTempPath(), "mkwc-verify-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(temp);
|
||||
try
|
||||
{
|
||||
using var payload = PayloadArchive.OpenCurrent();
|
||||
var manifest = payload.ReadManifest();
|
||||
var tool = Path.Combine(temp, "DolphinTool.exe");
|
||||
payload.ExtractEntry(InstalledLayout.ToolkitEntryPrefix + "DolphinTool.exe", tool);
|
||||
payload.ExtractDirectory(InstalledLayout.ToolkitEntryPrefix + "Redist", temp);
|
||||
reporter?.Progress(InstallStages.Validate, "Checking the Wii disc image...", 10);
|
||||
var header = InputValidation.ReadDiscHeaderAsync(tool, command.GamePath!).GetAwaiter().GetResult();
|
||||
InputValidation.EnsureCompatibleDisc(header, manifest);
|
||||
|
||||
if (command.RetroDirectoryPath is not null)
|
||||
{
|
||||
reporter?.Progress(InstallStages.Validate, "Checking the Retro Rewind folder...", 80);
|
||||
_ = CompileInputsFingerprint.Compute(command.RetroDirectoryPath);
|
||||
}
|
||||
reporter?.Success(command.InstallDirectory is null
|
||||
? ProductInfo.DefaultInstallDirectory
|
||||
: Path.GetFullPath(command.InstallDirectory));
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex) when (reporter is not null)
|
||||
{
|
||||
reporter.Failure(ex.Message);
|
||||
Console.Error.WriteLine(ex);
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
reporter?.EnsureFinished("Input verification stopped before it reached a result.");
|
||||
if (Directory.Exists(temp)) Directory.Delete(temp, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<int> Install(CommandLine command, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var logPath = Path.Combine(Path.GetTempPath(), "WiiCompiled-setup.log");
|
||||
var logLock = new object();
|
||||
void Log(string message)
|
||||
{
|
||||
lock (logLock)
|
||||
{
|
||||
File.AppendAllText(logPath, $"[{DateTime.Now:O}] {message}{Environment.NewLine}");
|
||||
}
|
||||
}
|
||||
File.WriteAllText(logPath, $"{ProductInfo.Name} setup {ProductInfo.Version}{Environment.NewLine}");
|
||||
|
||||
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(Log) : null;
|
||||
IInstallReporter reporter = ndjson ?? (IInstallReporter)new ConsoleInstallReporter(Log);
|
||||
var installDirectory = Path.GetFullPath(command.InstallDirectory!);
|
||||
try
|
||||
{
|
||||
var engine = new InstallerEngine(reporter);
|
||||
await engine.InstallAsync(new InstallOptions
|
||||
{
|
||||
GamePath = command.GamePath!,
|
||||
RetroDirectoryPath = command.RetroDirectoryPath,
|
||||
RetroWfcPayloadMode = command.RetroWfcPayloadMode,
|
||||
InstallDirectory = installDirectory,
|
||||
Portable = command.Portable
|
||||
}, cancellationToken);
|
||||
ndjson?.Success(installDirectory);
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex) when (ndjson is not null)
|
||||
{
|
||||
// The caller reads stdout as a protocol, so the failure has to arrive as the terminal
|
||||
// result line; the exception detail belongs on stderr and in the setup log.
|
||||
ndjson.Failure(ex.Message);
|
||||
Console.Error.WriteLine(ex);
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Nothing - not a cancellation, not a process-level abort path - may leave the caller
|
||||
// without the one line it waits for.
|
||||
ndjson?.EnsureFinished("The installer stopped before it reached a result.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reports whether the installed products are current, without building, mutating, networking,
|
||||
/// or walking the Retro Rewind asset tree. Exit code 2 means work is required, not a protocol failure.</summary>
|
||||
public static int CheckProducts(CommandLine command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var reporter = command.ProgressJson ? new NdjsonInstallReporter() : null;
|
||||
var installation = new Installation(string.IsNullOrWhiteSpace(command.InstallDirectory)
|
||||
? AppContext.BaseDirectory
|
||||
: Path.GetFullPath(command.InstallDirectory));
|
||||
try
|
||||
{
|
||||
// The lock also performs exact journal/scratch recovery. Acquire it for absent and
|
||||
// partial roots too: a killed first install can have recoverable state even when the
|
||||
// product executable/state file has not reached the live directory yet.
|
||||
using var operationLock = InstallOperationLock.Acquire(installation.Root, reporter);
|
||||
PortableInstallHealing.HealMovedInstall(installation, reporter);
|
||||
var report = InspectProducts(installation, command.RetroDirectoryPath,
|
||||
cancellationToken: cancellationToken);
|
||||
Console.Out.WriteLine(command.ProgressJson
|
||||
? SerializeProductsReport(report)
|
||||
: $"base: {report.Base.Reason} {report.Base.Detail}".TrimEnd());
|
||||
if (!command.ProgressJson)
|
||||
Console.Out.WriteLine(
|
||||
$"retro-rewind: {report.RetroRewind.Reason} {report.RetroRewind.Detail}".TrimEnd());
|
||||
Console.Out.Flush();
|
||||
reporter?.Success(installation.Root);
|
||||
return report.RebuildRequired ? 2 : 0;
|
||||
}
|
||||
catch (Exception ex) when (reporter is not null)
|
||||
{
|
||||
reporter.Failure(ex.Message);
|
||||
Console.Error.WriteLine(ex);
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
reporter?.EnsureFinished("The product check stopped before it reached a result.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reconciles only the product work identified by a preceding health check. Every invocation
|
||||
/// carries the canonical Retro Rewind folder and one payload option; the final inspection is
|
||||
/// authoritative, so success is emitted only when every required product is current.
|
||||
/// </summary>
|
||||
public static async Task<int> RepairProducts(CommandLine command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var logPath = Path.Combine(Path.GetTempPath(), "WiiCompiled-repair.log");
|
||||
var logLock = new object();
|
||||
void Log(string message)
|
||||
{
|
||||
lock (logLock)
|
||||
{
|
||||
File.AppendAllText(logPath, $"[{DateTime.Now:O}] {message}{Environment.NewLine}");
|
||||
}
|
||||
}
|
||||
|
||||
var ndjson = command.ProgressJson ? new NdjsonInstallReporter(Log) : null;
|
||||
IInstallReporter reporter = ndjson is null ? new ConsoleInstallReporter(Log) : ndjson;
|
||||
var installDirectory = Path.GetFullPath(command.InstallDirectory!);
|
||||
var installation = new Installation(installDirectory);
|
||||
try
|
||||
{
|
||||
if (!installation.IsPresent)
|
||||
throw new InvalidOperationException(
|
||||
$"WiiCompiled is not installed at {installDirectory}.");
|
||||
|
||||
using var operationLock = InstallOperationLock.Acquire(installDirectory, reporter);
|
||||
PortableInstallHealing.HealMovedInstall(installation, reporter);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var service = new ProductRepairService(installation, reporter);
|
||||
using var scratch = InstallScratchSpace.CreateInsideInstall(installDirectory, reporter);
|
||||
// Only compile inputs are copied out of the canonical installation, under the frontend's
|
||||
// source lease. Its assets stay where they are and are read live at launch.
|
||||
var canonicalRoot = RetroRewindSource.ResolveRetroRewind6(command.RetroDirectoryPath!);
|
||||
var snapshot = CompileInputsFingerprint.Snapshot(canonicalRoot,
|
||||
Path.Combine(scratch.Root, "compile-inputs"), cancellationToken);
|
||||
var options = new ProductRepairService.ReconcileOptions
|
||||
{
|
||||
ScratchRoot = scratch.Root,
|
||||
CanonicalRetroRewindRoot = canonicalRoot
|
||||
};
|
||||
var reconciliation = await service.RepairRetroAsync(snapshot, command.RetroWfcPayloadMode,
|
||||
options, cancellationToken);
|
||||
// ReconcileAsync is the completion barrier. Cancellation is observed throughout
|
||||
// preparation and immediately before publication; a request racing with or following
|
||||
// its uncancellable Publish/Commit must not turn committed state into a failure result.
|
||||
// Reconciliation returns the exact payload-cache observation it proved or published, so
|
||||
// terminal health does not hash the same payload a second time.
|
||||
var after = InspectProducts(installation, snapshot,
|
||||
reconciliation.CachedRetroWfcPayloadMatches);
|
||||
if (after.RebuildRequired)
|
||||
throw new InvalidOperationException(
|
||||
"Product repair completed without producing a current installation: " +
|
||||
after.ActionRequiredDetail);
|
||||
|
||||
ndjson?.Success(installDirectory);
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex) when (ndjson is not null)
|
||||
{
|
||||
ndjson.Failure(ex is OperationCanceledException ? "Operation canceled." : ex.Message);
|
||||
Console.Error.WriteLine(ex);
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ndjson?.EnsureFinished("Product repair stopped before it reached a result.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <c>products</c> record exactly as the v1 contract defines it: a typed status and a
|
||||
/// human-readable detail per product, plus one aggregate. Nothing else is derived, because
|
||||
/// nothing else is reported.
|
||||
/// </summary>
|
||||
internal sealed record ProductsReport(string InstallDirectory,
|
||||
ProductState Base, ProductState RetroRewind)
|
||||
{
|
||||
public bool RebuildRequired => Base.ActionRequired || RetroRewind.ActionRequired;
|
||||
|
||||
public string ActionRequiredDetail => string.Join(" ",
|
||||
new[] { Base, RetroRewind }
|
||||
.Where(state => state.ActionRequired)
|
||||
.Select(state => state.Detail)
|
||||
.Where(detail => !string.IsNullOrWhiteSpace(detail)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inspects both products against the canonical Retro Rewind installation: the caller's explicit
|
||||
/// folder when it supplied one, otherwise the recorded <c>retro_rewind_root</c>.
|
||||
/// </summary>
|
||||
internal static ProductsReport InspectProducts(Installation installation,
|
||||
string? canonicalRetroDirectory = null, bool? cachedRetroWfcPayloadMatches = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!installation.IsPresent) return AbsentReport(installation);
|
||||
var canonical = installation.ResolveCanonicalCompileInputs(canonicalRetroDirectory,
|
||||
out var canonicalError, cancellationToken);
|
||||
return BuildReport(installation, canonical, canonicalError, cachedRetroWfcPayloadMatches);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the report from a compile-input identity the operation already observed under its
|
||||
/// lock, so a repair does not hash the same canonical inputs at every decision boundary.
|
||||
/// </summary>
|
||||
internal static ProductsReport InspectProducts(Installation installation,
|
||||
RetroRewindCompileInputs canonical, bool? cachedRetroWfcPayloadMatches = null) =>
|
||||
installation.IsPresent
|
||||
? BuildReport(installation, canonical, null, cachedRetroWfcPayloadMatches)
|
||||
: AbsentReport(installation);
|
||||
|
||||
private static ProductsReport AbsentReport(Installation installation) =>
|
||||
// Probing a path nothing was installed to is a valid answer. It must not create a directory
|
||||
// or turn the frontend's first-install probe into a repair request.
|
||||
new(installation.Root,
|
||||
new ProductState(ProductStatus.Absent, "WiiCompiled is not installed here."),
|
||||
new ProductState(ProductStatus.Absent, "Retro Rewind is not installed."));
|
||||
|
||||
private static ProductsReport BuildReport(Installation installation,
|
||||
RetroRewindCompileInputs? canonical, string? canonicalError,
|
||||
bool? cachedRetroWfcPayloadMatches)
|
||||
{
|
||||
var toolkitFingerprint = installation.ResolveToolkitFingerprint();
|
||||
return new ProductsReport(installation.Root,
|
||||
installation.CheckBase(toolkitFingerprint),
|
||||
installation.CheckRetroRewind(toolkitFingerprint, canonical, canonicalError,
|
||||
cachedRetroWfcPayloadMatches));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits the stable <c>products</c> record. The frontend branches on <c>status</c> only, never
|
||||
/// on the detail text, and an unrecognized status fails closed on its side.
|
||||
/// </summary>
|
||||
internal static string SerializeProductsReport(ProductsReport report) =>
|
||||
System.Text.Json.JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "products",
|
||||
setupVersion = ProductInfo.Version,
|
||||
installDir = report.InstallDirectory,
|
||||
rebuildRequired = report.RebuildRequired,
|
||||
@base = new { status = report.Base.Reason, detail = report.Base.Detail },
|
||||
retroRewind = new { status = report.RetroRewind.Reason, detail = report.RetroRewind.Detail }
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
/// <summary>
|
||||
/// One entry of an exact regular directory tree. Directory topology is part of the content
|
||||
/// contract everywhere this walker is used: an empty directory can be a runtime-visible asset just
|
||||
/// as a regular file can be, so it must not disappear from a content identity or a staged copy.
|
||||
/// </summary>
|
||||
internal sealed record RegularTreeEntry(string RelativePath, string FullPath, bool IsDirectory,
|
||||
bool IsEmptyDirectory, long Length);
|
||||
|
||||
internal static class FileSystemUtilities
|
||||
{
|
||||
public static void CopyDirectory(string source, string destination,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Directory.CreateDirectory(destination);
|
||||
foreach (var directory in Directory.EnumerateDirectories(source, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
Directory.CreateDirectory(Path.Combine(destination, Path.GetRelativePath(source, directory)));
|
||||
}
|
||||
foreach (var file in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var output = Path.Combine(destination, Path.GetRelativePath(source, file));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(output)!);
|
||||
File.Copy(file, output, overwrite: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates an exact tree without following links, sorted by forward-slash relative path.
|
||||
/// Every directory appears exactly once and is flagged when it has no children at all.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<RegularTreeEntry> EnumerateRegularTree(string root,
|
||||
CancellationToken cancellationToken = default, string description = "The directory tree")
|
||||
{
|
||||
var rootInfo = new DirectoryInfo(Path.GetFullPath(root));
|
||||
if (!rootInfo.Exists)
|
||||
throw new DirectoryNotFoundException($"{description} is missing: {rootInfo.FullName}");
|
||||
RejectReparsePoint(rootInfo, description);
|
||||
|
||||
var entries = new List<RegularTreeEntry>();
|
||||
var pending = new Stack<DirectoryInfo>();
|
||||
pending.Push(rootInfo);
|
||||
while (pending.Count > 0)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var directory = pending.Pop();
|
||||
var hasChild = false;
|
||||
foreach (var entry in directory.EnumerateFileSystemInfos())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
RejectReparsePoint(entry, description);
|
||||
hasChild = true;
|
||||
switch (entry)
|
||||
{
|
||||
case DirectoryInfo child:
|
||||
pending.Push(child);
|
||||
break;
|
||||
case FileInfo file:
|
||||
entries.Add(new RegularTreeEntry(Relative(rootInfo, file.FullName), file.FullName,
|
||||
false, false, file.Length));
|
||||
break;
|
||||
default:
|
||||
throw new InvalidDataException(
|
||||
$"{description} contains an unsupported file-system entry: {entry.FullName}");
|
||||
}
|
||||
}
|
||||
|
||||
if (directory != rootInfo)
|
||||
entries.Add(new RegularTreeEntry(Relative(rootInfo, directory.FullName), directory.FullName,
|
||||
true, !hasChild, 0));
|
||||
}
|
||||
|
||||
entries.Sort((left, right) => StringComparer.Ordinal.Compare(left.RelativePath, right.RelativePath));
|
||||
return entries;
|
||||
}
|
||||
|
||||
/// <summary>Removes exactly this directory, clearing attributes that would refuse deletion.</summary>
|
||||
public static void DeleteDirectoryIfExists(string path)
|
||||
{
|
||||
if (!Directory.Exists(path)) return;
|
||||
foreach (var entry in Directory.EnumerateFileSystemEntries(path, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
try { File.SetAttributes(entry, FileAttributes.Normal); }
|
||||
catch { }
|
||||
}
|
||||
try { File.SetAttributes(path, FileAttributes.Normal); }
|
||||
catch { }
|
||||
Directory.Delete(path, recursive: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One comparable spelling of a path. Normalization happens before the trailing separator is
|
||||
/// dropped, because <c>Path.GetFullPath("C:")</c> is the current directory on that drive rather
|
||||
/// than the drive root.
|
||||
/// </summary>
|
||||
public static string NormalizePath(string path) =>
|
||||
Path.TrimEndingDirectorySeparator(Path.GetFullPath(path));
|
||||
|
||||
/// <summary>Whether two paths name the same location, after normalization.</summary>
|
||||
public static bool PathsEqual(string left, string right) =>
|
||||
NormalizePath(left).Equals(NormalizePath(right), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>Whether <paramref name="candidate"/> is <paramref name="container"/> or inside it, using a
|
||||
/// normalized prefix check so <c>C:\Games2</c> doesn't match inside <c>C:\Games</c>.</summary>
|
||||
public static bool PathContains(string container, string candidate)
|
||||
{
|
||||
container = NormalizePath(container);
|
||||
candidate = NormalizePath(candidate);
|
||||
return candidate.Equals(container, StringComparison.OrdinalIgnoreCase) ||
|
||||
candidate.StartsWith(container + Path.DirectorySeparatorChar,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Whether two paths are equal or one contains the other, after normalization.</summary>
|
||||
public static bool PathsOverlap(string left, string right) =>
|
||||
PathContains(left, right) || PathContains(right, left);
|
||||
|
||||
/// <summary>Location rules shared by install directories and portable roots (both are trees setup may
|
||||
/// replace and uninstall may delete). <paramref name="subject"/> only names what's being rejected.</summary>
|
||||
public static void EnsureUsableLocation(string path, string subject)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new InvalidOperationException($"{subject} is required.");
|
||||
if (path.StartsWith(@"\\", StringComparison.Ordinal))
|
||||
throw new InvalidOperationException($"{subject} must be on a local drive, not a network path.");
|
||||
if (path.Length > MaximumLocationLength)
|
||||
throw new InvalidOperationException(
|
||||
$"{subject} path is too long. Choose a path shorter than {MaximumLocationLength} characters.");
|
||||
|
||||
var full = NormalizePath(path);
|
||||
var drive = Path.GetPathRoot(full);
|
||||
if (string.IsNullOrWhiteSpace(drive) ||
|
||||
full.Equals(NormalizePath(drive), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{subject} must be a subfolder, not the root of a drive. " +
|
||||
"Choose a dedicated folder such as D:\\Games\\WiiCompiled\\Install.");
|
||||
}
|
||||
|
||||
var windows = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
|
||||
if (!string.IsNullOrEmpty(windows) && PathContains(windows, full))
|
||||
throw new InvalidOperationException($"{subject} cannot be inside the Windows directory.");
|
||||
|
||||
foreach (var folder in ReservedLocations)
|
||||
{
|
||||
var reserved = Environment.GetFolderPath(folder);
|
||||
if (string.IsNullOrEmpty(reserved)) continue;
|
||||
if (full.Equals(NormalizePath(reserved), StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidOperationException(
|
||||
$"{subject} cannot be {reserved}. Choose a dedicated folder.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The local build nests deep paths (llvm-mingw headers, CMake object directories) below the
|
||||
/// installation, so the root itself has to stay well inside the classic path limit.
|
||||
/// </summary>
|
||||
private const int MaximumLocationLength = 180;
|
||||
|
||||
/// <summary>Well-known folders that must never <em>be</em> an installation or portable root.</summary>
|
||||
private static readonly Environment.SpecialFolder[] ReservedLocations =
|
||||
[
|
||||
Environment.SpecialFolder.ProgramFiles,
|
||||
Environment.SpecialFolder.ProgramFilesX86,
|
||||
Environment.SpecialFolder.CommonProgramFiles,
|
||||
Environment.SpecialFolder.System,
|
||||
Environment.SpecialFolder.SystemX86,
|
||||
Environment.SpecialFolder.UserProfile,
|
||||
Environment.SpecialFolder.LocalApplicationData,
|
||||
Environment.SpecialFolder.ApplicationData,
|
||||
Environment.SpecialFolder.CommonApplicationData,
|
||||
Environment.SpecialFolder.DesktopDirectory,
|
||||
Environment.SpecialFolder.MyDocuments
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Writes a file so that a reader only ever sees the old contents or the new ones: the bytes go
|
||||
/// to a sibling temporary and are renamed over the destination, which is atomic on NTFS.
|
||||
/// Interrupting the write leaves the temporary behind, never a truncated document.
|
||||
/// </summary>
|
||||
public static void WriteAtomic(string path, byte[] contents)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))!);
|
||||
var temporary = path + $".tmp-{Guid.NewGuid():N}";
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes(temporary, contents);
|
||||
File.Move(temporary, path, overwrite: true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { File.Delete(temporary); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteAtomic(string path, string contents) =>
|
||||
WriteAtomic(path, System.Text.Encoding.UTF8.GetBytes(contents));
|
||||
|
||||
/// <summary>
|
||||
/// Refuses an operation that cannot fit on the destination drive. The caller owns the allowance
|
||||
/// it needs; this owns the one message and the one drive-readiness rule both paths report.
|
||||
/// </summary>
|
||||
public static void EnsureFreeSpace(string path, long required, string operationDescription)
|
||||
{
|
||||
var root = Path.GetPathRoot(path)
|
||||
?? throw new InvalidOperationException(
|
||||
"The installation directory is not on a local drive.");
|
||||
var drive = new DriveInfo(root);
|
||||
if (!drive.IsReady)
|
||||
throw new IOException($"The destination drive {root} is not ready.");
|
||||
if (drive.AvailableFreeSpace < required)
|
||||
{
|
||||
throw new IOException(
|
||||
$"Not enough free space on {root}. {operationDescription} needs approximately " +
|
||||
$"{FormatGiB(required)} free, but only {FormatGiB(drive.AvailableFreeSpace)} is available.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatGiB(long bytes) => $"{bytes / (1024d * 1024 * 1024):0.0} GiB";
|
||||
|
||||
public static void RejectReparsePoint(FileSystemInfo entry, string description)
|
||||
{
|
||||
if ((entry.Attributes & FileAttributes.ReparsePoint) != 0)
|
||||
throw new InvalidDataException(
|
||||
$"{description} contains a reparse point instead of a regular entry: {entry.FullName}");
|
||||
}
|
||||
|
||||
private static string Relative(DirectoryInfo root, string fullPath) =>
|
||||
Path.GetRelativePath(root.FullName, fullPath).Replace('\\', '/');
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
internal static class GameLaunchService
|
||||
{
|
||||
public static Task<int> LaunchAsync(BuildProfile profile)
|
||||
{
|
||||
var installation = new Installation(AppContext.BaseDirectory);
|
||||
using var operation = InstallOperationLock.Acquire(installation.Root);
|
||||
// A portable installation the user moved is reconciled before anything is read from its
|
||||
// recorded location. Launch never needs the discarded native build tree.
|
||||
PortableInstallHealing.HealMovedInstall(installation,
|
||||
new DelegatingInstallReporter(Console.Error.WriteLine));
|
||||
EnsureProductIsCurrent(installation, profile);
|
||||
return Task.FromResult(LaunchInstalledProduct(installation, profile));
|
||||
}
|
||||
|
||||
private static void EnsureProductIsCurrent(Installation installation, BuildProfile profile)
|
||||
{
|
||||
var state = InspectProductForLaunch(installation, profile);
|
||||
|
||||
if (state.Status == ProductStatus.Current) return;
|
||||
if (state.Status == ProductStatus.Absent)
|
||||
throw new InvalidDataException(
|
||||
"Retro Rewind is not installed. Install Retro Rewind through Wheel Wizard first.");
|
||||
|
||||
throw new InvalidDataException(string.IsNullOrWhiteSpace(state.Detail)
|
||||
? "This product must be repaired through Wheel Wizard before it can be launched."
|
||||
: state.Detail);
|
||||
}
|
||||
|
||||
/// <summary>Validates the executable, inputs, build provenance, runtime assets, payload cache, and the
|
||||
/// canonical Retro Rewind root, so a missing/invalid root fails launch instead of starting a vanilla-looking game.</summary>
|
||||
internal static ProductState InspectProductForLaunch(Installation installation, BuildProfile profile)
|
||||
{
|
||||
var toolkitFingerprint = installation.ResolveToolkitFingerprint();
|
||||
if (profile == BuildProfile.Base)
|
||||
return installation.CheckBase(toolkitFingerprint);
|
||||
var canonical = installation.ResolveCanonicalCompileInputs(null, out var canonicalError);
|
||||
return installation.CheckRetroRewind(toolkitFingerprint, canonical, canonicalError);
|
||||
}
|
||||
|
||||
private static int LaunchInstalledProduct(Installation installation, BuildProfile profile)
|
||||
{
|
||||
var runtimeRoot = profile == BuildProfile.Base ? installation.BaseDirectory : installation.RetroDirectory;
|
||||
var runtime = profile == BuildProfile.Base ? installation.BaseExecutable : installation.RetroExecutable;
|
||||
if (!File.Exists(runtime))
|
||||
throw new FileNotFoundException("The installed recomp is missing. Run the installer again.", runtime);
|
||||
if (!File.Exists(Path.Combine(installation.GameDataDirectory, "sys", "fst.bin")))
|
||||
throw new InvalidDataException("The installed Mario Kart Wii game data is missing. Run the installer again.");
|
||||
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
FileName = runtime,
|
||||
WorkingDirectory = runtimeRoot,
|
||||
UseShellExecute = false
|
||||
};
|
||||
using var process = Process.Start(info) ?? throw new InvalidOperationException("Could not start the recomp.");
|
||||
process.WaitForExit();
|
||||
return process.ExitCode;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
using System.Diagnostics;
|
||||
using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
internal static class InputValidation
|
||||
{
|
||||
private const long MaximumRetroWfcPayloadBytes = 16L * 1024 * 1024;
|
||||
// The payload is tens of kilobytes from a single fixed endpoint 30s is good.
|
||||
private static readonly TimeSpan RetroWfcDownloadTimeout = TimeSpan.FromSeconds(30);
|
||||
private static readonly TimeSpan RetroWfcRetryDelay = TimeSpan.FromSeconds(1);
|
||||
private static readonly HashSet<string> SupportedDiscImageExtensions = new(
|
||||
[".iso", ".gcm", ".gcz", ".ciso", ".wbfs", ".wia", ".rvz"],
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public const string CurrentRetroWfcPayloadUri = "http://nas.play.rwfc.net/payload?g=RMCPD00";
|
||||
private static readonly string RetroWfcOfflinePayloadFile =
|
||||
Path.Combine("binary", "payload.RMCPD00.bin");
|
||||
|
||||
/// <summary>Retro-WFC production payload signing key (PROD PayloadPublicKey). Verifies
|
||||
/// the payload past the 0x110 header; on-console stage1 pins the same key, so
|
||||
/// rotating it upstream is a breaking release there too.</summary>
|
||||
private static readonly byte[] RetroWfcPayloadSigningModulus = Convert.FromHexString(
|
||||
"e6e6ce416f350422cbe26c36a67eba613dddcd27d79afd077dcc593e5319eaa6" +
|
||||
"080293400033876d3dbdfda12c15f46ac8e4f5b40c56e7b5f67e91647d618cb9" +
|
||||
"99c041581b86d103bd7723fceac03ad3ad5134bf611cd47dc527002596821e94" +
|
||||
"1c9470938fea07238a84767323e4a610bd996465e59d04dae4febd915c96fc07" +
|
||||
"39e4e818300829d78f3f2275e1f3fbd2507f1bde74f24a5285e61007b959a583" +
|
||||
"b4820d75eca76680866efe5d79590b82c3577b796155899530e305b94b4ceef4" +
|
||||
"428644b719df3d8540c9588f5bb02d83d3938255d1a1e073d3408163ff93a615" +
|
||||
"a2106a03923a397aad6a29ebb43031ed06de1575c8ee2b54678fa059e025f455");
|
||||
|
||||
private const int RetroWfcPayloadSignedRegionOffset = 0x110;
|
||||
private const int RetroWfcPayloadSignatureOffset = 0x10;
|
||||
private const int RetroWfcPayloadMinimumBytes = 0x130;
|
||||
|
||||
public static void ValidateExtension(string gamePath)
|
||||
{
|
||||
if (!File.Exists(gamePath))
|
||||
throw new FileNotFoundException("The selected game image does not exist.", gamePath);
|
||||
var extension = Path.GetExtension(gamePath);
|
||||
if (!SupportedDiscImageExtensions.Contains(extension))
|
||||
throw new InvalidDataException(
|
||||
"Select a complete Wii disc image in ISO, GCM, GCZ, CISO, WBFS, WIA, or RVZ format.");
|
||||
}
|
||||
|
||||
public static async Task<DiscHeader> ReadDiscHeaderAsync(string dolphinTool, string gamePath,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ValidateExtension(gamePath);
|
||||
var result = await ProcessRunner.RunAsync(dolphinTool,
|
||||
["header", "-i", Path.GetFullPath(gamePath), "-j"], null, cancellationToken);
|
||||
if (result.ExitCode != 0)
|
||||
throw new InvalidDataException("DolphinTool could not read this disc image. " + result.CombinedOutput.Trim());
|
||||
|
||||
var json = result.StandardOutput.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
|
||||
.FirstOrDefault(line => line.TrimStart().StartsWith('{'));
|
||||
if (json is null)
|
||||
throw new InvalidDataException("DolphinTool did not return disc metadata.");
|
||||
return JsonSerializer.Deserialize<DiscHeader>(json)
|
||||
?? throw new InvalidDataException("DolphinTool returned invalid disc metadata.");
|
||||
}
|
||||
|
||||
public static void EnsureCompatibleDisc(DiscHeader header, PayloadManifest manifest)
|
||||
{
|
||||
if (!header.GameId.Equals(manifest.ExpectedGameId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"This build supports Mario Kart Wii PAL ({manifest.ExpectedGameId}). " +
|
||||
$"The selected image is {header.GameId} ({header.InternalName}, {header.Region}).");
|
||||
}
|
||||
}
|
||||
|
||||
public static string ValidateStagedRetroWfcPayloadDirectory(string stagedDirectory,
|
||||
RSAParameters? signingKey = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stagedDirectory))
|
||||
throw new InvalidDataException("The staged Retro-WFC payload directory is missing.");
|
||||
|
||||
var root = Path.GetFullPath(stagedDirectory);
|
||||
var payload = Path.Combine(root, RetroWfcOfflinePayloadFile);
|
||||
if (!File.Exists(payload))
|
||||
throw new InvalidDataException(
|
||||
"The staged Retro-WFC payload directory does not contain binary\\payload.RMCPD00.bin.");
|
||||
ValidateRetroWfcPayloadFile(payload, signingKey);
|
||||
return root;
|
||||
}
|
||||
|
||||
public static string ResolveRetroWfcPayloadFile(string stagedDirectory,
|
||||
RSAParameters? signingKey = null) =>
|
||||
Path.Combine(ValidateStagedRetroWfcPayloadDirectory(stagedDirectory, signingKey),
|
||||
RetroWfcOfflinePayloadFile);
|
||||
|
||||
public static string ComputeRetroWfcPayloadSha256(string stagedDirectory,
|
||||
RSAParameters? signingKey = null) =>
|
||||
Sha256File(ResolveRetroWfcPayloadFile(stagedDirectory, signingKey));
|
||||
|
||||
public static void ValidateRetroWfcPayloadUri(string uriText)
|
||||
{
|
||||
if (!string.Equals(uriText, CurrentRetroWfcPayloadUri, StringComparison.Ordinal))
|
||||
throw new InvalidDataException("The installer does not define the fixed Retro-WFC payload endpoint.");
|
||||
}
|
||||
|
||||
public static async Task<RetroWfcPayloadSnapshot> DownloadRetroWfcPayloadAsync(string uriText,
|
||||
string destinationDirectory,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ValidateRetroWfcPayloadUri(uriText);
|
||||
var uri = new Uri(uriText, UriKind.Absolute);
|
||||
|
||||
var root = Path.GetFullPath(destinationDirectory);
|
||||
var destination = Path.Combine(root, RetroWfcOfflinePayloadFile);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
|
||||
using var handler = new HttpClientHandler { AllowAutoRedirect = false };
|
||||
using var client = new HttpClient(handler) { Timeout = Timeout.InfiniteTimeSpan };
|
||||
|
||||
for (var attempt = 1; attempt <= 2; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await DownloadRetroWfcPayloadAttemptAsync(client, uri, root, destination,
|
||||
cancellationToken);
|
||||
}
|
||||
catch (Exception ex) when (attempt == 1 &&
|
||||
IsTransientRetroWfcDownloadFailure(ex, cancellationToken))
|
||||
{
|
||||
await Task.Delay(RetroWfcRetryDelay, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("The Retro-WFC download retry loop ended unexpectedly.");
|
||||
}
|
||||
|
||||
private static async Task<RetroWfcPayloadSnapshot> DownloadRetroWfcPayloadAttemptAsync(
|
||||
HttpClient client, Uri uri, string root, string destination, CancellationToken cancellationToken)
|
||||
{
|
||||
var temporary = destination + $".tmp-{Guid.NewGuid():N}";
|
||||
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
timeout.CancelAfter(RetroWfcDownloadTimeout);
|
||||
var attemptToken = timeout.Token;
|
||||
try
|
||||
{
|
||||
using var response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, attemptToken);
|
||||
if ((int)response.StatusCode is >= 300 and < 400)
|
||||
throw new InvalidDataException(
|
||||
"The fixed Retro-WFC payload endpoint redirected; refusing to fetch from a different target.");
|
||||
response.EnsureSuccessStatusCode();
|
||||
if (response.Content.Headers.ContentLength is > MaximumRetroWfcPayloadBytes)
|
||||
throw new InvalidDataException("The Retro-WFC payload is unexpectedly large.");
|
||||
await using var input = await response.Content.ReadAsStreamAsync(attemptToken);
|
||||
long total = 0;
|
||||
string actualSha256;
|
||||
await using (var output = new FileStream(temporary, FileMode.CreateNew, FileAccess.Write,
|
||||
FileShare.None, 64 * 1024, FileOptions.Asynchronous))
|
||||
{
|
||||
var buffer = new byte[64 * 1024];
|
||||
using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
|
||||
while (true)
|
||||
{
|
||||
var read = await input.ReadAsync(buffer, attemptToken);
|
||||
if (read == 0) break;
|
||||
total = checked(total + read);
|
||||
if (total > MaximumRetroWfcPayloadBytes)
|
||||
throw new InvalidDataException("The Retro-WFC payload is unexpectedly large.");
|
||||
hash.AppendData(buffer, 0, read);
|
||||
await output.WriteAsync(buffer.AsMemory(0, read), attemptToken);
|
||||
}
|
||||
actualSha256 = Convert.ToHexString(hash.GetHashAndReset()).ToLowerInvariant();
|
||||
}
|
||||
ValidateRetroWfcPayloadFile(temporary);
|
||||
File.Move(temporary, destination, overwrite: true);
|
||||
return new RetroWfcPayloadSnapshot(root, actualSha256, total);
|
||||
}
|
||||
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw new TimeoutException(
|
||||
$"The Retro-WFC payload download did not finish within {RetroWfcDownloadTimeout.TotalMinutes:0} minutes.",
|
||||
ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { File.Delete(temporary); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsTransientRetroWfcDownloadFailure(Exception exception,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested) return false;
|
||||
if (exception is TimeoutException) return true;
|
||||
if (exception is not HttpRequestException request) return false;
|
||||
return request.StatusCode is null or HttpStatusCode.RequestTimeout or
|
||||
HttpStatusCode.TooManyRequests ||
|
||||
request.StatusCode is { } status && (int)status >= 500;
|
||||
}
|
||||
|
||||
private static void ValidateRetroWfcPayloadFile(string payload, RSAParameters? signingKey = null)
|
||||
{
|
||||
byte[] image;
|
||||
using (var stream = File.OpenRead(payload))
|
||||
{
|
||||
if (stream.Length > MaximumRetroWfcPayloadBytes)
|
||||
throw new InvalidDataException("The Retro-WFC payload is unexpectedly large.");
|
||||
if (stream.Length < RetroWfcPayloadMinimumBytes)
|
||||
throw new InvalidDataException("The Retro-WFC payload has an invalid header.");
|
||||
image = new byte[stream.Length];
|
||||
stream.ReadExactly(image);
|
||||
}
|
||||
|
||||
if (!image.AsSpan(0, 12).SequenceEqual("WWFC/Payload"u8))
|
||||
throw new InvalidDataException("The Retro-WFC payload has an invalid header.");
|
||||
var declaredSize = BinaryPrimitives.ReadUInt32BigEndian(image.AsSpan(0x0C));
|
||||
if (declaredSize != image.Length)
|
||||
throw new InvalidDataException(
|
||||
$"The Retro-WFC payload declares {declaredSize} bytes but contains {image.Length}.");
|
||||
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportParameters(signingKey ?? new RSAParameters
|
||||
{
|
||||
Modulus = RetroWfcPayloadSigningModulus,
|
||||
Exponent = [0x01, 0x00, 0x01],
|
||||
});
|
||||
if (!rsa.VerifyData(
|
||||
image.AsSpan(RetroWfcPayloadSignedRegionOffset),
|
||||
image.AsSpan(RetroWfcPayloadSignatureOffset,
|
||||
RetroWfcPayloadSignedRegionOffset - RetroWfcPayloadSignatureOffset),
|
||||
HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1))
|
||||
throw new InvalidDataException(
|
||||
"The Retro-WFC payload is not signed by the pinned Retro-WFC signing key.");
|
||||
}
|
||||
|
||||
public static string Sha256File(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record ProcessResult(int ExitCode, string StandardOutput, string StandardError)
|
||||
{
|
||||
public string CombinedOutput => StandardOutput + Environment.NewLine + StandardError;
|
||||
}
|
||||
|
||||
internal static class ProcessRunner
|
||||
{
|
||||
/// <summary>Runs a redirected child process to completion. <paramref name="configure"/> sets up a
|
||||
/// working directory or scrubbed environment; <paramref name="capture"/> is off for callers that only
|
||||
/// forward output live, so a build's output isn't buffered in memory for nobody to read.</summary>
|
||||
public static async Task<ProcessResult> RunAsync(string executable, IReadOnlyList<string> arguments,
|
||||
Action<string>? output, CancellationToken cancellationToken,
|
||||
Action<ProcessStartInfo>? configure = null, bool capture = true,
|
||||
Action<Exception>? onTerminationFailure = null)
|
||||
{
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
FileName = executable,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
foreach (var argument in arguments) info.ArgumentList.Add(argument);
|
||||
configure?.Invoke(info);
|
||||
|
||||
using var process = new Process { StartInfo = info, EnableRaisingEvents = true };
|
||||
var stdout = new List<string>();
|
||||
var stderr = new List<string>();
|
||||
process.OutputDataReceived += (_, e) => { if (e.Data is not null) { if (capture) stdout.Add(e.Data); output?.Invoke(e.Data); } };
|
||||
process.ErrorDataReceived += (_, e) => { if (e.Data is not null) { if (capture) stderr.Add(e.Data); output?.Invoke(e.Data); } };
|
||||
if (!process.Start()) throw new InvalidOperationException($"Could not start {executable}.");
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
await WaitForExitAsync(process, cancellationToken, onTerminationFailure);
|
||||
return new ProcessResult(process.ExitCode, string.Join(Environment.NewLine, stdout),
|
||||
string.Join(Environment.NewLine, stderr));
|
||||
}
|
||||
|
||||
public static async Task WaitForExitAsync(Process process, CancellationToken cancellationToken,
|
||||
Action<Exception>? onTerminationFailure = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!process.HasExited) process.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
onTerminationFailure?.Invoke(ex);
|
||||
}
|
||||
await process.WaitForExitAsync(CancellationToken.None);
|
||||
process.WaitForExit();
|
||||
throw;
|
||||
}
|
||||
process.WaitForExit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
/// <summary>
|
||||
/// A fail-fast, cross-process lock covering install, repair and launch operations for one install
|
||||
/// root. The lock file lives beside the installation, so replacing the installation cannot replace
|
||||
/// or release the lock that protects it.
|
||||
/// </summary>
|
||||
internal sealed class InstallOperationLock : IDisposable
|
||||
{
|
||||
private readonly FileStream _stream;
|
||||
|
||||
private InstallOperationLock(FileStream stream) => _stream = stream;
|
||||
|
||||
public string LockPath => _stream.Name;
|
||||
|
||||
public static InstallOperationLock Acquire(string installDirectory, IInstallReporter? reporter = null)
|
||||
{
|
||||
var root = InstallOperationPaths.NormalizeRoot(installDirectory);
|
||||
var parent = InstallOperationPaths.GetParent(root);
|
||||
Directory.CreateDirectory(parent);
|
||||
var path = InstallOperationPaths.GetLockPath(root);
|
||||
|
||||
FileStream stream;
|
||||
try
|
||||
{
|
||||
stream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None,
|
||||
bufferSize: 1, FileOptions.WriteThrough);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Another WiiCompiled operation is already using this installation. " +
|
||||
"Wait for the current game, install, or repair operation to finish.", ex);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
InstallTransaction.Recover(root, reporter);
|
||||
InstallScratchSpace.Recover(root, reporter);
|
||||
return new InstallOperationLock(stream);
|
||||
}
|
||||
catch
|
||||
{
|
||||
stream.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => _stream.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>Deterministic paths shared by the operation lock and its recovery journal.</summary>
|
||||
internal static class InstallOperationPaths
|
||||
{
|
||||
public static string NormalizeRoot(string installDirectory)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(installDirectory))
|
||||
throw new ArgumentException("The installation directory is required.", nameof(installDirectory));
|
||||
return FileSystemUtilities.NormalizePath(installDirectory);
|
||||
}
|
||||
|
||||
public static string GetParent(string normalizedRoot) =>
|
||||
Directory.GetParent(normalizedRoot)?.FullName
|
||||
?? throw new InvalidOperationException("The installation directory must have a parent directory.");
|
||||
|
||||
|
||||
public static string GetScopeId(string normalizedRoot) =>
|
||||
HashId(16, normalizedRoot.ToUpperInvariant());
|
||||
|
||||
public static string HashId(int hexLength, params string[] canonicalParts) =>
|
||||
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(string.Join('\n', canonicalParts))))
|
||||
.ToLowerInvariant()[..hexLength];
|
||||
|
||||
public static string GetLockPath(string normalizedRoot) =>
|
||||
Path.Combine(GetParent(normalizedRoot), $".mkwc-operation-{GetScopeId(normalizedRoot)}.lock");
|
||||
|
||||
public static string GetJournalPath(string normalizedRoot) =>
|
||||
Path.Combine(GetParent(normalizedRoot), $".mkwc-transaction-{GetScopeId(normalizedRoot)}.json");
|
||||
|
||||
public static string GetWorkRoot(string normalizedRoot, Guid transactionId) =>
|
||||
Path.Combine(GetParent(normalizedRoot),
|
||||
$".mkwc-transaction-{GetScopeId(normalizedRoot)}-{transactionId:N}");
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
/// <summary>
|
||||
/// Stable stage identifiers reported by <c>--progress-json</c>. These are part of the public
|
||||
/// WheelWizard contract (docs/WHEELWIZARD_CONTRACT.md): the strings may gain new members, but an
|
||||
/// existing identifier never changes meaning.
|
||||
/// </summary>
|
||||
internal static class InstallStages
|
||||
{
|
||||
public const string Validate = "validate";
|
||||
public const string ExtractToolkit = "extract-toolkit";
|
||||
public const string ExtractDisc = "extract-disc";
|
||||
public const string PrepareRetro = "prepare-retro";
|
||||
public const string BuildBase = "build-base";
|
||||
public const string BuildRetro = "build-retro";
|
||||
public const string Publish = "publish";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Where an installation reports what it is doing. Progress is coarse and monotonic; raw translator
|
||||
/// and compiler output is a diagnostic, never progress, because it is unbounded and machine-hostile.
|
||||
/// </summary>
|
||||
internal interface IInstallReporter
|
||||
{
|
||||
void Progress(string stage, string message, int percent);
|
||||
void Diagnostic(string line);
|
||||
}
|
||||
|
||||
/// <summary>Reports to a caller-supplied sink; used by console and maintenance commands.</summary>
|
||||
internal sealed class DelegatingInstallReporter : IInstallReporter
|
||||
{
|
||||
private readonly Action<string> _sink;
|
||||
|
||||
public DelegatingInstallReporter(Action<string> sink) => _sink = sink;
|
||||
|
||||
public void Progress(string stage, string message, int percent) => _sink(message);
|
||||
public void Diagnostic(string line) => _sink(line);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <c>--progress-json</c> protocol: one JSON object per line on stdout, nothing else on stdout,
|
||||
/// diagnostics on stderr. The terminal <c>result</c> line is written exactly once.
|
||||
/// </summary>
|
||||
internal sealed class NdjsonInstallReporter : IInstallReporter
|
||||
{
|
||||
private static readonly JsonSerializerOptions Options = new() { WriteIndented = false };
|
||||
private readonly Action<string>? _log;
|
||||
private readonly object _gate = new();
|
||||
private int _lastPercent;
|
||||
private bool _finished;
|
||||
|
||||
public NdjsonInstallReporter(Action<string>? log = null) => _log = log;
|
||||
|
||||
public void Progress(string stage, string message, int percent)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_finished) return;
|
||||
// Percentages are clamped monotonic: a caller's progress bar must never walk backwards
|
||||
// because a later stage happened to estimate a lower number.
|
||||
_lastPercent = Math.Clamp(Math.Max(percent, _lastPercent), 0, 99);
|
||||
WriteLine(new { type = "progress", stage, message, percent = _lastPercent });
|
||||
}
|
||||
_log?.Invoke(message);
|
||||
}
|
||||
|
||||
public void Diagnostic(string line)
|
||||
{
|
||||
Console.Error.WriteLine(line);
|
||||
_log?.Invoke(line);
|
||||
}
|
||||
|
||||
public void Success(string installDirectory)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_finished) return;
|
||||
_finished = true;
|
||||
WriteLine(new { type = "result", success = true, version = ProductInfo.Version, installDir = installDirectory });
|
||||
}
|
||||
}
|
||||
|
||||
public void Failure(string error)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_finished) return;
|
||||
_finished = true;
|
||||
WriteLine(new { type = "result", success = false, error });
|
||||
}
|
||||
_log?.Invoke("FAILED: " + error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The terminal result line is the caller's only completion signal, so no exit path may skip it.
|
||||
/// Callers invoke this from a finally block; it is a no-op once a result was already written.
|
||||
/// </summary>
|
||||
public void EnsureFinished(string errorIfUnfinished) => Failure(errorIfUnfinished);
|
||||
|
||||
private static void WriteLine(object value)
|
||||
{
|
||||
Console.Out.WriteLine(JsonSerializer.Serialize(value, Options));
|
||||
Console.Out.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Plain-text console reporting for a silent install without <c>--progress-json</c>.</summary>
|
||||
internal sealed class ConsoleInstallReporter : IInstallReporter
|
||||
{
|
||||
private readonly Action<string>? _log;
|
||||
|
||||
public ConsoleInstallReporter(Action<string>? log = null) => _log = log;
|
||||
|
||||
public void Progress(string stage, string message, int percent)
|
||||
{
|
||||
Console.Out.WriteLine($"[{percent,3}%] {message}");
|
||||
_log?.Invoke(message);
|
||||
}
|
||||
|
||||
public void Diagnostic(string line)
|
||||
{
|
||||
Console.Out.WriteLine(line);
|
||||
_log?.Invoke(line);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Build step identifiers from the bundled script's <c>MKWCBUILD:STEP:<id></c> lines. The
|
||||
/// id is the contract; LocalBuild.ps1 emits these exactly and Test-PinnedFacts.ps1 fails the release if
|
||||
/// the two sets disagree, so progress never depends on matching reworded English prose.</summary>
|
||||
internal static class BuildStepIds
|
||||
{
|
||||
public const string ReuseBaseTranslation = "reuse-base-translation";
|
||||
public const string RetranslateBase = "retranslate-base";
|
||||
public const string TranslateBase = "translate-base";
|
||||
public const string EmitBaseManifest = "emit-base-manifest";
|
||||
public const string TranslateMod = "translate-mod";
|
||||
public const string GenerateDataInit = "generate-data-init";
|
||||
public const string EmitBuildShards = "emit-build-shards";
|
||||
public const string ConfigureNative = "configure-native";
|
||||
public const string Compile = "compile";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps one local translate-and-compile run onto a slice of the overall percentage. The bundled
|
||||
/// build script announces every step it starts with an <c>MKWCBUILD:</c> prefix, so the slice can
|
||||
/// advance on real events instead of on a timer.
|
||||
/// </summary>
|
||||
internal sealed class BuildProgressWindow
|
||||
{
|
||||
private const string Marker = "MKWCBUILD:";
|
||||
private const string StepMarker = "STEP:";
|
||||
|
||||
/// <summary>The fraction the compile step reaches; beyond it, compiler output is a heartbeat.</summary>
|
||||
private const double CompileFraction = 0.58;
|
||||
|
||||
private static readonly (string Id, double Fraction, string Message)[] Steps =
|
||||
[
|
||||
(BuildStepIds.ReuseBaseTranslation, 0.30, "Reusing the completed base translation"),
|
||||
(BuildStepIds.RetranslateBase, 0.05, "The base translation is stale; retranslating it"),
|
||||
(BuildStepIds.TranslateBase, 0.08, "Translating Mario Kart Wii"),
|
||||
(BuildStepIds.EmitBaseManifest, 0.34, "Creating the translation manifest"),
|
||||
(BuildStepIds.TranslateMod, 0.38, "Translating the Retro Rewind Code.pul"),
|
||||
(BuildStepIds.GenerateDataInit, 0.44, "Generating game data initialization"),
|
||||
(BuildStepIds.EmitBuildShards, 0.48, "Preparing the native build"),
|
||||
(BuildStepIds.ConfigureNative, 0.52, "Configuring the bundled compiler"),
|
||||
(BuildStepIds.Compile, CompileFraction, "Compiling the game. This is the longest step"),
|
||||
];
|
||||
|
||||
private readonly IInstallReporter _reporter;
|
||||
private readonly string _stage;
|
||||
private readonly int _start;
|
||||
private readonly int _end;
|
||||
private double _fraction;
|
||||
private string _message = "Preparing the local build";
|
||||
private int _reportedPercent = -1;
|
||||
|
||||
public BuildProgressWindow(IInstallReporter reporter, string stage, int start, int end)
|
||||
{
|
||||
_reporter = reporter;
|
||||
_stage = stage;
|
||||
_start = start;
|
||||
_end = end;
|
||||
}
|
||||
|
||||
public void Observe(string line)
|
||||
{
|
||||
var index = line.IndexOf(Marker, StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
{
|
||||
var text = line[(index + Marker.Length)..].Trim();
|
||||
if (text.StartsWith(StepMarker, StringComparison.Ordinal))
|
||||
{
|
||||
var identifier = text[StepMarker.Length..];
|
||||
var end = identifier.IndexOf(' ');
|
||||
if (end >= 0) identifier = identifier[..end];
|
||||
foreach (var (id, fraction, message) in Steps)
|
||||
{
|
||||
if (!id.Equals(identifier, StringComparison.Ordinal)) continue;
|
||||
if (fraction > _fraction)
|
||||
{
|
||||
_fraction = fraction;
|
||||
_message = message;
|
||||
Emit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Anything else - an unknown step identifier from a newer script, a plain MKWCBUILD note, or
|
||||
// raw tool output - stays a diagnostic and only feeds the heartbeat below.
|
||||
_reporter.Diagnostic(line);
|
||||
// Compilation announces itself once and then emits thousands of compiler lines. Treat that
|
||||
// output as a heartbeat so the slice keeps creeping forward, but only publish a progress
|
||||
// line when the rounded percentage actually changes.
|
||||
if (_fraction >= CompileFraction)
|
||||
{
|
||||
_fraction = Math.Min(0.97, _fraction + 0.0015);
|
||||
Emit();
|
||||
}
|
||||
}
|
||||
|
||||
private void Emit()
|
||||
{
|
||||
var percent = Interpolate(_fraction);
|
||||
if (percent == _reportedPercent) return;
|
||||
_reportedPercent = percent;
|
||||
_reporter.Progress(_stage, _message, percent);
|
||||
}
|
||||
|
||||
private int Interpolate(double fraction) =>
|
||||
(int)Math.Round(_start + (_end - _start) * Math.Clamp(fraction, 0, 1));
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
/// <summary>
|
||||
/// Owns one temporary directory for an install operation. The name carries the installation's scope
|
||||
/// id, so the next locked operation can remove trees left by forced process termination without
|
||||
/// touching scratch belonging to another installation.
|
||||
/// </summary>
|
||||
internal sealed class InstallScratchSpace : IDisposable
|
||||
{
|
||||
private readonly IInstallReporter? _reporter;
|
||||
private bool _disposed;
|
||||
|
||||
private InstallScratchSpace(string root, IInstallReporter? reporter)
|
||||
{
|
||||
Root = root;
|
||||
_reporter = reporter;
|
||||
}
|
||||
|
||||
public string Root { get; }
|
||||
|
||||
/// <summary>Creates staging beside the installation, suitable even when the install is absent.</summary>
|
||||
public static InstallScratchSpace CreateSibling(string installDirectory, IInstallReporter? reporter = null) =>
|
||||
Create(InstallOperationPaths.GetParent(InstallOperationPaths.NormalizeRoot(installDirectory)),
|
||||
installDirectory, reporter);
|
||||
|
||||
/// <summary>Creates build/update output inside an existing installation.</summary>
|
||||
public static InstallScratchSpace CreateInsideInstall(string installDirectory,
|
||||
IInstallReporter? reporter = null)
|
||||
{
|
||||
var installRoot = InstallOperationPaths.NormalizeRoot(installDirectory);
|
||||
if (!Directory.Exists(installRoot))
|
||||
throw new DirectoryNotFoundException(
|
||||
"An in-install scratch directory requires an existing installation root.");
|
||||
return Create(installRoot, installDirectory, reporter);
|
||||
}
|
||||
|
||||
/// <summary>Deletes scratch left by an interrupted operation. Call only while locked.</summary>
|
||||
public static void Recover(string installDirectory, IInstallReporter? reporter = null)
|
||||
{
|
||||
var installRoot = InstallOperationPaths.NormalizeRoot(installDirectory);
|
||||
foreach (var container in new[] { InstallOperationPaths.GetParent(installRoot), installRoot })
|
||||
{
|
||||
if (!Directory.Exists(container)) continue;
|
||||
foreach (var leftover in Directory.EnumerateDirectories(container, Pattern(installRoot)))
|
||||
{
|
||||
reporter?.Diagnostic("Removing scratch space from an interrupted operation: " + leftover);
|
||||
FileSystemUtilities.DeleteDirectoryIfExists(leftover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
try
|
||||
{
|
||||
FileSystemUtilities.DeleteDirectoryIfExists(Root);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// The next locked operation removes this exact tree. A transient AV/file-indexer handle
|
||||
// must not turn an already committed installation into a reported failure.
|
||||
_reporter?.Diagnostic("Scratch cleanup is pending and will be retried: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static InstallScratchSpace Create(string container, string installDirectory,
|
||||
IInstallReporter? reporter)
|
||||
{
|
||||
var installRoot = InstallOperationPaths.NormalizeRoot(installDirectory);
|
||||
var root = Path.Combine(container,
|
||||
$".mkwc-scratch-{InstallOperationPaths.GetScopeId(installRoot)}-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(root);
|
||||
reporter?.Diagnostic("Prepared exact update scratch space at " + root);
|
||||
return new InstallScratchSpace(root, reporter);
|
||||
}
|
||||
|
||||
private static string Pattern(string installRoot) =>
|
||||
$".mkwc-scratch-{InstallOperationPaths.GetScopeId(installRoot)}-*";
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
internal enum InstallTransactionEntryKind
|
||||
{
|
||||
Directory,
|
||||
File
|
||||
}
|
||||
|
||||
/// <summary>An exact prepared-path to live-path replacement made by an install transaction.</summary>
|
||||
internal sealed record InstallTransactionEntry(
|
||||
string PreparedPath,
|
||||
string DestinationPath,
|
||||
InstallTransactionEntryKind Kind)
|
||||
{
|
||||
public static InstallTransactionEntry Directory(string preparedPath, string destinationPath) =>
|
||||
new(preparedPath, destinationPath, InstallTransactionEntryKind.Directory);
|
||||
|
||||
public static InstallTransactionEntry File(string preparedPath, string destinationPath) =>
|
||||
new(preparedPath, destinationPath, InstallTransactionEntryKind.File);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes prepared files/directories as one recoverable operation. Destinations are journaled before the
|
||||
/// first move, so an interrupted operation rolls back on disposal or the next lock acquisition. Because every
|
||||
/// move is a same-volume rename, rollback needs no per-entry record, just backup/destination/prepared-path presence.
|
||||
/// </summary>
|
||||
internal sealed class InstallTransaction : IDisposable
|
||||
{
|
||||
// v3 renamed the journal's version field from "Schema" to "SchemaVersion", so that every
|
||||
// persisted document in an installation spells it the same way. A journal only exists between
|
||||
// the first move of an update and its commit, and a document from another version has always
|
||||
// been rejected rather than interpreted, so the rename needs no read compatibility - it only
|
||||
// needs to be visible as a version change.
|
||||
private const int SchemaVersion = 3;
|
||||
|
||||
private readonly string _journalPath;
|
||||
private readonly IInstallReporter? _reporter;
|
||||
private readonly TransactionJournal _journal;
|
||||
private bool _finished;
|
||||
|
||||
private InstallTransaction(string journalPath, TransactionJournal journal, IInstallReporter? reporter)
|
||||
{
|
||||
_journalPath = journalPath;
|
||||
_journal = journal;
|
||||
_reporter = reporter;
|
||||
}
|
||||
|
||||
public static InstallTransaction Begin(string installDirectory, IInstallReporter? reporter = null,
|
||||
params InstallTransactionEntry[] entries)
|
||||
{
|
||||
var root = InstallOperationPaths.NormalizeRoot(installDirectory);
|
||||
if (entries is null || entries.Length == 0)
|
||||
throw new ArgumentException("At least one publish entry is required.", nameof(entries));
|
||||
|
||||
var journalPath = InstallOperationPaths.GetJournalPath(root);
|
||||
if (File.Exists(journalPath))
|
||||
throw new InvalidOperationException(
|
||||
"This installation has an unfinished transaction. Acquire the installation operation lock " +
|
||||
"so it can recover before beginning another transaction.");
|
||||
|
||||
var transactionId = Guid.NewGuid();
|
||||
var workRoot = InstallOperationPaths.GetWorkRoot(root, transactionId);
|
||||
var journal = new TransactionJournal
|
||||
{
|
||||
SchemaVersion = SchemaVersion,
|
||||
TransactionId = transactionId,
|
||||
InstallRoot = root,
|
||||
WorkRoot = workRoot,
|
||||
State = TransactionState.Prepared,
|
||||
Entries = NormalizeEntries(root, workRoot, entries)
|
||||
};
|
||||
|
||||
Directory.CreateDirectory(Path.Combine(workRoot, "backup"));
|
||||
JsonState.Write(journalPath, journal);
|
||||
return new InstallTransaction(journalPath, journal, reporter);
|
||||
}
|
||||
|
||||
/// <summary>Moves every old destination aside and publishes every prepared entry.</summary>
|
||||
public void Publish()
|
||||
{
|
||||
EnsureActive(TransactionState.Prepared);
|
||||
try
|
||||
{
|
||||
foreach (var entry in _journal.Entries)
|
||||
{
|
||||
if (entry.OriginalExisted)
|
||||
Move(entry.DestinationPath, entry.BackupPath, entry.Kind);
|
||||
Move(entry.PreparedPath, entry.DestinationPath, entry.Kind);
|
||||
}
|
||||
|
||||
_journal.State = TransactionState.Published;
|
||||
WriteJournal();
|
||||
}
|
||||
catch (Exception publishFailure)
|
||||
{
|
||||
try
|
||||
{
|
||||
RollBack();
|
||||
}
|
||||
catch (Exception rollbackFailure)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"Publishing failed and the previous installation could not be fully restored. " +
|
||||
"The recovery journal was retained for the next operation.",
|
||||
publishFailure, rollbackFailure);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Durably records the runtime configuration preimage before the caller mutates it. Recovery of
|
||||
/// any uncommitted transaction restores this exact preimage; committed transactions retain the
|
||||
/// new configuration. Call after <see cref="Publish"/> and before the first config write.
|
||||
/// </summary>
|
||||
public void RecordRuntimeConfigurationMutation(RuntimeConfigSnapshot snapshot)
|
||||
{
|
||||
EnsureActive(TransactionState.Published);
|
||||
_journal.RuntimeConfigExisted = snapshot.Existed;
|
||||
_journal.RuntimeConfigContents = snapshot.Contents;
|
||||
_journal.RuntimeConfigMutationPlanned = true;
|
||||
WriteJournal();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes the published paths authoritative. Cleanup failure is non-fatal: the committed journal
|
||||
/// makes the next lock acquisition finish deleting only this transaction's exact backup paths.
|
||||
/// </summary>
|
||||
public void Commit()
|
||||
{
|
||||
EnsureActive(TransactionState.Published);
|
||||
_journal.State = TransactionState.Committed;
|
||||
WriteJournal();
|
||||
_finished = true;
|
||||
TryCleanCommitted(_journalPath, _journal, _reporter);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_finished) return;
|
||||
RollBack();
|
||||
}
|
||||
|
||||
/// <summary>Recovers the one deterministic journal for an installation. Call only while locked.</summary>
|
||||
public static void Recover(string installDirectory, IInstallReporter? reporter = null)
|
||||
{
|
||||
var root = InstallOperationPaths.NormalizeRoot(installDirectory);
|
||||
var journalPath = InstallOperationPaths.GetJournalPath(root);
|
||||
if (!File.Exists(journalPath)) return;
|
||||
|
||||
var journal = JsonState.TryRead<TransactionJournal>(journalPath)
|
||||
?? throw new InvalidDataException(
|
||||
$"The interrupted-install journal is unreadable: {journalPath}");
|
||||
ValidateJournal(root, journal);
|
||||
|
||||
if (journal.State is TransactionState.Committed or TransactionState.RolledBack)
|
||||
{
|
||||
reporter?.Diagnostic("Finishing cleanup from the previous completed update operation...");
|
||||
TryCleanCommitted(journalPath, journal, reporter);
|
||||
if (File.Exists(journalPath))
|
||||
throw new IOException("The previous committed update's exact backup could not be removed.");
|
||||
return;
|
||||
}
|
||||
|
||||
reporter?.Diagnostic("Restoring the installation after an interrupted update...");
|
||||
new InstallTransaction(journalPath, journal, reporter).RollBack();
|
||||
}
|
||||
|
||||
private void RollBack()
|
||||
{
|
||||
if (_finished) return;
|
||||
ValidateJournal(InstallOperationPaths.NormalizeRoot(_journal.InstallRoot), _journal);
|
||||
|
||||
foreach (var entry in _journal.Entries.AsEnumerable().Reverse())
|
||||
RollBackEntry(entry);
|
||||
|
||||
if (_journal.RuntimeConfigMutationPlanned)
|
||||
{
|
||||
// The journal records the installation root, which is what decides whether this
|
||||
// installation's configuration lives in its portable root or in per-user data.
|
||||
RuntimeConfiguration.Restore(
|
||||
RuntimeConfiguration.ResolveConfigPath(_journal.InstallRoot),
|
||||
new RuntimeConfigSnapshot(_journal.RuntimeConfigExisted,
|
||||
_journal.RuntimeConfigContents ?? []));
|
||||
}
|
||||
|
||||
_journal.State = TransactionState.RolledBack;
|
||||
WriteJournal();
|
||||
_finished = true;
|
||||
FileSystemUtilities.DeleteDirectoryIfExists(_journal.WorkRoot);
|
||||
File.Delete(_journalPath);
|
||||
}
|
||||
|
||||
private static void RollBackEntry(TransactionJournalEntry entry)
|
||||
{
|
||||
var destinationExists = Exists(entry.DestinationPath, entry.Kind);
|
||||
if (Exists(entry.BackupPath, entry.Kind))
|
||||
{
|
||||
if (destinationExists) Delete(entry.DestinationPath, entry.Kind);
|
||||
Move(entry.BackupPath, entry.DestinationPath, entry.Kind);
|
||||
return;
|
||||
}
|
||||
|
||||
// No backup: either the original was never moved aside, or it has already been restored.
|
||||
// Both leave the original in place, so only a vanished original is a failure.
|
||||
if (entry.OriginalExisted)
|
||||
{
|
||||
if (!destinationExists)
|
||||
throw new IOException($"The transaction backup is missing for {entry.DestinationPath}.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (destinationExists && Exists(entry.PreparedPath, entry.Kind))
|
||||
throw new IOException(
|
||||
$"Both prepared and published paths exist for {entry.DestinationPath}; recovery is ambiguous.");
|
||||
if (destinationExists) Delete(entry.DestinationPath, entry.Kind);
|
||||
}
|
||||
|
||||
private static List<TransactionJournalEntry> NormalizeEntries(string root, string workRoot,
|
||||
IReadOnlyList<InstallTransactionEntry> entries)
|
||||
{
|
||||
var normalized = new List<TransactionJournalEntry>(entries.Count);
|
||||
for (var index = 0; index < entries.Count; index++)
|
||||
{
|
||||
var requested = entries[index];
|
||||
var prepared = Path.GetFullPath(requested.PreparedPath);
|
||||
var destination = Path.GetFullPath(requested.DestinationPath);
|
||||
EnsureDestinationInScope(root, destination);
|
||||
if (FileSystemUtilities.PathsOverlap(prepared, destination))
|
||||
throw new InvalidOperationException("A prepared path and its destination must not overlap.");
|
||||
|
||||
if (!Exists(prepared, requested.Kind))
|
||||
throw new FileNotFoundException("A prepared transaction entry is missing.", prepared);
|
||||
if (ExistsAsOtherKind(prepared, requested.Kind))
|
||||
throw new InvalidDataException($"The prepared path has the wrong entry type: {prepared}");
|
||||
if (ExistsAsOtherKind(destination, requested.Kind))
|
||||
throw new InvalidDataException($"The destination has the wrong entry type: {destination}");
|
||||
|
||||
foreach (var previous in normalized)
|
||||
{
|
||||
if (FileSystemUtilities.PathsOverlap(destination, previous.DestinationPath))
|
||||
throw new InvalidOperationException("Transaction destinations must not overlap.");
|
||||
if (FileSystemUtilities.PathsOverlap(prepared, previous.PreparedPath))
|
||||
throw new InvalidOperationException("Transaction prepared paths must not overlap.");
|
||||
if (FileSystemUtilities.PathsOverlap(prepared, previous.DestinationPath) ||
|
||||
FileSystemUtilities.PathsOverlap(destination, previous.PreparedPath))
|
||||
throw new InvalidOperationException("Transaction sources and destinations must not overlap.");
|
||||
}
|
||||
|
||||
normalized.Add(new TransactionJournalEntry
|
||||
{
|
||||
PreparedPath = prepared,
|
||||
DestinationPath = destination,
|
||||
BackupPath = Path.Combine(workRoot, "backup", index.ToString("D4")),
|
||||
Kind = requested.Kind,
|
||||
OriginalExisted = Exists(destination, requested.Kind)
|
||||
});
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static void ValidateJournal(string expectedRoot, TransactionJournal journal)
|
||||
{
|
||||
if (journal.SchemaVersion != SchemaVersion || journal.TransactionId == Guid.Empty)
|
||||
throw new InvalidDataException("The interrupted-install journal has an unsupported format.");
|
||||
|
||||
var root = InstallOperationPaths.NormalizeRoot(journal.InstallRoot);
|
||||
if (!root.Equals(expectedRoot, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException("The interrupted-install journal belongs to another installation.");
|
||||
|
||||
var expectedWorkRoot = InstallOperationPaths.GetWorkRoot(root, journal.TransactionId);
|
||||
if (!Path.GetFullPath(journal.WorkRoot).Equals(expectedWorkRoot, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException("The interrupted-install journal has an invalid work directory.");
|
||||
if (journal.Entries.Count == 0 || journal.Entries.Count > 64)
|
||||
throw new InvalidDataException("The interrupted-install journal has an invalid entry count.");
|
||||
|
||||
for (var index = 0; index < journal.Entries.Count; index++)
|
||||
{
|
||||
var entry = journal.Entries[index];
|
||||
EnsureDestinationInScope(root, Path.GetFullPath(entry.DestinationPath));
|
||||
var expectedBackup = Path.Combine(expectedWorkRoot, "backup", index.ToString("D4"));
|
||||
if (!Path.GetFullPath(entry.BackupPath).Equals(expectedBackup, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException("The interrupted-install journal has an invalid backup path.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureDestinationInScope(string root, string destination)
|
||||
{
|
||||
if (!FileSystemUtilities.PathContains(root, destination))
|
||||
throw new InvalidOperationException("A transaction destination must be inside its installation root.");
|
||||
}
|
||||
|
||||
private void EnsureActive(TransactionState expected)
|
||||
{
|
||||
if (_finished || _journal.State != expected)
|
||||
throw new InvalidOperationException("The install transaction is not in the required state.");
|
||||
}
|
||||
|
||||
private void WriteJournal() => JsonState.Write(_journalPath, _journal);
|
||||
|
||||
private static bool Exists(string path, InstallTransactionEntryKind kind) =>
|
||||
kind == InstallTransactionEntryKind.Directory ? Directory.Exists(path) : File.Exists(path);
|
||||
|
||||
private static bool ExistsAsOtherKind(string path, InstallTransactionEntryKind kind) =>
|
||||
kind == InstallTransactionEntryKind.Directory ? File.Exists(path) : Directory.Exists(path);
|
||||
|
||||
private static void Move(string source, string destination, InstallTransactionEntryKind kind)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
|
||||
if (kind == InstallTransactionEntryKind.Directory) Directory.Move(source, destination);
|
||||
else File.Move(source, destination);
|
||||
}
|
||||
|
||||
private static void Delete(string path, InstallTransactionEntryKind kind)
|
||||
{
|
||||
if (kind == InstallTransactionEntryKind.Directory) FileSystemUtilities.DeleteDirectoryIfExists(path);
|
||||
else File.Delete(path);
|
||||
}
|
||||
|
||||
private static void TryCleanCommitted(string journalPath, TransactionJournal journal,
|
||||
IInstallReporter? reporter)
|
||||
{
|
||||
try
|
||||
{
|
||||
FileSystemUtilities.DeleteDirectoryIfExists(journal.WorkRoot);
|
||||
File.Delete(journalPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
reporter?.Diagnostic(
|
||||
"The update committed successfully, but its exact previous-version backup could not yet be " +
|
||||
"removed: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TransactionJournal
|
||||
{
|
||||
public TransactionJournal() { }
|
||||
|
||||
public int SchemaVersion { get; set; }
|
||||
public Guid TransactionId { get; set; }
|
||||
public string InstallRoot { get; set; } = "";
|
||||
public string WorkRoot { get; set; } = "";
|
||||
public TransactionState State { get; set; }
|
||||
public List<TransactionJournalEntry> Entries { get; set; } = [];
|
||||
public bool RuntimeConfigMutationPlanned { get; set; }
|
||||
public bool RuntimeConfigExisted { get; set; }
|
||||
public byte[]? RuntimeConfigContents { get; set; }
|
||||
}
|
||||
|
||||
private sealed class TransactionJournalEntry
|
||||
{
|
||||
public TransactionJournalEntry() { }
|
||||
|
||||
public string PreparedPath { get; set; } = "";
|
||||
public string DestinationPath { get; set; } = "";
|
||||
public string BackupPath { get; set; } = "";
|
||||
public InstallTransactionEntryKind Kind { get; set; }
|
||||
public bool OriginalExisted { get; set; }
|
||||
}
|
||||
|
||||
private enum TransactionState
|
||||
{
|
||||
Prepared,
|
||||
Published,
|
||||
Committed,
|
||||
RolledBack
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
/// <summary>Provenance written by the bundled build script next to every product it produces.</summary>
|
||||
internal sealed class LocalBuildProvenance
|
||||
{
|
||||
public int SchemaVersion { get; set; }
|
||||
public string Profile { get; set; } = "";
|
||||
public string DolSha256 { get; set; } = "";
|
||||
public string RelSha256 { get; set; } = "";
|
||||
public string? CodePulSha256 { get; set; }
|
||||
public string? RetroWfcPayloadMode { get; set; }
|
||||
public string? RetroWfcPayloadSha256 { get; set; }
|
||||
public long? RetroWfcPayloadLength { get; set; }
|
||||
public string Compiler { get; set; } = "";
|
||||
|
||||
public const string FileName = "local-build.json";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The typed product status of the <c>--check-products</c> contract. These are the only nine answers
|
||||
/// the backend gives; see docs/WHEELWIZARD_CONTRACT.md. A frontend branches on the status alone.
|
||||
/// </summary>
|
||||
internal enum ProductStatus
|
||||
{
|
||||
Absent,
|
||||
|
||||
/// <summary>The binary matches the installed toolkit and the canonical compile inputs.</summary>
|
||||
Current,
|
||||
|
||||
/// <summary>The toolkit that produced the binary is no longer the installed toolkit.</summary>
|
||||
ToolkitChanged,
|
||||
|
||||
/// <summary>The canonical Code.pul is not the one this binary statically embeds.</summary>
|
||||
CodePulChanged,
|
||||
|
||||
/// <summary>Another translator-consumed Retro Rewind input changed.</summary>
|
||||
CompileInputsChanged,
|
||||
|
||||
/// <summary>Only the cached Retro-WFC payload needs repair; no compilation.</summary>
|
||||
PayloadChanged,
|
||||
|
||||
/// <summary>
|
||||
/// The canonical Retro Rewind installation is missing or structurally invalid, so the product
|
||||
/// can neither be trusted nor rebuilt until the frontend supplies it.
|
||||
/// </summary>
|
||||
InputsMissing,
|
||||
|
||||
/// <summary>
|
||||
/// The product exists, but the installation cannot prove its provenance or provide the required
|
||||
/// toolkit. A caller must repair the installation before launching it.
|
||||
/// </summary>
|
||||
Blocked,
|
||||
|
||||
/// <summary>The binary or the support files it needs are missing or unusable.</summary>
|
||||
Broken
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The result of a non-mutating product inspection. Status and detail are exactly what the wire
|
||||
/// carries: nothing is derived that is not also reported.
|
||||
/// </summary>
|
||||
internal sealed record ProductState(ProductStatus Status, string Detail)
|
||||
{
|
||||
/// <summary>The stable machine-readable status string of the command-line contract.</summary>
|
||||
public string Reason => Status switch
|
||||
{
|
||||
ProductStatus.Absent => "absent",
|
||||
ProductStatus.Current => "current",
|
||||
ProductStatus.ToolkitChanged => "toolkit-changed",
|
||||
ProductStatus.CodePulChanged => "code-pul-changed",
|
||||
ProductStatus.CompileInputsChanged => "compile-inputs-changed",
|
||||
ProductStatus.PayloadChanged => "payload-changed",
|
||||
ProductStatus.InputsMissing => "inputs-missing",
|
||||
ProductStatus.Blocked => "blocked",
|
||||
_ => "broken"
|
||||
};
|
||||
|
||||
/// <summary>Whether a caller must do something before this product may be launched.</summary>
|
||||
public bool ActionRequired => Status is not ProductStatus.Current and not ProductStatus.Absent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The on-disk shape of an installation, plus the cheap checks a launch performs before starting a
|
||||
/// binary. Everything here is O(1), a single file hash, or the compile-input identity of the
|
||||
/// canonical Retro Rewind installation; the multi-gigabyte asset tree is never walked.
|
||||
/// </summary>
|
||||
internal sealed class Installation
|
||||
{
|
||||
public Installation(string root) => Root = FileSystemUtilities.NormalizePath(root);
|
||||
|
||||
public string Root { get; }
|
||||
public string ToolkitDirectory => InstalledLayout.Toolkit(Root);
|
||||
public string WorkspaceDirectory => InstalledLayout.Workspace(Root);
|
||||
public string WorkspaceAssetsDirectory => Path.Combine(WorkspaceDirectory, "Assets");
|
||||
public string WorkspaceRetroWfcPayload => Path.Combine(WorkspaceAssetsDirectory, "OfflinePayload");
|
||||
public string BaseDirectory => Path.Combine(Root, "Base");
|
||||
public string BaseExecutable => Path.Combine(BaseDirectory, "WiiCompiled.exe");
|
||||
public string RetroDirectory => Path.Combine(Root, "RetroRewind");
|
||||
public string RetroExecutable => Path.Combine(RetroDirectory, "RetroRewind.exe");
|
||||
public string GameDataDirectory => Path.Combine(Root, "GameAssets", "DATA");
|
||||
public string InstallStatePath => Path.Combine(Root, InstalledLayout.InstallStateFileName);
|
||||
public string ToolkitStatePath => Path.Combine(Root, ToolkitState.FileName);
|
||||
|
||||
/// <summary>The portable root this installation lives in, or null for an ordinary installation.</summary>
|
||||
public string? PortableRootDirectory => PortableRoot.TryFind(Root);
|
||||
|
||||
/// <summary>
|
||||
/// The runtime configuration this installation reads: <c><portable root>\UserData\Config.toml</c>
|
||||
/// when portable, otherwise the shared per-user file.
|
||||
/// </summary>
|
||||
public string ConfigPath => RuntimeConfiguration.ResolveConfigPath(Root);
|
||||
|
||||
/// <summary>The install directory copy of setup that acts as the CLI launcher.</summary>
|
||||
public string SetupCopyPath => Path.Combine(Root, ProductInfo.SetupCopyName);
|
||||
|
||||
private const string Toolkit = InstalledLayout.ToolkitDirectoryName + "\\";
|
||||
private const string Workspace = InstalledLayout.WorkspaceDirectoryName + "\\";
|
||||
|
||||
private static readonly string[] RequiredToolkitFiles =
|
||||
[
|
||||
Toolkit + @"Translator\Translator.Cli.exe",
|
||||
Toolkit + @"CMake\bin\cmake.exe",
|
||||
Toolkit + @"Ninja\ninja.exe",
|
||||
Toolkit + @"llvm-mingw\bin\clang-22.exe",
|
||||
Toolkit + @"llvm-mingw\bin\ld.lld.exe",
|
||||
Toolkit + @"llvm-mingw\bin\x86_64-w64-mingw32-clang.exe",
|
||||
Toolkit + @"llvm-mingw\bin\x86_64-w64-mingw32-clang++.exe",
|
||||
Toolkit + @"llvm-mingw\bin\x86_64-w64-mingw32-windres.exe",
|
||||
Workspace + "LocalBuild.ps1",
|
||||
Workspace + "NativeBuildFlags.ps1",
|
||||
Workspace + @"projects\mkwii\recomp.yml",
|
||||
Workspace + @"runtime\CMakeLists.txt",
|
||||
Workspace + @"runtime\assets\dsp\dsp_coef.bin"
|
||||
];
|
||||
|
||||
// These are the source trees and pinned dependency roots LocalBuild.ps1 consumes during every
|
||||
// native configure/build. Presence is intentionally a cheap structural test, not another full
|
||||
// toolkit hash on every status check. The dependency names come from the one shipped list, so a
|
||||
// dependency added to the payload cannot be missed here.
|
||||
private static readonly string[] RequiredNonEmptyToolkitDirectories =
|
||||
[
|
||||
Workspace + @"runtime\src",
|
||||
Workspace + @"runtime\assets\wii",
|
||||
Workspace + "aurora-main",
|
||||
.. InstalledLayout.DependencyNames.Select(name => Workspace + @"Dependencies\" + name)
|
||||
];
|
||||
|
||||
public bool Exists => File.Exists(InstallStatePath);
|
||||
|
||||
/// <summary>
|
||||
/// Whether this directory holds an installation at all. A missing or empty directory is not a
|
||||
/// broken installation, it is simply nothing - a frontend probing a path it has not installed to
|
||||
/// yet must get "absent", not an error.
|
||||
/// </summary>
|
||||
public bool IsPresent => Directory.Exists(Root) &&
|
||||
(Exists || File.Exists(BaseExecutable) || File.Exists(RetroExecutable));
|
||||
/// <summary>
|
||||
/// A cheap structural check for the programs and workspace inputs LocalBuild.ps1 actually
|
||||
/// invokes. Product inspection intentionally does not rehash the full toolkit every time, but
|
||||
/// it must not claim a product current when its compiler, linker, or native runtime is gone.
|
||||
/// </summary>
|
||||
public bool HasToolkit => RequiredToolkitFiles.All(relative => File.Exists(Path.Combine(Root, relative))) &&
|
||||
RequiredNonEmptyToolkitDirectories.All(relative =>
|
||||
IsNonEmptyRegularDirectory(Path.Combine(Root, relative)));
|
||||
public bool HasRetroProduct => File.Exists(RetroExecutable);
|
||||
|
||||
public InstallState? ReadInstallState() => JsonState.TryRead<InstallState>(InstallStatePath);
|
||||
|
||||
internal ToolkitState? ReadToolkitState() => JsonState.TryRead<ToolkitState>(ToolkitStatePath);
|
||||
|
||||
/// <summary>
|
||||
/// The identity of the installed toolkit. Inspection never writes or adopts state: a missing
|
||||
/// toolkit provenance record must not silently make an existing product look trustworthy.
|
||||
/// </summary>
|
||||
public string ResolveToolkitFingerprint()
|
||||
{
|
||||
var state = ReadToolkitState();
|
||||
if (state is { SchemaVersion: 2 } && !string.IsNullOrEmpty(state.ToolkitFingerprint) &&
|
||||
!string.IsNullOrEmpty(state.ToolkitPackageFingerprint) && HasToolkit)
|
||||
return state.ToolkitFingerprint;
|
||||
|
||||
// Do not derive a replacement identity from whatever happens to be on disk. A missing or
|
||||
// unsupported toolkit-state.json has no authoritative chain to the installed products and
|
||||
// is therefore blocked until setup repairs it.
|
||||
return "";
|
||||
}
|
||||
|
||||
/// <summary>The recorded package identity of the installed Toolkit directory, under the same
|
||||
/// fail-closed rules as <see cref="ResolveToolkitFingerprint"/>.</summary>
|
||||
public string ResolveToolkitPackageFingerprint()
|
||||
{
|
||||
var state = ReadToolkitState();
|
||||
if (state is { SchemaVersion: 2 } && !string.IsNullOrEmpty(state.ToolkitFingerprint) &&
|
||||
!string.IsNullOrEmpty(state.ToolkitPackageFingerprint) && HasToolkit)
|
||||
return state.ToolkitPackageFingerprint;
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public void WriteToolkitState(string fingerprint, string packageFingerprint, string releaseTag,
|
||||
string runtimeAssetsFingerprint = "", string translationFingerprint = "",
|
||||
string nativeToolchainFingerprint = "") =>
|
||||
JsonState.Write(ToolkitStatePath, new ToolkitState
|
||||
{
|
||||
ToolkitFingerprint = fingerprint,
|
||||
ToolkitPackageFingerprint = packageFingerprint,
|
||||
RuntimeAssetsFingerprint = runtimeAssetsFingerprint,
|
||||
TranslationFingerprint = translationFingerprint,
|
||||
NativeToolchainFingerprint = nativeToolchainFingerprint,
|
||||
ToolkitReleaseTag = releaseTag
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Reads the complete product provenance. A product without the current fingerprint schema is
|
||||
/// intentionally untrusted rather than guessed from an older, partial provenance document.
|
||||
/// </summary>
|
||||
public ProductFingerprint? ReadProductFingerprint(string productDirectory, string toolkitFingerprint)
|
||||
{
|
||||
var recorded = JsonState.TryRead<ProductFingerprint>(
|
||||
Path.Combine(productDirectory, ProductFingerprint.FileName));
|
||||
return recorded is { SchemaVersion: 1 } ? recorded : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The canonical Retro Rewind install to compare against: explicit <c>--retro-dir</c> if given, else the
|
||||
/// recorded <c>retro_rewind_root</c>. Reads only compile inputs, so it stays cheap for every status check and launch.
|
||||
/// </summary>
|
||||
internal RetroRewindCompileInputs? ResolveCanonicalCompileInputs(string? explicitDirectory,
|
||||
out string? error, CancellationToken cancellationToken = default)
|
||||
{
|
||||
error = null;
|
||||
var selected = string.IsNullOrWhiteSpace(explicitDirectory)
|
||||
? ConfiguredRetroRewindRoot
|
||||
: explicitDirectory;
|
||||
if (string.IsNullOrWhiteSpace(selected))
|
||||
{
|
||||
if (HasRetroProduct || ReadInstallState()?.RetroRewindInstalled == true)
|
||||
error = "No canonical Retro Rewind folder is recorded for this installation. " +
|
||||
"Repair it with Wheel Wizard's current Retro Rewind folder.";
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return CompileInputsFingerprint.Compute(selected, cancellationToken);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException
|
||||
or ArgumentException)
|
||||
{
|
||||
error = "The canonical Retro Rewind folder cannot be read: " + ex.Message;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The canonical Retro Rewind root the runtime reads its assets from, if one is set.</summary>
|
||||
public string? ConfiguredRetroRewindRoot => RuntimeConfiguration.GetRetroRewindRoot(ConfigPath);
|
||||
|
||||
public ProductState CheckBase(string toolkitFingerprint)
|
||||
{
|
||||
var state = CheckBaseCore(toolkitFingerprint);
|
||||
if (state.Status != ProductStatus.Current) return state;
|
||||
var runtimeAssetsError = ValidateCopiedRuntimeAssets(BaseDirectory, "base", toolkitFingerprint);
|
||||
return runtimeAssetsError is null ? state : new ProductState(ProductStatus.Broken, runtimeAssetsError);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Everything about the base product except its copied support files. Repair inspects those
|
||||
/// separately because they are republished from the installed workspace without recompiling.
|
||||
/// </summary>
|
||||
internal ProductState CheckBaseCore(string toolkitFingerprint)
|
||||
{
|
||||
if (!IsUsableExecutable(BaseExecutable))
|
||||
return new ProductState(ProductStatus.Broken, "The base recomp executable is missing.");
|
||||
if (!HasUsableToolkit(toolkitFingerprint))
|
||||
return MissingToolkit();
|
||||
var installState = ReadCurrentInstallState();
|
||||
if (installState is null)
|
||||
return new ProductState(ProductStatus.Blocked,
|
||||
"The installation state is missing, unsupported, or belongs to another directory.");
|
||||
var fingerprint = ReadProductFingerprint(BaseDirectory, toolkitFingerprint);
|
||||
if (fingerprint is null)
|
||||
return new ProductState(ProductStatus.Blocked,
|
||||
"The base recomp has no current build provenance. Repair it with the current setup.");
|
||||
if (!fingerprint.ToolkitFingerprint.Equals(toolkitFingerprint, StringComparison.Ordinal))
|
||||
return new ProductState(ProductStatus.ToolkitChanged,
|
||||
"The base recomp was produced by a different recompilation toolkit.");
|
||||
var provenanceError = ValidateProductProvenance(BaseDirectory, "base", fingerprint, installState);
|
||||
return provenanceError is null
|
||||
? new ProductState(ProductStatus.Current, "")
|
||||
: new ProductState(ProductStatus.Blocked, provenanceError);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Classifies the Retro Rewind product against the canonical installation's compile inputs. The
|
||||
/// asset tree is never inspected: the runtime reads it live, so an asset-only Retro Rewind
|
||||
/// update leaves this product current.
|
||||
/// </summary>
|
||||
public ProductState CheckRetroRewind(string toolkitFingerprint,
|
||||
RetroRewindCompileInputs? canonical, string? canonicalError = null,
|
||||
bool? cachedRetroWfcPayloadMatches = null)
|
||||
{
|
||||
var state = CheckRetroRewindCore(toolkitFingerprint, canonical, canonicalError,
|
||||
cachedRetroWfcPayloadMatches);
|
||||
if (state.Status != ProductStatus.Current) return state;
|
||||
var runtimeAssetsError = ValidateCopiedRuntimeAssets(RetroDirectory, "retro-rewind",
|
||||
toolkitFingerprint);
|
||||
return runtimeAssetsError is null ? state : new ProductState(ProductStatus.Broken, runtimeAssetsError);
|
||||
}
|
||||
|
||||
internal ProductState CheckRetroRewindCore(string toolkitFingerprint,
|
||||
RetroRewindCompileInputs? canonical, string? canonicalError = null,
|
||||
bool? cachedRetroWfcPayloadMatches = null)
|
||||
{
|
||||
var installState = ReadCurrentInstallState();
|
||||
var recordedAsInstalled = installState?.RetroRewindInstalled == true;
|
||||
if ((HasRetroProduct || recordedAsInstalled) && !HasUsableToolkit(toolkitFingerprint))
|
||||
return MissingToolkit();
|
||||
|
||||
if (!HasRetroProduct)
|
||||
{
|
||||
return recordedAsInstalled
|
||||
? new ProductState(ProductStatus.Broken,
|
||||
"Retro Rewind is recorded as installed, but its executable is missing.")
|
||||
: new ProductState(ProductStatus.Absent, "Retro Rewind is not installed.");
|
||||
}
|
||||
|
||||
if (canonicalError is not null)
|
||||
return new ProductState(ProductStatus.InputsMissing, canonicalError);
|
||||
|
||||
var fingerprint = ReadProductFingerprint(RetroDirectory, toolkitFingerprint);
|
||||
if (fingerprint is null)
|
||||
return new ProductState(ProductStatus.Blocked,
|
||||
"The Retro Rewind product has no current build provenance. " +
|
||||
"Repair it with Wheel Wizard's current Retro Rewind folder.");
|
||||
if (!fingerprint.ToolkitFingerprint.Equals(toolkitFingerprint, StringComparison.Ordinal))
|
||||
return new ProductState(ProductStatus.ToolkitChanged,
|
||||
"Retro Rewind was produced by a different recompilation toolkit.");
|
||||
if (installState is null)
|
||||
return new ProductState(ProductStatus.Blocked,
|
||||
"The installation state is missing, unsupported, or belongs to another directory.");
|
||||
|
||||
var provenanceError = ValidateProductProvenance(RetroDirectory, "retro-rewind", fingerprint,
|
||||
installState);
|
||||
if (provenanceError is not null)
|
||||
return new ProductState(ProductStatus.Blocked, provenanceError);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fingerprint.CodePulSha256) ||
|
||||
string.IsNullOrWhiteSpace(fingerprint.RetroRewindCompileInputsSha256) ||
|
||||
string.IsNullOrWhiteSpace(installState.RetroRewindCodePulSha256) ||
|
||||
string.IsNullOrWhiteSpace(installState.RetroRewindCompileInputsSha256) ||
|
||||
!Same(installState.RetroRewindCodePulSha256, fingerprint.CodePulSha256) ||
|
||||
!Same(installState.RetroRewindCompileInputsSha256,
|
||||
fingerprint.RetroRewindCompileInputsSha256))
|
||||
return new ProductState(ProductStatus.Blocked,
|
||||
"Retro Rewind's compile-input provenance is incomplete or inconsistent. " +
|
||||
"Repair it with Wheel Wizard's current Retro Rewind folder.");
|
||||
|
||||
if (canonical is not null)
|
||||
{
|
||||
if (!Same(canonical.CodePulSha256, fingerprint.CodePulSha256))
|
||||
return new ProductState(ProductStatus.CodePulChanged,
|
||||
"The canonical Retro Rewind Code.pul changed since WiiCompiled was translated.");
|
||||
if (!Same(canonical.CompileInputsSha256, fingerprint.RetroRewindCompileInputsSha256))
|
||||
return new ProductState(ProductStatus.CompileInputsChanged,
|
||||
"Translator-consumed Retro Rewind inputs changed since WiiCompiled was built.");
|
||||
}
|
||||
|
||||
if (fingerprint.RetroWfcPayloadMode == "downloaded" &&
|
||||
CheckRetroWfcPayloadCache(fingerprint, installState, cachedRetroWfcPayloadMatches)
|
||||
is { } payloadError)
|
||||
return payloadError;
|
||||
|
||||
return new ProductState(ProductStatus.Current, "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the workspace payload cache with the identity this product embeds.
|
||||
/// <paramref name="cachedMatches"/> carries an observation the operation already paid for:
|
||||
/// true/false skip the hash, null means nothing is known yet.
|
||||
/// </summary>
|
||||
private ProductState? CheckRetroWfcPayloadCache(ProductFingerprint fingerprint, InstallState installState,
|
||||
bool? cachedMatches)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fingerprint.RetroWfcPayloadSha256) ||
|
||||
fingerprint.RetroWfcPayloadLength <= 0 ||
|
||||
string.IsNullOrWhiteSpace(installState.RetroWfcPayloadSha256) ||
|
||||
installState.RetroWfcPayloadLength <= 0)
|
||||
return new ProductState(ProductStatus.Blocked,
|
||||
"The Retro-WFC payload provenance is incomplete.");
|
||||
if (cachedMatches == false)
|
||||
return new ProductState(ProductStatus.PayloadChanged,
|
||||
"The cached Retro-WFC payload is missing or does not match the current snapshot.");
|
||||
if (cachedMatches == true) return null;
|
||||
|
||||
try
|
||||
{
|
||||
var payload = InputValidation.ResolveRetroWfcPayloadFile(WorkspaceRetroWfcPayload);
|
||||
if (!InputValidation.Sha256File(payload).Equals(fingerprint.RetroWfcPayloadSha256,
|
||||
StringComparison.OrdinalIgnoreCase) ||
|
||||
new FileInfo(payload).Length != fingerprint.RetroWfcPayloadLength)
|
||||
return new ProductState(ProductStatus.PayloadChanged,
|
||||
"The cached Retro-WFC payload is not the one this product was built with.");
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException)
|
||||
{
|
||||
return new ProductState(ProductStatus.PayloadChanged,
|
||||
"The cached Retro-WFC payload is missing or invalid: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool Same(string left, string right) =>
|
||||
left.Equals(right, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
internal bool ProductUsesGameInputs(string productDirectory, string toolkitFingerprint,
|
||||
string expectedDolSha256, string expectedRelSha256)
|
||||
{
|
||||
var fingerprint = ReadProductFingerprint(productDirectory, toolkitFingerprint);
|
||||
return fingerprint is not null &&
|
||||
fingerprint.DolSha256.Equals(expectedDolSha256, StringComparison.OrdinalIgnoreCase) &&
|
||||
fingerprint.RelSha256.Equals(expectedRelSha256, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private bool HasUsableToolkit(string toolkitFingerprint) =>
|
||||
HasToolkit && !string.IsNullOrWhiteSpace(toolkitFingerprint);
|
||||
|
||||
private static ProductState MissingToolkit() =>
|
||||
new(ProductStatus.Blocked,
|
||||
"The recompilation toolkit is missing or incomplete. Repair the setup before launching or updating products.");
|
||||
|
||||
private InstallState? ReadCurrentInstallState()
|
||||
{
|
||||
var state = ReadInstallState();
|
||||
if (state is not { SchemaVersion: 1 } || string.IsNullOrWhiteSpace(state.InstallDir)) return null;
|
||||
try
|
||||
{
|
||||
return FileSystemUtilities.PathsEqual(state.InstallDir, Root) ? state : null;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
private string? ValidateProductProvenance(string productDirectory, string expectedProfile,
|
||||
ProductFingerprint fingerprint, InstallState state)
|
||||
{
|
||||
if (!fingerprint.Profile.Equals(expectedProfile, StringComparison.Ordinal) ||
|
||||
string.IsNullOrWhiteSpace(fingerprint.DolSha256) ||
|
||||
string.IsNullOrWhiteSpace(fingerprint.RelSha256) ||
|
||||
string.IsNullOrWhiteSpace(fingerprint.ExecutableSha256))
|
||||
return $"The {expectedProfile} product provenance is incomplete.";
|
||||
|
||||
var executable = expectedProfile == "base" ? BaseExecutable : RetroExecutable;
|
||||
if (!IsUsableExecutable(executable))
|
||||
return $"The {expectedProfile} executable is missing or incomplete.";
|
||||
try
|
||||
{
|
||||
if (!InputValidation.Sha256File(executable).Equals(fingerprint.ExecutableSha256,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
return $"The {expectedProfile} executable no longer matches its recorded build identity.";
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
return $"The {expectedProfile} executable cannot be verified: {ex.Message}";
|
||||
}
|
||||
|
||||
var dol = Path.Combine(GameDataDirectory, "sys", "main.dol");
|
||||
var rel = Path.Combine(GameDataDirectory, "files", "rel", "StaticR.rel");
|
||||
var fst = Path.Combine(GameDataDirectory, "sys", "fst.bin");
|
||||
if (!File.Exists(dol) || !File.Exists(rel) || !File.Exists(fst))
|
||||
return "The installed game assets are incomplete. Apply setup again with the disc image.";
|
||||
try
|
||||
{
|
||||
var dolSha = InputValidation.Sha256File(dol);
|
||||
var relSha = InputValidation.Sha256File(rel);
|
||||
if (!dolSha.Equals(fingerprint.DolSha256, StringComparison.OrdinalIgnoreCase) ||
|
||||
!relSha.Equals(fingerprint.RelSha256, StringComparison.OrdinalIgnoreCase) ||
|
||||
!dolSha.Equals(state.DolSha256, StringComparison.OrdinalIgnoreCase) ||
|
||||
!relSha.Equals(state.RelSha256, StringComparison.OrdinalIgnoreCase))
|
||||
return "The installed game inputs do not match the product provenance.";
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
return "The installed game inputs cannot be verified: " + ex.Message;
|
||||
}
|
||||
|
||||
var local = JsonState.TryRead<LocalBuildProvenance>(
|
||||
Path.Combine(productDirectory, LocalBuildProvenance.FileName));
|
||||
if (local is not { SchemaVersion: 1 } ||
|
||||
!local.Profile.Equals(expectedProfile, StringComparison.Ordinal) ||
|
||||
!local.DolSha256.Equals(fingerprint.DolSha256, StringComparison.OrdinalIgnoreCase) ||
|
||||
!local.RelSha256.Equals(fingerprint.RelSha256, StringComparison.OrdinalIgnoreCase))
|
||||
return $"The {expectedProfile} executable has invalid local-build provenance.";
|
||||
if (expectedProfile == "retro-rewind" &&
|
||||
(!string.Equals(local.RetroWfcPayloadMode, fingerprint.RetroWfcPayloadMode,
|
||||
StringComparison.Ordinal) ||
|
||||
!string.Equals(local.RetroWfcPayloadSha256 ?? "", fingerprint.RetroWfcPayloadSha256,
|
||||
StringComparison.OrdinalIgnoreCase) ||
|
||||
(local.RetroWfcPayloadLength ?? 0) != fingerprint.RetroWfcPayloadLength))
|
||||
{
|
||||
return "The Retro Rewind executable has invalid Retro-WFC payload provenance.";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime support files are copied beside products rather than embedded in the executable.
|
||||
/// The installed toolkit-state is their authoritative release identity; products with missing
|
||||
/// or altered copies cannot be declared launchable merely because their .exe still hashes.
|
||||
/// </summary>
|
||||
internal string? ValidateCopiedRuntimeAssets(string productDirectory, string expectedProfile,
|
||||
string toolkitFingerprint)
|
||||
{
|
||||
var toolkitState = ReadToolkitState();
|
||||
if (toolkitState is not { SchemaVersion: 2 } ||
|
||||
!string.Equals(toolkitState.ToolkitFingerprint, toolkitFingerprint, StringComparison.Ordinal) ||
|
||||
string.IsNullOrWhiteSpace(toolkitState.RuntimeAssetsFingerprint))
|
||||
{
|
||||
return "The installed runtime-asset provenance is missing or does not belong to the current toolkit.";
|
||||
}
|
||||
|
||||
string? failure = null;
|
||||
if (!ToolkitFingerprint.ProductRuntimeAssetsMatch(productDirectory,
|
||||
toolkitState.RuntimeAssetsFingerprint, diagnostic: line => failure = line))
|
||||
{
|
||||
// An access-denied or half-deleted tree must not masquerade as an ordinary hash mismatch.
|
||||
return failure ??
|
||||
$"The {expectedProfile} product's copied runtime assets are missing, corrupt, or stale.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsUsableExecutable(string path)
|
||||
{
|
||||
try { return File.Exists(path) && new FileInfo(path).Length >= 64 * 1024; }
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { return false; }
|
||||
}
|
||||
|
||||
private static bool IsNonEmptyRegularDirectory(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(path)) return false;
|
||||
var directory = new DirectoryInfo(path);
|
||||
if ((directory.Attributes & FileAttributes.ReparsePoint) != 0) return false;
|
||||
using var entries = Directory.EnumerateFileSystemEntries(path).GetEnumerator();
|
||||
return entries.MoveNext();
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The Retro-WFC payload choice this installation was built with.</summary>
|
||||
public RetroWfcPayloadMode ResolveRetroWfcPayloadMode(string toolkitFingerprint)
|
||||
{
|
||||
var recorded = ReadProductFingerprint(RetroDirectory, toolkitFingerprint)?.RetroWfcPayloadMode;
|
||||
recorded = string.IsNullOrEmpty(recorded) ? ReadInstallState()?.RetroWfcPayloadMode : recorded;
|
||||
return recorded switch
|
||||
{
|
||||
"downloaded" => RetroWfcPayloadMode.Online,
|
||||
"skipped" => RetroWfcPayloadMode.Skipped,
|
||||
_ => RetroWfcPayloadMode.NotApplicable
|
||||
};
|
||||
}
|
||||
|
||||
public bool MatchesRetroWfcPayloadSnapshot(string toolkitFingerprint,
|
||||
RetroWfcPayloadSnapshot snapshot)
|
||||
{
|
||||
var fingerprint = ReadProductFingerprint(RetroDirectory, toolkitFingerprint);
|
||||
var state = ReadCurrentInstallState();
|
||||
return fingerprint is { RetroWfcPayloadMode: "downloaded" } &&
|
||||
state is { RetroWfcPayloadMode: "downloaded" } &&
|
||||
string.Equals(fingerprint.RetroWfcPayloadSha256, snapshot.Sha256, StringComparison.Ordinal) &&
|
||||
string.Equals(state.RetroWfcPayloadSha256, snapshot.Sha256, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public bool CachedRetroWfcPayloadMatches(RetroWfcPayloadSnapshot snapshot)
|
||||
{
|
||||
try
|
||||
{
|
||||
var payload = InputValidation.ResolveRetroWfcPayloadFile(WorkspaceRetroWfcPayload);
|
||||
return new FileInfo(payload).Length == snapshot.ByteLength &&
|
||||
InputValidation.Sha256File(payload).Equals(snapshot.Sha256, StringComparison.Ordinal);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
/// <summary>
|
||||
/// Names of the installed/staged layout. Not cosmetic: payload and toolkit identities hash relative paths
|
||||
/// that begin with these strings, so changing one (including casing) invalidates every shipped fingerprint.
|
||||
/// Build-Installer.ps1 stages the payload with exactly these names.
|
||||
/// </summary>
|
||||
internal static class InstalledLayout
|
||||
{
|
||||
public const string ToolkitDirectoryName = "Toolkit";
|
||||
public const string WorkspaceDirectoryName = "BuildWorkspace";
|
||||
public const string InstallStateFileName = "install-state.json";
|
||||
public const string PayloadManifestFileName = "payload-manifest.json";
|
||||
|
||||
/// <summary>Payload entry names use forward slashes; the archive is written by tar.</summary>
|
||||
public const string ToolkitEntryPrefix = ToolkitDirectoryName + "/";
|
||||
public const string WorkspaceEntryPrefix = WorkspaceDirectoryName + "/";
|
||||
|
||||
public static string Toolkit(string root) => Path.Combine(root, ToolkitDirectoryName);
|
||||
public static string Workspace(string root) => Path.Combine(root, WorkspaceDirectoryName);
|
||||
|
||||
/// <summary>
|
||||
/// The pinned offline dependency sources shipped under <c>BuildWorkspace\Dependencies</c>. This
|
||||
/// is the one list: Build-Installer.ps1 stages exactly these directories and
|
||||
/// Launcher/Test-PinnedFacts.ps1 fails the release build if its copy and this one disagree.
|
||||
/// </summary>
|
||||
public static readonly string[] DependencyNames =
|
||||
[
|
||||
"abseil-cpp", "cppwinrt", "dawn_prebuilt", "fmt", "freetype", "imgui", "native_prebuilt",
|
||||
"png", "SDL", "sqlite3", "tracy", "xxhash", "zlib", "zstd"
|
||||
];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Source-owned files copied verbatim beside every product. One inventory drives identity
|
||||
/// (<see cref="ToolkitFingerprint.ComputeRuntimeAssets"/>), verification, and publication. Renaming
|
||||
/// an identity key below requires bumping the fingerprint's version salt.
|
||||
/// </summary>
|
||||
internal static class ProductRuntimeAssets
|
||||
{
|
||||
/// <summary>The bootstrap tree, copied from <c>runtime/assets/wii</c> into every product.</summary>
|
||||
public const string SourceBootstrapDirectoryName = "wii";
|
||||
public const string ProductBootstrapDirectoryName = "wii_bootstrap";
|
||||
|
||||
/// <summary>Single files, given as their path below <c>runtime/assets</c> and their product name.</summary>
|
||||
public static readonly (string[] SourceRelativePath, string ProductFileName)[] Files =
|
||||
[
|
||||
(["dsp", "dsp_coef.bin"], "dsp_coef.bin"),
|
||||
(["pipeline", "initial_pipeline_cache.db"], "initial_pipeline_cache.db")
|
||||
];
|
||||
|
||||
public static string SourceFile(string sourceAssets, string[] relativePath) =>
|
||||
Path.Combine([sourceAssets, .. relativePath]);
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
internal sealed class InstallerEngine
|
||||
{
|
||||
private readonly IInstallReporter _reporter;
|
||||
|
||||
public InstallerEngine(IInstallReporter reporter) => _reporter = reporter;
|
||||
|
||||
public async Task InstallAsync(InstallOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var installDirectory = Path.GetFullPath(options.InstallDirectory);
|
||||
ValidateInstallDirectory(installDirectory);
|
||||
|
||||
InputValidation.ValidateExtension(options.GamePath);
|
||||
var parent = Directory.GetParent(installDirectory)?.FullName
|
||||
?? throw new InvalidOperationException("The installation directory has no parent.");
|
||||
Directory.CreateDirectory(parent);
|
||||
|
||||
using var operationLock = InstallOperationLock.Acquire(installDirectory, _reporter);
|
||||
var existing = new Installation(installDirectory);
|
||||
|
||||
PortableInstallHealing.HealMovedInstall(existing, _reporter);
|
||||
var previousState = existing.ReadInstallState();
|
||||
using var scratch = Directory.Exists(installDirectory)
|
||||
? InstallScratchSpace.CreateInsideInstall(installDirectory, _reporter)
|
||||
: InstallScratchSpace.CreateSibling(installDirectory, _reporter);
|
||||
var staging = scratch.Root;
|
||||
|
||||
|
||||
const long payloadStagingAllowance = 2L * 1024 * 1024 * 1024;
|
||||
FileSystemUtilities.EnsureFreeSpace(installDirectory, payloadStagingAllowance, "Setup");
|
||||
|
||||
using var payload = PayloadArchive.OpenCurrent();
|
||||
var manifest = payload.ReadManifest();
|
||||
var toolkit = InstalledLayout.Toolkit(staging);
|
||||
var workspace = InstalledLayout.Workspace(staging);
|
||||
|
||||
var candidateToolkitFingerprint = manifest.ToolkitFingerprint;
|
||||
var candidateToolkitPackageFingerprint = manifest.ToolkitPackageFingerprint;
|
||||
var candidateRuntimeAssetsFingerprint = manifest.RuntimeAssetsFingerprint;
|
||||
var installedToolkitFingerprint = existing.IsPresent
|
||||
? existing.ResolveToolkitFingerprint()
|
||||
: "";
|
||||
var installedToolkitPackageFingerprint = existing.IsPresent
|
||||
? existing.ResolveToolkitPackageFingerprint()
|
||||
: "";
|
||||
var reusableGameAssets = FindReusableGameAssets(existing, manifest);
|
||||
|
||||
var sameToolkit = existing.IsPresent && reusableGameAssets is not null &&
|
||||
installedToolkitFingerprint.Equals(candidateToolkitFingerprint,
|
||||
StringComparison.Ordinal);
|
||||
var samePackageContent = existing.IsPresent &&
|
||||
installedToolkitPackageFingerprint.Equals(
|
||||
candidateToolkitPackageFingerprint, StringComparison.Ordinal);
|
||||
var sameToolkitPackage = sameToolkit && samePackageContent;
|
||||
var hadRetro = existing.HasRetroProduct || previousState?.RetroRewindInstalled == true;
|
||||
|
||||
var runtimeAssetsCurrent = sameToolkit && RuntimeAssetsAreCurrent(existing,
|
||||
candidateRuntimeAssetsFingerprint, cancellationToken);
|
||||
var installedDolphinTool = Path.Combine(existing.ToolkitDirectory, "DolphinTool.exe");
|
||||
var extractToolkit = MustRefreshToolkit(sameToolkit, samePackageContent,
|
||||
File.Exists(installedDolphinTool));
|
||||
var extractWorkspace = !sameToolkit || !runtimeAssetsCurrent;
|
||||
|
||||
_reporter.Progress(InstallStages.ExtractToolkit,
|
||||
"Inspecting the release's local recompilation toolkit...", 1);
|
||||
if (extractToolkit) payload.ExtractDirectory(InstalledLayout.ToolkitDirectoryName, toolkit);
|
||||
if (extractWorkspace)
|
||||
{
|
||||
payload.ExtractDirectory(InstalledLayout.WorkspaceDirectoryName, workspace);
|
||||
WorkspaceTimestamps.MarkChangedFiles(existing.WorkspaceDirectory, workspace,
|
||||
_reporter.Diagnostic, cancellationToken);
|
||||
}
|
||||
payload.ExtractEntry("host/WiiCompiled-Setup.exe",
|
||||
Path.Combine(staging, ProductInfo.SetupCopyName));
|
||||
payload.ExtractDirectory("licenses", Path.Combine(staging, "licenses"));
|
||||
payload.ExtractEntry(InstalledLayout.PayloadManifestFileName,
|
||||
Path.Combine(staging, InstalledLayout.PayloadManifestFileName));
|
||||
|
||||
var dolphinTool = extractToolkit ? Path.Combine(toolkit, "DolphinTool.exe") : installedDolphinTool;
|
||||
_reporter.Progress(InstallStages.Validate, "Checking the Wii disc image...", 2);
|
||||
var header = await InputValidation.ReadDiscHeaderAsync(dolphinTool, options.GamePath,
|
||||
cancellationToken);
|
||||
InputValidation.EnsureCompatibleDisc(header, manifest);
|
||||
var canonicalRetroRoot = options.RetroDirectoryPath is null
|
||||
? null
|
||||
: RetroRewindSource.ResolveRetroRewind6(options.RetroDirectoryPath);
|
||||
ValidateRetroOptions(canonicalRetroRoot, options.RetroWfcPayloadMode, manifest);
|
||||
|
||||
var retroCompileInputs = canonicalRetroRoot is null
|
||||
? null
|
||||
: SnapshotRetroRewindCompileInputs(canonicalRetroRoot,
|
||||
Path.Combine(staging, "compile-inputs"), cancellationToken);
|
||||
if (options.Portable)
|
||||
{
|
||||
var portableRoot = PortableRoot.Create(parent);
|
||||
_reporter.Diagnostic($"Installing as a portable installation under {portableRoot}.");
|
||||
}
|
||||
|
||||
if (sameToolkit)
|
||||
{
|
||||
var desiredInputDrift = ProductRepairService.FindDesiredInputDrift(existing,
|
||||
candidateToolkitFingerprint, manifest.ExpectedDolSha256, manifest.ExpectedRelSha256);
|
||||
if (desiredInputDrift.Retro && retroCompileInputs is null)
|
||||
throw new InvalidOperationException(
|
||||
"This release changes the clean game inputs used by the installed Retro Rewind product. " +
|
||||
"Supply Wheel Wizard's current Retro Rewind folder explicitly so it can be rebuilt safely.");
|
||||
|
||||
var reconciliation = await ReconcileSameToolkitAsync(existing, retroCompileInputs,
|
||||
canonicalRetroRoot, options.RetroWfcPayloadMode, Path.Combine(staging, "repair"),
|
||||
manifest.ExpectedDolSha256, manifest.ExpectedRelSha256, cancellationToken);
|
||||
var remainingCancellation = reconciliation.PublicationCommitted
|
||||
? CancellationToken.None
|
||||
: cancellationToken;
|
||||
|
||||
|
||||
if (!runtimeAssetsCurrent)
|
||||
runtimeAssetsCurrent = RuntimeAssetsAreCurrent(existing,
|
||||
candidateRuntimeAssetsFingerprint, remainingCancellation);
|
||||
|
||||
remainingCancellation.ThrowIfCancellationRequested();
|
||||
EnsureReconciliationCurrent(existing, candidateToolkitFingerprint, retroCompileInputs,
|
||||
manifest.ExpectedDolSha256, manifest.ExpectedRelSha256,
|
||||
reconciliation.CachedRetroWfcPayloadMatches);
|
||||
|
||||
var updatedState = BuildInstallState(existing.ReadInstallState(), installDirectory, manifest,
|
||||
retroCompileInputs, canonicalRetroRoot, options.RetroWfcPayloadMode);
|
||||
PrepareMetadata(staging, manifest, updatedState);
|
||||
remainingCancellation.ThrowIfCancellationRequested();
|
||||
|
||||
var releaseEntries = new List<InstallTransactionEntry>();
|
||||
if (!sameToolkitPackage)
|
||||
AddComponent(releaseEntries, staging, installDirectory,
|
||||
InstalledLayout.ToolkitDirectoryName);
|
||||
if (!runtimeAssetsCurrent)
|
||||
AddRuntimeAssetPublicationEntries(releaseEntries, staging, installDirectory,
|
||||
updatedState.RetroRewindInstalled, candidateRuntimeAssetsFingerprint,
|
||||
remainingCancellation);
|
||||
Publish(staging, installDirectory, canonicalRetroRoot, updatedState,
|
||||
releaseEntries, remainingCancellation);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hadRetro && retroCompileInputs is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The installed Retro Rewind product must be produced again. Supply Wheel Wizard's " +
|
||||
"current Retro Rewind folder explicitly so it can be rebuilt safely.");
|
||||
}
|
||||
|
||||
EnsureSufficientRemainingDiskSpace(installDirectory,
|
||||
needsExtractedDisc: reusableGameAssets is null, needsLocalBuild: true);
|
||||
|
||||
if (reusableGameAssets is null)
|
||||
{
|
||||
await ExtractGameAssetsAsync(dolphinTool, options.GamePath,
|
||||
Path.Combine(staging, "GameAssets"), manifest, cancellationToken);
|
||||
}
|
||||
|
||||
await PublishToolkitAndReconcileProductsAsync(existing, staging, workspace, manifest,
|
||||
previousState, options, canonicalRetroRoot, retroCompileInputs,
|
||||
publishGameAssets: reusableGameAssets is null, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether this operation must replace the installed <c>Toolkit</c>. The shipped translator and translation
|
||||
/// project are one unit, so <c>samePackageContent</c> alone isn't enough: <c>sameToolkit</c> tracks the compile
|
||||
/// identity separately, since gating on package identity alone let the two disagree after the v0.2.5 healing salt bump.
|
||||
/// </summary>
|
||||
internal static bool MustRefreshToolkit(bool sameToolkit, bool samePackageContent,
|
||||
bool dolphinToolPresent) =>
|
||||
!sameToolkit || !samePackageContent || !dolphinToolPresent;
|
||||
|
||||
private static void AddComponent(List<InstallTransactionEntry> entries, string staging,
|
||||
string installDirectory, string name) =>
|
||||
entries.Add(InstallTransactionEntry.Directory(Path.Combine(staging, name),
|
||||
Path.Combine(installDirectory, name)));
|
||||
|
||||
/// <summary>
|
||||
/// Build-produced workspace directories (not shipped in a release) grafted from the installed workspace into
|
||||
/// the staged one, carrying the base translation, native build dir, verified inputs, and Retro-WFC payload cache
|
||||
/// across a toolkit update. Each is re-validated by the build, so stale caches degrade to a clean rebuild, never reuse.
|
||||
/// </summary>
|
||||
private static readonly string[] WorkspaceCacheDirectories =
|
||||
["generated", "build", "native-build", "Assets", "PulsarPacks"];
|
||||
|
||||
private async Task PublishToolkitAndReconcileProductsAsync(Installation existing, string staging,
|
||||
string stagedWorkspace, PayloadManifest manifest, InstallState? previousState,
|
||||
InstallOptions options, string? canonicalRetroRoot,
|
||||
RetroRewindCompileInputs? retroCompileInputs,
|
||||
bool publishGameAssets, CancellationToken cancellationToken)
|
||||
{
|
||||
var installDirectory = existing.Root;
|
||||
|
||||
|
||||
foreach (var cacheDirectory in WorkspaceCacheDirectories)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var source = Path.Combine(existing.WorkspaceDirectory, cacheDirectory);
|
||||
if (!Directory.Exists(source)) continue;
|
||||
var destination = Path.Combine(stagedWorkspace, cacheDirectory);
|
||||
if (Directory.Exists(destination)) Directory.Delete(destination, recursive: true);
|
||||
Directory.Move(source, destination);
|
||||
}
|
||||
|
||||
|
||||
var state = BuildInstallState(previousState, installDirectory, manifest,
|
||||
compileInputs: null, canonicalRetroRoot, options.RetroWfcPayloadMode);
|
||||
PrepareMetadata(staging, manifest, state);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var entries = new List<InstallTransactionEntry>();
|
||||
|
||||
AddComponent(entries, staging, installDirectory, InstalledLayout.ToolkitDirectoryName);
|
||||
AddComponent(entries, staging, installDirectory, InstalledLayout.WorkspaceDirectoryName);
|
||||
if (publishGameAssets) AddComponent(entries, staging, installDirectory, "GameAssets");
|
||||
|
||||
Publish(staging, installDirectory, canonicalRetroRoot, state,
|
||||
entries, cancellationToken, progressPercent: 8, completionPercent: 10);
|
||||
|
||||
_reporter.Progress(InstallStages.BuildBase,
|
||||
"Producing the installed products with the published toolkit...", 11);
|
||||
|
||||
var reconciliation = await ReconcileSameToolkitAsync(existing, retroCompileInputs,
|
||||
canonicalRetroRoot, options.RetroWfcPayloadMode, repairScratch: null,
|
||||
manifest.ExpectedDolSha256, manifest.ExpectedRelSha256, cancellationToken);
|
||||
EnsureReconciliationCurrent(existing, manifest.ToolkitFingerprint, retroCompileInputs,
|
||||
manifest.ExpectedDolSha256, manifest.ExpectedRelSha256,
|
||||
reconciliation.CachedRetroWfcPayloadMatches);
|
||||
}
|
||||
|
||||
private Task<ProductRepairService.ReconciliationResult> ReconcileSameToolkitAsync(Installation existing,
|
||||
RetroRewindCompileInputs? retroCompileInputs, string? canonicalRetroRoot,
|
||||
RetroWfcPayloadMode payloadMode, string? repairScratch, string expectedDolSha256,
|
||||
string expectedRelSha256, CancellationToken cancellationToken)
|
||||
{
|
||||
var repair = new ProductRepairService(existing, _reporter);
|
||||
var options = new ProductRepairService.ReconcileOptions
|
||||
{
|
||||
ScratchRoot = repairScratch,
|
||||
ExpectedDolSha256 = expectedDolSha256,
|
||||
ExpectedRelSha256 = expectedRelSha256,
|
||||
CanonicalRetroRewindRoot = canonicalRetroRoot
|
||||
};
|
||||
|
||||
return retroCompileInputs is null
|
||||
? repair.RepairBaseAsync(options, cancellationToken)
|
||||
: repair.RepairRetroAsync(retroCompileInputs, payloadMode, options, cancellationToken);
|
||||
}
|
||||
|
||||
private static void EnsureReconciliationCurrent(Installation installation, string toolkitFingerprint,
|
||||
RetroRewindCompileInputs? retroCompileInputs, string expectedDolSha256, string expectedRelSha256,
|
||||
bool? cachedRetroWfcPayloadMatches)
|
||||
{
|
||||
var baseState = installation.CheckBase(toolkitFingerprint);
|
||||
var retroState = installation.CheckRetroRewind(toolkitFingerprint, retroCompileInputs,
|
||||
canonicalError: null, cachedRetroWfcPayloadMatches);
|
||||
var desiredInputsCurrent = installation.ProductUsesGameInputs(installation.BaseDirectory,
|
||||
toolkitFingerprint, expectedDolSha256, expectedRelSha256) &&
|
||||
(!installation.HasRetroProduct ||
|
||||
installation.ProductUsesGameInputs(installation.RetroDirectory,
|
||||
toolkitFingerprint, expectedDolSha256, expectedRelSha256));
|
||||
if (baseState.Status == ProductStatus.Current && !retroState.ActionRequired && desiredInputsCurrent) return;
|
||||
|
||||
var details = string.Join(" ", new[] { baseState, retroState }
|
||||
.Where(state => state.ActionRequired)
|
||||
.Select(state => state.Detail)
|
||||
.Where(detail => !string.IsNullOrWhiteSpace(detail)));
|
||||
throw new InvalidOperationException(
|
||||
"Product reconciliation did not produce a current installation with the release's game inputs; " +
|
||||
"release metadata was not advanced." +
|
||||
(string.IsNullOrWhiteSpace(details) ? "" : " " + details));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes the caller's component entries together with the release metadata every install and
|
||||
/// update advances, then applies the runtime configuration this installation owns.
|
||||
/// </summary>
|
||||
private void Publish(string staging, string installDirectory,
|
||||
string? canonicalRetroRoot, InstallState state,
|
||||
List<InstallTransactionEntry> entries, CancellationToken cancellationToken,
|
||||
int progressPercent = 95, int completionPercent = 99)
|
||||
{
|
||||
entries.Add(InstallTransactionEntry.Directory(Path.Combine(staging, "licenses"),
|
||||
Path.Combine(installDirectory, "licenses")));
|
||||
entries.Add(InstallTransactionEntry.File(Path.Combine(staging, ProductInfo.SetupCopyName),
|
||||
Path.Combine(installDirectory, ProductInfo.SetupCopyName)));
|
||||
entries.Add(InstallTransactionEntry.File(
|
||||
Path.Combine(staging, InstalledLayout.PayloadManifestFileName),
|
||||
Path.Combine(installDirectory, InstalledLayout.PayloadManifestFileName)));
|
||||
entries.Add(InstallTransactionEntry.File(Path.Combine(staging, ToolkitState.FileName),
|
||||
Path.Combine(installDirectory, ToolkitState.FileName)));
|
||||
entries.Add(InstallTransactionEntry.File(
|
||||
Path.Combine(staging, InstalledLayout.InstallStateFileName),
|
||||
Path.Combine(installDirectory, InstalledLayout.InstallStateFileName)));
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
_reporter.Progress(InstallStages.Publish, "Publishing the completed installation...", progressPercent);
|
||||
var configPath = RuntimeConfiguration.ResolveConfigPath(installDirectory);
|
||||
var configSnapshot = RuntimeConfiguration.Capture(configPath);
|
||||
using var transaction = InstallTransaction.Begin(installDirectory, _reporter, entries.ToArray());
|
||||
transaction.Publish();
|
||||
transaction.RecordRuntimeConfigurationMutation(configSnapshot);
|
||||
RuntimeConfiguration.SetDvdRoot(configPath, Path.Combine(installDirectory, "GameAssets", "DATA"));
|
||||
// The runtime scans this directory live for its asset overlay, so it is recorded on every
|
||||
// operation that receives one and is never rewritten by a base-only operation.
|
||||
if (canonicalRetroRoot is not null)
|
||||
RuntimeConfiguration.SetRetroRewindRoot(configPath, canonicalRetroRoot);
|
||||
transaction.Commit();
|
||||
|
||||
// A portable installation is owned by the folder it lives in, not by this machine. Adding a
|
||||
// machine-wide uninstall entry for it would outlive the folder and, worse, would overwrite
|
||||
// the entry of a normal installation on the same account.
|
||||
if (PortableRoot.TryFind(installDirectory) is not null)
|
||||
{
|
||||
_reporter.Diagnostic(
|
||||
"Portable installation: no Windows uninstall entry was registered. Remove the folder, " +
|
||||
"or run the copied setup with --uninstall, to uninstall it.");
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
ShellIntegration.RegisterUninstaller(installDirectory, state.RetroRewindInstalled);
|
||||
ShellIntegration.RemoveAllShortcuts();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_reporter.Diagnostic("The installation succeeded, but Windows uninstall registration failed: " +
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
_reporter.Progress(InstallStages.Publish, "Installation complete.", completionPercent);
|
||||
}
|
||||
|
||||
private static void PrepareMetadata(string staging, PayloadManifest manifest, InstallState state)
|
||||
{
|
||||
JsonState.Write(Path.Combine(staging, ToolkitState.FileName), new ToolkitState
|
||||
{
|
||||
ToolkitFingerprint = manifest.ToolkitFingerprint,
|
||||
ToolkitPackageFingerprint = manifest.ToolkitPackageFingerprint,
|
||||
RuntimeAssetsFingerprint = manifest.RuntimeAssetsFingerprint,
|
||||
TranslationFingerprint = manifest.TranslationFingerprint,
|
||||
NativeToolchainFingerprint = manifest.NativeToolchainFingerprint,
|
||||
ToolkitReleaseTag = manifest.ToolkitReleaseTag
|
||||
});
|
||||
JsonState.Write(Path.Combine(staging, InstalledLayout.InstallStateFileName), state);
|
||||
}
|
||||
|
||||
private static InstallState BuildInstallState(InstallState? previousState, string installDirectory,
|
||||
PayloadManifest manifest, RetroRewindCompileInputs? compileInputs, string? canonicalRetroRoot,
|
||||
RetroWfcPayloadMode payloadMode,
|
||||
RetroWfcPayloadSnapshot? retroWfcPayloadSnapshot = null)
|
||||
{
|
||||
// Preserve the pre-update object for runtime-configuration rollback/ownership decisions.
|
||||
var state = new InstallState
|
||||
{
|
||||
SchemaVersion = 1,
|
||||
SetupVersion = ProductInfo.Version,
|
||||
ProductVersion = manifest.ProductVersion,
|
||||
InstallDir = installDirectory,
|
||||
InstalledUtc = previousState?.InstalledUtc ?? DateTime.UtcNow,
|
||||
RetroRewindInstalled = previousState?.RetroRewindInstalled ?? false,
|
||||
ToolkitReleaseTag = manifest.ToolkitReleaseTag,
|
||||
DolSha256 = manifest.ExpectedDolSha256,
|
||||
RelSha256 = manifest.ExpectedRelSha256,
|
||||
RetroRewindCodePulSha256 = previousState?.RetroRewindCodePulSha256 ?? "",
|
||||
RetroRewindCompileInputsSha256 = previousState?.RetroRewindCompileInputsSha256 ?? "",
|
||||
RetroWfcPayloadMode = previousState?.RetroWfcPayloadMode ?? "",
|
||||
RetroWfcPayloadSha256 = previousState?.RetroWfcPayloadSha256 ?? "",
|
||||
RetroWfcPayloadLength = previousState?.RetroWfcPayloadLength ?? 0,
|
||||
RetroRewindRoot = canonicalRetroRoot is null
|
||||
? previousState?.RetroRewindRoot ?? ""
|
||||
: Path.GetFullPath(canonicalRetroRoot)
|
||||
};
|
||||
if (compileInputs is not null)
|
||||
{
|
||||
state.RetroRewindInstalled = true;
|
||||
state.RetroRewindCodePulSha256 = compileInputs.CodePulSha256;
|
||||
state.RetroRewindCompileInputsSha256 = compileInputs.CompileInputsSha256;
|
||||
state.RetroWfcPayloadMode = payloadMode == RetroWfcPayloadMode.Online
|
||||
? "downloaded"
|
||||
: "skipped";
|
||||
state.RetroWfcPayloadSha256 = payloadMode == RetroWfcPayloadMode.Online
|
||||
? retroWfcPayloadSnapshot?.Sha256 ?? state.RetroWfcPayloadSha256
|
||||
: "";
|
||||
state.RetroWfcPayloadLength = payloadMode == RetroWfcPayloadMode.Online
|
||||
? retroWfcPayloadSnapshot?.ByteLength ?? state.RetroWfcPayloadLength
|
||||
: 0;
|
||||
if (payloadMode == RetroWfcPayloadMode.Online &&
|
||||
(string.IsNullOrWhiteSpace(state.RetroWfcPayloadSha256) || state.RetroWfcPayloadLength <= 0))
|
||||
throw new InvalidDataException("The downloaded Retro-WFC payload snapshot is missing.");
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the canonical installation's compile inputs into operation-owned staging. Only
|
||||
/// Code.pul and the bundled Pulsar sources are copied; the asset tree stays where Wheel Wizard
|
||||
/// owns it and is read live by the runtime through <c>retro_rewind_root</c>.
|
||||
/// </summary>
|
||||
private RetroRewindCompileInputs SnapshotRetroRewindCompileInputs(string canonicalRetroRoot,
|
||||
string destination, CancellationToken cancellationToken)
|
||||
{
|
||||
_reporter.Progress(InstallStages.Validate, "Snapshotting the Retro Rewind compile inputs...", 3);
|
||||
return CompileInputsFingerprint.Snapshot(canonicalRetroRoot, destination, cancellationToken);
|
||||
}
|
||||
|
||||
private bool RuntimeAssetsAreCurrent(Installation installation, string expectedFingerprint,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var installedSource = ToolkitFingerprint.TryComputeRuntimeAssets(installation.Root, cancellationToken,
|
||||
_reporter.Diagnostic);
|
||||
if (!expectedFingerprint.Equals(installedSource, StringComparison.Ordinal)) return false;
|
||||
if (File.Exists(installation.BaseExecutable) &&
|
||||
!ToolkitFingerprint.ProductRuntimeAssetsMatch(installation.BaseDirectory, expectedFingerprint,
|
||||
cancellationToken, _reporter.Diagnostic))
|
||||
return false;
|
||||
return !installation.HasRetroProduct ||
|
||||
ToolkitFingerprint.ProductRuntimeAssetsMatch(installation.RetroDirectory,
|
||||
expectedFingerprint, cancellationToken, _reporter.Diagnostic);
|
||||
}
|
||||
|
||||
private void AddRuntimeAssetPublicationEntries(List<InstallTransactionEntry> entries,
|
||||
string staging, string installDirectory, bool includeRetro, string expectedFingerprint,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// The identity was computed from this exact operation-owned payload extraction, so the
|
||||
// staged assets are published as they are; only repair has an installed source to re-verify.
|
||||
var sourceAssets = Path.Combine(InstalledLayout.Workspace(staging), "runtime", "assets");
|
||||
var products = (includeRetro ? new[] { "Base", "RetroRewind" } : new[] { "Base" })
|
||||
.Select(product => (product, Path.Combine(installDirectory, product)));
|
||||
RuntimeAssetPublication.AddEntries(entries, sourceAssets,
|
||||
Path.Combine(staging, "runtime-asset-publication"), products, expectedFingerprint,
|
||||
"publication", _reporter.Diagnostic, cancellationToken);
|
||||
|
||||
entries.Add(InstallTransactionEntry.Directory(sourceAssets,
|
||||
Path.Combine(InstalledLayout.Workspace(installDirectory), "runtime", "assets")));
|
||||
}
|
||||
|
||||
private static void ValidateRetroOptions(string? canonicalRetroRoot, RetroWfcPayloadMode mode,
|
||||
PayloadManifest manifest)
|
||||
{
|
||||
if (canonicalRetroRoot is null && mode != RetroWfcPayloadMode.NotApplicable)
|
||||
throw new InvalidOperationException(
|
||||
"A Retro-WFC payload option requires the canonical Retro Rewind folder.");
|
||||
if (canonicalRetroRoot is not null && mode == RetroWfcPayloadMode.NotApplicable)
|
||||
throw new InvalidOperationException(
|
||||
"Download the Retro-WFC payload, or explicitly skip the optional payload.");
|
||||
if (canonicalRetroRoot is not null && mode == RetroWfcPayloadMode.Online)
|
||||
InputValidation.ValidateRetroWfcPayloadUri(manifest.RetroWfcPayloadUri);
|
||||
}
|
||||
|
||||
private string? FindReusableGameAssets(Installation existing, PayloadManifest manifest)
|
||||
{
|
||||
var dataRoot = existing.GameDataDirectory;
|
||||
// No fst.bin simply means nothing was extracted here before; anything past that gate is an
|
||||
// existing extraction that fails reuse, which the log must say before the expensive
|
||||
// re-extraction quietly repairs it.
|
||||
if (!File.Exists(Path.Combine(dataRoot, "sys", "fst.bin"))) return null;
|
||||
try
|
||||
{
|
||||
ValidateExtractedGame(dataRoot, manifest);
|
||||
return dataRoot;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException)
|
||||
{
|
||||
_reporter.Diagnostic(
|
||||
"The installed game assets cannot be reused; the disc will be extracted again: " + ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExtractGameAssetsAsync(string dolphinTool, string gamePath, string destination,
|
||||
PayloadManifest manifest, CancellationToken cancellationToken)
|
||||
{
|
||||
_reporter.Progress(InstallStages.ExtractDisc,
|
||||
"Extracting the game disc. This is the longest preparation step...", 6);
|
||||
var extraction = await ProcessRunner.RunAsync(dolphinTool,
|
||||
["extract", "-i", Path.GetFullPath(gamePath), "-o", destination, "-g", "-q"],
|
||||
line => { if (!string.IsNullOrWhiteSpace(line)) _reporter.Diagnostic(line); },
|
||||
cancellationToken);
|
||||
if (extraction.ExitCode != 0)
|
||||
throw new InvalidDataException("Game extraction failed. " + extraction.CombinedOutput.Trim());
|
||||
ValidateExtractedGame(Path.Combine(destination, "DATA"), manifest);
|
||||
}
|
||||
|
||||
private static void ValidateExtractedGame(string dataRoot, PayloadManifest manifest)
|
||||
{
|
||||
var dol = Path.Combine(dataRoot, "sys", "main.dol");
|
||||
var rel = Path.Combine(dataRoot, "files", "rel", "StaticR.rel");
|
||||
var fst = Path.Combine(dataRoot, "sys", "fst.bin");
|
||||
if (!File.Exists(dol) || !File.Exists(rel) || !File.Exists(fst))
|
||||
throw new InvalidDataException("The disc extraction is missing required Mario Kart Wii files.");
|
||||
var dolHash = InputValidation.Sha256File(dol);
|
||||
var relHash = InputValidation.Sha256File(rel);
|
||||
if (!dolHash.Equals(manifest.ExpectedDolSha256, StringComparison.OrdinalIgnoreCase) ||
|
||||
!relHash.Equals(manifest.ExpectedRelSha256, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"The disc is RMCP01 but does not match the supported clean PAL revision. " +
|
||||
"Patched or otherwise modified game code cannot be installed safely.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateInstallDirectory(string path) =>
|
||||
FileSystemUtilities.EnsureUsableLocation(path, "The installation folder");
|
||||
|
||||
private static void EnsureSufficientRemainingDiskSpace(string installDirectory,
|
||||
bool needsExtractedDisc, bool needsLocalBuild)
|
||||
{
|
||||
const long extractedDiscAllowance = 5L * 1024 * 1024 * 1024;
|
||||
const long localBuildAllowance = 14L * 1024 * 1024 * 1024;
|
||||
const long safetyAllowance = 2L * 1024 * 1024 * 1024;
|
||||
// This preflight runs after the release payload and the compile-input snapshot have already
|
||||
// been staged. Count only work that can still allocate space from this point.
|
||||
var required = checked((needsExtractedDisc ? extractedDiscAllowance : 0) +
|
||||
(needsLocalBuild ? localBuildAllowance : 0) + safetyAllowance);
|
||||
|
||||
FileSystemUtilities.EnsureFreeSpace(installDirectory, required, "Setup");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
/// <summary>Reads and atomically writes the small JSON state documents kept inside an installation.</summary>
|
||||
internal static class JsonState
|
||||
{
|
||||
private static readonly JsonSerializerOptions ReadOptions = new() { PropertyNameCaseInsensitive = true };
|
||||
private static readonly JsonSerializerOptions WriteOptions = new() { WriteIndented = true };
|
||||
|
||||
public static T? TryRead<T>(string path) where T : class
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.Exists(path) ? JsonSerializer.Deserialize<T>(File.ReadAllText(path), ReadOptions) : null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// A truncated or hand-edited state document must degrade into "unknown", which every
|
||||
// caller already treats as "assume stale and rebuild", not into a failed launch.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Write<T>(string path, T value) =>
|
||||
FileSystemUtilities.WriteAtomic(path, JsonSerializer.Serialize(value, WriteOptions));
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Both"/> runs one retro-aware translation and compiles the two products from a single
|
||||
/// build graph, sharing every profile-neutral shard object. Building the legs separately re-emits
|
||||
/// the shared base shards with retro-aware content in the second leg and recompiles all of them.
|
||||
/// </summary>
|
||||
internal enum BuildProfile { Base, RetroRewind, Both }
|
||||
|
||||
internal sealed class LocalBuildService
|
||||
{
|
||||
private readonly IInstallReporter _reporter;
|
||||
|
||||
public LocalBuildService(IInstallReporter reporter) => _reporter = reporter;
|
||||
|
||||
/// <summary>
|
||||
/// Runs the bundled translate-and-compile script against <paramref name="installStaging"/>, which
|
||||
/// holds a complete <c>Toolkit</c> and <c>BuildWorkspace</c> pair - either an installation being
|
||||
/// staged or an existing installation being repaired in place.
|
||||
/// </summary>
|
||||
public async Task BuildAsync(string installStaging, BuildProfile profile, string outputDirectory,
|
||||
RetroWfcPayloadMode retroWfcPayloadMode, string? retroWfcOfflinePayloadDirectory,
|
||||
CancellationToken cancellationToken, ToolkitFingerprintComponents? toolkitComponents = null,
|
||||
bool forceCleanBuild = false, BuildProgressWindow? progress = null,
|
||||
string? retroRewindPackageDirectory = null, string? baseOutputDirectory = null)
|
||||
{
|
||||
var workspace = InstalledLayout.Workspace(installStaging);
|
||||
var toolkit = InstalledLayout.Toolkit(installStaging);
|
||||
var script = Path.Combine(workspace, "LocalBuild.ps1");
|
||||
if (!File.Exists(script)) throw new FileNotFoundException("The local build script is missing.", script);
|
||||
|
||||
var buildsRetro = profile is BuildProfile.RetroRewind or BuildProfile.Both;
|
||||
if (!buildsRetro && retroWfcPayloadMode != RetroWfcPayloadMode.NotApplicable)
|
||||
throw new ArgumentException("The base build cannot select a Retro-WFC payload.");
|
||||
if (buildsRetro && retroWfcPayloadMode == RetroWfcPayloadMode.NotApplicable)
|
||||
throw new ArgumentException("The Retro Rewind build must select or explicitly skip the Retro-WFC payload.");
|
||||
if (retroRewindPackageDirectory is not null && !buildsRetro)
|
||||
throw new ArgumentException("Only Retro Rewind can select an explicit package snapshot.");
|
||||
if ((profile == BuildProfile.Both) != (baseOutputDirectory is not null))
|
||||
throw new ArgumentException("A combined build takes exactly one base output directory.");
|
||||
if (retroRewindPackageDirectory is not null)
|
||||
retroRewindPackageDirectory = RetroRewindSource.ResolveRetroRewind6(
|
||||
retroRewindPackageDirectory);
|
||||
if (retroWfcPayloadMode == RetroWfcPayloadMode.Online)
|
||||
retroWfcOfflinePayloadDirectory =
|
||||
InputValidation.ValidateStagedRetroWfcPayloadDirectory(retroWfcOfflinePayloadDirectory!);
|
||||
|
||||
var arguments = new List<string>
|
||||
{
|
||||
"-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script,
|
||||
"-Workspace", workspace, "-Toolkit", toolkit,
|
||||
"-Profile", profile switch
|
||||
{
|
||||
BuildProfile.Base => "base",
|
||||
BuildProfile.RetroRewind => "retro-rewind",
|
||||
_ => "both"
|
||||
},
|
||||
"-OutputDirectory", outputDirectory
|
||||
};
|
||||
if (baseOutputDirectory is not null)
|
||||
{
|
||||
arguments.Add("-BaseOutputDirectory");
|
||||
arguments.Add(baseOutputDirectory);
|
||||
}
|
||||
// The script is the single owner of every cache-reuse decision; these identities are the
|
||||
// evidence it compares against the provenance it recorded with the caches themselves.
|
||||
// Without them (or with -ForceCleanBuild) it degrades to a full clean translate and build.
|
||||
if (toolkitComponents is not null)
|
||||
{
|
||||
arguments.Add("-TranslationFingerprint");
|
||||
arguments.Add(toolkitComponents.Translation);
|
||||
arguments.Add("-NativeToolchainFingerprint");
|
||||
arguments.Add(toolkitComponents.NativeToolchain);
|
||||
}
|
||||
if (forceCleanBuild)
|
||||
arguments.Add("-ForceCleanBuild");
|
||||
if (retroRewindPackageDirectory is not null)
|
||||
{
|
||||
arguments.Add("-RetroRewindPackageDirectory");
|
||||
arguments.Add(retroRewindPackageDirectory);
|
||||
}
|
||||
if (retroWfcPayloadMode == RetroWfcPayloadMode.Online)
|
||||
{
|
||||
arguments.Add("-RetroWfcOfflineDirectory");
|
||||
arguments.Add(retroWfcOfflinePayloadDirectory!);
|
||||
arguments.Add("-RetroWfcPayloadOrigin");
|
||||
arguments.Add("downloaded");
|
||||
}
|
||||
else if (retroWfcPayloadMode == RetroWfcPayloadMode.Skipped)
|
||||
{
|
||||
arguments.Add("-SkipRetroWfcPayload");
|
||||
}
|
||||
// Scrubs the ambient VS environment so the bundled clang-mingw toolchain is the only one visible.
|
||||
// The actual search path is set by LocalBuild.ps1's one canonical definition (Get-MkwToolchainPath,
|
||||
// shared with Prepare-NativePrebuilt.ps1), not duplicated here, to avoid drift.
|
||||
void ScrubEnvironment(ProcessStartInfo start)
|
||||
{
|
||||
start.WorkingDirectory = workspace;
|
||||
foreach (var inherited in new[]
|
||||
{ "INCLUDE", "LIB", "LIBPATH", "VSINSTALLDIR", "VCToolsInstallDir", "WindowsSdkDir" })
|
||||
start.Environment.Remove(inherited);
|
||||
}
|
||||
|
||||
void Observe(string line)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line)) return;
|
||||
if (progress is not null) progress.Observe(line);
|
||||
else _reporter.Diagnostic(line);
|
||||
}
|
||||
|
||||
var build = await ProcessRunner.RunAsync(
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System),
|
||||
"WindowsPowerShell", "v1.0", "powershell.exe"),
|
||||
arguments, Observe, cancellationToken, ScrubEnvironment, capture: false,
|
||||
ex => _reporter.Diagnostic(
|
||||
"The cancelled build process could not be terminated immediately: " + ex.Message));
|
||||
if (build.ExitCode != 0)
|
||||
throw new InvalidOperationException($"Local recompilation failed with exit code {build.ExitCode}. See the setup log for the failed translator or compiler command.");
|
||||
|
||||
var expectedOutputs = profile switch
|
||||
{
|
||||
BuildProfile.Base => [(outputDirectory, "WiiCompiled.exe")],
|
||||
BuildProfile.RetroRewind => [(outputDirectory, "RetroRewind.exe")],
|
||||
_ => new[] { (baseOutputDirectory!, "WiiCompiled.exe"), (outputDirectory, "RetroRewind.exe") }
|
||||
};
|
||||
foreach (var (directory, executableName) in expectedOutputs)
|
||||
{
|
||||
var executable = Path.Combine(directory, executableName);
|
||||
if (!File.Exists(executable) || new FileInfo(executable).Length < 64 * 1024)
|
||||
throw new InvalidDataException("The local build completed without producing a valid game executable.");
|
||||
if (!File.Exists(Path.Combine(directory, "local-build.json")))
|
||||
throw new InvalidDataException("The local build did not produce provenance information.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records what this product was built from: <paramref name="toolkitFingerprint"/> ties it to the
|
||||
/// translator/workspace, <paramref name="codePulSha256"/> to Retro Rewind's embedded Code.pul, so a
|
||||
/// later launch or update can tell whether the binary is still correct.
|
||||
/// </summary>
|
||||
public static void WriteFingerprint(string outputDirectory, BuildProfile profile, string toolkitFingerprint,
|
||||
string dolSha256, string relSha256, string codePulSha256, RetroWfcPayloadMode retroWfcPayloadMode,
|
||||
string retroRewindCompileInputsSha256 = "", string retroWfcPayloadSha256 = "",
|
||||
long retroWfcPayloadLength = 0)
|
||||
{
|
||||
var executable = Path.Combine(outputDirectory,
|
||||
profile == BuildProfile.Base ? "WiiCompiled.exe" : "RetroRewind.exe");
|
||||
if (!File.Exists(executable))
|
||||
throw new FileNotFoundException("Cannot record build provenance because the compiled executable is missing.",
|
||||
executable);
|
||||
if (profile == BuildProfile.RetroRewind)
|
||||
{
|
||||
var local = JsonState.TryRead<LocalBuildProvenance>(
|
||||
Path.Combine(outputDirectory, LocalBuildProvenance.FileName));
|
||||
var expectedMode = retroWfcPayloadMode == RetroWfcPayloadMode.Online
|
||||
? "downloaded"
|
||||
: "skipped";
|
||||
if (local is not { SchemaVersion: 1 } ||
|
||||
!string.Equals(local.DolSha256, dolSha256, StringComparison.OrdinalIgnoreCase) ||
|
||||
!string.Equals(local.RelSha256, relSha256, StringComparison.OrdinalIgnoreCase) ||
|
||||
!string.Equals(local.CodePulSha256, codePulSha256,
|
||||
StringComparison.OrdinalIgnoreCase) ||
|
||||
!string.Equals(local.RetroWfcPayloadMode, expectedMode, StringComparison.Ordinal) ||
|
||||
!string.Equals(local.RetroWfcPayloadSha256,
|
||||
retroWfcPayloadMode == RetroWfcPayloadMode.Online ? retroWfcPayloadSha256 : null,
|
||||
StringComparison.OrdinalIgnoreCase) ||
|
||||
(local.RetroWfcPayloadLength ?? 0) !=
|
||||
(retroWfcPayloadMode == RetroWfcPayloadMode.Online ? retroWfcPayloadLength : 0))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"The local Retro Rewind build did not consume the selected Retro-WFC payload snapshot.");
|
||||
}
|
||||
}
|
||||
|
||||
var fingerprint = new ProductFingerprint
|
||||
{
|
||||
Profile = profile == BuildProfile.Base ? "base" : "retro-rewind",
|
||||
ToolkitFingerprint = toolkitFingerprint,
|
||||
DolSha256 = dolSha256,
|
||||
RelSha256 = relSha256,
|
||||
ExecutableSha256 = InputValidation.Sha256File(executable),
|
||||
CodePulSha256 = codePulSha256,
|
||||
RetroRewindCompileInputsSha256 = retroRewindCompileInputsSha256,
|
||||
RetroWfcPayloadMode = retroWfcPayloadMode switch
|
||||
{
|
||||
RetroWfcPayloadMode.Online => "downloaded",
|
||||
RetroWfcPayloadMode.Skipped => "skipped",
|
||||
_ => ""
|
||||
},
|
||||
RetroWfcPayloadSha256 = retroWfcPayloadSha256,
|
||||
RetroWfcPayloadLength = retroWfcPayloadLength
|
||||
};
|
||||
JsonState.Write(Path.Combine(outputDirectory, ProductFingerprint.FileName), fingerprint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
internal enum RetroWfcPayloadMode
|
||||
{
|
||||
NotApplicable,
|
||||
Online,
|
||||
Skipped
|
||||
}
|
||||
|
||||
internal sealed class InstallOptions
|
||||
{
|
||||
public required string GamePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The canonical Retro Rewind installation Wheel Wizard owns. Its resolved path is recorded as
|
||||
/// <c>retro_rewind_root</c>; only its compile inputs are ever snapshotted.
|
||||
/// </summary>
|
||||
public string? RetroDirectoryPath { get; init; }
|
||||
|
||||
public RetroWfcPayloadMode RetroWfcPayloadMode { get; init; }
|
||||
public required string InstallDirectory { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Establish a portable installation: the parent of <see cref="InstallDirectory"/> becomes the
|
||||
/// portable root, gets the <c>portable.txt</c> marker and a <c>UserData</c> directory, and all
|
||||
/// runtime user state and <c>[paths]</c> settings stay inside it.
|
||||
/// </summary>
|
||||
public bool Portable { get; init; }
|
||||
|
||||
public bool HasRetroRewind => RetroDirectoryPath is not null;
|
||||
}
|
||||
|
||||
internal sealed class PayloadManifest
|
||||
{
|
||||
public int SchemaVersion { get; set; }
|
||||
public string ProductVersion { get; set; } = "";
|
||||
public string ExpectedGameId { get; set; } = "RMCP01";
|
||||
public string ExpectedDolSha256 { get; set; } = "";
|
||||
public string ExpectedRelSha256 { get; set; } = "";
|
||||
public string ToolkitReleaseTag { get; set; } = "";
|
||||
public string BuildModel { get; set; } = "";
|
||||
public string RetroWfcPayloadUri { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Content identities computed once at release-build time (<c>Build-Installer.ps1 --emit-payload-identities</c>)
|
||||
/// so install doesn't re-hash gigabytes per user; compilation alone re-derives the toolkit identity from disk.
|
||||
/// </summary>
|
||||
public string ToolkitFingerprint { get; set; } = "";
|
||||
public string ToolkitPackageFingerprint { get; set; } = "";
|
||||
public string RuntimeAssetsFingerprint { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Cache-reuse subsets of <see cref="ToolkitFingerprint"/>. Empty in a pre-0.2.0 manifest,
|
||||
/// which every consumer treats as "changed" and therefore as a full rebuild - never as reuse.
|
||||
/// </summary>
|
||||
public string TranslationFingerprint { get; set; } = "";
|
||||
public string NativeToolchainFingerprint { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One validated, content-identified download in operation-owned scratch space. Callers use this
|
||||
/// exact directory for both the update decision and any resulting build.
|
||||
/// </summary>
|
||||
internal sealed record RetroWfcPayloadSnapshot(string Directory, string Sha256, long ByteLength);
|
||||
|
||||
internal sealed class DiscHeader
|
||||
{
|
||||
[JsonPropertyName("game_id")]
|
||||
public string GameId { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("internal_name")]
|
||||
public string InternalName { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("region")]
|
||||
public string Region { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("revision")]
|
||||
public int Revision { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class InstallState
|
||||
{
|
||||
[JsonRequired]
|
||||
public int SchemaVersion { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The setup version that produced this installation. This is the field WheelWizard compares
|
||||
/// against the latest published <c>v*</c> release tag; see docs/WHEELWIZARD_CONTRACT.md.
|
||||
/// </summary>
|
||||
public string SetupVersion { get; set; } = ProductInfo.Version;
|
||||
|
||||
public string ProductVersion { get; set; } = ProductInfo.Version;
|
||||
|
||||
/// <summary>The directory this state describes, so a frontend can confirm what it found.</summary>
|
||||
public string InstallDir { get; set; } = "";
|
||||
|
||||
public DateTime InstalledUtc { get; set; } = DateTime.UtcNow;
|
||||
public bool RetroRewindInstalled { get; set; }
|
||||
public string ToolkitReleaseTag { get; set; } = "";
|
||||
public string DolSha256 { get; set; } = "";
|
||||
public string RelSha256 { get; set; } = "";
|
||||
public string RetroRewindCodePulSha256 { get; set; } = "";
|
||||
public string RetroRewindCompileInputsSha256 { get; set; } = "";
|
||||
public string RetroWfcPayloadMode { get; set; } = "";
|
||||
public string RetroWfcPayloadSha256 { get; set; } = "";
|
||||
public long RetroWfcPayloadLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <c>RetroRewind6</c> directory this installation was pointed at. The runtime's actual copy is
|
||||
/// <c>[paths] retro_rewind_root</c>; this mirror lets uninstall drop only the setting it owns.
|
||||
/// </summary>
|
||||
public string RetroRewindRoot { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Written next to every locally produced product (<c>Base</c>, <c>RetroRewind</c>): the exact inputs
|
||||
/// it was built from, so launch/update can decide without re-running anything whether it's still correct.
|
||||
/// </summary>
|
||||
internal sealed class ProductFingerprint
|
||||
{
|
||||
[JsonRequired]
|
||||
public int SchemaVersion { get; set; } = 1;
|
||||
public string Profile { get; set; } = "";
|
||||
public string SetupVersion { get; set; } = ProductInfo.Version;
|
||||
public string ToolkitFingerprint { get; set; } = "";
|
||||
public string DolSha256 { get; set; } = "";
|
||||
public string RelSha256 { get; set; } = "";
|
||||
/// <summary>
|
||||
/// Identity of the exact executable produced by this build. A valid input provenance is not
|
||||
/// enough if a partial copy, third-party replacement, or disk corruption changed the product
|
||||
/// after it was built.
|
||||
/// </summary>
|
||||
public string ExecutableSha256 { get; set; } = "";
|
||||
public string CodePulSha256 { get; set; } = "";
|
||||
/// <summary>
|
||||
/// Identity of every translator-consumed Retro Rewind input this product was built from. An
|
||||
/// asset-only change to the canonical installation does not alter it, which is exactly why an
|
||||
/// asset-only Retro Rewind update needs no backend work at all.
|
||||
/// </summary>
|
||||
public string RetroRewindCompileInputsSha256 { get; set; } = "";
|
||||
public string RetroWfcPayloadMode { get; set; } = "";
|
||||
public string RetroWfcPayloadSha256 { get; set; } = "";
|
||||
public long RetroWfcPayloadLength { get; set; }
|
||||
public DateTimeOffset BuiltUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
|
||||
public const string FileName = "build-fingerprint.json";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Written whenever the toolkit (translator + build workspace + compiler) is installed or replaced;
|
||||
/// records compile, packaged-tool, and copied-runtime identities. Launch checks compare the compile
|
||||
/// identity against each product's <see cref="ProductFingerprint.ToolkitFingerprint"/> in O(1).
|
||||
/// </summary>
|
||||
internal sealed class ToolkitState
|
||||
{
|
||||
[JsonRequired]
|
||||
public int SchemaVersion { get; set; } = 2;
|
||||
public string ToolkitFingerprint { get; set; } = "";
|
||||
public string ToolkitPackageFingerprint { get; set; } = "";
|
||||
/// <summary>Identity of the runtime assets copied verbatim beside each product.</summary>
|
||||
public string RuntimeAssetsFingerprint { get; set; } = "";
|
||||
/// <summary>Cache-reuse subsets of the compile identity; empty on a pre-0.2.0 installation.</summary>
|
||||
public string TranslationFingerprint { get; set; } = "";
|
||||
public string NativeToolchainFingerprint { get; set; } = "";
|
||||
public string ToolkitReleaseTag { get; set; } = "";
|
||||
public string SetupVersion { get; set; } = ProductInfo.Version;
|
||||
public DateTimeOffset UpdatedUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||
|
||||
public const string FileName = "toolkit-state.json";
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
internal sealed class PayloadArchive : IDisposable
|
||||
{
|
||||
private static readonly byte[] FooterMagic = Encoding.ASCII.GetBytes("MKWCPAY1");
|
||||
private static readonly DateTime NormalizedPayloadTimestampUtc =
|
||||
new(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
private const int FooterSize = 24;
|
||||
private readonly FileStream _executable;
|
||||
private readonly SliceStream _payloadStream;
|
||||
private readonly ZipArchive _zip;
|
||||
|
||||
private PayloadArchive(FileStream executable, SliceStream payloadStream, ZipArchive zip)
|
||||
{
|
||||
_executable = executable;
|
||||
_payloadStream = payloadStream;
|
||||
_zip = zip;
|
||||
}
|
||||
|
||||
public static PayloadArchive OpenCurrent()
|
||||
{
|
||||
var path = Environment.ProcessPath ?? throw new InvalidOperationException("Cannot locate the setup executable.");
|
||||
var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
try
|
||||
{
|
||||
if (stream.Length < FooterSize)
|
||||
throw new InvalidDataException("This setup executable does not contain an installation payload.");
|
||||
|
||||
stream.Position = stream.Length - FooterSize;
|
||||
using var reader = new BinaryReader(stream, Encoding.ASCII, leaveOpen: true);
|
||||
var magic = reader.ReadBytes(FooterMagic.Length);
|
||||
var offset = reader.ReadInt64();
|
||||
var length = reader.ReadInt64();
|
||||
if (!magic.SequenceEqual(FooterMagic) || offset < 0 || length <= 0 || offset + length != stream.Length - FooterSize)
|
||||
throw new InvalidDataException("The installation payload is missing or damaged.");
|
||||
|
||||
var slice = new SliceStream(stream, offset, length);
|
||||
var zip = new ZipArchive(slice, ZipArchiveMode.Read, leaveOpen: true);
|
||||
return new PayloadArchive(stream, slice, zip);
|
||||
}
|
||||
catch
|
||||
{
|
||||
stream.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public PayloadManifest ReadManifest()
|
||||
{
|
||||
var entry = FindEntry(InstalledLayout.PayloadManifestFileName)
|
||||
?? throw new InvalidDataException("The payload manifest is missing.");
|
||||
using var stream = entry.Open();
|
||||
var manifest = JsonSerializer.Deserialize<PayloadManifest>(stream,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
|
||||
?? throw new InvalidDataException("The payload manifest is invalid.");
|
||||
if (manifest.SchemaVersion != 2)
|
||||
throw new InvalidDataException($"Unsupported payload schema {manifest.SchemaVersion}.");
|
||||
// The payload identities are the release-computed replacement for hashing the whole
|
||||
// toolkit on the user's machine; a payload without them was packaged incorrectly.
|
||||
if (string.IsNullOrWhiteSpace(manifest.ToolkitFingerprint) ||
|
||||
string.IsNullOrWhiteSpace(manifest.ToolkitPackageFingerprint) ||
|
||||
string.IsNullOrWhiteSpace(manifest.RuntimeAssetsFingerprint))
|
||||
throw new InvalidDataException("The payload manifest is missing its content identities.");
|
||||
InputValidation.ValidateRetroWfcPayloadUri(manifest.RetroWfcPayloadUri);
|
||||
return manifest;
|
||||
}
|
||||
|
||||
public void ExtractEntry(string entryName, string destination)
|
||||
{
|
||||
var entry = FindEntry(entryName)
|
||||
?? throw new InvalidDataException($"Payload entry is missing: {entryName}");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
|
||||
using (var input = entry.Open())
|
||||
using (var output = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
input.CopyTo(output);
|
||||
NormalizeExtractedTimestamp(destination);
|
||||
}
|
||||
|
||||
public void ExtractDirectory(string prefix, string destination)
|
||||
{
|
||||
prefix = NormalizeEntryName(prefix).TrimEnd('/') + "/";
|
||||
var destinationRoot = Path.GetFullPath(destination).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
foreach (var entry in _zip.Entries)
|
||||
{
|
||||
var normalizedEntry = NormalizeEntryName(entry.FullName);
|
||||
if (!normalizedEntry.StartsWith(prefix, StringComparison.Ordinal) || normalizedEntry.EndsWith('/'))
|
||||
continue;
|
||||
var relative = normalizedEntry[prefix.Length..].Replace('/', Path.DirectorySeparatorChar);
|
||||
var outputPath = Path.GetFullPath(Path.Combine(destinationRoot, relative));
|
||||
if (!outputPath.StartsWith(destinationRoot, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException($"Unsafe payload path: {entry.FullName}");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
|
||||
entry.ExtractToFile(outputPath, overwrite: true);
|
||||
NormalizeExtractedTimestamp(outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void NormalizeExtractedTimestamp(string path) =>
|
||||
File.SetLastWriteTimeUtc(path, NormalizedPayloadTimestampUtc);
|
||||
|
||||
private static string NormalizeEntryName(string value) => value.Replace('\\', '/').TrimStart('/');
|
||||
private ZipArchiveEntry? FindEntry(string name)
|
||||
{
|
||||
var normalized = NormalizeEntryName(name);
|
||||
return _zip.Entries.FirstOrDefault(entry =>
|
||||
NormalizeEntryName(entry.FullName).Equals(normalized, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_zip.Dispose();
|
||||
_payloadStream.Dispose();
|
||||
_executable.Dispose();
|
||||
}
|
||||
|
||||
private sealed class SliceStream : Stream
|
||||
{
|
||||
private readonly Stream _inner;
|
||||
private readonly long _offset;
|
||||
private readonly long _length;
|
||||
private long _position;
|
||||
|
||||
public SliceStream(Stream inner, long offset, long length)
|
||||
{
|
||||
_inner = inner;
|
||||
_offset = offset;
|
||||
_length = length;
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => true;
|
||||
public override bool CanWrite => false;
|
||||
public override long Length => _length;
|
||||
public override long Position { get => _position; set => Seek(value, SeekOrigin.Begin); }
|
||||
public override void Flush() { }
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (_position >= _length) return 0;
|
||||
count = (int)Math.Min(count, _length - _position);
|
||||
lock (_inner)
|
||||
{
|
||||
_inner.Position = _offset + _position;
|
||||
var read = _inner.Read(buffer, offset, count);
|
||||
_position += read;
|
||||
return read;
|
||||
}
|
||||
}
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
var next = origin switch
|
||||
{
|
||||
SeekOrigin.Begin => offset,
|
||||
SeekOrigin.Current => _position + offset,
|
||||
SeekOrigin.End => _length + offset,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(origin))
|
||||
};
|
||||
if (next < 0 || next > _length) throw new IOException("Attempted to seek outside the payload.");
|
||||
return _position = next;
|
||||
}
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
/// <summary>
|
||||
/// A portable installation is a self-contained directory tree the user can move or carry on removable media:
|
||||
/// <code>
|
||||
/// <root>\portable.txt marker; its contents are irrelevant
|
||||
/// <root>\Install\ the installation directory
|
||||
/// <root>\UserData\ runtime user state (Config.toml, NAND, Cache, Logs)
|
||||
/// </code>
|
||||
/// The runtime finds the same root independently (<c>runtime/include/runtime_config.h</c>,
|
||||
/// <c>PortableRootDirectory</c>); this class must keep the same marker name, layout, and depth bound.
|
||||
/// </summary>
|
||||
internal static class PortableRoot
|
||||
{
|
||||
public const string MarkerFileName = "portable.txt";
|
||||
public const string UserDataDirectoryName = "UserData";
|
||||
public const string InstallDirectoryName = "Install";
|
||||
|
||||
/// <summary>
|
||||
/// Parents searched above the start directory. Kept small (installs sit 2 levels below root,
|
||||
/// <c><root>\Install\Base\game.exe</c>) so an unrelated marker far up a drive can't capture it.
|
||||
/// </summary>
|
||||
public const int MaximumSearchDepth = 4;
|
||||
|
||||
/// <summary>
|
||||
/// The portable root <paramref name="startDirectory"/> belongs to, or null otherwise. Callers pass the
|
||||
/// installation directory (not the running executable's location), since a downloaded setup runs from Downloads.
|
||||
/// </summary>
|
||||
public static string? TryFind(string startDirectory)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(startDirectory)) return null;
|
||||
string current;
|
||||
try
|
||||
{
|
||||
// Normalize before trimming: "C:\" trimmed to "C:" is the current directory on that
|
||||
// drive, not the drive root.
|
||||
current = Normalize(startDirectory);
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
for (var level = 0; level <= MaximumSearchDepth; level++)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current, MarkerFileName))) return current;
|
||||
var parent = Path.GetDirectoryName(current);
|
||||
if (string.IsNullOrEmpty(parent) || parent.Equals(current, StringComparison.Ordinal)) break;
|
||||
current = parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string UserDataDirectory(string root) => Path.Combine(root, UserDataDirectoryName);
|
||||
|
||||
/// <summary>Whether <paramref name="candidate"/> is <paramref name="root"/> or lives inside it.</summary>
|
||||
public static bool Contains(string root, string candidate) =>
|
||||
FileSystemUtilities.PathContains(root, candidate);
|
||||
|
||||
/// <summary>
|
||||
/// Establishes the root an install is about to publish into: the marker and the user-state
|
||||
/// directory must exist before anything resolves a configuration path, because that resolution is
|
||||
/// what decides whether the installation writes portable or machine-wide settings.
|
||||
/// </summary>
|
||||
public static string Create(string root)
|
||||
{
|
||||
var full = Normalize(root);
|
||||
EnsureUsableRoot(full);
|
||||
Directory.CreateDirectory(full);
|
||||
Directory.CreateDirectory(UserDataDirectory(full));
|
||||
var marker = Path.Combine(full, MarkerFileName);
|
||||
if (!File.Exists(marker))
|
||||
{
|
||||
File.WriteAllText(marker,
|
||||
$"{ProductInfo.Name} portable installation." + Environment.NewLine +
|
||||
"This marker makes the runtime keep Config.toml, NAND, Cache, and Logs in UserData\\ " +
|
||||
"beside it instead of in %LOCALAPPDATA%." + Environment.NewLine +
|
||||
"Delete it to make this installation use per-user application data again." +
|
||||
Environment.NewLine);
|
||||
}
|
||||
return full;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A portable root is deleted wholesale by the user, so like an install directory it may never be a
|
||||
/// drive root or well-known system location; both share <see cref="FileSystemUtilities.EnsureUsableLocation"/>.
|
||||
/// </summary>
|
||||
public static void EnsureUsableRoot(string root) =>
|
||||
FileSystemUtilities.EnsureUsableLocation(root, "A portable installation root");
|
||||
|
||||
private static string Normalize(string path) => FileSystemUtilities.NormalizePath(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A portable root can be moved or renamed between operations. Every installed-host operation that
|
||||
/// reads <c>install-state.json</c> passes through here first so exactly one place decides what a
|
||||
/// moved installation means, and so a non-portable installation is never touched.
|
||||
/// </summary>
|
||||
internal static class PortableInstallHealing
|
||||
{
|
||||
/// <summary>
|
||||
/// Reconciles a moved portable installation with its recorded location: the state file adopts the
|
||||
/// directory it was actually found in, and the native build tree is discarded because its
|
||||
/// CMake cache holds absolute paths from the old location. Returns whether anything was healed.
|
||||
/// </summary>
|
||||
public static bool HealMovedInstall(Installation installation, IInstallReporter? reporter = null)
|
||||
{
|
||||
// Guard: an ordinary installation that disagrees with its state file is a real problem for
|
||||
// the operation to report, not something to silently rewrite.
|
||||
if (PortableRoot.TryFind(installation.Root) is null) return false;
|
||||
|
||||
var state = installation.ReadInstallState();
|
||||
if (state is not { SchemaVersion: 1 } || string.IsNullOrWhiteSpace(state.InstallDir)) return false;
|
||||
|
||||
string recorded;
|
||||
try
|
||||
{
|
||||
recorded = FileSystemUtilities.NormalizePath(state.InstallDir);
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException)
|
||||
{
|
||||
recorded = state.InstallDir;
|
||||
}
|
||||
if (recorded.Equals(installation.Root, StringComparison.OrdinalIgnoreCase)) return false;
|
||||
|
||||
var previous = state.InstallDir;
|
||||
state.InstallDir = installation.Root;
|
||||
JsonState.Write(installation.InstallStatePath, state);
|
||||
|
||||
// The configured native build directory bakes absolute source, toolchain, and output paths
|
||||
// into CMakeCache.txt. After a move it is unusable and would fail the next configure rather
|
||||
// than being reused, so it is removed and reconfigured from scratch on the next build.
|
||||
var nativeBuild = Path.Combine(installation.WorkspaceDirectory, "native-build");
|
||||
var hadNativeBuild = Directory.Exists(nativeBuild);
|
||||
if (hadNativeBuild) FileSystemUtilities.DeleteDirectoryIfExists(nativeBuild);
|
||||
|
||||
reporter?.Diagnostic(
|
||||
$"This portable installation moved from {previous} to {installation.Root}. " +
|
||||
"The recorded location was updated" +
|
||||
(hadNativeBuild ? " and the location-bound native build cache was discarded." : "."));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,646 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
/// <summary>
|
||||
/// Reconciles installed products against the canonical Retro Rewind install Wheel Wizard owns: the
|
||||
/// toolkit compiles, the source only contributes inputs and its path (<c>retro_rewind_root</c>).
|
||||
/// </summary>
|
||||
internal sealed class ProductRepairService
|
||||
{
|
||||
internal enum RetroWfcPayloadAction { None, PublishCacheOnly, RebuildRetro }
|
||||
|
||||
/// <summary>
|
||||
/// Inputs every reconciliation entry point shares. Everything here is optional context the
|
||||
/// caller already owns under the operation lock; the entry point decides what work is possible.
|
||||
/// </summary>
|
||||
internal sealed record ReconcileOptions
|
||||
{
|
||||
/// <summary>Operation scratch the caller owns, or null to own one for this call.</summary>
|
||||
public string? ScratchRoot { get; init; }
|
||||
|
||||
/// <summary>The release's authoritative clean-disc inputs, supplied together or not at all.</summary>
|
||||
public string? ExpectedDolSha256 { get; init; }
|
||||
public string? ExpectedRelSha256 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The frontend's own canonical <c>RetroRewind6</c> directory, recorded as
|
||||
/// <c>retro_rewind_root</c>. Required by every Retro Rewind operation: the compile-input
|
||||
/// snapshot lives in operation scratch, so it can never be the durable value.
|
||||
/// </summary>
|
||||
public string? CanonicalRetroRewindRoot { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record ReconciliationResult(
|
||||
RetroRewindCompileInputs? CompileInputs,
|
||||
bool PublicationCommitted,
|
||||
/// <summary>
|
||||
/// true when this operation proved or published the online payload cache, null when it made
|
||||
/// no observation and the next inspection must check for itself. false is never produced
|
||||
/// here; product inspection reserves it for a cache already proven stale.
|
||||
/// </summary>
|
||||
bool? CachedRetroWfcPayloadMatches);
|
||||
|
||||
private readonly Installation _installation;
|
||||
private readonly IInstallReporter _reporter;
|
||||
|
||||
public ProductRepairService(Installation installation, IInstallReporter reporter)
|
||||
{
|
||||
_installation = installation;
|
||||
_reporter = reporter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Repairs the base product and any copy-only drift the installation can fix from its own
|
||||
/// authoritative workspace. The caller must hold <see cref="InstallOperationLock"/>.
|
||||
/// </summary>
|
||||
public Task<ReconciliationResult> RepairBaseAsync(ReconcileOptions options,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
ReconcileAsync(null, RetroWfcPayloadMode.NotApplicable, options, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Reconciles Retro Rewind against one immutable compile-input snapshot of the canonical
|
||||
/// installation, recompiling only what those inputs actually change.
|
||||
/// </summary>
|
||||
public Task<ReconciliationResult> RepairRetroAsync(RetroRewindCompileInputs snapshot,
|
||||
RetroWfcPayloadMode payloadMode, ReconcileOptions options,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(snapshot);
|
||||
if (payloadMode == RetroWfcPayloadMode.NotApplicable)
|
||||
throw new InvalidOperationException(
|
||||
"A Retro Rewind operation requires a Retro-WFC payload mode.");
|
||||
return ReconcileAsync(snapshot, payloadMode, options, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<ReconciliationResult> ReconcileAsync(RetroRewindCompileInputs? snapshot,
|
||||
RetroWfcPayloadMode requestedPayloadMode, ReconcileOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_installation.IsPresent)
|
||||
throw new InvalidOperationException("WiiCompiled is not installed here.");
|
||||
if (!_installation.HasToolkit)
|
||||
throw new InvalidOperationException(
|
||||
"The installed recompilation toolkit is missing. Apply the current setup release before repairing products.");
|
||||
var expectedDolSha256 = options.ExpectedDolSha256;
|
||||
var expectedRelSha256 = options.ExpectedRelSha256;
|
||||
if (string.IsNullOrWhiteSpace(expectedDolSha256) != string.IsNullOrWhiteSpace(expectedRelSha256))
|
||||
throw new ArgumentException("Both authoritative game-input hashes must be supplied together.");
|
||||
|
||||
var toolkitFingerprint = _installation.ResolveToolkitFingerprint();
|
||||
if (string.IsNullOrWhiteSpace(toolkitFingerprint))
|
||||
throw new InvalidDataException("The installed recompilation toolkit cannot be identified safely.");
|
||||
|
||||
using var ownedScratch = options.ScratchRoot is null
|
||||
? InstallScratchSpace.CreateInsideInstall(_installation.Root, _reporter)
|
||||
: null;
|
||||
var scratchRoot = options.ScratchRoot ?? ownedScratch!.Root;
|
||||
Directory.CreateDirectory(scratchRoot);
|
||||
|
||||
RetroWfcPayloadSnapshot? payloadSnapshot = null;
|
||||
if (requestedPayloadMode == RetroWfcPayloadMode.Online)
|
||||
{
|
||||
_reporter.Progress(InstallStages.PrepareRetro,
|
||||
"Downloading the current shared Retro-WFC payload snapshot...", 3);
|
||||
var payloadScratch = Path.Combine(scratchRoot, "retro-wfc-payload");
|
||||
try
|
||||
{
|
||||
payloadSnapshot = await InputValidation.DownloadRetroWfcPayloadAsync(
|
||||
InputValidation.CurrentRetroWfcPayloadUri, payloadScratch, cancellationToken);
|
||||
}
|
||||
catch (Exception ex) when (!cancellationToken.IsCancellationRequested &&
|
||||
ex is HttpRequestException or IOException or InvalidDataException
|
||||
or InvalidOperationException or OperationCanceledException)
|
||||
{
|
||||
payloadSnapshot = RecoverInstalledRetroWfcPayload(toolkitFingerprint,
|
||||
Path.Combine(scratchRoot, "retro-wfc-payload-recovered"), ex, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_reporter.Progress(InstallStages.PrepareRetro,
|
||||
"Checking which installed products are still current...", 3);
|
||||
var downloadedPayloadMatchesProduct = payloadSnapshot is not null &&
|
||||
_installation.MatchesRetroWfcPayloadSnapshot(
|
||||
toolkitFingerprint, payloadSnapshot);
|
||||
var cachedPayloadMatchesSnapshot = payloadSnapshot is null
|
||||
? (bool?)null
|
||||
: _installation.CachedRetroWfcPayloadMatches(payloadSnapshot);
|
||||
var payloadAction = payloadSnapshot is null
|
||||
? RetroWfcPayloadAction.None
|
||||
: PlanRetroWfcPayloadReconciliation(downloadedPayloadMatchesProduct,
|
||||
cachedPayloadMatchesSnapshot == true);
|
||||
|
||||
// The canonical root the runtime must read assets from. A base-only operation never changes
|
||||
// it, so its recorded value stays exactly as the last Retro Rewind operation left it.
|
||||
var canonicalRoot = snapshot is null
|
||||
? null
|
||||
: Path.GetFullPath(options.CanonicalRetroRewindRoot ?? throw new InvalidOperationException(
|
||||
"A Retro Rewind operation must record the canonical Retro Rewind folder."));
|
||||
var canonicalRootChanged = canonicalRoot is not null && !RecordedCanonicalRootMatches(canonicalRoot);
|
||||
|
||||
var baseState = _installation.CheckBaseCore(toolkitFingerprint);
|
||||
var retroState = _installation.CheckRetroRewindCore(toolkitFingerprint, snapshot,
|
||||
canonicalError: null, cachedPayloadMatchesSnapshot);
|
||||
var desiredInputDrift = FindDesiredInputDrift(_installation, toolkitFingerprint,
|
||||
expectedDolSha256, expectedRelSha256);
|
||||
var rebuildBase = RequiresCompilation(baseState.Status) || desiredInputDrift.Base;
|
||||
var rebuildRetro = false;
|
||||
var syncRetroWfcPayload = false;
|
||||
|
||||
if (snapshot is null)
|
||||
{
|
||||
if (desiredInputDrift.Retro)
|
||||
throw new InvalidOperationException(
|
||||
"The release changes the clean game inputs used by the installed Retro Rewind product. " +
|
||||
"Supply Wheel Wizard's current Retro Rewind folder explicitly so it can be rebuilt safely.");
|
||||
if (retroState.ActionRequired)
|
||||
throw new InvalidOperationException(
|
||||
"Retro Rewind needs repair, but Wheel Wizard's current Retro Rewind folder was not supplied. " +
|
||||
retroState.Detail);
|
||||
}
|
||||
else
|
||||
{
|
||||
rebuildRetro = desiredInputDrift.Retro || RequiresCompilation(retroState.Status) ||
|
||||
retroState.Status == ProductStatus.Absent;
|
||||
syncRetroWfcPayload = retroState.Status == ProductStatus.PayloadChanged;
|
||||
|
||||
if (payloadAction == RetroWfcPayloadAction.RebuildRetro)
|
||||
{
|
||||
rebuildRetro = true;
|
||||
syncRetroWfcPayload = true;
|
||||
}
|
||||
else if (payloadAction == RetroWfcPayloadAction.PublishCacheOnly)
|
||||
{
|
||||
syncRetroWfcPayload = true;
|
||||
}
|
||||
|
||||
var installedMode = _installation.ResolveRetroWfcPayloadMode(toolkitFingerprint);
|
||||
if (_installation.HasRetroProduct && installedMode != RetroWfcPayloadMode.NotApplicable &&
|
||||
installedMode != requestedPayloadMode)
|
||||
{
|
||||
rebuildRetro = true;
|
||||
syncRetroWfcPayload = requestedPayloadMode == RetroWfcPayloadMode.Online;
|
||||
}
|
||||
if (rebuildRetro && requestedPayloadMode == RetroWfcPayloadMode.Online)
|
||||
syncRetroWfcPayload = true;
|
||||
}
|
||||
|
||||
// Copied support files are republished from the installed workspace, never recompiled. A
|
||||
// rebuild already stages fresh copies, so only a product that is not being rebuilt needs it.
|
||||
var syncBaseRuntimeAssets = !rebuildBase && File.Exists(_installation.BaseExecutable) &&
|
||||
_installation.ValidateCopiedRuntimeAssets(_installation.BaseDirectory,
|
||||
"base", toolkitFingerprint) is not null;
|
||||
var syncRetroRuntimeAssets = !rebuildRetro && _installation.HasRetroProduct &&
|
||||
_installation.ValidateCopiedRuntimeAssets(_installation.RetroDirectory,
|
||||
"retro-rewind", toolkitFingerprint) is not null;
|
||||
|
||||
if (!rebuildBase && !rebuildRetro && !syncRetroWfcPayload && !syncBaseRuntimeAssets &&
|
||||
!syncRetroRuntimeAssets && !canonicalRootChanged)
|
||||
{
|
||||
_reporter.Progress(InstallStages.Publish, "The installed products are already current.", 99);
|
||||
return new ReconciliationResult(snapshot, PublicationCommitted: false,
|
||||
VerifiedOnlinePayloadCache(toolkitFingerprint));
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var builder = new LocalBuildService(_reporter);
|
||||
string? baseOutput = null;
|
||||
string? retroOutput = null;
|
||||
|
||||
if (rebuildBase || rebuildRetro)
|
||||
{
|
||||
EnsureSufficientRepairDiskSpace();
|
||||
_reporter.Progress(InstallStages.PrepareRetro,
|
||||
"Verifying the installed recompilation toolkit...", 4);
|
||||
var toolkitComponents = VerifyToolkitForCompilation(toolkitFingerprint, cancellationToken);
|
||||
RefreshTranslationAssets(expectedDolSha256, expectedRelSha256, cancellationToken);
|
||||
|
||||
// A broken or blocked product may mean the caches themselves are damaged, so nothing
|
||||
// recorded on disk is allowed to contribute to the repaired product.
|
||||
var forceCleanBuild =
|
||||
(rebuildBase && baseState.Status is ProductStatus.Broken or ProductStatus.Blocked) ||
|
||||
(rebuildRetro && retroState.Status is ProductStatus.Broken or ProductStatus.Blocked);
|
||||
var (dolSha, relSha) = TranslationInputHashes();
|
||||
|
||||
if (rebuildBase && rebuildRetro)
|
||||
{
|
||||
// One retro-aware translation, one build graph, both products - the same reason the
|
||||
// install path uses BuildProfile.Both: sequential legs would re-emit every shared
|
||||
// base shard with retro-aware content and recompile all of them twice.
|
||||
var compileInputs = snapshot ?? throw new InvalidOperationException(
|
||||
"Retro Rewind recompilation requires a compile-input snapshot.");
|
||||
baseOutput = Path.Combine(scratchRoot, "base-output");
|
||||
retroOutput = Path.Combine(scratchRoot, "retro-output");
|
||||
_reporter.Progress(InstallStages.BuildBase,
|
||||
"Recompiling Mario Kart Wii and Retro Rewind with the installed toolkit...", 5);
|
||||
await builder.BuildAsync(_installation.Root, BuildProfile.Both, retroOutput,
|
||||
requestedPayloadMode, payloadSnapshot?.Directory, cancellationToken,
|
||||
toolkitComponents, forceCleanBuild,
|
||||
progress: new BuildProgressWindow(_reporter, InstallStages.BuildBase, 5, 92),
|
||||
retroRewindPackageDirectory: compileInputs.RetroRewindRoot,
|
||||
baseOutputDirectory: baseOutput);
|
||||
LocalBuildService.WriteFingerprint(baseOutput, BuildProfile.Base, toolkitFingerprint,
|
||||
dolSha, relSha, "", RetroWfcPayloadMode.NotApplicable);
|
||||
LocalBuildService.WriteFingerprint(retroOutput, BuildProfile.RetroRewind,
|
||||
toolkitFingerprint, dolSha, relSha, compileInputs.CodePulSha256,
|
||||
requestedPayloadMode, compileInputs.CompileInputsSha256,
|
||||
payloadSnapshot?.Sha256 ?? "", payloadSnapshot?.ByteLength ?? 0);
|
||||
}
|
||||
else if (rebuildBase)
|
||||
{
|
||||
baseOutput = Path.Combine(scratchRoot, "base-output");
|
||||
_reporter.Progress(InstallStages.BuildBase,
|
||||
"Recompiling Mario Kart Wii with the installed toolkit...", 5);
|
||||
await builder.BuildAsync(_installation.Root, BuildProfile.Base, baseOutput,
|
||||
RetroWfcPayloadMode.NotApplicable, null, cancellationToken,
|
||||
toolkitComponents, forceCleanBuild,
|
||||
progress: new BuildProgressWindow(_reporter, InstallStages.BuildBase, 5, 92));
|
||||
LocalBuildService.WriteFingerprint(baseOutput, BuildProfile.Base, toolkitFingerprint,
|
||||
dolSha, relSha, "", RetroWfcPayloadMode.NotApplicable);
|
||||
}
|
||||
else
|
||||
{
|
||||
var compileInputs = snapshot ?? throw new InvalidOperationException(
|
||||
"Retro Rewind recompilation requires a compile-input snapshot.");
|
||||
retroOutput = Path.Combine(scratchRoot, "retro-output");
|
||||
_reporter.Progress(InstallStages.BuildRetro,
|
||||
"Recompiling Retro Rewind for the canonical Code.pul...", 5);
|
||||
await builder.BuildAsync(_installation.Root, BuildProfile.RetroRewind, retroOutput,
|
||||
requestedPayloadMode, payloadSnapshot?.Directory, cancellationToken,
|
||||
toolkitComponents, forceCleanBuild,
|
||||
progress: new BuildProgressWindow(_reporter, InstallStages.BuildRetro, 5, 92),
|
||||
retroRewindPackageDirectory: compileInputs.RetroRewindRoot);
|
||||
LocalBuildService.WriteFingerprint(retroOutput, BuildProfile.RetroRewind,
|
||||
toolkitFingerprint, dolSha, relSha, compileInputs.CodePulSha256,
|
||||
requestedPayloadMode, compileInputs.CompileInputsSha256,
|
||||
payloadSnapshot?.Sha256 ?? "", payloadSnapshot?.ByteLength ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (retroOutput is not null)
|
||||
PreserveProductConfig(_installation.RetroDirectory, retroOutput);
|
||||
if (baseOutput is not null)
|
||||
PreserveProductConfig(_installation.BaseDirectory, baseOutput);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
PublishProducts(new PublicationPlan
|
||||
{
|
||||
BaseOutput = baseOutput,
|
||||
RetroOutput = retroOutput,
|
||||
CompileInputs = snapshot,
|
||||
CanonicalRetroRewindRoot = canonicalRoot,
|
||||
SyncBaseRuntimeAssets = syncBaseRuntimeAssets,
|
||||
SyncRetroRuntimeAssets = syncRetroRuntimeAssets,
|
||||
SyncRetroWfcPayload = syncRetroWfcPayload,
|
||||
ToolkitFingerprint = toolkitFingerprint,
|
||||
ScratchRoot = scratchRoot,
|
||||
RequestedPayloadMode = requestedPayloadMode,
|
||||
PayloadSnapshot = payloadSnapshot,
|
||||
ExpectedDolSha256 = expectedDolSha256,
|
||||
ExpectedRelSha256 = expectedRelSha256
|
||||
}, cancellationToken);
|
||||
_reporter.Progress(InstallStages.Publish, "Product repair complete.", 99);
|
||||
return new ReconciliationResult(snapshot, PublicationCommitted: true,
|
||||
VerifiedOnlinePayloadCache(toolkitFingerprint));
|
||||
}
|
||||
|
||||
|
||||
private RetroWfcPayloadSnapshot RecoverInstalledRetroWfcPayload(string toolkitFingerprint,
|
||||
string destinationDirectory, Exception downloadFailure, CancellationToken cancellationToken)
|
||||
{
|
||||
InvalidOperationException Unavailable(string reason) => new(
|
||||
"The Retro-WFC payload service is unavailable" +
|
||||
$" ({downloadFailure.Message.TrimEnd('.')}), and {reason}" +
|
||||
" Try again once the service is reachable.", downloadFailure);
|
||||
|
||||
var fingerprint = _installation.ReadProductFingerprint(_installation.RetroDirectory,
|
||||
toolkitFingerprint);
|
||||
var state = _installation.ReadInstallState();
|
||||
if (fingerprint is not { RetroWfcPayloadMode: "downloaded" } ||
|
||||
state is not { RetroWfcPayloadMode: "downloaded" } ||
|
||||
string.IsNullOrWhiteSpace(fingerprint.RetroWfcPayloadSha256) ||
|
||||
!fingerprint.RetroWfcPayloadSha256.Equals(state.RetroWfcPayloadSha256,
|
||||
StringComparison.Ordinal) ||
|
||||
fingerprint.RetroWfcPayloadLength <= 0)
|
||||
{
|
||||
throw Unavailable("this installation has no verified payload snapshot to fall back to.");
|
||||
}
|
||||
|
||||
string cachedFile;
|
||||
try
|
||||
{
|
||||
cachedFile = InputValidation.ResolveRetroWfcPayloadFile(
|
||||
_installation.WorkspaceRetroWfcPayload);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (new FileInfo(cachedFile).Length != fingerprint.RetroWfcPayloadLength ||
|
||||
!InputValidation.Sha256File(cachedFile).Equals(fingerprint.RetroWfcPayloadSha256,
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
throw Unavailable("the installed payload snapshot no longer matches its recorded identity.");
|
||||
}
|
||||
|
||||
var destination = Path.Combine(Path.GetFullPath(destinationDirectory), "binary",
|
||||
"payload.RMCPD00.bin");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
|
||||
File.Copy(cachedFile, destination, overwrite: true);
|
||||
var root = InputValidation.ValidateStagedRetroWfcPayloadDirectory(destinationDirectory);
|
||||
_reporter.Diagnostic(
|
||||
"The Retro-WFC payload service is unavailable; reusing the installation's verified " +
|
||||
$"payload snapshot (sha256 {fingerprint.RetroWfcPayloadSha256[..12]}..., " +
|
||||
$"{fingerprint.RetroWfcPayloadLength} bytes).");
|
||||
return new RetroWfcPayloadSnapshot(root, fingerprint.RetroWfcPayloadSha256,
|
||||
fingerprint.RetroWfcPayloadLength);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException)
|
||||
{
|
||||
throw Unavailable($"the installed payload snapshot could not be reused ({ex.Message.TrimEnd('.')}).");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The statuses that can only be resolved by producing the product again.</summary>
|
||||
private static bool RequiresCompilation(ProductStatus status) =>
|
||||
status is ProductStatus.ToolkitChanged or ProductStatus.CodePulChanged or
|
||||
ProductStatus.CompileInputsChanged or ProductStatus.InputsMissing or
|
||||
ProductStatus.Blocked or ProductStatus.Broken;
|
||||
|
||||
private bool? VerifiedOnlinePayloadCache(string toolkitFingerprint) =>
|
||||
_installation.HasRetroProduct &&
|
||||
_installation.ResolveRetroWfcPayloadMode(toolkitFingerprint) == RetroWfcPayloadMode.Online
|
||||
? true
|
||||
: null;
|
||||
|
||||
internal static RetroWfcPayloadAction PlanRetroWfcPayloadReconciliation(bool productMatchesSnapshot,
|
||||
bool cacheMatchesSnapshot) =>
|
||||
!productMatchesSnapshot ? RetroWfcPayloadAction.RebuildRetro
|
||||
: cacheMatchesSnapshot ? RetroWfcPayloadAction.None
|
||||
: RetroWfcPayloadAction.PublishCacheOnly;
|
||||
|
||||
private bool RecordedCanonicalRootMatches(string canonicalRoot)
|
||||
{
|
||||
var configured = _installation.ConfiguredRetroRewindRoot;
|
||||
var recorded = _installation.ReadInstallState()?.RetroRewindRoot;
|
||||
return configured is not null && recorded is not null &&
|
||||
SamePath(configured, canonicalRoot) && SamePath(recorded, canonicalRoot);
|
||||
}
|
||||
|
||||
private static bool SamePath(string left, string right)
|
||||
{
|
||||
try { return FileSystemUtilities.PathsEqual(left, right); }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
/// <summary>Everything one reconciliation decided to publish as a single transaction.</summary>
|
||||
private sealed record PublicationPlan
|
||||
{
|
||||
public string? BaseOutput { get; init; }
|
||||
public string? RetroOutput { get; init; }
|
||||
public RetroRewindCompileInputs? CompileInputs { get; init; }
|
||||
public string? CanonicalRetroRewindRoot { get; init; }
|
||||
public bool SyncBaseRuntimeAssets { get; init; }
|
||||
public bool SyncRetroRuntimeAssets { get; init; }
|
||||
public bool SyncRetroWfcPayload { get; init; }
|
||||
|
||||
public required string ToolkitFingerprint { get; init; }
|
||||
public required string ScratchRoot { get; init; }
|
||||
public RetroWfcPayloadMode RequestedPayloadMode { get; init; }
|
||||
public RetroWfcPayloadSnapshot? PayloadSnapshot { get; init; }
|
||||
public string? ExpectedDolSha256 { get; init; }
|
||||
public string? ExpectedRelSha256 { get; init; }
|
||||
}
|
||||
|
||||
private void PublishProducts(PublicationPlan plan, CancellationToken cancellationToken)
|
||||
{
|
||||
var entries = new List<InstallTransactionEntry>();
|
||||
if (plan.BaseOutput is not null)
|
||||
entries.Add(InstallTransactionEntry.Directory(plan.BaseOutput, _installation.BaseDirectory));
|
||||
if (plan.RetroOutput is not null)
|
||||
entries.Add(InstallTransactionEntry.Directory(plan.RetroOutput, _installation.RetroDirectory));
|
||||
|
||||
if (plan.SyncBaseRuntimeAssets || plan.SyncRetroRuntimeAssets)
|
||||
AddRuntimeAssetPublicationEntries(entries, plan.ScratchRoot, plan.SyncBaseRuntimeAssets,
|
||||
plan.SyncRetroRuntimeAssets, plan.ToolkitFingerprint, cancellationToken);
|
||||
|
||||
if (plan.SyncRetroWfcPayload)
|
||||
{
|
||||
if (plan.PayloadSnapshot is null)
|
||||
throw new InvalidDataException("The downloaded Retro-WFC payload was not staged.");
|
||||
entries.Add(InstallTransactionEntry.Directory(plan.PayloadSnapshot.Directory,
|
||||
_installation.WorkspaceRetroWfcPayload));
|
||||
}
|
||||
if (plan.RetroOutput is not null && plan.RequestedPayloadMode == RetroWfcPayloadMode.Skipped)
|
||||
{
|
||||
var emptyPayload = Path.Combine(plan.ScratchRoot, "retro-wfc-payload-removed");
|
||||
Directory.CreateDirectory(emptyPayload);
|
||||
entries.Add(InstallTransactionEntry.Directory(emptyPayload,
|
||||
_installation.WorkspaceRetroWfcPayload));
|
||||
}
|
||||
|
||||
// Copied-runtime repair does not change product or input provenance. Publish only the
|
||||
// authoritative support files in that case, leaving install-state byte-for-byte intact.
|
||||
var publishState = plan.BaseOutput is not null || plan.RetroOutput is not null ||
|
||||
plan.CanonicalRetroRewindRoot is not null;
|
||||
if (publishState)
|
||||
{
|
||||
var statePath = Path.Combine(plan.ScratchRoot, InstalledLayout.InstallStateFileName);
|
||||
JsonState.Write(statePath, BuildUpdatedInstallState(plan.ToolkitFingerprint,
|
||||
plan.CompileInputs, plan.CanonicalRetroRewindRoot,
|
||||
plan.RetroOutput is null ? null : plan.RequestedPayloadMode,
|
||||
plan.RetroOutput is null ? null : plan.PayloadSnapshot, plan.ExpectedDolSha256,
|
||||
plan.ExpectedRelSha256));
|
||||
entries.Add(InstallTransactionEntry.File(statePath, _installation.InstallStatePath));
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var configPath = RuntimeConfiguration.ResolveConfigPath(_installation.Root);
|
||||
var configSnapshot = RuntimeConfiguration.Capture(configPath);
|
||||
using var transaction = InstallTransaction.Begin(_installation.Root, _reporter, entries.ToArray());
|
||||
transaction.Publish();
|
||||
if (plan.CanonicalRetroRewindRoot is not null)
|
||||
{
|
||||
transaction.RecordRuntimeConfigurationMutation(configSnapshot);
|
||||
RuntimeConfiguration.SetRetroRewindRoot(configPath, plan.CanonicalRetroRewindRoot);
|
||||
}
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
private void EnsureSufficientRepairDiskSpace()
|
||||
{
|
||||
const long localBuildAllowance = 14L * 1024 * 1024 * 1024;
|
||||
const long safetyAllowance = 2L * 1024 * 1024 * 1024;
|
||||
FileSystemUtilities.EnsureFreeSpace(_installation.Root,
|
||||
localBuildAllowance + safetyAllowance, "Local recompilation");
|
||||
}
|
||||
|
||||
internal ToolkitFingerprintComponents VerifyToolkitForCompilation(string authoritativeFingerprint,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var state = _installation.ReadToolkitState();
|
||||
if (state is not { SchemaVersion: 2 } ||
|
||||
string.IsNullOrWhiteSpace(state.ToolkitFingerprint) ||
|
||||
!state.ToolkitFingerprint.Equals(authoritativeFingerprint, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"The installed toolkit provenance is missing or does not match this repair operation.");
|
||||
}
|
||||
|
||||
var components = ToolkitFingerprint.ComputeComponents(_installation.Root, cancellationToken);
|
||||
if (!components.Compile.Equals(authoritativeFingerprint, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"The installed recompilation toolkit was modified or is incomplete. Apply the current setup release before compiling products.");
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
private void AddRuntimeAssetPublicationEntries(List<InstallTransactionEntry> entries,
|
||||
string scratchRoot, bool includeBase, bool includeRetro, string toolkitFingerprint,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var state = _installation.ReadToolkitState();
|
||||
if (state is not { SchemaVersion: 2 } ||
|
||||
!state.ToolkitFingerprint.Equals(toolkitFingerprint, StringComparison.Ordinal) ||
|
||||
string.IsNullOrWhiteSpace(state.RuntimeAssetsFingerprint))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"The installed runtime-asset provenance is missing or does not belong to the current toolkit.");
|
||||
}
|
||||
|
||||
var expectedFingerprint = state.RuntimeAssetsFingerprint;
|
||||
var sourceFingerprint = ToolkitFingerprint.ComputeRuntimeAssets(_installation.Root, cancellationToken);
|
||||
if (!sourceFingerprint.Equals(expectedFingerprint, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"The installed BuildWorkspace runtime assets do not match their authoritative toolkit provenance.");
|
||||
}
|
||||
|
||||
var products = new[]
|
||||
{
|
||||
(Include: includeBase, Name: "Base", Destination: _installation.BaseDirectory),
|
||||
(Include: includeRetro, Name: "RetroRewind", Destination: _installation.RetroDirectory)
|
||||
}
|
||||
.Where(product => product.Include)
|
||||
.Select(product => (product.Name, product.Destination));
|
||||
RuntimeAssetPublication.AddEntries(entries,
|
||||
Path.Combine(_installation.WorkspaceDirectory, "runtime", "assets"),
|
||||
Path.Combine(scratchRoot, "runtime-asset-repair"), products, expectedFingerprint,
|
||||
"repair", _reporter.Diagnostic, cancellationToken);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var sourceAfter = ToolkitFingerprint.ComputeRuntimeAssets(_installation.Root, cancellationToken);
|
||||
if (!sourceAfter.Equals(sourceFingerprint, StringComparison.Ordinal))
|
||||
throw new IOException("The installed BuildWorkspace runtime assets changed while repair was being prepared.");
|
||||
}
|
||||
|
||||
private InstallState BuildUpdatedInstallState(string toolkitFingerprint,
|
||||
RetroRewindCompileInputs? compileInputs, string? canonicalRetroRewindRoot,
|
||||
RetroWfcPayloadMode? payloadMode = null,
|
||||
RetroWfcPayloadSnapshot? retroWfcPayloadSnapshot = null, string? expectedDolSha256 = null,
|
||||
string? expectedRelSha256 = null)
|
||||
{
|
||||
var state = _installation.ReadInstallState() ?? new InstallState { InstallDir = _installation.Root };
|
||||
state.InstallDir = _installation.Root;
|
||||
state.SchemaVersion = 1;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(state.SetupVersion)) state.SetupVersion = ProductInfo.Version;
|
||||
if (string.IsNullOrWhiteSpace(state.ProductVersion)) state.ProductVersion = state.SetupVersion;
|
||||
if (!string.IsNullOrWhiteSpace(expectedDolSha256)) state.DolSha256 = expectedDolSha256;
|
||||
if (!string.IsNullOrWhiteSpace(expectedRelSha256)) state.RelSha256 = expectedRelSha256;
|
||||
state.RetroRewindInstalled = compileInputs is not null || _installation.HasRetroProduct;
|
||||
if (compileInputs is not null)
|
||||
{
|
||||
state.RetroRewindCodePulSha256 = compileInputs.CodePulSha256;
|
||||
state.RetroRewindCompileInputsSha256 = compileInputs.CompileInputsSha256;
|
||||
}
|
||||
if (canonicalRetroRewindRoot is not null)
|
||||
state.RetroRewindRoot = Path.GetFullPath(canonicalRetroRewindRoot);
|
||||
if (payloadMode is not null)
|
||||
{
|
||||
state.RetroWfcPayloadMode = payloadMode == RetroWfcPayloadMode.Online
|
||||
? "downloaded"
|
||||
: "skipped";
|
||||
state.RetroWfcPayloadSha256 = payloadMode == RetroWfcPayloadMode.Online
|
||||
? retroWfcPayloadSnapshot?.Sha256 ?? throw new InvalidDataException(
|
||||
"The downloaded Retro-WFC payload snapshot is missing.")
|
||||
: "";
|
||||
state.RetroWfcPayloadLength = payloadMode == RetroWfcPayloadMode.Online
|
||||
? retroWfcPayloadSnapshot!.ByteLength
|
||||
: 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var retro = _installation.ReadProductFingerprint(_installation.RetroDirectory,
|
||||
toolkitFingerprint);
|
||||
if (retro is not null && !string.IsNullOrWhiteSpace(retro.RetroWfcPayloadMode))
|
||||
{
|
||||
state.RetroWfcPayloadMode = retro.RetroWfcPayloadMode;
|
||||
state.RetroWfcPayloadSha256 = retro.RetroWfcPayloadSha256;
|
||||
state.RetroWfcPayloadLength = retro.RetroWfcPayloadLength;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
internal static (bool Base, bool Retro) FindDesiredInputDrift(Installation installation,
|
||||
string toolkitFingerprint, string? expectedDolSha256, string? expectedRelSha256)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(expectedDolSha256) || string.IsNullOrWhiteSpace(expectedRelSha256))
|
||||
return (false, false);
|
||||
return (
|
||||
File.Exists(installation.BaseExecutable) &&
|
||||
!installation.ProductUsesGameInputs(installation.BaseDirectory, toolkitFingerprint,
|
||||
expectedDolSha256, expectedRelSha256),
|
||||
installation.HasRetroProduct &&
|
||||
!installation.ProductUsesGameInputs(installation.RetroDirectory, toolkitFingerprint,
|
||||
expectedDolSha256, expectedRelSha256));
|
||||
}
|
||||
|
||||
|
||||
private void RefreshTranslationAssets(string? expectedDolSha256, string? expectedRelSha256,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(expectedDolSha256) && string.IsNullOrWhiteSpace(expectedRelSha256))
|
||||
{
|
||||
var state = _installation.ReadInstallState();
|
||||
expectedDolSha256 = state is { SchemaVersion: 1 } ? state.DolSha256 : null;
|
||||
expectedRelSha256 = state is { SchemaVersion: 1 } ? state.RelSha256 : null;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(expectedDolSha256) || string.IsNullOrWhiteSpace(expectedRelSha256))
|
||||
throw new InvalidDataException(
|
||||
"The installed game-input provenance is missing or unsupported. Apply the current setup with the disc image.");
|
||||
|
||||
Directory.CreateDirectory(_installation.WorkspaceAssetsDirectory);
|
||||
foreach (var (name, source, expectedSha256) in new[]
|
||||
{
|
||||
("main.dol", Path.Combine(_installation.GameDataDirectory, "sys", "main.dol"), expectedDolSha256),
|
||||
("StaticR.rel", Path.Combine(_installation.GameDataDirectory, "files", "rel", "StaticR.rel"),
|
||||
expectedRelSha256)
|
||||
})
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (!File.Exists(source))
|
||||
throw new InvalidDataException(
|
||||
$"The installed Mario Kart Wii {name} is missing. Apply setup again with the disc image.");
|
||||
if (!InputValidation.Sha256File(source).Equals(expectedSha256, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException(
|
||||
$"The installed Mario Kart Wii {name} does not match its recorded clean-disc identity. " +
|
||||
"Apply the current setup with the disc image.");
|
||||
|
||||
var workspaceInput = Path.Combine(_installation.WorkspaceAssetsDirectory, name);
|
||||
if (File.Exists(workspaceInput) &&
|
||||
InputValidation.Sha256File(workspaceInput)
|
||||
.Equals(expectedSha256, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
File.Copy(source, workspaceInput, overwrite: true);
|
||||
}
|
||||
}
|
||||
|
||||
private (string DolSha, string RelSha) TranslationInputHashes() =>
|
||||
(InputValidation.Sha256File(Path.Combine(_installation.WorkspaceAssetsDirectory, "main.dol")),
|
||||
InputValidation.Sha256File(Path.Combine(_installation.WorkspaceAssetsDirectory, "StaticR.rel")));
|
||||
|
||||
private static void PreserveProductConfig(string currentProduct, string preparedProduct)
|
||||
{
|
||||
var config = Path.Combine(currentProduct, "config");
|
||||
if (Directory.Exists(config))
|
||||
FileSystemUtilities.CopyDirectory(config, Path.Combine(preparedProduct, "config"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
var progressJson = CommandLine.WantsProgressJson(args);
|
||||
|
||||
try
|
||||
{
|
||||
PlatformChecks.EnsureSupportedHost();
|
||||
|
||||
if (args.Length == 0 && GetConsoleProcessList(new uint[1], 1) == 1)
|
||||
{
|
||||
Console.Out.WriteLine(
|
||||
"Mario Kart WiiCompiled is installed through Wheel Wizard - download it from " +
|
||||
"https://github.com/TeamWheelWizard/WheelWizard");
|
||||
Console.ReadKey(intercept: true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
CommandLine command;
|
||||
try
|
||||
{
|
||||
command = CommandLine.Parse(args);
|
||||
}
|
||||
catch (ArgumentException ex) when (args.Length > 0)
|
||||
{
|
||||
// A command line was supplied, so a caller is driving this process. Rejecting the
|
||||
// command with a modal dialog would hang that caller forever.
|
||||
if (progressJson) new NdjsonInstallReporter().Failure(ex.Message);
|
||||
else Console.Error.WriteLine(ex.Message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (command.Mode == AppMode.Version)
|
||||
return ConsoleCommands.Version();
|
||||
|
||||
if (command.Mode == AppMode.Help)
|
||||
return ConsoleCommands.Help();
|
||||
|
||||
if (command.Mode == AppMode.SelfTest)
|
||||
return SelfTests.Run();
|
||||
|
||||
if (command.Mode == AppMode.VerifyInputs)
|
||||
return ConsoleCommands.VerifyInputs(command);
|
||||
|
||||
if (command.Mode == AppMode.EmitPayloadIdentities)
|
||||
return ConsoleCommands.EmitPayloadIdentities(command);
|
||||
|
||||
if (command.Mode == AppMode.CheckProducts)
|
||||
{
|
||||
using var cancellationSignal = CancellationSignal.ObserveEnvironment();
|
||||
return ConsoleCommands.CheckProducts(command, cancellationSignal.Token);
|
||||
}
|
||||
|
||||
if (command.Mode is AppMode.LaunchBase or AppMode.LaunchRetro)
|
||||
return GameLaunchService.LaunchAsync(
|
||||
command.Mode == AppMode.LaunchBase ? BuildProfile.Base : BuildProfile.RetroRewind)
|
||||
.GetAwaiter().GetResult();
|
||||
|
||||
if (command.Mode is AppMode.SilentInstall or AppMode.RepairProducts)
|
||||
{
|
||||
using var cancellationSignal = CancellationSignal.ObserveEnvironment();
|
||||
return command.Mode == AppMode.SilentInstall
|
||||
? ConsoleCommands.Install(command, cancellationSignal.Token).GetAwaiter().GetResult()
|
||||
: ConsoleCommands.RepairProducts(command, cancellationSignal.Token)
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
if (command.Mode is AppMode.Uninstall or AppMode.SilentUninstall)
|
||||
{
|
||||
UninstallService.StartWorker(command.InstallDirectory!,
|
||||
quiet: command.Mode == AppMode.SilentUninstall || command.Quiet);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (command.Mode == AppMode.UninstallWorker)
|
||||
return UninstallService.RunWorker(command.InstallDirectory!, command.Quiet);
|
||||
|
||||
return ConsoleCommands.Help();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// A caller reading the NDJSON protocol must always receive a terminal result line, even
|
||||
// when the failure happened before the installer was reached (a bad command line, for
|
||||
// instance). ConsoleCommands already reports its own failures, so this only fires for
|
||||
// errors raised outside it.
|
||||
if (progressJson)
|
||||
{
|
||||
new NdjsonInstallReporter().Failure(ex.Message);
|
||||
Console.Error.WriteLine(ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine(ex);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern uint GetConsoleProcessList(
|
||||
[Out] uint[] processList,
|
||||
uint processCount);
|
||||
}
|
||||
|
||||
internal static class PlatformChecks
|
||||
{
|
||||
public static void EnsureSupportedHost()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows() || !Environment.Is64BitOperatingSystem)
|
||||
throw new PlatformNotSupportedException("WiiCompiled requires 64-bit Windows.");
|
||||
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 18362))
|
||||
throw new PlatformNotSupportedException("WiiCompiled requires Windows 10 version 1903 or newer.");
|
||||
}
|
||||
}
|
||||
|
||||
internal static class ProductInfo
|
||||
{
|
||||
public const string Name = "WiiCompiled";
|
||||
public const string Version = "0.2.13";
|
||||
|
||||
/// <summary>
|
||||
/// The setup executable is copied into the installation under this name. It is the launcher and
|
||||
/// launch entry point named by the Wheel Wizard contract, so the name is part of that interface.
|
||||
/// </summary>
|
||||
public const string SetupCopyName = "WiiCompiled-Setup.exe";
|
||||
|
||||
public const string UninstallKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall\WiiCompiled";
|
||||
public static string DefaultInstallDirectory =>
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Programs", "WiiCompiled");
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the one canonical Retro Rewind install. Wheel Wizard owns and passes it as
|
||||
/// <c>--retro-dir</c>; the backend only resolves, reads, and records it, never packages or copies it.
|
||||
/// </summary>
|
||||
internal static class RetroRewindSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves the <c>RetroRewind6</c> folder from a selection that may be the folder itself or a
|
||||
/// parent containing exactly one <c>RetroRewind6/Binaries/Code.pul</c>.
|
||||
/// </summary>
|
||||
internal static string ResolveRetroRewind6(string selected)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(selected))
|
||||
throw new InvalidDataException("Choose the canonical Retro Rewind folder.");
|
||||
var root = Path.GetFullPath(selected);
|
||||
if (!Directory.Exists(root))
|
||||
throw new DirectoryNotFoundException($"The Retro Rewind folder does not exist: {root}");
|
||||
|
||||
var candidates = new List<string> { root, Path.Combine(root, "RetroRewind6") };
|
||||
// A folder produced by unpacking the published distribution keeps an extra wrapper directory.
|
||||
foreach (var child in Directory.EnumerateDirectories(root))
|
||||
candidates.Add(Path.Combine(child, "RetroRewind6"));
|
||||
|
||||
var matches = candidates
|
||||
.Where(candidate => File.Exists(Path.Combine(candidate, "Binaries", "Code.pul")))
|
||||
.Select(Path.GetFullPath)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
return matches.Count switch
|
||||
{
|
||||
1 => ResolveFinalDirectory(matches[0]),
|
||||
0 => throw new InvalidDataException(
|
||||
$"{root} does not contain RetroRewind6\\Binaries\\Code.pul. " +
|
||||
"Select the canonical Retro Rewind folder, or the folder that contains it."),
|
||||
_ => throw new InvalidDataException(
|
||||
$"{root} contains more than one RetroRewind6\\Binaries\\Code.pul. " +
|
||||
"Select the exact Retro Rewind folder to use.")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Must resolve a junction/symlink RetroRewind6 to its real directory before fingerprinting: the
|
||||
/// compile-input identity records on-disk kind, and a snapshot copy is always a real directory, so an
|
||||
/// unresolved link can never match its own copy and installs fail forever with a misleading error.
|
||||
/// </summary>
|
||||
private static string ResolveFinalDirectory(string path)
|
||||
{
|
||||
var directory = new DirectoryInfo(path);
|
||||
if (directory.LinkTarget is null)
|
||||
return path;
|
||||
var target = directory.ResolveLinkTarget(returnFinalTarget: true)?.FullName;
|
||||
if (target is null || !Directory.Exists(target))
|
||||
throw new InvalidDataException($"The Retro Rewind folder is a link to a missing target: {path}");
|
||||
return target;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
/// <summary>
|
||||
/// The one path by which a product receives its copied runtime assets, shared by install and repair.
|
||||
/// Each product gets its own moved (not copied) entry, re-verified against the authoritative identity
|
||||
/// in case the source changed mid-preparation.
|
||||
/// </summary>
|
||||
internal static class RuntimeAssetPublication
|
||||
{
|
||||
public static void AddEntries(List<InstallTransactionEntry> entries, string sourceAssets,
|
||||
string preparedRoot, IEnumerable<(string Name, string Destination)> products,
|
||||
string expectedFingerprint, string operationDescription, Action<string> diagnostic,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var (name, destination) in products)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var preparedProduct = Path.Combine(preparedRoot, name);
|
||||
Directory.CreateDirectory(preparedProduct);
|
||||
FileSystemUtilities.CopyDirectory(
|
||||
Path.Combine(sourceAssets, ProductRuntimeAssets.SourceBootstrapDirectoryName),
|
||||
Path.Combine(preparedProduct, ProductRuntimeAssets.ProductBootstrapDirectoryName),
|
||||
cancellationToken);
|
||||
foreach (var (relativePath, fileName) in ProductRuntimeAssets.Files)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
File.Copy(ProductRuntimeAssets.SourceFile(sourceAssets, relativePath),
|
||||
Path.Combine(preparedProduct, fileName));
|
||||
}
|
||||
|
||||
if (!ToolkitFingerprint.ProductRuntimeAssetsMatch(preparedProduct, expectedFingerprint,
|
||||
cancellationToken, diagnostic))
|
||||
{
|
||||
throw new IOException(
|
||||
$"A copied product runtime asset changed while {operationDescription} was being prepared.");
|
||||
}
|
||||
|
||||
entries.Add(InstallTransactionEntry.Directory(
|
||||
Path.Combine(preparedProduct, ProductRuntimeAssets.ProductBootstrapDirectoryName),
|
||||
Path.Combine(destination, ProductRuntimeAssets.ProductBootstrapDirectoryName)));
|
||||
foreach (var (_, fileName) in ProductRuntimeAssets.Files)
|
||||
entries.Add(InstallTransactionEntry.File(Path.Combine(preparedProduct, fileName),
|
||||
Path.Combine(destination, fileName)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
internal sealed record RuntimeConfigSnapshot(bool Existed, byte[] Contents);
|
||||
|
||||
/// <summary>
|
||||
/// Reads and writes the runtime's <c>Config.toml</c>. Every entry point takes the file it operates on
|
||||
/// since its location depends on the installation (portable <c>UserData</c> vs. per-user app data);
|
||||
/// callers obtain it once via <see cref="ResolveConfigPath"/>.
|
||||
/// </summary>
|
||||
internal static class RuntimeConfiguration
|
||||
{
|
||||
public const string ConfigFileName = "Config.toml";
|
||||
|
||||
/// <summary>
|
||||
/// Ordinal, matching the runtime parser/writer (<c>runtime_config.h</c>) exactly. Case-insensitive
|
||||
/// matching here would let setup rewrite a <c>[Paths]</c>/<c>Dvd_Root</c> spelling the runtime ignores.
|
||||
/// </summary>
|
||||
private const StringComparison KeyComparison = StringComparison.Ordinal;
|
||||
|
||||
/// <summary>The per-user configuration an ordinary (non-portable) installation shares.</summary>
|
||||
public static string ApplicationDataConfigPath => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"WiiCompiled", ConfigFileName);
|
||||
|
||||
/// <summary>
|
||||
/// The configuration file that governs <paramref name="installDirectory"/>. This mirrors
|
||||
/// <c>RuntimeConfigFile::ResolveConfigPath</c> in the runtime: portable roots win, everything
|
||||
/// else is per-user application data.
|
||||
/// </summary>
|
||||
public static string ResolveConfigPath(string? installDirectory)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(installDirectory) &&
|
||||
PortableRoot.TryFind(installDirectory) is { } portableRoot)
|
||||
return Path.Combine(PortableRoot.UserDataDirectory(portableRoot), ConfigFileName);
|
||||
return ApplicationDataConfigPath;
|
||||
}
|
||||
|
||||
public static RuntimeConfigSnapshot Capture(string configPath) =>
|
||||
File.Exists(configPath)
|
||||
? new RuntimeConfigSnapshot(true, File.ReadAllBytes(configPath))
|
||||
: new RuntimeConfigSnapshot(false, []);
|
||||
|
||||
public static void Restore(string configPath, RuntimeConfigSnapshot snapshot)
|
||||
{
|
||||
if (snapshot.Existed)
|
||||
FileSystemUtilities.WriteAtomic(configPath, snapshot.Contents);
|
||||
else
|
||||
File.Delete(configPath);
|
||||
}
|
||||
|
||||
internal static void SetDvdRoot(string configPath, string dvdRoot) =>
|
||||
SetPath(configPath, "dvd_root", dvdRoot);
|
||||
|
||||
/// <summary>
|
||||
/// Records the one canonical Retro Rewind installation Wheel Wizard owns. The runtime builds its
|
||||
/// asset overlay by scanning this directory live at launch, so an asset-only Retro Rewind update
|
||||
/// needs no backend work: the next launch simply reads the new files.
|
||||
/// </summary>
|
||||
internal static void SetRetroRewindRoot(string configPath, string retroRewindRoot) =>
|
||||
SetPath(configPath, "retro_rewind_root", retroRewindRoot);
|
||||
|
||||
/// <summary>The canonical Retro Rewind root, or null when no installation has recorded one.</summary>
|
||||
public static string? GetRetroRewindRoot(string configPath) =>
|
||||
GetResolvedPath(configPath, "retro_rewind_root");
|
||||
|
||||
internal static void RemoveRetroRewindRootIfOwned(string configPath, string retroRewindRoot) =>
|
||||
RemovePathIfOwned(configPath, "retro_rewind_root", retroRewindRoot);
|
||||
|
||||
internal static void RemoveDvdRootIfOwned(string configPath, string dvdRoot) =>
|
||||
RemovePathIfOwned(configPath, "dvd_root", dvdRoot);
|
||||
|
||||
/// <summary>The raw stored text of a <c>[paths]</c> key, exactly as the file holds it.</summary>
|
||||
internal static string? GetPath(string configPath, string key) =>
|
||||
TryUnquoteToml(GetRawValue(configPath, "paths", key) ?? "", out var value) ? value : null;
|
||||
|
||||
/// <summary>
|
||||
/// A <c>[paths]</c> value as the runtime will actually use it. The runtime resolves every relative
|
||||
/// <c>[paths]</c> value against the directory holding <c>Config.toml</c> (never the working
|
||||
/// directory), so the host must resolve it the same way before comparing or reading it.
|
||||
/// </summary>
|
||||
internal static string? GetResolvedPath(string configPath, string key)
|
||||
{
|
||||
var stored = GetPath(configPath, key);
|
||||
return string.IsNullOrWhiteSpace(stored) ? null : ResolveAgainstConfig(configPath, stored);
|
||||
}
|
||||
|
||||
internal static string ConfigDirectory(string configPath) =>
|
||||
Path.GetDirectoryName(Path.GetFullPath(configPath))
|
||||
?? throw new InvalidOperationException($"{configPath} has no containing directory.");
|
||||
|
||||
internal static string ResolveAgainstConfig(string configPath, string value) =>
|
||||
Path.GetFullPath(value, ConfigDirectory(configPath));
|
||||
|
||||
private static void SetPath(string configPath, string key, string value)
|
||||
{
|
||||
var lines = ReadLinesOrDefault(configPath);
|
||||
SetSectionValue(lines, "paths", key, QuoteToml(FormatPathValue(configPath, value)));
|
||||
WriteLines(configPath, lines);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Paths inside the portable root are stored relative to the config directory (forward slashes,
|
||||
/// needing no TOML escaping) so the tree keeps working if the user moves or renames the root.
|
||||
/// Everything else, including all non-portable paths, is stored absolute.
|
||||
/// </summary>
|
||||
private static string FormatPathValue(string configPath, string value)
|
||||
{
|
||||
var full = Path.GetFullPath(value);
|
||||
var configDirectory = ConfigDirectory(configPath);
|
||||
if (PortableRoot.TryFind(configDirectory) is not { } root || !PortableRoot.Contains(root, full))
|
||||
return full;
|
||||
var relative = Path.GetRelativePath(configDirectory, full);
|
||||
return Path.IsPathRooted(relative)
|
||||
? full
|
||||
: relative.Replace(Path.DirectorySeparatorChar, '/');
|
||||
}
|
||||
|
||||
private static void RemovePathIfOwned(string configPath, string key, string expectedValue)
|
||||
{
|
||||
if (!File.Exists(configPath)) return;
|
||||
var lines = File.ReadAllLines(configPath).ToList();
|
||||
var inPaths = false;
|
||||
var changed = false;
|
||||
for (var index = 0; index < lines.Count; index++)
|
||||
{
|
||||
var trimmed = RemoveComment(lines[index]).Trim();
|
||||
if (IsSection(trimmed))
|
||||
{
|
||||
inPaths = trimmed.Equals("[paths]", KeyComparison);
|
||||
continue;
|
||||
}
|
||||
if (!inPaths || trimmed.Length == 0) continue;
|
||||
var equals = trimmed.IndexOf('=');
|
||||
if (equals < 0 || !trimmed[..equals].Trim().Equals(key, KeyComparison))
|
||||
continue;
|
||||
if (!TryUnquoteToml(trimmed[(equals + 1)..].Trim(), out var configured)) return;
|
||||
// A portable installation stores this relative, so ownership is decided on the path the
|
||||
// runtime would actually resolve, not on the stored text.
|
||||
if (!ResolveAgainstConfig(configPath, configured)
|
||||
.Equals(Path.GetFullPath(expectedValue), StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
lines.RemoveAt(index);
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
if (changed) WriteLines(configPath, lines);
|
||||
}
|
||||
|
||||
/// <summary>The raw TOML literal stored for a key, or null when the section or key is absent.</summary>
|
||||
internal static string? GetRawValue(string configPath, string section, string key)
|
||||
{
|
||||
if (!File.Exists(configPath)) return null;
|
||||
var header = $"[{section}]";
|
||||
var inSection = false;
|
||||
foreach (var line in File.ReadLines(configPath))
|
||||
{
|
||||
var trimmed = RemoveComment(line).Trim();
|
||||
if (IsSection(trimmed))
|
||||
{
|
||||
inSection = trimmed.Equals(header, KeyComparison);
|
||||
continue;
|
||||
}
|
||||
if (!inSection || trimmed.Length == 0) continue;
|
||||
var equals = trimmed.IndexOf('=');
|
||||
if (equals < 0 || !trimmed[..equals].Trim().Equals(key, KeyComparison)) continue;
|
||||
return trimmed[(equals + 1)..].Trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces one key in one section, preserving comments, ordering, and every unrelated setting -
|
||||
/// this file belongs to the user and to the in-game settings bar as much as to setup. A
|
||||
/// commented-out key is deliberately not matched, matching the runtime's own writer.
|
||||
/// </summary>
|
||||
private static void SetSectionValue(List<string> lines, string section, string key, string value)
|
||||
{
|
||||
var header = $"[{section}]";
|
||||
var sectionStart = -1;
|
||||
var sectionEnd = lines.Count;
|
||||
for (var index = 0; index < lines.Count; index++)
|
||||
{
|
||||
var trimmed = RemoveComment(lines[index]).Trim();
|
||||
if (!IsSection(trimmed)) continue;
|
||||
if (sectionStart >= 0)
|
||||
{
|
||||
sectionEnd = index;
|
||||
break;
|
||||
}
|
||||
if (trimmed.Equals(header, KeyComparison)) sectionStart = index;
|
||||
}
|
||||
if (sectionStart < 0)
|
||||
{
|
||||
if (lines.Count > 0 && lines[^1].Length != 0) lines.Add("");
|
||||
lines.Add(header);
|
||||
lines.Add($"{key} = {value}");
|
||||
return;
|
||||
}
|
||||
|
||||
for (var index = sectionStart + 1; index < sectionEnd; index++)
|
||||
{
|
||||
var trimmed = RemoveComment(lines[index]).Trim();
|
||||
if (trimmed.Length == 0) continue;
|
||||
var equals = trimmed.IndexOf('=');
|
||||
if (equals >= 0 && trimmed[..equals].Trim().Equals(key, KeyComparison))
|
||||
{
|
||||
lines[index] = $"{key} = {value}";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Append after the section's last real line, not after the blank line that separates it from
|
||||
// the next header, so a generated file stays readable for the user who also edits it by hand.
|
||||
var insertAt = sectionEnd;
|
||||
while (insertAt > sectionStart + 1 && lines[insertAt - 1].Trim().Length == 0) insertAt--;
|
||||
lines.Insert(insertAt, $"{key} = {value}");
|
||||
}
|
||||
|
||||
private static List<string> ReadLinesOrDefault(string configPath) =>
|
||||
File.Exists(configPath) ? File.ReadAllLines(configPath).ToList() : DefaultConfigLines();
|
||||
|
||||
private static void WriteLines(string configPath, List<string> lines) =>
|
||||
FileSystemUtilities.WriteAtomic(configPath,
|
||||
Encoding.UTF8.GetBytes(string.Join(Environment.NewLine, lines) + Environment.NewLine));
|
||||
|
||||
private static List<string> DefaultConfigLines() =>
|
||||
[
|
||||
"# WiiCompiled user configuration",
|
||||
"",
|
||||
"[paths]"
|
||||
];
|
||||
|
||||
private static bool IsSection(string value) =>
|
||||
value.Length >= 2 && value[0] == '[' && value[^1] == ']';
|
||||
|
||||
/// <summary>
|
||||
/// Line-for-line port of <c>RemoveComment</c> in <c>runtime_config.h</c>, including its backslash
|
||||
/// escaping inside basic strings: without it a Windows path ending in <c>\\"</c> reads as
|
||||
/// re-opening the quote, and the two implementations disagree about which <c>#</c> starts a comment.
|
||||
/// </summary>
|
||||
private static string RemoveComment(string line)
|
||||
{
|
||||
var inSingle = false;
|
||||
var inDouble = false;
|
||||
var escaped = false;
|
||||
for (var index = 0; index < line.Length; index++)
|
||||
{
|
||||
var character = line[index];
|
||||
if (inDouble && character == '\\' && !escaped)
|
||||
{
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (character == '\'' && !inDouble) inSingle = !inSingle;
|
||||
else if (character == '"' && !inSingle && !escaped) inDouble = !inDouble;
|
||||
else if (character == '#' && !inSingle && !inDouble) return line[..index];
|
||||
escaped = false;
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
private static string QuoteToml(string value) =>
|
||||
"\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
|
||||
|
||||
internal static bool TryUnquoteToml(string value, out string result)
|
||||
{
|
||||
result = "";
|
||||
if (value.Length < 2) return false;
|
||||
// Literal strings carry no escapes; the runtime's parser treats them the same way.
|
||||
if (value[0] == '\'' && value[^1] == '\'')
|
||||
{
|
||||
result = value[1..^1];
|
||||
return true;
|
||||
}
|
||||
if (value[0] != '"' || value[^1] != '"') return false;
|
||||
var builder = new StringBuilder(value.Length - 2);
|
||||
for (var index = 1; index < value.Length - 1; index++)
|
||||
{
|
||||
if (value[index] == '\\' && index + 1 < value.Length - 1)
|
||||
{
|
||||
var escaped = value[++index];
|
||||
builder.Append(escaped switch { '\\' => '\\', '"' => '"', _ => escaped });
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(value[index]);
|
||||
}
|
||||
}
|
||||
result = builder.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
internal static class ShellIntegration
|
||||
{
|
||||
public static void RegisterUninstaller(string installDirectory, bool retroInstalled)
|
||||
{
|
||||
using var key = Registry.CurrentUser.CreateSubKey(ProductInfo.UninstallKey, writable: true)
|
||||
?? throw new InvalidOperationException("Could not register the uninstaller.");
|
||||
var cli = Path.Combine(installDirectory, ProductInfo.SetupCopyName);
|
||||
key.SetValue("DisplayName", ProductInfo.Name);
|
||||
key.SetValue("DisplayVersion", ProductInfo.Version);
|
||||
key.SetValue("Publisher", "WiiCompiled");
|
||||
key.SetValue("InstallLocation", installDirectory);
|
||||
key.SetValue("DisplayIcon", cli);
|
||||
key.SetValue("UninstallString", $"\"{cli}\" --uninstall --install-dir \"{installDirectory}\"");
|
||||
key.SetValue("QuietUninstallString", $"\"{cli}\" --silent-uninstall --install-dir \"{installDirectory}\"");
|
||||
key.SetValue("NoModify", 1, RegistryValueKind.DWord);
|
||||
key.SetValue("NoRepair", 1, RegistryValueKind.DWord);
|
||||
key.SetValue("EstimatedSize", EstimateSizeKb(installDirectory), RegistryValueKind.DWord);
|
||||
key.SetValue("Comments", retroInstalled ? "Includes the Retro Rewind profile" : "WiiCompiled");
|
||||
}
|
||||
|
||||
public static void UnregisterUninstaller() =>
|
||||
Registry.CurrentUser.DeleteSubKeyTree(ProductInfo.UninstallKey, throwOnMissingSubKey: false);
|
||||
|
||||
/// <summary>Removes shortcuts left by GUI-capable releases from before Wheel Wizard owned the UI.</summary>
|
||||
public static void RemoveAllShortcuts()
|
||||
{
|
||||
var desktop = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
|
||||
var startMenuFolder = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs", ProductInfo.Name);
|
||||
RemoveAllShortcuts(desktop, startMenuFolder);
|
||||
}
|
||||
|
||||
internal static void RemoveAllShortcuts(string desktop, string startMenuFolder)
|
||||
{
|
||||
var failures = new List<Exception>();
|
||||
DeleteFileBestEffort(Path.Combine(desktop, "WiiCompiled.lnk"), failures);
|
||||
DeleteFileBestEffort(Path.Combine(desktop, "Retro Rewind.lnk"), failures);
|
||||
DeleteDirectoryBestEffort(startMenuFolder, failures);
|
||||
|
||||
if (failures.Count != 0)
|
||||
throw new AggregateException("One or more WiiCompiled shortcuts could not be removed.", failures);
|
||||
}
|
||||
|
||||
private static void DeleteFileBestEffort(string path, List<Exception> failures)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failures.Add(new IOException($"Could not delete shortcut {path}: {ex.Message}", ex));
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteDirectoryBestEffort(string path, List<Exception> failures)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(path)) Directory.Delete(path, recursive: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failures.Add(new IOException($"Could not delete shortcut folder {path}: {ex.Message}", ex));
|
||||
}
|
||||
}
|
||||
|
||||
private static int EstimateSizeKb(string directory)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytes = Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories)
|
||||
.Sum(path => new FileInfo(path).Length);
|
||||
return (int)Math.Min(int.MaxValue, (bytes + 1023) / 1024);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
/// <summary>
|
||||
/// Content identity of everything that decides what the locally produced executables contain.
|
||||
/// First link of toolkit content -> toolkit-state.json -> per-product build-fingerprint.json:
|
||||
/// unchanged skips retranslation, any change forces a rebuild. Content-based so re-tags are free.
|
||||
/// </summary>
|
||||
internal static class ToolkitFingerprint
|
||||
{
|
||||
// v6/native-toolchain v2 forces one clean rebuild past a Ninja stale-object bug.
|
||||
// translation v2 added runtime/src, which the translator regex-scans for native overrides.
|
||||
private const string Version = "mkwc-toolkit-compile-v6";
|
||||
private const string TranslationVersion = "mkwc-toolkit-translation-v2";
|
||||
private const string NativeToolchainVersion = "mkwc-toolkit-native-toolchain-v2";
|
||||
private const string PackageVersion = "mkwc-toolkit-package-v1";
|
||||
private const string RuntimeAssetsVersion = "mkwc-product-runtime-assets-v3";
|
||||
private const string RuntimeAssetsDescription = "The product runtime assets";
|
||||
|
||||
private static readonly string[] SourceExtensions =
|
||||
[".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".inl", ".s", ".asm",
|
||||
".cmake", ".txt", ".yml", ".yaml", ".toml", ".json", ".ps1", ".in", ".def", ".patch"];
|
||||
|
||||
/// <summary>
|
||||
/// Inputs that decide translator output: translator, translation project, runtime sources it
|
||||
/// scans for native override registrations/effect contracts, and the build script's translator
|
||||
/// command lines. Unchanged means the completed base translation can be reused instead of re-run.
|
||||
/// </summary>
|
||||
private static readonly string[] TranslationPrefixes =
|
||||
[
|
||||
InstalledLayout.ToolkitEntryPrefix + "Translator/",
|
||||
InstalledLayout.WorkspaceEntryPrefix + "projects/",
|
||||
InstalledLayout.WorkspaceEntryPrefix + "runtime/src/"
|
||||
];
|
||||
|
||||
private static readonly string[] TranslationFiles =
|
||||
[InstalledLayout.WorkspaceEntryPrefix + "LocalBuild.ps1"];
|
||||
|
||||
/// <summary>
|
||||
/// Inputs that make compiled objects unsafe to reuse: the compiler/CMake/Ninja toolchain, the
|
||||
/// shipped runtime DLLs, and the scripts owning configure flags. Source/dependency changes are
|
||||
/// deliberately excluded, since CMake/Ninja already rebuild exactly the affected objects.
|
||||
/// </summary>
|
||||
private static readonly string[] NativeToolchainPrefixes =
|
||||
[
|
||||
InstalledLayout.ToolkitEntryPrefix + "llvm-mingw/",
|
||||
InstalledLayout.ToolkitEntryPrefix + "CMake/",
|
||||
InstalledLayout.ToolkitEntryPrefix + "Ninja/",
|
||||
InstalledLayout.ToolkitEntryPrefix + "Redist/"
|
||||
];
|
||||
|
||||
private static readonly string[] NativeToolchainFiles =
|
||||
[
|
||||
InstalledLayout.WorkspaceEntryPrefix + "LocalBuild.ps1",
|
||||
InstalledLayout.WorkspaceEntryPrefix + "NativeBuildFlags.ps1"
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Fingerprint of the toolkit at <paramref name="root"/>: the staged payload root at
|
||||
/// release-build time, or an install directory when a compile path re-verifies it. The install
|
||||
/// path itself never computes this, it compares manifest identity against toolkit-state.json.
|
||||
/// </summary>
|
||||
public static string Compute(string root, CancellationToken cancellationToken = default) =>
|
||||
ComputeComponents(root, cancellationToken).Compile;
|
||||
|
||||
/// <summary>
|
||||
/// One walk, three identities: the compile identity plus its translation and native-toolchain
|
||||
/// subsets. The subsets never gate correctness, compile identity alone decides if products are
|
||||
/// current, they only decide how much completed work carries into the next build.
|
||||
/// </summary>
|
||||
public static ToolkitFingerprintComponents ComputeComponents(string root,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
root = Path.GetFullPath(root);
|
||||
var toolkit = InstalledLayout.Toolkit(root);
|
||||
var workspace = InstalledLayout.Workspace(root);
|
||||
var entries = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||
|
||||
// DolphinTool validates/extracts the user disc but does not influence generated products.
|
||||
// Everything else in Toolkit can affect translation, compilation, linking, or copied
|
||||
// runtime support and therefore belongs to the compile identity.
|
||||
AddDirectory(entries, root, toolkit, null,
|
||||
cancellationToken,
|
||||
file => !Path.GetFileName(file).Equals("DolphinTool.exe", StringComparison.OrdinalIgnoreCase));
|
||||
AddFile(entries, root, Path.Combine(workspace, "LocalBuild.ps1"), cancellationToken);
|
||||
AddFile(entries, root, Path.Combine(workspace, "NativeBuildFlags.ps1"), cancellationToken);
|
||||
AddDirectory(entries, root, Path.Combine(workspace, "projects"), null, cancellationToken);
|
||||
var runtime = Path.Combine(workspace, "runtime");
|
||||
var runtimeAssets = Path.Combine(runtime, "assets");
|
||||
// runtime/assets is copied beside the executable; it is deliberately tracked by the
|
||||
// independent runtime-assets identity below so changing those bytes does not recompile code.
|
||||
AddDirectory(entries, root, runtime, SourceExtensions, cancellationToken,
|
||||
file => !FileSystemUtilities.PathContains(runtimeAssets, file));
|
||||
AddDirectory(entries, root, Path.Combine(workspace, "aurora-main"), SourceExtensions,
|
||||
cancellationToken);
|
||||
// The shipped precompiled aurora archives and the pinned Dawn runtime are linked into the
|
||||
// product, so a change there changes the binary just as surely as a translator change does.
|
||||
// native_prebuilt's provenance.json is excluded: its Contents list already identifies every
|
||||
// shipped byte, while its BuiltUtc stamp would make a bit-identical re-harvest look like a
|
||||
// toolkit change and force a global user-side rebuild for nothing.
|
||||
AddDirectory(entries, root, Path.Combine(workspace, "Dependencies"), null, cancellationToken,
|
||||
file => !file.EndsWith(
|
||||
Path.Combine("native_prebuilt", "provenance.json"), StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (entries.Count == 0)
|
||||
throw new InvalidDataException($"No toolkit files were found under {root}; the installation is incomplete.");
|
||||
|
||||
return new ToolkitFingerprintComponents(
|
||||
BuildIdentity(Version, entries),
|
||||
BuildSubsetIdentity(TranslationVersion, entries, TranslationPrefixes, TranslationFiles),
|
||||
BuildSubsetIdentity(NativeToolchainVersion, entries, NativeToolchainPrefixes,
|
||||
NativeToolchainFiles));
|
||||
}
|
||||
|
||||
private static string BuildSubsetIdentity(string version, SortedDictionary<string, string> entries,
|
||||
string[] prefixes, string[] files)
|
||||
{
|
||||
var subset = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||
foreach (var (relative, hash) in entries)
|
||||
{
|
||||
if (prefixes.Any(prefix => relative.StartsWith(prefix, StringComparison.Ordinal)) ||
|
||||
files.Any(file => relative.Equals(file, StringComparison.Ordinal)))
|
||||
subset[relative] = hash;
|
||||
}
|
||||
return BuildIdentity(version, subset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Content identity of the source-owned files copied verbatim beside every product. This is
|
||||
/// intentionally independent of <see cref="Compute"/>: an asset-only release is published
|
||||
/// transactionally without translating or compiling unchanged code.
|
||||
/// </summary>
|
||||
public static string ComputeRuntimeAssets(string root, CancellationToken cancellationToken = default)
|
||||
{
|
||||
root = Path.GetFullPath(root);
|
||||
var assets = Path.Combine(InstalledLayout.Workspace(root), "runtime", "assets");
|
||||
var entries = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||
AddMappedDirectory(entries, Path.Combine(assets, ProductRuntimeAssets.SourceBootstrapDirectoryName),
|
||||
ProductRuntimeAssets.ProductBootstrapDirectoryName, cancellationToken);
|
||||
foreach (var (relativePath, productFileName) in ProductRuntimeAssets.Files)
|
||||
AddRequiredMappedFile(entries, ProductRuntimeAssets.SourceFile(assets, relativePath),
|
||||
productFileName, cancellationToken);
|
||||
return BuildIdentity(RuntimeAssetsVersion, entries);
|
||||
}
|
||||
|
||||
public static string? TryComputeRuntimeAssets(string root, CancellationToken cancellationToken = default,
|
||||
Action<string>? diagnostic = null)
|
||||
{
|
||||
try { return ComputeRuntimeAssets(root, cancellationToken); }
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException)
|
||||
{
|
||||
diagnostic?.Invoke(
|
||||
$"The workspace runtime assets under {root} could not be fingerprinted: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Checks the actual copied product files against a source runtime-assets identity.</summary>
|
||||
public static bool ProductRuntimeAssetsMatch(string productDirectory, string expectedFingerprint,
|
||||
CancellationToken cancellationToken = default, Action<string>? diagnostic = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entries = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||
AddMappedDirectory(entries,
|
||||
Path.Combine(productDirectory, ProductRuntimeAssets.ProductBootstrapDirectoryName),
|
||||
ProductRuntimeAssets.ProductBootstrapDirectoryName, cancellationToken);
|
||||
foreach (var (_, productFileName) in ProductRuntimeAssets.Files)
|
||||
AddRequiredMappedFile(entries, Path.Combine(productDirectory, productFileName),
|
||||
productFileName, cancellationToken);
|
||||
return BuildIdentity(RuntimeAssetsVersion, entries).Equals(expectedFingerprint,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException)
|
||||
{
|
||||
diagnostic?.Invoke(
|
||||
$"The copied runtime assets under {productDirectory} could not be verified: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Identity of the shipped Toolkit directory, including installer-only tools.</summary>
|
||||
public static string ComputePackage(string root, CancellationToken cancellationToken = default)
|
||||
{
|
||||
root = Path.GetFullPath(root);
|
||||
var entries = new SortedDictionary<string, string>(StringComparer.Ordinal);
|
||||
AddDirectory(entries, root, InstalledLayout.Toolkit(root), null, cancellationToken);
|
||||
if (entries.Count == 0)
|
||||
throw new InvalidDataException($"No toolkit package files were found under {root}.");
|
||||
return BuildIdentity(PackageVersion, entries);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The toolkit is ~26,000 mostly tiny files (llvm-mingw's headers/libraries dominate), so this
|
||||
/// walk is bound by per-file open/read overhead, not hashing throughput, minutes rather than
|
||||
/// seconds on a cold cache under on-access antivirus. Hashing concurrently keeps a compiling
|
||||
/// repair from taking longer to prove toolkit integrity than to translate with it.
|
||||
/// </summary>
|
||||
private static void AddDirectory(SortedDictionary<string, string> entries, string root, string directory,
|
||||
string[]? extensions, CancellationToken cancellationToken = default,
|
||||
Func<string, bool>? include = null)
|
||||
{
|
||||
if (!Directory.Exists(directory)) return;
|
||||
var selected = new List<string>();
|
||||
foreach (var file in Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (include is not null && !include(file)) continue;
|
||||
if (extensions is not null &&
|
||||
!extensions.Contains(Path.GetExtension(file), StringComparer.OrdinalIgnoreCase))
|
||||
continue;
|
||||
selected.Add(file);
|
||||
}
|
||||
|
||||
var hashed = new System.Collections.Concurrent.ConcurrentBag<(string Relative, string Hash)>();
|
||||
Parallel.ForEach(selected,
|
||||
new ParallelOptions
|
||||
{
|
||||
CancellationToken = cancellationToken,
|
||||
MaxDegreeOfParallelism = Math.Max(2, Environment.ProcessorCount)
|
||||
},
|
||||
file =>
|
||||
{
|
||||
if (!File.Exists(file)) return;
|
||||
var relative = Path.GetRelativePath(root, file).Replace('\\', '/');
|
||||
hashed.Add((relative, InputValidation.Sha256File(file)));
|
||||
});
|
||||
foreach (var (relative, hash) in hashed) entries[relative] = hash;
|
||||
}
|
||||
|
||||
private static void AddFile(SortedDictionary<string, string> entries, string root, string file,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!File.Exists(file)) return;
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var relative = Path.GetRelativePath(root, file).Replace('\\', '/');
|
||||
entries[relative] = InputValidation.Sha256File(file);
|
||||
}
|
||||
|
||||
private static void AddMappedDirectory(SortedDictionary<string, string> entries, string directory,
|
||||
string identityRoot, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
throw new InvalidDataException($"Required product runtime asset directory is missing: {directory}");
|
||||
var tree = FileSystemUtilities.EnumerateRegularTree(directory, cancellationToken,
|
||||
RuntimeAssetsDescription);
|
||||
if (tree.Count == 0)
|
||||
throw new InvalidDataException($"Required product runtime asset directory is empty: {directory}");
|
||||
foreach (var entry in tree)
|
||||
{
|
||||
if (entry.IsEmptyDirectory)
|
||||
entries[$"empty-directory:{identityRoot}/{entry.RelativePath}"] = "";
|
||||
else if (!entry.IsDirectory)
|
||||
entries[$"{identityRoot}/{entry.RelativePath}"] = InputValidation.Sha256File(entry.FullPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddRequiredMappedFile(SortedDictionary<string, string> entries, string file,
|
||||
string identityPath, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!File.Exists(file))
|
||||
throw new InvalidDataException($"Required product runtime asset is missing: {file}");
|
||||
FileSystemUtilities.RejectReparsePoint(new FileInfo(file), RuntimeAssetsDescription);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
entries[identityPath] = InputValidation.Sha256File(file);
|
||||
}
|
||||
|
||||
private static string BuildIdentity(string version, SortedDictionary<string, string> entries)
|
||||
{
|
||||
var builder = new StringBuilder(version).Append('\n');
|
||||
foreach (var (relative, hash) in entries)
|
||||
builder.Append(relative).Append('|').Append(hash).Append('\n');
|
||||
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(builder.ToString())))
|
||||
.ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compile identity plus the two subsets deciding cache reuse: <see cref="Compile"/> is the
|
||||
/// authoritative freshness identity, <see cref="Translation"/> decides if the base translation is
|
||||
/// still current, <see cref="NativeToolchain"/> decides if compiled objects may still be linked.
|
||||
/// </summary>
|
||||
internal sealed record ToolkitFingerprintComponents(string Compile, string Translation,
|
||||
string NativeToolchain);
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
internal static class UninstallService
|
||||
{
|
||||
public static void StartWorker(string installDirectory, bool quiet = false)
|
||||
{
|
||||
var source = Environment.ProcessPath ?? throw new InvalidOperationException("Cannot locate the uninstaller.");
|
||||
var temporary = Path.Combine(Path.GetTempPath(), $"WiiCompiled-Uninstall-{Guid.NewGuid():N}.exe");
|
||||
File.Copy(source, temporary, overwrite: true);
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
FileName = temporary,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden
|
||||
};
|
||||
info.ArgumentList.Add("--uninstall-worker");
|
||||
info.ArgumentList.Add("--install-dir");
|
||||
info.ArgumentList.Add(Path.GetFullPath(installDirectory));
|
||||
if (quiet) info.ArgumentList.Add("--quiet");
|
||||
_ = Process.Start(info) ?? throw new InvalidOperationException("Could not start the uninstall worker.");
|
||||
}
|
||||
|
||||
public static int RunWorker(string installDirectory, bool quiet)
|
||||
{
|
||||
installDirectory = Path.GetFullPath(installDirectory);
|
||||
Thread.Sleep(750);
|
||||
TryCleanup("remove shortcuts", quiet, ShellIntegration.RemoveAllShortcuts);
|
||||
|
||||
// A portable installation never registered an uninstall entry, and the single machine-wide
|
||||
// key may belong to a normal installation on the same account. Removing it here would
|
||||
// uninstall that one from Windows' point of view.
|
||||
bool? isPortable = null;
|
||||
TryCleanup("determine whether the installation is portable", quiet,
|
||||
() => isPortable = PortableRoot.TryFind(installDirectory) is not null);
|
||||
if (isPortable == false)
|
||||
TryCleanup("remove the Add/Remove Programs entry", quiet,
|
||||
ShellIntegration.UnregisterUninstaller);
|
||||
|
||||
var installation = new Installation(installDirectory);
|
||||
string? configPath = null;
|
||||
TryCleanup("locate the runtime configuration", quiet,
|
||||
() => configPath = RuntimeConfiguration.ResolveConfigPath(installDirectory));
|
||||
|
||||
InstallState? state = null;
|
||||
TryCleanup("read the installation state", quiet,
|
||||
() => state = installation.ReadInstallState());
|
||||
|
||||
if (configPath is not null)
|
||||
{
|
||||
TryCleanup("remove the installed game path from the runtime configuration", quiet,
|
||||
() => RuntimeConfiguration.RemoveDvdRootIfOwned(configPath,
|
||||
Path.Combine(installDirectory, "GameAssets", "DATA")));
|
||||
}
|
||||
|
||||
// Remove only this install's Retro Rewind setting; leave user-owned paths untouched.
|
||||
if (configPath is not null && !string.IsNullOrEmpty(state?.RetroRewindRoot))
|
||||
TryCleanup("remove the Retro Rewind path from the runtime configuration", quiet,
|
||||
() => RuntimeConfiguration.RemoveRetroRewindRootIfOwned(configPath, state.RetroRewindRoot));
|
||||
|
||||
Exception? lastError = null;
|
||||
for (var attempt = 0; attempt < 20; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(installDirectory)) Directory.Delete(installDirectory, recursive: true);
|
||||
lastError = null;
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastError = ex;
|
||||
Thread.Sleep(250);
|
||||
}
|
||||
}
|
||||
|
||||
var current = Environment.ProcessPath;
|
||||
if (current is not null) MoveFileEx(current, null, MoveFileFlags.DelayUntilReboot);
|
||||
if (lastError is not null)
|
||||
{
|
||||
if (!quiet)
|
||||
Console.Error.WriteLine("Uninstall could not remove every file. " + lastError.Message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!quiet)
|
||||
Console.Out.WriteLine("WiiCompiled was removed from this computer.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static void TryCleanup(string description, bool quiet, Action cleanup)
|
||||
{
|
||||
try
|
||||
{
|
||||
cleanup();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!quiet)
|
||||
Console.Error.WriteLine($"Uninstall could not {description}; continuing. {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum MoveFileFlags : uint { DelayUntilReboot = 0x4 }
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern bool MoveFileEx(string existingFileName, string? newFileName, MoveFileFlags flags);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>WiiCompiled.Setup</AssemblyName>
|
||||
<RootNamespace>WiiCompiled.Setup</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Version>0.2.13</Version>
|
||||
<Authors>patchzy</Authors>
|
||||
<Product>WiiCompiled</Product>
|
||||
<Description>Command-line installer and launcher for WiiCompiled</Description>
|
||||
<DebugType>embedded</DebugType>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,61 @@
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
|
||||
internal static class WorkspaceTimestamps
|
||||
{
|
||||
public static void MarkChangedFiles(string installedWorkspace, string stagedWorkspace,
|
||||
Action<string> diagnostic, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!Directory.Exists(installedWorkspace) || !Directory.Exists(stagedWorkspace)) return;
|
||||
installedWorkspace = Path.GetFullPath(installedWorkspace);
|
||||
stagedWorkspace = Path.GetFullPath(stagedWorkspace);
|
||||
|
||||
var stampUtc = DateTime.UtcNow;
|
||||
var changed = 0;
|
||||
var unchanged = 0;
|
||||
foreach (var stagedFile in Directory.EnumerateFiles(stagedWorkspace, "*",
|
||||
SearchOption.AllDirectories))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var relative = Path.GetRelativePath(stagedWorkspace, stagedFile);
|
||||
if (FileUnchanged(Path.Combine(installedWorkspace, relative), stagedFile))
|
||||
{
|
||||
unchanged++;
|
||||
continue;
|
||||
}
|
||||
File.SetLastWriteTimeUtc(stagedFile, stampUtc);
|
||||
changed++;
|
||||
}
|
||||
diagnostic($"Marked {changed} changed workspace file(s) for recompilation; " +
|
||||
$"{unchanged} unchanged file(s) keep the incremental build cache valid.");
|
||||
}
|
||||
|
||||
private static bool FileUnchanged(string installedFile, string stagedFile)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(installedFile)) return false;
|
||||
using var installed = File.OpenRead(installedFile);
|
||||
using var staged = File.OpenRead(stagedFile);
|
||||
if (installed.Length != staged.Length) return false;
|
||||
var installedBuffer = new byte[81920];
|
||||
var stagedBuffer = new byte[81920];
|
||||
while (true)
|
||||
{
|
||||
var installedRead = installed.ReadAtLeast(installedBuffer, installedBuffer.Length,
|
||||
throwOnEndOfStream: false);
|
||||
var stagedRead = staged.ReadAtLeast(stagedBuffer, stagedBuffer.Length,
|
||||
throwOnEndOfStream: false);
|
||||
if (installedRead != stagedRead) return false;
|
||||
if (installedRead == 0) return true;
|
||||
if (!installedBuffer.AsSpan(0, installedRead)
|
||||
.SequenceEqual(stagedBuffer.AsSpan(0, stagedRead)))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="WiiCompiled.Setup" />
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -0,0 +1,198 @@
|
||||
|
||||
# WiiCompiled
|
||||
|
||||
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
|
||||
anywhere at runtime.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> There is no Nintendo code, no assets and no game data anywhere in this project or its releases.
|
||||
> You need your own legally dumped copy of the PAL version of the game. Setup only ships the
|
||||
> toolchain, the translation runs on your machine against your disc image, and nothing ever gets
|
||||
> uploaded.
|
||||
|
||||
[What is a github, I just want to play](https://github.com/TeamWheelWizard/WheelWizard/releases/latest)
|
||||
|
||||
---
|
||||
|
||||
## What it does
|
||||
|
||||
**Unlocked framerate with interpolation.**
|
||||
The original game is hard-locked to 60 fps. The runtime can generate interpolated frames in between, so on a
|
||||
120/144 Hz monitor things genuinely look smoother.
|
||||
|
||||
> [!WARNING]
|
||||
> Interpolation is experimental right now and will show artifacts in specific scenarios.
|
||||
|
||||
**Any aspect ratio you want.**
|
||||
Drag the window bigger, wider, whatever, the camera adjusts
|
||||
live.
|
||||
|
||||
**Native rendering via aurora.**
|
||||
The graphics layer is built on
|
||||
[aurora](https://github.com/encounter/aurora). Aurora is a source-level GameCube & Wii compatibility layer.
|
||||
|
||||
**High internal resolution.**
|
||||
Play at several times the console's resolution.
|
||||
|
||||
**Music ducking.**
|
||||
Start playing something else, Spotify, a YouTube video, and
|
||||
the game automatically mutes its own music until the other audio stops. Optional, if you'd
|
||||
rather it didn't. All audio that shows in your display media controls on your windows pc fall under this.
|
||||
|
||||
**An in-game settings bar.**
|
||||
Press **F10** while the game window has focus:
|
||||
- Internal resolution
|
||||
- FPS counter
|
||||
- Controller assignment for all four ports
|
||||
- Full per-controller button mapping
|
||||
- Volume, instant mute, and the music ducking toggle
|
||||
|
||||
Everything you change is saved to `Config.toml` on the spot and restored next launch.
|
||||
|
||||
**Real controller support.**
|
||||
Controllers are fed to the game as a GameCube controller.
|
||||
The port does NOT pretend to be a Wii Remote or Classic Controller.
|
||||
Mappings are positional (`south`, `east`, `west`, `north`) rather than Xbox-labelled, so the
|
||||
same config makes sense on Xbox, PlayStation, Nintendo and generic SDL pads alike, and extra
|
||||
inputs like paddles, touchpads and share buttons show up when the hardware reports them.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Windows 10 or 11, 64-bit
|
||||
- GPU: GTX 1650 / RX 6400 / Arc A310 or higher
|
||||
- CPU: Intel Core i5-8400 / AMD Ryzen 5 2600 (4c/6c, ~3.5GHz+) or higher
|
||||
- About 20 GB of free disk space during installation
|
||||
- A clean, unmodified **PAL `RMCP01`** disc image of Mario Kart Wii, dumped by you. ISO, GCM,
|
||||
GCZ, CISO, WBFS, WIA and RVZ are accepted.
|
||||
|
||||
> [!NOTE]
|
||||
> GPU/CPU minimums are set by driver support and D3D12/Vulkan feature requirements, not by the game's actual demands.
|
||||
|
||||
Only the clean PAL revision will work. Anything else (other
|
||||
regions, patched executables) is rejected outright.
|
||||
|
||||
> [!NOTE]
|
||||
> Nobody here will tell you where to get the game. Dumping your own disc is on you, and links to
|
||||
> game files won't be provided or tolerated.
|
||||
|
||||
## Installing
|
||||
|
||||
For an easy experience, use [Wheel Wizard](https://github.com/TeamWheelWizard/WheelWizard). Pick your clean PAL `RMCP01`
|
||||
image under Settings, turn on **WiiCompiled (beta)**, and hit install from the Home page.
|
||||
Wheel Wizard downloads the setup tool from this repo and walks you through install, updates and
|
||||
launching. The backend itself is deliberately command-line only, Wheel Wizard is a wrapper around it.
|
||||
|
||||
> [!CAUTION]
|
||||
> Only take builds from this repository's
|
||||
> [Releases](https://github.com/patchzyy/Wiicompiled/releases) page. If someone's sharing an
|
||||
> installer through Discord or some random download site, don't touch it!!
|
||||
|
||||
## Retro Rewind
|
||||
|
||||
[Retro Rewind](https://wiki.tockdom.com/wiki/Retro_Rewind), ZPL's Mario Kart Wii mod distribution,
|
||||
can be built as its **own static profile**: instead of applying `Code.pul` as runtime patches,
|
||||
the Kamek/Pulsar code is statically translated together with the base game into a separate native
|
||||
executable.
|
||||
|
||||
Wheel Wizard drives this too.
|
||||
|
||||
## Building from source
|
||||
|
||||
Owning the game is still required even if you compile everything yourself.
|
||||
|
||||
You'll need: .NET 8 SDK, CMake, Ninja, and LLVM/Clang (the shipped build uses LLVM-MinGW targeting
|
||||
`x86-64-v3`).
|
||||
|
||||
Build the translator:
|
||||
|
||||
```powershell
|
||||
dotnet build translator/Translator.sln -c Release
|
||||
```
|
||||
|
||||
The default test suite needs no binaries and no host C++ compiler, so you can hack on the
|
||||
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).
|
||||
|
||||
## FAQ
|
||||
|
||||
**Is this an emulator?**
|
||||
No. Everything is compiled to native code before you ever press play. At runtime there's nothing
|
||||
emulating a Wii CPU or GPU.
|
||||
|
||||
**Do you provide the game?**
|
||||
No. Don't ask. Nothing in this repo or any release contains Nintendo code or assets.
|
||||
|
||||
**Why does setup take so long?**
|
||||
Because we **don't** ship the translated binary, most other recomp projects do, but we
|
||||
don't want to risk it right now, setup has to run a static recompiler over the whole game
|
||||
and then throw a C++ compiler at the result. It's a **one-time cost** on your machine.
|
||||
|
||||
**Which game version works?**
|
||||
Clean PAL `RMCP01`. Other regions and modified executables are **rejected**. Translating
|
||||
them against the wrong manifest would give you a subtly broken game that's miserable to debug for us.
|
||||
|
||||
**Can I recompile other GameCube/Wii games with it?**
|
||||
The translator itself handles DOLs and RELs generically, see
|
||||
`projects/examples/generic-dol.yml`. The catch is that a *playable* port also needs a runtime:
|
||||
audio, input, GX, everything the game touches.
|
||||
|
||||
**The game crashed / stopped with an error.**
|
||||
Errors are deliberately loud instead of quietly swallowed. Send a report along with the run log
|
||||
from `%LOCALAPPDATA%\WiiCompiled\Logs`.
|
||||
|
||||
**Will you fix original bugs?**
|
||||
Not in the base game, behavior identical to real hardware is the goal. Only report things where this port differs
|
||||
from the original game. As for Retro Rewind, some base-game behavior **is** patched, so if it differs from the
|
||||
base game, that's normal. If Retro Rewind behavior differs between Dolphin/Wii and WiiCompiled, open an issue on GitHub.
|
||||
|
||||
**How accurate are the physics?**
|
||||
100% - this is proven by in-game ghosts. Since ghosts are replay files based on inputs rather
|
||||
than tracked positions, matching ghosts prove the physics match across Dolphin/Wii/WiiCompiled.
|
||||
|
||||
**Is it done?**
|
||||
Not fully. The game is in a state where everything should be playable and the physics do match
|
||||
100% with the original game, but compatibility, rendering, networking and performance are all
|
||||
actively being worked on. If you do find an issue, we strongly encourage you to open one on
|
||||
GitHub so we can take a look at it.
|
||||
|
||||
## AI usage
|
||||
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
|
||||
|
||||
- **[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
|
||||
aurora's Direct3D, Vulkan and OpenGL backends.
|
||||
- **[Dolphin Emulator](https://github.com/dolphin-emu/dolphin)** - an invaluable reference for Wii
|
||||
hardware behavior during development, plus the source of the free DSP coefficient ROM and the
|
||||
unmodified default WiiConnect24 bootstrap tree bundled with the runtime.
|
||||
- **[Retro Rewind](https://wiki.tockdom.com/wiki/Retro_Rewind)** by ZPL and team - the mod
|
||||
distribution this project supports.
|
||||
- **[Wheel Wizard](https://github.com/TeamWheelWizard/WheelWizard)** - the mod manager this
|
||||
project integrates with as a launch backend.
|
||||
- Everyone in the static recompilation community.
|
||||
|
||||
Bundled third-party components and their licenses live in
|
||||
[`THIRD-PARTY-NOTICES.md`](THIRD-PARTY-NOTICES.md).
|
||||
|
||||
|
||||
## License
|
||||
|
||||
WiiCompiled is free software: you can redistribute it and/or modify it under the terms of the
|
||||
[GNU General Public License, version 3](LICENSE) as published by the Free Software Foundation.
|
||||
|
||||
WiiCompiled is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
||||
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
Any mkwii distribution making use of WiiCompiled must be licensed under GPL v3.0.
|
||||
|
||||
Not affiliated with, endorsed by, or associated with Nintendo. Mario Kart Wii is a trademark of
|
||||
Nintendo. No Nintendo intellectual property is contained in, distributed with, or obtainable
|
||||
through this project.
|
||||
@@ -0,0 +1,195 @@
|
||||
# Third-Party Notices
|
||||
|
||||
WiiCompiled itself is licensed under the GNU General Public License v3.0
|
||||
(see [`LICENSE`](LICENSE)). It incorporates, links against, or redistributes the third-party
|
||||
components listed below. Each remains under its own license and copyright.
|
||||
|
||||
Nothing listed here is Nintendo intellectual property. This project ships no game code, assets,
|
||||
or data of any kind - see the [README](README.md).
|
||||
|
||||
---
|
||||
|
||||
## Bundled in this repository
|
||||
|
||||
### aurora - MIT
|
||||
|
||||
Copyright (c) 2022 Luke Street.
|
||||
Source: <https://github.com/encounter/aurora> - vendored in `aurora-main/`, license text at
|
||||
`aurora-main/LICENSE`.
|
||||
|
||||
Aurora itself vendors:
|
||||
|
||||
- **magic_enum** 0.7.2 - MIT, Copyright (c) 2019-2021 Daniil Goncharov.
|
||||
`aurora-main/include/magic_enum.hpp`.
|
||||
Source: <https://github.com/Neargye/magic_enum>
|
||||
- **libogc-derived SRAM structures** - zlib-style license, Copyright (c) Michael Wiedenbauer
|
||||
(shagkur) and Dave Murphy (WinterMute). `aurora-main/lib/card/SRAM.hpp`.
|
||||
Source: <https://github.com/devkitPro/libogc>
|
||||
|
||||
> [!NOTE]
|
||||
> Upstream aurora ships `assets/screenshot.png`, a rendered frame from a different Nintendo
|
||||
> title. It is intentionally omitted from this repository.
|
||||
|
||||
### Dolphin Emulator data files - GPL-2.0-or-later
|
||||
|
||||
Copyright (c) 2003+ Dolphin Emulator Project.
|
||||
Source: <https://github.com/dolphin-emu/dolphin> - license at
|
||||
<https://github.com/dolphin-emu/dolphin/blob/master/COPYING>
|
||||
|
||||
Two data sets from Dolphin's `Data/Sys` tree are redistributed here under GPL-2.0-or-later:
|
||||
|
||||
| File(s) | Upstream path | Notes |
|
||||
| --- | --- | --- |
|
||||
| `runtime/assets/dsp/dsp_coef.bin` | `Data/Sys/GC/dsp_coef.bin` | Free DSP polyphase-resampling coefficient ROM written by the Dolphin team. 4096 bytes, SHA-256 `D7741279C2E8EC5C5FB318F8FBDD6DE6BF583520D288E836A5383233A4238179`. The runtime verifies this hash at build time. |
|
||||
| `runtime/assets/wii/shared2/wc24/**` | `Data/Sys/Wii/shared2/wc24` | Dolphin's unmodified default WiiConnect24 bootstrap tree, used to seed a newly created per-user NAND on first run. It is byte-identical to the upstream directory. `nwc24dl.bin` contains Dolphin's default task list, which references Nintendo endpoints shut down in 2014; `nwc24msg.cfg` contains Dolphin's placeholder account fields, not a real user's account data. |
|
||||
|
||||
Neither contains Nintendo executable code or game assets; both are redistributed under
|
||||
GPL-2.0-or-later from Dolphin's `Data/Sys` tree.
|
||||
|
||||
SHA-256 hashes for the WiiConnect24 bootstrap tree:
|
||||
|
||||
| File | SHA-256 |
|
||||
| --- | --- |
|
||||
| `runtime/assets/wii/shared2/wc24/misc.bin` | `13DD5B6B2682DEFD3B23AFD8E2983D00EDC25BD4DC28A8389380DEE0EC45A4A5` |
|
||||
| `runtime/assets/wii/shared2/wc24/nwc24dl.bin` | `057B6F840C19B41CE080318BC7E717E2B910965CE72AB781A7E319017636C38E` |
|
||||
| `runtime/assets/wii/shared2/wc24/nwc24fl.bin` | `ED94AF416C47ED3BC2C944EBCD1D734B8935D9697FEB0F7039D8FEA3EC514C18` |
|
||||
| `runtime/assets/wii/shared2/wc24/nwc24fls.bin` | `C3A4A5649D6ED2322A0DE98D2258B96A6A1D3C0179854FD21E9835D529736822` |
|
||||
| `runtime/assets/wii/shared2/wc24/nwc24msg.cbk` | `7AFEBF33EEB0035397CC74E15E892E700CD2903641D26562F5D46CFBB6171109` |
|
||||
| `runtime/assets/wii/shared2/wc24/nwc24msg.cfg` | `7AFEBF33EEB0035397CC74E15E892E700CD2903641D26562F5D46CFBB6171109` |
|
||||
| `runtime/assets/wii/shared2/wc24/mbox/Readme.txt` | `E5A888912968050C6C1D46D1C364C324684E1D15AAA62CFE36CF7FCE2C687B21` |
|
||||
| `runtime/assets/wii/shared2/wc24/mbox/wc24recv.ctl` | `EFA39268E7071941E4FE429D49C86D73BEE952DF95D91D7909C478DD1BC9050A` |
|
||||
| `runtime/assets/wii/shared2/wc24/mbox/wc24recv.mbx` | `DD2AD8C9FB38884523459963BFAEC5D5AEAA5FD20EFCDC209764D461E690E435` |
|
||||
| `runtime/assets/wii/shared2/wc24/mbox/wc24send.ctl` | `430C3795F1A0AEB198BF626A4A2FF6D123321D453807DD7B904DC3B74DB35D13` |
|
||||
| `runtime/assets/wii/shared2/wc24/mbox/wc24send.mbx` | `C248DC031CE09F7BE1E55956B6F173E79D6A47D913C22A16593C4687325692B7` |
|
||||
|
||||
Dolphin was also used extensively as a behavioural reference during development of this project's
|
||||
hardware and IOS high-level implementations.
|
||||
|
||||
### Dolphin Emulator Riivolution code - GPL-2.0-or-later
|
||||
|
||||
Copyright (c) 2021 Dolphin Emulator Project.
|
||||
The runtime's Riivolution patch handling is a port of Dolphin's
|
||||
`Source/Core/DiscIO/RiivolutionParser.{h,cpp}` and the external-path resolution rules of
|
||||
`Source/Core/DiscIO/RiivolutionPatcher.cpp`, adapted in
|
||||
`runtime/include/hle/riivolution_contract.h` and `runtime/src/hle/storage/riivolution.cpp`
|
||||
(both marked `SPDX-License-Identifier: GPL-2.0-or-later`).
|
||||
Source: <https://github.com/dolphin-emu/dolphin>
|
||||
|
||||
### pugixml - MIT
|
||||
|
||||
Copyright (c) 2006-2025 Arseny Kapoulkine.
|
||||
The Riivolution XML reader uses pugixml 1.15, vendored in
|
||||
`runtime/third_party/pugixml` from commit `ee86beb30e4973f5feffe3ce63bfa4fbadf72f38`.
|
||||
Source and license: <https://github.com/zeux/pugixml>
|
||||
|
||||
### Crypto++ 8.9.0 - Boost Software License 1.0 / public domain
|
||||
|
||||
Copyright (c) 1995-2019 Wei Dai and contributors.
|
||||
The runtime uses Crypto++ for SHA-1 and sect233r1 ECDSA key derivation and signing. Its portable
|
||||
sources are vendored in `runtime/third_party/cryptopp`; assembly implementations are disabled.
|
||||
Source: <https://github.com/weidai11/cryptopp/tree/CRYPTOPP_8_9_0>. Full license text:
|
||||
`runtime/third_party/cryptopp/License.txt`.
|
||||
|
||||
### toml11 4.4.0 - MIT
|
||||
|
||||
Copyright (c) 2017 Toru Niina.
|
||||
The runtime configuration reader and scalar string writer use the single-header distribution,
|
||||
vendored in `runtime/third_party/toml11`.
|
||||
Source: <https://github.com/ToruNiina/toml11/tree/v4.4.0>. Full license text:
|
||||
`runtime/third_party/toml11/LICENSE`.
|
||||
|
||||
### YamlDotNet - MIT
|
||||
|
||||
Copyright (c) Antoine Aubry and contributors.
|
||||
Referenced by `translator/src/Translator.Core`. Source: <https://github.com/aaubry/YamlDotNet>
|
||||
|
||||
---
|
||||
|
||||
## Fetched at build time and redistributed in release builds
|
||||
|
||||
These are pinned in `aurora-main/extern/CMakeLists.txt` and
|
||||
`aurora-main/cmake/AuroraDawnProvider.cmake`. They are not stored in this repository; the build
|
||||
downloads them, and release installers carry the resulting binaries. Their license texts are
|
||||
included in the installer's `licenses/` folder.
|
||||
|
||||
| Component | Version | License | Upstream |
|
||||
| --- | --- | --- | --- |
|
||||
| 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> |
|
||||
| SDL | 3.4.4 | zlib | <https://github.com/libsdl-org/SDL> |
|
||||
| Abseil | LTS 20240722.0 | Apache-2.0 | <https://github.com/abseil/abseil-cpp> |
|
||||
| Dear ImGui | 1.91.9b-docking | MIT | <https://github.com/ocornut/imgui> |
|
||||
| {fmt} | 11.1.4 | MIT | <https://github.com/fmtlib/fmt> |
|
||||
| xxHash | 0.8.3 | BSD-2-Clause | <https://github.com/Cyan4973/xxHash> |
|
||||
| zlib | 1.3.2 | zlib | <https://github.com/madler/zlib> |
|
||||
| libpng | 1.6.58 | PNG Reference Library License v2 | <https://github.com/pnggroup/libpng> |
|
||||
| FreeType | 2.14.3 | **FreeType License (FTL)** - see below | <https://freetype.org/> |
|
||||
| Zstandard | 1.5.7 | **BSD-3-Clause** - see below | <https://github.com/facebook/zstd> |
|
||||
| SQLite | 3.51.3 amalgamation | Public domain | <https://sqlite.org/> |
|
||||
| Tracy Profiler | pinned commit | BSD-3-Clause | <https://github.com/wolfpld/tracy> |
|
||||
| C++/WinRT | - | MIT (Microsoft) | <https://github.com/microsoft/cppwinrt> |
|
||||
|
||||
### Dual-licensed components - elections made by this project
|
||||
|
||||
- **FreeType** is offered under the FreeType License (FTL) or GPL-2.0. **This project elects the
|
||||
FreeType License.** The FTL requires the following credit, which is given here and reproduced in
|
||||
distributed builds:
|
||||
|
||||
> Portions of this software are copyright © 2026 The FreeType Project (www.freetype.org).
|
||||
> All rights reserved.
|
||||
|
||||
- **Zstandard** is offered under BSD-3-Clause or GPL-2.0. **This project elects BSD-3-Clause.**
|
||||
Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
|
||||
## Bundled in the setup executable's toolkit payload
|
||||
|
||||
The distributed `WiiCompiled-Setup.exe` carries a build toolkit so that translation and
|
||||
compilation can run on a machine with nothing preinstalled. These tools are redistributed
|
||||
unmodified, with their license texts, in the installer's `licenses/` folder.
|
||||
|
||||
| Component | License | Upstream |
|
||||
| --- | --- | --- |
|
||||
| llvm-mingw (Clang, LLD, libc++, libunwind, MinGW-w64 runtime) | Apache-2.0 with LLVM Exception; MinGW-w64 runtime under its own permissive terms; bundled GNU utilities under GPL-2.0-or-later or GPL-3.0-or-later | <https://github.com/mstorsjo/llvm-mingw> |
|
||||
| CMake | BSD-3-Clause | <https://cmake.org/> |
|
||||
| Ninja | Apache-2.0 | <https://ninja-build.org/> |
|
||||
| DolphinTool (disc image extraction) | GPL-2.0-or-later | <https://github.com/dolphin-emu/dolphin> |
|
||||
| Microsoft Visual C++ Runtime (`vcruntime140.dll`, `vcruntime140_1.dll`, `msvcp140.dll`) | Microsoft redistributable terms | Microsoft Visual Studio |
|
||||
| `dxil.dll` | Microsoft redistributable (proprietary signing library) | Microsoft |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Several toolkit components are GPL-licensed (DolphinTool, and the GNU utilities inside
|
||||
> llvm-mingw). Their complete corresponding source is available from the upstream projects linked
|
||||
> above at their pinned versions, and this project will supply it on request for the exact versions
|
||||
> shipped in any given release. Pins live in `Launcher/Prepare-PortableTools.ps1` and
|
||||
> `Launcher/NativeBuildFlags.ps1`.
|
||||
|
||||
---
|
||||
|
||||
## Development-only dependencies
|
||||
|
||||
Not redistributed in any release artifact.
|
||||
|
||||
| Component | License |
|
||||
| --- | --- |
|
||||
| xUnit.net 2.4.2, xunit.runner.visualstudio 2.4.5 | Apache-2.0 |
|
||||
| Microsoft.NET.Test.Sdk 17.6.0 | MIT |
|
||||
| coverlet.collector 6.0.0 | MIT |
|
||||
| .NET 8 SDK | MIT |
|
||||
|
||||
---
|
||||
|
||||
## Reference material
|
||||
|
||||
Not code, but the documentation this project depends on:
|
||||
|
||||
- [WiiBrew](https://wiibrew.org/wiki/) - Wii hardware and IOS documentation.
|
||||
- [Custom Mario Kart Wiiki (Tockdom)](https://wiki.tockdom.com/) - Mario Kart Wii file formats and
|
||||
modding documentation.
|
||||
- [Retro Rewind](https://wiki.tockdom.com/wiki/Retro_Rewind) by ZPL - the mod distribution this
|
||||
project can build as a static profile. No Retro Rewind content is redistributed here; users
|
||||
supply their own copy.
|
||||
|
||||
---
|
||||
|
||||
If you believe a component is missing or misattributed here, please open an issue.
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
BasedOnStyle: LLVM
|
||||
ColumnLimit: 120
|
||||
UseTab: Never
|
||||
TabWidth: 2
|
||||
---
|
||||
Language: Cpp
|
||||
DerivePointerAlignment: false
|
||||
PointerAlignment: Left
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveAssignments: false
|
||||
IndentCaseLabels: false
|
||||
AllowShortBlocksOnASingleLine: Always
|
||||
AlignOperands: true
|
||||
AlignTrailingComments: true
|
||||
AlwaysBreakBeforeMultilineStrings: true
|
||||
AlwaysBreakTemplateDeclarations: Yes
|
||||
BreakConstructorInitializersBeforeComma: true
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
AllowShortFunctionsOnASingleLine: All
|
||||
Cpp11BracedListStyle: true
|
||||
NamespaceIndentation: None
|
||||
BinPackArguments: true
|
||||
BinPackParameters: true
|
||||
SortIncludes: false
|
||||
AccessModifierOffset: -2
|
||||
ConstructorInitializerIndentWidth: 0
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: true
|
||||
@@ -0,0 +1,10 @@
|
||||
.buildcache/
|
||||
.DS_Store
|
||||
.idea/
|
||||
.cache/
|
||||
.vscode/
|
||||
.vs/
|
||||
build/
|
||||
cmake-build-*/
|
||||
CMakeUserPresets.json
|
||||
out/
|
||||
@@ -0,0 +1,83 @@
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
project(aurora LANGUAGES C CXX)
|
||||
if (APPLE)
|
||||
enable_language(OBJC)
|
||||
endif()
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
option(AURORA_ENABLE_GX "Enable GX implementation and WebGPU renderer" ON)
|
||||
option(AURORA_ENABLE_DVD "Enable DVD implementation backed by nod" OFF) # TODO: need to fix tests
|
||||
option(AURORA_ENABLE_CARD "Enable CARD implementation based on kabufuda" ON)
|
||||
option(AURORA_CACHE_USE_ZSTD "Compress WebGPU cache entries with zstd" ON)
|
||||
|
||||
# Dependency versions
|
||||
set(AURORA_DAWN_VERSION "v20260603.191052" CACHE STRING "Dawn version tag (https://github.com/encounter/dawn-build/releases)")
|
||||
set(AURORA_SDL3_VERSION "3.4.4" CACHE STRING "SDL3 version tag (https://github.com/libsdl-org/SDL/releases)")
|
||||
set(AURORA_NOD_VERSION "v2.0.0-alpha.8" CACHE STRING "nod version tag (https://github.com/encounter/nod/releases)")
|
||||
|
||||
# Platform-specific defaults
|
||||
if (CMAKE_CROSSCOMPILING)
|
||||
# Cross-compiling: vendor everything, don't use system packages
|
||||
set(_default_provider "vendor")
|
||||
set(_default_linkage "static")
|
||||
else ()
|
||||
set(_default_provider "auto")
|
||||
if (WIN32)
|
||||
set(_default_linkage "shared")
|
||||
else ()
|
||||
set(_default_linkage "static")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
# Dawn/WebGPU (if AURORA_ENABLE_GX)
|
||||
set(AURORA_DAWN_PROVIDER "${_default_provider}" CACHE STRING
|
||||
"How to provide Dawn: auto, vendor (build from source), system (find_package/imported), package (download prebuilt)")
|
||||
set_property(CACHE AURORA_DAWN_PROVIDER PROPERTY STRINGS auto vendor system package)
|
||||
set(AURORA_DAWN_LINKAGE "${_default_linkage}" CACHE STRING "Dawn linkage type preference")
|
||||
set_property(CACHE AURORA_DAWN_LINKAGE PROPERTY STRINGS shared static)
|
||||
|
||||
# SDL3
|
||||
set(AURORA_SDL3_PROVIDER "${_default_provider}" CACHE STRING
|
||||
"How to provide SDL3: auto, vendor (build from source), system (find_package/imported), or package (download prebuilt)")
|
||||
set_property(CACHE AURORA_SDL3_PROVIDER PROPERTY STRINGS auto vendor system package)
|
||||
set(AURORA_SDL3_LINKAGE "${_default_linkage}" CACHE STRING "SDL3 linkage type preference")
|
||||
set_property(CACHE AURORA_SDL3_LINKAGE PROPERTY STRINGS shared static)
|
||||
|
||||
# nod (if AURORA_ENABLE_DVD)
|
||||
set(AURORA_NOD_PROVIDER "${_default_provider}" CACHE STRING
|
||||
"How to provide nod: auto, vendor (build from source), system (find_package/imported), package (download prebuilt)")
|
||||
set_property(CACHE AURORA_NOD_PROVIDER PROPERTY STRINGS auto vendor system package)
|
||||
set(AURORA_NOD_LINKAGE "${_default_linkage}" CACHE STRING "nod linkage type preference")
|
||||
set_property(CACHE AURORA_NOD_LINKAGE PROPERTY STRINGS shared static)
|
||||
|
||||
add_subdirectory(extern)
|
||||
|
||||
include(cmake/aurora_core.cmake)
|
||||
include(cmake/aurora_os.cmake)
|
||||
if (AURORA_ENABLE_GX)
|
||||
include(cmake/aurora_gx.cmake)
|
||||
include(cmake/aurora_gd.cmake)
|
||||
endif ()
|
||||
include(cmake/aurora_pad.cmake)
|
||||
include(cmake/aurora_ms.cmake)
|
||||
include(cmake/aurora_si.cmake)
|
||||
include(cmake/aurora_main.cmake)
|
||||
include(cmake/aurora_mtx.cmake)
|
||||
include(cmake/aurora_vi.cmake)
|
||||
if(AURORA_ENABLE_CARD)
|
||||
include(cmake/aurora_card.cmake)
|
||||
endif()
|
||||
if (AURORA_ENABLE_DVD)
|
||||
include(cmake/aurora_dvd.cmake)
|
||||
endif ()
|
||||
|
||||
if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
add_subdirectory(examples)
|
||||
endif ()
|
||||
|
||||
if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR AND NOT CMAKE_CROSSCOMPILING)
|
||||
enable_testing()
|
||||
add_subdirectory(tests)
|
||||
endif ()
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2022 Luke Street
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,46 @@
|
||||
<div align="center">
|
||||
<img src="assets/aurora.png" alt="Logo" width="640">
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
Aurora is a source-level GameCube & Wii compatibility layer intended for use with game decompilation projects.
|
||||
|
||||
Originally developed for use in [Metaforce](https://github.com/AxioDL/metaforce), a Metroid Prime reverse engineering project.
|
||||
It now powers several completed source ports, including [Dusklight](https://github.com/TwilitRealm/dusklight).
|
||||
|
||||
### Features
|
||||
|
||||
- Application layer using SDL3
|
||||
- Runs on Windows, Linux, macOS, iOS, tvOS, Android
|
||||
- GX compatibility layer
|
||||
- Graphics API support: D3D12, Vulkan, Metal
|
||||
- Highly accurate and performant GX implementation
|
||||
- Robust pipeline cache system with "transferable" cache support for releases
|
||||
- Dolphin-compatible texture pack support
|
||||
- Widescreen & resolution scaling support
|
||||
- Custom APIs for offscreen rendering
|
||||
- PAD compatibility layer
|
||||
- Utilizes `SDL_Gamepad` for wide controller support, including GameCube controller adapters
|
||||
- Automatically saves and loads controller bindings and port mappings
|
||||
- Gyro & mouse support
|
||||
- DVD compatibility layer
|
||||
- Utilizes [nod](https://github.com/encounter/nod) to support all GameCube/Wii disc image types, including RVZ
|
||||
- CARD compatibility layer
|
||||
- Full compatibility with Dolphin `.gci` and `.raw` for game saves
|
||||
- [Dear ImGui](https://github.com/ocornut/imgui) built-in for simple debug UIs
|
||||
|
||||
### Graphics
|
||||
|
||||
The GX compatibility layer is built on top of [WebGPU](https://www.w3.org/TR/webgpu/), a cross-platform graphics API
|
||||
abstraction layer. WebGPU allows targeting all major platforms simultaneously with minimal overhead. The WebGPU
|
||||
implementation used is Chromium's [Dawn](https://dawn.googlesource.com/dawn/).
|
||||
|
||||

|
||||
|
||||
### Building
|
||||
|
||||
See [docs/building.md](docs/building.md) for build instructions, CMake integration, and configuration options.
|
||||
|
||||
### License
|
||||
|
||||
Aurora is licensed under the [MIT License](LICENSE).
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,55 @@
|
||||
include_guard(GLOBAL)
|
||||
|
||||
# aurora_copy_runtime_dlls(<executable_target> [<additional_targets>...])
|
||||
#
|
||||
# POST_BUILD command that copies all TARGET_RUNTIME_DLLS from the given targets
|
||||
# next to the executable, plus any extra DLLs that Dawn loads dynamically
|
||||
# (dxcompiler.dll, dxil.dll).
|
||||
function(aurora_copy_runtime_dlls target)
|
||||
if (NOT WIN32)
|
||||
return()
|
||||
endif ()
|
||||
|
||||
# Copy TARGET_RUNTIME_DLLS for each target
|
||||
foreach (_t IN ITEMS ${target} ${ARGN})
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_RUNTIME_DLLS:${_t}> $<TARGET_FILE_DIR:${target}>
|
||||
COMMAND_EXPAND_LISTS
|
||||
)
|
||||
endforeach ()
|
||||
|
||||
# Copy DXC DLLs that Dawn loads via LoadLibrary at runtime
|
||||
if (TARGET dawn::webgpu_dawn)
|
||||
get_target_property(_dawn_loc dawn::webgpu_dawn IMPORTED_LOCATION)
|
||||
if (NOT _dawn_loc)
|
||||
get_target_property(_dawn_loc dawn::webgpu_dawn IMPORTED_LOCATION_RELEASE)
|
||||
endif ()
|
||||
if (_dawn_loc)
|
||||
cmake_path(GET _dawn_loc PARENT_PATH _dawn_bin_dir)
|
||||
foreach (_dll IN ITEMS dxcompiler.dll dxil.dll)
|
||||
if (EXISTS "${_dawn_bin_dir}/${_dll}")
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_dawn_bin_dir}/${_dll}" $<TARGET_FILE_DIR:${target}>
|
||||
)
|
||||
endif ()
|
||||
endforeach ()
|
||||
endif ()
|
||||
endif ()
|
||||
endfunction()
|
||||
|
||||
# aurora_install_runtime_dlls(<executable_target> <install_destination>)
|
||||
#
|
||||
# INSTALL rule that copies all DLLs already placed next to the built target
|
||||
# into the install destination.
|
||||
function(aurora_install_runtime_dlls target install_destination)
|
||||
if (NOT WIN32)
|
||||
return()
|
||||
endif ()
|
||||
|
||||
install(CODE
|
||||
"file(GLOB _aurora_runtime_dlls \"$<TARGET_FILE_DIR:${target}>/*.dll\")\n\
|
||||
if(_aurora_runtime_dlls)\n\
|
||||
file(INSTALL DESTINATION \"${install_destination}\" TYPE FILE FILES \${_aurora_runtime_dlls})\n\
|
||||
endif()"
|
||||
)
|
||||
endfunction()
|
||||
@@ -0,0 +1,255 @@
|
||||
include_guard(GLOBAL)
|
||||
|
||||
# Resolve Dawn/WebGPU dependency based on AURORA_DAWN_PROVIDER and AURORA_DAWN_LINKAGE.
|
||||
#
|
||||
# After this module runs:
|
||||
# - dawn::webgpu_dawn target is available
|
||||
# - dawn::dawncpp_headers target is available (for tests)
|
||||
# - DAWN_ENABLE_* backend variables are set for the target platform
|
||||
# - AURORA_DAWN_IS_SHARED is set to TRUE/FALSE
|
||||
|
||||
# When using a non-vendored Dawn, we don't get DAWN_ENABLE_* from its build.
|
||||
# Infer from the target platform instead.
|
||||
function(_aurora_dawn_set_platform_backends)
|
||||
if (WIN32)
|
||||
set(DAWN_ENABLE_D3D12 ON CACHE INTERNAL "")
|
||||
# Disable D3D11 because this Aurora fork does not support it.
|
||||
set(DAWN_ENABLE_D3D11 OFF CACHE INTERNAL "")
|
||||
set(DAWN_ENABLE_VULKAN ON CACHE INTERNAL "")
|
||||
set(DAWN_ENABLE_METAL OFF CACHE INTERNAL "")
|
||||
set(DAWN_ENABLE_DESKTOP_GL OFF CACHE INTERNAL "")
|
||||
set(DAWN_ENABLE_OPENGLES OFF CACHE INTERNAL "")
|
||||
set(DAWN_ENABLE_NULL ON CACHE INTERNAL "")
|
||||
elseif (APPLE)
|
||||
set(DAWN_ENABLE_D3D12 OFF CACHE INTERNAL "")
|
||||
set(DAWN_ENABLE_D3D11 OFF CACHE INTERNAL "")
|
||||
set(DAWN_ENABLE_VULKAN OFF CACHE INTERNAL "")
|
||||
set(DAWN_ENABLE_METAL ON CACHE INTERNAL "")
|
||||
set(DAWN_ENABLE_DESKTOP_GL OFF CACHE INTERNAL "")
|
||||
set(DAWN_ENABLE_OPENGLES OFF CACHE INTERNAL "")
|
||||
set(DAWN_ENABLE_NULL ON CACHE INTERNAL "")
|
||||
else () # Linux / other
|
||||
set(DAWN_ENABLE_D3D12 OFF CACHE INTERNAL "")
|
||||
set(DAWN_ENABLE_D3D11 OFF CACHE INTERNAL "")
|
||||
set(DAWN_ENABLE_VULKAN ON CACHE INTERNAL "")
|
||||
set(DAWN_ENABLE_METAL OFF CACHE INTERNAL "")
|
||||
set(DAWN_ENABLE_DESKTOP_GL OFF CACHE INTERNAL "")
|
||||
set(DAWN_ENABLE_OPENGLES OFF CACHE INTERNAL "")
|
||||
set(DAWN_ENABLE_NULL ON CACHE INTERNAL "")
|
||||
endif ()
|
||||
endfunction()
|
||||
|
||||
# ── Auto: resolve provider based on platform availability ──
|
||||
set(_aurora_dawn_provider "${AURORA_DAWN_PROVIDER}")
|
||||
if (_aurora_dawn_provider STREQUAL "auto")
|
||||
# Prebuilt Dawn packages available for: windows-{amd64,arm64}, linux-{x86_64,aarch64}, darwin-{arm64,x86_64}
|
||||
set(_has_package FALSE)
|
||||
if (WIN32 AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|x86_64|ARM64|aarch64)$")
|
||||
set(_has_package TRUE)
|
||||
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|aarch64)$")
|
||||
set(_has_package TRUE)
|
||||
elseif (APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|x86_64)$")
|
||||
set(_has_package TRUE)
|
||||
endif ()
|
||||
|
||||
if (_has_package)
|
||||
set(_aurora_dawn_provider "package")
|
||||
else ()
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
|
||||
find_package(Dawn QUIET)
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
|
||||
if (Dawn_FOUND)
|
||||
set(_aurora_dawn_provider "system")
|
||||
else ()
|
||||
set(_aurora_dawn_provider "vendor")
|
||||
endif ()
|
||||
endif ()
|
||||
set(AURORA_DAWN_PROVIDER "${_aurora_dawn_provider}" CACHE STRING "" FORCE)
|
||||
message(STATUS "aurora: Dawn auto-resolved provider: ${_aurora_dawn_provider}")
|
||||
endif ()
|
||||
|
||||
if (_aurora_dawn_provider STREQUAL "vendor")
|
||||
# ── Vendor: FetchContent build from source ──
|
||||
if (NOT TARGET webgpu_dawn)
|
||||
message(STATUS "aurora: Fetching Dawn (provider=vendor, linkage=${AURORA_DAWN_LINKAGE})")
|
||||
|
||||
if (AURORA_DAWN_LINKAGE STREQUAL "shared")
|
||||
set(DAWN_BUILD_MONOLITHIC_LIBRARY SHARED CACHE INTERNAL
|
||||
"Bundle all dawn components into a single shared library.")
|
||||
else ()
|
||||
set(DAWN_BUILD_MONOLITHIC_LIBRARY STATIC CACHE INTERNAL
|
||||
"Bundle all dawn components into a single static library.")
|
||||
endif ()
|
||||
set(DAWN_BUILD_SAMPLES OFF CACHE INTERNAL "Disable Dawn sample applications")
|
||||
set(DAWN_BUILD_BENCHMARKS OFF CACHE INTERNAL "Disable Dawn benchmarks")
|
||||
set(DAWN_SUPPORTS_GLFW_FOR_WINDOWING OFF CACHE INTERNAL "Disable Dawn GLFW windowing")
|
||||
set(DAWN_ENABLE_DESKTOP_GL OFF CACHE INTERNAL "Enable compilation of the OpenGL backend")
|
||||
set(DAWN_ENABLE_OPENGLES OFF CACHE INTERNAL "Enable compilation of the OpenGL ES backend")
|
||||
set(DAWN_FETCH_DEPENDENCIES ON CACHE INTERNAL
|
||||
"Use fetch_dawn_dependencies.py as an alternative to using depot_tools")
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL Linux)
|
||||
set(DAWN_USE_WAYLAND ON CACHE INTERNAL "Enable support for Wayland surface")
|
||||
endif ()
|
||||
set(TINT_BUILD_TESTS OFF CACHE INTERNAL "Build tests")
|
||||
set(TINT_BUILD_CMD_TOOLS OFF CACHE INTERNAL "Build the Tint command line tools")
|
||||
if (NOT DEFINED CMAKE_MSVC_RUNTIME_LIBRARY OR CMAKE_MSVC_RUNTIME_LIBRARY MATCHES "DLL$")
|
||||
set(ABSL_MSVC_STATIC_RUNTIME OFF CACHE INTERNAL "Link static runtime libraries")
|
||||
else ()
|
||||
set(ABSL_MSVC_STATIC_RUNTIME ON CACHE INTERNAL "Link static runtime libraries")
|
||||
endif ()
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(dawn
|
||||
URL "https://github.com/google/dawn/archive/refs/tags/${AURORA_DAWN_VERSION}.tar.gz"
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(dawn)
|
||||
if (NOT TARGET webgpu_dawn)
|
||||
message(FATAL_ERROR "Failed to make dawn available")
|
||||
endif ()
|
||||
else ()
|
||||
message(STATUS "aurora: Using existing Dawn")
|
||||
endif ()
|
||||
|
||||
if (AURORA_DAWN_LINKAGE STREQUAL "shared")
|
||||
set(AURORA_DAWN_IS_SHARED TRUE PARENT_SCOPE)
|
||||
else ()
|
||||
set(AURORA_DAWN_IS_SHARED FALSE PARENT_SCOPE)
|
||||
endif ()
|
||||
|
||||
elseif (_aurora_dawn_provider STREQUAL "system")
|
||||
# ── System: find_package(Dawn) ──
|
||||
message(STATUS "aurora: Using system Dawn (provider=system)")
|
||||
if (NOT Dawn_FOUND)
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
|
||||
find_package(Dawn REQUIRED)
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
|
||||
endif ()
|
||||
if (NOT TARGET dawn::webgpu_dawn)
|
||||
message(FATAL_ERROR "find_package(Dawn) succeeded but dawn::webgpu_dawn target not found")
|
||||
endif ()
|
||||
_aurora_dawn_set_platform_backends()
|
||||
|
||||
get_target_property(_dawn_type dawn::webgpu_dawn TYPE)
|
||||
if (_dawn_type STREQUAL "SHARED_LIBRARY")
|
||||
set(AURORA_DAWN_IS_SHARED TRUE PARENT_SCOPE)
|
||||
else ()
|
||||
set(AURORA_DAWN_IS_SHARED FALSE PARENT_SCOPE)
|
||||
endif ()
|
||||
|
||||
elseif (_aurora_dawn_provider STREQUAL "package")
|
||||
# ── Package: download prebuilt Dawn install tree ──
|
||||
if (NOT AURORA_DAWN_PACKAGE_URL)
|
||||
string(TOLOWER "${CMAKE_SYSTEM_NAME}" _dawn_system)
|
||||
string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _dawn_arch)
|
||||
if (_dawn_system STREQUAL "windows")
|
||||
if (_dawn_arch STREQUAL "x86_64")
|
||||
set(_dawn_arch "amd64")
|
||||
elseif (_dawn_arch STREQUAL "aarch64")
|
||||
set(_dawn_arch "arm64")
|
||||
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")
|
||||
|
||||
# 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")
|
||||
endif ()
|
||||
endif ()
|
||||
message(STATUS "aurora: Fetching prebuilt Dawn package from ${AURORA_DAWN_PACKAGE_URL}")
|
||||
|
||||
set(_dawn_prebuilt_hash_argument "")
|
||||
if (AURORA_DAWN_PACKAGE_URL_HASH)
|
||||
set(_dawn_prebuilt_hash_argument URL_HASH "${AURORA_DAWN_PACKAGE_URL_HASH}")
|
||||
endif ()
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(dawn_prebuilt
|
||||
URL "${AURORA_DAWN_PACKAGE_URL}"
|
||||
${_dawn_prebuilt_hash_argument}
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(dawn_prebuilt)
|
||||
|
||||
# Detect package layout: may be nested in a subdirectory
|
||||
set(_dawn_pkg_dir "${dawn_prebuilt_SOURCE_DIR}")
|
||||
file(GLOB _dawn_subdirs
|
||||
"${dawn_prebuilt_SOURCE_DIR}/dawn-*"
|
||||
"${dawn_prebuilt_SOURCE_DIR}/Dawn-*"
|
||||
)
|
||||
foreach (_sd IN LISTS _dawn_subdirs)
|
||||
if (IS_DIRECTORY "${_sd}")
|
||||
set(_dawn_pkg_dir "${_sd}")
|
||||
break()
|
||||
endif ()
|
||||
endforeach ()
|
||||
if (IS_DIRECTORY "${dawn_prebuilt_SOURCE_DIR}/dawn-install")
|
||||
set(_dawn_pkg_dir "${dawn_prebuilt_SOURCE_DIR}/dawn-install")
|
||||
endif ()
|
||||
|
||||
# Static Dawn packages may link Threads::Threads
|
||||
find_package(Threads QUIET)
|
||||
|
||||
# Find DawnConfig.cmake in the package
|
||||
set(_dawn_cmake_found FALSE)
|
||||
foreach (_cmake_path
|
||||
"${_dawn_pkg_dir}/lib/cmake/Dawn"
|
||||
"${_dawn_pkg_dir}/lib64/cmake/Dawn"
|
||||
"${_dawn_pkg_dir}/cmake/Dawn"
|
||||
"${_dawn_pkg_dir}/cmake"
|
||||
)
|
||||
if (EXISTS "${_cmake_path}/DawnConfig.cmake")
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
|
||||
find_package(Dawn REQUIRED CONFIG PATHS "${_cmake_path}" NO_DEFAULT_PATH)
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
|
||||
set(_dawn_cmake_found TRUE)
|
||||
break()
|
||||
endif ()
|
||||
endforeach ()
|
||||
|
||||
if (NOT _dawn_cmake_found OR NOT TARGET dawn::webgpu_dawn)
|
||||
message(FATAL_ERROR
|
||||
"Dawn package does not contain DawnConfig.cmake.\n"
|
||||
"Searched: ${_dawn_pkg_dir}/{lib,lib64,cmake}/cmake/Dawn/\n"
|
||||
"The package must be a Dawn install tree built with DAWN_ENABLE_INSTALL=ON.")
|
||||
endif ()
|
||||
|
||||
_aurora_dawn_set_platform_backends()
|
||||
|
||||
get_target_property(_dawn_pkg_type dawn::webgpu_dawn TYPE)
|
||||
if (_dawn_pkg_type STREQUAL "SHARED_LIBRARY")
|
||||
set(AURORA_DAWN_IS_SHARED TRUE PARENT_SCOPE)
|
||||
|
||||
else ()
|
||||
set(AURORA_DAWN_IS_SHARED FALSE PARENT_SCOPE)
|
||||
endif ()
|
||||
|
||||
else ()
|
||||
message(FATAL_ERROR "Invalid AURORA_DAWN_PROVIDER: ${AURORA_DAWN_PROVIDER} "
|
||||
"(must be auto, vendor, system, or package)")
|
||||
endif ()
|
||||
|
||||
# Ensure dawn::dawncpp_headers target exists (needed by tests).
|
||||
# When Dawn is vendored, this target is created by Dawn's CMake.
|
||||
# For package/system providers, create it from the same include dirs.
|
||||
if (NOT TARGET dawn::dawncpp_headers)
|
||||
add_library(dawn_dawncpp_headers INTERFACE IMPORTED GLOBAL)
|
||||
get_target_property(_dawn_inc dawn::webgpu_dawn INTERFACE_INCLUDE_DIRECTORIES)
|
||||
if (NOT _dawn_inc)
|
||||
if (TARGET dawn::dawn_public_config)
|
||||
get_target_property(_dawn_inc dawn::dawn_public_config INTERFACE_INCLUDE_DIRECTORIES)
|
||||
endif ()
|
||||
endif ()
|
||||
if (_dawn_inc)
|
||||
target_include_directories(dawn_dawncpp_headers INTERFACE ${_dawn_inc})
|
||||
else ()
|
||||
target_link_libraries(dawn_dawncpp_headers INTERFACE dawn::webgpu_dawn)
|
||||
endif ()
|
||||
add_library(dawn::dawncpp_headers ALIAS dawn_dawncpp_headers)
|
||||
endif ()
|
||||
@@ -0,0 +1,150 @@
|
||||
include_guard(GLOBAL)
|
||||
|
||||
# Resolve nod dependency based on AURORA_NOD_PROVIDER.
|
||||
#
|
||||
# After this module runs:
|
||||
# - nod::nod target is available
|
||||
#
|
||||
# AURORA_NOD_LINKAGE controls shared vs static linkage:
|
||||
# - "shared" (default): prefer nod::nod_shared
|
||||
# - "static": prefer nod::nod_static
|
||||
|
||||
# ── Auto: resolve provider based on platform availability ──
|
||||
set(_aurora_nod_provider "${AURORA_NOD_PROVIDER}")
|
||||
if (_aurora_nod_provider STREQUAL "auto")
|
||||
# Prebuilt nod packages available for: windows-x86_64, linux-x86_64, macos-arm64
|
||||
set(_has_package FALSE)
|
||||
if (WIN32 AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64)$")
|
||||
set(_has_package TRUE)
|
||||
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64)$")
|
||||
set(_has_package TRUE)
|
||||
elseif (APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64)$")
|
||||
set(_has_package TRUE)
|
||||
endif ()
|
||||
|
||||
if (_has_package)
|
||||
set(_aurora_nod_provider "package")
|
||||
else ()
|
||||
find_package(nod QUIET CONFIG)
|
||||
if (nod_FOUND)
|
||||
set(_aurora_nod_provider "system")
|
||||
else ()
|
||||
set(_aurora_nod_provider "vendor")
|
||||
endif ()
|
||||
endif ()
|
||||
set(AURORA_NOD_PROVIDER "${_aurora_nod_provider}" CACHE STRING "" FORCE)
|
||||
message(STATUS "aurora: nod auto-resolved provider: ${_aurora_nod_provider}")
|
||||
endif ()
|
||||
|
||||
if (_aurora_nod_provider STREQUAL "vendor")
|
||||
# ── Vendor: FetchContent + Corrosion build from source ──
|
||||
if (NOT TARGET nod::nod)
|
||||
message(STATUS "aurora: Building nod (provider=vendor, linkage=${AURORA_NOD_LINKAGE})")
|
||||
|
||||
# Corrosion may default to the host Rust target triple (commonly x86_64 on
|
||||
# Windows). Force Rust to i686 for true Win32 builds so nod and its vendored
|
||||
# compression libraries are built for the same architecture as the C/C++ build.
|
||||
if (WIN32 AND CMAKE_SIZEOF_VOID_P EQUAL 4)
|
||||
set(Rust_CARGO_TARGET "i686-pc-windows-msvc" CACHE STRING "Rust target triple" FORCE)
|
||||
set(Rust_RUSTUP_INSTALL_MISSING_TARGET ON CACHE BOOL "Allow Corrosion to install missing Rust targets" FORCE)
|
||||
message(STATUS "aurora: forcing Rust_CARGO_TARGET=${Rust_CARGO_TARGET} for Win32 nod build")
|
||||
endif ()
|
||||
|
||||
# Control whether nod builds shared, static, or both.
|
||||
# Save/restore BUILD_SHARED_LIBS to avoid leaking into other subdirectories.
|
||||
set(_aurora_nod_saved_bsl "${BUILD_SHARED_LIBS}")
|
||||
if (AURORA_NOD_LINKAGE STREQUAL "static")
|
||||
set(BUILD_SHARED_LIBS OFF)
|
||||
else ()
|
||||
set(BUILD_SHARED_LIBS ON)
|
||||
endif ()
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(aurora_nod
|
||||
GIT_REPOSITORY "https://github.com/encounter/nod.git"
|
||||
GIT_TAG "${AURORA_NOD_VERSION}"
|
||||
GIT_SHALLOW TRUE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(aurora_nod)
|
||||
set(BUILD_SHARED_LIBS "${_aurora_nod_saved_bsl}")
|
||||
unset(_aurora_nod_saved_bsl)
|
||||
if (NOT TARGET nod::nod)
|
||||
message(FATAL_ERROR "Failed to make nod available")
|
||||
endif ()
|
||||
else ()
|
||||
message(STATUS "aurora: Using existing nod")
|
||||
endif ()
|
||||
|
||||
elseif (_aurora_nod_provider STREQUAL "system")
|
||||
# ── System: find preinstalled nod ──
|
||||
message(STATUS "aurora: Using system nod (provider=system, linkage=${AURORA_NOD_LINKAGE})")
|
||||
|
||||
if (NOT nod_FOUND)
|
||||
set(_aurora_nod_saved_bsl "${BUILD_SHARED_LIBS}")
|
||||
if (AURORA_NOD_LINKAGE STREQUAL "static")
|
||||
set(BUILD_SHARED_LIBS OFF)
|
||||
else ()
|
||||
set(BUILD_SHARED_LIBS ON)
|
||||
endif ()
|
||||
|
||||
find_package(nod REQUIRED CONFIG)
|
||||
set(BUILD_SHARED_LIBS "${_aurora_nod_saved_bsl}")
|
||||
unset(_aurora_nod_saved_bsl)
|
||||
endif ()
|
||||
if (NOT TARGET nod::nod)
|
||||
message(FATAL_ERROR "find_package(nod) succeeded but nod::nod target not found")
|
||||
endif ()
|
||||
message(STATUS "aurora: Found nod via find_package(nod)")
|
||||
|
||||
elseif (_aurora_nod_provider STREQUAL "package")
|
||||
# ── Package: download prebuilt nod library ──
|
||||
if (NOT AURORA_NOD_PACKAGE_URL)
|
||||
if (WIN32 AND CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
set(_nod_platform "windows-x86_64")
|
||||
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$")
|
||||
set(_nod_platform "linux-x86_64")
|
||||
elseif (APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
|
||||
set(_nod_platform "macos-arm64")
|
||||
else ()
|
||||
message(FATAL_ERROR
|
||||
"AURORA_NOD_PROVIDER=package requires AURORA_NOD_PACKAGE_URL on this platform.\n"
|
||||
"Prebuilt packages: https://github.com/encounter/nod/releases")
|
||||
endif ()
|
||||
set(AURORA_NOD_PACKAGE_URL
|
||||
"https://github.com/encounter/nod/releases/download/${AURORA_NOD_VERSION}/libnod-${_nod_platform}.tar.gz")
|
||||
endif ()
|
||||
message(STATUS "aurora: Fetching prebuilt nod package (provider=package, linkage=${AURORA_NOD_LINKAGE})")
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(nod_prebuilt
|
||||
URL "${AURORA_NOD_PACKAGE_URL}"
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(nod_prebuilt)
|
||||
|
||||
# The prebuilt package ships a CMake config that creates nod::nod_shared,
|
||||
# nod::nod_static, and a default nod::nod (based on BUILD_SHARED_LIBS).
|
||||
# Set BUILD_SHARED_LIBS so the config selects the right default target.
|
||||
set(_aurora_nod_saved_bsl "${BUILD_SHARED_LIBS}")
|
||||
if (AURORA_NOD_LINKAGE STREQUAL "static")
|
||||
set(BUILD_SHARED_LIBS OFF)
|
||||
else ()
|
||||
set(BUILD_SHARED_LIBS ON)
|
||||
endif ()
|
||||
find_package(nod REQUIRED CONFIG
|
||||
PATHS "${nod_prebuilt_SOURCE_DIR}"
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
set(BUILD_SHARED_LIBS "${_aurora_nod_saved_bsl}")
|
||||
unset(_aurora_nod_saved_bsl)
|
||||
if (NOT TARGET nod::nod)
|
||||
message(FATAL_ERROR "Prebuilt nod package did not provide nod::nod target")
|
||||
endif ()
|
||||
|
||||
else ()
|
||||
message(FATAL_ERROR "Invalid AURORA_NOD_PROVIDER: ${AURORA_NOD_PROVIDER} "
|
||||
"(must be auto, vendor, system, or package)")
|
||||
endif ()
|
||||
@@ -0,0 +1,149 @@
|
||||
include_guard(GLOBAL)
|
||||
|
||||
# Resolve SDL3 dependency based on AURORA_SDL3_PROVIDER and AURORA_SDL3_LINKAGE.
|
||||
#
|
||||
# After this module runs, the variable AURORA_SDL3_TARGET is set to the
|
||||
# appropriate target name for use in target_link_libraries() calls.
|
||||
|
||||
# Select the SDL3 target matching the requested linkage.
|
||||
function(_aurora_sdl3_select_target)
|
||||
if (AURORA_SDL3_LINKAGE STREQUAL "static")
|
||||
if (TARGET SDL3::SDL3-static)
|
||||
set(AURORA_SDL3_TARGET SDL3::SDL3-static CACHE INTERNAL "")
|
||||
message(STATUS "aurora: Using static SDL3")
|
||||
return()
|
||||
elseif (TARGET SDL3::SDL3)
|
||||
message(WARNING "aurora: Static SDL3 requested but only shared is available")
|
||||
set(AURORA_SDL3_TARGET SDL3::SDL3 CACHE INTERNAL "")
|
||||
message(STATUS "aurora: Using shared SDL3")
|
||||
return()
|
||||
endif ()
|
||||
else ()
|
||||
if (TARGET SDL3::SDL3)
|
||||
set(AURORA_SDL3_TARGET SDL3::SDL3 CACHE INTERNAL "")
|
||||
message(STATUS "aurora: Using shared SDL3")
|
||||
return()
|
||||
elseif (TARGET SDL3::SDL3-static)
|
||||
message(WARNING "aurora: Shared SDL3 requested but only static is available")
|
||||
set(AURORA_SDL3_TARGET SDL3::SDL3-static CACHE INTERNAL "")
|
||||
message(STATUS "aurora: Using static SDL3")
|
||||
return()
|
||||
endif ()
|
||||
endif ()
|
||||
message(FATAL_ERROR "No SDL3 target found (looked for SDL3::SDL3 and SDL3::SDL3-static)")
|
||||
endfunction()
|
||||
|
||||
# ── Auto: resolve provider based on platform availability ──
|
||||
set(_aurora_sdl3_provider "${AURORA_SDL3_PROVIDER}")
|
||||
if (_aurora_sdl3_provider STREQUAL "auto")
|
||||
# Prebuilt SDL3 packages are available for Windows (MSVC and MinGW)
|
||||
if (WIN32)
|
||||
set(_aurora_sdl3_provider "package")
|
||||
else ()
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
|
||||
find_package(SDL3 QUIET)
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
|
||||
if (SDL3_FOUND)
|
||||
set(_aurora_sdl3_provider "system")
|
||||
else ()
|
||||
set(_aurora_sdl3_provider "vendor")
|
||||
endif ()
|
||||
endif ()
|
||||
set(AURORA_SDL3_PROVIDER "${_aurora_sdl3_provider}" CACHE STRING "" FORCE)
|
||||
message(STATUS "aurora: SDL3 auto-resolved provider: ${_aurora_sdl3_provider}")
|
||||
endif ()
|
||||
|
||||
if (_aurora_sdl3_provider STREQUAL "system")
|
||||
# ── System: find_package(SDL3) ──
|
||||
message(STATUS "aurora: Using system SDL3 (provider=system)")
|
||||
if (NOT SDL3_FOUND)
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
|
||||
find_package(SDL3 REQUIRED)
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
|
||||
endif ()
|
||||
_aurora_sdl3_select_target()
|
||||
|
||||
elseif (_aurora_sdl3_provider STREQUAL "package")
|
||||
# ── Package: download custom build or official SDL3 development package ──
|
||||
if (NOT AURORA_SDL3_PACKAGE_URL)
|
||||
if (MINGW)
|
||||
# WIN32 is also true for MinGW. Select the matching GNU-ABI package
|
||||
# before considering Visual C++ packages.
|
||||
set(AURORA_SDL3_PACKAGE_URL
|
||||
"https://github.com/libsdl-org/SDL/releases/download/release-${AURORA_SDL3_VERSION}/SDL3-devel-${AURORA_SDL3_VERSION}-mingw.tar.gz")
|
||||
elseif (WIN32)
|
||||
# We have custom builds of SDL3 for Win32 x86/AMD64 with libusb support included
|
||||
if (CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|x86)$")
|
||||
string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _sdl3_arch)
|
||||
set(AURORA_SDL3_PACKAGE_URL
|
||||
"https://github.com/encounter/sdl3-build/releases/download/v${AURORA_SDL3_VERSION}/SDL3-windows-${_sdl3_arch}.tar.gz")
|
||||
else ()
|
||||
set(AURORA_SDL3_PACKAGE_URL
|
||||
"https://github.com/libsdl-org/SDL/releases/download/release-${AURORA_SDL3_VERSION}/SDL3-devel-${AURORA_SDL3_VERSION}-VC.zip")
|
||||
endif ()
|
||||
else ()
|
||||
message(FATAL_ERROR
|
||||
"AURORA_SDL3_PROVIDER=package requires AURORA_SDL3_PACKAGE_URL on non-Windows platforms.\n"
|
||||
"Official SDL3 packages: https://github.com/libsdl-org/SDL/releases")
|
||||
endif ()
|
||||
endif ()
|
||||
message(STATUS "aurora: Fetching prebuilt SDL3 package from ${AURORA_SDL3_PACKAGE_URL}")
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(sdl3_prebuilt
|
||||
URL "${AURORA_SDL3_PACKAGE_URL}"
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(sdl3_prebuilt)
|
||||
|
||||
set(_sdl3_search_paths
|
||||
"${sdl3_prebuilt_SOURCE_DIR}/cmake"
|
||||
"${sdl3_prebuilt_SOURCE_DIR}/lib/cmake/SDL3"
|
||||
"${sdl3_prebuilt_SOURCE_DIR}"
|
||||
)
|
||||
file(GLOB _sdl3_subdirs "${sdl3_prebuilt_SOURCE_DIR}/SDL3-*")
|
||||
foreach (_sd IN LISTS _sdl3_subdirs)
|
||||
list(APPEND _sdl3_search_paths
|
||||
"${_sd}/cmake"
|
||||
"${_sd}/lib/cmake/SDL3"
|
||||
"${_sd}"
|
||||
)
|
||||
endforeach ()
|
||||
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
|
||||
find_package(SDL3 REQUIRED CONFIG PATHS ${_sdl3_search_paths} NO_DEFAULT_PATH)
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL OFF)
|
||||
_aurora_sdl3_select_target()
|
||||
|
||||
elseif (_aurora_sdl3_provider STREQUAL "vendor")
|
||||
# ── Vendor: FetchContent build from source ──
|
||||
if (NOT TARGET SDL3-static AND NOT TARGET SDL3-shared)
|
||||
message(STATUS "aurora: Fetching SDL3 (provider=vendor)")
|
||||
|
||||
if (AURORA_SDL3_LINKAGE STREQUAL "static")
|
||||
set(SDL_STATIC ON CACHE BOOL "Build SDL3 static library" FORCE)
|
||||
set(SDL_SHARED OFF CACHE BOOL "Build SDL3 shared library" FORCE)
|
||||
else ()
|
||||
set(SDL_STATIC OFF CACHE BOOL "Build SDL3 static library" FORCE)
|
||||
set(SDL_SHARED ON CACHE BOOL "Build SDL3 shared library" FORCE)
|
||||
endif ()
|
||||
if (WIN32)
|
||||
set(SDL_LIBC ON CACHE BOOL "Use the system C library" FORCE)
|
||||
endif ()
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(SDL
|
||||
URL "https://github.com/libsdl-org/SDL/releases/download/release-${AURORA_SDL3_VERSION}/SDL3-${AURORA_SDL3_VERSION}.tar.gz"
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(SDL)
|
||||
else ()
|
||||
message(STATUS "aurora: Using existing SDL3")
|
||||
endif ()
|
||||
_aurora_sdl3_select_target()
|
||||
|
||||
else ()
|
||||
message(FATAL_ERROR "Invalid AURORA_SDL3_PROVIDER: ${AURORA_SDL3_PROVIDER} "
|
||||
"(must be auto, vendor, system, or package)")
|
||||
endif ()
|
||||
@@ -0,0 +1,17 @@
|
||||
add_library(aurora_card STATIC
|
||||
lib/card/BlockAllocationTable.cpp
|
||||
lib/card/CardRawFile.cpp
|
||||
lib/card/CardGciFolder.cpp
|
||||
lib/card/Directory.cpp
|
||||
lib/card/DolphinCardPath.cpp
|
||||
lib/card/File.cpp
|
||||
lib/card/FileIO.cpp
|
||||
lib/card/SRAM.cpp
|
||||
lib/card/Util.cpp
|
||||
lib/dolphin/card.cpp
|
||||
)
|
||||
add_library(aurora::card ALIAS aurora_card)
|
||||
set_target_properties(aurora_card PROPERTIES FOLDER "aurora")
|
||||
|
||||
target_link_libraries(aurora_card PUBLIC aurora::core)
|
||||
target_include_directories(aurora_card PRIVATE include)
|
||||
@@ -0,0 +1,64 @@
|
||||
add_library(aurora_core STATIC
|
||||
lib/aurora.cpp
|
||||
lib/input.cpp
|
||||
lib/window.cpp
|
||||
lib/logging.cpp
|
||||
lib/system_info.cpp
|
||||
lib/system_info.hpp
|
||||
)
|
||||
add_library(aurora::core ALIAS aurora_core)
|
||||
set_target_properties(aurora_core PROPERTIES FOLDER "aurora")
|
||||
|
||||
target_compile_definitions(aurora_core PUBLIC AURORA TARGET_PC)
|
||||
target_include_directories(aurora_core PUBLIC include)
|
||||
target_link_libraries(aurora_core PUBLIC fmt::fmt ${AURORA_SDL3_TARGET} xxhash)
|
||||
target_link_libraries(aurora_core PRIVATE absl::btree absl::flat_hash_map sqlite3 TracyClient)
|
||||
if (AURORA_ENABLE_GX AND AURORA_CACHE_USE_ZSTD)
|
||||
target_compile_definitions(aurora_core PRIVATE AURORA_CACHE_USE_ZSTD)
|
||||
target_link_libraries(aurora_core PRIVATE libzstd_static)
|
||||
endif ()
|
||||
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL Windows)
|
||||
# stuff for fetching system info.
|
||||
target_link_libraries(aurora_core PRIVATE ntdll dxgi advapi32 user32)
|
||||
elseif (APPLE)
|
||||
target_sources(aurora_core PRIVATE lib/system_info_mac.mm)
|
||||
endif ()
|
||||
|
||||
if (AURORA_ENABLE_GX)
|
||||
target_sources(aurora_core PRIVATE lib/imgui.cpp)
|
||||
target_link_libraries(aurora_core PUBLIC imgui)
|
||||
endif ()
|
||||
|
||||
if (AURORA_ENABLE_GX)
|
||||
target_compile_definitions(aurora_core PUBLIC AURORA_ENABLE_GX WEBGPU_DAWN)
|
||||
target_sources(aurora_core PRIVATE lib/webgpu/gpu.cpp lib/webgpu/gpu_cache.cpp lib/dawn/BackendBinding.cpp)
|
||||
target_link_libraries(aurora_core PRIVATE dawn::webgpu_dawn)
|
||||
if (DAWN_ENABLE_VULKAN)
|
||||
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_VULKAN)
|
||||
endif ()
|
||||
if (DAWN_ENABLE_METAL)
|
||||
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_METAL)
|
||||
target_sources(aurora_core PRIVATE lib/dawn/MetalBinding.mm)
|
||||
set_source_files_properties(lib/dawn/MetalBinding.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
|
||||
target_link_options(aurora_core PUBLIC "LINKER:-weak_framework,Metal")
|
||||
endif ()
|
||||
if (DAWN_ENABLE_D3D11)
|
||||
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_D3D11)
|
||||
endif ()
|
||||
if (DAWN_ENABLE_D3D12)
|
||||
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_D3D12)
|
||||
endif ()
|
||||
if (DAWN_ENABLE_DESKTOP_GL OR DAWN_ENABLE_OPENGLES)
|
||||
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_OPENGL)
|
||||
if (DAWN_ENABLE_DESKTOP_GL)
|
||||
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_DESKTOP_GL)
|
||||
endif ()
|
||||
if (DAWN_ENABLE_OPENGLES)
|
||||
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_OPENGLES)
|
||||
endif ()
|
||||
endif ()
|
||||
if (DAWN_ENABLE_NULL)
|
||||
target_compile_definitions(aurora_core PRIVATE DAWN_ENABLE_BACKEND_NULL)
|
||||
endif ()
|
||||
endif ()
|
||||
@@ -0,0 +1,10 @@
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/AuroraNodProvider.cmake)
|
||||
|
||||
add_library(aurora_dvd STATIC lib/dolphin/dvd/dvd.cpp)
|
||||
add_library(aurora::dvd ALIAS aurora_dvd)
|
||||
set_target_properties(aurora_dvd PROPERTIES FOLDER "aurora")
|
||||
|
||||
target_compile_definitions(aurora_dvd PUBLIC AURORA TARGET_PC)
|
||||
target_include_directories(aurora_dvd PUBLIC include)
|
||||
target_link_libraries(aurora_dvd PUBLIC nod::nod ${AURORA_SDL3_TARGET})
|
||||
target_link_libraries(aurora_dvd PRIVATE fmt::fmt)
|
||||
@@ -0,0 +1,16 @@
|
||||
add_library(aurora_gd STATIC
|
||||
lib/dolphin/gd/GDBase.cpp
|
||||
lib/dolphin/gd/GDGeometry.cpp
|
||||
lib/dolphin/gd/GDIndirect.cpp
|
||||
lib/dolphin/gd/GDLight.cpp
|
||||
lib/dolphin/gd/GDPixel.cpp
|
||||
lib/dolphin/gd/GDTev.cpp
|
||||
lib/dolphin/gd/GDTexture.cpp
|
||||
lib/dolphin/gd/GDTransform.cpp
|
||||
lib/dolphin/gd/gd.hpp
|
||||
lib/dolphin/gd/GDAurora.cpp
|
||||
)
|
||||
add_library(aurora::gd ALIAS aurora_gd)
|
||||
set_target_properties(aurora_gd PROPERTIES FOLDER "aurora")
|
||||
|
||||
target_link_libraries(aurora_gd PUBLIC aurora::gx)
|
||||
@@ -0,0 +1,47 @@
|
||||
add_library(aurora_gx STATIC
|
||||
lib/gfx/clear.cpp
|
||||
lib/gfx/common.cpp
|
||||
lib/gfx/depth_peek.cpp
|
||||
lib/gfx/efb_ram_copy.cpp
|
||||
lib/gfx/efb_ram_encoder.cpp
|
||||
lib/gfx/pipeline_cache.cpp
|
||||
lib/gfx/dds_io.cpp
|
||||
lib/gfx/tex_copy_conv.cpp
|
||||
lib/gfx/tex_palette_conv.cpp
|
||||
lib/gfx/texture.cpp
|
||||
lib/gfx/texture_convert.cpp
|
||||
lib/gfx/texture_replacement.cpp
|
||||
lib/gx/command_processor.cpp
|
||||
lib/gx/fifo.cpp
|
||||
lib/gx/frame_interpolation.cpp
|
||||
lib/gx/gx.cpp
|
||||
lib/gx/pipeline.cpp
|
||||
lib/gx/shader.cpp
|
||||
lib/gx/shader_info.cpp
|
||||
lib/dolphin/gx/GXBump.cpp
|
||||
lib/dolphin/gx/GXCull.cpp
|
||||
lib/dolphin/gx/GXCpu2Efb.cpp
|
||||
lib/dolphin/gx/GXDispList.cpp
|
||||
lib/dolphin/gx/GXDraw.cpp
|
||||
lib/dolphin/gx/GXExtra.cpp
|
||||
lib/dolphin/gx/GXFifo.cpp
|
||||
lib/dolphin/gx/GXFrameBuffer.cpp
|
||||
lib/dolphin/gx/GXGeometry.cpp
|
||||
lib/dolphin/gx/GXGet.cpp
|
||||
lib/dolphin/gx/GXLighting.cpp
|
||||
lib/dolphin/gx/GXManage.cpp
|
||||
lib/dolphin/gx/GXPixel.cpp
|
||||
lib/dolphin/gx/GXTev.cpp
|
||||
lib/dolphin/gx/GXTexture.cpp
|
||||
lib/dolphin/gx/GXTransform.cpp
|
||||
lib/dolphin/gx/GXVert.cpp
|
||||
lib/dolphin/gx/GXAurora.cpp
|
||||
lib/gfx/png_io.cpp
|
||||
lib/gfx/png_io.hpp
|
||||
)
|
||||
add_library(aurora::gx ALIAS aurora_gx)
|
||||
set_target_properties(aurora_gx PROPERTIES FOLDER "aurora")
|
||||
|
||||
target_link_libraries(aurora_gx PUBLIC aurora::core xxhash)
|
||||
target_link_libraries(aurora_gx PRIVATE absl::btree absl::flat_hash_map dawn::webgpu_dawn sqlite3 TracyClient PNG::PNG)
|
||||
target_compile_definitions(aurora_gx PRIVATE WEBGPU_DAWN)
|
||||
@@ -0,0 +1,6 @@
|
||||
add_library(aurora_main STATIC lib/main.cpp)
|
||||
add_library(aurora::main ALIAS aurora_main)
|
||||
set_target_properties(aurora_main PROPERTIES FOLDER "aurora")
|
||||
|
||||
target_include_directories(aurora_main PUBLIC include)
|
||||
target_link_libraries(aurora_main PUBLIC ${AURORA_SDL3_TARGET})
|
||||
@@ -0,0 +1,7 @@
|
||||
add_library(aurora_ms STATIC lib/dolphin/ms/mouse.cpp)
|
||||
add_library(aurora::ms ALIAS aurora_ms)
|
||||
set_target_properties(aurora_ms PROPERTIES FOLDER "aurora")
|
||||
|
||||
target_include_directories(aurora_ms PUBLIC include)
|
||||
target_link_libraries(aurora_ms PUBLIC aurora::core)
|
||||
target_link_libraries(aurora_ms PRIVATE absl::flat_hash_map)
|
||||
@@ -0,0 +1,12 @@
|
||||
add_library(aurora_mtx STATIC
|
||||
lib/dolphin/mtx/mtx.c
|
||||
lib/dolphin/mtx/mtxstack.c
|
||||
lib/dolphin/mtx/mtxvec.c
|
||||
lib/dolphin/mtx/mtx44.c
|
||||
lib/dolphin/mtx/vec.c
|
||||
lib/dolphin/mtx/quat.c
|
||||
)
|
||||
add_library(aurora::mtx ALIAS aurora_mtx)
|
||||
set_target_properties(aurora_mtx PROPERTIES FOLDER "aurora")
|
||||
|
||||
target_include_directories(aurora_mtx PUBLIC include)
|
||||
@@ -0,0 +1,14 @@
|
||||
add_library(aurora_os STATIC lib/dolphin/os/OSInit.cpp
|
||||
lib/dolphin/os/OSMemory.cpp
|
||||
lib/dolphin/os/internal.hpp
|
||||
lib/dolphin/os/OSBootInfo.cpp
|
||||
lib/dolphin/os/OSTime.cpp
|
||||
lib/dolphin/os/OSArena.cpp
|
||||
lib/dolphin/os/OSAlloc.cpp
|
||||
lib/dolphin/os/OSAddress.cpp
|
||||
lib/dolphin/AR.cpp)
|
||||
add_library(aurora::os ALIAS aurora_os)
|
||||
set_target_properties(aurora_os PROPERTIES FOLDER "aurora")
|
||||
|
||||
target_include_directories(aurora_os PUBLIC include)
|
||||
target_link_libraries(aurora_os PRIVATE aurora::core)
|
||||
@@ -0,0 +1,7 @@
|
||||
add_library(aurora_pad STATIC lib/dolphin/pad/pad.cpp)
|
||||
add_library(aurora::pad ALIAS aurora_pad)
|
||||
set_target_properties(aurora_pad PROPERTIES FOLDER "aurora")
|
||||
|
||||
target_include_directories(aurora_pad PUBLIC include)
|
||||
target_link_libraries(aurora_pad PUBLIC aurora::core aurora::si)
|
||||
target_link_libraries(aurora_pad PRIVATE absl::flat_hash_map)
|
||||
@@ -0,0 +1,7 @@
|
||||
add_library(aurora_si STATIC lib/dolphin/si/si.cpp)
|
||||
add_library(aurora::si ALIAS aurora_si)
|
||||
set_target_properties(aurora_si PROPERTIES FOLDER "aurora")
|
||||
|
||||
target_include_directories(aurora_si PUBLIC include)
|
||||
target_link_libraries(aurora_si PUBLIC aurora::core)
|
||||
target_link_libraries(aurora_si PRIVATE absl::flat_hash_map)
|
||||
@@ -0,0 +1,7 @@
|
||||
add_library(aurora_vi STATIC
|
||||
lib/dolphin/vi/vi.cpp
|
||||
)
|
||||
add_library(aurora::vi ALIAS aurora_vi)
|
||||
set_target_properties(aurora_vi PROPERTIES FOLDER "aurora")
|
||||
|
||||
target_link_libraries(aurora_vi PUBLIC aurora::core)
|
||||
@@ -0,0 +1,55 @@
|
||||
# Building
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- CMake 3.25 or later
|
||||
- A C++20 compatible compiler (Clang, GCC, or MSVC)
|
||||
- Git (for cloning)
|
||||
|
||||
## Building the Examples
|
||||
|
||||
To build Aurora's included examples:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/encounter/aurora.git
|
||||
cd aurora
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
cmake --build . --target simple
|
||||
```
|
||||
|
||||
The `simple` example demonstrates a minimal Aurora application with a blue screen. The built executable will be in `build/examples/simple`.
|
||||
|
||||
## Using Aurora in Your Project
|
||||
|
||||
Aurora is designed to be integrated as a library in GameCube/Wii decompilation projects.
|
||||
|
||||
**CMakeLists.txt example:**
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
project(your_game)
|
||||
|
||||
# Add Aurora as a subdirectory
|
||||
add_subdirectory(extern/aurora EXCLUDE_FROM_ALL)
|
||||
|
||||
# Create your executable
|
||||
add_executable(your_game src/main.c)
|
||||
|
||||
# Link against Aurora components
|
||||
target_link_libraries(your_game PRIVATE
|
||||
aurora::core
|
||||
aurora::gx
|
||||
aurora::main
|
||||
aurora::vi
|
||||
)
|
||||
```
|
||||
|
||||
See [examples/simple.c](../examples/simple.c) for a minimal application template.
|
||||
|
||||
## CMake Options
|
||||
|
||||
- `AURORA_ENABLE_GX` (default: ON) - Enable GX implementation and WebGPU renderer
|
||||
- `AURORA_ENABLE_DVD` (default: OFF) - Enable DVD implementation backed by nod
|
||||
- `AURORA_ENABLE_CARD` (default: ON) - Enable CARD implementation based on kabufuda
|
||||
- `AURORA_CACHE_USE_ZSTD` (default: ON) - Compress WebGPU cache entries with zstd
|
||||
@@ -0,0 +1,4 @@
|
||||
if (AURORA_ENABLE_GX)
|
||||
add_executable(simple simple.c)
|
||||
target_link_libraries(simple PRIVATE aurora::core aurora::gx aurora::main aurora::vi)
|
||||
endif ()
|
||||
@@ -0,0 +1,90 @@
|
||||
#include <aurora/aurora.h>
|
||||
#include <aurora/event.h>
|
||||
#include <aurora/main.h>
|
||||
#include <dolphin/gx.h>
|
||||
#include <dolphin/os.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static void log_callback(AuroraLogLevel level, const char* module, const char* message, unsigned int len) {
|
||||
const char* levelStr;
|
||||
FILE* out = stdout;
|
||||
switch (level) {
|
||||
case LOG_DEBUG:
|
||||
levelStr = "DEBUG";
|
||||
break;
|
||||
case LOG_INFO:
|
||||
levelStr = "INFO";
|
||||
break;
|
||||
case LOG_WARNING:
|
||||
levelStr = "WARNING";
|
||||
break;
|
||||
case LOG_ERROR:
|
||||
levelStr = "ERROR";
|
||||
out = stderr;
|
||||
break;
|
||||
case LOG_FATAL:
|
||||
levelStr = "FATAL";
|
||||
out = stderr;
|
||||
break;
|
||||
}
|
||||
fprintf(out, "[%s: %s;%s]\n", levelStr, module, message);
|
||||
if (level == LOG_FATAL) {
|
||||
fflush(out);
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
static void draw() {
|
||||
GXSetCopyClear(
|
||||
(GXColor){
|
||||
.r = 0,
|
||||
.g = 0,
|
||||
.b = 100,
|
||||
.a = 255,
|
||||
},
|
||||
GX_MAX_Z24);
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
const AuroraConfig config = {
|
||||
.appName = "Demo",
|
||||
.logCallback = &log_callback,
|
||||
};
|
||||
AuroraInfo initInfo = aurora_initialize(argc, argv, &config);
|
||||
|
||||
bool exiting = false;
|
||||
bool paused = false;
|
||||
while (!exiting) {
|
||||
const AuroraEvent* event = aurora_update();
|
||||
while (event != NULL && event->type != AURORA_NONE) {
|
||||
switch (event->type) {
|
||||
case AURORA_EXIT:
|
||||
exiting = true;
|
||||
break;
|
||||
case AURORA_PAUSED:
|
||||
paused = true;
|
||||
break;
|
||||
case AURORA_UNPAUSED:
|
||||
paused = false;
|
||||
break;
|
||||
case AURORA_WINDOW_RESIZED:
|
||||
initInfo.windowSize = event->windowSize;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
++event;
|
||||
}
|
||||
if (exiting || paused || !aurora_begin_frame()) {
|
||||
continue;
|
||||
}
|
||||
draw();
|
||||
aurora_end_frame();
|
||||
}
|
||||
|
||||
aurora_shutdown();
|
||||
return 0;
|
||||
}
|
||||
Vendored
+310
@@ -0,0 +1,310 @@
|
||||
include(FetchContent)
|
||||
|
||||
# Don't build tests for third party dependencies
|
||||
set(BUILD_TESTING OFF CACHE INTERNAL "Build tests")
|
||||
set(SKIP_INSTALL_ALL ON)
|
||||
if (NOT DEFINED BUILD_SHARED_LIBS)
|
||||
set(_USE_SHARED ON)
|
||||
else ()
|
||||
set(_USE_SHARED OFF)
|
||||
endif ()
|
||||
|
||||
function(aurora_find_package_global)
|
||||
set(_PREV_FIND_PACKAGE_TARGETS_GLOBAL ${CMAKE_FIND_PACKAGE_TARGETS_GLOBAL})
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
|
||||
find_package(${ARGV})
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ${_PREV_FIND_PACKAGE_TARGETS_GLOBAL})
|
||||
endfunction()
|
||||
|
||||
if (AURORA_ENABLE_GX)
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/AuroraDawnProvider.cmake)
|
||||
endif ()
|
||||
|
||||
# Abseil is needed for core libraries. It normally comes via Dawn's vendor build.
|
||||
# Otherwise prefer a system package and only fetch it as a last resort.
|
||||
if (NOT TARGET absl::flat_hash_map OR NOT TARGET absl::btree)
|
||||
aurora_find_package_global(absl CONFIG QUIET GLOBAL)
|
||||
endif ()
|
||||
|
||||
if (TARGET absl::flat_hash_map AND TARGET absl::btree)
|
||||
message(STATUS "aurora: Using system/existing abseil")
|
||||
else ()
|
||||
message(STATUS "aurora: Fetching abseil-cpp (standalone)")
|
||||
if(NOT DEFINED CMAKE_MSVC_RUNTIME_LIBRARY OR CMAKE_MSVC_RUNTIME_LIBRARY MATCHES "DLL$")
|
||||
set(ABSL_MSVC_STATIC_RUNTIME OFF CACHE INTERNAL "Link static runtime libraries")
|
||||
else()
|
||||
set(ABSL_MSVC_STATIC_RUNTIME ON CACHE INTERNAL "Link static runtime libraries")
|
||||
endif()
|
||||
set(ABSL_PROPAGATE_CXX_STD ON)
|
||||
FetchContent_Declare(abseil-cpp
|
||||
URL https://github.com/abseil/abseil-cpp/archive/refs/tags/20240722.0.tar.gz
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(abseil-cpp)
|
||||
if (NOT TARGET absl::flat_hash_map OR NOT TARGET absl::btree)
|
||||
message(FATAL_ERROR "Failed to make abseil-cpp available")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/AuroraSDL3Provider.cmake)
|
||||
|
||||
if (NOT TARGET xxhash)
|
||||
message(STATUS "aurora: Fetching xxhash")
|
||||
FetchContent_Declare(xxhash
|
||||
URL https://github.com/Cyan4973/xxHash/archive/refs/tags/v0.8.3.tar.gz
|
||||
URL_HASH SHA256=aae608dfe8213dfd05d909a57718ef82f30722c392344583d3f39050c7f29a80
|
||||
SOURCE_SUBDIR cmake_unofficial
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
set(XXHASH_BUILD_XXHSUM OFF CACHE INTERNAL "Build the xxhsum binary")
|
||||
# xxhash uses CMAKE_DEPENDENT_OPTION(BUILD_SHARED_LIBS ...) which writes
|
||||
# a sticky CACHE INTERNAL entry on reconfigure. Undo it afterward.
|
||||
set(_aurora_xxhash_saved_bsl "${BUILD_SHARED_LIBS}")
|
||||
FetchContent_MakeAvailable(xxhash)
|
||||
unset(BUILD_SHARED_LIBS CACHE)
|
||||
set(BUILD_SHARED_LIBS "${_aurora_xxhash_saved_bsl}")
|
||||
unset(_aurora_xxhash_saved_bsl)
|
||||
if (NOT TARGET xxhash)
|
||||
message(FATAL_ERROR "Failed to make xxhash available")
|
||||
endif ()
|
||||
else ()
|
||||
message(STATUS "aurora: Using existing xxhash")
|
||||
endif ()
|
||||
|
||||
if (NOT TARGET fmt)
|
||||
message(STATUS "aurora: Fetching fmt")
|
||||
FetchContent_Declare(fmt
|
||||
URL https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz
|
||||
URL_HASH SHA256=ac366b7b4c2e9f0dde63a59b3feb5ee59b67974b14ee5dc9ea8ad78aa2c1ee1e
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(fmt)
|
||||
if (NOT TARGET fmt)
|
||||
message(FATAL_ERROR "Failed to make fmt available")
|
||||
endif ()
|
||||
else ()
|
||||
message(STATUS "aurora: Using existing fmt")
|
||||
endif ()
|
||||
|
||||
# fmt 11.1 drops its libc++ transitive includes but still calls malloc/free without <cstdlib>.
|
||||
# Force the missing header for fmt and its consumers until the pinned release is updated.
|
||||
if (MINGW AND TARGET fmt)
|
||||
target_compile_options(fmt PUBLIC
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-include>
|
||||
$<$<COMPILE_LANGUAGE:CXX>:cstdlib>)
|
||||
endif ()
|
||||
|
||||
if (AURORA_ENABLE_GX)
|
||||
if (NOT TARGET ZLIB::ZLIB)
|
||||
aurora_find_package_global(ZLIB QUIET)
|
||||
endif ()
|
||||
if (NOT TARGET ZLIB::ZLIB)
|
||||
message(STATUS "aurora: Fetching zlib")
|
||||
set(_AURORA_ZLIB_SHARED ${_USE_SHARED})
|
||||
# ClangCL injects Clang's C headers into the MSVC resource compiler path, and zlib's DLL version
|
||||
# resource then hits syntax rc.exe cannot parse. Use the resource-free static target here.
|
||||
if (MSVC AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
set(_AURORA_ZLIB_SHARED OFF)
|
||||
endif ()
|
||||
set(ZLIB_BUILD_SHARED ${_AURORA_ZLIB_SHARED} CACHE BOOL "Enable building zlib shared library" FORCE)
|
||||
set(ZLIB_BUILD_TESTING OFF CACHE BOOL "Enable Zlib Examples as tests" FORCE)
|
||||
set(ZLIB_INSTALL OFF CACHE BOOL "Enable installation of zlib" FORCE)
|
||||
FetchContent_Declare(
|
||||
ZLIB
|
||||
URL https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz
|
||||
URL_HASH SHA256=bb329a0a2cd0274d05519d61c667c062e06990d72e125ee2dfa8de64f0119d16
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(ZLIB)
|
||||
if (NOT TARGET ZLIB::ZLIB AND TARGET ZLIB::ZLIBSTATIC)
|
||||
add_library(ZLIB::ZLIB ALIAS zlibstatic)
|
||||
endif ()
|
||||
if (NOT TARGET ZLIB::ZLIB)
|
||||
message(FATAL_ERROR "Failed to make zlib available")
|
||||
endif ()
|
||||
# libpng calls find_package(ZLIB) even when zlib was already provided by FetchContent.
|
||||
file(WRITE "${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/ZLIBConfig.cmake"
|
||||
"set(ZLIB_FOUND TRUE)\n"
|
||||
"set(ZLIB_INCLUDE_DIR \"${zlib_SOURCE_DIR}\")\n"
|
||||
"set(ZLIB_INCLUDE_DIRS \"${zlib_BINARY_DIR};${zlib_SOURCE_DIR}\")\n"
|
||||
)
|
||||
endif ()
|
||||
|
||||
if (NOT TARGET PNG::PNG)
|
||||
if (TARGET png_shared AND _USE_SHARED)
|
||||
add_library(PNG::PNG ALIAS png_shared)
|
||||
elseif (TARGET png_static)
|
||||
add_library(PNG::PNG ALIAS png_static)
|
||||
else ()
|
||||
aurora_find_package_global(PNG QUIET)
|
||||
endif ()
|
||||
endif ()
|
||||
if (NOT TARGET PNG::PNG)
|
||||
message(STATUS "aurora: Fetching libpng")
|
||||
set(PNG_SHARED ${_USE_SHARED} CACHE BOOL "Build libpng as a shared library" FORCE)
|
||||
set(PNG_TESTS OFF CACHE BOOL "Build the libpng tests" FORCE)
|
||||
set(PNG_TOOLS OFF CACHE BOOL "Build the libpng tools" FORCE)
|
||||
FetchContent_Declare(
|
||||
PNG
|
||||
URL https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.58.tar.gz
|
||||
URL_HASH SHA256=A9D4DF463D36A6E5F9C29BD6F4967312D17E996C1854F3511F833924EB1993CF
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(PNG)
|
||||
if (TARGET png_shared AND _USE_SHARED)
|
||||
add_library(PNG::PNG ALIAS png_shared)
|
||||
elseif (TARGET png_static)
|
||||
add_library(PNG::PNG ALIAS png_static)
|
||||
else ()
|
||||
message(FATAL_ERROR "Failed to make libpng available")
|
||||
endif ()
|
||||
else ()
|
||||
message(STATUS "aurora: Using existing libpng")
|
||||
endif ()
|
||||
|
||||
aurora_find_package_global(Freetype QUIET)
|
||||
if (NOT TARGET Freetype::Freetype)
|
||||
message(STATUS "aurora: Fetching Freetype")
|
||||
set(FT_DISABLE_BROTLI ON CACHE BOOL "" FORCE)
|
||||
set(FT_DISABLE_HARFBUZZ ON CACHE BOOL "" FORCE)
|
||||
set(FT_DISABLE_PNG ON CACHE BOOL "" FORCE)
|
||||
FetchContent_Declare(Freetype
|
||||
URL https://files.twilitrealm.dev/freetype-2.14.3.tar.gz
|
||||
URL_HASH SHA256=e61b31ab26358b946e767ed7eb7f4bb2e507da1cfefeb7a8861ace7fd5c899a1
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(Freetype)
|
||||
add_library(Freetype::Freetype ALIAS freetype)
|
||||
else ()
|
||||
message(STATUS "aurora: Using existing Freetype")
|
||||
endif ()
|
||||
|
||||
if (NOT TARGET imgui)
|
||||
message(STATUS "aurora: Fetching imgui")
|
||||
FetchContent_Declare(imgui
|
||||
URL https://github.com/ocornut/imgui/archive/refs/tags/v1.91.9b-docking.tar.gz
|
||||
URL_HASH SHA256=466fdef9b18de15f0bb6e288e3d00ffa3d82200ec458ce5e4f724a161d9528a5
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(imgui)
|
||||
|
||||
add_library(imgui STATIC
|
||||
${imgui_SOURCE_DIR}/imgui.cpp
|
||||
${imgui_SOURCE_DIR}/imgui_demo.cpp
|
||||
${imgui_SOURCE_DIR}/imgui_draw.cpp
|
||||
${imgui_SOURCE_DIR}/imgui_tables.cpp
|
||||
${imgui_SOURCE_DIR}/imgui_widgets.cpp
|
||||
${imgui_SOURCE_DIR}/misc/cpp/imgui_stdlib.cpp
|
||||
)
|
||||
target_include_directories(imgui PUBLIC ${imgui_SOURCE_DIR})
|
||||
|
||||
add_library(imgui_backends STATIC
|
||||
${imgui_SOURCE_DIR}/backends/imgui_impl_sdl3.cpp
|
||||
${imgui_SOURCE_DIR}/backends/imgui_impl_sdlrenderer3.cpp
|
||||
${imgui_SOURCE_DIR}/backends/imgui_impl_wgpu.cpp
|
||||
)
|
||||
target_compile_definitions(imgui_backends PRIVATE IMGUI_IMPL_WEBGPU_BACKEND_DAWN)
|
||||
target_link_libraries(imgui_backends PRIVATE imgui ${AURORA_SDL3_TARGET} dawn::webgpu_dawn)
|
||||
target_link_libraries(imgui PUBLIC imgui_backends)
|
||||
|
||||
if (TARGET Freetype::Freetype)
|
||||
target_sources(imgui PRIVATE ${imgui_SOURCE_DIR}/misc/freetype/imgui_freetype.cpp)
|
||||
target_compile_definitions(imgui PUBLIC IMGUI_ENABLE_FREETYPE)
|
||||
target_link_libraries(imgui PRIVATE Freetype::Freetype)
|
||||
endif ()
|
||||
else ()
|
||||
message(STATUS "aurora: Using existing imgui")
|
||||
endif ()
|
||||
|
||||
if (TARGET imgui)
|
||||
target_include_directories(imgui PUBLIC ${PROJECT_SOURCE_DIR}/include)
|
||||
target_compile_definitions(imgui PUBLIC IMGUI_USER_CONFIG="aurora/imgui_config.h")
|
||||
target_sources(imgui PRIVATE ${PROJECT_SOURCE_DIR}/lib/imgui_config.cpp)
|
||||
target_link_libraries(imgui PRIVATE ${AURORA_SDL3_TARGET})
|
||||
endif ()
|
||||
|
||||
if (NOT TARGET sqlite3)
|
||||
message(STATUS "aurora: Fetching sqlite3")
|
||||
FetchContent_Declare(sqlite3
|
||||
URL https://sqlite.org/2026/sqlite-amalgamation-3510300.zip
|
||||
URL_HASH SHA256=acb1e6f5d832484bf6d32b681e858c38add8b2acdfd42ac5df24b8afb46552b4
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(sqlite3)
|
||||
|
||||
add_library(sqlite3 STATIC ${sqlite3_SOURCE_DIR}/sqlite3.c)
|
||||
target_include_directories(sqlite3 PUBLIC ${sqlite3_SOURCE_DIR})
|
||||
else ()
|
||||
message(STATUS "aurora: Using existing sqlite3")
|
||||
endif ()
|
||||
|
||||
if (AURORA_CACHE_USE_ZSTD)
|
||||
if (NOT TARGET libzstd_static)
|
||||
message(STATUS "aurora: Fetching zstd")
|
||||
set(ZSTD_BUILD_STATIC ON)
|
||||
set(ZSTD_BUILD_SHARED OFF)
|
||||
set(ZSTD_BUILD_PROGRAMS OFF CACHE BOOL "BUILD PROGRAMS" FORCE)
|
||||
set(ZSTD_BUILD_TESTS OFF CACHE BOOL "BUILD TESTS" FORCE)
|
||||
|
||||
if (WIN32 AND CMAKE_C_COMPILER_ID MATCHES "Clang" AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
set(C_FLAG_WALL TRUE CACHE INTERNAL "")
|
||||
set(CXX_FLAG_WALL TRUE CACHE INTERNAL "")
|
||||
set(C_FLAG_WEXTRA TRUE CACHE INTERNAL "")
|
||||
set(CXX_FLAG_WEXTRA TRUE CACHE INTERNAL "")
|
||||
set(C_FLAG_WUNDEF TRUE CACHE INTERNAL "")
|
||||
set(CXX_FLAG_WUNDEF TRUE CACHE INTERNAL "")
|
||||
set(C_FLAG_WSHADOW TRUE CACHE INTERNAL "")
|
||||
set(CXX_FLAG_WSHADOW TRUE CACHE INTERNAL "")
|
||||
set(C_FLAG_WCAST_ALIGN TRUE CACHE INTERNAL "")
|
||||
set(CXX_FLAG_WCAST_ALIGN TRUE CACHE INTERNAL "")
|
||||
set(C_FLAG_WCAST_QUAL TRUE CACHE INTERNAL "")
|
||||
set(CXX_FLAG_WCAST_QUAL TRUE CACHE INTERNAL "")
|
||||
set(C_FLAG_WSTRICT_PROTOTYPES TRUE CACHE INTERNAL "")
|
||||
set(LD_FLAG_WL_Z_NOEXECSTACK FALSE CACHE INTERNAL "")
|
||||
set(C_FLAG_QUNUSED_ARGUMENTS TRUE CACHE INTERNAL "")
|
||||
set(CXX_FLAG_QUNUSED_ARGUMENTS TRUE CACHE INTERNAL "")
|
||||
set(C_FLAG_WA_NOEXECSTACK TRUE CACHE INTERNAL "")
|
||||
set(CXX_FLAG_WA_NOEXECSTACK TRUE CACHE INTERNAL "")
|
||||
endif()
|
||||
|
||||
FetchContent_Declare(zstd
|
||||
URL https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz
|
||||
URL_HASH SHA256=eb33e51f49a15e023950cd7825ca74a4a2b43db8354825ac24fc1b7ee09e6fa3
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
SOURCE_SUBDIR build/cmake
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(zstd)
|
||||
else ()
|
||||
message(STATUS "aurora: Using existing zstd")
|
||||
endif ()
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (NOT TARGET TracyClient)
|
||||
message(STATUS "aurora: Fetching Tracy")
|
||||
|
||||
# Default to off, users can enable the cache variable if they want to use it.
|
||||
set(TRACY_ENABLE OFF CACHE BOOL "Enable Tracy profiling")
|
||||
|
||||
FetchContent_Declare(
|
||||
tracy
|
||||
URL https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz
|
||||
URL_HASH SHA256=24d342b5127d7f659dc3cf94f24347b348cc736f25b17a691fd7f69541937658
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(tracy)
|
||||
|
||||
if (NOT TARGET TracyClient)
|
||||
message(FATAL_ERROR "Failed to make TracyClient available")
|
||||
endif ()
|
||||
endif ()
|
||||
@@ -0,0 +1,172 @@
|
||||
#ifndef AURORA_AURORA_H
|
||||
#define AURORA_AURORA_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
extern "C" {
|
||||
#else
|
||||
#include "stdbool.h"
|
||||
#include "stddef.h"
|
||||
#include "stdint.h"
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
BACKEND_AUTO,
|
||||
BACKEND_D3D11,
|
||||
BACKEND_D3D12,
|
||||
BACKEND_METAL,
|
||||
BACKEND_VULKAN,
|
||||
BACKEND_OPENGL,
|
||||
BACKEND_OPENGLES,
|
||||
BACKEND_WEBGPU,
|
||||
BACKEND_NULL,
|
||||
} AuroraBackend;
|
||||
|
||||
typedef enum {
|
||||
LOG_DEBUG,
|
||||
LOG_INFO,
|
||||
LOG_WARNING,
|
||||
LOG_ERROR,
|
||||
LOG_FATAL,
|
||||
} AuroraLogLevel;
|
||||
|
||||
typedef enum {
|
||||
AURORA_DISPLAY_MODE_WINDOWED,
|
||||
AURORA_DISPLAY_MODE_BORDERLESS,
|
||||
AURORA_DISPLAY_MODE_EXCLUSIVE,
|
||||
} AuroraDisplayMode;
|
||||
|
||||
typedef struct {
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
} AuroraWindowPos;
|
||||
|
||||
typedef struct {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
|
||||
/**
|
||||
* Width of the main GX framebuffer.
|
||||
*/
|
||||
uint32_t fb_width;
|
||||
|
||||
/**
|
||||
* Height of the main GX framebuffer.
|
||||
*/
|
||||
uint32_t fb_height;
|
||||
|
||||
/**
|
||||
* The size of the framebuffer used to present to the operating system.
|
||||
* May differ from fb_width if Aurora is instructed to force an aspect ratio or resolution configuration.
|
||||
*/
|
||||
uint32_t native_fb_width;
|
||||
|
||||
/**
|
||||
* The size of the framebuffer used to present to the operating system.
|
||||
* May differ from fb_height if Aurora is instructed to force an aspect ratio or resolution configuration.
|
||||
*/
|
||||
uint32_t native_fb_height;
|
||||
float scale;
|
||||
} AuroraWindowSize;
|
||||
|
||||
typedef struct SDL_Window SDL_Window;
|
||||
typedef struct AuroraEvent AuroraEvent;
|
||||
|
||||
typedef void (*AuroraLogCallback)(AuroraLogLevel level, const char* module, const char* message, unsigned int len);
|
||||
typedef void (*AuroraImGuiInitCallback)(const AuroraWindowSize* size);
|
||||
|
||||
typedef struct {
|
||||
const char* appName;
|
||||
const char* userPath;
|
||||
const char* cachePath;
|
||||
// Read-only application resources. Defaults to SDL_GetBasePath(), which is
|
||||
// where release builds place initial_pipeline_cache.db.
|
||||
const char* resourcesPath;
|
||||
AuroraBackend desiredBackend;
|
||||
uint32_t msaa;
|
||||
uint16_t maxTextureAnisotropy;
|
||||
// No vsync knob exists: the swapchain is always configured for a
|
||||
// non-blocking present mode (Immediate, else Mailbox). See best_present_mode.
|
||||
bool startFullscreen;
|
||||
bool allowJoystickBackgroundEvents;
|
||||
bool pauseOnFocusLost;
|
||||
bool allowTextureReplacements;
|
||||
bool allowTextureDumps;
|
||||
bool disableCopyFilter;
|
||||
// When false, Aurora centers the first window. When true, windowPosX/Y are restored verbatim,
|
||||
// including negative coordinates on monitors left of or above the primary display.
|
||||
bool hasWindowPosition;
|
||||
int32_t windowPosX;
|
||||
int32_t windowPosY;
|
||||
uint32_t windowWidth;
|
||||
uint32_t windowHeight;
|
||||
void* iconRGBA8;
|
||||
uint32_t iconWidth;
|
||||
uint32_t iconHeight;
|
||||
AuroraLogCallback logCallback;
|
||||
AuroraLogLevel logLevel;
|
||||
AuroraImGuiInitCallback imGuiInitCallback;
|
||||
|
||||
/*
|
||||
* The size of the GameCube's main memory, or MEM1 on the Wii.
|
||||
* Note that it will not be allocated at the exact 0x80000000 address, as that cannot be guaranteed.
|
||||
* This can be set to 0 to disable allocating this region.
|
||||
*/
|
||||
uint32_t mem1Size;
|
||||
|
||||
/*
|
||||
* The size of the GameCube's ARAM, or MEM2 on the Wii.
|
||||
* This can be set to 0 to disable allocating this region.
|
||||
*/
|
||||
uint32_t mem2Size;
|
||||
|
||||
// Optional directory for the portable GX pipeline database. When null, the
|
||||
// database is stored in cachePath with Dawn's machine-specific cache.
|
||||
const char* pipelineCachePath;
|
||||
} AuroraConfig;
|
||||
|
||||
typedef struct {
|
||||
AuroraBackend backend;
|
||||
const char* userPath;
|
||||
const char* cachePath;
|
||||
SDL_Window* window;
|
||||
AuroraWindowSize windowSize;
|
||||
} AuroraInfo;
|
||||
|
||||
AuroraInfo aurora_initialize(int argc, char* argv[], const AuroraConfig* config);
|
||||
void aurora_shutdown();
|
||||
const AuroraEvent* aurora_update();
|
||||
bool aurora_begin_frame();
|
||||
void aurora_end_frame();
|
||||
typedef void (*AuroraFrameWorkerWaitCallback)();
|
||||
// Called from the producer thread at bounded intervals while Aurora waits for
|
||||
// the asynchronous frame worker. The callback must not enter Aurora.
|
||||
void aurora_set_frame_worker_wait_callback(AuroraFrameWorkerWaitCallback callback);
|
||||
void aurora_wait_for_frame_worker();
|
||||
bool aurora_wait_for_frame_worker_for(uint32_t timeoutMicros);
|
||||
// Absolute schedule for the next sealed frame, on steady_clock: baseNanos anchors the group and
|
||||
// intervalNanos is the period, so slot k of N+1 fires at base + k*interval/(N+1). Zeros clear it.
|
||||
void aurora_set_present_schedule(uint64_t baseNanos, uint64_t intervalNanos);
|
||||
// Reports whether the frame about to be sealed met its display boundary. Interpolation sizes its
|
||||
// slot group from this, backing off after misses. Only paced presents may report.
|
||||
void aurora_report_producer_paced(bool paced);
|
||||
void aurora_request_frame_capture(uint32_t frame, const char* outputPath);
|
||||
bool aurora_flush_efb_copies_to_ram();
|
||||
bool aurora_flush_efb_copy_to_ram(void* dest);
|
||||
|
||||
void aurora_set_log_level(AuroraLogLevel level);
|
||||
void aurora_set_pause_on_focus_lost(bool value);
|
||||
void aurora_set_background_input(bool value);
|
||||
void aurora_set_display_mode(AuroraDisplayMode mode);
|
||||
AuroraDisplayMode aurora_get_display_mode();
|
||||
|
||||
AuroraBackend aurora_get_backend();
|
||||
const AuroraBackend* aurora_get_available_backends(size_t* count);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef AURORA_DVD_H
|
||||
#define AURORA_DVD_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <dolphin/types.h>
|
||||
|
||||
/**
|
||||
* Open a GC/Wii disc image for use by the DVD API.
|
||||
* Must be called before DVDInit().
|
||||
* Returns true on success, false on failure.
|
||||
*/
|
||||
bool aurora_dvd_open(const char* disc_path);
|
||||
|
||||
/**
|
||||
* Close the disc image and free all resources.
|
||||
*/
|
||||
void aurora_dvd_close(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,44 @@
|
||||
#ifndef AURORA_EVENT_H
|
||||
#define AURORA_EVENT_H
|
||||
|
||||
#include "aurora.h"
|
||||
|
||||
#include <SDL3/SDL_events.h>
|
||||
#include <SDL3/SDL_joystick.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <cstdint>
|
||||
|
||||
extern "C" {
|
||||
#else
|
||||
#include "stdint.h"
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
AURORA_NONE,
|
||||
AURORA_EXIT,
|
||||
AURORA_SDL_EVENT,
|
||||
AURORA_WINDOW_MOVED,
|
||||
AURORA_WINDOW_RESIZED,
|
||||
AURORA_CONTROLLER_ADDED,
|
||||
AURORA_CONTROLLER_REMOVED,
|
||||
AURORA_PAUSED,
|
||||
AURORA_UNPAUSED,
|
||||
AURORA_DISPLAY_SCALE_CHANGED,
|
||||
} AuroraEventType;
|
||||
|
||||
struct AuroraEvent {
|
||||
AuroraEventType type;
|
||||
union {
|
||||
SDL_Event sdl;
|
||||
AuroraWindowPos windowPos;
|
||||
AuroraWindowSize windowSize;
|
||||
SDL_JoystickID controller;
|
||||
};
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,97 @@
|
||||
#ifndef AURORA_GFX_H
|
||||
#define AURORA_GFX_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
extern "C" {
|
||||
#else
|
||||
#include "stddef.h"
|
||||
#include "stdint.h"
|
||||
#endif
|
||||
|
||||
#ifndef NDEBUG
|
||||
#define AURORA_GFX_DEBUG_GROUPS
|
||||
#endif
|
||||
|
||||
void aurora_push_debug_group(const char* label);
|
||||
void aurora_pop_debug_group();
|
||||
|
||||
typedef struct {
|
||||
uint32_t queuedPipelines;
|
||||
uint32_t createdPipelines;
|
||||
uint32_t drawCallCount;
|
||||
uint32_t mergedDrawCallCount;
|
||||
uint32_t lastVertSize;
|
||||
uint32_t lastUniformSize;
|
||||
uint32_t lastIndexSize;
|
||||
uint32_t lastStorageSize;
|
||||
uint32_t lastTextureUploadSize;
|
||||
uint32_t presentedFrameCount;
|
||||
uint32_t interpolatedFrameCount;
|
||||
} AuroraStats;
|
||||
|
||||
typedef struct {
|
||||
uint64_t totalPresentCount;
|
||||
uint32_t sampleCount;
|
||||
double framesPerSecond;
|
||||
double averageFrameTimeMs;
|
||||
double p95FrameTimeMs;
|
||||
double jitterMs;
|
||||
// framesPerSecond with duplicated presentation slots scaled out, so this is the rate of frames
|
||||
// that carried new motion. Equal to framesPerSecond when every slot replayed real interpolation.
|
||||
double effectiveFramesPerSecond;
|
||||
} AuroraPresentTiming;
|
||||
|
||||
const AuroraStats* aurora_get_stats();
|
||||
void aurora_get_present_timing(AuroraPresentTiming* timing);
|
||||
|
||||
// Interpolation health: the per-frame fields describe the last sealed frame, the counters
|
||||
// accumulate since it was configured. This answers "output FPS dropped but the game held 60".
|
||||
typedef struct {
|
||||
uint32_t targetFps; // configured target, 0 when interpolation is off
|
||||
uint32_t targetSamples; // slots the pacing controller currently aims for
|
||||
uint32_t activeSamples; // slots latched for the latest sealed frame
|
||||
uint32_t candidates; // perspective draws in the latest sealed frame
|
||||
uint32_t matchable; // candidates whose identity also existed last frame
|
||||
uint32_t matches; // draws matched to the previous frame
|
||||
uint32_t eligible; // latest frame inserted interpolated slots
|
||||
uint32_t replaySafe; // latest frame could replay its command stream
|
||||
uint64_t framesSealed;
|
||||
uint64_t framesLowMatch;
|
||||
uint64_t framesReplayUnsafe;
|
||||
uint64_t slotReductions;
|
||||
uint64_t lateSealDrops;
|
||||
} AuroraFrameInterpolationDiagnostics;
|
||||
|
||||
void aurora_get_frame_interpolation_diagnostics(AuroraFrameInterpolationDiagnostics* diagnostics);
|
||||
|
||||
// Generates transform-interpolated perspective frames between consecutive 60 Hz logical frames.
|
||||
// Supported targets are 0 (off), 120, 180 and 240. Guest simulation and VI timing are unchanged.
|
||||
void aurora_set_frame_interpolation_fps(uint32_t targetFps);
|
||||
uint32_t aurora_get_frame_interpolation_fps();
|
||||
|
||||
// Newly encountered GX pipelines compile on the bounded worker queue. Draws whose pipeline is not
|
||||
// ready are skipped rather than stalling submission, and pick it up once compilation finishes.
|
||||
void aurora_set_skip_unready_pipelines(bool enabled);
|
||||
bool aurora_get_skip_unready_pipelines();
|
||||
uint32_t aurora_get_queued_pipeline_count();
|
||||
|
||||
// Controls whether display copies bypass the Wii's vertical copy filter.
|
||||
void aurora_set_disable_copy_filter(bool disabled);
|
||||
bool aurora_get_disable_copy_filter();
|
||||
|
||||
// Guest-RAM write tracking. `generation` changes whenever guest RAM covering a host range was
|
||||
// written (or returns AURORA_GUEST_WRITE_UNTRACKED); `notify` reports writes aurora made itself.
|
||||
#define AURORA_GUEST_WRITE_UNTRACKED UINT64_MAX
|
||||
typedef uint64_t (*AuroraGuestWriteGenerationCallback)(const void* hostPtr, size_t size);
|
||||
typedef void (*AuroraGuestWriteNotifyCallback)(const void* hostPtr, size_t size);
|
||||
void aurora_set_guest_write_hooks(AuroraGuestWriteGenerationCallback generation,
|
||||
AuroraGuestWriteNotifyCallback notify);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef AURORA_IMGUI_H
|
||||
#define AURORA_IMGUI_H
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <cstdint>
|
||||
|
||||
extern "C" {
|
||||
#else
|
||||
#include "stdint.h"
|
||||
#endif
|
||||
|
||||
ImTextureID aurora_imgui_add_texture(uint32_t width, uint32_t height, const void* rgba8);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS 1
|
||||
|
||||
typedef struct SDL_IOStream SDL_IOStream;
|
||||
typedef SDL_IOStream* ImFileHandle;
|
||||
typedef unsigned long long ImU64;
|
||||
|
||||
ImFileHandle ImFileOpen(const char* filename, const char* mode);
|
||||
bool ImFileClose(ImFileHandle file);
|
||||
ImU64 ImFileGetSize(ImFileHandle file);
|
||||
ImU64 ImFileRead(void* data, ImU64 size, ImU64 count, ImFileHandle file);
|
||||
ImU64 ImFileWrite(const void* data, ImU64 size, ImU64 count, ImFileHandle file);
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef AURORA_MAIN_H
|
||||
#define AURORA_MAIN_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
int aurora_main(int argc, char* argv[]);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#define main aurora_main
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,254 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#ifndef AURORA_VEC2_EXTRA
|
||||
#define AURORA_VEC2_EXTRA
|
||||
#endif
|
||||
#ifndef AURORA_VEC3_EXTRA
|
||||
#define AURORA_VEC3_EXTRA
|
||||
#endif
|
||||
#ifndef AURORA_VEC4_EXTRA
|
||||
#define AURORA_VEC4_EXTRA
|
||||
#endif
|
||||
#ifndef AURORA_MAT4X4_EXTRA
|
||||
#define AURORA_MAT4X4_EXTRA
|
||||
#endif
|
||||
|
||||
#ifndef __has_attribute
|
||||
#define __has_attribute(x) 0
|
||||
#endif
|
||||
#ifndef __has_builtin
|
||||
#define __has_builtin(x) 0
|
||||
#endif
|
||||
#if __has_attribute(vector_size) && !defined(__clang__)
|
||||
#define USE_GCC_VECTOR_EXTENSIONS
|
||||
#endif
|
||||
|
||||
namespace aurora {
|
||||
template <typename T>
|
||||
struct Vec2 {
|
||||
T x{};
|
||||
T y{};
|
||||
|
||||
constexpr Vec2() = default;
|
||||
constexpr Vec2(T x, T y) : x(x), y(y) {}
|
||||
AURORA_VEC2_EXTRA
|
||||
|
||||
bool operator==(const Vec2& rhs) const { return x == rhs.x && y == rhs.y; }
|
||||
bool operator!=(const Vec2& rhs) const { return !(*this == rhs); }
|
||||
};
|
||||
template <typename T>
|
||||
struct Vec3 {
|
||||
T x{};
|
||||
T y{};
|
||||
T z{};
|
||||
|
||||
constexpr Vec3() = default;
|
||||
constexpr Vec3(T x, T y, T z) : x(x), y(y), z(z) {}
|
||||
AURORA_VEC3_EXTRA
|
||||
|
||||
bool operator==(const Vec3& rhs) const { return x == rhs.x && y == rhs.y && z == rhs.z; }
|
||||
bool operator!=(const Vec3& rhs) const { return !(*this == rhs); }
|
||||
};
|
||||
template <typename T>
|
||||
struct Vec4 {
|
||||
#ifdef USE_GCC_VECTOR_EXTENSIONS
|
||||
typedef T Vt __attribute__((vector_size(sizeof(T) * 4)));
|
||||
Vt m;
|
||||
#else
|
||||
using Vt = T[4];
|
||||
Vt m;
|
||||
#endif
|
||||
|
||||
constexpr Vec4() = default;
|
||||
constexpr Vec4(Vt m) : m(m) {}
|
||||
constexpr Vec4(T x, T y, T z, T w) : m{x, y, z, w} {}
|
||||
// For Vec3 with padding
|
||||
constexpr Vec4(T x, T y, T z) : m{x, y, z, {}} {}
|
||||
// For Vec3 -> Vec4
|
||||
constexpr Vec4(Vec3<T> v, T w) : m{v.x, v.y, v.z, w} {}
|
||||
AURORA_VEC4_EXTRA
|
||||
|
||||
inline Vec4& operator=(const Vec4& other) {
|
||||
memcpy(&m, &other.m, sizeof(Vt));
|
||||
return *this;
|
||||
}
|
||||
|
||||
[[nodiscard]] inline T& x() { return m[0]; }
|
||||
[[nodiscard]] inline T x() const { return m[0]; }
|
||||
[[nodiscard]] inline T& y() { return m[1]; }
|
||||
[[nodiscard]] inline T y() const { return m[1]; }
|
||||
[[nodiscard]] inline T& z() { return m[2]; }
|
||||
[[nodiscard]] inline T z() const { return m[2]; }
|
||||
[[nodiscard]] inline T& w() { return m[3]; }
|
||||
[[nodiscard]] inline T w() const { return m[3]; }
|
||||
[[nodiscard]] inline T& operator[](size_t i) { return m[i]; }
|
||||
[[nodiscard]] inline T operator[](size_t i) const { return m[i]; }
|
||||
|
||||
template <size_t x, size_t y, size_t z, size_t w>
|
||||
[[nodiscard]] constexpr Vec4 shuffle() const {
|
||||
static_assert(x < 4 && y < 4 && z < 4 && w < 4);
|
||||
#if defined(USE_GCC_VECTOR_EXTENSIONS) && __has_builtin(__builtin_shuffle)
|
||||
typedef int Vi __attribute__((vector_size(16)));
|
||||
return __builtin_shuffle(m, Vi{x, y, z, w});
|
||||
#else
|
||||
return {m[x], m[y], m[z], m[w]};
|
||||
#endif
|
||||
}
|
||||
|
||||
bool operator==(const Vec4& rhs) const {
|
||||
#if defined(USE_GCC_VECTOR_EXTENSIONS) && __has_builtin(__builtin_reduce_and)
|
||||
return __builtin_reduce_and(m == rhs.m) != 0;
|
||||
#else
|
||||
return m[0] == rhs.m[0] && m[1] == rhs.m[1] && m[2] == rhs.m[2] && m[3] == rhs.m[3];
|
||||
#endif
|
||||
}
|
||||
bool operator!=(const Vec4& rhs) const { return !(*this == rhs); }
|
||||
};
|
||||
template <typename T>
|
||||
[[nodiscard]] Vec4<T> operator+(const Vec4<T>& a, const Vec4<T>& b) {
|
||||
#ifdef USE_GCC_VECTOR_EXTENSIONS
|
||||
return a.m + b.m;
|
||||
#else
|
||||
return {a.m[0] + b.m[0], a.m[1] + b.m[1], a.m[2] + b.m[2], a.m[3] + b.m[3]};
|
||||
#endif
|
||||
}
|
||||
template <typename T>
|
||||
[[nodiscard]] Vec4<T> operator*(const Vec4<T>& a, const Vec4<T>& b) {
|
||||
#ifdef USE_GCC_VECTOR_EXTENSIONS
|
||||
return a.m * b.m;
|
||||
#else
|
||||
return {a.m[0] * b.m[0], a.m[1] * b.m[1], a.m[2] * b.m[2], a.m[3] * b.m[3]};
|
||||
#endif
|
||||
}
|
||||
template <typename T>
|
||||
struct Mat3x2 {
|
||||
Vec2<T> m0{};
|
||||
Vec2<T> m1{};
|
||||
Vec2<T> m2{};
|
||||
|
||||
constexpr Mat3x2() = default;
|
||||
constexpr Mat3x2(const Vec2<T>& m0, const Vec2<T>& m1, const Vec2<T>& m2) : m0(m0), m1(m1), m2(m2) {}
|
||||
|
||||
bool operator==(const Mat3x2& rhs) const { return m0 == rhs.m0 && m1 == rhs.m1 && m2 == rhs.m2; }
|
||||
bool operator!=(const Mat3x2& rhs) const { return !(*this == rhs); }
|
||||
};
|
||||
template <typename T>
|
||||
struct Mat4x2 {
|
||||
Vec2<T> m0{};
|
||||
Vec2<T> m1{};
|
||||
Vec2<T> m2{};
|
||||
Vec2<T> m3{};
|
||||
|
||||
constexpr Mat4x2() = default;
|
||||
constexpr Mat4x2(const Vec2<T>& m0, const Vec2<T>& m1, const Vec2<T>& m2, const Vec2<T>& m3)
|
||||
: m0(m0), m1(m1), m2(m2), m3(m3) {}
|
||||
|
||||
inline Mat4x2 transpose() const {
|
||||
return {
|
||||
{m0.x, m2.x},
|
||||
{m0.y, m2.y},
|
||||
{m1.x, m3.x},
|
||||
{m1.y, m3.y},
|
||||
};
|
||||
}
|
||||
|
||||
bool operator==(const Mat4x2& rhs) const { return m0 == rhs.m0 && m1 == rhs.m1 && m2 == rhs.m2 && m3 == rhs.m3; }
|
||||
bool operator!=(const Mat4x2& rhs) const { return !(*this == rhs); }
|
||||
};
|
||||
template <typename T>
|
||||
struct Mat2x4 {
|
||||
Vec4<T> m0{};
|
||||
Vec4<T> m1{};
|
||||
|
||||
constexpr Mat2x4() = default;
|
||||
constexpr Mat2x4(const Vec4<T>& m0, const Vec4<T>& m1, const Vec4<T>& m2) : m0(m0), m1(m1) {}
|
||||
|
||||
bool operator==(const Mat2x4& rhs) const { return m0 == rhs.m0 && m1 == rhs.m1; }
|
||||
bool operator!=(const Mat2x4& rhs) const { return !(*this == rhs); }
|
||||
};
|
||||
static_assert(sizeof(Mat2x4<float>) == 32);
|
||||
template <typename T>
|
||||
struct Mat4x4;
|
||||
template <typename T>
|
||||
struct Mat3x4 {
|
||||
Vec4<T> m0{};
|
||||
Vec4<T> m1{};
|
||||
Vec4<T> m2{};
|
||||
|
||||
constexpr Mat3x4() = default;
|
||||
constexpr Mat3x4(const Vec4<T>& m0, const Vec4<T>& m1, const Vec4<T>& m2) : m0(m0), m1(m1), m2(m2) {}
|
||||
|
||||
[[nodiscard]] Mat4x4<T> to4x4() const;
|
||||
[[nodiscard]] Mat4x4<T> toTransposed4x4() const;
|
||||
|
||||
bool operator==(const Mat3x4& rhs) const { return m0 == rhs.m0 && m1 == rhs.m1 && m2 == rhs.m2; }
|
||||
bool operator!=(const Mat3x4& rhs) const { return !(*this == rhs); }
|
||||
};
|
||||
static_assert(sizeof(Mat3x4<float>) == 48);
|
||||
template <typename T>
|
||||
struct Mat4x4 {
|
||||
Vec4<T> m0{};
|
||||
Vec4<T> m1{};
|
||||
Vec4<T> m2{};
|
||||
Vec4<T> m3{};
|
||||
|
||||
constexpr Mat4x4() = default;
|
||||
constexpr Mat4x4(const Vec4<T>& m0, const Vec4<T>& m1, const Vec4<T>& m2, const Vec4<T>& m3)
|
||||
: m0(m0), m1(m1), m2(m2), m3(m3) {}
|
||||
AURORA_MAT4X4_EXTRA
|
||||
|
||||
[[nodiscard]] Mat4x4 transpose() const {
|
||||
return {
|
||||
{m0[0], m1[0], m2[0], m3[0]},
|
||||
{m0[1], m1[1], m2[1], m3[1]},
|
||||
{m0[2], m1[2], m2[2], m3[2]},
|
||||
{m0[3], m1[3], m2[3], m3[3]},
|
||||
};
|
||||
}
|
||||
Mat4x4& operator=(const Mat4x4& other) = default;
|
||||
|
||||
Vec4<T>& operator[](size_t i) { return *(&m0 + i); }
|
||||
const Vec4<T>& operator[](size_t i) const { return *(&m0 + i); }
|
||||
|
||||
bool operator==(const Mat4x4& rhs) const { return m0 == rhs.m0 && m1 == rhs.m1 && m2 == rhs.m2 && m3 == rhs.m3; }
|
||||
bool operator!=(const Mat4x4& rhs) const { return !(*this == rhs); }
|
||||
};
|
||||
static_assert(sizeof(Mat4x4<float>) == 64);
|
||||
template <typename T>
|
||||
[[nodiscard]] Mat4x4<T> operator*(const Mat4x4<T>& a, const Mat4x4<T>& b) {
|
||||
Mat4x4<T> out;
|
||||
for (size_t i = 0; i < 4; ++i) {
|
||||
*(&out.m0 + i) = a.m0 * b[i].template shuffle<0, 0, 0, 0>() + a.m1 * b[i].template shuffle<1, 1, 1, 1>() +
|
||||
a.m2 * b[i].template shuffle<2, 2, 2, 2>() + a.m3 * b[i].template shuffle<3, 3, 3, 3>();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
template <typename T>
|
||||
[[nodiscard]] Mat4x4<T> Mat3x4<T>::to4x4() const {
|
||||
return {
|
||||
{m0[0], m0[1], m0[2], 0.f},
|
||||
{m1[0], m1[1], m1[2], 0.f},
|
||||
{m2[0], m2[1], m2[2], 0.f},
|
||||
{m0[3], m1[3], m2[3], 1.f},
|
||||
};
|
||||
}
|
||||
template <typename T>
|
||||
[[nodiscard]] Mat4x4<T> Mat3x4<T>::toTransposed4x4() const {
|
||||
return Mat4x4<T>{
|
||||
{m0[0], m1[0], m2[0], 0.f},
|
||||
{m0[1], m1[1], m2[1], 0.f},
|
||||
{m0[2], m1[2], m2[2], 0.f},
|
||||
{m0[3], m1[3], m2[3], 1.f},
|
||||
};
|
||||
}
|
||||
constexpr Mat4x4 Mat4x4_Identity{
|
||||
Vec4{1.f, 0.f, 0.f, 0.f},
|
||||
Vec4{0.f, 1.f, 0.f, 0.f},
|
||||
Vec4{0.f, 0.f, 1.f, 0.f},
|
||||
Vec4{0.f, 0.f, 0.f, 1.f},
|
||||
};
|
||||
} // namespace aurora
|
||||
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
namespace aurora::render_size_limits {
|
||||
|
||||
struct Size {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
};
|
||||
|
||||
// The canvas follows the window aspect however wide it gets, since the presenter stretches it and
|
||||
// a cap would distort the UI. Only the portrait side clamps, where the guest cannot fill taller.
|
||||
inline constexpr float kMaxDynamicPortraitExpansion = 6.f;
|
||||
inline constexpr float kMinDynamicAspect = (16.f / 9.f) / kMaxDynamicPortraitExpansion;
|
||||
|
||||
// The adapter limit is an API ceiling, not an allocation budget: a 16384-wide RGBA attachment is
|
||||
// near 100 MiB. 8192 plus an 8K-UHD pixel budget keeps 8x 16:9 while avoiding device removal.
|
||||
inline constexpr uint32_t kMaxFramebufferDimension = 8192;
|
||||
inline constexpr uint64_t kMaxFramebufferPixels = 7680ull * 4320ull;
|
||||
|
||||
inline float clamp_dynamic_aspect(float aspect) noexcept {
|
||||
if (!std::isfinite(aspect) || aspect <= 0.f) {
|
||||
return 1.f;
|
||||
}
|
||||
return std::max(aspect, kMinDynamicAspect);
|
||||
}
|
||||
|
||||
inline Size scale_framebuffer_to_aspect(uint32_t width, uint32_t height, float scale, float requestedAspect) noexcept {
|
||||
if (width == 0 || height == 0 || !std::isfinite(scale) || scale <= 0.f) {
|
||||
return {std::max(width, 1u), std::max(height, 1u)};
|
||||
}
|
||||
|
||||
const auto scaledWidth = std::max(1u, static_cast<uint32_t>(std::lround(static_cast<double>(width) * scale)));
|
||||
const auto scaledHeight = std::max(1u, static_cast<uint32_t>(std::lround(static_cast<double>(height) * scale)));
|
||||
const float aspect = clamp_dynamic_aspect(requestedAspect);
|
||||
if (aspect >= static_cast<float>(width) / static_cast<float>(height)) {
|
||||
return {
|
||||
std::max(1u, static_cast<uint32_t>(std::lround(static_cast<double>(scaledHeight) * aspect))),
|
||||
scaledHeight,
|
||||
};
|
||||
}
|
||||
return {
|
||||
scaledWidth,
|
||||
std::max(1u, static_cast<uint32_t>(std::lround(static_cast<double>(scaledWidth) / aspect))),
|
||||
};
|
||||
}
|
||||
|
||||
inline Size fit_framebuffer_to_budget(uint32_t width, uint32_t height, uint32_t adapterMaxDimension) noexcept {
|
||||
if (width == 0 || height == 0) {
|
||||
return {width, height};
|
||||
}
|
||||
|
||||
uint32_t dimensionLimit = kMaxFramebufferDimension;
|
||||
if (adapterMaxDimension != 0 && adapterMaxDimension != std::numeric_limits<uint32_t>::max()) {
|
||||
dimensionLimit = std::min(dimensionLimit, adapterMaxDimension);
|
||||
}
|
||||
dimensionLimit = std::max(dimensionLimit, 1u);
|
||||
|
||||
double fitScale = 1.0;
|
||||
fitScale = std::min(fitScale, static_cast<double>(dimensionLimit) / static_cast<double>(width));
|
||||
fitScale = std::min(fitScale, static_cast<double>(dimensionLimit) / static_cast<double>(height));
|
||||
|
||||
const double requestedPixels = static_cast<double>(width) * static_cast<double>(height);
|
||||
if (requestedPixels > static_cast<double>(kMaxFramebufferPixels)) {
|
||||
fitScale = std::min(fitScale, std::sqrt(static_cast<double>(kMaxFramebufferPixels) / requestedPixels));
|
||||
}
|
||||
|
||||
if (fitScale >= 1.0) {
|
||||
return {width, height};
|
||||
}
|
||||
return {
|
||||
std::max(1u, static_cast<uint32_t>(std::floor(static_cast<double>(width) * fitScale))),
|
||||
std::max(1u, static_cast<uint32_t>(std::floor(static_cast<double>(height) * fitScale))),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace aurora::render_size_limits
|
||||
@@ -0,0 +1,50 @@
|
||||
#ifndef _DOLPHIN_AI_H_
|
||||
#define _DOLPHIN_AI_H_
|
||||
|
||||
#include <dolphin/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void (*AISCallback)(u32 count);
|
||||
typedef void (*AIDCallback)();
|
||||
|
||||
#define AI_STREAM_START 1
|
||||
#define AI_STREAM_STOP 0
|
||||
|
||||
#define AI_SAMPLERATE_32KHZ 0
|
||||
#define AI_SAMPLERATE_48KHZ 1
|
||||
|
||||
AIDCallback AIRegisterDMACallback(AIDCallback callback);
|
||||
void AIInitDMA(u32 start_addr, u32 length);
|
||||
BOOL AIGetDMAEnableFlag(void);
|
||||
void AIStartDMA(void);
|
||||
void AIStopDMA(void);
|
||||
u32 AIGetDMABytesLeft(void);
|
||||
u32 AIGetDMAStartAddr(void);
|
||||
u32 AIGetDMALength(void);
|
||||
BOOL AICheckInit(void);
|
||||
AISCallback AIRegisterStreamCallback(AISCallback callback);
|
||||
u32 AIGetStreamSampleCount(void);
|
||||
void AIResetStreamSampleCount(void);
|
||||
void AISetStreamTrigger(u32 trigger);
|
||||
u32 AIGetStreamTrigger(void);
|
||||
void AISetStreamPlayState(u32 state);
|
||||
u32 AIGetStreamPlayState(void);
|
||||
void AISetDSPSampleRate(u32 rate);
|
||||
u32 AIGetDSPSampleRate(void);
|
||||
void AISetStreamSampleRate(u32 rate);
|
||||
u32 AIGetStreamSampleRate(void);
|
||||
void AISetStreamVolLeft(u8 vol);
|
||||
u8 AIGetStreamVolLeft(void);
|
||||
void AISetStreamVolRight(u8 vol);
|
||||
u8 AIGetStreamVolRight(void);
|
||||
void AIInit(u8* stack);
|
||||
void AIReset(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,79 @@
|
||||
#ifndef _DOLPHIN_AR_H_
|
||||
#define _DOLPHIN_AR_H_
|
||||
|
||||
#include <dolphin/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void (*ARQCallback)(uintptr_t pointerToARQRequest);
|
||||
|
||||
struct ARQRequest {
|
||||
/* 0x00 */ struct ARQRequest *next;
|
||||
/* 0x04 */ u32 owner;
|
||||
/* 0x08 */ u32 type;
|
||||
/* 0x0C */ u32 priority;
|
||||
/* 0x10 */ u32 source;
|
||||
/* 0x14 */ u32 dest;
|
||||
/* 0x18 */ u32 length;
|
||||
/* 0x1C */ ARQCallback callback;
|
||||
};
|
||||
|
||||
#define ARQ_DMA_ALIGNMENT 32
|
||||
|
||||
#define ARAM_DIR_MRAM_TO_ARAM 0x00
|
||||
#define ARAM_DIR_ARAM_TO_MRAM 0x01
|
||||
|
||||
#define ARStartDMARead(mmem, aram, len) \
|
||||
ARStartDMA(ARAM_DIR_ARAM_TO_MRAM, mmem, aram, len)
|
||||
#define ARStartDMAWrite(mmem, aram, len) \
|
||||
ARStartDMA(ARAM_DIR_MRAM_TO_ARAM, mmem, aram, len)
|
||||
|
||||
typedef struct ARQRequest ARQRequest;
|
||||
|
||||
#define ARQ_TYPE_MRAM_TO_ARAM ARAM_DIR_MRAM_TO_ARAM
|
||||
#define ARQ_TYPE_ARAM_TO_MRAM ARAM_DIR_ARAM_TO_MRAM
|
||||
|
||||
#define ARQ_PRIORITY_LOW 0
|
||||
#define ARQ_PRIORITY_HIGH 1
|
||||
|
||||
// AR
|
||||
ARQCallback ARRegisterDMACallback(ARQCallback callback);
|
||||
u32 ARGetDMAStatus(void);
|
||||
void ARStartDMA(u32 type, u32 mainmem_addr, u32 aram_addr, u32 length);
|
||||
u32 ARAlloc(u32 length);
|
||||
u32 ARFree(u32* length);
|
||||
BOOL ARCheckInit(void);
|
||||
u32 ARInit(u32* stack_index_addr, u32 num_entries);
|
||||
void ARReset(void);
|
||||
void ARSetSize(void);
|
||||
u32 ARGetBaseAddress(void);
|
||||
u32 ARGetSize(void);
|
||||
u32 ARGetInternalSize(void);
|
||||
void ARClear(u32 flag);
|
||||
|
||||
/**
|
||||
* Aurora extension: gets the memory address where ARAM is stored, for direct access.
|
||||
*/
|
||||
void* ARGetStorageAddress();
|
||||
|
||||
// ARQ
|
||||
void ARQInit(void);
|
||||
void ARQReset(void);
|
||||
void ARQPostRequest(ARQRequest* request, u32 owner, u32 type, u32 priority, uintptr_t source, uintptr_t dest, u32 length, ARQCallback callback);
|
||||
void ARQRemoveRequest(ARQRequest* request);
|
||||
void ARQRemoveOwnerRequest(u32 owner);
|
||||
void ARQFlushQueue(void);
|
||||
void ARQSetChunkSize(u32 size);
|
||||
u32 ARQGetChunkSize(void);
|
||||
BOOL ARQCheckInit(void);
|
||||
|
||||
u16 __ARGetInterruptStatus(void);
|
||||
void __ARClearInterrupt(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,542 @@
|
||||
#ifndef _DOLPHIN_PPCARCH
|
||||
#define _DOLPHIN_PPCARCH
|
||||
|
||||
#include <dolphin/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define CTR 9
|
||||
#define XER 1
|
||||
#define LR 8
|
||||
|
||||
#define UPMC1 937
|
||||
#define UPMC2 938
|
||||
#define UPMC3 941
|
||||
#define UPMC4 942
|
||||
|
||||
#define USIA 939
|
||||
|
||||
#define UMMCR0 936
|
||||
#define UMMCR1 940
|
||||
|
||||
#define HID0 1008
|
||||
#define HID1 1009
|
||||
|
||||
#define PVR 287
|
||||
|
||||
#define IBAT0U 528
|
||||
#define IBAT0L 529
|
||||
#define IBAT1U 530
|
||||
#define IBAT1L 531
|
||||
#define IBAT2U 532
|
||||
#define IBAT2L 533
|
||||
#define IBAT3U 534
|
||||
#define IBAT3L 535
|
||||
|
||||
#define DBAT0U 536
|
||||
#define DBAT0L 537
|
||||
#define DBAT1U 538
|
||||
#define DBAT1L 539
|
||||
#define DBAT2U 540
|
||||
#define DBAT2L 541
|
||||
#define DBAT3U 542
|
||||
#define DBAT3L 543
|
||||
|
||||
#define SDR1 25
|
||||
|
||||
#define SPRG0 272
|
||||
#define SPRG1 273
|
||||
#define SPRG2 274
|
||||
#define SPRG3 275
|
||||
|
||||
#define DAR 19
|
||||
#define DSISR 18
|
||||
|
||||
#define SRR0 26
|
||||
#define SRR1 27
|
||||
|
||||
#define EAR 282
|
||||
|
||||
#define DABR 1013
|
||||
|
||||
#define TBL 284
|
||||
#define TBU 285
|
||||
|
||||
#define L2CR 1017
|
||||
|
||||
#define DEC 22
|
||||
|
||||
#define IABR 1010
|
||||
|
||||
#define PMC1 953
|
||||
#define PMC2 954
|
||||
#define PMC3 957
|
||||
#define PMC4 958
|
||||
|
||||
#define SIA 955
|
||||
|
||||
#define MMCR0 952
|
||||
#define MMCR1 956
|
||||
|
||||
#define THRM1 1020
|
||||
#define THRM2 1021
|
||||
#define THRM3 1022
|
||||
|
||||
#define ICTC 1019
|
||||
|
||||
#define GQR0 912
|
||||
#define GQR1 913
|
||||
#define GQR2 914
|
||||
#define GQR3 915
|
||||
#define GQR4 916
|
||||
#define GQR5 917
|
||||
#define GQR6 918
|
||||
#define GQR7 919
|
||||
|
||||
#define HID2 920
|
||||
|
||||
#define WPAR 921
|
||||
|
||||
#define DMA_U 922
|
||||
#define DMA_L 923
|
||||
|
||||
#define MSR_POW 0x00040000 // Power Management
|
||||
#define MSR_ILE 0x00010000 // Interrupt Little Endian
|
||||
#define MSR_EE 0x00008000 // external interrupt
|
||||
#define MSR_PR 0x00004000 // privilege level(should be 0)
|
||||
#define MSR_FP 0x00002000 // floating point available
|
||||
#define MSR_ME 0x00001000 // machine check enable
|
||||
#define MSR_FE0 0x00000800 // floating point exception enable
|
||||
#define MSR_SE 0x00000400 // single step trace enable
|
||||
#define MSR_BE 0x00000200 // branch trace enable
|
||||
#define MSR_FE1 0x00000100 // floating point exception enable
|
||||
#define MSR_IP 0x00000040 // Exception prefix
|
||||
#define MSR_IR 0x00000020 // instruction relocate
|
||||
#define MSR_DR 0x00000010 // data relocate
|
||||
#define MSR_PM 0x00000004 // Performance monitor marked mode
|
||||
#define MSR_RI 0x00000002 // Recoverable interrupt
|
||||
#define MSR_LE 0x00000001 // Little Endian
|
||||
|
||||
#define MSR_POW_BIT 13 // Power Management
|
||||
#define MSR_ILE_BIT 15 // Interrupt Little Endian
|
||||
#define MSR_EE_BIT 16 // external interrupt
|
||||
#define MSR_PR_BIT 17 // privilege level (should be 0)
|
||||
#define MSR_FP_BIT 18 // floating point available
|
||||
#define MSR_ME_BIT 19 // machine check enable
|
||||
#define MSR_FE0_BIT 20 // floating point exception enable
|
||||
#define MSR_SE_BIT 21 // single step trace enable
|
||||
#define MSR_BE_BIT 22 // branch trace enable
|
||||
#define MSR_FE1_BIT 23 // floating point exception enable
|
||||
#define MSR_IP_BIT 25 // Exception prefix
|
||||
#define MSR_IR_BIT 26 // instruction relocate
|
||||
#define MSR_DR_BIT 27 // data relocate
|
||||
#define MSR_PM_BIT 29 // Performance monitor marked mode
|
||||
#define MSR_RI_BIT 30 // Recoverable interrupt
|
||||
#define MSR_LE_BIT 31 // Little Endian
|
||||
|
||||
/*---------------------------------------------------------------------------*
|
||||
HID0 bits
|
||||
*---------------------------------------------------------------------------*/
|
||||
#define HID0_EMCP 0x80000000u // Enable MCP
|
||||
#define HID0_DBP 0x40000000u // Enable 60x bus address and data parity chk
|
||||
#define HID0_EBA 0x20000000u // Enable 60x address parity checking
|
||||
#define HID0_EBD 0x10000000u // Enable 60x data parity checking
|
||||
#define HID0_BCLK 0x08000000u // CLK_OUT output enable and clk selection
|
||||
#define HID0_ECLK 0x02000000u // CLK_OUT output enable and clk selection
|
||||
#define HID0_PAR 0x01000000u // Disable !ARTRY precharge
|
||||
#define HID0_DOZE 0x00800000u // Doze mode enable
|
||||
#define HID0_NAP 0x00400000u // Nap mode enable
|
||||
#define HID0_SLEEP 0x00200000u // Sleep mode enable
|
||||
#define HID0_DPM 0x00100000u // Dynamic power management enable
|
||||
#define HID0_NHR 0x00010000u // Not hard reset (0 hard reset if s/w set it)
|
||||
#define HID0_ICE 0x00008000u // Instruction cache enable
|
||||
#define HID0_DCE 0x00004000u // Data cache enable
|
||||
#define HID0_ILOCK 0x00002000u // ICache lock
|
||||
#define HID0_DLOCK 0x00001000u // DCache lock
|
||||
#define HID0_ICFI 0x00000800u // ICache flash invalidate
|
||||
#define HID0_DCFI 0x00000400u // DCache flash invalidate
|
||||
#define HID0_SPD 0x00000200u // Speculative cache access enable (0 enable)
|
||||
#define HID0_IFEM 0x00000100u // Enable M bit on bus for Ifetch
|
||||
#define HID0_SGE 0x00000080u // Store gathering enable
|
||||
#define HID0_DCFA 0x00000040u // DCache flush assist - set before a flush
|
||||
#define HID0_BTIC 0x00000020u // Branch target icache enable
|
||||
#define HID0_ABE 0x00000008u // Address bcast enable
|
||||
#define HID0_BHT 0x00000004u // Branch history table enable
|
||||
#define HID0_NOOPTI 0x00000001u // No-op Dcache touch instructions
|
||||
|
||||
#define HID0_ICE_BIT 16 // Instruction cache enable
|
||||
#define HID0_DCE_BIT 17 // Data cache enable
|
||||
#define HID0_ILOCK_BIT 18 // ICache lock
|
||||
#define HID0_DLOCK_BIT 19 // DCache lock
|
||||
|
||||
#define HID2_LSQE 0x80000000 // L/S quantize enable
|
||||
#define HID2_WPE 0x40000000 // Write pipe enable
|
||||
#define HID2_PSE 0x20000000 // Paired single enable
|
||||
#define HID2_LCE 0x10000000 // Locked cache enable
|
||||
|
||||
#define HID2_DCHERR 0x00800000 // ERROR: dcbz_l cache hit
|
||||
#define HID2_DNCERR 0x00400000 // ERROR: DMA access to normal cache
|
||||
#define HID2_DCMERR 0x00200000 // ERROR: DMA cache miss error
|
||||
#define HID2_DQOERR 0x00100000 // ERROR: DMA queue overflow
|
||||
#define HID2_DCHEE 0x00080000 // dcbz_l cache hit error enable
|
||||
#define HID2_DNCEE 0x00040000 // DMA access to normal cache error enable
|
||||
#define HID2_DCMEE 0x00020000 // DMA cache miss error error enable
|
||||
#define HID2_DQOEE 0x00010000 // DMA queue overflow error enable
|
||||
|
||||
#define HID2_DMAQL_MASK 0x0F000000 // DMA queue length mask
|
||||
#define HID2_DMAQL_SHIFT 24 // DMA queue shift
|
||||
|
||||
#define HID2_LSQE_BIT 0
|
||||
#define HID2_WPE_BIT 1
|
||||
#define HID2_PSE_BIT 2
|
||||
#define HID2_LCE_BIT 3
|
||||
|
||||
#define HID2_DCHERR_BIT 8
|
||||
#define HID2_DNCERR_BIT 9
|
||||
#define HID2_DCMERR_BIT 10
|
||||
#define HID2_DQOERR_BIT 11
|
||||
#define HID2_DCHEE_BIT 12
|
||||
#define HID2_DNCEE_BIT 13
|
||||
#define HID2_DCMEE_BIT 14
|
||||
#define HID2_DQOEE_BIT 15
|
||||
|
||||
#define GQR_LOAD_SCALE_MASK 0x3F000000 // load scale field
|
||||
#define GQR_LOAD_TYPE_MASK 0x00070000 // load type field
|
||||
#define GQR_STORE_SCALE_MASK 0x00003F00 // store scale field
|
||||
#define GQR_STORE_TYPE_MASK 0x00000007 // store type field
|
||||
|
||||
typedef struct
|
||||
{
|
||||
u32 _pad0 :2;
|
||||
u32 loadScale :6;
|
||||
u32 _pad1 :5;
|
||||
u32 loadType :3;
|
||||
u32 _pad2 :2;
|
||||
u32 storeScale :6;
|
||||
u32 _pad3 :5;
|
||||
u32 storeType :3;
|
||||
} PPC_GQR_t;
|
||||
|
||||
typedef union
|
||||
{
|
||||
u32 val;
|
||||
PPC_GQR_t f;
|
||||
} PPC_GQR_u;
|
||||
|
||||
|
||||
#define DMA_U_ADDR_MASK 0xFFFFFFE0 // Start addr in memory
|
||||
#define DMA_U_LEN_U_MASK 0x0000001F // lines to transfer (U)
|
||||
|
||||
#define DMA_L_LC_ADDR_MASK 0xFFFFFFE0 // Start addr in LC
|
||||
#define DMA_L_LOAD 0x00000010 // 0 - store, 1 - load
|
||||
#define DMA_L_STORE 0x00000000 // 0 - store, 1 - load
|
||||
#define DMA_L_LEN_MASK 0x0000000C // lines to transfer (L)
|
||||
#define DMA_L_TRIGGER 0x00000002 // 0 - cmd inactive, 1 - cmd rdy
|
||||
#define DMA_L_FLUSH 0x00000001 // 1 - Flush DMA queue
|
||||
|
||||
typedef struct
|
||||
{
|
||||
u32 memAddr :27;
|
||||
u32 dmaLenU :5;
|
||||
} PPC_DMA_U_t;
|
||||
|
||||
typedef union
|
||||
{
|
||||
u32 val;
|
||||
PPC_DMA_U_t f;
|
||||
} PPC_DMA_U_u;
|
||||
|
||||
|
||||
typedef struct
|
||||
{
|
||||
u32 lcAddr :27;
|
||||
u32 dmaLd :1;
|
||||
u32 dmaLenL :2;
|
||||
u32 dmaTrigger :1;
|
||||
u32 dmaFlush :1;
|
||||
} PPC_DMA_L_t;
|
||||
|
||||
|
||||
typedef union
|
||||
{
|
||||
u32 val;
|
||||
PPC_DMA_L_t f;
|
||||
} PPC_DMA_L_u;
|
||||
|
||||
|
||||
#define WPAR_ADDR 0xFFFFFFE0 // 32byte gather address
|
||||
#define WPAR_BNE 0x00000001 // Buffer not empty (R)
|
||||
|
||||
#define SRR1_DMA_BIT 0x00200000
|
||||
#define SRR1_L2DP_BIT 0x00100000
|
||||
|
||||
#define L2CR_L2E 0x80000000 // L2 Enable
|
||||
#define L2CR_L2PE 0x40000000 // L2 data parity generation and checking enable
|
||||
|
||||
#define L2CR_L2SIZ_256K 0x10000000 // L2 size 256K
|
||||
#define L2CR_L2SIZ_512K 0x20000000 // L2 size 512
|
||||
#define L2CR_L2SIZ_1M 0x30000000 // L2 size 1M
|
||||
|
||||
#define L2CR_L2CLK_1_0 0x02000000 // L2 clock ratio 1
|
||||
#define L2CR_L2CLK_1_5 0x04000000 // L2 clock ratio 1.5
|
||||
#define L2CR_L2CLK_2_0 0x08000000 // L2 clock ratio 2
|
||||
#define L2CR_L2CLK_2_5 0x0A000000 // L2 clock ratio 2.5
|
||||
#define L2CR_L2CLK_3_0 0x0C000000 // L2 clock ratio 3
|
||||
|
||||
#define L2CR_RAM_FLOW_THRU_BURST 0x00000000 // L2 RAM type flow-through sync. burst SRAM
|
||||
#define L2CR_RAM_PIPELINE_BURST 0x01000000 // L2 RAM type pipelined sync. burst SRAM
|
||||
#define L2CR_RAM_PIPELINE_LATE 0x01800000 // L2 RAM type pipelined sync. late-write SRAM
|
||||
|
||||
#define L2CR_L2DO 0x00400000 // Data only
|
||||
#define L2CR_L2I 0x00200000 // Global invalidate
|
||||
#define L2CR_L2CTL 0x00100000 // ZZ enable
|
||||
#define L2CR_L2WT 0x00080000 // L2 write through
|
||||
#define L2CR_L2TS 0x00040000 // L2 test support
|
||||
|
||||
#define L2CR_L2OH_0_5 0x00000000 // L2 output hold 0.5 ns
|
||||
#define L2CR_L2OH_1_0 0x00010000 // L2 output hold 1.0 ns
|
||||
|
||||
#define L2CR_L2SL 0x00008000 // L2 DLL slow
|
||||
#define L2CR_L2DF 0x00004000 // L2 differential clock
|
||||
#define L2CR_L2BYP 0x00002000 // L2 DLL bypass
|
||||
#define L2CR_L2CS 0x00000200 // L2 clock stop
|
||||
#define L2CR_L2DRO 0x00000100 // L2 DLL rollover checkstop enable
|
||||
#define L2CR_L2CTR_MASK 0x000000FE // L2 counter value mask
|
||||
#define L2CR_L2IP 0x00000001 // L2 global invalidate in progress
|
||||
|
||||
#define MMCR0_DIS 0x80000000 // Disables counting unconditionally
|
||||
#define MMCR0_DP 0x40000000 // Disables counting while in supervisor mode
|
||||
#define MMCR0_DU 0x20000000 // Disables counting while in user mode
|
||||
#define MMCR0_DMS 0x10000000 // Disables counting while MSR[PM] is set
|
||||
#define MMCR0_DMR 0x08000000 // Disables counting while MSR[PM] is zero
|
||||
#define MMCR0_ENINT 0x04000000 // Enables performance monitor interrupt signaling
|
||||
#define MMCR0_DISCOUNT 0x02000000 // Disables counting of PMCn when a performance monitor interrupt is signaled or...
|
||||
#define MMCR0_RTCSELECT_MASK 0x01800000 // 64-bit time base, bit selection enable
|
||||
#define MMCR0_RTCSELECT_63 0x00000000 // Pick bit 63 to count
|
||||
#define MMCR0_RTCSELECT_55 0x00800000 // Pick bit 55 to count
|
||||
#define MMCR0_RTCSELECT_51 0x01000000 // Pick bit 51 to count
|
||||
#define MMCR0_RTCSELECT_47 0x01800000 // Pick bit 47 to count
|
||||
#define MMCR0_INTONBITTRANS 0x00400000 // Causes interrupt signaling on bit transition from off to on
|
||||
#define MMCR0_THRESHOLD_MASK 0x003F0000 // Threshold value
|
||||
#define MMCR0_THRESHOLD(n) ((n) << 16) // Threshold value (0 - 63)
|
||||
#define MMCR0_PMC1INTCONTROL 0x00008000 // Enables interrupt signaling due to PMC1 counter overflow
|
||||
#define MMCR0_PMC2INTCONTROL 0x00004000 // Enables interrupt signaling due to PMC2-PMC4 counter overflow
|
||||
#define MMCR0_PMCTRIGGER 0x00002000 // Can be used to trigger counting of PMC2-PMC4 after PMC1 has overflowed or...
|
||||
#define MMCR0_PMC1SELECT_MASK 0x00001FC0 // PMC1 input selector
|
||||
#define MMCR0_PMC2SELECT_MASK 0x0000003F // PMC2 input selector
|
||||
|
||||
#define MMCR1_PMC3SELECT_MASK 0xF8000000 // PMC3 input selector
|
||||
#define MMCR1_PMC4SELECT_MASK 0x07C00000 // PMC4 input selector
|
||||
|
||||
#define PMC1_OV 0x80000000 // Overflow
|
||||
#define PMC1_COUNTER 0x7FFFFFFF // Counter value
|
||||
#define PMC2_OV 0x80000000 // Overflow
|
||||
#define PMC2_COUNTER 0x7FFFFFFF // Counter value
|
||||
#define PMC3_OV 0x80000000 // Overflow
|
||||
#define PMC3_COUNTER 0x7FFFFFFF // Counter value
|
||||
#define PMC4_OV 0x80000000 // Overflow
|
||||
#define PMC4_COUNTER 0x7FFFFFFF // Counter value
|
||||
|
||||
/*---------------------------------------------------------------------------*
|
||||
PMC1 Events
|
||||
*---------------------------------------------------------------------------*/
|
||||
#define MMCR0_PMC1_HOLD 0x00000000 // Register holds current value
|
||||
#define MMCR0_PMC1_CYCLE 0x00000040 // Processor cycles
|
||||
#define MMCR0_PMC1_INSTRUCTION 0x00000080 // # of instructions completed.
|
||||
#define MMCR0_PMC1_TRANSITION 0x000000C0 // # of transitions for 0 to 1
|
||||
#define MMCR0_PMC1_DISPATCHED 0x00000100 // # of instructions dispatched
|
||||
#define MMCR0_PMC1_EIEIO 0x00000140 // # of eieio instructions completed
|
||||
#define MMCR0_PMC1_ITLB_CYCLE 0x00000180 // # of cycles spent performing table search op. for the ITLB
|
||||
#define MMCR0_PMC1_L2_HIT 0x000001C0 // # of access that hit the L2.
|
||||
#define MMCR0_PMC1_EA 0x00000200 // # of valid instruction EAs delivered to the memory subsystem
|
||||
#define MMCR0_PMC1_IABR 0x00000240 // # of time the address of an instruction matches the IABR
|
||||
#define MMCR0_PMC1_L1_MISS 0x00000280 // # of loads that miss the L1
|
||||
#define MMCR0_PMC1_Bx_UNRESOLVED 0x000002C0 // # of branches that are unresolved when processed
|
||||
#define MMCR0_PMC1_Bx_STALL_CYCLE 0x00000300 // # of cycles that dispatcher stalls due to a second
|
||||
// unresolved branch in the instruction stream
|
||||
#define MMCR0_PMC1_IC_FETCH_MISS 0x00000340 // # of times an instruction fetch missed the L1 Icache
|
||||
#define MMCR0_PMC2_HOLD 0x00000000 // Register holds current value
|
||||
#define MMCR0_PMC2_CYCLE 0x00000001 // Processor cycles
|
||||
#define MMCR0_PMC2_INSTRUCTION 0x00000002 // # of instructions completed
|
||||
#define MMCR0_PMC2_TRANSITION 0x00000003 // # of time-base (lower) bit transitions
|
||||
#define MMCR0_PMC2_DISPATCHED 0x00000004 // # of instructions dispatched
|
||||
#define MMCR0_PMC2_IC_MISS 0x00000005 // # of L1 instruction cache misses
|
||||
#define MMCR0_PMC2_ITLB_MISS 0x00000006 // # of ITLB misses
|
||||
#define MMCR0_PMC2_L2_I_MISS 0x00000007 // # of L2 instruction misses
|
||||
#define MMCR0_PMC2_Bx_FALL_TROUGH 0x00000008 // # of fall-through branches
|
||||
#define MMCR0_PMC2_PR_SWITCH 0x00000009 // # of MSR[PR] bit toggles
|
||||
#define MMCR0_PMC2_RESERVED_LOAD 0x0000000A // # of reserved loads completed
|
||||
#define MMCR0_PMC2_LOAD_STORE 0x0000000B // # of completed loads and stores
|
||||
#define MMCR0_PMC2_SNOOP 0x0000000C // # of snoops
|
||||
#define MMCR0_PMC2_L1_CASTOUT 0x0000000D // # of L1 castouts to L2
|
||||
#define MMCR0_PMC2_SYSTEM 0x0000000E // # of completed system unit instructions
|
||||
#define MMCR0_PMC2_IC_FETCH_MISS 0x0000000F // # of instruction fetch misses in the L1
|
||||
#define MMCR0_PMC2_Bx_OUT_OF_ORDER 0x00000010 // # of branches allowing out-of-order execution
|
||||
|
||||
/*---------------------------------------------------------------------------*
|
||||
PMC3 Events
|
||||
*---------------------------------------------------------------------------*/
|
||||
#define MMCR1_PMC3_HOLD 0x00000000 // Register holds current value
|
||||
#define MMCR1_PMC3_CYCLE 0x08000000 // Processor cycles
|
||||
#define MMCR1_PMC3_INSTRUCTION 0x10000000 // # of instructions completed
|
||||
#define MMCR1_PMC3_TRANSITION 0x18000000 // # of time-base (lower) bit transitions
|
||||
#define MMCR1_PMC3_DISPATCHED 0x20000000 // # of instructions dispatched
|
||||
#define MMCR1_PMC3_DC_MISS 0x28000000 // # of L1 data cache misses
|
||||
#define MMCR1_PMC3_DTLB_MISS 0x30000000 // # of DTLB misses
|
||||
#define MMCR1_PMC3_L2_D_MISS 0x38000000 // # of L2 data misses
|
||||
#define MMCR1_PMC3_Bx_TAKEN 0x40000000 // # predicted branches that were taken
|
||||
#define MMCR1_PMC3_PM_SWITCH 0x48000000 // # of transitions between marked and unmarked processes
|
||||
#define MMCR1_PMC3_COND_STORE 0x50000000 // # of store conditional instructions completed
|
||||
#define MMCR1_PMC3_FPU 0x58000000 // # of instructions completed from the FPU
|
||||
#define MMCR1_PMC3_L2_SNOOP_CASTOUT 0x60000000 // # of L2 castout caused by snoops to modified lines
|
||||
#define MMCR1_PMC3_L2_HIT 0x68000000 // # of cache operations that hit in the L2 cache
|
||||
#define MMCR1_PMC3_L1_MISS_CYCLE 0x78000000 // # of cycles generated by L1 load misses
|
||||
#define MMCR1_PMC3_Bx_SECOND 0x80000000 // # of branches in the second speculative branch
|
||||
// resolved correctly
|
||||
#define MMCR1_PMC3_BPU_LR_CR 0x88000000 // # of cycles the BPU stalls due to LR or CR unresolved
|
||||
// dependencies
|
||||
|
||||
#define MMCR1_PMC4_HOLD 0x00000000 // Register holds current value
|
||||
#define MMCR1_PMC4_CYCLE 0x00400000 // Processor cycles
|
||||
#define MMCR1_PMC4_INSTRUCTION 0x00800000 // # of instructions completed
|
||||
#define MMCR1_PMC4_TRANSITION 0x00C00000 // # of time-base (lower) bit transitions
|
||||
#define MMCR1_PMC4_DISPATCHED 0x01000000 // # of instructions dispatched
|
||||
#define MMCR1_PMC4_L2_CASTOUT 0x01400000 // # of L2 castouts
|
||||
#define MMCR1_PMC4_DTLB_CYCLE 0x01800000 // # of cycles spent performing table searches for DTLB accesses
|
||||
#define MMCR1_PMC4_Bx_MISSED 0x02000000 // # of mispredicted branches
|
||||
#define MMCR1_PMC4_COND_STORE_INT 0x02800000 // # of store conditional instructions completed
|
||||
// with reservation intact
|
||||
#define MMCR1_PMC4_SYNC 0x02C00000 // # of completed sync instructions
|
||||
#define MMCR1_PMC4_SNOOP_RETRY 0x03000000 // # of snoop request retries
|
||||
#define MMCR1_PMC4_INTEGER 0x03400000 // # of completed integer operations
|
||||
#define MMCR1_PMC4_BPU_THIRD 0x03800000 // # of cycles the BPU cannot process new branches
|
||||
// due to having two unresolved branches
|
||||
#define MMCR1_PMC4_DC_MISS 0x07C00000 // # of L1 data cache misses
|
||||
|
||||
/*---------------------------------------------------------------------------*
|
||||
FPSCR bits
|
||||
*---------------------------------------------------------------------------*/
|
||||
#ifndef FPSCR_FX
|
||||
#define FPSCR_FX 0x80000000 // Exception summary
|
||||
#define FPSCR_FEX 0x40000000 // Enabled exception summary
|
||||
#define FPSCR_VX 0x20000000 // Invalid operation
|
||||
#define FPSCR_OX 0x10000000 // Overflow exception
|
||||
#define FPSCR_UX 0x08000000 // Underflow exception
|
||||
#define FPSCR_ZX 0x04000000 // Zero divide exception
|
||||
#define FPSCR_XX 0x02000000 // Inexact exception
|
||||
#define FPSCR_VXSNAN 0x01000000 // SNaN
|
||||
#define FPSCR_VXISI 0x00800000 // Infinity - Infinity
|
||||
#define FPSCR_VXIDI 0x00400000 // Infinity / Infinity
|
||||
#define FPSCR_VXZDZ 0x00200000 // 0 / 0
|
||||
#define FPSCR_VXIMZ 0x00100000 // Infinity * 0
|
||||
#define FPSCR_VXVC 0x00080000 // Invalid compare
|
||||
#define FPSCR_FR 0x00040000 // Fraction rounded
|
||||
#define FPSCR_FI 0x00020000 // Fraction inexact
|
||||
#define FPSCR_VXSOFT 0x00000400 // Software request
|
||||
#define FPSCR_VXSQRT 0x00000200 // Invalid square root
|
||||
#define FPSCR_VXCVI 0x00000100 // Invalid integer convert
|
||||
#define FPSCR_VE 0x00000080 // Invalid operation exception enable
|
||||
#define FPSCR_OE 0x00000040 // Overflow exception enable
|
||||
#define FPSCR_UE 0x00000020 // Underflow exception enable
|
||||
#define FPSCR_ZE 0x00000010 // Zero divide exception enable
|
||||
#define FPSCR_XE 0x00000008 // Inexact exception enable
|
||||
#define FPSCR_NI 0x00000004 // Non-IEEE mode
|
||||
#endif
|
||||
|
||||
#ifndef FPSCR_FX_BIT
|
||||
#define FPSCR_FX_BIT 0 // Exception summary
|
||||
#define FPSCR_FEX_BIT 1 // Enabled exception summary
|
||||
#define FPSCR_VX_BIT 2 // Invalid operation
|
||||
#define FPSCR_OX_BIT 3 // Overflow exception
|
||||
#define FPSCR_UX_BIT 4 // Underflow exception
|
||||
#define FPSCR_ZX_BIT 5 // Zero divide exception
|
||||
#define FPSCR_XX_BIT 6 // Inexact exception
|
||||
#define FPSCR_VXSNAN_BIT 7 // SNaN
|
||||
#define FPSCR_VXISI_BIT 8 // Infinity - Infinity
|
||||
#define FPSCR_VXIDI_BIT 9 // Infinity / Infinity
|
||||
#define FPSCR_VXZDZ_BIT 10 // 0 / 0
|
||||
#define FPSCR_VXIMZ_BIT 11 // Infinity * 0
|
||||
#define FPSCR_VXVC_BIT 12 // Invalid compare
|
||||
#define FPSCR_FR_BIT 13 // Fraction rounded
|
||||
#define FPSCR_FI_BIT 14 // Fraction inexact
|
||||
#define FPSCR_VXSOFT_BIT 21 // Software request
|
||||
#define FPSCR_VXSQRT_BIT 22 // Invalid square root
|
||||
#define FPSCR_VXCVI_BIT 23 // Invalid integer convert
|
||||
#define FPSCR_VE_BIT 24 // Invalid operation exception enable
|
||||
#define FPSCR_OE_BIT 25 // Overflow exception enable
|
||||
#define FPSCR_UE_BIT 26 // Underflow exception enable
|
||||
#define FPSCR_ZE_BIT 27 // Zero divide exception enable
|
||||
#define FPSCR_XE_BIT 28 // Inexact exception enable
|
||||
#define FPSCR_NI_BIT 29 // Non-IEEE mode
|
||||
#endif
|
||||
|
||||
union FpscrUnion {
|
||||
f64 f;
|
||||
struct {
|
||||
u32 fpscr_pad;
|
||||
u32 fpscr;
|
||||
} u;
|
||||
};
|
||||
|
||||
// PPCArch
|
||||
u32 PPCMfmsr();
|
||||
void PPCMtmsr(u32 newMSR);
|
||||
u32 PPCOrMsr(u32 value);
|
||||
u32 PPCMfhid0();
|
||||
void PPCMthid0(u32 newHID0);
|
||||
u32 PPCMfl2cr();
|
||||
void PPCMtl2cr(u32 newL2cr);
|
||||
void PPCMtdec(u32 newDec);
|
||||
void PPCSync();
|
||||
void PPCHalt();
|
||||
u32 PPCMffpscr();
|
||||
void PPCMtfpscr(u32 newFPSCR);
|
||||
u32 PPCMfhid2();
|
||||
void PPCMthid2(u32 newhid2);
|
||||
u32 PPCMfwpar();
|
||||
void PPCMtwpar(u32 newwpar);
|
||||
void PPCEnableSpeculation();
|
||||
void PPCDisableSpeculation();
|
||||
void PPCSetFpIEEEMode();
|
||||
void PPCSetFpNonIEEEMode();
|
||||
u32 PPCMfpmc4();
|
||||
u32 PPCMfpmc3();
|
||||
u32 PPCMfpmc1();
|
||||
void PPCMtpmc1(u32 newPmc1);
|
||||
void PPCMtpmc2(u32 newPmc1);
|
||||
void PPCMtpmc3(u32 newPmc1);
|
||||
void PPCMtpmc4(u32 newPmc1);
|
||||
void PPCMtmmcr0(u32 newMmcr0);
|
||||
void PPCMtmmcr1(u32 newMmcr0);
|
||||
void PPCMtdmaU(u32 newdmau);
|
||||
void PPCMtdmaL(u32 newdmal);
|
||||
u32 PPCMfdec(void);
|
||||
u32 PPCMfpmc2(void);
|
||||
u32 PPCAndMsr(u32 value);
|
||||
u32 PPCAndCMsr(u32 value);
|
||||
u32 PPCMfhid1();
|
||||
void PPCEieio();
|
||||
u32 PPCMfmmcr0();
|
||||
u32 PPCMfmmcr1();
|
||||
u32 PPCMfpmc2();
|
||||
u32 PPCMfsia();
|
||||
void PPCMtsia(u32 newSia);
|
||||
u32 PPCMfdmaL();
|
||||
u32 PPCMfpvr();
|
||||
u32 PPCMfdmaU();
|
||||
|
||||
// PPCPm
|
||||
void PMBegin(void);
|
||||
void PMEnd(void);
|
||||
void PMCycles(void);
|
||||
void PML1FetchMisses(void);
|
||||
void PML1MissCycles(void);
|
||||
void PMInstructions(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // _DOLPHIN_PPCARCH
|
||||
@@ -0,0 +1,340 @@
|
||||
#ifndef _DOLPHIN_CARD_H_
|
||||
#define _DOLPHIN_CARD_H_
|
||||
|
||||
#include <dolphin/os.h>
|
||||
#include <dolphin/dsp.h>
|
||||
#include <dolphin/dvd.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define CARD_FILENAME_MAX 32
|
||||
#define CARD_MAX_FILE 127
|
||||
#define CARD_ICON_MAX 8
|
||||
|
||||
#if TARGET_PC
|
||||
|
||||
typedef enum {
|
||||
CARD_RAWIMAGE,
|
||||
CARD_GCIFOLDER,
|
||||
} CARDFileType;
|
||||
|
||||
#endif
|
||||
|
||||
typedef void (*CARDCallback)(s32 chan, s32 result);
|
||||
|
||||
typedef struct CARDFileInfo {
|
||||
s32 chan;
|
||||
s32 fileNo;
|
||||
s32 offset;
|
||||
s32 length;
|
||||
u16 iBlock;
|
||||
} CARDFileInfo;
|
||||
|
||||
typedef struct CARDDir {
|
||||
u8 gameName[4];
|
||||
u8 company[2];
|
||||
u8 _padding0;
|
||||
u8 bannerFormat;
|
||||
u8 fileName[CARD_FILENAME_MAX];
|
||||
u32 time; // seconds since 01/01/2000 midnight
|
||||
u32 iconAddr; // 0xffffffff if not used
|
||||
u16 iconFormat;
|
||||
u16 iconSpeed;
|
||||
u8 permission;
|
||||
u8 copyTimes;
|
||||
u16 startBlock;
|
||||
u16 length;
|
||||
u8 _padding1[2];
|
||||
u32 commentAddr; // 0xffffffff if not used
|
||||
} CARDDir;
|
||||
|
||||
typedef struct CARDControl {
|
||||
/* 0x000 */ BOOL attached;
|
||||
/* 0x004 */ s32 result;
|
||||
/* 0x008 */ u16 size;
|
||||
/* 0x00A */ u16 pageSize;
|
||||
/* 0x00C */ s32 sectorSize;
|
||||
/* 0x010 */ u16 cBlock;
|
||||
/* 0x012 */ u16 vendorID;
|
||||
/* 0x014 */ s32 latency;
|
||||
/* 0x018 */ u8 id[12];
|
||||
/* 0x024 */ int mountStep;
|
||||
/* 0x028 */ int formatStep;
|
||||
/* 0x028 */ u32 scramble;
|
||||
/* 0x02C */ DSPTaskInfo task;
|
||||
/* 0x080 */ void* workArea;
|
||||
/* 0x084 */ CARDDir *currentDir;
|
||||
/* 0x088 */ u16* currentFat;
|
||||
/* 0x08C */ OSThreadQueue threadQueue;
|
||||
/* 0x094 */ u8 cmd[9];
|
||||
/* 0x0A0 */ s32 cmdlen;
|
||||
/* 0x0A4 */ volatile u32 mode;
|
||||
/* 0x0A8 */ int retry;
|
||||
/* 0x0AC */ int repeat;
|
||||
/* 0x0B0 */ u32 addr;
|
||||
/* 0x0B4 */ void* buffer;
|
||||
/* 0x0B8 */ s32 xferred;
|
||||
/* 0x0BC */ u16 freeNo;
|
||||
/* 0x0BE */ u16 startBlock;
|
||||
/* 0x0C0 */ CARDFileInfo* fileInfo;
|
||||
/* 0x0C4 */ CARDCallback extCallback;
|
||||
/* 0x0C8 */ CARDCallback txCallback;
|
||||
/* 0x0CC */ CARDCallback exiCallback;
|
||||
/* 0x0D0 */ CARDCallback apiCallback;
|
||||
/* 0x0D4 */ CARDCallback xferCallback;
|
||||
/* 0x0D8 */ CARDCallback eraseCallback;
|
||||
/* 0x0DC */ CARDCallback unlockCallback;
|
||||
/* 0x0E0 */ OSAlarm alarm;
|
||||
/* 0x108 */ u32 cid;
|
||||
/* 0x10C */ const DVDDiskID* diskID;
|
||||
} CARDControl;
|
||||
|
||||
typedef struct CARDDecParam {
|
||||
/* 0x00 */ u8* inputAddr;
|
||||
/* 0x04 */ u32 inputLength;
|
||||
/* 0x08 */ u32 aramAddr;
|
||||
/* 0x0C */ u8* outputAddr;
|
||||
} CARDDecParam;
|
||||
|
||||
typedef struct CARDID {
|
||||
/* 0x000 */ u8 serial[32];
|
||||
/* 0x020 */ u16 deviceID;
|
||||
/* 0x022 */ u16 size;
|
||||
/* 0x024 */ u16 encode;
|
||||
/* 0x026 */ u8 padding[470];
|
||||
/* 0x1FC */ u16 checkSum;
|
||||
/* 0x1FE */ u16 checkSumInv;
|
||||
} CARDID;
|
||||
|
||||
typedef struct CARDDirCheck {
|
||||
/* 0x00 */ u8 padding0[56];
|
||||
/* 0x38 */ u16 padding1;
|
||||
/* 0x3A */ s16 checkCode;
|
||||
/* 0x3C */ u16 checkSum;
|
||||
/* 0x3E */ u16 checkSumInv;
|
||||
} CARDDirCheck;
|
||||
|
||||
typedef struct CARDStat {
|
||||
/* 0x00 */ char fileName[CARD_FILENAME_MAX];
|
||||
/* 0x20 */ u32 length;
|
||||
/* 0x24 */ u32 time;
|
||||
/* 0x28 */ u8 gameName[4];
|
||||
/* 0x2C */ u8 company[2];
|
||||
/* 0x2E */ u8 bannerFormat;
|
||||
/* 0x30 */ u32 iconAddr;
|
||||
/* 0x34 */ u16 iconFormat;
|
||||
/* 0x36 */ u16 iconSpeed;
|
||||
/* 0x38 */ u32 commentAddr;
|
||||
/* 0x3C */ u32 offsetBanner;
|
||||
/* 0x40 */ u32 offsetBannerTlut;
|
||||
/* 0x44 */ u32 offsetIcon[CARD_ICON_MAX];
|
||||
/* 0x64 */ u32 offsetIconTlut;
|
||||
/* 0x68 */ u32 offsetData;
|
||||
} CARDStat;
|
||||
|
||||
#define CARD_ATTR_PUBLIC 0x04u
|
||||
#define CARD_ATTR_NO_COPY 0x08u
|
||||
#define CARD_ATTR_NO_MOVE 0x10u
|
||||
#define CARD_ATTR_GLOBAL 0x20u
|
||||
#define CARD_ATTR_COMPANY 0x40u
|
||||
|
||||
#define CARD_FAT_AVAIL 0x0000u
|
||||
#define CARD_FAT_CHECKSUM 0x0000u
|
||||
#define CARD_FAT_CHECKSUMINV 0x0001u
|
||||
#define CARD_FAT_CHECKCODE 0x0002u
|
||||
#define CARD_FAT_FREEBLOCKS 0x0003u
|
||||
#define CARD_FAT_LASTSLOT 0x0004u
|
||||
|
||||
#define CARD_WORKAREA_SIZE (5 * 8 * 1024)
|
||||
|
||||
#define CARD_SEG_SIZE 0x200u
|
||||
#define CARD_PAGE_SIZE 0x80u
|
||||
#define CARD_MAX_SIZE 0x01000000U
|
||||
|
||||
#define CARD_NUM_SYSTEM_BLOCK 5
|
||||
#define CARD_SYSTEM_BLOCK_SIZE (8 * 1024u)
|
||||
|
||||
#define CARD_MAX_MOUNT_STEP (CARD_NUM_SYSTEM_BLOCK + 2)
|
||||
|
||||
#define CARD_STAT_SPEED_END 0
|
||||
#define CARD_STAT_SPEED_FAST 1
|
||||
#define CARD_STAT_SPEED_MIDDLE 2
|
||||
#define CARD_STAT_SPEED_SLOW 3
|
||||
#define CARD_STAT_SPEED_MASK 3
|
||||
|
||||
#define CARD_STAT_ANIM_LOOP 0
|
||||
#define CARD_STAT_ANIM_BOUNCE 4
|
||||
#define CARD_STAT_ANIM_MASK 0x4
|
||||
|
||||
#define CARD_RESULT_UNLOCKED 1
|
||||
#define CARD_RESULT_READY 0
|
||||
#define CARD_RESULT_BUSY -1
|
||||
#define CARD_RESULT_WRONGDEVICE -2
|
||||
#define CARD_RESULT_NOCARD -3
|
||||
#define CARD_RESULT_NOFILE -4
|
||||
#define CARD_RESULT_IOERROR -5
|
||||
#define CARD_RESULT_BROKEN -6
|
||||
#define CARD_RESULT_EXIST -7
|
||||
#define CARD_RESULT_NOENT -8
|
||||
#define CARD_RESULT_INSSPACE -9
|
||||
#define CARD_RESULT_NOPERM -10
|
||||
#define CARD_RESULT_LIMIT -11
|
||||
#define CARD_RESULT_NAMETOOLONG -12
|
||||
#define CARD_RESULT_ENCODING -13
|
||||
#define CARD_RESULT_CANCELED -14
|
||||
#define CARD_RESULT_FATAL_ERROR -128
|
||||
|
||||
#define CARDIsValidBlockNo(card, blockNo) ((blockNo) >= CARD_NUM_SYSTEM_BLOCK && (blockNo) < (card)->cBlock)
|
||||
|
||||
#define CARD_READ_SIZE 512
|
||||
|
||||
#define CARD_COMMENT_SIZE 64
|
||||
|
||||
#define CARD_ICON_WIDTH 32
|
||||
#define CARD_ICON_HEIGHT 32
|
||||
|
||||
#define CARD_BANNER_WIDTH 96
|
||||
#define CARD_BANNER_HEIGHT 32
|
||||
|
||||
#define CARD_STAT_ICON_NONE 0
|
||||
#define CARD_STAT_ICON_C8 1
|
||||
#define CARD_STAT_ICON_RGB5A3 2
|
||||
#define CARD_STAT_ICON_MASK 3
|
||||
|
||||
#define CARD_STAT_BANNER_NONE 0
|
||||
#define CARD_STAT_BANNER_C8 1
|
||||
#define CARD_STAT_BANNER_RGB5A3 2
|
||||
#define CARD_STAT_BANNER_MASK 3
|
||||
|
||||
#define CARD_ENCODE_ANSI 0
|
||||
#define CARD_ENCODE_SJIS 1
|
||||
|
||||
#define CARDGetDirCheck(dir) ((CARDDirCheck*)&(dir)[CARD_MAX_FILE])
|
||||
#define CARDGetBannerFormat(stat) (((stat)->bannerFormat) & CARD_STAT_BANNER_MASK)
|
||||
#define CARDGetIconAnim(stat) (((stat)->bannerFormat) & CARD_STAT_ANIM_MASK)
|
||||
#define CARDGetIconFormat(stat, n) (((stat)->iconFormat >> (2 * (n))) & CARD_STAT_ICON_MASK)
|
||||
#define CARDGetIconSpeed(stat, n) (((stat)->iconSpeed >> (2 * (n))) & CARD_STAT_SPEED_MASK)
|
||||
#define CARDSetBannerFormat(stat, f) \
|
||||
((stat)->bannerFormat = (u8)(((stat)->bannerFormat & ~CARD_STAT_BANNER_MASK) | (f)))
|
||||
#define CARDSetIconAnim(stat, f) \
|
||||
((stat)->bannerFormat = (u8)(((stat)->bannerFormat & ~CARD_STAT_ANIM_MASK) | (f)))
|
||||
#define CARDSetIconFormat(stat, n, f) \
|
||||
((stat)->iconFormat = \
|
||||
(u16)(((stat)->iconFormat & ~(CARD_STAT_ICON_MASK << (2 * (n)))) | ((f) << (2 * (n)))))
|
||||
#define CARDSetIconSpeed(stat, n, f) \
|
||||
((stat)->iconSpeed = \
|
||||
(u16)(((stat)->iconSpeed & ~(CARD_STAT_SPEED_MASK << (2 * (n)))) | ((f) << (2 * (n)))))
|
||||
#define CARDSetIconAddress(stat, addr) ((stat)->iconAddr = (u32)(addr))
|
||||
#define CARDSetCommentAddress(stat, addr) ((stat)->commentAddr = (u32)(addr))
|
||||
#define CARDGetFileNo(fileInfo) ((fileInfo)->fileNo)
|
||||
|
||||
extern u32 __CARDFreq;
|
||||
|
||||
#if DEBUG
|
||||
#define CARDFreq __CARDFreq
|
||||
#else
|
||||
#define CARDFreq EXI_FREQ_16M
|
||||
#endif
|
||||
|
||||
s32 CARDRenameAsync(s32 chan, const char* oldName, const char* newName, CARDCallback callback);
|
||||
|
||||
// CARDBios
|
||||
#if TARGET_PC
|
||||
void CARDInit(const char* game, const char* maker);
|
||||
void CARDSetGameAndMaker(const s32 chan, const char* game, const char* maker);
|
||||
|
||||
// pass -1 to set both
|
||||
void CARDDetectDolphin(s32 chan);
|
||||
// pass -1 to set both
|
||||
void CARDSetBasePath(const char*, s32 chan);
|
||||
void CARDSetLoadType(CARDFileType type);
|
||||
|
||||
#else
|
||||
void CARDInit(void);
|
||||
#endif
|
||||
s32 CARDGetResultCode(s32 chan);
|
||||
s32 CARDFreeBlocks(s32 chan, s32* byteNotUsed, s32* filesNotUsed);
|
||||
s32 CARDGetEncoding(s32 chan, u16* encode);
|
||||
s32 CARDGetMemSize(s32 chan, u16* size);
|
||||
s32 CARDGetSectorSize(s32 chan, u32* size);
|
||||
const DVDDiskID* CARDGetDiskID(s32 chan);
|
||||
s32 CARDSetDiskID(s32 chan, const DVDDiskID* diskID);
|
||||
BOOL CARDSetFastMode(BOOL enable);
|
||||
BOOL CARDGetFastMode(void);
|
||||
s32 CARDGetCurrentMode(s32 chan, u32* mode);
|
||||
|
||||
// CARDCheck
|
||||
s32 CARDCheckExAsync(s32 chan, s32* xferBytes, CARDCallback callback);
|
||||
s32 CARDCheckAsync(s32 chan, CARDCallback callback);
|
||||
s32 CARDCheckEx(s32 chan, s32* xferBytes);
|
||||
s32 CARDCheck(s32 chan);
|
||||
|
||||
// CARDCreate
|
||||
s32 CARDCreateAsync(s32 chan, const char* fileName, u32 size, CARDFileInfo* fileInfo, CARDCallback callback);
|
||||
s32 CARDCreate(s32 chan, const char* fileName, u32 size, CARDFileInfo* fileInfo);
|
||||
|
||||
// CARDDelete
|
||||
s32 CARDFastDeleteAsync(s32 chan, s32 fileNo, CARDCallback callback);
|
||||
s32 CARDFastDelete(s32 chan, s32 fileNo);
|
||||
s32 CARDDeleteAsync(s32 chan, const char* fileName, CARDCallback callback);
|
||||
s32 CARDDelete(s32 chan, const char* fileName);
|
||||
|
||||
// CARDErase
|
||||
s32 CARDEraseAsync(CARDFileInfo* fileInfo, s32 length, s32 offset, CARDCallback callback);
|
||||
s32 CARDErase(CARDFileInfo* fileInfo, s32 length, s32 offset);
|
||||
|
||||
// CARDFormat
|
||||
s32 CARDFormat(s32 chan);
|
||||
|
||||
// CARDMount
|
||||
int CARDProbe(s32 chan);
|
||||
s32 CARDProbeEx(s32 chan, s32* memSize, s32* sectorSize);
|
||||
s32 CARDMountAsync(s32 chan, void* workArea, CARDCallback detachCallback, CARDCallback attachCallback);
|
||||
s32 CARDMount(s32 chan, void* workArea, CARDCallback detachCallback);
|
||||
s32 CARDUnmount(s32 chan);
|
||||
|
||||
// CARDNet
|
||||
u16 CARDSetVendorID(u16 vendorID);
|
||||
u16 CARDGetVendorID();
|
||||
s32 CARDGetSerialNo(s32 chan, u64* serialNo);
|
||||
s32 CARDGetUniqueCode(s32 chan, u64* uniqueCode);
|
||||
s32 CARDGetAttributes(s32 chan, s32 fileNo, u8* attr);
|
||||
s32 CARDSetAttributesAsync(s32 chan, s32 fileNo, u8 attr, CARDCallback callback);
|
||||
s32 CARDSetAttributes(s32 chan, s32 fileNo, u8 attr);
|
||||
|
||||
// CARDOpen
|
||||
s32 CARDFastOpen(s32 chan, s32 fileNo, CARDFileInfo* fileInfo);
|
||||
s32 CARDOpen(s32 chan, const char* fileName, CARDFileInfo* fileInfo);
|
||||
s32 CARDClose(CARDFileInfo* fileInfo);
|
||||
|
||||
// CARDProgram
|
||||
s32 CARDProgramAsync(CARDFileInfo* fileInfo, void* buf, s32 length, s32 offset, CARDCallback callback);
|
||||
s32 CARDProgram(CARDFileInfo* fileInfo, void* buf, s32 length, s32 offset);
|
||||
|
||||
// CARDRdwr
|
||||
s32 CARDGetXferredBytes(s32 chan);
|
||||
|
||||
// CARDRead
|
||||
s32 CARDReadAsync(const CARDFileInfo* fileInfo, void* addr, s32 length, s32 offset, CARDCallback callback);
|
||||
s32 CARDRead(const CARDFileInfo* fileInfo, void* addr, s32 length, s32 offset);
|
||||
s32 CARDCancel(CARDFileInfo* fileInfo);
|
||||
|
||||
// CARDRename
|
||||
s32 CARDRename(s32 chan, const char* oldName, const char* newName);
|
||||
|
||||
// CARDStat
|
||||
s32 CARDGetStatus(s32 chan, s32 fileNo, CARDStat* stat);
|
||||
s32 CARDSetStatusAsync(s32 chan, s32 fileNo, const CARDStat* stat, CARDCallback callback);
|
||||
s32 CARDSetStatus(s32 chan, s32 fileNo, const CARDStat* stat);
|
||||
|
||||
// CARDWrite
|
||||
s32 CARDWriteAsync(const CARDFileInfo* fileInfo, const void* addr, s32 length, s32 offset, CARDCallback callback);
|
||||
s32 CARDWrite(const CARDFileInfo* fileInfo, const void* addr, s32 length, s32 offset);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef _DOLPHIN_DB_H_
|
||||
#define _DOLPHIN_DB_H_
|
||||
|
||||
#include <dolphin/types.h>
|
||||
#include <dolphin/db/DBInterface.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define OS_DBINTERFACE_ADDR 0x00000040
|
||||
|
||||
BOOL DBIsDebuggerPresent(void);
|
||||
void DBPrintf(char* str, ...);
|
||||
s32 DBQueryData(void);
|
||||
u32 DBRead(u8*, u32);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // _DOLPHIN_DB_H_
|
||||
@@ -0,0 +1,31 @@
|
||||
#ifndef _DOLPHIN_DBINTERFACE_H_
|
||||
#define _DOLPHIN_DBINTERFACE_H_
|
||||
|
||||
#include <dolphin/os.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct DBInterface {
|
||||
u32 bPresent;
|
||||
u32 exceptionMask;
|
||||
void (*ExceptionDestination)(void);
|
||||
void* exceptionReturn;
|
||||
} DBInterface;
|
||||
|
||||
extern DBInterface* __DBInterface;
|
||||
|
||||
void DBInit(void);
|
||||
void DBInitComm(int* inputFlagPtr, int* mtrCallback);
|
||||
void __DBExceptionDestination(void);
|
||||
void __DBExceptionDestinationAux(void);
|
||||
BOOL __DBIsExceptionMarked(__OSException exception);
|
||||
void __DBMarkException(u8 exception, int value);
|
||||
void __DBSetPresent(u32 value);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef _DOLPHIN_H_
|
||||
#define _DOLPHIN_H_
|
||||
|
||||
#include <dolphin/types.h>
|
||||
#include <dolphin/os.h>
|
||||
#include <dolphin/dsp.h>
|
||||
#include <dolphin/dvd.h>
|
||||
#include <dolphin/gx.h>
|
||||
#include <dolphin/hio.h>
|
||||
#include <dolphin/mtx.h>
|
||||
#include <dolphin/vi.h>
|
||||
#include <dolphin/card.h>
|
||||
#include <dolphin/perf.h>
|
||||
#include <dolphin/ar.h>
|
||||
#include <dolphin/base/PPCArch.h>
|
||||
#include <dolphin/db.h>
|
||||
#include <dolphin/pad.h>
|
||||
#include <dolphin/dtk.h>
|
||||
#include <dolphin/ai.h>
|
||||
// #include <dolphin/demo.h>
|
||||
#include <dolphin/exi.h>
|
||||
#include <dolphin/si.h>
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,57 @@
|
||||
#ifndef _DOLPHIN_DSP_H_
|
||||
#define _DOLPHIN_DSP_H_
|
||||
|
||||
#include <dolphin/os.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void (*DSPCallback)(void* task);
|
||||
typedef struct STRUCT_DSP_TASK DSPTaskInfo;
|
||||
|
||||
typedef struct STRUCT_DSP_TASK {
|
||||
/* 0x00 */ volatile u32 state;
|
||||
/* 0x04 */ volatile u32 priority;
|
||||
/* 0x08 */ volatile u32 flags;
|
||||
/* 0x0C */ u16* iram_mmem_addr;
|
||||
/* 0x10 */ u32 iram_length;
|
||||
/* 0x14 */ u32 iram_addr;
|
||||
/* 0x18 */ u16* dram_mmem_addr;
|
||||
/* 0x1C */ u32 dram_length;
|
||||
/* 0x20 */ u32 dram_addr;
|
||||
/* 0x24 */ u16 dsp_init_vector;
|
||||
/* 0x26 */ u16 dsp_resume_vector;
|
||||
/* 0x28 */ DSPCallback init_cb;
|
||||
/* 0x2C */ DSPCallback res_cb;
|
||||
/* 0x30 */ DSPCallback done_cb;
|
||||
/* 0x34 */ DSPCallback req_cb;
|
||||
/* 0x38 */ DSPTaskInfo* next;
|
||||
/* 0x3C */ DSPTaskInfo* prev;
|
||||
/* 0x40 */ OSTime t_context;
|
||||
/* 0x48 */ OSTime t_task;
|
||||
} DSPTaskInfo;
|
||||
|
||||
u32 DSPCheckMailToDSP(void);
|
||||
u32 DSPCheckMailFromDSP(void);
|
||||
u32 DSPReadCPUToDSPMbox(void);
|
||||
u32 DSPReadMailFromDSP(void);
|
||||
void DSPSendMailToDSP(u32 mail);
|
||||
void DSPAssertInt(void);
|
||||
void DSPInit(void);
|
||||
BOOL DSPCheckInit(void);
|
||||
void DSPReset(void);
|
||||
void DSPHalt(void);
|
||||
void DSPUnhalt(void);
|
||||
u32 DSPGetDMAStatus(void);
|
||||
DECL_WEAK DSPTaskInfo* DSPAddTask(DSPTaskInfo* task);
|
||||
DSPTaskInfo* DSPCancelTask(DSPTaskInfo* task);
|
||||
DSPTaskInfo* DSPAssertTask(DSPTaskInfo* task);
|
||||
|
||||
DSPTaskInfo* __DSPGetCurrentTask(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,56 @@
|
||||
#ifndef _DOLPHIN_DTK_H_
|
||||
#define _DOLPHIN_DTK_H_
|
||||
|
||||
#include <dolphin/dvd.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void (*DTKCallback)(u32 eventMask);
|
||||
typedef void (*DTKFlushCallback)(void);
|
||||
|
||||
typedef struct DTKTrack {
|
||||
/* 0x00 */ struct DTKTrack* prev;
|
||||
/* 0x04 */ struct DTKTrack* next;
|
||||
/* 0x08 */ char* fileName;
|
||||
/* 0x0C */ u32 eventMask;
|
||||
/* 0x10 */ DTKCallback callback;
|
||||
/* 0x14 */ DVDFileInfo dvdFileInfo;
|
||||
} DTKTrack;
|
||||
|
||||
#define DTK_STATE_STOP 0
|
||||
#define DTK_STATE_RUN 1
|
||||
#define DTK_STATE_PAUSE 2
|
||||
#define DTK_STATE_BUSY 3
|
||||
#define DTK_STATE_PREPARE 4
|
||||
|
||||
#define DTK_MODE_NOREPEAT 0
|
||||
#define DTK_MODE_ALLREPEAT 1
|
||||
#define DTK_MODE_REPEAT1 2
|
||||
|
||||
void DTKInit(void);
|
||||
void DTKShutdown(void);
|
||||
u32 DTKQueueTrack(char* fileName, DTKTrack* track, u32 eventMask, DTKCallback callback);
|
||||
u32 DTKRemoveTrack(DTKTrack* track);
|
||||
int DTKFlushTracks(DTKFlushCallback callback);
|
||||
void DTKSetSampleRate(u32 samplerate);
|
||||
void DTKSetInterruptFrequency(u32 samples);
|
||||
void DTKSetRepeatMode(u32 repeat);
|
||||
int DTKSetState(u32 state);
|
||||
int DTKNextTrack(void);
|
||||
int DTKPrevTrack(void);
|
||||
u32 DTKGetSampleRate(void);
|
||||
u32 DTKGetRepeatMode(void);
|
||||
u32 DTKGetState(void);
|
||||
u32 DTKGetPosition(void);
|
||||
u32 DTKGetInterruptFrequency(void);
|
||||
DTKTrack* DTKGetCurrentTrack(void);
|
||||
void DTKSetVolume(u8 left, u8 right);
|
||||
u16 DTKGetVolume(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,237 @@
|
||||
#ifndef _DOLPHIN_DVD_H_
|
||||
#define _DOLPHIN_DVD_H_
|
||||
|
||||
#include <dolphin/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define DVD_ASSERTMSGLINE(line, cond, msg) \
|
||||
if (!(cond)) \
|
||||
OSPanic(__FILE__, line, msg)
|
||||
|
||||
#define DVD_ASSERTMSG1LINE(line, cond, msg, arg1) \
|
||||
if (!(cond)) \
|
||||
OSPanic(__FILE__, line, msg, arg1)
|
||||
|
||||
#define DVD_ASSERTMSG2LINE(line, cond, msg, arg1, arg2) \
|
||||
if (!(cond)) \
|
||||
OSPanic(__FILE__, line, msg, arg1, arg2)
|
||||
|
||||
#define DVD_RESULT_GOOD 0
|
||||
#define DVD_RESULT_FATAL_ERROR -1
|
||||
#define DVD_RESULT_IGNORED -2
|
||||
#define DVD_RESULT_CANCELED -6
|
||||
|
||||
#define DVD_STATE_FATAL_ERROR -1
|
||||
#define DVD_STATE_END 0
|
||||
#define DVD_STATE_BUSY 1
|
||||
#define DVD_STATE_WAITING 2
|
||||
#define DVD_STATE_COVER_CLOSED 3
|
||||
#define DVD_STATE_NO_DISK 4
|
||||
#define DVD_STATE_COVER_OPEN 5
|
||||
#define DVD_STATE_WRONG_DISK 6
|
||||
#define DVD_STATE_MOTOR_STOPPED 7
|
||||
#define DVD_STATE_PAUSING 8
|
||||
#define DVD_STATE_IGNORED 9
|
||||
#define DVD_STATE_CANCELED 10
|
||||
#define DVD_STATE_RETRY 11
|
||||
|
||||
#define DVD_MIN_TRANSFER_SIZE 32
|
||||
|
||||
// could be bitfields
|
||||
#define DVD_INTTYPE_TC 1
|
||||
#define DVD_INTTYPE_DE 2
|
||||
// unk type 3
|
||||
#define DVD_INTTYPE_CVR 4
|
||||
|
||||
// DVD Commands
|
||||
#define DVD_COMMAND_NONE 0
|
||||
#define DVD_COMMAND_READ 1
|
||||
#define DVD_COMMAND_SEEK 2
|
||||
#define DVD_COMMAND_CHANGE_DISK 3
|
||||
#define DVD_COMMAND_BSREAD 4
|
||||
#define DVD_COMMAND_READID 5
|
||||
#define DVD_COMMAND_INITSTREAM 6
|
||||
#define DVD_COMMAND_CANCELSTREAM 7
|
||||
#define DVD_COMMAND_STOP_STREAM_AT_END 8
|
||||
#define DVD_COMMAND_REQUEST_AUDIO_ERROR 9
|
||||
#define DVD_COMMAND_REQUEST_PLAY_ADDR 10
|
||||
#define DVD_COMMAND_REQUEST_START_ADDR 11
|
||||
#define DVD_COMMAND_REQUEST_LENGTH 12
|
||||
#define DVD_COMMAND_AUDIO_BUFFER_CONFIG 13
|
||||
#define DVD_COMMAND_INQUIRY 14
|
||||
#define DVD_COMMAND_BS_CHANGE_DISK 15
|
||||
#define DVD_COMMAND_UNK_16 16
|
||||
|
||||
typedef struct DVDDiskID {
|
||||
char gameName[4];
|
||||
char company[2];
|
||||
u8 diskNumber;
|
||||
u8 gameVersion;
|
||||
u8 streaming;
|
||||
u8 streamingBufSize;
|
||||
u8 padding[22];
|
||||
} DVDDiskID;
|
||||
|
||||
typedef struct DVDCommandBlock DVDCommandBlock;
|
||||
typedef void (*DVDCBCallback)(s32 result, DVDCommandBlock* block);
|
||||
typedef void (*DVDLowCallback)(u32);
|
||||
|
||||
typedef void (*DVDCommandCheckerCallback)(u32);
|
||||
typedef void (*DVDCommandChecker)(DVDCommandBlock*, DVDCommandCheckerCallback);
|
||||
|
||||
struct DVDCommandBlock {
|
||||
/* 0x00 */ DVDCommandBlock* next;
|
||||
/* 0x04 */ DVDCommandBlock* prev;
|
||||
/* 0x08 */ u32 command;
|
||||
/* 0x0C */ s32 state;
|
||||
/* 0x10 */ u32 offset;
|
||||
/* 0x14 */ u32 length;
|
||||
/* 0x18 */ void* addr;
|
||||
/* 0x1C */ u32 currTransferSize;
|
||||
/* 0x20 */ u32 transferredSize;
|
||||
/* 0x24 */ DVDDiskID* id;
|
||||
/* 0x28 */ DVDCBCallback callback;
|
||||
/* 0x2C */ void* userData;
|
||||
};
|
||||
|
||||
typedef struct DVDFileInfo DVDFileInfo;
|
||||
typedef void (*DVDCallback)(s32 result, DVDFileInfo *fileInfo);
|
||||
struct DVDFileInfo {
|
||||
/* 0x00 */ DVDCommandBlock cb;
|
||||
/* 0x30 */ u32 startAddr;
|
||||
/* 0x34 */ u32 length;
|
||||
/* 0x38 */ DVDCallback callback;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
u32 entryNum;
|
||||
u32 location;
|
||||
u32 next;
|
||||
} DVDDir;
|
||||
|
||||
typedef struct {
|
||||
u32 entryNum;
|
||||
BOOL isDir;
|
||||
char* name;
|
||||
} DVDDirEntry;
|
||||
|
||||
typedef struct DVDBB2 {
|
||||
/* 0x00 */ u32 bootFilePosition;
|
||||
/* 0x04 */ u32 FSTPosition;
|
||||
/* 0x08 */ u32 FSTLength;
|
||||
/* 0x0C */ u32 FSTMaxLength;
|
||||
/* 0x10 */ void* FSTAddress;
|
||||
/* 0x14 */ u32 userPosition;
|
||||
/* 0x18 */ u32 userLength;
|
||||
/* 0x1C */ u32 padding0;
|
||||
} DVDBB2;
|
||||
|
||||
typedef struct DVDDriveInfo {
|
||||
/* 0x00 */ u16 revisionLevel;
|
||||
/* 0x02 */ u16 deviceCode;
|
||||
/* 0x04 */ u32 releaseDate;
|
||||
/* 0x08 */ u8 padding[24];
|
||||
} DVDDriveInfo;
|
||||
|
||||
// DVD
|
||||
void DVDInit(void);
|
||||
int DVDReadAbsAsyncPrio(DVDCommandBlock* block, void* addr, s32 length, s32 offset, DVDCBCallback callback, s32 prio);
|
||||
int DVDSeekAbsAsyncPrio(DVDCommandBlock* block, s32 offset, DVDCBCallback callback, s32 prio);
|
||||
int DVDReadAbsAsyncForBS(DVDCommandBlock* block, void* addr, s32 length, s32 offset, DVDCBCallback callback);
|
||||
int DVDReadDiskID(DVDCommandBlock* block, DVDDiskID* diskID, DVDCBCallback callback);
|
||||
int DVDPrepareStreamAbsAsync(DVDCommandBlock* block, u32 length, u32 offset, DVDCBCallback callback);
|
||||
int DVDCancelStreamAsync(DVDCommandBlock* block, DVDCBCallback callback);
|
||||
s32 DVDCancelStream(DVDCommandBlock* block);
|
||||
int DVDStopStreamAtEndAsync(DVDCommandBlock* block, DVDCBCallback callback);
|
||||
s32 DVDStopStreamAtEnd(DVDCommandBlock* block);
|
||||
int DVDGetStreamErrorStatusAsync(DVDCommandBlock* block, DVDCBCallback callback);
|
||||
s32 DVDGetStreamErrorStatus(DVDCommandBlock* block);
|
||||
int DVDGetStreamPlayAddrAsync(DVDCommandBlock* block, DVDCBCallback callback);
|
||||
s32 DVDGetStreamPlayAddr(DVDCommandBlock* block);
|
||||
int DVDGetStreamStartAddrAsync(DVDCommandBlock* block, DVDCBCallback callback);
|
||||
s32 DVDGetStreamStartAddr(DVDCommandBlock* block);
|
||||
int DVDGetStreamLengthAsync(DVDCommandBlock* block, DVDCBCallback callback);
|
||||
s32 DVDGetStreamLength(DVDCommandBlock* block);
|
||||
int DVDChangeDiskAsyncForBS(DVDCommandBlock* block, DVDCBCallback callback);
|
||||
int DVDChangeDiskAsync(DVDCommandBlock* block, DVDDiskID* id, DVDCBCallback callback);
|
||||
s32 DVDChangeDisk(DVDCommandBlock* block, DVDDiskID* id);
|
||||
int DVDStopMotorAsync(DVDCommandBlock* block, DVDCBCallback callback);
|
||||
s32 DVDStopMotor(DVDCommandBlock* block);
|
||||
int DVDInquiryAsync(DVDCommandBlock* block, DVDDriveInfo* info, DVDCBCallback callback);
|
||||
s32 DVDInquiry(DVDCommandBlock* block, DVDDriveInfo* info);
|
||||
void DVDReset(void);
|
||||
int DVDResetRequired(void);
|
||||
s32 DVDGetCommandBlockStatus(const DVDCommandBlock* block);
|
||||
s32 DVDGetDriveStatus(void);
|
||||
BOOL DVDSetAutoInvalidation(BOOL autoInval);
|
||||
void DVDPause(void);
|
||||
void DVDResume(void);
|
||||
int DVDCancelAsync(DVDCommandBlock* block, DVDCBCallback callback);
|
||||
s32 DVDCancel(volatile DVDCommandBlock* block);
|
||||
int DVDCancelAllAsync(DVDCBCallback callback);
|
||||
s32 DVDCancelAll(void);
|
||||
DVDDiskID* DVDGetCurrentDiskID(void);
|
||||
BOOL DVDCheckDisk(void);
|
||||
|
||||
// DVD FATAL
|
||||
int DVDSetAutoFatalMessaging(BOOL enable);
|
||||
|
||||
// DVD FS
|
||||
s32 DVDConvertPathToEntrynum(const char* pathPtr);
|
||||
BOOL DVDFastOpen(s32 entrynum, DVDFileInfo* fileInfo);
|
||||
BOOL DVDOpen(const char* fileName, DVDFileInfo* fileInfo);
|
||||
BOOL DVDClose(DVDFileInfo* fileInfo);
|
||||
BOOL DVDGetCurrentDir(char* path, u32 maxlen);
|
||||
BOOL DVDChangeDir(const char* dirName);
|
||||
BOOL DVDReadAsyncPrio(DVDFileInfo* fileInfo, void* addr, s32 length, s32 offset,
|
||||
DVDCallback callback, s32 prio);
|
||||
s32 DVDReadPrio(DVDFileInfo* fileInfo, void* addr, s32 length, s32 offset, s32 prio);
|
||||
int DVDSeekAsyncPrio(DVDFileInfo* fileInfo, s32 offset, void (* callback)(s32, DVDFileInfo *), s32 prio);
|
||||
s32 DVDSeekPrio(DVDFileInfo* fileInfo, s32 offset, s32 prio);
|
||||
s32 DVDGetFileInfoStatus(const DVDFileInfo* fileInfo);
|
||||
BOOL DVDFastOpenDir(s32 entrynum, DVDDir* dir);
|
||||
int DVDOpenDir(const char* dirName, DVDDir* dir);
|
||||
int DVDReadDir(DVDDir* dir, DVDDirEntry* dirent);
|
||||
int DVDCloseDir(DVDDir* dir);
|
||||
void DVDRewindDir(DVDDir* dir);
|
||||
void* DVDGetFSTLocation(void);
|
||||
const u8* DVDGetDOLLocation(s32* out_size);
|
||||
BOOL DVDPrepareStreamAsync(DVDFileInfo* fileInfo, u32 length, u32 offset, DVDCallback callback);
|
||||
s32 DVDPrepareStream(DVDFileInfo* fileInfo, u32 length, u32 offset);
|
||||
s32 DVDGetTransferredSize(DVDFileInfo* fileinfo);
|
||||
|
||||
#define DVDReadAsync(fileInfo, addr, length, offset, callback) \
|
||||
DVDReadAsyncPrio((fileInfo), (addr), (length), (offset), (callback), 2)
|
||||
|
||||
// DVD ID UTILS
|
||||
int DVDCompareDiskID(const DVDDiskID* id1, const DVDDiskID* id2);
|
||||
DVDDiskID* DVDGenerateDiskID(DVDDiskID* id, const char* game, const char* company, u8 diskNum, u8 version);
|
||||
|
||||
// DVD LOW
|
||||
BOOL DVDLowRead(void* addr, u32 length, u32 offset, DVDLowCallback callback);
|
||||
BOOL DVDLowSeek(u32 offset, DVDLowCallback callback);
|
||||
BOOL DVDLowWaitCoverClose(DVDLowCallback callback);
|
||||
BOOL DVDLowReadDiskID(DVDDiskID* diskID, DVDLowCallback callback);
|
||||
BOOL DVDLowStopMotor(DVDLowCallback callback);
|
||||
BOOL DVDLowRequestError(DVDLowCallback callback);
|
||||
BOOL DVDLowInquiry(DVDDriveInfo* info, DVDLowCallback callback);
|
||||
BOOL DVDLowAudioStream(u32 subcmd, u32 length, u32 offset, DVDLowCallback callback);
|
||||
BOOL DVDLowRequestAudioStatus(u32 subcmd, DVDLowCallback callback);
|
||||
BOOL DVDLowAudioBufferConfig(BOOL enable, u32 size, DVDLowCallback callback);
|
||||
void DVDLowReset(void);
|
||||
DVDLowCallback DVDLowSetResetCoverCallback(DVDLowCallback callback);
|
||||
BOOL DVDLowBreak(void);
|
||||
DVDLowCallback DVDLowClearCallback(void);
|
||||
u32 DVDLowGetCoverStatus(void);
|
||||
|
||||
// DVD QUEUE
|
||||
void DVDDumpWaitingQueue(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,103 @@
|
||||
#ifndef _DOLPHIN_EXI_H_
|
||||
#define _DOLPHIN_EXI_H_
|
||||
|
||||
#include <dolphin/os.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define EXI_MEMORY_CARD_59 0x00000004
|
||||
#define EXI_MEMORY_CARD_123 0x00000008
|
||||
#define EXI_MEMORY_CARD_251 0x00000010
|
||||
#define EXI_MEMORY_CARD_507 0x00000020
|
||||
|
||||
#define EXI_MEMORY_CARD_1019 0x00000040
|
||||
#define EXI_MEMORY_CARD_2043 0x00000080
|
||||
|
||||
#define EXI_MEMORY_CARD_1019A 0x00000140
|
||||
#define EXI_MEMORY_CARD_1019B 0x00000240
|
||||
#define EXI_MEMORY_CARD_1019C 0x00000340
|
||||
#define EXI_MEMORY_CARD_1019D 0x00000440
|
||||
#define EXI_MEMORY_CARD_1019E 0x00000540
|
||||
#define EXI_MEMORY_CARD_1019F 0x00000640
|
||||
#define EXI_MEMORY_CARD_1019G 0x00000740
|
||||
|
||||
#define EXI_MEMORY_CARD_2043A 0x00000180
|
||||
#define EXI_MEMORY_CARD_2043B 0x00000280
|
||||
#define EXI_MEMORY_CARD_2043C 0x00000380
|
||||
#define EXI_MEMORY_CARD_2043D 0x00000480
|
||||
#define EXI_MEMORY_CARD_2043E 0x00000580
|
||||
#define EXI_MEMORY_CARD_2043F 0x00000680
|
||||
#define EXI_MEMORY_CARD_2043G 0x00000780
|
||||
|
||||
#define EXI_USB_ADAPTER 0x01010000
|
||||
#define EXI_NPDP_GDEV 0x01020000
|
||||
|
||||
#define EXI_MODEM 0x02020000
|
||||
#define EXI_ETHER 0x04020200
|
||||
#define EXI_MIC 0x04060000
|
||||
#define EXI_AD16 0x04120000
|
||||
#define EXI_RS232C 0x04040404
|
||||
#define EXI_ETHER_VIEWER 0x04220001
|
||||
#define EXI_STREAM_HANGER 0x04130000
|
||||
|
||||
#define EXI_MARLIN 0x03010000
|
||||
|
||||
#define EXI_IS_VIEWER 0x05070000
|
||||
|
||||
#define EXI_READ 0
|
||||
#define EXI_WRITE 1
|
||||
|
||||
#define EXI_FREQ_1M 0
|
||||
#define EXI_FREQ_2M 1
|
||||
#define EXI_FREQ_4M 2
|
||||
#define EXI_FREQ_8M 3
|
||||
#define EXI_FREQ_16M 4
|
||||
#define EXI_FREQ_32M 5
|
||||
|
||||
typedef void (*EXICallback)(s32 chan, OSContext* context);
|
||||
typedef struct EXIControl {
|
||||
EXICallback exiCallback;
|
||||
EXICallback tcCallback;
|
||||
EXICallback extCallback;
|
||||
volatile u32 state;
|
||||
int immLen;
|
||||
u8* immBuf;
|
||||
u32 dev;
|
||||
u32 id;
|
||||
s32 idTime;
|
||||
int items;
|
||||
struct {
|
||||
u32 dev;
|
||||
EXICallback callback;
|
||||
} queue[3];
|
||||
} EXIControl;
|
||||
|
||||
EXICallback EXISetExiCallback(s32 channel, EXICallback callback);
|
||||
|
||||
void EXIInit(void);
|
||||
BOOL EXILock(s32 channel, u32 device, EXICallback callback);
|
||||
BOOL EXIUnlock(s32 channel);
|
||||
BOOL EXISelect(s32 channel, u32 device, u32 frequency);
|
||||
BOOL EXIDeselect(s32 channel);
|
||||
BOOL EXIImm(s32 channel, void* buffer, s32 length, u32 type, EXICallback callback);
|
||||
BOOL EXIImmEx(s32 channel, void* buffer, s32 length, u32 type);
|
||||
BOOL EXIDma(s32 channel, void* buffer, s32 length, u32 type, EXICallback callback);
|
||||
BOOL EXISync(s32 channel);
|
||||
BOOL EXIProbe(s32 channel);
|
||||
s32 EXIProbeEx(s32 channel);
|
||||
BOOL EXIAttach(s32 channel, EXICallback callback);
|
||||
BOOL EXIDetach(s32 channel);
|
||||
u32 EXIGetState(s32 channel);
|
||||
s32 EXIGetID(s32 channel, u32 device, u32* id);
|
||||
void EXIProbeReset(void);
|
||||
int EXISelectSD(s32 chan, u32 dev, u32 freq);
|
||||
s32 EXIGetType(s32 chan, u32 dev, u32* type);
|
||||
char* EXIGetTypeString(u32 type);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user