This commit is contained in:
patchzyy
2026-08-23 17:10:50 +02:00
commit ec226e8348
1099 changed files with 400981 additions and 0 deletions
+5
View File
@@ -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
+97
View File
@@ -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
+91
View File
@@ -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"
+79
View File
@@ -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.
+8
View File
@@ -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.
+36
View File
@@ -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
View File
@@ -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
+58
View File
@@ -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.
+674
View File
@@ -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>.
+4
View File
@@ -0,0 +1,4 @@
/artifacts/
/dist/
**/bin/
**/obj/
+302
View File
@@ -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))
+460
View File
@@ -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 }
}
+264
View File
@@ -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
}
+412
View File
@@ -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)
+74
View File
@@ -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"
+30
View File
@@ -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.'
+69
View File
@@ -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."
+43
View File
@@ -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."
+133
View File
@@ -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();
}
}
+191
View File
@@ -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:&lt;id&gt;</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
}
}
+591
View File
@@ -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>&lt;portable root&gt;\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");
}
}
+27
View File
@@ -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);
}
}
+172
View File
@@ -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();
}
}
+143
View File
@@ -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>
/// &lt;root&gt;\portable.txt marker; its contents are irrelevant
/// &lt;root&gt;\Install\ the installation directory
/// &lt;root&gt;\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>&lt;root&gt;\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"));
}
}
+136
View File
@@ -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 -&gt; toolkit-state.json -&gt; 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;
}
}
}
+21
View File
@@ -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>
+198
View File
@@ -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.
+195
View File
@@ -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.
+29
View File
@@ -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
+10
View File
@@ -0,0 +1,10 @@
.buildcache/
.DS_Store
.idea/
.cache/
.vscode/
.vs/
build/
cmake-build-*/
CMakeUserPresets.json
out/
+83
View File
@@ -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 ()
+21
View File
@@ -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.
+46
View File
@@ -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/).
![Screenshot](assets/screenshot.png)
### 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()
+255
View File
@@ -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 ()
+150
View File
@@ -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 ()
+149
View File
@@ -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 ()
+17
View File
@@ -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)
+64
View File
@@ -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 ()
+10
View File
@@ -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)
+16
View File
@@ -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)
+47
View File
@@ -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)
+6
View File
@@ -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})
+7
View File
@@ -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)
+12
View File
@@ -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)
+14
View File
@@ -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)
+7
View File
@@ -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)
+7
View File
@@ -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)
+7
View File
@@ -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)
+55
View File
@@ -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
+4
View File
@@ -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 ()
+90
View File
@@ -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;
}
+310
View File
@@ -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 ()
+172
View File
@@ -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
+27
View File
@@ -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
+44
View File
@@ -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
+97
View File
@@ -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
+20
View File
@@ -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
+13
View File
@@ -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);
+16
View 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
+254
View File
@@ -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
+50
View File
@@ -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
+79
View File
@@ -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
+542
View File
@@ -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
+340
View File
@@ -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
+22
View File
@@ -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
+24
View File
@@ -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
+57
View File
@@ -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
+56
View File
@@ -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
+237
View File
@@ -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
+103
View File
@@ -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